@radio-garden/ditojs-server 2.85.2-0.5067ad799

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (137) hide show
  1. package/README.md +6 -0
  2. package/package.json +95 -0
  3. package/src/app/Application.js +1186 -0
  4. package/src/app/Validator.js +405 -0
  5. package/src/app/index.js +2 -0
  6. package/src/cli/console.js +152 -0
  7. package/src/cli/db/createMigration.js +241 -0
  8. package/src/cli/db/index.js +7 -0
  9. package/src/cli/db/listAssetConfig.js +10 -0
  10. package/src/cli/db/migrate.js +12 -0
  11. package/src/cli/db/reset.js +23 -0
  12. package/src/cli/db/rollback.js +12 -0
  13. package/src/cli/db/seed.js +80 -0
  14. package/src/cli/db/unlock.js +9 -0
  15. package/src/cli/index.js +72 -0
  16. package/src/controllers/AdminController.js +322 -0
  17. package/src/controllers/CollectionController.js +274 -0
  18. package/src/controllers/Controller.js +657 -0
  19. package/src/controllers/ControllerAction.js +370 -0
  20. package/src/controllers/MemberAction.js +27 -0
  21. package/src/controllers/ModelController.js +63 -0
  22. package/src/controllers/RelationController.js +93 -0
  23. package/src/controllers/UsersController.js +64 -0
  24. package/src/controllers/index.js +5 -0
  25. package/src/errors/AssetError.js +7 -0
  26. package/src/errors/AuthenticationError.js +7 -0
  27. package/src/errors/AuthorizationError.js +7 -0
  28. package/src/errors/ControllerError.js +14 -0
  29. package/src/errors/DatabaseError.js +37 -0
  30. package/src/errors/GraphError.js +7 -0
  31. package/src/errors/ModelError.js +12 -0
  32. package/src/errors/NotFoundError.js +7 -0
  33. package/src/errors/NotImplementedError.js +7 -0
  34. package/src/errors/QueryBuilderError.js +7 -0
  35. package/src/errors/RelationError.js +21 -0
  36. package/src/errors/ResponseError.js +56 -0
  37. package/src/errors/ValidationError.js +7 -0
  38. package/src/errors/index.js +13 -0
  39. package/src/graph/DitoGraphProcessor.js +213 -0
  40. package/src/graph/expression.js +53 -0
  41. package/src/graph/graph.js +258 -0
  42. package/src/graph/index.js +3 -0
  43. package/src/index.js +9 -0
  44. package/src/lib/EventEmitter.js +66 -0
  45. package/src/lib/KnexHelper.js +30 -0
  46. package/src/lib/index.js +2 -0
  47. package/src/middleware/attachLogger.js +8 -0
  48. package/src/middleware/createTransaction.js +33 -0
  49. package/src/middleware/extendContext.js +10 -0
  50. package/src/middleware/findRoute.js +20 -0
  51. package/src/middleware/handleConnectMiddleware.js +99 -0
  52. package/src/middleware/handleError.js +29 -0
  53. package/src/middleware/handleRoute.js +23 -0
  54. package/src/middleware/handleSession.js +77 -0
  55. package/src/middleware/handleUser.js +31 -0
  56. package/src/middleware/index.js +11 -0
  57. package/src/middleware/logRequests.js +125 -0
  58. package/src/middleware/setupRequestStorage.js +14 -0
  59. package/src/mixins/AssetMixin.js +78 -0
  60. package/src/mixins/SessionMixin.js +17 -0
  61. package/src/mixins/TimeStampedMixin.js +41 -0
  62. package/src/mixins/UserMixin.js +171 -0
  63. package/src/mixins/index.js +4 -0
  64. package/src/models/AssetModel.js +4 -0
  65. package/src/models/Model.js +1205 -0
  66. package/src/models/RelationAccessor.js +41 -0
  67. package/src/models/SessionModel.js +4 -0
  68. package/src/models/TimeStampedModel.js +4 -0
  69. package/src/models/UserModel.js +4 -0
  70. package/src/models/definitions/assets.js +5 -0
  71. package/src/models/definitions/filters.js +121 -0
  72. package/src/models/definitions/hooks.js +8 -0
  73. package/src/models/definitions/index.js +22 -0
  74. package/src/models/definitions/modifiers.js +5 -0
  75. package/src/models/definitions/options.js +5 -0
  76. package/src/models/definitions/properties.js +73 -0
  77. package/src/models/definitions/relations.js +5 -0
  78. package/src/models/definitions/schema.js +5 -0
  79. package/src/models/definitions/scopes.js +36 -0
  80. package/src/models/index.js +5 -0
  81. package/src/query/QueryBuilder.js +1077 -0
  82. package/src/query/QueryFilters.js +66 -0
  83. package/src/query/QueryParameters.js +79 -0
  84. package/src/query/Registry.js +29 -0
  85. package/src/query/index.js +3 -0
  86. package/src/schema/formats/_empty.js +4 -0
  87. package/src/schema/formats/_required.js +4 -0
  88. package/src/schema/formats/index.js +2 -0
  89. package/src/schema/index.js +5 -0
  90. package/src/schema/keywords/_computed.js +7 -0
  91. package/src/schema/keywords/_foreign.js +7 -0
  92. package/src/schema/keywords/_hidden.js +7 -0
  93. package/src/schema/keywords/_index.js +7 -0
  94. package/src/schema/keywords/_instanceof.js +45 -0
  95. package/src/schema/keywords/_primary.js +7 -0
  96. package/src/schema/keywords/_range.js +18 -0
  97. package/src/schema/keywords/_relate.js +13 -0
  98. package/src/schema/keywords/_specificType.js +7 -0
  99. package/src/schema/keywords/_unique.js +7 -0
  100. package/src/schema/keywords/_unsigned.js +7 -0
  101. package/src/schema/keywords/_validate.js +73 -0
  102. package/src/schema/keywords/index.js +12 -0
  103. package/src/schema/relations.js +324 -0
  104. package/src/schema/relations.test.js +177 -0
  105. package/src/schema/schema.js +289 -0
  106. package/src/schema/schema.test.js +720 -0
  107. package/src/schema/types/_asset.js +31 -0
  108. package/src/schema/types/_color.js +4 -0
  109. package/src/schema/types/index.js +2 -0
  110. package/src/services/Service.js +35 -0
  111. package/src/services/index.js +1 -0
  112. package/src/storage/AssetFile.js +81 -0
  113. package/src/storage/DiskStorage.js +114 -0
  114. package/src/storage/S3Storage.js +169 -0
  115. package/src/storage/Storage.js +231 -0
  116. package/src/storage/index.js +9 -0
  117. package/src/utils/duration.js +15 -0
  118. package/src/utils/emitter.js +8 -0
  119. package/src/utils/fs.js +10 -0
  120. package/src/utils/function.js +17 -0
  121. package/src/utils/function.test.js +77 -0
  122. package/src/utils/handler.js +17 -0
  123. package/src/utils/json.js +3 -0
  124. package/src/utils/model.js +35 -0
  125. package/src/utils/net.js +17 -0
  126. package/src/utils/object.js +82 -0
  127. package/src/utils/object.test.js +86 -0
  128. package/src/utils/scope.js +7 -0
  129. package/types/index.d.ts +3547 -0
  130. package/types/tests/application.test-d.ts +26 -0
  131. package/types/tests/controller.test-d.ts +113 -0
  132. package/types/tests/errors.test-d.ts +53 -0
  133. package/types/tests/fixtures.ts +19 -0
  134. package/types/tests/model.test-d.ts +193 -0
  135. package/types/tests/query-builder.test-d.ts +106 -0
  136. package/types/tests/relation.test-d.ts +83 -0
  137. package/types/tests/storage.test-d.ts +113 -0
