Add support for pluginsfeature/transactions
@@ -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) | |||
} | |||
@@ -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" | |||
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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() | |||
} | |||
} |
@@ -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() | |||
} | |||
} |
@@ -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() | |||
} | |||
} |
@@ -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() | |||
} | |||
} |
@@ -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) | |||
} | |||
} |
@@ -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 | |||
} | |||
} |
@@ -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') | |||
} |
@@ -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? | |||
} |
@@ -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) | |||
} | |||
} |
@@ -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') | |||
} |
@@ -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, | |||
})) | |||
), | |||
} |
@@ -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() | |||
} | |||
} |
@@ -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[]> | |||
@@ -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 | |||
}) | |||
} | |||
@@ -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({ |
@@ -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 {} |
@@ -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, | |||
} |
@@ -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 |
@@ -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.') | |||
} | |||
} |
@@ -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>) { | |||
@@ -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" | |||
@@ -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', }), | |||
] | |||
} |