ultimabase 0.0.0-stage → 0.1.1
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.
- package/CHANGELOG.md +13 -0
- package/README.md +4 -2
- package/dist/access.d.ts +62 -0
- package/dist/access.d.ts.map +1 -0
- package/dist/access.js +258 -0
- package/dist/adapter-errors.d.ts +43 -0
- package/dist/adapter-errors.d.ts.map +1 -0
- package/dist/adapter-errors.js +72 -0
- package/dist/adapter-json.d.ts +20 -0
- package/dist/adapter-json.d.ts.map +1 -0
- package/dist/adapter-json.js +2 -0
- package/dist/adapter-migrations.d.ts +90 -0
- package/dist/adapter-migrations.d.ts.map +1 -0
- package/dist/adapter-migrations.js +1 -0
- package/dist/adapter.d.ts +333 -0
- package/dist/adapter.d.ts.map +1 -0
- package/dist/adapter.js +5 -0
- package/dist/aggregate.d.ts +31 -0
- package/dist/aggregate.d.ts.map +1 -0
- package/dist/aggregate.js +99 -0
- package/dist/app-internals.d.ts +10 -0
- package/dist/app-internals.d.ts.map +1 -0
- package/dist/app-internals.js +12 -0
- package/dist/app-options.d.ts +22 -0
- package/dist/app-options.d.ts.map +1 -0
- package/dist/app-options.js +3 -0
- package/dist/cascade.d.ts +47 -0
- package/dist/cascade.d.ts.map +1 -0
- package/dist/cascade.js +209 -0
- package/dist/cli-config.d.ts +6 -0
- package/dist/cli-config.d.ts.map +1 -0
- package/dist/cli-config.js +45 -0
- package/dist/cli-deploy.d.ts +14 -0
- package/dist/cli-deploy.d.ts.map +1 -0
- package/dist/cli-deploy.js +193 -0
- package/dist/cli-main.d.ts +15 -0
- package/dist/cli-main.d.ts.map +1 -0
- package/dist/cli-main.js +155 -0
- package/dist/cli-migrate.d.ts +20 -0
- package/dist/cli-migrate.d.ts.map +1 -0
- package/dist/cli-migrate.js +105 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +4 -0
- package/dist/complete.d.ts +13 -0
- package/dist/complete.d.ts.map +1 -0
- package/dist/complete.js +60 -0
- package/dist/console-logger.d.ts +3 -0
- package/dist/console-logger.d.ts.map +1 -0
- package/dist/console-logger.js +62 -0
- package/dist/create-many.d.ts +7 -0
- package/dist/create-many.d.ts.map +1 -0
- package/dist/create-many.js +88 -0
- package/dist/decimal.d.ts +23 -0
- package/dist/decimal.d.ts.map +1 -0
- package/dist/decimal.js +50 -0
- package/dist/define-app.d.ts +59 -0
- package/dist/define-app.d.ts.map +1 -0
- package/dist/define-app.js +5 -0
- package/dist/define-enum.d.ts +6 -0
- package/dist/define-enum.d.ts.map +1 -0
- package/dist/define-enum.js +18 -0
- package/dist/define-function.d.ts +25 -0
- package/dist/define-function.d.ts.map +1 -0
- package/dist/define-function.js +111 -0
- package/dist/define-model.d.ts +48 -0
- package/dist/define-model.d.ts.map +1 -0
- package/dist/define-model.js +85 -0
- package/dist/definition.d.ts +179 -0
- package/dist/definition.d.ts.map +1 -0
- package/dist/definition.js +165 -0
- package/dist/delete-many.d.ts +13 -0
- package/dist/delete-many.d.ts.map +1 -0
- package/dist/delete-many.js +58 -0
- package/dist/engine.d.ts +16 -0
- package/dist/engine.d.ts.map +1 -0
- package/dist/engine.js +174 -0
- package/dist/errors.d.ts +50 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +43 -0
- package/dist/exact.d.ts +20 -0
- package/dist/exact.d.ts.map +1 -0
- package/dist/exact.js +1 -0
- package/dist/feed.d.ts +6 -0
- package/dist/feed.d.ts.map +1 -0
- package/dist/feed.js +33 -0
- package/dist/field-codec.d.ts +11 -0
- package/dist/field-codec.d.ts.map +1 -0
- package/dist/field-codec.js +48 -0
- package/dist/field-rules.d.ts +57 -0
- package/dist/field-rules.d.ts.map +1 -0
- package/dist/field-rules.js +1 -0
- package/dist/field-type.d.ts +51 -0
- package/dist/field-type.d.ts.map +1 -0
- package/dist/field-type.js +106 -0
- package/dist/fields.d.ts +295 -0
- package/dist/fields.d.ts.map +1 -0
- package/dist/fields.js +1 -0
- package/dist/function.d.ts +126 -0
- package/dist/function.d.ts.map +1 -0
- package/dist/function.js +1 -0
- package/dist/functions.d.ts +18 -0
- package/dist/functions.d.ts.map +1 -0
- package/dist/functions.js +99 -0
- package/dist/group-by.d.ts +10 -0
- package/dist/group-by.d.ts.map +1 -0
- package/dist/group-by.js +145 -0
- package/dist/group-having.d.ts +8 -0
- package/dist/group-having.d.ts.map +1 -0
- package/dist/group-having.js +97 -0
- package/dist/hooks.d.ts +41 -0
- package/dist/hooks.d.ts.map +1 -0
- package/dist/hooks.js +149 -0
- package/dist/hosts.d.ts +3 -0
- package/dist/hosts.d.ts.map +1 -0
- package/dist/hosts.js +8 -0
- package/dist/identify.d.ts +15 -0
- package/dist/identify.d.ts.map +1 -0
- package/dist/identify.js +71 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +12 -0
- package/dist/internal.d.ts +28 -0
- package/dist/internal.d.ts.map +1 -0
- package/dist/internal.js +39 -0
- package/dist/lifecycle.d.ts +21 -0
- package/dist/lifecycle.d.ts.map +1 -0
- package/dist/lifecycle.js +149 -0
- package/dist/list-update.d.ts +12 -0
- package/dist/list-update.d.ts.map +1 -0
- package/dist/list-update.js +52 -0
- package/dist/live.d.ts +37 -0
- package/dist/live.d.ts.map +1 -0
- package/dist/live.js +139 -0
- package/dist/manifest.d.ts +11 -0
- package/dist/manifest.d.ts.map +1 -0
- package/dist/manifest.js +60 -0
- package/dist/memory-aggregate.d.ts +7 -0
- package/dist/memory-aggregate.d.ts.map +1 -0
- package/dist/memory-aggregate.js +107 -0
- package/dist/memory-conditions.d.ts +8 -0
- package/dist/memory-conditions.d.ts.map +1 -0
- package/dist/memory-conditions.js +82 -0
- package/dist/memory-draft.d.ts +34 -0
- package/dist/memory-draft.d.ts.map +1 -0
- package/dist/memory-draft.js +255 -0
- package/dist/memory-json.d.ts +6 -0
- package/dist/memory-json.d.ts.map +1 -0
- package/dist/memory-json.js +86 -0
- package/dist/memory-read.d.ts +45 -0
- package/dist/memory-read.d.ts.map +1 -0
- package/dist/memory-read.js +244 -0
- package/dist/memory-search.d.ts +16 -0
- package/dist/memory-search.d.ts.map +1 -0
- package/dist/memory-search.js +110 -0
- package/dist/memory-values.d.ts +23 -0
- package/dist/memory-values.d.ts.map +1 -0
- package/dist/memory-values.js +82 -0
- package/dist/memory.d.ts +4 -0
- package/dist/memory.d.ts.map +1 -0
- package/dist/memory.js +157 -0
- package/dist/migrate.d.ts +22 -0
- package/dist/migrate.d.ts.map +1 -0
- package/dist/migrate.js +131 -0
- package/dist/model.d.ts +239 -0
- package/dist/model.d.ts.map +1 -0
- package/dist/model.js +1 -0
- package/dist/nested-write.d.ts +64 -0
- package/dist/nested-write.d.ts.map +1 -0
- package/dist/nested-write.js +274 -0
- package/dist/number-update.d.ts +18 -0
- package/dist/number-update.d.ts.map +1 -0
- package/dist/number-update.js +96 -0
- package/dist/plugin-app.d.ts +48 -0
- package/dist/plugin-app.d.ts.map +1 -0
- package/dist/plugin-app.js +230 -0
- package/dist/plugin-code.d.ts +17 -0
- package/dist/plugin-code.d.ts.map +1 -0
- package/dist/plugin-code.js +125 -0
- package/dist/plugin-use.d.ts +22 -0
- package/dist/plugin-use.d.ts.map +1 -0
- package/dist/plugin-use.js +161 -0
- package/dist/plugin.d.ts +357 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/plugin.js +17 -0
- package/dist/query-aggregate.d.ts +82 -0
- package/dist/query-aggregate.d.ts.map +1 -0
- package/dist/query-aggregate.js +1 -0
- package/dist/query-args.d.ts +41 -0
- package/dist/query-args.d.ts.map +1 -0
- package/dist/query-args.js +263 -0
- package/dist/query-bulk.d.ts +35 -0
- package/dist/query-bulk.d.ts.map +1 -0
- package/dist/query-bulk.js +1 -0
- package/dist/query-conditions.d.ts +9 -0
- package/dist/query-conditions.d.ts.map +1 -0
- package/dist/query-conditions.js +46 -0
- package/dist/query-context.d.ts +32 -0
- package/dist/query-context.d.ts.map +1 -0
- package/dist/query-context.js +55 -0
- package/dist/query-filter.d.ts +46 -0
- package/dist/query-filter.d.ts.map +1 -0
- package/dist/query-filter.js +1 -0
- package/dist/query-group.d.ts +82 -0
- package/dist/query-group.d.ts.map +1 -0
- package/dist/query-group.js +1 -0
- package/dist/query-json.d.ts +9 -0
- package/dist/query-json.d.ts.map +1 -0
- package/dist/query-json.js +66 -0
- package/dist/query-order.d.ts +15 -0
- package/dist/query-order.d.ts.map +1 -0
- package/dist/query-order.js +115 -0
- package/dist/query-project.d.ts +15 -0
- package/dist/query-project.d.ts.map +1 -0
- package/dist/query-project.js +45 -0
- package/dist/query-search.d.ts +24 -0
- package/dist/query-search.d.ts.map +1 -0
- package/dist/query-search.js +2 -0
- package/dist/query-values.d.ts +28 -0
- package/dist/query-values.d.ts.map +1 -0
- package/dist/query-values.js +91 -0
- package/dist/query-where.d.ts +46 -0
- package/dist/query-where.d.ts.map +1 -0
- package/dist/query-where.js +200 -0
- package/dist/query-write.d.ts +14 -0
- package/dist/query-write.d.ts.map +1 -0
- package/dist/query-write.js +260 -0
- package/dist/query.d.ts +254 -0
- package/dist/query.d.ts.map +1 -0
- package/dist/query.js +1 -0
- package/dist/record.d.ts +257 -0
- package/dist/record.d.ts.map +1 -0
- package/dist/record.js +1 -0
- package/dist/recorder.d.ts +32 -0
- package/dist/recorder.d.ts.map +1 -0
- package/dist/recorder.js +152 -0
- package/dist/relation-write.d.ts +41 -0
- package/dist/relation-write.d.ts.map +1 -0
- package/dist/relation-write.js +112 -0
- package/dist/relations.d.ts +42 -0
- package/dist/relations.d.ts.map +1 -0
- package/dist/relations.js +119 -0
- package/dist/rules.d.ts +97 -0
- package/dist/rules.d.ts.map +1 -0
- package/dist/rules.js +71 -0
- package/dist/running.d.ts +29 -0
- package/dist/running.d.ts.map +1 -0
- package/dist/running.js +39 -0
- package/dist/schema-indexes.d.ts +11 -0
- package/dist/schema-indexes.d.ts.map +1 -0
- package/dist/schema-indexes.js +59 -0
- package/dist/schema-virtuals.d.ts +19 -0
- package/dist/schema-virtuals.d.ts.map +1 -0
- package/dist/schema-virtuals.js +100 -0
- package/dist/schema.d.ts +35 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +267 -0
- package/dist/scope.d.ts +46 -0
- package/dist/scope.d.ts.map +1 -0
- package/dist/scope.js +82 -0
- package/dist/scratch.d.ts +14 -0
- package/dist/scratch.d.ts.map +1 -0
- package/dist/scratch.js +10 -0
- package/dist/serve.d.ts +2 -0
- package/dist/serve.d.ts.map +1 -0
- package/dist/serve.js +4 -0
- package/dist/symbols.d.ts +10 -0
- package/dist/symbols.d.ts.map +1 -0
- package/dist/symbols.js +1 -0
- package/dist/transaction.d.ts +28 -0
- package/dist/transaction.d.ts.map +1 -0
- package/dist/transaction.js +156 -0
- package/dist/types.d.ts +58 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -0
- package/dist/update-many.d.ts +7 -0
- package/dist/update-many.d.ts.map +1 -0
- package/dist/update-many.js +182 -0
- package/dist/uuid.d.ts +7 -0
- package/dist/uuid.d.ts.map +1 -0
- package/dist/uuid.js +48 -0
- package/dist/validation.d.ts +24 -0
- package/dist/validation.d.ts.map +1 -0
- package/dist/validation.js +72 -0
- package/dist/virtual-read.d.ts +9 -0
- package/dist/virtual-read.d.ts.map +1 -0
- package/dist/virtual-read.js +145 -0
- package/dist/virtual.d.ts +142 -0
- package/dist/virtual.d.ts.map +1 -0
- package/dist/virtual.js +1 -0
- package/dist/write-context.d.ts +46 -0
- package/dist/write-context.d.ts.map +1 -0
- package/dist/write-context.js +102 -0
- package/dist/write-data.d.ts +26 -0
- package/dist/write-data.d.ts.map +1 -0
- package/dist/write-data.js +144 -0
- package/dist/write-plan.d.ts +107 -0
- package/dist/write-plan.d.ts.map +1 -0
- package/dist/write-plan.js +170 -0
- package/dist/write-target.d.ts +17 -0
- package/dist/write-target.d.ts.map +1 -0
- package/dist/write-target.js +59 -0
- package/dist/writes.d.ts +23 -0
- package/dist/writes.d.ts.map +1 -0
- package/dist/writes.js +246 -0
- package/dist/writing.d.ts +10 -0
- package/dist/writing.d.ts.map +1 -0
- package/dist/writing.js +66 -0
- package/package.json +42 -3
- package/src/access.ts +329 -0
- package/src/adapter-errors.ts +89 -0
- package/src/adapter-json.ts +21 -0
- package/src/adapter-migrations.ts +98 -0
- package/src/adapter.ts +363 -0
- package/src/aggregate.ts +136 -0
- package/src/app-internals.ts +26 -0
- package/src/app-options.ts +24 -0
- package/src/cascade.ts +297 -0
- package/src/cli-config.ts +45 -0
- package/src/cli-deploy.ts +235 -0
- package/src/cli-main.ts +166 -0
- package/src/cli-migrate.ts +143 -0
- package/src/cli.ts +5 -0
- package/src/complete.ts +73 -0
- package/src/console-logger.ts +87 -0
- package/src/create-many.ts +126 -0
- package/src/decimal.ts +74 -0
- package/src/define-app.ts +108 -0
- package/src/define-enum.ts +26 -0
- package/src/define-function.ts +188 -0
- package/src/define-model.ts +220 -0
- package/src/definition.ts +352 -0
- package/src/delete-many.ts +76 -0
- package/src/engine.ts +281 -0
- package/src/errors.ts +97 -0
- package/src/exact.ts +49 -0
- package/src/feed.ts +39 -0
- package/src/field-codec.ts +72 -0
- package/src/field-rules.ts +77 -0
- package/src/field-type.ts +219 -0
- package/src/fields.ts +383 -0
- package/src/function.ts +143 -0
- package/src/functions.ts +154 -0
- package/src/group-by.ts +186 -0
- package/src/group-having.ts +143 -0
- package/src/hooks.ts +215 -0
- package/src/hosts.ts +11 -0
- package/src/identify.ts +95 -0
- package/src/index.ts +125 -0
- package/src/internal.ts +68 -0
- package/src/lifecycle.ts +214 -0
- package/src/list-update.ts +71 -0
- package/src/live.ts +199 -0
- package/src/manifest.ts +71 -0
- package/src/memory-aggregate.ts +162 -0
- package/src/memory-conditions.ts +88 -0
- package/src/memory-draft.ts +319 -0
- package/src/memory-json.ts +91 -0
- package/src/memory-read.ts +311 -0
- package/src/memory-search.ts +149 -0
- package/src/memory-values.ts +106 -0
- package/src/memory.ts +173 -0
- package/src/migrate.ts +152 -0
- package/src/model.ts +312 -0
- package/src/nested-write.ts +390 -0
- package/src/number-update.ts +142 -0
- package/src/plugin-app.ts +308 -0
- package/src/plugin-code.ts +158 -0
- package/src/plugin-use.ts +217 -0
- package/src/plugin.ts +400 -0
- package/src/query-aggregate.ts +96 -0
- package/src/query-args.ts +375 -0
- package/src/query-bulk.ts +53 -0
- package/src/query-conditions.ts +53 -0
- package/src/query-context.ts +104 -0
- package/src/query-filter.ts +54 -0
- package/src/query-group.ts +137 -0
- package/src/query-json.ts +90 -0
- package/src/query-order.ts +158 -0
- package/src/query-project.ts +73 -0
- package/src/query-search.ts +31 -0
- package/src/query-values.ts +131 -0
- package/src/query-where.ts +318 -0
- package/src/query-write.ts +360 -0
- package/src/query.ts +400 -0
- package/src/record.ts +379 -0
- package/src/recorder.ts +212 -0
- package/src/relation-write.ts +176 -0
- package/src/relations.ts +165 -0
- package/src/rules.ts +188 -0
- package/src/running.ts +62 -0
- package/src/schema-indexes.ts +88 -0
- package/src/schema-virtuals.ts +134 -0
- package/src/schema.ts +367 -0
- package/src/scope.ts +123 -0
- package/src/scratch.ts +28 -0
- package/src/serve.ts +5 -0
- package/src/symbols.ts +11 -0
- package/src/transaction.ts +211 -0
- package/src/types.ts +79 -0
- package/src/update-many.ts +268 -0
- package/src/uuid.ts +51 -0
- package/src/validation.ts +107 -0
- package/src/virtual-read.ts +198 -0
- package/src/virtual.ts +187 -0
- package/src/write-context.ts +170 -0
- package/src/write-data.ts +188 -0
- package/src/write-plan.ts +273 -0
- package/src/write-target.ts +92 -0
- package/src/writes.ts +361 -0
- package/src/writing.ts +93 -0
package/src/cli-main.ts
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
// The `ultimabase` command (D-105): `dev`, `start`, `sync`, `reset` and
|
|
2
|
+
// `migrate`.
|
|
3
|
+
import { spawn } from 'node:child_process'
|
|
4
|
+
import { dirname, extname, join } from 'node:path'
|
|
5
|
+
import { fileURLToPath } from 'node:url'
|
|
6
|
+
import { parseArgs } from 'node:util'
|
|
7
|
+
import { configPath, load } from './cli-config.ts'
|
|
8
|
+
import { migrateDeploy, migrateStatus, reset } from './cli-deploy.ts'
|
|
9
|
+
import { migrateCreate, sync } from './cli-migrate.ts'
|
|
10
|
+
import { Refused } from './errors.ts'
|
|
11
|
+
|
|
12
|
+
const usage = `Usage: ultimabase <command> [--config <path>]
|
|
13
|
+
|
|
14
|
+
dev Start the application, sync its development
|
|
15
|
+
database, and restart it when a file changes.
|
|
16
|
+
start Start the application in production.
|
|
17
|
+
sync [--force] Sync the development database to the models.
|
|
18
|
+
reset [--force] Rebuild the development database from the
|
|
19
|
+
migrations: every table is dropped. --force
|
|
20
|
+
allows a database on another machine.
|
|
21
|
+
migrate create <name> Write the models' changes as a migration, or an
|
|
22
|
+
empty one for your SQL.
|
|
23
|
+
migrate deploy Apply the pending migrations.
|
|
24
|
+
migrate status Say what a deploy would do; exit 1 if anything.`
|
|
25
|
+
|
|
26
|
+
export type Io = {
|
|
27
|
+
readonly cwd: string
|
|
28
|
+
readonly out: (text: string) => void
|
|
29
|
+
readonly err: (text: string) => void
|
|
30
|
+
readonly now: () => Date
|
|
31
|
+
/** Runs Node with `args` until it exits, and returns its exit code. */
|
|
32
|
+
readonly node: (args: readonly string[]) => Promise<number>
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The file `dev` runs under `node --watch`: `serve.ts` beside the sources,
|
|
37
|
+
* `serve.js` in the published `dist`.
|
|
38
|
+
*/
|
|
39
|
+
const here = fileURLToPath(import.meta.url)
|
|
40
|
+
export const serveFile: string = join(dirname(here), `serve${extname(here)}`)
|
|
41
|
+
|
|
42
|
+
function runNode(args: readonly string[]): Promise<number> {
|
|
43
|
+
return new Promise((done, fail) => {
|
|
44
|
+
const child = spawn(process.execPath, args, { stdio: 'inherit' })
|
|
45
|
+
const forward = () => child.kill('SIGTERM')
|
|
46
|
+
process.on('SIGTERM', forward)
|
|
47
|
+
child.on('error', fail)
|
|
48
|
+
child.on('exit', (code) => {
|
|
49
|
+
process.off('SIGTERM', forward)
|
|
50
|
+
done(code ?? 1)
|
|
51
|
+
})
|
|
52
|
+
})
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export const processIo: Io = {
|
|
56
|
+
cwd: process.cwd(),
|
|
57
|
+
out: (text) => process.stdout.write(`${text}\n`),
|
|
58
|
+
err: (text) => process.stderr.write(`${text}\n`),
|
|
59
|
+
now: () => new Date(),
|
|
60
|
+
node: runNode,
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* An expected failure, a refusal or an error with a `code` (from Node,
|
|
65
|
+
* PostgreSQL or the kernel), prints its message; any other, a bug, its
|
|
66
|
+
* stack (D-105).
|
|
67
|
+
*/
|
|
68
|
+
function report(error: unknown, io: Io): number {
|
|
69
|
+
if (!(error instanceof Error)) io.err(String(error))
|
|
70
|
+
else if (
|
|
71
|
+
error.name === 'Refused' ||
|
|
72
|
+
typeof Reflect.get(error, 'code') === 'string'
|
|
73
|
+
)
|
|
74
|
+
io.err(error.message)
|
|
75
|
+
else io.err(error.stack ?? error.message)
|
|
76
|
+
return 1
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Reads `args`, refusing an option it does not know with its first sentence. */
|
|
80
|
+
function read(args: readonly string[]) {
|
|
81
|
+
try {
|
|
82
|
+
return parseArgs({
|
|
83
|
+
args: [...args],
|
|
84
|
+
allowPositionals: true,
|
|
85
|
+
options: {
|
|
86
|
+
config: { type: 'string' },
|
|
87
|
+
force: { type: 'boolean', default: false },
|
|
88
|
+
help: { type: 'boolean', default: false },
|
|
89
|
+
},
|
|
90
|
+
})
|
|
91
|
+
} catch (error) {
|
|
92
|
+
const message: unknown = Reflect.get(Object(error), 'message')
|
|
93
|
+
const first = String(message).replace(/\.( .*)?$/s, '')
|
|
94
|
+
throw new Refused(`${first}. Run \`npx ultimabase --help\`.`)
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function run(args: readonly string[], io: Io): Promise<number> {
|
|
99
|
+
const { values, positionals } = read(args)
|
|
100
|
+
const [command, sub, name, ...rest] = positionals
|
|
101
|
+
const known =
|
|
102
|
+
rest.length === 0 &&
|
|
103
|
+
(command === 'migrate'
|
|
104
|
+
? (sub === 'create' && name !== undefined) ||
|
|
105
|
+
(['deploy', 'status'].includes(sub ?? '') && name === undefined)
|
|
106
|
+
: ['dev', 'start', 'sync', 'reset'].includes(command ?? '') &&
|
|
107
|
+
sub === undefined)
|
|
108
|
+
if (values.help) {
|
|
109
|
+
io.out(usage)
|
|
110
|
+
return 0
|
|
111
|
+
}
|
|
112
|
+
if (!known) {
|
|
113
|
+
if (command !== undefined)
|
|
114
|
+
io.err(`Unknown command '${positionals.join(' ')}'.`)
|
|
115
|
+
io.err(usage)
|
|
116
|
+
return 1
|
|
117
|
+
}
|
|
118
|
+
const path = configPath(io.cwd, values.config)
|
|
119
|
+
if (command === 'dev') {
|
|
120
|
+
process.env['NODE_ENV'] ??= 'development'
|
|
121
|
+
return io.node(['--watch', serveFile, path])
|
|
122
|
+
}
|
|
123
|
+
if (command === 'start') process.env['NODE_ENV'] ??= 'production'
|
|
124
|
+
const app = await load(path)
|
|
125
|
+
const dir = join(dirname(path), 'migrations')
|
|
126
|
+
if (command === 'start') await app.app.start()
|
|
127
|
+
else if (command === 'sync') {
|
|
128
|
+
const said = await sync(app, values.force)
|
|
129
|
+
if (said !== undefined) io.out(said)
|
|
130
|
+
} else if (command === 'reset') {
|
|
131
|
+
for (const line of await reset(app, dir, values.force)) io.out(line)
|
|
132
|
+
} else if (sub === 'deploy') {
|
|
133
|
+
for (const line of await migrateDeploy(app, dir)) io.out(line)
|
|
134
|
+
} else if (sub === 'status') {
|
|
135
|
+
const { lines, ok } = await migrateStatus(app, dir)
|
|
136
|
+
for (const line of lines) io.out(line)
|
|
137
|
+
return ok ? 0 : 1
|
|
138
|
+
} else io.out(await migrateCreate(app, dir, name ?? '', io.now()))
|
|
139
|
+
return 0
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Runs the command `args` name, and returns its exit code. */
|
|
143
|
+
export async function main(
|
|
144
|
+
args: readonly string[],
|
|
145
|
+
io: Io = processIo,
|
|
146
|
+
): Promise<number> {
|
|
147
|
+
try {
|
|
148
|
+
return await run(args, io)
|
|
149
|
+
} catch (error) {
|
|
150
|
+
return report(error, io)
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Starts the application of `path`, for `dev` (D-105). */
|
|
155
|
+
export async function serve(
|
|
156
|
+
path: string | undefined,
|
|
157
|
+
io: Io = processIo,
|
|
158
|
+
): Promise<number> {
|
|
159
|
+
try {
|
|
160
|
+
const { app } = await load(configPath(io.cwd, path))
|
|
161
|
+
await app.start()
|
|
162
|
+
return 0
|
|
163
|
+
} catch (error) {
|
|
164
|
+
return report(error, io)
|
|
165
|
+
}
|
|
166
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
// `migrate create` and `sync` (D-105), and the migration files both
|
|
2
|
+
// `migrate` commands read.
|
|
3
|
+
import { createHash } from 'node:crypto'
|
|
4
|
+
import { existsSync } from 'node:fs'
|
|
5
|
+
import { mkdir, readdir, readFile, writeFile } from 'node:fs/promises'
|
|
6
|
+
import { join } from 'node:path'
|
|
7
|
+
import type { Migration, Statement } from './adapter.ts'
|
|
8
|
+
import type { Internals } from './app-internals.ts'
|
|
9
|
+
import { kernelLogger, schemaOf } from './lifecycle.ts'
|
|
10
|
+
import { Refused } from './errors.ts'
|
|
11
|
+
import { hashOf, migrationsOf, synced } from './migrate.ts'
|
|
12
|
+
|
|
13
|
+
export const header = '-- ultimabase schema '
|
|
14
|
+
|
|
15
|
+
/** For a statement a transaction refuses: `create index concurrently`. */
|
|
16
|
+
export const noTransaction = '-- ultimabase no transaction'
|
|
17
|
+
export const create = '`npx ultimabase migrate create <name>`'
|
|
18
|
+
|
|
19
|
+
export type Folder = {
|
|
20
|
+
readonly files: readonly Migration[]
|
|
21
|
+
readonly snapshot: unknown
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** The `.sql` files of `dir` in name order, and its `schema.json`. */
|
|
25
|
+
export async function read(dir: string): Promise<Folder> {
|
|
26
|
+
if (!existsSync(dir)) return { files: [], snapshot: undefined }
|
|
27
|
+
const names = (await readdir(dir))
|
|
28
|
+
.filter((name) => name.endsWith('.sql'))
|
|
29
|
+
.toSorted()
|
|
30
|
+
const files = await Promise.all(
|
|
31
|
+
names.map(async (name) => {
|
|
32
|
+
// A Windows checkout's line endings change neither checksum nor header.
|
|
33
|
+
const text = await readFile(join(dir, name), 'utf8')
|
|
34
|
+
const sql = text.replaceAll('\r\n', '\n')
|
|
35
|
+
const checksum = createHash('sha256').update(sql).digest('hex')
|
|
36
|
+
const [first = '', second] = sql.split('\n', 2)
|
|
37
|
+
const schema = first.startsWith(header)
|
|
38
|
+
? first.slice(header.length)
|
|
39
|
+
: undefined
|
|
40
|
+
return {
|
|
41
|
+
name,
|
|
42
|
+
sql,
|
|
43
|
+
checksum,
|
|
44
|
+
...(schema === undefined ? {} : { schema }),
|
|
45
|
+
transaction: second !== noTransaction,
|
|
46
|
+
}
|
|
47
|
+
}),
|
|
48
|
+
)
|
|
49
|
+
const json = join(dir, 'schema.json')
|
|
50
|
+
if (!existsSync(json)) {
|
|
51
|
+
if (files.length > 0) {
|
|
52
|
+
throw new Refused(
|
|
53
|
+
'migrations/ holds SQL files but no schema.json: restore it from version control.',
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
return { files, snapshot: undefined }
|
|
57
|
+
}
|
|
58
|
+
const snapshot: unknown = JSON.parse(await readFile(json, 'utf8'))
|
|
59
|
+
return { files, snapshot }
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** `yyyyMMddHHmmss`, in UTC. */
|
|
63
|
+
const stamp = (now: Date): string =>
|
|
64
|
+
now.toISOString().replaceAll(/[-:T]/g, '').slice(0, 'yyyyMMddHHmmss'.length)
|
|
65
|
+
|
|
66
|
+
function body(
|
|
67
|
+
hash: string,
|
|
68
|
+
statements: readonly Statement[],
|
|
69
|
+
none: string,
|
|
70
|
+
): string {
|
|
71
|
+
const blocks = statements.map(({ sql, loses, tries, rename }) =>
|
|
72
|
+
[
|
|
73
|
+
...(loses === undefined ? [] : [`-- ${loses}.`]),
|
|
74
|
+
...(tries === undefined ? [] : [`-- ${tries}.`]),
|
|
75
|
+
...(rename === undefined ? [] : [`-- a rename? write: ${rename.sql};`]),
|
|
76
|
+
`${sql};`,
|
|
77
|
+
].join('\n'),
|
|
78
|
+
)
|
|
79
|
+
const parts = blocks.length > 0 ? blocks : [none]
|
|
80
|
+
return `${header}${hash}\n\n${parts.join('\n\n')}\n`
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Writes the migration from `schema.json` to the models; with no model
|
|
85
|
+
* changed, a file for the developer's SQL, as a data migration.
|
|
86
|
+
*/
|
|
87
|
+
export async function migrateCreate(
|
|
88
|
+
{ options }: Internals,
|
|
89
|
+
dir: string,
|
|
90
|
+
name: string,
|
|
91
|
+
now: Date,
|
|
92
|
+
): Promise<string> {
|
|
93
|
+
if (!/^[a-z0-9_]+$/.test(name)) {
|
|
94
|
+
throw new Refused(
|
|
95
|
+
`"${name}" is not a migration name: use lowercase letters, digits and _.`,
|
|
96
|
+
)
|
|
97
|
+
}
|
|
98
|
+
const migrations = migrationsOf(options.database)
|
|
99
|
+
const schema = schemaOf(options)
|
|
100
|
+
const to = migrations.snapshot(schema)
|
|
101
|
+
const { snapshot: from } = await read(dir)
|
|
102
|
+
const hash = hashOf(to)
|
|
103
|
+
const file = `${stamp(now)}_${name}.sql`
|
|
104
|
+
await mkdir(dir, { recursive: true })
|
|
105
|
+
if (from !== undefined && hashOf(from) === hash) {
|
|
106
|
+
const sql = body(hash, [], '-- No model changed: write your SQL here.')
|
|
107
|
+
await writeFile(join(dir, file), sql, { flag: 'wx' })
|
|
108
|
+
return `Wrote migrations/${file}: no model changed, write your SQL in it.`
|
|
109
|
+
}
|
|
110
|
+
const statements = migrations.plan(from, schema)
|
|
111
|
+
const sql = body(hash, statements, '-- No statement: only names changed.')
|
|
112
|
+
await writeFile(join(dir, file), sql, { flag: 'wx' })
|
|
113
|
+
await writeFile(
|
|
114
|
+
join(dir, 'schema.json'),
|
|
115
|
+
`${JSON.stringify(to, null, '\t')}\n`,
|
|
116
|
+
)
|
|
117
|
+
return `Wrote migrations/${file}.`
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Syncs a development database, losing data only with `force`. */
|
|
121
|
+
export async function sync(
|
|
122
|
+
{ options }: Internals,
|
|
123
|
+
force: boolean,
|
|
124
|
+
): Promise<string | undefined> {
|
|
125
|
+
if (process.env['NODE_ENV'] === 'production') {
|
|
126
|
+
throw new Refused(
|
|
127
|
+
'sync changes only a development database: run `npx ultimabase migrate deploy` in production.',
|
|
128
|
+
)
|
|
129
|
+
}
|
|
130
|
+
const { database } = options
|
|
131
|
+
migrationsOf(database)
|
|
132
|
+
const schema = schemaOf(options)
|
|
133
|
+
const logger = kernelLogger(options)
|
|
134
|
+
await database.start(schema, logger)
|
|
135
|
+
try {
|
|
136
|
+
const ran = await synced(database, schema, logger, force)
|
|
137
|
+
return ran === undefined
|
|
138
|
+
? 'The database already matches the models.'
|
|
139
|
+
: undefined
|
|
140
|
+
} finally {
|
|
141
|
+
await database.stop()
|
|
142
|
+
}
|
|
143
|
+
}
|
package/src/cli.ts
ADDED
package/src/complete.ts
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// A user caller completed with its plugins' keys (D-121): once per object,
|
|
2
|
+
// each plugin's `caller` in the order of `plugins`, a key given by one only,
|
|
3
|
+
// a symbol key as a string one (D-126).
|
|
4
|
+
import { isObject } from './query-where.ts'
|
|
5
|
+
import type { Caller, Identity } from './rules.ts'
|
|
6
|
+
|
|
7
|
+
/** A plugin's `caller`, bound to its context. */
|
|
8
|
+
export type PluginCaller = {
|
|
9
|
+
readonly plugin: string
|
|
10
|
+
readonly caller: (identity: Identity) => Promise<unknown>
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export type Complete = (identity: Identity) => Promise<Caller>
|
|
14
|
+
|
|
15
|
+
// The keys every caller holds, which no plugin gives.
|
|
16
|
+
const own = new Set<PropertyKey>(['kind', 'user'])
|
|
17
|
+
|
|
18
|
+
async function completed(
|
|
19
|
+
identity: Identity,
|
|
20
|
+
plugins: readonly PluginCaller[],
|
|
21
|
+
): Promise<Caller> {
|
|
22
|
+
const keys: { [key: PropertyKey]: unknown } = {}
|
|
23
|
+
const givers = new Map<PropertyKey, string>()
|
|
24
|
+
for (const { plugin, caller } of plugins) {
|
|
25
|
+
const given = await caller(identity)
|
|
26
|
+
if (!isObject(given)) {
|
|
27
|
+
throw new TypeError(`${plugin}.caller returns an object of keys.`)
|
|
28
|
+
}
|
|
29
|
+
for (const key of Reflect.ownKeys(given)) {
|
|
30
|
+
if (!Object.prototype.propertyIsEnumerable.call(given, key)) continue
|
|
31
|
+
const other = own.has(key) ? 'the kernel' : givers.get(key)
|
|
32
|
+
if (other !== undefined) {
|
|
33
|
+
throw new Error(
|
|
34
|
+
`${plugin}.caller gives the caller ${String(key)}, which ${other} gives; a key has one plugin.`,
|
|
35
|
+
)
|
|
36
|
+
}
|
|
37
|
+
givers.set(key, plugin)
|
|
38
|
+
const value: unknown = Reflect.get(given, key)
|
|
39
|
+
keys[key] = value
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
// Unnecessary only while no plugin declares a key: once one does, the
|
|
43
|
+
// kernel's types cannot see it given (D-121). caller-keys-typing proves it.
|
|
44
|
+
// oxlint-disable-next-line typescript/no-unnecessary-type-assertion -- see above.
|
|
45
|
+
return Object.freeze({ ...identity, ...keys }) as Caller
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Completes a user caller the first time the kernel receives it; `plugins`
|
|
50
|
+
* is read on each call, filled once their contexts exist.
|
|
51
|
+
*/
|
|
52
|
+
export function completer(plugins: readonly PluginCaller[]): Complete {
|
|
53
|
+
const pending = new WeakMap<object, Promise<Caller>>()
|
|
54
|
+
const done = new WeakSet<object>()
|
|
55
|
+
// A completed caller, or a user one when no plugin gives a key.
|
|
56
|
+
const kept = (identity: Identity): identity is Caller & Identity =>
|
|
57
|
+
done.has(identity)
|
|
58
|
+
return async (identity) => {
|
|
59
|
+
if (identity.kind !== 'user') return identity
|
|
60
|
+
if (plugins.length === 0) done.add(identity)
|
|
61
|
+
if (kept(identity)) return identity
|
|
62
|
+
const known = pending.get(identity)
|
|
63
|
+
if (known !== undefined) return known
|
|
64
|
+
const caller = completed(identity, plugins).then((result) => {
|
|
65
|
+
done.add(result)
|
|
66
|
+
return result
|
|
67
|
+
})
|
|
68
|
+
pending.set(identity, caller)
|
|
69
|
+
// A failure is not kept: the next request tries again.
|
|
70
|
+
caller.catch(() => pending.delete(identity))
|
|
71
|
+
return caller
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// The logger the kernel writes to when the application gives none (D-58,
|
|
2
|
+
// D-88, D-93): a readable line in a terminal, one JSON line with pino's
|
|
3
|
+
// fields everywhere else.
|
|
4
|
+
import { styleText } from 'node:util'
|
|
5
|
+
import type { Logger } from './app-options.ts'
|
|
6
|
+
|
|
7
|
+
type Level = 'info' | 'warn' | 'error'
|
|
8
|
+
|
|
9
|
+
const colors = { info: 'green', warn: 'yellow', error: 'red' } as const
|
|
10
|
+
|
|
11
|
+
/** An error as pino's `err` serializer writes it. */
|
|
12
|
+
function serialized(value: unknown): unknown {
|
|
13
|
+
if (!(value instanceof Error)) return value
|
|
14
|
+
return { type: value.name, message: value.message, stack: value.stack }
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function json(level: Level, fields: object, msg: string | undefined): string {
|
|
18
|
+
return JSON.stringify({
|
|
19
|
+
level,
|
|
20
|
+
time: Date.now(),
|
|
21
|
+
...Object.fromEntries(
|
|
22
|
+
Object.entries(fields).map(([key, value]) => [key, serialized(value)]),
|
|
23
|
+
),
|
|
24
|
+
...(msg === undefined ? {} : { msg }),
|
|
25
|
+
})
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** A value as `key=value` shows it: quoted when it would read ambiguously. */
|
|
29
|
+
function shown(value: unknown): string {
|
|
30
|
+
if (typeof value === 'string' && /^[^\s"=]+$/u.test(value)) return value
|
|
31
|
+
return JSON.stringify(value) ?? String(value)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** `10:42:07 INFO [cache] Warm. size=2`, then each error's stack, indented. */
|
|
35
|
+
function readable(
|
|
36
|
+
level: Level,
|
|
37
|
+
fields: object,
|
|
38
|
+
msg: string | undefined,
|
|
39
|
+
stream: NodeJS.WriteStream,
|
|
40
|
+
): string {
|
|
41
|
+
const { name, ...rest }: { readonly name?: unknown } = fields
|
|
42
|
+
const errors = Object.values(rest).filter((value) => value instanceof Error)
|
|
43
|
+
const details = Object.entries(rest)
|
|
44
|
+
.filter(([, value]) => !(value instanceof Error))
|
|
45
|
+
.map(([key, value]) => `${key}=${shown(value)}`)
|
|
46
|
+
const line = [
|
|
47
|
+
styleText('dim', new Date().toTimeString().slice(0, 8), { stream }),
|
|
48
|
+
styleText(colors[level], level.toUpperCase().padEnd(5), { stream }),
|
|
49
|
+
name === undefined ? undefined : `[${shown(name)}]`,
|
|
50
|
+
msg,
|
|
51
|
+
...details,
|
|
52
|
+
]
|
|
53
|
+
const stacks = errors.map((error) =>
|
|
54
|
+
(error.stack ?? `${error.name}: ${error.message}`).replaceAll(
|
|
55
|
+
/^/gmu,
|
|
56
|
+
' ',
|
|
57
|
+
),
|
|
58
|
+
)
|
|
59
|
+
return [line.filter((part) => part !== undefined).join(' '), ...stacks].join(
|
|
60
|
+
'\n',
|
|
61
|
+
)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function consoleLogger(bindings: object = {}): Logger {
|
|
65
|
+
const log =
|
|
66
|
+
(level: Level) =>
|
|
67
|
+
(first: string | object, message?: string): void => {
|
|
68
|
+
const fields = {
|
|
69
|
+
...bindings,
|
|
70
|
+
...(typeof first === 'string' ? {} : first),
|
|
71
|
+
}
|
|
72
|
+
const msg = typeof first === 'string' ? first : message
|
|
73
|
+
// console.info writes to stdout, console.warn and console.error to stderr.
|
|
74
|
+
const stream = level === 'info' ? process.stdout : process.stderr
|
|
75
|
+
console[level](
|
|
76
|
+
stream.isTTY
|
|
77
|
+
? readable(level, fields, msg, stream)
|
|
78
|
+
: json(level, fields, msg),
|
|
79
|
+
)
|
|
80
|
+
}
|
|
81
|
+
return {
|
|
82
|
+
info: log('info'),
|
|
83
|
+
warn: log('warn'),
|
|
84
|
+
error: log('error'),
|
|
85
|
+
child: (more) => consoleLogger({ ...bindings, ...more }),
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
// `createMany` (D-135): what a `create` runs, on each row of `data`, all or
|
|
2
|
+
// nothing; the rows go in one write (D-99). `createManyAndReturn` returns
|
|
3
|
+
// them in `data`'s order, each as `create` does (D-149).
|
|
4
|
+
import type { Change } from './adapter.ts'
|
|
5
|
+
import { InvalidQuery } from './errors.ts'
|
|
6
|
+
import type { Event } from './hooks.ts'
|
|
7
|
+
import { rereads, runHook, settle, settles } from './hooks.ts'
|
|
8
|
+
import type { Plan as Shaped } from './query-args.ts'
|
|
9
|
+
import { oneKeys } from './query-args.ts'
|
|
10
|
+
import type { Context } from './query-context.ts'
|
|
11
|
+
import { given, shapedRows } from './query-context.ts'
|
|
12
|
+
import { byIds, entries, isObject } from './query-where.ts'
|
|
13
|
+
import { createPlan } from './query-write.ts'
|
|
14
|
+
import type { Commit, Store } from './transaction.ts'
|
|
15
|
+
import type { Draft } from './write-data.ts'
|
|
16
|
+
import { prepared, readData } from './write-data.ts'
|
|
17
|
+
import { guarded } from './write-target.ts'
|
|
18
|
+
import { writingOf } from './writing.ts'
|
|
19
|
+
import { returned } from './writes.ts'
|
|
20
|
+
|
|
21
|
+
/** A row's data, as `create` reads it, with no write but `connect` on a `one`. */
|
|
22
|
+
function rowData(context: Context, data: unknown, at: string): Draft {
|
|
23
|
+
const { models, model } = context
|
|
24
|
+
for (const [name, value] of entries(data, at)) {
|
|
25
|
+
const field = model.fields.get(name)
|
|
26
|
+
if (field === undefined || field.kind === 'scalar') continue
|
|
27
|
+
const path = `${at}.${name}`
|
|
28
|
+
if (field.kind === 'many') {
|
|
29
|
+
throw new InvalidQuery(
|
|
30
|
+
path,
|
|
31
|
+
`${path} is a many relation, which createMany does not write.`,
|
|
32
|
+
)
|
|
33
|
+
}
|
|
34
|
+
if (
|
|
35
|
+
!isObject(value) ||
|
|
36
|
+
Object.keys(value).some((key) => key !== 'connect')
|
|
37
|
+
) {
|
|
38
|
+
throw new InvalidQuery(path, `${path} takes only connect in createMany.`)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return readData(models, model, data, true, at)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Creates the rows of `data`: their ids, or with `at` the rows as it shapes
|
|
46
|
+
* them, in `data`'s order. The rows come back even when a read rule would
|
|
47
|
+
* hide them, as `create`'s does (D-86).
|
|
48
|
+
*/
|
|
49
|
+
async function created(
|
|
50
|
+
context: Context,
|
|
51
|
+
store: Store,
|
|
52
|
+
commits: Commit[],
|
|
53
|
+
data: unknown,
|
|
54
|
+
at?: Shaped,
|
|
55
|
+
): Promise<readonly unknown[]> {
|
|
56
|
+
const { model, caller, scope } = context
|
|
57
|
+
const drafts = Array.isArray(data)
|
|
58
|
+
? data.map((row: unknown, index) => rowData(context, row, `data.${index}`))
|
|
59
|
+
: [rowData(context, data, 'data')]
|
|
60
|
+
const sent: readonly Readonly<Draft>[] = drafts.map((draft) => ({ ...draft }))
|
|
61
|
+
// One row refused, none written (D-111).
|
|
62
|
+
for (const each of sent) scope.access?.allow(model, 'create', { data: each })
|
|
63
|
+
if (drafts.length === 0) return []
|
|
64
|
+
for (const draft of drafts) {
|
|
65
|
+
await prepared(model, draft, caller, true)
|
|
66
|
+
await runHook(model, 'beforeCreate', { caller, data: draft })
|
|
67
|
+
}
|
|
68
|
+
const events: Event[] = []
|
|
69
|
+
const writing = writingOf(scope, store, events)
|
|
70
|
+
const changes: Change[] = []
|
|
71
|
+
const ids: string[] = []
|
|
72
|
+
for (const [index, draft] of drafts.entries()) {
|
|
73
|
+
const { id, plan } = await createPlan(
|
|
74
|
+
scope,
|
|
75
|
+
model,
|
|
76
|
+
draft,
|
|
77
|
+
sent[index] ?? {},
|
|
78
|
+
store,
|
|
79
|
+
)
|
|
80
|
+
ids.push(id)
|
|
81
|
+
events.push({ model, operation: 'create', id }, ...plan.events)
|
|
82
|
+
await plan.check(writing)
|
|
83
|
+
changes.push(...(await plan.changes(writing)))
|
|
84
|
+
}
|
|
85
|
+
const read = at && { model: model.name, ...at.read, where: byIds(ids) }
|
|
86
|
+
// An `after*` hook may change the rows: they are read once it ran (D-88).
|
|
87
|
+
const again = rereads(events)
|
|
88
|
+
const write = {
|
|
89
|
+
checks: writing.checks.map(({ check }) => check),
|
|
90
|
+
changes,
|
|
91
|
+
...(read && !again && { read }),
|
|
92
|
+
}
|
|
93
|
+
// Nothing follows a write without hooks: it may need no transaction.
|
|
94
|
+
const run = settles(events) ? store.write(write) : store.last(write)
|
|
95
|
+
const rows = await guarded(run, writing.checks)
|
|
96
|
+
await settle(store, caller, events, commits)
|
|
97
|
+
if (at === undefined || read === undefined) return ids
|
|
98
|
+
const found = new Map(
|
|
99
|
+
(again ? await store.find(read) : rows).map((row) => [row.id, row]),
|
|
100
|
+
)
|
|
101
|
+
const ordered = ids.flatMap((id) => found.get(id) ?? [])
|
|
102
|
+
return shapedRows(context, store, ordered, at.shape, scope.access)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export async function createMany(
|
|
106
|
+
context: Context,
|
|
107
|
+
store: Store,
|
|
108
|
+
commits: Commit[],
|
|
109
|
+
args: unknown,
|
|
110
|
+
): Promise<{ readonly count: number }> {
|
|
111
|
+
const found = given(args, context.model.name, ['data'], ['data'])
|
|
112
|
+
const ids = await created(context, store, commits, found.get('data'))
|
|
113
|
+
return { count: ids.length }
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export async function createManyAndReturn(
|
|
117
|
+
context: Context,
|
|
118
|
+
store: Store,
|
|
119
|
+
commits: Commit[],
|
|
120
|
+
args: unknown,
|
|
121
|
+
): Promise<readonly unknown[]> {
|
|
122
|
+
const keys = ['data', ...oneKeys]
|
|
123
|
+
const found = given(args, context.model.name, keys, ['data'])
|
|
124
|
+
const at = await returned(context, found)
|
|
125
|
+
return created(context, store, commits, found.get('data'), at)
|
|
126
|
+
}
|
package/src/decimal.ts
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// Decimal values (D-145): exact numbers as strings, normalized to their
|
|
2
|
+
// field's scale, compared and summed through `BigInt`.
|
|
3
|
+
|
|
4
|
+
/** A decimal string's shape, as transports describe it. */
|
|
5
|
+
export const decimalPattern = '^-?\\d+(\\.\\d+)?$'
|
|
6
|
+
const shape = new RegExp(decimalPattern)
|
|
7
|
+
|
|
8
|
+
/** A decimal's digits as a whole number, and how many follow its point. */
|
|
9
|
+
type Exact = { readonly units: bigint; readonly scale: number }
|
|
10
|
+
|
|
11
|
+
function exact(value: string): Exact {
|
|
12
|
+
const [whole = '', fraction = ''] = value.split('.')
|
|
13
|
+
return { units: BigInt(`${whole}${fraction}`), scale: fraction.length }
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** `value`'s units at `to` digits after the point; a cut digit is a 0. */
|
|
17
|
+
const scaled = ({ units, scale }: Exact, to: number): bigint =>
|
|
18
|
+
to >= scale
|
|
19
|
+
? units * 10n ** BigInt(to - scale)
|
|
20
|
+
: units / 10n ** BigInt(scale - to)
|
|
21
|
+
|
|
22
|
+
function text(units: bigint, scale: number): string {
|
|
23
|
+
const digits = (units < 0n ? -units : units)
|
|
24
|
+
.toString()
|
|
25
|
+
.padStart(scale + 1, '0')
|
|
26
|
+
const whole = digits.slice(0, digits.length - scale)
|
|
27
|
+
const sign = units < 0n ? '-' : ''
|
|
28
|
+
return scale === 0
|
|
29
|
+
? `${sign}${whole}`
|
|
30
|
+
: `${sign}${whole}.${digits.slice(-scale)}`
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** A decimal field's precision and scale. */
|
|
34
|
+
export type Digits = { readonly precision: number; readonly scale: number }
|
|
35
|
+
|
|
36
|
+
/** A field, which has digits if it is a decimal. */
|
|
37
|
+
type Digited = { readonly type: string } & Partial<Digits>
|
|
38
|
+
|
|
39
|
+
/** The message of a value a decimal `field` cannot hold, if it cannot. */
|
|
40
|
+
export function decimalIssue(
|
|
41
|
+
value: unknown,
|
|
42
|
+
field: Digited,
|
|
43
|
+
): string | undefined {
|
|
44
|
+
const digits = { precision: field.precision ?? 0, scale: field.scale ?? 0 }
|
|
45
|
+
const expected = `Expected a decimal string of at most ${digits.precision} digits, ${digits.scale} after the point, such as '12.50'.`
|
|
46
|
+
if (typeof value !== 'string' || !shape.test(value)) return expected
|
|
47
|
+
const [whole = '', fraction = ''] = value.replace('-', '').split('.')
|
|
48
|
+
const significant = whole.replace(/^0+/, '').length
|
|
49
|
+
const decimals = fraction.replace(/0+$/, '').length
|
|
50
|
+
return significant > digits.precision - digits.scale ||
|
|
51
|
+
decimals > digits.scale
|
|
52
|
+
? expected
|
|
53
|
+
: undefined
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** `value`, which fits, at `scale`: `'012.5'` is `'12.50'`. */
|
|
57
|
+
export const normalized = (value: string, scale: number): string =>
|
|
58
|
+
text(scaled(exact(value), scale), scale)
|
|
59
|
+
|
|
60
|
+
/** A value that fits `field`, as stored: a decimal at its scale. */
|
|
61
|
+
export const storedValue = (value: unknown, field: Digited): unknown =>
|
|
62
|
+
field.type === 'decimal' && typeof value === 'string'
|
|
63
|
+
? normalized(value, field.scale ?? 0)
|
|
64
|
+
: value
|
|
65
|
+
|
|
66
|
+
/** A decimal string's units at `scale`, which order decimals by value. */
|
|
67
|
+
export const unitsOf = (value: string, scale: number): bigint =>
|
|
68
|
+
scaled(exact(value), scale)
|
|
69
|
+
|
|
70
|
+
/** The exact sum of decimal strings, at `scale`. */
|
|
71
|
+
export function sumDecimals(values: readonly string[], scale: number): string {
|
|
72
|
+
const total = values.reduce((sum, value) => sum + unitsOf(value, scale), 0n)
|
|
73
|
+
return text(total, scale)
|
|
74
|
+
}
|