@@ -0,0 +1,241 @@
1
+ import path from 'path'
2
+ import fs from 'fs/promises'
3
+ import pico from 'picocolors'
4
+ import {
5
+ isObject,
6
+ isArray,
7
+ isString,
8
+ deindent,
9
+ capitalize
10
+ } from '@ditojs/utils'
11
+ import {
12
+ getRelationClass,
13
+ isThroughRelationClass
14
+ } from '../../schema/relations.js'
15
+ import { exists } from '../../utils/fs.js'
16
+
17
+ const typeToKnex = {
18
+ number: 'double',
19
+ object: 'json',
20
+ array: 'json'
21
+ }
22
+
23
+ const defaultValues = {
24
+ 'now()': `knex.raw('CURRENT_TIMESTAMP')`
25
+ }
26
+
27
+ export async function createMigration(app, name, ...modelNames) {
28
+ const migrationDir = path.join(app.basePath, 'migrations')
29
+ const models = modelNames.map(modelName => {
30
+ const modelClass = app.models[modelName]
31
+ if (!modelClass) {
32
+ throw new Error(`Model class with name '${modelName}' does not exist`)
33
+ }
34
+ return modelClass
35
+ })
36
+ const tables = []
37
+ for (const modelClass of models) {
38
+ collectModelTables(modelClass, app, tables)
39
+ }
40
+ for (const modelClass of models) {
41
+ collectThroughTables(modelClass, app, tables)
42
+ }
43
+ const createTables = []
44
+ const dropTables = []
45
+ for (const { tableName, statements } of tables) {
46
+ createTables.push(deindent`
47
+ .createTable('${tableName}', table => {
48
+ ${statements.join('\n')}
49
+ })`)
50
+ dropTables.unshift(deindent`
51
+ .dropTableIfExists('${tableName}')`)
52
+ }
53
+ const getCode = tables =>
54
+ tables.length > 0
55
+ ? deindent`
56
+ await knex.schema
57
+ ${tables.join('\n')}`
58
+ : ''
59
+ const filename = `${getTimestamp()}_${name}.js`
60
+ const file = path.join(migrationDir, filename)
61
+ if (await exists(file)) {
62
+ // This should never happen, but let's be on the safe side here:
63
+ console.info(pico.red(`Migration '${filename}' already exists.`))
64
+ return false
65
+ } else {
66
+ await fs.writeFile(
67
+ file,
68
+ deindent`
69
+ /** @param {import('knex').Knex} knex */
70
+ export async function up(knex) {
71
+ ${getCode(createTables)}
72
+ }
73
+
74
+ /** @param {import('knex').Knex} knex */
75
+ export async function down(knex) {
76
+ ${getCode(dropTables)}
77
+ }
78
+ `
79
+ )
80
+ console.info(pico.cyan(`Migration '${filename}' successfully created.`))
81
+ return true
82
+ }
83
+ }
84
+
85
+ async function collectModelTables(modelClass, app, tables) {
86
+ const tableName = app.normalizeIdentifier(modelClass.tableName)
87
+ const { properties, relations } = modelClass.definition
88
+ const statements = []
89
+ tables.push({ tableName, statements })
90
+ const uniqueComposites = {}
91
+ for (const [name, property] of Object.entries(properties)) {
92
+ const column = app.normalizeIdentifier(name)
93
+ let {
94
+ description,
95
+ type,
96
+ specificType,
97
+ unsigned,
98
+ computed,
99
+ nullable,
100
+ required,
101
+ primary,
102
+ foreign,
103
+ unique,
104
+ index,
105
+ default: _default
106
+ } = property
107
+ const knexType = typeToKnex[type] || type
108
+ if (!computed) {
109
+ if (description) {
110
+ statements.push(`// ${description.replace(/\s{2,}/g, ' ').trim()}`)
111
+ }
112
+ if (isString(unique)) {
113
+ // To declare composite foreign keys as unique, you can give each
114
+ // property the same string value in the `unique` keywords, e.g.:
115
+ // `unique: 'customerId_name'
116
+ const composites = (
117
+ uniqueComposites[unique] ||
118
+ (uniqueComposites[unique] = [])
119
+ )
120
+ composites.push(column)
121
+ unique = false
122
+ }
123
+ const statement = primary
124
+ ? [`table.increments('${column}').primary()`]
125
+ : specificType
126
+ ? [`table.specificType('${column}', '${specificType}')`]
127
+ : [`table.${knexType}('${column}')`]
128
+ statement.push(
129
+ unsigned && 'unsigned()',
130
+ !primary && required && 'notNullable()',
131
+ nullable && 'nullable()',
132
+ unique && 'unique()',
133
+ index && 'index()'
134
+ )
135
+ if (_default !== undefined) {
136
+ let value = defaultValues[_default]
137
+ if (!value) {
138
+ value =
139
+ isArray(_default) || isObject(_default)
140
+ ? JSON.stringify(_default)
141
+ : _default
142
+ if (isString(value)) {
143
+ value = `'${value}'`
144
+ }
145
+ }
146
+ statement.push(`defaultTo(${value})`)
147
+ }
148
+ if (foreign) {
149
+ for (const relation of Object.values(relations)) {
150
+ // TODO: Support composite keys for foreign references:
151
+ // Use `asArray(from)`, `asArray(to)`
152
+ const { from, to, owner } = relation
153
+ const [fromClass, fromProperty] = from?.split('.') || []
154
+ if (fromProperty === name) {
155
+ if (fromClass !== modelClass.name) {
156
+ throw Error(`Invalid relation declaration: ${relation}`)
157
+ }
158
+ const [toClass, toProperty] = to?.split('.') || []
159
+ statement.push(
160
+ '\n',
161
+ `references('${app.normalizeIdentifier(toProperty)}')`,
162
+ `inTable('${app.normalizeIdentifier(toClass)}')`,
163
+ // Only relations that aren't owners of their data should cascade
164
+ // on delete.
165
+ // TODO: Find the reverse relation of this in the other class and
166
+ // only add `onDelete('CASCADE')` if that one's an owner.
167
+ !owner && `onDelete('CASCADE')`
168
+ )
169
+ }
170
+ }
171
+ }
172
+ statements.push(
173
+ statement
174
+ .filter(str => !!str)
175
+ .join('.')
176
+ .replace(/\.\n\./g, '\n .')
177
+ )
178
+ }
179
+ }
180
+ for (const composites of Object.values(uniqueComposites)) {
181
+ statements.push(
182
+ `table.unique([${
183
+ composites.map(column => `'${column}'`).join(', ')
184
+ }])`
185
+ )
186
+ }
187
+ }
188
+
189
+ async function collectThroughTables(modelClass, app, tables) {
190
+ const { relations } = modelClass.definition
191
+ for (const relation of Object.values(relations)) {
192
+ const { from, to, inverse } = relation
193
+ const relationClass = getRelationClass(relation.relation)
194
+ if (isThroughRelationClass(relationClass) && !inverse) {
195
+ // TODO: Support composite keys for foreign references:
196
+ // Use `asArray(from)`, `asArray(to)`
197
+ const [fromClass, fromProperty] = from?.split('.') || []
198
+ const [toClass, toProperty] = to?.split('.') || []
199
+ const statements = []
200
+ // See convertRelations()
201
+ const tableName = app.normalizeIdentifier(`${fromClass}${toClass}`)
202
+ const fromId = app.normalizeIdentifier(
203
+ `${fromClass}${capitalize(fromProperty)}`
204
+ )
205
+ const toId = app.normalizeIdentifier(
206
+ `${toClass}${capitalize(toProperty)}`
207
+ )
208
+ tables.push({ tableName, statements })
209
+ statements.push(`table.increments('id').primary()`)
210
+ statements.push(deindent`
211
+ table.integer('${fromId}').unsigned().index()
212
+ .references('${app.normalizeIdentifier(fromProperty)}')\\
213
+ .inTable('${app.normalizeIdentifier(fromClass)}')\\
214
+ .onDelete('CASCADE')`)
215
+ statements.push(deindent`
216
+ table.integer('${toId}').unsigned().index()
217
+ .references('${app.normalizeIdentifier(toProperty)}')\\
218
+ .inTable('${app.normalizeIdentifier(toClass)}')\\
219
+ .onDelete('CASCADE')`)
220
+ }
221
+ }
222
+ }
223
+
224
+ // Ensure that we have 2 places for each of the date segments.
225
+ function padDate(segment) {
226
+ return segment.toString().padStart(2, '0')
227
+ }
228
+
229
+ // Get a date object in the correct format, without requiring a full out library
230
+ // like "moment.js".
231
+ function getTimestamp() {
232
+ const d = new Date()
233
+ return (
234
+ d.getFullYear().toString() +
235
+ padDate(d.getMonth() + 1) +
236
+ padDate(d.getDate()) +
237
+ padDate(d.getHours()) +
238
+ padDate(d.getMinutes()) +
239
+ padDate(d.getSeconds())
240
+ )
241
+ }
@@ -0,0 +1,7 @@
1
+ export * from './createMigration.js'
2
+ export * from './listAssetConfig.js'
3
+ export * from './migrate.js'
4
+ export * from './rollback.js'
5
+ export * from './reset.js'
6
+ export * from './seed.js'
7
+ export * from './unlock.js'
@@ -0,0 +1,10 @@
1
+ import { formatJson } from '../../utils/json.js'
2
+
3
+ export async function listAssetConfig(app, ...args) {
4
+ const assetConfig = app.getAssetConfig({
5
+ models: args.length > 0 ? args : Object.keys(app.models),
6
+ normalizeDbNames: true
7
+ })
8
+ console.info(formatJson(assetConfig))
9
+ return true
10
+ }
@@ -0,0 +1,12 @@
1
+ import pico from 'picocolors'
2
+
3
+ export async function migrate(knex) {
4
+ const [batch, log] = await knex.migrate.latest()
5
+ console.info(
6
+ log.length === 0
7
+ ? pico.cyan('Already up to date')
8
+ : pico.green(`Batch ${batch} run: ${log.length} migrations\n`) +
9
+ pico.cyan(log.join('\n'))
10
+ )
11
+ return true
12
+ }
@@ -0,0 +1,23 @@
1
+ import pico from 'picocolors'
2
+ import { migrate } from './migrate.js'
3
+
4
+ export async function reset(knex) {
5
+ const batches = []
6
+ const migrations = []
7
+ while (true) {
8
+ const [batch, log] = await knex.migrate.rollback()
9
+ if (log.length === 0) break
10
+ batches.push(batch)
11
+ migrations.push(...log)
12
+ }
13
+ console.info(
14
+ migrations.length === 0
15
+ ? pico.cyan('Already at the base migration')
16
+ : pico.green(
17
+ `${batches.length > 1 ? 'Batches' : 'Batch'} ${batches} ` +
18
+ `rolled back: ${migrations.length} migrations\n`
19
+ ) + pico.cyan(migrations.join('\n'))
20
+ )
21
+ await migrate(knex)
22
+ return true
23
+ }
@@ -0,0 +1,12 @@
1
+ import pico from 'picocolors'
2
+
3
+ export async function rollback(knex) {
4
+ const [batch, log] = await knex.migrate.rollback()
5
+ console.info(
6
+ log.length === 0
7
+ ? pico.cyan('Already at the base migration')
8
+ : pico.green(`Batch ${batch} rolled back: ${log.length} migrations\n`) +
9
+ pico.cyan(log.join('\n'))
10
+ )
11
+ return true
12
+ }
@@ -0,0 +1,80 @@
1
+ import path from 'path'
2
+ import fs from 'fs/promises'
3
+ import pico from 'picocolors'
4
+ import util from 'util'
5
+ import pluralize from 'pluralize'
6
+ import { isFunction, isArray, camelize } from '@ditojs/utils'
7
+
8
+ export async function seed(app) {
9
+ const seedDir = path.join(app.basePath, 'seeds')
10
+ const files = await fs.readdir(seedDir)
11
+ const seeds = []
12
+ // Create a lookup table with sort indices per model name.
13
+ const modelIndices = Object.keys(app.models).reduce(
14
+ (indices, name, index) => {
15
+ indices[name] = index
16
+ return indices
17
+ },
18
+ {}
19
+ )
20
+ // Collect all seed, and separate between seed functions and model see data:
21
+ for (const file of files) {
22
+ const { name, ext, base } = path.parse(file)
23
+ if (!name.startsWith('.') && ['.js', '.json'].includes(ext)) {
24
+ const object = await import(path.resolve(seedDir, file))
25
+ const seed = object.default || object
26
+ // Try to determine the related model from the seed name, and use it also
27
+ // to determine seed sequence based on its index in `app.models`.
28
+ const modelClass = (
29
+ app.models[name] ||
30
+ app.models[camelize(pluralize.singular(name), true)]
31
+ )
32
+ const index = modelClass ? modelIndices[modelClass.name] : Infinity
33
+ seeds.push({
34
+ base,
35
+ seed,
36
+ modelClass,
37
+ index
38
+ })
39
+ }
40
+ }
41
+ // Now sort the seed model data according to `app.models` sorting,
42
+ // as determined by `Application.sortModels()`:
43
+ seeds.sort((entry1, entry2) => entry1.index - entry2.index)
44
+ for (const { base, seed, modelClass } of seeds) {
45
+ await handleSeed(app, base, seed, modelClass)
46
+ }
47
+ return true
48
+ }
49
+
50
+ async function handleSeed(app, base, seed, modelClass) {
51
+ try {
52
+ let res
53
+ if (isFunction(seed)) {
54
+ res = await seed(app.models)
55
+ } else if (modelClass) {
56
+ await modelClass.truncate({ cascade: true })
57
+ res = await modelClass.insertGraph(seed)
58
+ }
59
+ if (isArray(res)) {
60
+ console.info(
61
+ pico.green(`${base}:`),
62
+ pico.cyan(`${res.length} seed records created.`)
63
+ )
64
+ } else {
65
+ console.info(
66
+ pico.red(`${base}:`),
67
+ pico.cyan('No seed records created.')
68
+ )
69
+ }
70
+ } catch (err) {
71
+ console.error(
72
+ pico.red(`${base}:`),
73
+ util.inspect(err, {
74
+ colors: true,
75
+ depth: null,
76
+ maxArrayLength: null
77
+ })
78
+ )
79
+ }
80
+ }
@@ -0,0 +1,9 @@
1
+ import pico from 'picocolors'
2
+
3
+ export async function unlock(knex) {
4
+ await knex.migrate.forceFreeMigrationsLock()
5
+ console.info(
6
+ pico.green(`Successfully unlocked the migrations lock table`)
7
+ )
8
+ return true
9
+ }
@@ -0,0 +1,72 @@
1
+ #!/usr/bin/env node
2
+
3
+ import path from 'path'
4
+ import fs from 'fs'
5
+ import pico from 'picocolors'
6
+ import Knex from 'knex'
7
+ import { isPlainObject, isFunction, camelize } from '@ditojs/utils'
8
+ import * as db from './db/index.js'
9
+ import startConsole from './console.js'
10
+
11
+ const commands = { db, console: startConsole }
12
+
13
+ function getCommand(commands, parts) {
14
+ const part = parts.shift()
15
+ return commands && part
16
+ ? getCommand(commands[camelize(part)], parts)
17
+ : commands
18
+ }
19
+
20
+ function setSilent(silent) {
21
+ const wasSilent = process.env.DITO_SILENT
22
+ process.env.DITO_SILENT = silent
23
+ return wasSilent
24
+ }
25
+
26
+ async function execute() {
27
+ try {
28
+ // Dynamically load app or config from the path provided package.json script
29
+ const [, , command, importPath, ...args] = process.argv
30
+ const execute = command && getCommand(commands, command.split(':'))
31
+ if (!isFunction(execute)) {
32
+ throw new Error(`Unknown command: ${command}`)
33
+ }
34
+ const silent = setSilent(true)
35
+ let arg
36
+ try {
37
+ arg = (await import(path.resolve(importPath))).default
38
+ } finally {
39
+ setSilent(silent)
40
+ }
41
+ if (isFunction(arg)) {
42
+ arg = await arg()
43
+ }
44
+ if (isPlainObject(arg) && arg.knex) {
45
+ // A config object with a knex field was passed in, create a knex object
46
+ // from it to pass on to the execute function.
47
+ arg = Knex(arg.knex)
48
+ }
49
+ const res = await execute(arg, ...args)
50
+ process.exit(res === true ? 0 : 1)
51
+ } catch (err) {
52
+ if (err instanceof Error) {
53
+ console.error(
54
+ pico.red(`${err.detail ? `${err.detail}\n` : ''}${err.stack}`)
55
+ )
56
+ } else {
57
+ console.error(pico.red(err))
58
+ }
59
+ process.exit(1)
60
+ }
61
+ }
62
+
63
+ // Start the console if `node ./cli/index.js`
64
+
65
+ // See module was not imported but called directly
66
+ const path1 = fs.realpathSync(import.meta.url.replace(/^file:\/\//, ''))
67
+ const path2 = fs.realpathSync(process.argv[1])
68
+ if (path1 === path2) {
69
+ execute()
70
+ }
71
+
72
+ export default execute