Browse Source

Attempt to implement pluggable interface

Add support for plugins
feature/transactions
TheoryOfNekomata 3 years ago
parent
commit
b244429f08
29 changed files with 557 additions and 238 deletions
  1. +7
    -2
      migrate.ts
  2. +5
    -5
      package.json
  3. +12
    -10
      src/models/Folder.ts
  4. +13
    -11
      src/models/Note.ts
  5. +9
    -7
      src/models/Operation.ts
  6. +9
    -7
      src/models/Tag.ts
  7. +12
    -10
      src/models/Transaction.ts
  8. +34
    -3
      src/pages/api/folders.ts
  9. +41
    -3
      src/pages/api/folders/[id].ts
  10. +34
    -3
      src/pages/api/notes.ts
  11. +41
    -3
      src/pages/api/notes/[id].ts
  12. +23
    -0
      src/plugins/local-storage/Engine.ts
  13. +55
    -0
      src/plugins/local-storage/Storage.ts
  14. +18
    -0
      src/plugins/local-storage/index.ts
  15. +70
    -0
      src/plugins/remote-storage/Engine.ts
  16. +27
    -0
      src/plugins/remote-storage/Storage.ts
  17. +18
    -0
      src/plugins/remote-storage/index.ts
  18. +9
    -6
      src/seeds.ts
  19. +0
    -74
      src/services/Controller.ts
  20. +2
    -2
      src/services/Storage.ts
  21. +19
    -11
      src/services/entities/Folder.ts
  22. +5
    -5
      src/services/entities/Note.ts
  23. +12
    -13
      src/services/storages/Storage.ts
  24. +60
    -0
      src/utilities/ColumnTypes.ts
  25. +0
    -36
      src/utilities/Instance.ts
  26. +8
    -3
      src/utilities/ORM.ts
  27. +5
    -5
      src/utilities/Response.ts
  28. +0
    -19
      yarn.lock
  29. +9
    -0
      zeichen.config.ts

+ 7
- 2
migrate.ts View File

@@ -1,13 +1,18 @@
import models from './src/models'
import seeds from './src/seeds'

// TODO support NoSQL

export const up = async queryInterface => {
const createTablePromises = models.map(m => queryInterface.createTable(m.tableName, m.rawAttributes))
const createTablePromises = models.map(m => queryInterface.createTable(m.tableName, m.attributes))
await Promise.all(createTablePromises)

const seedTablePromise = models
.filter(m => Boolean(seeds[m.modelName]))
.map(m => queryInterface.bulkInsert(m.tableName, seeds[m.modelName]))
.map(m => {
console.log(JSON.stringify(seeds[m.modelName]))
return queryInterface.bulkInsert(m.tableName, seeds[m.tableName])
})
return Promise.all(seedTablePromise)
}



+ 5
- 5
package.json View File

