Browse Source

Decouple backend

Use backend repo for backend-related stuff.
master
Allan Crisostomo 3 years ago
parent
commit
0faf37dde5
30 changed files with 119 additions and 1850 deletions
  1. +0
    -9
      .babelrc
  2. +0
    -7
      .sequelizerc
  3. +0
    -18
      config/config.js
  4. +0
    -25
      migrate.ts
  5. +3
    -11
      package.json
  6. +2
    -2
      src/controllers/Folder.ts
  7. +12
    -6
      src/controllers/Note.ts
  8. +0
    -13
      src/models.ts
  9. +0
    -42
      src/models/Folder.ts
  10. +0
    -46
      src/models/Note.ts
  11. +0
    -22
      src/models/Operation.ts
  12. +0
    -22
      src/models/Tag.ts
  13. +0
    -34
      src/models/Transaction.ts
  14. +3
    -3
      src/pages/api/folders.ts
  15. +3
    -3
      src/pages/api/folders/[id].ts
  16. +3
    -3
      src/pages/api/notes.ts
  17. +3
    -3
      src/pages/api/notes/[id].ts
  18. +20
    -13
      src/pages/notes.tsx
  19. +0
    -13
      src/seeds.ts
  20. +0
    -23
      src/services/LocalStorage.ts
  21. +0
    -15
      src/services/Operation.ts
  22. +35
    -99
      src/services/Storage.ts
  23. +0
    -64
      src/services/entities/Folder.ts
  24. +0
    -78
      src/services/entities/Note.ts
  25. +0
    -60
      src/utilities/ColumnTypes.ts
  26. +0
    -82
      src/utilities/Date.ts
  27. +0
    -51
      src/utilities/ORM.ts
  28. +0
    -49
      src/utilities/Response.ts
  29. +33
    -1032
      yarn.lock
  30. +2
    -2
      zeichen.config.ts

+ 0
- 9
.babelrc View File

@@ -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"
]
}

+ 0
- 7
.sequelizerc View File

@@ -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')
};

+ 0
- 18
config/config.js View File

@@ -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
}
}

+ 0
- 25
migrate.ts View File

@@ -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)
}

+ 3
- 11
package.json View File

@@ -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"
}

+ 2
- 2
src/controllers/Folder.ts View File

@@ -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)
}

+ 12
- 6
src/controllers/Note.ts View File

@@ -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)
}

+ 0
- 13
src/models.ts View File

@@ -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,
]

+ 0
- 42
src/models/Folder.ts View File

@@ -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

+ 0
- 46
src/models/Note.ts View File

@@ -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

+ 0
- 22
src/models/Operation.ts View File

@@ -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

+ 0
- 22
src/models/Tag.ts View File

@@ -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

+ 0
- 34
src/models/Transaction.ts View File

@@ -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

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

@@ -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({


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

@@ -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({


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

@@ -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({


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

@@ -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({


+ 20
- 13
src/pages/notes.tsx View File

@@ -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..."
/>


+ 0
- 13
src/seeds.ts View File

@@ -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,
}))
),
}

+ 0
- 23
src/services/LocalStorage.ts View File

@@ -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)
}
}

+ 0
- 15
src/services/Operation.ts View File

@@ -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

+ 35
- 99
src/services/Storage.ts View File

@@ -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
}

+ 0
- 64
src/services/entities/Folder.ts View File

@@ -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()
}

+ 0
- 78
src/services/entities/Note.ts View File

@@ -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()
}

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

@@ -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,
}

+ 0
- 82
src/utilities/Date.ts View File

@@ -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.')
}
}

+ 0
- 51
src/utilities/ORM.ts View File

@@ -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.')
}
}

+ 0
- 49
src/utilities/Response.ts View File

@@ -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
}

+ 33
- 1032
yarn.lock
File diff suppressed because it is too large
View File


+ 2
- 2
zeichen.config.ts View File

@@ -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', }),
]
}

Loading…
Cancel
Save