@workerstack/database 0.21.0 → 0.22.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/README.md +243 -40
- package/docs/meta-contract.md +55 -1
- package/package.json +3 -7
- package/src/cli/args.ts +4 -2
- package/src/cli/codegen.ts +2 -2
- package/src/cli/comb-config.ts +8 -3
- package/src/cli/file-path.ts +25 -0
- package/src/cli/migrate-apply.ts +5 -21
- package/src/cli/migrate-d1.ts +5 -1
- package/src/cli/migrate-json.ts +48 -0
- package/src/cli/migrate.ts +26 -4
- package/src/codegen/analyzer-types.ts +6 -1
- package/src/codegen/annotations.ts +66 -9
- package/src/codegen/format-constraints.ts +114 -0
- package/src/codegen/generators/dtos/entity-file.ts +2 -1
- package/src/codegen/generators/dtos/example-meta.ts +22 -2
- package/src/codegen/generators/dtos/field-schema.ts +31 -8
- package/src/codegen/generators/dtos/ip-patterns.ts +25 -11
- package/src/codegen/generators/dtos/number-range.ts +22 -3
- package/src/codegen/generators/dtos/schema-aliases.ts +5 -3
- package/src/codegen/generators/dtos/text-format.ts +63 -0
- package/src/codegen/generators/dtos/text-schema.ts +49 -2
- package/src/codegen/generators/manifest.ts +9 -1
- package/src/codegen/runtime/read-comb.ts +71 -3
- package/src/codegen/runtime/read-tables.ts +31 -19
- package/src/codegen/runtime/snapshot-types.ts +3 -1
- package/src/codegen/runtime/to-meta.ts +6 -3
- package/src/formats/catalog.ts +255 -0
- package/src/formats/iana-time-zone.ts +117 -0
- package/src/formats/iso3166-1.ts +78 -0
- package/src/formats/iso4217.ts +42 -0
- package/src/formats/iso639-1.ts +41 -0
- package/src/formats/uts35.ts +143 -0
- package/src/index.ts +5 -0
- package/src/meta/column.ts +22 -9
- package/src/migrate/core/apply.ts +63 -22
- package/src/migrate/core/atlas-apply-runs.ts +15 -26
- package/src/migrate/core/cleanup.ts +22 -10
- package/src/migrate/core/d1-engine-child.mjs +27 -11
- package/src/migrate/core/d1-history.ts +23 -26
- package/src/migrate/core/d1-wrangler-error.ts +24 -1
- package/src/migrate/core/pg-replay-commit.ts +41 -38
- package/src/migrate/core/pg-replay-drops.ts +17 -32
- package/src/migrate/core/pg-replay-hidden.ts +75 -0
- package/src/migrate/core/pg-replay-roles.ts +2 -4
- package/src/migrate/core/pg-replay.ts +21 -19
- package/src/migrate/core/pg-server-snapshot.ts +4 -24
- package/src/migrate/core/proof-readers.ts +80 -0
- package/src/migrate/core/proof-run.ts +19 -5
- package/src/migrate/core/proof-scope.ts +27 -14
- package/src/migrate/core/sql-code.ts +36 -5
- package/src/migrate/core/sql-scan.ts +7 -1
- package/src/migrate/core/sqlite-keywords.ts +70 -0
- package/src/migrate/core/stopped.ts +70 -0
- package/src/migrate/core/trigger-write.d.mts +7 -0
- package/src/migrate/core/trigger-write.mjs +42 -0
- package/src/pg/columns.ts +6 -4
- package/src/query/cursor-mint.ts +11 -18
- package/src/query/cursor-refusals.ts +22 -11
- package/src/query/descriptions.ts +4 -9
- package/src/query/filter-parse.ts +80 -22
- package/src/query/filter-validate.ts +10 -13
- package/src/query/filter.ts +40 -18
- package/src/query/list-extras.ts +7 -10
- package/src/query/list-schema.ts +0 -2
- package/src/query/merged.ts +3 -2
- package/src/query/pagination.ts +30 -6
- package/src/query/rounded-key.ts +5 -8
- package/src/query/schema-params.ts +12 -5
- package/src/query/select-validate.ts +7 -4
- package/src/query/sqlite/cursor-fit.ts +10 -2
- package/src/query/trusted.ts +7 -2
- package/src/sqlite/columns.ts +6 -4
- package/src/sqlite/constraints.ts +120 -6
- package/types/cli/args.d.ts.map +1 -1
- package/types/cli/comb-config.d.ts +5 -2
- package/types/cli/comb-config.d.ts.map +1 -1
- package/types/cli/file-path.d.ts +11 -0
- package/types/cli/file-path.d.ts.map +1 -0
- package/types/cli/migrate-apply.d.ts +1 -0
- package/types/cli/migrate-apply.d.ts.map +1 -1
- package/types/cli/migrate-d1.d.ts.map +1 -1
- package/types/cli/migrate-json.d.ts +4 -0
- package/types/cli/migrate-json.d.ts.map +1 -0
- package/types/cli/migrate.d.ts.map +1 -1
- package/types/codegen/analyzer-types.d.ts +6 -1
- package/types/codegen/analyzer-types.d.ts.map +1 -1
- package/types/codegen/annotations.d.ts +12 -7
- package/types/codegen/annotations.d.ts.map +1 -1
- package/types/codegen/format-constraints.d.ts +22 -0
- package/types/codegen/format-constraints.d.ts.map +1 -0
- package/types/codegen/generators/dtos/entity-file.d.ts.map +1 -1
- package/types/codegen/generators/dtos/example-meta.d.ts.map +1 -1
- package/types/codegen/generators/dtos/field-schema.d.ts.map +1 -1
- package/types/codegen/generators/dtos/ip-patterns.d.ts +7 -0
- package/types/codegen/generators/dtos/ip-patterns.d.ts.map +1 -1
- package/types/codegen/generators/dtos/number-range.d.ts +8 -0
- package/types/codegen/generators/dtos/number-range.d.ts.map +1 -1
- package/types/codegen/generators/dtos/schema-aliases.d.ts +1 -1
- package/types/codegen/generators/dtos/schema-aliases.d.ts.map +1 -1
- package/types/codegen/generators/dtos/text-format.d.ts +31 -0
- package/types/codegen/generators/dtos/text-format.d.ts.map +1 -0
- package/types/codegen/generators/dtos/text-schema.d.ts +8 -1
- package/types/codegen/generators/dtos/text-schema.d.ts.map +1 -1
- package/types/codegen/generators/manifest.d.ts +5 -0
- package/types/codegen/generators/manifest.d.ts.map +1 -1
- package/types/codegen/runtime/read-comb.d.ts +17 -3
- package/types/codegen/runtime/read-comb.d.ts.map +1 -1
- package/types/codegen/runtime/read-tables.d.ts.map +1 -1
- package/types/codegen/runtime/snapshot-types.d.ts +3 -1
- package/types/codegen/runtime/snapshot-types.d.ts.map +1 -1
- package/types/codegen/runtime/to-meta.d.ts.map +1 -1
- package/types/formats/catalog.d.ts +51 -0
- package/types/formats/catalog.d.ts.map +1 -0
- package/types/formats/iana-time-zone.d.ts +7 -0
- package/types/formats/iana-time-zone.d.ts.map +1 -0
- package/types/formats/iso3166-1.d.ts +12 -0
- package/types/formats/iso3166-1.d.ts.map +1 -0
- package/types/formats/iso4217.d.ts +7 -0
- package/types/formats/iso4217.d.ts.map +1 -0
- package/types/formats/iso639-1.d.ts +6 -0
- package/types/formats/iso639-1.d.ts.map +1 -0
- package/types/formats/uts35.d.ts +13 -0
- package/types/formats/uts35.d.ts.map +1 -0
- package/types/index.d.ts +5 -0
- package/types/index.d.ts.map +1 -1
- package/types/meta/column.d.ts +7 -1
- package/types/meta/column.d.ts.map +1 -1
- package/types/migrate/core/apply.d.ts +3 -2
- package/types/migrate/core/apply.d.ts.map +1 -1
- package/types/migrate/core/atlas-apply-runs.d.ts +6 -0
- package/types/migrate/core/atlas-apply-runs.d.ts.map +1 -1
- package/types/migrate/core/cleanup.d.ts +16 -4
- package/types/migrate/core/cleanup.d.ts.map +1 -1
- package/types/migrate/core/d1-history.d.ts.map +1 -1
- package/types/migrate/core/d1-wrangler-error.d.ts +11 -0
- package/types/migrate/core/d1-wrangler-error.d.ts.map +1 -1
- package/types/migrate/core/pg-replay-commit.d.ts +14 -9
- package/types/migrate/core/pg-replay-commit.d.ts.map +1 -1
- package/types/migrate/core/pg-replay-drops.d.ts +7 -12
- package/types/migrate/core/pg-replay-drops.d.ts.map +1 -1
- package/types/migrate/core/pg-replay-hidden.d.ts +36 -0
- package/types/migrate/core/pg-replay-hidden.d.ts.map +1 -0
- package/types/migrate/core/pg-replay-roles.d.ts +2 -4
- package/types/migrate/core/pg-replay-roles.d.ts.map +1 -1
- package/types/migrate/core/pg-replay.d.ts.map +1 -1
- package/types/migrate/core/pg-server-snapshot.d.ts +4 -11
- package/types/migrate/core/pg-server-snapshot.d.ts.map +1 -1
- package/types/migrate/core/proof-readers.d.ts +19 -0
- package/types/migrate/core/proof-readers.d.ts.map +1 -0
- package/types/migrate/core/proof-run.d.ts.map +1 -1
- package/types/migrate/core/proof-scope.d.ts +15 -7
- package/types/migrate/core/proof-scope.d.ts.map +1 -1
- package/types/migrate/core/sql-code.d.ts +7 -3
- package/types/migrate/core/sql-code.d.ts.map +1 -1
- package/types/migrate/core/sql-scan.d.ts.map +1 -1
- package/types/migrate/core/sqlite-keywords.d.ts +11 -0
- package/types/migrate/core/sqlite-keywords.d.ts.map +1 -0
- package/types/migrate/core/stopped.d.ts +23 -0
- package/types/migrate/core/stopped.d.ts.map +1 -0
- package/types/pg/columns.d.ts +3 -3
- package/types/pg/columns.d.ts.map +1 -1
- package/types/query/cursor-mint.d.ts.map +1 -1
- package/types/query/cursor-refusals.d.ts +17 -1
- package/types/query/cursor-refusals.d.ts.map +1 -1
- package/types/query/descriptions.d.ts +1 -1
- package/types/query/descriptions.d.ts.map +1 -1
- package/types/query/filter-parse.d.ts +15 -4
- package/types/query/filter-parse.d.ts.map +1 -1
- package/types/query/filter-validate.d.ts +1 -1
- package/types/query/filter-validate.d.ts.map +1 -1
- package/types/query/filter.d.ts +9 -1
- package/types/query/filter.d.ts.map +1 -1
- package/types/query/list-extras.d.ts.map +1 -1
- package/types/query/list-schema.d.ts.map +1 -1
- package/types/query/merged.d.ts.map +1 -1
- package/types/query/pagination.d.ts +13 -1
- package/types/query/pagination.d.ts.map +1 -1
- package/types/query/rounded-key.d.ts +2 -11
- package/types/query/rounded-key.d.ts.map +1 -1
- package/types/query/schema-params.d.ts.map +1 -1
- package/types/query/select-validate.d.ts.map +1 -1
- package/types/query/sqlite/cursor-fit.d.ts.map +1 -1
- package/types/query/trusted.d.ts +1 -1
- package/types/query/trusted.d.ts.map +1 -1
- package/types/sqlite/columns.d.ts +3 -3
- package/types/sqlite/columns.d.ts.map +1 -1
- package/types/sqlite/constraints.d.ts +117 -5
- package/types/sqlite/constraints.d.ts.map +1 -1
- package/src/sqlite/patterns.ts +0 -19
- package/types/sqlite/patterns.d.ts +0 -19
- package/types/sqlite/patterns.d.ts.map +0 -1
package/docs/meta-contract.md
CHANGED
|
@@ -166,6 +166,55 @@ no `anyOf` walk. A nullable trimmed text with a `max` of 9, as codegen publishes
|
|
|
166
166
|
- `x-comb-rectangular`: `true` on a multidimensional array: every sub-array of one level, across the
|
|
167
167
|
whole level, has the same length, as Postgres stores an array: `[[1, 2], [3, 4]]` is one,
|
|
168
168
|
`[[1, 2], [3]]` is not.
|
|
169
|
+
- `x-comb-format`: the standard a written value is held to, by its id, on a text, an enum or an
|
|
170
|
+
integer. On a text, the value after its `x-comb-transform` matches the published `pattern` and
|
|
171
|
+
length bounds; where the id below names a registry, it is also a member of that pinned registry,
|
|
172
|
+
which no pattern states, so a reader must check it, with the predicate named, which
|
|
173
|
+
`@workerstack/database` exports, or with the release named (`HRK` matches `^[A-Z]{3}$` and is no
|
|
174
|
+
`iso4217` value: withdrawn in 2023). A text failing it gets zod's `invalid_format` issue with the
|
|
175
|
+
id as its `format` and no message of comb's, so a zod locale or error map words it. On an enum the
|
|
176
|
+
listed values are the rule, each one the format takes, and the id is what they mean; every DTO of
|
|
177
|
+
an enum publishes it, as each holds the values. On an integer the published `minimum` and
|
|
178
|
+
`maximum` are the standard's range, or a declared one inside it. A read of a text or an integer
|
|
179
|
+
publishes none. The ids, each with its standard and release:
|
|
180
|
+
- `base64url`: RFC 4648 §5 base64url, unpadded.
|
|
181
|
+
- `bcp47`: BCP 47 language tag, RFC 5646 §2.1 well-formed.
|
|
182
|
+
- `e164`: ITU-T E.164 phone number, + and up to 15 digits.
|
|
183
|
+
- `iana-time-zone`: IANA time zone name, tzdata 2026f. Registry: `isIanaTimeZone`.
|
|
184
|
+
- `ip`: IPv4 dotted quad or IPv6 address, RFC 4291 §2.2.
|
|
185
|
+
- `ipv4`: IPv4 address, dotted quad without leading zeros.
|
|
186
|
+
- `ipv6`: IPv6 address, RFC 4291 §2.2.
|
|
187
|
+
- `iso3166-1-alpha-2`: ISO 3166-1 alpha-2 country code, Debian iso-codes v4.20.1. Registry: `isIso3166Alpha2`.
|
|
188
|
+
- `iso3166-1-alpha-3`: ISO 3166-1 alpha-3 country code, Debian iso-codes v4.20.1. Registry: `isIso3166Alpha3`.
|
|
189
|
+
- `iso4217`: ISO 4217 alphabetic currency code, SIX list 2026-09-17. Registry: `isIso4217`.
|
|
190
|
+
- `iso639-1`: ISO 639-1 language code, Debian iso-codes v4.20.1. Registry: `isIso639Alpha2`.
|
|
191
|
+
- `kebab-slug`: kebab-case slug, lowercase letters and digits joined by single hyphens.
|
|
192
|
+
- `pkce-s256-challenge`: RFC 7636 §4.2 PKCE SHA-256 code challenge.
|
|
193
|
+
- `rfc1123-hostname`: RFC 1123 §2.1 host name.
|
|
194
|
+
- `semver`: Semantic Versioning 2.0.0 version.
|
|
195
|
+
- `sha256-hex`: SHA-256 digest, FIPS 180-4, lowercase hex.
|
|
196
|
+
- `sha512-hex`: SHA-512 digest, FIPS 180-4, lowercase hex.
|
|
197
|
+
- `srgb-hex`: sRGB color as #rrggbb, CSS Color 4.
|
|
198
|
+
- `srgb-hex-alpha`: sRGB color with alpha as #rrggbbaa, CSS Color 4.
|
|
199
|
+
- `ulid`: ULID, Crockford base32 in lowercase.
|
|
200
|
+
- `uts35-ca`: UTS #35 calendar (ca) key value, CLDR 49.
|
|
201
|
+
- `uts35-fw`: UTS #35 first day of the week (fw) key value, CLDR 49.
|
|
202
|
+
- `uts35-hc`: UTS #35 hour cycle (hc) key value, CLDR 49.
|
|
203
|
+
- `uts35-ms`: UTS #35 measurement system (ms) key value, CLDR 49.
|
|
204
|
+
- `uts35-mu`: UTS #35 measurement unit (mu) key value, CLDR 49.
|
|
205
|
+
- `uts35-nu`: UTS #35 numbering system (nu) key value, CLDR 49.
|
|
206
|
+
- `uuid`: RFC 9562 UUID, lowercase hex.
|
|
207
|
+
- `uuid-v4`: RFC 9562 UUID version 4, lowercase hex.
|
|
208
|
+
- `uuid-v7`: RFC 9562 UUID version 7, lowercase hex.
|
|
209
|
+
- `iso8601-month`: ISO 8601 month number, 1 January to 12 December.
|
|
210
|
+
- `iso8601-weekday`: ISO 8601 §3.2.2 weekday number, 1 Monday to 7 Sunday.
|
|
211
|
+
- `x-comb-schema`: the export name of the zod schema a written text or number is piped into after
|
|
212
|
+
every other rule, on the value as rewritten; its issues reach the caller as the schema yields
|
|
213
|
+
them. The `allOf` beside it is the schema's published part: its input JSON Schema, last, after
|
|
214
|
+
the patterns of the field's own chain when that has two or more. A refine in the schema publishes
|
|
215
|
+
nothing, so a rule it states only as a refine is one the producer alone runs: a reader may take a
|
|
216
|
+
value the producer then refuses. On a nullable field the `allOf` sits on the non-null member
|
|
217
|
+
alone, as its `type` would refuse the null the field takes. A read publishes none.
|
|
169
218
|
- `x-comb-minLength`, `x-comb-maxLength`, `x-comb-minimum`, `x-comb-maximum`: on a read, the
|
|
170
219
|
declared bounds it states without enforcing them, so a stored row past one still reads.
|
|
171
220
|
|
|
@@ -652,7 +701,12 @@ reader joins through the manifest alone. A composite foreign key is one relation
|
|
|
652
701
|
per column, each carrying the whole key as `key: { columns, refColumns }`, paired by position, so
|
|
653
702
|
a reader joins on every column together rather than on independent pairs. Per column it carries the helper that
|
|
654
703
|
declared it, its `sqlType`, the column's SQL type with its modifier as drizzle writes it (`timestamp(3) with time zone`, `interval day`, `numeric(3, -2)`), an array's that of its element, beside its `dimensions` (0 for a column of one value, 2 for `integer[][]`) and `nullElements`, whether a written element may be null, as the column's drizzle type declares it (false for a column of one value), the bounds a write takes and enum values, `textKind` (`"email"`, `"url"`, `"imageUri"`,
|
|
655
|
-
`"password"`, `"name"`, or null), `
|
|
704
|
+
`"password"`, `"name"`, or null), `format`, the id of the standard format a write checks the
|
|
705
|
+
value against (`"iso4217"`, `"iso8601-weekday"`), as `x-comb-format` publishes it (§2.1), or null
|
|
706
|
+
when the column declares none, `schema`, the export name of the zod schema a text or number column
|
|
707
|
+
declares, which a write runs after the column's own rules and whose published part is
|
|
708
|
+
`x-comb-schema`'s (§2.1), or null when none and on a `c.json` column, whose schema is its value's
|
|
709
|
+
shape rather than a rule beside it, `transform`, the list the DTO publishes as `x-comb-transform`
|
|
656
710
|
(§2.1), and `generated`, `immutable` and `private` flags. `min`, `max`, `patterns` and `maxBytes`
|
|
657
711
|
are what the create DTO enforces, not what the column declares, from `writeRules`, which reads
|
|
658
712
|
the rules the DTO's builders read: a text's declared bounds with comb's own (a required text's 1, a
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@workerstack/database",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.22.1",
|
|
4
4
|
"description": "Database framework for Cloudflare D1, PostgreSQL, and SQLite. Schema builders, error handling, D1 sessions, sharding, codegen, migrations.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cloudflare-d1",
|
|
@@ -124,11 +124,6 @@
|
|
|
124
124
|
"types": "./types/typescript-floor-types.d.ts",
|
|
125
125
|
"default": "./src/sqlite/d1/sharding/index.ts"
|
|
126
126
|
},
|
|
127
|
-
"./sqlite/patterns": {
|
|
128
|
-
"types@>=5.7": "./types/sqlite/patterns.d.ts",
|
|
129
|
-
"types": "./types/typescript-floor-types.d.ts",
|
|
130
|
-
"default": "./src/sqlite/patterns.ts"
|
|
131
|
-
},
|
|
132
127
|
"./states": {
|
|
133
128
|
"types@>=5.7": "./types/states.d.ts",
|
|
134
129
|
"types": "./types/typescript-floor-types.d.ts",
|
|
@@ -147,7 +142,8 @@
|
|
|
147
142
|
"prepack": "rm -rf types && cp ../../README.md ../../LICENSE . && bun run types:emit",
|
|
148
143
|
"postpack": "rm -rf types README.md LICENSE",
|
|
149
144
|
"test": "vitest run",
|
|
150
|
-
"test:bun": "bun test tests/bun",
|
|
145
|
+
"test:bun": "bun test tests/bun --path-ignore-patterns=tests/bun/differential-lists.test.ts",
|
|
146
|
+
"test:differential": "vitest run --config vitest.differential.config.ts && bun test tests/bun/differential-lists.test.ts",
|
|
151
147
|
"test:coverage": "vitest run --coverage",
|
|
152
148
|
"test:consumers": "vitest run --config vitest.consumers.config.ts",
|
|
153
149
|
"test:e2e": "vitest run --config vitest.e2e.config.ts",
|
package/src/cli/args.ts
CHANGED
|
@@ -55,7 +55,8 @@ export const OPTIONS: readonly OptionSpec[] = [
|
|
|
55
55
|
},
|
|
56
56
|
{
|
|
57
57
|
commands: ["migrate diff"],
|
|
58
|
-
description:
|
|
58
|
+
description:
|
|
59
|
+
"Write nothing: exit 1 printing the migration a diff would write, 0 in sync, 2 when the check could not run",
|
|
59
60
|
long: "--dry-run",
|
|
60
61
|
},
|
|
61
62
|
{ commands: ["migrate apply"], description: "Apply this one migration file", long: "--file", value: "path" },
|
|
@@ -78,7 +79,8 @@ export const OPTIONS: readonly OptionSpec[] = [
|
|
|
78
79
|
},
|
|
79
80
|
{
|
|
80
81
|
commands: ["migrate apply", "migrate check", "migrate status"],
|
|
81
|
-
description:
|
|
82
|
+
description:
|
|
83
|
+
"Print one JSON object (database, applied, pending, refused, unknown; stoppedBy when a signal stopped the run) as the only stdout",
|
|
82
84
|
long: "--json",
|
|
83
85
|
},
|
|
84
86
|
]
|
package/src/cli/codegen.ts
CHANGED
|
@@ -249,7 +249,7 @@ async function generate(
|
|
|
249
249
|
/** The databases a codegen run covers: the one --tables names, or those comb.config.ts declares */
|
|
250
250
|
async function databases(positionals: readonly string[], tables: string | undefined, io: CliIo): Promise<Database[]> {
|
|
251
251
|
if (tables !== undefined) {
|
|
252
|
-
const tablesPath = tablesFile(io.cwd, tables)
|
|
252
|
+
const tablesPath = tablesFile(io.cwd, tables, "--tables")
|
|
253
253
|
return [{ db: { tables }, direct: true, exportNamespace: undefined, generators: DIRECT, name: "", tablesPath }]
|
|
254
254
|
}
|
|
255
255
|
const config = await loadCombConfig(io.cwd)
|
|
@@ -263,7 +263,7 @@ async function databases(positionals: readonly string[], tables: string | undefi
|
|
|
263
263
|
exportNamespace: namespaces.get(name),
|
|
264
264
|
generators: db.generators ?? GENERATORS,
|
|
265
265
|
name,
|
|
266
|
-
tablesPath: tablesFile(io.cwd, db.tables),
|
|
266
|
+
tablesPath: tablesFile(io.cwd, db.tables, "tables"),
|
|
267
267
|
}))
|
|
268
268
|
}
|
|
269
269
|
|
package/src/cli/comb-config.ts
CHANGED
|
@@ -6,6 +6,7 @@ import * as z from "zod"
|
|
|
6
6
|
import type { CodegenConfig, DatabaseCodegen } from "../codegen/config.ts"
|
|
7
7
|
import type { FtsTableConfig } from "../codegen/generators/fts-sql.ts"
|
|
8
8
|
import { codegenConfigSchema } from "../codegen/config-schema.ts"
|
|
9
|
+
import { refuseNotFile } from "./file-path.ts"
|
|
9
10
|
import { CliError, type CliIo } from "./output.ts"
|
|
10
11
|
|
|
11
12
|
export type { CodegenConfig, DatabaseCodegen }
|
|
@@ -58,10 +59,14 @@ export function databaseIn(config: CodegenConfig, name: string, hint?: string):
|
|
|
58
59
|
)
|
|
59
60
|
}
|
|
60
61
|
|
|
61
|
-
/**
|
|
62
|
-
|
|
62
|
+
/**
|
|
63
|
+
* The tables file `tables` names, resolved against `cwd`, as `what` gave it (--tables, or
|
|
64
|
+
* comb.config.ts `tables`); a CliError when it is missing or not a file
|
|
65
|
+
*/
|
|
66
|
+
export function tablesFile(cwd: string, tables: string, what: "--tables" | "tables"): string {
|
|
63
67
|
const resolved = path.resolve(cwd, tables)
|
|
64
68
|
if (!fs.existsSync(resolved)) throw new CliError(`Tables file not found: ${resolved}`)
|
|
69
|
+
refuseNotFile(resolved, what, "point it at the module that exports the tables")
|
|
65
70
|
return resolved
|
|
66
71
|
}
|
|
67
72
|
|
|
@@ -85,7 +90,7 @@ export async function declaredFts(
|
|
|
85
90
|
|
|
86
91
|
/** The FTS5 DDL `declared` asks for, for the migration diff to keep apart from what Atlas sees */
|
|
87
92
|
export async function ftsSql(cwd: string, declared: DeclaredFts, io: CliIo): Promise<string> {
|
|
88
|
-
const resolvedTables = tablesFile(cwd, declared.tables)
|
|
93
|
+
const resolvedTables = tablesFile(cwd, declared.tables, "tables")
|
|
89
94
|
const { analyze } = await import("../codegen/analyzer.ts")
|
|
90
95
|
const { generateFtsSql } = await import("../codegen/generators/fts-sql.ts")
|
|
91
96
|
return generateFtsSql((await analyze(resolvedTables, { cwd })).tables, declared.fts, io.err)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The CLI's checks of a path or a name a command or a config gives before it reaches the
|
|
3
|
+
* filesystem, so a wrong kind is refused by name rather than as the error of whatever reads it.
|
|
4
|
+
*/
|
|
5
|
+
import fs from "node:fs"
|
|
6
|
+
import { CliError } from "./output.ts"
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Refuse `file`, which `what` names (an option or a config field), when it exists as other than a
|
|
10
|
+
* file, saying what it is, then `fix`; a missing one is left to the caller, which refuses it its own way
|
|
11
|
+
*/
|
|
12
|
+
export function refuseNotFile(file: string, what: string, fix: string): void {
|
|
13
|
+
const stat = fs.statSync(file, { throwIfNoEntry: false })
|
|
14
|
+
if (stat === undefined || stat.isFile()) return
|
|
15
|
+
throw new CliError(`${what} ${file} ${stat.isDirectory() ? "is a directory, not a file" : "is not a file"}: ${fix}`)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Refuse a name a file is made from, `what` naming it, when it would name a path: one holding a
|
|
20
|
+
* separator, or `.` or `..` alone; then `usage`
|
|
21
|
+
*/
|
|
22
|
+
export function refusePathName(name: string, what: string, usage: string): void {
|
|
23
|
+
if (!/[/\\]/.test(name) && name !== "." && name !== "..") return
|
|
24
|
+
throw new CliError(`${what} "${name}" names a path: give a name alone, such as add_users\n${usage}`)
|
|
25
|
+
}
|
package/src/cli/migrate-apply.ts
CHANGED
|
@@ -33,8 +33,8 @@ import type { Connection } from "../migrate/types.ts"
|
|
|
33
33
|
import { parseArgs, type ParsedArgs } from "./args.ts"
|
|
34
34
|
import { fileName, openSqliteTarget, type SqliteTarget } from "./migrate-sqlite-target.ts"
|
|
35
35
|
import { resolveMigrateTarget } from "./migrate-target.ts"
|
|
36
|
-
import {
|
|
37
|
-
import { checkPassed, CliError, type CliContext, type MigrateOutcome
|
|
36
|
+
import { asJson } from "./migrate-json.ts"
|
|
37
|
+
import { checkPassed, CliError, type CliContext, type MigrateOutcome } from "./output.ts"
|
|
38
38
|
|
|
39
39
|
type Mode = "apply" | "baseline" | "check" | "status"
|
|
40
40
|
|
|
@@ -220,11 +220,6 @@ async function migrateSqlite({ args, io, mode }: Request, target: Target): Promi
|
|
|
220
220
|
return { applied: pendingNames, pending: [] }
|
|
221
221
|
}
|
|
222
222
|
|
|
223
|
-
/**
|
|
224
|
-
* `migrate apply|baseline|check|status`. With --json the human lines go to stderr and stdout is
|
|
225
|
-
* one JSON object: the database, the files applied (or found applied) and pending, what was
|
|
226
|
-
* refused, and the files whose answer was lost, which may have committed; a refusal still exits 1.
|
|
227
|
-
*/
|
|
228
223
|
/** `mode`'s argv read: its database and options; refused by usage without one database */
|
|
229
224
|
function readArgv(mode: Mode, argv: readonly string[]): { args: ParsedArgs; db: string } {
|
|
230
225
|
const args = parseArgs(argv, `migrate ${mode}`)
|
|
@@ -232,22 +227,11 @@ function readArgv(mode: Mode, argv: readonly string[]): { args: ParsedArgs; db:
|
|
|
232
227
|
return { args, db: args.positionals[0] as string }
|
|
233
228
|
}
|
|
234
229
|
|
|
230
|
+
/** `migrate apply|baseline|check|status`, with --json as one JSON object (migrate-json.ts) */
|
|
235
231
|
export async function migrateApply(mode: Mode, argv: readonly string[], io: CliContext): Promise<void> {
|
|
236
232
|
const { args, db } = readArgv(mode, argv)
|
|
237
|
-
if (args.options["json"]
|
|
238
|
-
|
|
239
|
-
return
|
|
240
|
-
}
|
|
241
|
-
const report = (outcome: MigrateOutcome, refused: string[], unknown: string[] = []) =>
|
|
242
|
-
io.out(JSON.stringify({ applied: outcome.applied, database: db, pending: outcome.pending, refused, unknown }))
|
|
243
|
-
try {
|
|
244
|
-
report(await runMode({ args, db, io: { ...io, out: io.err }, mode }), [])
|
|
245
|
-
} catch (error) {
|
|
246
|
-
/* a refusal or a failed command is an Error; a user's config can throw anything */
|
|
247
|
-
const { applied, pending, unknown } = error instanceof Refused ? error : { applied: [], pending: [], unknown: [] }
|
|
248
|
-
report({ applied, pending }, [error instanceof Error ? error.message : String(error)], unknown)
|
|
249
|
-
throw new Reported(error)
|
|
250
|
-
}
|
|
233
|
+
if (args.options["json"] === true) await asJson(db, io, (each) => runMode({ args, db, io: each, mode }))
|
|
234
|
+
else await runMode({ args, db, io, mode })
|
|
251
235
|
}
|
|
252
236
|
|
|
253
237
|
/**
|
package/src/cli/migrate-d1.ts
CHANGED
|
@@ -14,6 +14,7 @@ import { connectD1, wranglerFlags } from "../migrate/drivers/d1.ts"
|
|
|
14
14
|
import type { D1ProviderConfig } from "../migrate/types.ts"
|
|
15
15
|
import type { ParsedArgs } from "./args.ts"
|
|
16
16
|
import { type MigrateTarget, refuseNoDir } from "./migrate-target.ts"
|
|
17
|
+
import { refuseNotFile } from "./file-path.ts"
|
|
17
18
|
import { checkPassed, CliError, type CliContext, type MigrateOutcome } from "./output.ts"
|
|
18
19
|
|
|
19
20
|
type D1Mode = "apply" | "baseline" | "check" | "status"
|
|
@@ -27,7 +28,10 @@ function d1Config(request: D1Request, resolved: MigrateTarget | undefined): { co
|
|
|
27
28
|
if (resolved?.provider.type === "d1") {
|
|
28
29
|
if (args.options["local"] !== undefined)
|
|
29
30
|
throw new CliError("--local applies without comb.migrate.config.ts; set local: true on its d1 provider")
|
|
30
|
-
const
|
|
31
|
+
const configured = resolved.provider.wranglerConfig
|
|
32
|
+
const fix = `point ${db}'s d1 provider at its wrangler.json, wrangler.jsonc or wrangler.toml`
|
|
33
|
+
if (configured !== undefined) refuseNotFile(configured, "wranglerConfig", fix)
|
|
34
|
+
const wranglerConfig = configured ?? findWranglerConfig(resolved.configDir)
|
|
31
35
|
if (wranglerConfig === undefined)
|
|
32
36
|
throw new CliError(`no wrangler config next to comb.migrate.config.ts; set wranglerConfig on ${db}'s d1 provider`)
|
|
33
37
|
return { config: { ...resolved.provider, wranglerConfig }, dir: resolved.migrationsDir }
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `--json` for `migrate apply|baseline|check|status`: the human lines go to stderr and stdout is one
|
|
3
|
+
* JSON object, the database, the files applied (or found applied) and pending, what was refused, and
|
|
4
|
+
* the files whose answer was lost, which may have committed; a refusal still exits 1. A run a signal
|
|
5
|
+
* stopped (SIGHUP, SIGINT, SIGQUIT, SIGTERM) prints that object too, once the loop that applies files
|
|
6
|
+
* has said what committed (stopped.ts), with `stoppedBy` naming the signal, and then ends by the
|
|
7
|
+
* signal. A stop parks the run itself, so that object comes from the step held here.
|
|
8
|
+
*/
|
|
9
|
+
import { hold } from "../migrate/core/cleanup.ts"
|
|
10
|
+
import { Refused } from "../migrate/core/refused.ts"
|
|
11
|
+
import { Stopped } from "../migrate/core/stopped.ts"
|
|
12
|
+
import { type CliContext, type MigrateOutcome, Reported } from "./output.ts"
|
|
13
|
+
|
|
14
|
+
/** One end of a run, as the object prints it */
|
|
15
|
+
type End = MigrateOutcome & { refused: string[]; stoppedBy?: NodeJS.Signals | undefined; unknown: string[] }
|
|
16
|
+
|
|
17
|
+
/** `run` on `db`, its human lines on stderr and its one JSON object on `io.out` */
|
|
18
|
+
export async function asJson(
|
|
19
|
+
db: string,
|
|
20
|
+
io: CliContext,
|
|
21
|
+
run: (io: CliContext) => Promise<MigrateOutcome>,
|
|
22
|
+
): Promise<void> {
|
|
23
|
+
/* once: a run that fails as a signal stops it must not print a second object */
|
|
24
|
+
let printed = false
|
|
25
|
+
const report = ({ applied, pending, refused, stoppedBy, unknown }: End) => {
|
|
26
|
+
if (printed) return
|
|
27
|
+
printed = true
|
|
28
|
+
io.out(JSON.stringify({ applied, database: db, pending, refused, ...(stoppedBy && { stoppedBy }), unknown }))
|
|
29
|
+
}
|
|
30
|
+
/* held before the run, so it runs after every step the run holds: they have said what committed by then */
|
|
31
|
+
const release = hold((signal, earlier) => {
|
|
32
|
+
const found = earlier.find((each) => each instanceof Stopped)
|
|
33
|
+
/* a stop before any file applied: nothing committed, and no loop has a report to give */
|
|
34
|
+
const stopped = found ?? new Stopped(signal, `Stopped by ${signal}`, { pending: [] })
|
|
35
|
+
if (found === undefined) io.err(stopped.message)
|
|
36
|
+
report({ ...stopped, refused: [stopped.message], stoppedBy: signal, unknown: stopped.unknown })
|
|
37
|
+
})
|
|
38
|
+
try {
|
|
39
|
+
report({ ...(await run({ ...io, out: io.err })), refused: [], unknown: [] })
|
|
40
|
+
} catch (error) {
|
|
41
|
+
/* a refusal or a failed command is an Error; a user's config can throw anything */
|
|
42
|
+
const { applied, pending, unknown } = error instanceof Refused ? error : { applied: [], pending: [], unknown: [] }
|
|
43
|
+
report({ applied, pending, refused: [error instanceof Error ? error.message : String(error)], unknown })
|
|
44
|
+
throw new Reported(error)
|
|
45
|
+
} finally {
|
|
46
|
+
release()
|
|
47
|
+
}
|
|
48
|
+
}
|
package/src/cli/migrate.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/** `comb migrate`: init, diff, check, apply, baseline and status. */
|
|
2
2
|
import fs from "node:fs"
|
|
3
3
|
import path from "node:path"
|
|
4
|
-
import { parseArgs, refuseFurther } from "./args.ts"
|
|
4
|
+
import { parseArgs, refuseFurther, usageOf } from "./args.ts"
|
|
5
5
|
import { binOf } from "../migrate/core/optional-peer.ts"
|
|
6
6
|
import { declaredFts, ftsSql } from "./comb-config.ts"
|
|
7
7
|
import {
|
|
@@ -14,7 +14,8 @@ import {
|
|
|
14
14
|
} from "./drizzle-dialect.ts"
|
|
15
15
|
import { migrateApply, needPeers } from "./migrate-apply.ts"
|
|
16
16
|
import { type MigrateTarget, refuseBlank, refuseNoDir, resolveMigrateTarget } from "./migrate-target.ts"
|
|
17
|
-
import {
|
|
17
|
+
import { refusePathName } from "./file-path.ts"
|
|
18
|
+
import { CliError, type CliContext, ShowHelp } from "./output.ts"
|
|
18
19
|
import { scratchDir } from "../migrate/core/cleanup.ts"
|
|
19
20
|
|
|
20
21
|
const DRIZZLE_CONFIG = `import { defineConfig } from "drizzle-kit"
|
|
@@ -100,10 +101,12 @@ function migrateInit(argv: readonly string[], io: CliContext): void {
|
|
|
100
101
|
* database's dialect, directory and drizzle config come from it; without, it is SQLite in
|
|
101
102
|
* ./atlas-migrations.
|
|
102
103
|
*/
|
|
103
|
-
async function
|
|
104
|
+
async function runDiff(argv: readonly string[], io: CliContext): Promise<void> {
|
|
104
105
|
const args = parseArgs(argv, "migrate diff")
|
|
105
106
|
/* parseArgs refuses a diff that names no database */
|
|
106
107
|
const [db, name] = args.positionals as [string, (string | undefined)?]
|
|
108
|
+
/* Atlas makes the file's name from it: a path would land outside the directory, or nowhere */
|
|
109
|
+
if (name !== undefined) refusePathName(name, "Migration name", usageOf("migrate diff"))
|
|
107
110
|
const env = args.options["env"] as string | undefined
|
|
108
111
|
const { d1, expected, exported, fts, migrationsDir, pg, provider, target } = await diffTarget(db, env, io)
|
|
109
112
|
/* a diff starts a missing directory, and refuses a file before anything runs */
|
|
@@ -229,7 +232,26 @@ function reportDrift(db: string, files: readonly string[], io: CliContext): neve
|
|
|
229
232
|
io.out(`-- ${path.basename(file)}`)
|
|
230
233
|
io.out(fs.readFileSync(file, "utf8"))
|
|
231
234
|
}
|
|
232
|
-
throw new
|
|
235
|
+
throw new Drifted(`${db} has drifted from its migrations: comb migrate diff ${db} would write the SQL above`)
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** A dry run's refusal for drift, which exits 1 */
|
|
239
|
+
class Drifted extends CliError {}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* `migrate diff`. A dry run is a drift gate, so it exits 1 for drift alone, and 2 for every failure
|
|
243
|
+
* that kept it from checking, the argv's own refusal among them: --dry-run is read from argv, as
|
|
244
|
+
* the CLI reads --json, so that refusal knows it is a dry run's
|
|
245
|
+
*/
|
|
246
|
+
async function migrateDiff(argv: readonly string[], io: CliContext): Promise<void> {
|
|
247
|
+
if (!argv.includes("--dry-run")) return runDiff(argv, io)
|
|
248
|
+
try {
|
|
249
|
+
await runDiff(argv, io)
|
|
250
|
+
} catch (error) {
|
|
251
|
+
if (error instanceof Drifted || error instanceof ShowHelp) throw error
|
|
252
|
+
/* an Error: comb throws only Errors, and a config's load wraps whatever the config throws (migrate/config.ts) */
|
|
253
|
+
throw new CliError((error as Error).message, 2, { cause: error })
|
|
254
|
+
}
|
|
233
255
|
}
|
|
234
256
|
|
|
235
257
|
export async function cmdMigrate(argv: readonly string[], io: CliContext, help: string): Promise<void> {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { IntegerFormat, TextFormat } from "../formats/catalog.ts"
|
|
1
2
|
import type { CombColumnKind } from "../meta/column.ts"
|
|
2
3
|
/**
|
|
3
4
|
* TableMeta — serializable intermediate format produced by TableAnalyzer.
|
|
@@ -10,6 +11,8 @@ type FieldConstraintsMeta = {
|
|
|
10
11
|
autogenerate: boolean
|
|
11
12
|
/** the generated DTOs take only an email address */
|
|
12
13
|
email: boolean
|
|
14
|
+
/** the standard the value is held to: a text's or an enum's text format, an integer's integer format; null when none is declared */
|
|
15
|
+
format: TextFormat | IntegerFormat | null
|
|
13
16
|
/** an https URL or a data:image/ URI */
|
|
14
17
|
imageUri: boolean
|
|
15
18
|
/** the generated DTOs lowercase the text before they check it */
|
|
@@ -127,8 +130,10 @@ type FieldMeta = {
|
|
|
127
130
|
isPrimaryKey: boolean
|
|
128
131
|
/** the name the generated DTO imports a c.json column's schema under, null for any other column */
|
|
129
132
|
jsonSchemaName: string | null
|
|
130
|
-
/** the names the field's generated schema uses and must import
|
|
133
|
+
/** the names the field's generated schema uses and must import: a c.json schema, or the zod schema a text or number column's `schema` key names */
|
|
131
134
|
schemaImports: ImportRef[]
|
|
135
|
+
/** the input JSON Schema of the zod schema a text or number column's `schema` key names, self-contained and without `$schema`, which the generated DTOs publish beside it; null for any other column. The schema's import is the field's one `schemaImports` entry */
|
|
136
|
+
schemaJson: Readonly<Record<string, unknown>> | null
|
|
132
137
|
/** a Postgres array's declared dimensions; 0 or absent for a column of one value */
|
|
133
138
|
dimensions?: number | undefined
|
|
134
139
|
/** whether a Postgres array's elements may be null, as its drizzle type declares; false or absent otherwise */
|
|
@@ -5,12 +5,14 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import type { CombColumnKind } from "../meta/column.ts"
|
|
7
7
|
import type { ConstantValue, FieldConstraintsMeta } from "./analyzer-types.ts"
|
|
8
|
+
import { formatOf } from "./format-constraints.ts"
|
|
8
9
|
import { patternLiteral } from "./utils/pattern.ts"
|
|
9
10
|
|
|
10
11
|
/** Field constraints extracted from type arguments */
|
|
11
12
|
export type FieldConstraints = FieldConstraintsMeta
|
|
12
13
|
|
|
13
14
|
/* the keys of each constraint type in src/sqlite/constraints.ts, which tests/types/constraint-keys.typetest.ts holds them to */
|
|
15
|
+
/* a timestamp's keys: its bounds and the flags every helper takes */
|
|
14
16
|
export const NUMBER_KEYS = [
|
|
15
17
|
"autogenerate",
|
|
16
18
|
"max",
|
|
@@ -21,11 +23,57 @@ export const NUMBER_KEYS = [
|
|
|
21
23
|
"relation",
|
|
22
24
|
"tenant",
|
|
23
25
|
] as const
|
|
26
|
+
/* a real's keys: a timestamp's, and the zod schema its value is piped into */
|
|
27
|
+
export const REAL_KEYS = [
|
|
28
|
+
"autogenerate",
|
|
29
|
+
"max",
|
|
30
|
+
"min",
|
|
31
|
+
"mutable",
|
|
32
|
+
"nomutate",
|
|
33
|
+
"private",
|
|
34
|
+
"relation",
|
|
35
|
+
"schema",
|
|
36
|
+
"tenant",
|
|
37
|
+
] as const
|
|
38
|
+
/* an integer's keys: a real's, and the standard format that numbers its values */
|
|
39
|
+
export const INTEGER_KEYS = [
|
|
40
|
+
"autogenerate",
|
|
41
|
+
"format",
|
|
42
|
+
"max",
|
|
43
|
+
"min",
|
|
44
|
+
"mutable",
|
|
45
|
+
"nomutate",
|
|
46
|
+
"private",
|
|
47
|
+
"relation",
|
|
48
|
+
"schema",
|
|
49
|
+
"tenant",
|
|
50
|
+
] as const
|
|
24
51
|
export const JSON_KEYS = ["autogenerate", "maxBytes", "mutable", "nomutate", "private", "relation", "tenant"] as const
|
|
25
|
-
export const ENUM_KEYS = ["nomutate", "private", "tenant"] as const
|
|
52
|
+
export const ENUM_KEYS = ["format", "nomutate", "private", "tenant"] as const
|
|
53
|
+
/* a foreign key's keys: a text's but format and schema, as its value is another table's key */
|
|
54
|
+
export const REF_KEYS = [
|
|
55
|
+
"autogenerate",
|
|
56
|
+
"email",
|
|
57
|
+
"imageUri",
|
|
58
|
+
"lowercase",
|
|
59
|
+
"max",
|
|
60
|
+
"min",
|
|
61
|
+
"mutable",
|
|
62
|
+
"name",
|
|
63
|
+
"nomutate",
|
|
64
|
+
"password",
|
|
65
|
+
"pattern",
|
|
66
|
+
"private",
|
|
67
|
+
"relation",
|
|
68
|
+
"tenant",
|
|
69
|
+
"trim",
|
|
70
|
+
"uppercase",
|
|
71
|
+
"url",
|
|
72
|
+
] as const
|
|
26
73
|
export const TEXT_KEYS = [
|
|
27
74
|
"autogenerate",
|
|
28
75
|
"email",
|
|
76
|
+
"format",
|
|
29
77
|
"imageUri",
|
|
30
78
|
"lowercase",
|
|
31
79
|
"max",
|
|
@@ -37,6 +85,7 @@ export const TEXT_KEYS = [
|
|
|
37
85
|
"pattern",
|
|
38
86
|
"private",
|
|
39
87
|
"relation",
|
|
88
|
+
"schema",
|
|
40
89
|
"tenant",
|
|
41
90
|
"trim",
|
|
42
91
|
"uppercase",
|
|
@@ -53,10 +102,10 @@ export type ConstraintKeys = {
|
|
|
53
102
|
readonly deletedAt: Keys
|
|
54
103
|
readonly enum: typeof ENUM_KEYS
|
|
55
104
|
readonly id: None
|
|
56
|
-
readonly integer:
|
|
105
|
+
readonly integer: typeof INTEGER_KEYS
|
|
57
106
|
readonly json: typeof JSON_KEYS
|
|
58
|
-
readonly real:
|
|
59
|
-
readonly ref: typeof
|
|
107
|
+
readonly real: typeof REAL_KEYS
|
|
108
|
+
readonly ref: typeof REF_KEYS
|
|
60
109
|
readonly serialId: None
|
|
61
110
|
readonly text: typeof TEXT_KEYS
|
|
62
111
|
readonly timestamp: Keys
|
|
@@ -70,10 +119,10 @@ export const CONSTRAINT_KEYS: ConstraintKeys = {
|
|
|
70
119
|
deletedAt: NUMBER_KEYS,
|
|
71
120
|
enum: ENUM_KEYS,
|
|
72
121
|
id: [],
|
|
73
|
-
integer:
|
|
122
|
+
integer: INTEGER_KEYS,
|
|
74
123
|
json: JSON_KEYS,
|
|
75
|
-
real:
|
|
76
|
-
ref:
|
|
124
|
+
real: REAL_KEYS,
|
|
125
|
+
ref: REF_KEYS,
|
|
77
126
|
serialId: [],
|
|
78
127
|
text: TEXT_KEYS,
|
|
79
128
|
timestamp: NUMBER_KEYS,
|
|
@@ -115,10 +164,13 @@ const DROPPED: Readonly<Record<string, { keys: readonly string[]; said: string;
|
|
|
115
164
|
url: { keys: ["lowercase", "uppercase"], said: "it is kept as written, but for its trim", what: "a url" },
|
|
116
165
|
}
|
|
117
166
|
|
|
118
|
-
/** Refuses a second kind, or a key the one declared would drop, each of which a reader of the tables file believes holds */
|
|
167
|
+
/** Refuses a second kind, a format beside a kind, or a key the one declared would drop, each of which a reader of the tables file believes holds */
|
|
119
168
|
function refuseKinds(constraints: Readonly<Record<string, ConstantValue>>, place: string): void {
|
|
120
169
|
const [kind, second] = KINDS.filter((each) => constraints[each] === true)
|
|
121
170
|
if (second !== undefined) throw new Error(`${place}: ${kind} and ${second} contradict; declare one`)
|
|
171
|
+
/* a format is a text's whole rule, as a kind is */
|
|
172
|
+
if (kind !== undefined && constraints["format"] !== undefined)
|
|
173
|
+
throw new Error(`${place}: format and ${kind} contradict; declare one`)
|
|
122
174
|
const dropped = DROPPED[kind as string]
|
|
123
175
|
const key = dropped?.keys.find((each) => constraints[each] === true)
|
|
124
176
|
if (key !== undefined) throw new Error(`${place}: ${dropped?.what} takes no ${key}: ${dropped?.said}`)
|
|
@@ -180,7 +232,7 @@ function refuseMalformed(
|
|
|
180
232
|
/**
|
|
181
233
|
* A column's declared constraints, checked and with comb's defaults applied. Throws on a key the
|
|
182
234
|
* helper does not take, a value of the wrong type, two keys that contradict, or bounds no value
|
|
183
|
-
* meets, each message prefixed with `place`
|
|
235
|
+
* meets, or a format the helper cannot hold, each message prefixed with `place`
|
|
184
236
|
*/
|
|
185
237
|
export function fieldConstraintsOf(
|
|
186
238
|
constraints: Readonly<Record<string, ConstantValue>>,
|
|
@@ -190,6 +242,7 @@ export function fieldConstraintsOf(
|
|
|
190
242
|
helper,
|
|
191
243
|
measure,
|
|
192
244
|
place,
|
|
245
|
+
values,
|
|
193
246
|
}: {
|
|
194
247
|
/** the column's property name; one starting with `_` is private */
|
|
195
248
|
fieldName: string
|
|
@@ -201,9 +254,12 @@ export function fieldConstraintsOf(
|
|
|
201
254
|
measure: "length" | "value"
|
|
202
255
|
/** where the column is declared, as each error message names it */
|
|
203
256
|
place: string
|
|
257
|
+
/** the column's enum values, each of which a c.enum's format must take; null when it has none */
|
|
258
|
+
values: readonly string[] | null
|
|
204
259
|
},
|
|
205
260
|
): FieldConstraints {
|
|
206
261
|
refuseMalformed(constraints, { helper, measure, place })
|
|
262
|
+
const format = formatOf(constraints, { helper, place, values })
|
|
207
263
|
const relation = constraints["relation"]
|
|
208
264
|
|
|
209
265
|
const autogenerate = filled ? constraints["autogenerate"] !== false : constraints["autogenerate"] === true
|
|
@@ -211,6 +267,7 @@ export function fieldConstraintsOf(
|
|
|
211
267
|
return {
|
|
212
268
|
autogenerate,
|
|
213
269
|
email: constraints["email"] === true,
|
|
270
|
+
format,
|
|
214
271
|
imageUri: constraints["imageUri"] === true,
|
|
215
272
|
lowercase: constraints["lowercase"] === true,
|
|
216
273
|
max: typeof constraints["max"] === "number" ? constraints["max"] : null,
|