@@ -6,7 +6,8 @@
"dev": "next dev",
"build": "next build",
"start": "next start",
"migrate": "tsc migrate.ts --module commonjs --esModuleInterop --outDir database/migrations && sequelize-cli db:migrate"
"migrate": "tsc migrate.ts --module commonjs --esModuleInterop --outDir database/migrations && sequelize-cli db:migrate",
"migrate:run": "sequelize-cli db:migrate"
},
"dependencies": {
"@fingerprintjs/fingerprintjs": "^3.0.3",
@@ -18,9 +19,6 @@
"react-feather": "^2.0.8",
"react-mobiledoc-editor": "^0.10.0",
"sequelize": "5.22.0",
"sequelize-cli": "^6.2.0",
"sequelize-typescript": "^1.1.0",
"sqlite3": "^5.0.0",
"styled-components": "^5.2.0",
"uuid": "^8.3.1"
},
@@ -29,7 +27,9 @@
"@types/react": "^16.9.53",
"@types/styled-components": "^5.1.4",
"@types/uuid": "^8.3.0",
"typescript": "^4.0.3"
"typescript": "^4.0.3",
"sequelize-cli": "^6.2.0",
"sqlite3": "^5.0.0"
},
"optionalDependencies": {
"sqlite3": "^5.0.0"


+ 12
- 10
src/models/Folder.ts View File

@@ -1,8 +1,8 @@
import { UUIDV4, STRING, DATE, } from 'sequelize'
import * as ColumnTypes from '../utilities/ColumnTypes'

export default {
tableName: 'folders',
const Folder: ColumnTypes.Model = {
modelName: 'Folder',
tableName: 'folders',
options: {
timestamps: true,
paranoid: true,
@@ -10,31 +10,33 @@ export default {
updatedAt: 'updatedAt',
deletedAt: 'deletedAt',
},
rawAttributes: {
attributes: {
id: {
allowNull: true,
primaryKey: true,
type: UUIDV4,
type: ColumnTypes.UUIDV4,
},
name: {
allowNull: false,
type: STRING,
type: ColumnTypes.STRING,
},
parentId: {
allowNull: true,
type: UUIDV4,
type: ColumnTypes.UUIDV4,
},
createdAt: {
allowNull: false,
type: DATE,
type: ColumnTypes.DATE,
},
updatedAt: {
allowNull: false,
type: DATE,
type: ColumnTypes.DATE,
},
deletedAt: {
allowNull: true,
type: DATE,
type: ColumnTypes.DATE,
},
}
}

export default Folder

+ 13
- 11
src/models/Note.ts View File

@@ -1,8 +1,8 @@
import { UUIDV4, STRING, TEXT, DATE, } from 'sequelize'
import * as ColumnTypes from '../utilities/ColumnTypes'

export default {
tableName: 'notes',
const Note: ColumnTypes.Model = {
modelName: 'Note',
tableName: 'notes',
options: {
timestamps: true,
paranoid: true,
@@ -10,35 +10,37 @@ export default {
updatedAt: 'updatedAt',
deletedAt: 'deletedAt',
},
rawAttributes: {
attributes: {
id: {
allowNull: true,
primaryKey: true,
type: UUIDV4,
type: ColumnTypes.UUIDV4,
},
title: {
allowNull: false,
type: STRING,
type: ColumnTypes.STRING,
},
content: {
allowNull: true,
type: TEXT({ length: 'long', }),
type: ColumnTypes.TEXT({ length: 'long', }),
},
folderId: {
allowNull: true,
type: UUIDV4,
type: ColumnTypes.UUIDV4,
},
createdAt: {
allowNull: false,
type: DATE,
type: ColumnTypes.DATE,
},
updatedAt: {
allowNull: false,
type: DATE,
type: ColumnTypes.DATE,
},
deletedAt: {
allowNull: true,
type: DATE,
type: ColumnTypes.DATE,
},
},
}

export default Note

+ 9
- 7
src/models/Operation.ts View File

@@ -1,20 +1,22 @@
import { INTEGER, STRING, } from 'sequelize'
import * as ColumnTypes from '../utilities/ColumnTypes'

export default {
const Operation: ColumnTypes.Model = {
modelName: 'Operation',
tableName: 'operations',
options: {
timestamps: false,
},
modelName: 'Operation',
tableName: 'operations',
rawAttributes: {
attributes: {
id: {
allowNull: true,
primaryKey: true,
type: INTEGER,
type: ColumnTypes.INTEGER,
},
name: {
allowNull: false,
type: STRING,
type: ColumnTypes.STRING,
},
}
}

export default Operation

+ 9
- 7
src/models/Tag.ts View File

@@ -1,20 +1,22 @@
import { UUIDV4, STRING, } from 'sequelize'
import * as ColumnTypes from '../utilities/ColumnTypes'

export default {
const Tag: ColumnTypes.Model = {
modelName: 'Tag',
tableName: 'tags',
options: {
timestamps: false,
},
modelName: 'Tag',
tableName: 'tags',
rawAttributes: {
attributes: {
id: {
allowNull: true,
primaryKey: true,
type: UUIDV4,
type: ColumnTypes.UUIDV4,
},
name: {
allowNull: false,
type: STRING,
type: ColumnTypes.STRING,
},
}
}

export default Tag

+ 12
- 10
src/models/Transaction.ts View File

@@ -1,32 +1,34 @@
import { UUIDV4, STRING, DATE, INTEGER, } from 'sequelize'
import * as ColumnTypes from '../utilities/ColumnTypes'

export default {
const Transaction: ColumnTypes.Model = {
modelName: 'Transaction',
tableName: 'transactions',
options: {
timestamps: false,
},
modelName: 'Transaction',
tableName: 'transactions',
rawAttributes: {
attributes: {
id: {
allowNull: true,
primaryKey: true,
type: UUIDV4,
type: ColumnTypes.UUIDV4,
},
deviceId: {
allowNull: false,
type: STRING,
type: ColumnTypes.STRING,
},
operation: {
allowNull: false,
type: INTEGER,
type: ColumnTypes.INTEGER,
},
objectId: {
allowNull: false,
type: STRING,
type: ColumnTypes.STRING,
},
performedAt: {
allowNull: false,
type: DATE,
type: ColumnTypes.DATE,
},
},
}

export default Transaction

+ 34
- 3
src/pages/api/folders.ts View File

@@ -1,5 +1,36 @@
import ORM, { DatabaseKind } from '../../utilities/ORM'
import * as Service from '../../services/entities/Folder'
import Model from '../../models/Folder'
import * as Service from '../../services/Folder'
import { collection } from '../../services/Controller'

export default collection(Model, Service)
export default async (req, res) => {
const orm = new ORM({
kind: process.env.DATABASE_DIALECT as DatabaseKind,
url: process.env.DATABASE_URL,
})
const repository = orm.getRepository(Model)
const methodHandlers = {
'GET': Service.getMultiple(repository),
}

const { [req.method as keyof typeof methodHandlers]: handler = null } = methodHandlers
if (handler === null) {
res.statusCode = 415
res.json({ message: 'Method not allowed.' })
return
}

try {
const { status, data, } = await handler(req.query)
res.statusCode = status
res.json(data)
} catch (err) {
console.error(err)
const { status, data, } = err
res.statusCode = status
if (data && status !== 204) {
res.json(data)
return
}
res.end()
}
}

+ 41
- 3
src/pages/api/folders/[id].ts View File

@@ -1,5 +1,43 @@
import ORM, { DatabaseKind } from '../../../utilities/ORM'
import * as Service from '../../../services/entities/Folder'
import Model from '../../../models/Folder'
import * as Service from '../../../services/Folder'
import { item } from '../../../services/Controller'

export default item(Model, Service)
export default async (req, res) => {
const orm = new ORM({
kind: process.env.DATABASE_DIALECT as DatabaseKind,
url: process.env.DATABASE_URL,
})
const repository = orm.getRepository(Model)
const methodHandlers = {
'GET': Service.getSingle(repository),
'PUT': Service.save(repository)(req.body),
'DELETE': Service.remove(repository)
}

const { [req.method as keyof typeof methodHandlers]: handler = null } = methodHandlers
if (handler === null) {
res.statusCode = 415
res.json({ message: 'Method not allowed.' })
return
}

const { id } = req.query
try {
const { status, ...etcReturn } = await handler(id)
res.statusCode = status
if (etcReturn['data']) {
res.json(etcReturn['data'])
return
}
res.end()
} catch (err) {
console.error(err)
const { status, data, } = err
res.statusCode = status || 500
if (data && status !== 204) {
res.json(data)
return
}
res.end()
}
}

+ 34
- 3
src/pages/api/notes.ts View File

@@ -1,5 +1,36 @@
import ORM, { DatabaseKind } from '../../utilities/ORM'
import * as Service from '../../services/entities/Note'
import Model from '../../models/Note'
import * as Service from '../../services/Note'
import { collection } from '../../services/Controller'

export default collection(Model, Service)
export default async (req, res) => {
const orm = new ORM({
kind: process.env.DATABASE_DIALECT as DatabaseKind,
url: process.env.DATABASE_URL,
})
const repository = orm.getRepository(Model)
const methodHandlers = {
'GET': Service.getMultiple(repository),
}

const { [req.method as keyof typeof methodHandlers]: handler = null } = methodHandlers
if (handler === null) {
res.statusCode = 415
res.json({ message: 'Method not allowed.' })
return
}

try {
const { status, data, } = await handler(req.query)
res.statusCode = status
res.json(data)
} catch (err) {
console.error(err)
const { status, data, } = err
res.statusCode = status
if (data && status !== 204) {
res.json(data)
return
}
res.end()
}
}

+ 41
- 3
src/pages/api/notes/[id].ts View File

@@ -1,5 +1,43 @@
import ORM, { DatabaseKind } from '../../../utilities/ORM'
import * as Service from '../../../services/entities/Note'
import Model from '../../../models/Note'
import * as Service from '../../../services/Note'
import { item } from '../../../services/Controller'

export default item(Model, Service)
export default async (req, res) => {
const orm = new ORM({
kind: process.env.DATABASE_DIALECT as DatabaseKind,
url: process.env.DATABASE_URL,
})
const repository = orm.getRepository(Model)
const methodHandlers = {
'GET': Service.getSingle(repository),
'PUT': Service.save(repository)(req.body),
'DELETE': Service.remove(repository)
}

const { [req.method as keyof typeof methodHandlers]: handler = null } = methodHandlers
if (handler === null) {
res.statusCode = 415
res.json({ message: 'Method not allowed.' })
return
}

const { id } = req.query
try {
const { status, ...etcReturn } = await handler(id)
res.statusCode = status
if (etcReturn['data']) {
res.json(etcReturn['data'])
return
}
res.end()
} catch (err) {
console.error(err)
const { status, data, } = err
res.statusCode = status || 500
if (data && status !== 204) {
res.json(data)
return
}
res.end()
}
}

+ 23
- 0
src/plugins/local-storage/Engine.ts View File

@@ -0,0 +1,23 @@
export default class LocalStorage<T> {
constructor(
private readonly source: Storage,
private readonly serializer: (t: T) => string = JSON.stringify,
private readonly deserializer: (s: string) => T = JSON.parse,
) {}

getCollection(collectionId: string) {
const raw = this.source.getItem(collectionId)
if (raw !== null) {
return this.deserializer(raw)
}
return null
}

replaceCollection(collectionId: string, collectionData: T) {
this.source.setItem(collectionId, this.serializer(collectionData))
}

removeCollection(collectionId: string) {
this.source.removeItem(collectionId)
}
}

+ 55
- 0
src/plugins/local-storage/Storage.ts View File

@@ -0,0 +1,55 @@
import Storage, { StorageMeta, OutOfSyncError } from '../../services/storages/Storage'
import Engine from './Engine'

export default class LocalStorage<T> implements Storage<T> {
private readonly engine: Engine<StorageMeta<T>>

constructor(
private readonly ownerId: string,
private readonly storageId: string,
private readonly getItemId = item => item['id'],
) {
this.engine = new Engine<StorageMeta<T>>(window.localStorage)
}

private getMeta() {
const oldMeta = this.engine.getCollection(this.storageId)
if (oldMeta === null) {
throw new OutOfSyncError()
}
return oldMeta
}

private existenceCheck(newItem: T) {
return oldItem => this.getItemId(oldItem) === this.getItemId(newItem)
}

async queryItems() {
return this.getMeta().items
}

async saveItem(newItem: T) {
const oldMeta = this.getMeta()
const isExistingItem = oldMeta.items.some(this.existenceCheck(newItem))
const newMeta: StorageMeta<T> = {
items: isExistingItem
? oldMeta.items.map(oldItem => this.existenceCheck(newItem)(oldItem) ? newItem : oldItem)
: [...oldMeta.items, newItem],
lastModifiedBy: this.ownerId,
lastModifiedAt: new Date(),
}
this.engine.replaceCollection(this.storageId, newMeta)
}

async deleteItem(newItem: T) {
const oldMeta = this.getMeta()
const newItems = oldMeta.items.filter(oldItem => !this.existenceCheck(newItem)(oldItem))
const newMeta: StorageMeta<T> = {
items: newItems,
lastModifiedBy: this.ownerId,
lastModifiedAt: new Date(),
}
this.engine.replaceCollection(this.storageId, newMeta)
return oldMeta.items.length !== newItems.length
}
}

+ 18
- 0
src/plugins/local-storage/index.ts View File

@@ -0,0 +1,18 @@
import Storage from './Storage'

type PluginConfig = {

}

type State = {
currentUserId: string,
}

type Plugin = (config: PluginConfig) => (state: State) => void

const LocalStoragePlugin: Plugin = config => ({
currentUserId,
}) => {
new Storage(currentUserId, 'notes')
new Storage(currentUserId, 'folders')
}

+ 70
- 0
src/plugins/remote-storage/Engine.ts View File

@@ -0,0 +1,70 @@
import { Deserializer, Serializer } from '../../services/storages/Storage'

const CREATED = 201
const NO_CONTENT = 204
const NOT_FOUND = 404
const GONE = 410

export default class RemoteStorage<U, T> {
constructor(
private readonly baseUrl: string,
private readonly getItemId = item => item['id'],
private readonly serializers: Map<string, Serializer> = new Map([
['*/*', JSON.stringify],
['application/json', JSON.stringify],
['text/json', JSON.stringify],
]),
private readonly deserializers: Map<string, Deserializer> = new Map([
['*/*', JSON.parse],
['application/json', JSON.parse],
['text/json', JSON.parse],
]),
) {}

async getCollection(collectionId: string) {
const response = await window.fetch([this.baseUrl, collectionId].join('/'))
const contentType = response.headers.get('content-type')
const { [contentType]: deserializer = this.deserializers.get('*/*'), } = Object.fromEntries(this.deserializers.entries())
const payload = await response.text()
return deserializer(payload)
}

async setItem(collectionId: string, item: U, contentType = 'application/json') {
const { [contentType]: serializer = this.serializers.get('application/json'), } = Object.fromEntries(this.serializers.entries())
const response = await window.fetch([this.baseUrl, collectionId, this.getItemId(item)].join('/'), {
method: 'put',
body: serializer(item),
})
// resource is created
if (response.status === CREATED) {
return
}
if (100 <= response.status && response.status <= 399) {
console.warn(`Expected response is ${CREATED}, got ${response.status}.`)
return
}
throw new Error(response.statusText)
}

async removeItem(collectionId: string, item: U) {
const response = await window.fetch([this.baseUrl, collectionId, this.getItemId(item)].join('/'), {
method: 'delete',
})
// resource is deleted
if (response.status === NO_CONTENT) {
return true
}
// resource is already deleted
if (response.status === NOT_FOUND || response.status === GONE) {
return false
}
if (100 <= response.status && response.status <= 399) {
console.warn(`Expected response is ${NO_CONTENT}, got ${response.status}.`)
// assume there's a change in the collection
return true
}
throw new Error(response.statusText)
}

// TODO do removeCollection for account closing?
}

+ 27
- 0
src/plugins/remote-storage/Storage.ts View File

@@ -0,0 +1,27 @@
import Storage, { StorageMeta } from '../../services/storages/Storage'
import Engine from './Engine'

export default class RemoteStorage<T> implements Storage<T> {
private readonly engine: Engine<T, StorageMeta<T>>

constructor(
private ownerId: string,
private baseUrl: string,
private collectionId: string,
) {
this.engine = new Engine(baseUrl)
}

async deleteItem(item: T) {
return this.engine.removeItem(this.collectionId, item)
}

async queryItems() {
const response = await this.engine.getCollection(this.collectionId) as StorageMeta<T>;
return response.items;
}

async saveItem(newItem: T) {
return this.engine.setItem(this.collectionId, newItem)
}
}

+ 18
- 0
src/plugins/remote-storage/index.ts View File

@@ -0,0 +1,18 @@
import Storage from './Storage'

type PluginConfig = {
baseUrl: string,
}

type State = {
currentUserId: string,
}

type Plugin = (config: PluginConfig) => (state: State) => void

const RemoteStoragePlugin: Plugin = config => ({
currentUserId,
}) => {
new Storage(currentUserId, config.baseUrl, 'notes')
new Storage(currentUserId, config.baseUrl, 'folders')
}

+ 9
- 6
src/seeds.ts View File

@@ -1,10 +1,13 @@
import Operation from './services/Operation'

export default {
'Operation': Object
.entries(Operation)
.map(([name, id]) => ({
id: Number(id),
name,
})),
'operations': (
Object
.entries(Operation)
.filter(([, value]) => !isNaN(Number(value)))
.map(([name, id]) => ({
id: Number(id),
name,
}))
),
}

+ 0
- 74
src/services/Controller.ts View File

@@ -1,74 +0,0 @@
import ORM, { DatabaseKind } from '../utilities/ORM'

export const collection = (Model, Service) => async (req, res) => {
const orm = new ORM({
kind: process.env.DATABASE_DIALECT as DatabaseKind,
url: process.env.DATABASE_URL,
})
const repository = orm.getRepository(Model)
const methodHandlers = {
'GET': Service.getMultiple(repository),
}

const { [req.method as keyof typeof methodHandlers]: handler = null } = methodHandlers
if (handler === null) {
res.statusCode = 415
res.json({ message: 'Method not allowed.' })
return
}

try {
const { status, data, } = await handler(req.query)
res.statusCode = status
res.json(data)
} catch (err) {
console.error(err)
const { status, data, } = err
res.statusCode = status
if (data && status !== 204) {
res.json(data)
return
}
res.end()
}
}

export const item = (Model, Service) => async (req, res) => {
const orm = new ORM({
kind: process.env.DATABASE_DIALECT as DatabaseKind,
url: process.env.DATABASE_URL,
})
const repository = orm.getRepository(Model)
const methodHandlers = {
'GET': Service.getSingle(repository),
'PUT': Service.save(repository)(req.body),
'DELETE': Service.remove(repository)
}

const { [req.method as keyof typeof methodHandlers]: handler = null } = methodHandlers
if (handler === null) {
res.statusCode = 415
res.json({ message: 'Method not allowed.' })
return
}

const { id } = req.query
try {
const { status, data, } = await handler(id)
res.statusCode = status
if (data) {
res.json(data)
return
}
res.end()
} catch (err) {
console.error(err)
const { status, data, } = err
res.statusCode = status || 500
if (data && status !== 204) {
res.json(data)
return
}
res.end()
}
}

+ 2
- 2
src/services/Storage.ts View File

@@ -1,7 +1,7 @@
import { addTime, TimeDivision } from '../utilities/Date'
import * as Serialization from '../utilities/Serialization'
import NoteModel from '../models/Note'
import InferModel from '../utilities/Instance'
import * as ColumnTypes from '../utilities/ColumnTypes'
import LocalStorage from './LocalStorage'

type StorageParams = {
@@ -9,7 +9,7 @@ type StorageParams = {
url: string,
}

type Note = InferModel<typeof NoteModel>
type Note = ColumnTypes.InferModel<typeof NoteModel>

type LoadItems = <T extends Record<string, unknown>>(params: StorageParams) => () => Promise<T[]>



src/services/Folder.ts → src/services/entities/Folder.ts View File

@@ -1,8 +1,8 @@
import FolderModel from '../models/Folder'
import Instance from '../utilities/Instance'
import * as Response from '../utilities/Response'
import FolderModel from '../../models/Folder'
import * as ColumnTypes from '../../utilities/ColumnTypes'
import * as Response from '../../utilities/Response'

type Folder = Instance<typeof FolderModel>
type Folder = ColumnTypes.InferModel<typeof FolderModel>

export const getSingle = repository => async (id: string) => {
const instance = await repository.findByPk(id)
@@ -10,14 +10,22 @@ export const getSingle = repository => async (id: string) => {
throw new Response.NotFound({ message: 'Not found.' })
}
return new Response.Retrieved({
data: instance,
data: instance.toJSON() as Folder,
})
}

export const getMultiple = repository => async (query: Record<string, unknown>) => {
const instances = await repository.findAll()
return new Response.Retrieved({
data: instances,
export const getMultiple = repository => async (query?: Record<string, unknown>) => {
const fetchMethod = async query => {
if (query) {
return repository.findWhere({
attributes: query,
})
}
return repository.findAll()
}
const instances = await fetchMethod(query)
return new Response.Retrieved<Folder[]>({
data: instances.map(i => i.toJSON()),
})
}

@@ -32,7 +40,7 @@ export const save = repository => (body: Partial<Folder>) => async (id: string,

if (created) {
return new Response.Created({
data: newInstance.toJSON()
data: newInstance.toJSON() as Folder
})
}

@@ -42,7 +50,7 @@ export const save = repository => (body: Partial<Folder>) => async (id: string,
newInstance[idColumnName] = id
const updatedInstance = await newInstance.save()
return new Response.Saved({
data: updatedInstance.toJSON()
data: updatedInstance.toJSON() as Folder
})
}


src/services/Note.ts → src/services/entities/Note.ts View File

@@ -1,8 +1,8 @@
import Model from '../models/Note'
import InferType from '../utilities/Instance'
import * as Response from '../utilities/Response'
import Model from '../../models/Note'
import * as ColumnTypes from '../../utilities/ColumnTypes'
import * as Response from '../../utilities/Response'

type ModelInstance = InferType<typeof Model>
type Note = ColumnTypes.InferModel<typeof Model>

export const getSingle = repository => async (id: string) => {
const instanceDAO = await repository.findByPk(id)
@@ -31,7 +31,7 @@ export const getMultiple = repository => async (query: Record<string, unknown>)
})
}

export const save = repository => (body: Partial<ModelInstance>) => async (id: string, idColumnName = 'id') => {
export const save = repository => (body: Partial<Note>) => async (id: string, idColumnName = 'id') => {
const { content: contentRaw, ...etcBody } = body
const content = contentRaw! ? JSON.stringify(contentRaw) : null
const [dao, created] = await repository.findOrCreate({

+ 12
- 13
src/services/storages/Storage.ts View File

@@ -1,17 +1,16 @@
type Collection<T> = {
items: T[],
total: number,
skip: number,
take: number,
export default interface Storage<T> {
queryItems(): Promise<T[]>
saveItem(item: T): Promise<void>
deleteItem(item: T): Promise<boolean>
}

type Query = {
q: string,
export interface StorageMeta<T> {
items: T[],
lastModifiedBy: string,
lastModifiedAt: Date,
}

export default interface Storage<T> {
loadSingle(id: string): T,
loadMultiple(query: Query): Collection<T>,
saveSingle(data: T): (id: string) => boolean,
deleteSingle(id: string): boolean,
}
export type Serializer<T = unknown> = (t: T) => string
export type Deserializer<T = unknown> = (s: string) => T

export class OutOfSyncError {}

+ 60
- 0
src/utilities/ColumnTypes.ts View File

@@ -0,0 +1,60 @@
import {
DataType,
INTEGER,
STRING,
TEXT,
DATE,
DATEONLY,
UUIDV4,
} from 'sequelize'

type ModelAttribute = {
allowNull?: boolean,
primaryKey?: boolean,
type: DataType,
}

export type Model = {
tableName?: string,
modelName?: string,
options?: {
timestamps?: boolean,
paranoid?: boolean,
createdAt?: string | boolean,
updatedAt?: string | boolean,
deletedAt?: string | boolean,
},
attributes: Record<string, ModelAttribute>,
}

type IntegerType = typeof INTEGER | ReturnType<typeof INTEGER>
type NumberType = IntegerType

type VarcharType = typeof STRING
type TextType = typeof TEXT | ReturnType<typeof TEXT>
type StringType = VarcharType | TextType

type DateTimeType = typeof DATE
type DateOnlyType = typeof DATEONLY
type DateType = DateTimeType | DateOnlyType

type InferType<V extends DataType> = (
V extends NumberType ? number :
V extends StringType ? string :
V extends DateType ? Date :
V extends typeof UUIDV4 ? string :
unknown
)

export type InferModel<M extends Model> = {
[K in keyof M['attributes']]-?: InferType<M['attributes'][K]['type']>
}

export {
INTEGER,
STRING,
TEXT,
DATEONLY,
DATE,
UUIDV4,
}

+ 0
- 36
src/utilities/Instance.ts View File

@@ -1,36 +0,0 @@
import * as Sequelize from 'sequelize'

type ModelAttribute = {
allowNull?: boolean,
primaryKey?: boolean,
type: Sequelize.DataType,
}

type Model = {
tableName?: string,
modelName?: string,
options?: {
timestamps?: boolean,
paranoid?: boolean,
createdAt?: string | boolean,
updatedAt?: string | boolean,
deletedAt?: string | boolean,
},
rawAttributes: Record<string, ModelAttribute>,
}

type InferType<V extends Sequelize.DataType> = (
V extends typeof Sequelize.STRING ? string :
V extends typeof Sequelize.TEXT ? string :
V extends ReturnType<typeof Sequelize.TEXT> ? string :
V extends typeof Sequelize.DATE ? Date :
V extends typeof Sequelize.DATEONLY ? Date :
V extends typeof Sequelize.UUIDV4 ? string :
unknown
)

type InferProps<M extends Model> = {
[K in keyof M['rawAttributes']]-?: InferType<M['rawAttributes'][K]['type']>
}

export default InferProps

+ 8
- 3
src/utilities/ORM.ts View File

@@ -1,4 +1,5 @@
import { Sequelize, Dialect, ModelAttributes, ModelOptions } from 'sequelize'
import { Sequelize, Dialect, } from 'sequelize'
import * as ColumnTypes from './ColumnTypes'

export enum DatabaseKind {
MSSQL = 'mssql',
@@ -34,13 +35,17 @@ export default class ORM {
host: url,
})
return
// TODO add nosql dbs
default:
break
}
throw new Error(`Database kind "${kind as string}" not yet supported.`)
}

getRepository(model: { modelName: string, rawAttributes: ModelAttributes, options: ModelOptions }) {
return this.instance.define(model.modelName, model.rawAttributes, model.options)
getRepository(model: ColumnTypes.Model) {
if (this.instance instanceof Sequelize) {
return this.instance.define(model.modelName, model.attributes, model.options)
}
throw new Error('Interface not yet initialized.')
}
}

+ 5
- 5
src/utilities/Response.ts View File

@@ -1,4 +1,4 @@
interface Response<T = Record<string, unknown>> {
interface Response<T extends {} = {}> {
status: number,
data?: T,
}
@@ -7,7 +7,7 @@ interface ErrorResponse extends Response {
message: string,
}

type ResponseParams<T extends Record<string, unknown> = Record<string, unknown>> = {
type ResponseParams<T extends {} = {}> = {
message?: string,
data?: T,
}
@@ -20,7 +20,7 @@ export class NotFound implements ErrorResponse {
}
}

export class Created<T extends Record<string, unknown>> implements Response {
export class Created<T extends {}> implements Response {
public readonly status = 201
public readonly data: T
constructor(params: ResponseParams<T>) {
@@ -28,7 +28,7 @@ export class Created<T extends Record<string, unknown>> implements Response {
}
}

export class Saved<T extends Record<string, unknown>> implements Response {
export class Saved<T extends {}> implements Response {
public readonly status = 200
public readonly data: T
constructor(params: ResponseParams<T>) {
@@ -36,7 +36,7 @@ export class Saved<T extends Record<string, unknown>> implements Response {
}
}

export class Retrieved<T extends Record<string, unknown>> implements Response {
export class Retrieved<T extends {}> implements Response {
public readonly status = 200
public readonly data: T
constructor(params: ResponseParams<T>) {


+ 0
- 19
yarn.lock View File

@@ -3088,18 +3088,6 @@ glob-to-regexp@^0.4.1:
resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e"
integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==

glob@7.1.2:
version "7.1.2"
resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15"
integrity sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==
dependencies:
fs.realpath "^1.0.0"
inflight "^1.0.4"
inherits "2"
minimatch "^3.0.4"
once "^1.3.0"
path-is-absolute "^1.0.0"

glob@^7.0.3, glob@^7.1.3, glob@^7.1.4:
version "7.1.6"
resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6"
@@ -5168,13 +5156,6 @@ sequelize-pool@^2.3.0:
resolved "https://registry.yarnpkg.com/sequelize-pool/-/sequelize-pool-2.3.0.tgz#64f1fe8744228172c474f530604b6133be64993d"
integrity sha512-Ibz08vnXvkZ8LJTiUOxRcj1Ckdn7qafNZ2t59jYHMX1VIebTAOYefWdRYFt6z6+hy52WGthAHAoLc9hvk3onqA==

sequelize-typescript@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/sequelize-typescript/-/sequelize-typescript-1.1.0.tgz#d5c2945e7fbfe55a934917b27d84589858d79123"
integrity sha512-FAPEQPeAhIaFQNLAcf9Q2IWcqWhNcvn5OZZ7BzGB0CJMtImIsGg4E/EAb7huMmPaPwDArxJUWGqk1KurphTNRA==
dependencies:
glob "7.1.2"

sequelize@5.22.0:
version "5.22.0"
resolved "https://registry.yarnpkg.com/sequelize/-/sequelize-5.22.0.tgz#72344a3aecd6767a8ceb02b8cba739e3ebeadeaf"


+ 9
- 0
zeichen.config.ts View File

@@ -0,0 +1,9 @@
import LocalStorage from './src/plugins/local-storage'
import RemoteStorage from './src/plugins/remote-storage'

export default {
plugins: [
LocalStorage,
RemoteStorage({ baseUrl: 'https://localhost:3000/api', }),
]
}

Loading…
Cancel
Save