@@ -1,9 +0,0 @@ | |||
{ | |||
"presets": ["next/babel"], | |||
"plugins": [ | |||
"babel-plugin-transform-typescript-metadata", | |||
["@babel/plugin-proposal-decorators", { "legacy": true }], | |||
["@babel/plugin-proposal-class-properties", { "loose": true }], | |||
"babel-plugin-parameter-decorator" | |||
] | |||
} |
@@ -1,7 +0,0 @@ | |||
const path = require('path'); | |||
module.exports = { | |||
'config-path': path.resolve('database', 'config', 'config.js'), | |||
'seeders-path': path.resolve('database', 'seeders'), | |||
'migrations-path': path.resolve('database', 'migrations') | |||
}; |
@@ -1,18 +0,0 @@ | |||
const dotenv = require('dotenv') | |||
dotenv.config() | |||
module.exports = { | |||
"development": { | |||
"host": process.env.DATABASE_URL, | |||
"dialect": process.env.DATABASE_DIALECT | |||
}, | |||
"test": { | |||
"host": process.env.DATABASE_URL, | |||
"dialect": process.env.DATABASE_DIALECT | |||
}, | |||
"production": { | |||
"host": process.env.DATABASE_URL, | |||
"dialect": process.env.DATABASE_DIALECT | |||
} | |||
} |
@@ -1,25 +0,0 @@ | |||
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.attributes)) | |||
await Promise.all(createTablePromises) | |||
const seedTablePromise = models | |||
.filter(m => Boolean(seeds[m.modelName])) | |||
.map(m => { | |||
console.log(JSON.stringify(seeds[m.modelName])) | |||
return queryInterface.bulkInsert(m.tableName, seeds[m.tableName]) | |||
}) | |||
return Promise.all(seedTablePromise) | |||
} | |||
export const down = async queryInterface => { | |||
const dropTablePromises = models | |||
.reduce((reverse, m) => [m, ...reverse], []) | |||
.map(m => queryInterface.dropTable(m.tableName)) | |||
return Promise.all(dropTablePromises) | |||
} |
@@ -6,34 +6,26 @@ | |||
"scripts": { | |||
"dev": "next dev", | |||
"build": "next build", | |||
"start": "next start", | |||
"migrate": "tsc migrate.ts --module commonjs --esModuleInterop --outDir database/migrations && sequelize-cli db:migrate", | |||
"migrate:run": "sequelize-cli db:migrate" | |||
"start": "next start" | |||
}, | |||
"dependencies": { | |||
"@fingerprintjs/fingerprintjs": "^3.0.3", | |||
"dotenv": "^8.2.0", | |||
"mobiledoc-kit": "^0.13.1", | |||
"next": "9.5.5", | |||
"react": "17.0.1", | |||
"react-dom": "17.0.1", | |||
"react-feather": "^2.0.8", | |||
"react-mobiledoc-editor": "^0.10.0", | |||
"sequelize": "5.22.0", | |||
"styled-components": "^5.2.0", | |||
"uuid": "^8.3.1" | |||
}, | |||
"devDependencies": { | |||
"@types/node": "^14.14.2", | |||
"@types/react": "^16.9.53", | |||
"@types/react-dom": "^17.0.0", | |||
"@types/styled-components": "^5.1.4", | |||
"@types/uuid": "^8.3.0", | |||
"typescript": "^4.0.3", | |||
"sequelize-cli": "^6.2.0", | |||
"sqlite3": "^5.0.0" | |||
}, | |||
"optionalDependencies": { | |||
"sqlite3": "^5.0.0" | |||
"typescript": "^4.0.3" | |||
}, | |||
"license": "MIT" | |||
} |
@@ -1,6 +1,6 @@ | |||
import * as Storage from '../services/Storage' | |||
export const load = async ({ setFolders, }) => { | |||
const theFolders = await Storage.loadFolders() | |||
export const load = async ({ userId, setFolders, }) => { | |||
const theFolders = await Storage.load({ userId, })('folders') | |||
setFolders(theFolders) | |||
} |
@@ -1,8 +1,8 @@ | |||
import * as Storage from '../services/Storage' | |||
const save = async ({ stateRef, router, id, setNotes, }) => { | |||
const save = async ({ userId, stateRef, router, id, setNotes, }) => { | |||
stateRef.current.updatedAt = new Date().toISOString() | |||
const newNote = await Storage.saveNote(stateRef.current) | |||
const newNote = await Storage.save({ userId, })('notes')(stateRef.current) | |||
if (router.query.id !== id) { | |||
await router.replace( | |||
{ | |||
@@ -32,12 +32,13 @@ const triggerAutoSave = ({ | |||
router, | |||
id, | |||
setNotes, | |||
userId, | |||
}) => { | |||
if (timeoutRef.current !== null) { | |||
clearTimeout(timeoutRef.current) | |||
} | |||
timeoutRef.current = setTimeout(async () => { | |||
await save({ stateRef, router, id, setNotes, }) | |||
await save({ userId, stateRef, router, id, setNotes, }) | |||
timeoutRef.current = null | |||
}, 3000) | |||
} | |||
@@ -48,9 +49,11 @@ export const updateContent = ({ | |||
router, | |||
id, | |||
setNotes, | |||
userId, | |||
}) => e => { | |||
stateRef.current.content = e | |||
triggerAutoSave({ | |||
userId, | |||
stateRef, | |||
timeoutRef, | |||
router, | |||
@@ -66,9 +69,11 @@ export const updateTitle = ({ | |||
id, | |||
setNotes, | |||
setTitle, | |||
userId, | |||
}) => e => { | |||
setTitle(stateRef.current.title = e.target.value) | |||
triggerAutoSave({ | |||
userId, | |||
stateRef, | |||
timeoutRef, | |||
router, | |||
@@ -81,6 +86,7 @@ export const remove = ({ | |||
setNotes, | |||
notes, | |||
router, | |||
userId, | |||
}) => note => async () => { | |||
setNotes(notes.filter(n => n.id !== note.id)) | |||
await router.replace( | |||
@@ -92,13 +98,13 @@ export const remove = ({ | |||
shallow: true, | |||
} | |||
) | |||
const result = await Storage.deleteNote(note) | |||
const result = await Storage.remove({ userId, })('notes')(note) | |||
if (!result) { | |||
setNotes(notes) | |||
} | |||
} | |||
export const load = async ({ setNotes, }) => { | |||
const theNotes = await Storage.loadNotes() | |||
export const load = async ({ setNotes, userId, }) => { | |||
const theNotes = await Storage.load({ userId, })('notes') | |||
setNotes(theNotes) | |||
} |
@@ -1,13 +0,0 @@ | |||
import Folder from './models/Folder' | |||
import Note from './models/Note' | |||
import Tag from './models/Tag' | |||
import Operation from './models/Operation' | |||
import Transaction from './models/Transaction' | |||
export default [ | |||
Folder, | |||
Note, | |||
Tag, | |||
Operation, | |||
Transaction, | |||
] |
@@ -1,42 +0,0 @@ | |||
import * as ColumnTypes from '../utilities/ColumnTypes' | |||
const Folder: ColumnTypes.Model = { | |||
modelName: 'Folder', | |||
tableName: 'folders', | |||
options: { | |||
timestamps: true, | |||
paranoid: true, | |||
createdAt: 'createdAt', | |||
updatedAt: 'updatedAt', | |||
deletedAt: 'deletedAt', | |||
}, | |||
attributes: { | |||
id: { | |||
allowNull: true, | |||
primaryKey: true, | |||
type: ColumnTypes.UUIDV4, | |||
}, | |||
name: { | |||
allowNull: false, | |||
type: ColumnTypes.STRING, | |||
}, | |||
parentId: { | |||
allowNull: true, | |||
type: ColumnTypes.UUIDV4, | |||
}, | |||
createdAt: { | |||
allowNull: false, | |||
type: ColumnTypes.DATE, | |||
}, | |||
updatedAt: { | |||
allowNull: false, | |||
type: ColumnTypes.DATE, | |||
}, | |||
deletedAt: { | |||
allowNull: true, | |||
type: ColumnTypes.DATE, | |||
}, | |||
} | |||
} | |||
export default Folder |
@@ -1,46 +0,0 @@ | |||
import * as ColumnTypes from '../utilities/ColumnTypes' | |||
const Note: ColumnTypes.Model = { | |||
modelName: 'Note', | |||
tableName: 'notes', | |||
options: { | |||
timestamps: true, | |||
paranoid: true, | |||
createdAt: 'createdAt', | |||
updatedAt: 'updatedAt', | |||
deletedAt: 'deletedAt', | |||
}, | |||
attributes: { | |||
id: { | |||
allowNull: true, | |||
primaryKey: true, | |||
type: ColumnTypes.UUIDV4, | |||
}, | |||
title: { | |||
allowNull: false, | |||
type: ColumnTypes.STRING, | |||
}, | |||
content: { | |||
allowNull: true, | |||
type: ColumnTypes.TEXT({ length: 'long', }), | |||
}, | |||
folderId: { | |||
allowNull: true, | |||
type: ColumnTypes.UUIDV4, | |||
}, | |||
createdAt: { | |||
allowNull: false, | |||
type: ColumnTypes.DATE, | |||
}, | |||
updatedAt: { | |||
allowNull: false, | |||
type: ColumnTypes.DATE, | |||
}, | |||
deletedAt: { | |||
allowNull: true, | |||
type: ColumnTypes.DATE, | |||
}, | |||
}, | |||
} | |||
export default Note |
@@ -1,22 +0,0 @@ | |||
import * as ColumnTypes from '../utilities/ColumnTypes' | |||
const Operation: ColumnTypes.Model = { | |||
modelName: 'Operation', | |||
tableName: 'operations', | |||
options: { | |||
timestamps: false, | |||
}, | |||
attributes: { | |||
id: { | |||
allowNull: true, | |||
primaryKey: true, | |||
type: ColumnTypes.INTEGER, | |||
}, | |||
name: { | |||
allowNull: false, | |||
type: ColumnTypes.STRING, | |||
}, | |||
} | |||
} | |||
export default Operation |
@@ -1,22 +0,0 @@ | |||
import * as ColumnTypes from '../utilities/ColumnTypes' | |||
const Tag: ColumnTypes.Model = { | |||
modelName: 'Tag', | |||
tableName: 'tags', | |||
options: { | |||
timestamps: false, | |||
}, | |||
attributes: { | |||
id: { | |||
allowNull: true, | |||
primaryKey: true, | |||
type: ColumnTypes.UUIDV4, | |||
}, | |||
name: { | |||
allowNull: false, | |||
type: ColumnTypes.STRING, | |||
}, | |||
} | |||
} | |||
export default Tag |
@@ -1,34 +0,0 @@ | |||
import * as ColumnTypes from '../utilities/ColumnTypes' | |||
const Transaction: ColumnTypes.Model = { | |||
modelName: 'Transaction', | |||
tableName: 'transactions', | |||
options: { | |||
timestamps: false, | |||
}, | |||
attributes: { | |||
id: { | |||
allowNull: true, | |||
primaryKey: true, | |||
type: ColumnTypes.UUIDV4, | |||
}, | |||
deviceId: { | |||
allowNull: false, | |||
type: ColumnTypes.STRING, | |||
}, | |||
operation: { | |||
allowNull: false, | |||
type: ColumnTypes.INTEGER, | |||
}, | |||
objectId: { | |||
allowNull: false, | |||
type: ColumnTypes.STRING, | |||
}, | |||
performedAt: { | |||
allowNull: false, | |||
type: ColumnTypes.DATE, | |||
}, | |||
}, | |||
} | |||
export default Transaction |
@@ -1,6 +1,6 @@ | |||
import ORM, { DatabaseKind } from '../../utilities/ORM' | |||
import * as Service from '../../services/entities/Folder' | |||
import Model from '../../models/Folder' | |||
import ORM, { DatabaseKind } from '../../../../backend/src/utilities/ORM' | |||
import * as Service from '../../../../backend/src/services/Folder' | |||
import Model from '../../../../backend/src/models/Folder' | |||
export default async (req, res) => { | |||
const orm = new ORM({ | |||
@@ -1,6 +1,6 @@ | |||
import ORM, { DatabaseKind } from '../../../utilities/ORM' | |||
import * as Service from '../../../services/entities/Folder' | |||
import Model from '../../../models/Folder' | |||
import ORM, { DatabaseKind } from '../../../../../backend/src/utilities/ORM' | |||
import * as Service from '../../../../../backend/src/services/Folder' | |||
import Model from '../../../../../backend/src/models/Folder' | |||
export default async (req, res) => { | |||
const orm = new ORM({ | |||
@@ -1,6 +1,6 @@ | |||
import ORM, { DatabaseKind } from '../../utilities/ORM' | |||
import * as Service from '../../services/entities/Note' | |||
import Model from '../../models/Note' | |||
import ORM, { DatabaseKind } from '../../../../backend/src/utilities/ORM' | |||
import * as Service from '../../../../backend/src/services/Note' | |||
import Model from '../../../../backend/src/models/Note' | |||
export default async (req, res) => { | |||
const orm = new ORM({ | |||
@@ -1,6 +1,6 @@ | |||
import ORM, { DatabaseKind } from '../../../utilities/ORM' | |||
import * as Service from '../../../services/entities/Note' | |||
import Model from '../../../models/Note' | |||
import ORM, { DatabaseKind } from '../../../../../backend/src/utilities/ORM' | |||
import * as Service from '../../../../../backend/src/services/Note' | |||
import Model from '../../../../../backend/src/models/Note' | |||
export default async (req, res) => { | |||
const orm = new ORM({ | |||
@@ -51,32 +51,37 @@ const PostPrimary = styled('div')({ | |||
marginBottom: '2rem', | |||
}) | |||
type NoteInstance = { id: string, title: string, content?: object, updatedAt: string, } | |||
type NoteInstance = { id: string, title: string, content?: object, updatedAt: string, folderId: string, } | |||
const Notes = ({ id: idProp }) => { | |||
const Notes = ({ id: idProp, userId, }) => { | |||
const router = useRouter() | |||
// TODO remove extra state for ID | |||
const [id, setId, ] = React.useState(idProp) | |||
const [title, setTitle, ] = React.useState('') | |||
const [notes, setNotes, ] = React.useState(null) | |||
const [folders, setFolders, ] = React.useState(null) | |||
const stateRef = React.useRef<NoteInstance>({ id, title: '', updatedAt: new Date().toISOString(), }) | |||
const stateRef = React.useRef<NoteInstance>({ id, title: '', updatedAt: new Date().toISOString(), folderId: router.query.parentFolderId as string, }) | |||
const timeoutRef = React.useRef<number>(null) | |||
const router = useRouter() | |||
React.useEffect(() => { | |||
Note.load({ setNotes }) | |||
}, []) | |||
Note.load({ setNotes, userId, }) | |||
}, [userId, ]) | |||
React.useEffect(() => { | |||
Folder.load({ setFolders }) | |||
}, []) | |||
Folder.load({ setFolders, userId, }) | |||
}, [userId, ]) | |||
React.useEffect(() => { | |||
if (!Array.isArray(notes!)) { | |||
return | |||
} | |||
const theNote = notes.find(n => n.id === id) | |||
stateRef.current = theNote ? theNote : { id, title: '', updatedAt: new Date().toISOString(), } | |||
stateRef.current = theNote ? theNote : { | |||
id, | |||
title: '', | |||
updatedAt: new Date().toISOString(), | |||
folderId: '00000000-0000-0000-000000000000', | |||
} | |||
setTitle(stateRef.current.title) | |||
}, [id, notes]) | |||
@@ -107,7 +112,7 @@ const Notes = ({ id: idProp }) => { | |||
secondaryVisible={Boolean(router.query.navbar)} | |||
primaryItemsStart={[ | |||
{ | |||
id: 'sidebar', | |||
id: 'menu', | |||
mobileOnly: true, | |||
active: Boolean(router.query.navbar), | |||
href: { | |||
@@ -182,7 +187,7 @@ const Notes = ({ id: idProp }) => { | |||
pathname: '/notes', | |||
query: { | |||
action: 'new', | |||
parentFolderId: '00000000-0000-0000-000000000000', | |||
parentFolderId: router.query.parentFolderId as string, | |||
}, | |||
}, | |||
iconName: 'new-note', | |||
@@ -206,7 +211,7 @@ const Notes = ({ id: idProp }) => { | |||
pathname: '/notes', | |||
query: { | |||
action: 'view-map', | |||
parentFolderId: '00000000-0000-0000-000000000000', | |||
parentFolderId: router.query.parentFolderId as string, | |||
}, | |||
}, | |||
iconName: 'mind-map', | |||
@@ -236,7 +241,7 @@ const Notes = ({ id: idProp }) => { | |||
{ | |||
id: 'bin', | |||
iconName: 'bin', | |||
onClick: Note.remove({ setNotes, notes, router, })(n), | |||
onClick: Note.remove({ userId, setNotes, notes, router, })(n), | |||
} | |||
], | |||
})) | |||
@@ -260,6 +265,7 @@ const Notes = ({ id: idProp }) => { | |||
id, | |||
setNotes, | |||
setTitle, | |||
userId, | |||
})} | |||
/> | |||
<PostMeta> | |||
@@ -286,6 +292,7 @@ const Notes = ({ id: idProp }) => { | |||
router, | |||
id, | |||
setNotes, | |||
userId, | |||
})} | |||
placeholder="Start typing here..." | |||
/> | |||
@@ -1,13 +0,0 @@ | |||
import Operation from './services/Operation' | |||
export default { | |||
'operations': ( | |||
Object | |||
.entries(Operation) | |||
.filter(([, value]) => !isNaN(Number(value))) | |||
.map(([name, id]) => ({ | |||
id: Number(id), | |||
name, | |||
})) | |||
), | |||
} |
@@ -1,23 +0,0 @@ | |||
export default class LocalStorage<T> { | |||
constructor( | |||
private readonly source: Storage, | |||
private readonly serializer: (t: T) => string, | |||
private readonly deserializer: (s: string) => T | |||
) {} | |||
getItem(id: string, fallback: T = null) { | |||
const raw = this.source.getItem(id) | |||
if (raw === null) { | |||
return fallback | |||
} | |||
return this.deserializer(raw) | |||
} | |||
setItem(id: string, item: T) { | |||
this.source.setItem(id, this.serializer(item)) | |||
} | |||
removeItem(id: string) { | |||
this.source.removeItem(id) | |||
} | |||
} |
@@ -1,15 +0,0 @@ | |||
enum Operation { | |||
'note:read' = 1, | |||
'note:write' = 2, | |||
'note:delete' = 3, | |||
'note:import' = 4, | |||
'note:export' = 5, | |||
'folder:read' = 7, | |||
'folder:write' = 8, | |||
'social:publish' = 9, | |||
'social:unpublish' = 10, | |||
'social:share' = 11, | |||
'userinfo:manage' = 12, | |||
} | |||
export default Operation |
@@ -1,110 +1,46 @@ | |||
import { addTime, TimeDivision } from '../utilities/Date' | |||
import * as Serialization from '../utilities/Serialization' | |||
import NoteModel from '../models/Note' | |||
import * as ColumnTypes from '../utilities/ColumnTypes' | |||
import LocalStorage from './LocalStorage' | |||
type StorageParams = { | |||
id: string, | |||
url: string, | |||
} | |||
type Note = ColumnTypes.InferModel<typeof NoteModel> | |||
type LoadItems = <T extends Record<string, unknown>>(params: StorageParams) => () => Promise<T[]> | |||
const loadItems: LoadItems = <T extends Record<string, unknown>>(params) => async (): Promise<T[]> => { | |||
const { id, url, } = params | |||
const storage = new LocalStorage( | |||
window.localStorage, | |||
Serialization.serialize, | |||
Serialization.deserialize | |||
import Config from '../../zeichen.config' | |||
export const load = state => async (collectionId: string, itemId?: unknown) => { | |||
const collectionVersions = await Promise.all( | |||
Config.plugins | |||
.filter(p => p['type'] === 'storage') | |||
.map(Plugin => { | |||
const pluginInstance = new Plugin(state) | |||
return pluginInstance.load(collectionId, itemId) | |||
}) | |||
) | |||
const localData = storage.getItem(id) | |||
if (localData === null) { | |||
const remoteItems = await window.fetch(url) | |||
// TODO add custom serialization method | |||
const theItems: T[] = await remoteItems.json() | |||
storage.setItem(id, { | |||
// TODO backend should set expiry | |||
expiry: addTime(new Date(), 30)(TimeDivision.DAYS).getTime(), | |||
items: theItems, | |||
}) | |||
return theItems | |||
} | |||
const dataExpiry = new Date(Number(localData.expiry)) | |||
const now = new Date() | |||
if (now.getTime() > dataExpiry.getTime()) { | |||
storage.removeItem(id) | |||
const loader: () => Promise<T[]> = loadItems(params) | |||
return loader() | |||
} | |||
return localData.items | |||
} | |||
// TODO reduce collectionVersions to get the most correct version. | |||
type SaveItem = <T extends Record<string, unknown>>(p: StorageParams) => (item: T) => Promise<T> | |||
return collectionVersions | |||
} | |||
const saveItem: SaveItem = <T extends Record<string, unknown>>(params) => async (item) => { | |||
const { id: storageId, url } = params | |||
const storage = new LocalStorage( | |||
window.localStorage, | |||
Serialization.serialize, | |||
Serialization.deserialize | |||
) | |||
const localData = storage.getItem(storageId, { | |||
expiry: addTime(new Date(), 30)(TimeDivision.DAYS).getTime(), | |||
items: [], | |||
}) | |||
const { items: localItems } = localData | |||
const { id: itemId, ...theBody } = item | |||
const theItems: T[] = ( | |||
localItems.some(i => i.id === itemId) | |||
? localItems.map(i => i.id === itemId ? item : i) | |||
: [...localItems, item] | |||
export const save = state => (collectionId: string) => async (item: unknown) => { | |||
const collectionVersions = await Promise.all( | |||
Config.plugins | |||
.filter(p => p['type'] === 'storage') | |||
.map(Plugin => { | |||
const pluginInstance = new Plugin(state) | |||
return pluginInstance.save(collectionId, item) | |||
}) | |||
) | |||
storage.setItem(storageId, { ...localData, items: theItems }) | |||
const response = await window.fetch(`${url}/${itemId}`, { | |||
method: 'put', | |||
body: JSON.stringify(theBody), | |||
headers: { | |||
'Content-Type': 'application/json', | |||
}, | |||
}) | |||
const responseBody = await response.json() | |||
if (response.status !== 201 && response.status !== 200) { | |||
throw responseBody | |||
} | |||
return responseBody as T | |||
} | |||
// TODO reduce collectionVersions to get the most correct version. | |||
type DeleteItem = <T extends Record<string, unknown>>(p: StorageParams) => (item: T) => Promise<boolean> | |||
return collectionVersions | |||
} | |||
const deleteItem: DeleteItem = <T extends Record<string, unknown>>(params) => async (item) => { | |||
const { id: storageId, url } = params | |||
const storage = new LocalStorage( | |||
window.localStorage, | |||
Serialization.serialize, | |||
Serialization.deserialize | |||
export const remove = state => (collectionId: string) => async (item: unknown) => { | |||
const collectionVersions = await Promise.all( | |||
Config.plugins | |||
.filter(p => p['type'] === 'storage') | |||
.map(Plugin => { | |||
const pluginInstance = new Plugin(state) | |||
return pluginInstance.remove(collectionId, item) | |||
}) | |||
) | |||
const localData = storage.getItem(storageId, { | |||
expiry: addTime(new Date(), 30)(TimeDivision.DAYS).getTime(), | |||
items: [], | |||
}) | |||
const { items: localItems } = localData | |||
const { id: itemId } = item | |||
const theItems: T[] = localItems.filter(i => i.id !== itemId) | |||
storage.setItem(storageId, { ...localData, items: theItems }) | |||
const response = await window.fetch(`${url}/${itemId}`, { | |||
method: 'delete', | |||
}) | |||
return response.status === 204 | |||
} | |||
// TODO reduce collectionVersions to get the most correct version. | |||
export const loadNotes = loadItems<Note>({ id: 'notes', url: '/api/notes' }) | |||
export const loadFolders = loadItems({ id: 'folders', url: '/api/folders' }) | |||
export const saveNote = saveItem<Note>({ id: 'notes', url: '/api/notes' }) | |||
export const saveFolder = saveItem({ id: 'folders', url: '/api/folders' }) | |||
export const deleteNote = deleteItem<Note>({ id: 'notes', url: '/api/notes' }) | |||
return collectionVersions | |||
} |
@@ -1,64 +0,0 @@ | |||
import FolderModel from '../../models/Folder' | |||
import * as ColumnTypes from '../../utilities/ColumnTypes' | |||
import * as Response from '../../utilities/Response' | |||
type Folder = ColumnTypes.InferModel<typeof FolderModel> | |||
export const getSingle = repository => async (id: string) => { | |||
const instance = await repository.findByPk(id) | |||
if (instance === null) { | |||
throw new Response.NotFound({ message: 'Not found.' }) | |||
} | |||
return new Response.Retrieved({ | |||
data: instance.toJSON() as Folder, | |||
}) | |||
} | |||
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()), | |||
}) | |||
} | |||
export const save = repository => (body: Partial<Folder>) => async (id: string, idColumnName = 'id') => { | |||
const [newInstance, created] = await repository.findOrCreate({ | |||
where: { [idColumnName]: id }, | |||
defaults: { | |||
...body, | |||
[idColumnName]: id, | |||
}, | |||
}) | |||
if (created) { | |||
return new Response.Created({ | |||
data: newInstance.toJSON() as Folder | |||
}) | |||
} | |||
Object.entries(body).forEach(([key, value]) => { | |||
newInstance[key] = value | |||
}) | |||
newInstance[idColumnName] = id | |||
const updatedInstance = await newInstance.save() | |||
return new Response.Saved({ | |||
data: updatedInstance.toJSON() as Folder | |||
}) | |||
} | |||
export const remove = repository => async (id: string) => { | |||
const instanceDAO = repository.findByPk(id) | |||
if (instanceDAO === null) { | |||
throw new Response.NotFound({ message: 'Not found.' }) | |||
} | |||
await instanceDAO.destroy() | |||
return new Response.Destroyed() | |||
} |
@@ -1,78 +0,0 @@ | |||
import Model from '../../models/Note' | |||
import * as ColumnTypes from '../../utilities/ColumnTypes' | |||
import * as Response from '../../utilities/Response' | |||
type Note = ColumnTypes.InferModel<typeof Model> | |||
export const getSingle = repository => async (id: string) => { | |||
const instanceDAO = await repository.findByPk(id) | |||
if (instanceDAO === null) { | |||
throw new Response.NotFound({ message: 'Not found.' }) | |||
} | |||
const instance = instanceDAO.toJSON() | |||
return new Response.Retrieved({ | |||
data: { | |||
...instance, | |||
content: instance.content ? JSON.parse(instance.content) : null, | |||
}, | |||
}) | |||
} | |||
export const getMultiple = repository => async (query: Record<string, unknown>) => { | |||
const instances = await repository.findAll() | |||
return new Response.Retrieved({ | |||
data: instances.map(instanceDAO => { | |||
const instance = instanceDAO.toJSON() | |||
return { | |||
...instance, | |||
content: instance.content ? JSON.parse(instance.content) : null, | |||
} | |||
}), | |||
}) | |||
} | |||
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({ | |||
where: { [idColumnName]: id }, | |||
defaults: { | |||
...etcBody, | |||
[idColumnName]: id, | |||
content, | |||
}, | |||
}) | |||
if (created) { | |||
const newInstance = dao.toJSON() | |||
return new Response.Created({ | |||
data: { | |||
...newInstance, | |||
content: newInstance.content ? JSON.parse(newInstance.content) : null, | |||
}, | |||
}) | |||
} | |||
Object.entries(body).forEach(([key, value]) => { | |||
dao[key] = value | |||
}) | |||
dao['content'] = content | |||
dao[idColumnName] = id | |||
const updatedDAO = await dao.save() | |||
const updatedInstance = updatedDAO.toJSON() | |||
return new Response.Saved({ | |||
data: { | |||
...updatedInstance, | |||
content: updatedInstance.content ? JSON.parse(updatedInstance.content) : null, | |||
} | |||
}) | |||
} | |||
export const remove = repository => async (id: string) => { | |||
const instanceDAO = await repository.findByPk(id) | |||
if (instanceDAO === null) { | |||
throw new Response.NotFound({ message: 'Not found.' }) | |||
} | |||
await instanceDAO.destroy() | |||
return new Response.Destroyed() | |||
} |
@@ -1,60 +0,0 @@ | |||
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, | |||
} |
@@ -14,85 +14,3 @@ export const formatDate: FormatDate = d => { | |||
return `${year}-${month}-${date} ${hour}:${minute}` | |||
} | |||
export enum TimeDivision { | |||
MILLISECONDS, | |||
SECONDS, | |||
MINUTES, | |||
HOURS, | |||
DAYS | |||
} | |||
type GetTimeDifference = (a: Date, b: Date) => (c: TimeDivision) => number | |||
export const getTimeDifference: GetTimeDifference = (a, b) => { | |||
const ms = b.getTime() - a.getTime() | |||
const absoluteMs = ms < 0 ? -ms : ms | |||
return c => { | |||
let divisionDifference = absoluteMs | |||
if (c === TimeDivision.MILLISECONDS) { | |||
return divisionDifference | |||
} | |||
divisionDifference /= 1000 | |||
if (c === TimeDivision.SECONDS) { | |||
return divisionDifference | |||
} | |||
divisionDifference /= 60 | |||
if (c === TimeDivision.MINUTES) { | |||
return divisionDifference | |||
} | |||
divisionDifference /= 60 | |||
if (c === TimeDivision.HOURS) { | |||
return divisionDifference | |||
} | |||
divisionDifference /= 24 | |||
if (c === TimeDivision.DAYS) { | |||
return divisionDifference | |||
} | |||
throw new Error('Unknown time division.') | |||
} | |||
} | |||
type AddTime = (refDate: Date, increment: number) => (c: TimeDivision) => Date | |||
export const addTime: AddTime = (refDate, increment) => { | |||
const futureDate = new Date(refDate.getTime()) | |||
return c => { | |||
let msIncrement = increment | |||
if (c === TimeDivision.MILLISECONDS) { | |||
futureDate.setMilliseconds(futureDate.getMilliseconds() + msIncrement) | |||
return futureDate | |||
} | |||
msIncrement *= 1000 | |||
if (c === TimeDivision.SECONDS) { | |||
futureDate.setMilliseconds(futureDate.getMilliseconds() + msIncrement) | |||
return futureDate | |||
} | |||
msIncrement *= 60 | |||
if (c === TimeDivision.MINUTES) { | |||
futureDate.setMilliseconds(futureDate.getMilliseconds() + msIncrement) | |||
return futureDate | |||
} | |||
msIncrement *= 60 | |||
if (c === TimeDivision.HOURS) { | |||
futureDate.setMilliseconds(futureDate.getMilliseconds() + msIncrement) | |||
return futureDate | |||
} | |||
msIncrement *= 24 | |||
if (c === TimeDivision.DAYS) { | |||
futureDate.setMilliseconds(futureDate.getMilliseconds() + msIncrement) | |||
return futureDate | |||
} | |||
throw new Error('Unknown time division.') | |||
} | |||
} |
@@ -1,51 +0,0 @@ | |||
import { Sequelize, Dialect, } from 'sequelize' | |||
import * as ColumnTypes from './ColumnTypes' | |||
export enum DatabaseKind { | |||
MSSQL = 'mssql', | |||
SQLITE = 'sqlite', | |||
MYSQL = 'mysql', | |||
MARIADB = 'mariadb', | |||
POSTGRESQL = 'postgres', | |||
} | |||
type RepositoryParams = { | |||
url: string, | |||
kind: DatabaseKind, | |||
} | |||
export default class ORM { | |||
private readonly instance: Sequelize | |||
constructor(params: RepositoryParams) { | |||
const { kind, url, } = params | |||
switch (kind as DatabaseKind) { | |||
case DatabaseKind.SQLITE: | |||
this.instance = new Sequelize({ | |||
dialect: kind as string as Dialect, | |||
storage: url, | |||
}) | |||
return | |||
case DatabaseKind.POSTGRESQL: | |||
case DatabaseKind.MARIADB: | |||
case DatabaseKind.MSSQL: | |||
case DatabaseKind.MYSQL: | |||
this.instance = new Sequelize({ | |||
dialect: kind as string as Dialect, | |||
host: url, | |||
}) | |||
return | |||
// TODO add nosql dbs | |||
default: | |||
break | |||
} | |||
throw new Error(`Database kind "${kind as string}" not yet supported.`) | |||
} | |||
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,49 +0,0 @@ | |||
interface Response<T extends {} = {}> { | |||
status: number, | |||
data?: T, | |||
} | |||
interface ErrorResponse extends Response { | |||
message: string, | |||
} | |||
type ResponseParams<T extends {} = {}> = { | |||
message?: string, | |||
data?: T, | |||
} | |||
export class NotFound implements ErrorResponse { | |||
public readonly status = 404 | |||
public readonly message: string | |||
constructor(params: ResponseParams) { | |||
this.message = params.message | |||
} | |||
} | |||
export class Created<T extends {}> implements Response { | |||
public readonly status = 201 | |||
public readonly data: T | |||
constructor(params: ResponseParams<T>) { | |||
this.data = params.data | |||
} | |||
} | |||
export class Saved<T extends {}> implements Response { | |||
public readonly status = 200 | |||
public readonly data: T | |||
constructor(params: ResponseParams<T>) { | |||
this.data = params.data | |||
} | |||
} | |||
export class Retrieved<T extends {}> implements Response { | |||
public readonly status = 200 | |||
public readonly data: T | |||
constructor(params: ResponseParams<T>) { | |||
this.data = params.data | |||
} | |||
} | |||
export class Destroyed implements Response { | |||
public readonly status = 204 | |||
} |
@@ -3,7 +3,7 @@ import RemoteStorage from '../plugin-remote-storage/src' | |||
export default { | |||
plugins: [ | |||
LocalStorage, | |||
RemoteStorage({ baseUrl: 'https://localhost:3000/api', }), | |||
LocalStorage(), | |||
RemoteStorage({ baseUrl: 'http://localhost:3000/api', }), | |||
] | |||
} |