@workerstack/database 0.21.0 → 0.22.0
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 +197 -33
- package/docs/meta-contract.md +55 -1
- package/package.json +1 -6
- 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/pg/columns.ts +6 -4
- package/src/sqlite/columns.ts +6 -4
- package/src/sqlite/constraints.ts +120 -6
- 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/pg/columns.d.ts +3 -3
- package/types/pg/columns.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/README.md
CHANGED
|
@@ -4,6 +4,8 @@ Database framework for Cloudflare D1, PostgreSQL, and SQLite. Dialect-agnostic c
|
|
|
4
4
|
|
|
5
5
|
This repo is the source of the [`@workerstack/database`](https://www.npmjs.com/package/@workerstack/database) npm package. The CLI binary is `comb`.
|
|
6
6
|
|
|
7
|
+
Moved from `@lovrozagar/comb`: `@workerstack/database@0.21.0` is the same release as `@lovrozagar/comb@0.21.0` under the new name, and the old package is deprecated. To switch, follow step 3 of [Upgrading to 0.22](#upgrading-to-022).
|
|
8
|
+
|
|
7
9
|
This README is the full usage manual. An agent that reads only this file should be able to define tables, generate artifacts, run migrations, and serve list queries without opening source.
|
|
8
10
|
|
|
9
11
|
## Table of contents
|
|
@@ -18,7 +20,8 @@ This README is the full usage manual. An agent that reads only this file should
|
|
|
18
20
|
- [SQLite factories](#sqlite-factories)
|
|
19
21
|
- [PostgreSQL factories](#postgresql-factories)
|
|
20
22
|
- [Constraints](#constraints)
|
|
21
|
-
- [
|
|
23
|
+
- [Formats](#formats)
|
|
24
|
+
- [Column schemas](#column-schemas)
|
|
22
25
|
- [Errors](#errors)
|
|
23
26
|
- [`CombError`](#comberror)
|
|
24
27
|
- [Constraint maps](#constraint-maps)
|
|
@@ -49,6 +52,7 @@ This README is the full usage manual. An agent that reads only this file should
|
|
|
49
52
|
- [Package exports](#package-exports)
|
|
50
53
|
- [Repository layout](#repository-layout)
|
|
51
54
|
- [Develop](#develop)
|
|
55
|
+
- [Upgrading to 0.22](#upgrading-to-022)
|
|
52
56
|
- [Upgrading to 0.21](#upgrading-to-021)
|
|
53
57
|
- [Upgrading to 0.20.1](#upgrading-to-0201)
|
|
54
58
|
- [Upgrading to 0.20](#upgrading-to-020)
|
|
@@ -150,7 +154,6 @@ Define a table with Comb's column factories instead of Drizzle's raw ones.
|
|
|
150
154
|
/* src/db/core.tables.ts */
|
|
151
155
|
import { sqliteTable, index } from "drizzle-orm/sqlite-core"
|
|
152
156
|
import { c } from "@workerstack/database/sqlite/columns"
|
|
153
|
-
import { patterns } from "@workerstack/database/sqlite/patterns"
|
|
154
157
|
import * as z from "zod"
|
|
155
158
|
|
|
156
159
|
/* a c.json schema is exported, so the generated DTOs can import it, and is the stored document:
|
|
@@ -163,7 +166,7 @@ export const author = sqliteTable(
|
|
|
163
166
|
id: c.id("aut"),
|
|
164
167
|
email: c.text("email", { max: 254, email: true, lowercase: true, trim: true }),
|
|
165
168
|
display_name: c.text("display_name", { min: 2, max: 64, trim: true }),
|
|
166
|
-
locale: c.text("locale", {
|
|
169
|
+
locale: c.text("locale", { format: "bcp47" }),
|
|
167
170
|
settings: c.json("settings", authorSettings),
|
|
168
171
|
created_at: c.createdAt("created_at"),
|
|
169
172
|
updated_at: c.updatedAt("updated_at"),
|
|
@@ -183,7 +186,7 @@ export const post = sqliteTable("post", {
|
|
|
183
186
|
})
|
|
184
187
|
```
|
|
185
188
|
|
|
186
|
-
The second argument to each factory is a **constraint object**. At runtime most of it is inert — `c.text("title", { max: 200 })` just becomes `text("title", { length: 200 })` — and the helper records it on the column for codegen, which turns `{ min: 2, max: 64, trim: true }` into a Zod schema, a CHECK constraint, and an OpenAPI description. Constraints live next to the column, once. Codegen loads the tables module, so a constraint value is whatever the module computes: a constant (`pattern:
|
|
189
|
+
The second argument to each factory is a **constraint object**. At runtime most of it is inert — `c.text("title", { max: 200 })` just becomes `text("title", { length: 200 })` — and the helper records it on the column for codegen, which turns `{ min: 2, max: 64, trim: true }` into a Zod schema, a CHECK constraint, and an OpenAPI description. Constraints live next to the column, once. Codegen loads the tables module, so a constraint value is whatever the module computes: a constant (`pattern: SKU_PATTERN`, `max: NAME_MAX`), an enum member, an object spread over another, a constraint object passed by name, or an enum list built by a call. Each value must be a boolean, a number or a string; `pattern` is a `/source/flags` string, and `schema`, the one value of another kind, an exported zod schema ([Column schemas](#column-schemas)).
|
|
187
190
|
|
|
188
191
|
Point a config at the file:
|
|
189
192
|
|
|
@@ -322,6 +325,8 @@ The constraint object is shared by both dialects. `@workerstack/database/sqlite/
|
|
|
322
325
|
| `min` / `max` | text, integer, real | length or numeric bounds |
|
|
323
326
|
| `maxBytes` | json | UTF-8 byte bound on the serialized value, on writes |
|
|
324
327
|
| `pattern` | text | regex, as a source-level string |
|
|
328
|
+
| `format` | text, enum, integer | a standard by its id: a text's pattern, case, length and registry, an enum's values checked at codegen, an integer's range ([Formats](#formats)) |
|
|
329
|
+
| `schema` | text, integer, real | an exported zod schema the write DTOs pipe the value into after comb's own rules ([Column schemas](#column-schemas)) |
|
|
325
330
|
| `email` / `url` | text | an email checked by zod's email pattern, which publishes; a url by the WHATWG URL parser, https only, its https pattern published; neither publishes a `format` |
|
|
326
331
|
| `trim`, `lowercase`, `uppercase` | text | transforms applied in generated schemas |
|
|
327
332
|
| `password` | text | a stored password hash: kept as written, held to its declared bounds and pattern alone; `passwordText()` holds the plaintext |
|
|
@@ -350,19 +355,155 @@ const signIn = z.object({ email: z.email(), password: passwordText.typed() })
|
|
|
350
355
|
|
|
351
356
|
`passwordText()` sets a password: any storable text of 15 to 128 characters, counted by code point, with no composition rule and nothing trimmed. A password used only beside a second factor may take `passwordText({ min: 8 })`, as rev 4 allows; `min` and `max` are yours to set. `passwordText.typed()` reads one typed to sign in and holds only the max, so a password set under an older or shorter minimum still signs in. Both normalize the value to NFC before they count it, and give the NFC string, so a password typed in another normalization hashes to the same value; an app that adopts it over hashes made from input it never normalized rehashes each one on its next successful sign-in. Under `{ hash: "bcrypt" }` both refuse past 72 UTF-8 bytes, which bcrypt reads no further than, and publish `x-comb-maxUtf8Bytes: 72`, the string's own bytes (`x-comb-maxBytes` is a JSON text's); `passwordText({ hash: "bcrypt", min })` with a `min` past 72 throws, as no password would pass it. The JSON Schema states the rewrite as `x-comb-transform: ["nfc"]` and the bounds as `minLength` and `maxLength`, which count the NFC form's code points, and a password out of bounds is zod's own `too_small` or `too_big` issue with its `minimum` or `maximum`, bcrypt's bytes included.
|
|
352
357
|
|
|
353
|
-
###
|
|
358
|
+
### Formats
|
|
359
|
+
|
|
360
|
+
`format` holds a column to a published standard, named by its id: a `c.text` value's syntax, case and length, and its membership where the standard is a registry; a `c.enum`'s listed values, at codegen; a `c.integer`'s range. Like every constraint it shapes the generated DTOs alone, with no CHECK and no change to the column's type, the same on SQLite, D1 and Postgres, and a column stays nullable unless `.notNull()`: a null passes, a value is checked.
|
|
361
|
+
|
|
362
|
+
```ts
|
|
363
|
+
import { sqliteTable } from "drizzle-orm/sqlite-core"
|
|
364
|
+
import { c } from "@workerstack/database/sqlite/columns"
|
|
365
|
+
|
|
366
|
+
export const users = sqliteTable("users", {
|
|
367
|
+
id: c.id("usr"),
|
|
368
|
+
currency: c.text("currency", { format: "iso4217" }),
|
|
369
|
+
time_zone: c.text("time_zone", { format: "iana-time-zone" }),
|
|
370
|
+
locale: c.text("locale", { format: "bcp47" }),
|
|
371
|
+
country: c.text("country", { format: "iso3166-1-alpha-2" }),
|
|
372
|
+
pay_currency: c.enum("pay_currency", ["USD", "EUR", "GBP"], { format: "iso4217" }),
|
|
373
|
+
hour_cycle: c.enum("hour_cycle", ["h12", "h23"], { format: "uts35-hc" }),
|
|
374
|
+
week_start: c.integer("week_start", { format: "iso8601-weekday" }), // 1 Monday … 7 Sunday
|
|
375
|
+
})
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
A text format trims the value, then writes it in its standard's case, before it checks it. The table names each text format's standard and release, the case a written value is put in, and the registry it must also belong to; an editor shows the same list on the `format` key.
|
|
379
|
+
|
|
380
|
+
| Format | Standard | Case | Registry |
|
|
381
|
+
| --------------------- | ---------------------------------------------------------------------- | ---------- | ----------------- |
|
|
382
|
+
| `base64url` | RFC 4648 §5 base64url, unpadded | kept | |
|
|
383
|
+
| `bcp47` | BCP 47 language tag, RFC 5646 §2.1 well-formed | kept | |
|
|
384
|
+
| `e164` | ITU-T E.164 phone number, + and up to 15 digits | kept | |
|
|
385
|
+
| `iana-time-zone` | IANA time zone name, tzdata 2026f | kept | `isIanaTimeZone` |
|
|
386
|
+
| `ip` | IPv4 dotted quad or IPv6 address, RFC 4291 §2.2 | lowercased | |
|
|
387
|
+
| `ipv4` | IPv4 address, dotted quad without leading zeros | kept | |
|
|
388
|
+
| `ipv6` | IPv6 address, RFC 4291 §2.2 | lowercased | |
|
|
389
|
+
| `iso3166-1-alpha-2` | ISO 3166-1 alpha-2 country code, Debian iso-codes v4.20.1 | uppercased | `isIso3166Alpha2` |
|
|
390
|
+
| `iso3166-1-alpha-3` | ISO 3166-1 alpha-3 country code, Debian iso-codes v4.20.1 | uppercased | `isIso3166Alpha3` |
|
|
391
|
+
| `iso4217` | ISO 4217 alphabetic currency code, SIX list 2026-09-17 | uppercased | `isIso4217` |
|
|
392
|
+
| `iso639-1` | ISO 639-1 language code, Debian iso-codes v4.20.1 | lowercased | `isIso639Alpha2` |
|
|
393
|
+
| `kebab-slug` | kebab-case slug, lowercase letters and digits joined by single hyphens | kept | |
|
|
394
|
+
| `pkce-s256-challenge` | RFC 7636 §4.2 PKCE SHA-256 code challenge | kept | |
|
|
395
|
+
| `rfc1123-hostname` | RFC 1123 §2.1 host name | lowercased | |
|
|
396
|
+
| `semver` | Semantic Versioning 2.0.0 version | kept | |
|
|
397
|
+
| `sha256-hex` | SHA-256 digest, FIPS 180-4, lowercase hex | lowercased | |
|
|
398
|
+
| `sha512-hex` | SHA-512 digest, FIPS 180-4, lowercase hex | lowercased | |
|
|
399
|
+
| `srgb-hex` | sRGB color as #rrggbb, CSS Color 4 | lowercased | |
|
|
400
|
+
| `srgb-hex-alpha` | sRGB color with alpha as #rrggbbaa, CSS Color 4 | lowercased | |
|
|
401
|
+
| `ulid` | ULID, Crockford base32 in lowercase | lowercased | |
|
|
402
|
+
| `uts35-ca` | UTS #35 calendar (ca) key value, CLDR 49 | lowercased | |
|
|
403
|
+
| `uts35-fw` | UTS #35 first day of the week (fw) key value, CLDR 49 | lowercased | |
|
|
404
|
+
| `uts35-hc` | UTS #35 hour cycle (hc) key value, CLDR 49 | lowercased | |
|
|
405
|
+
| `uts35-ms` | UTS #35 measurement system (ms) key value, CLDR 49 | lowercased | |
|
|
406
|
+
| `uts35-mu` | UTS #35 measurement unit (mu) key value, CLDR 49 | lowercased | |
|
|
407
|
+
| `uts35-nu` | UTS #35 numbering system (nu) key value, CLDR 49 | lowercased | |
|
|
408
|
+
| `uuid` | RFC 9562 UUID, lowercase hex | lowercased | |
|
|
409
|
+
| `uuid-v4` | RFC 9562 UUID version 4, lowercase hex | lowercased | |
|
|
410
|
+
| `uuid-v7` | RFC 9562 UUID version 7, lowercase hex | lowercased | |
|
|
411
|
+
|
|
412
|
+
A text format's rule is its pattern, which Go's RE2, Rust's regex and JavaScript's `u` mode read alike, and its length bounds, published as `pattern`, `minLength` and `maxLength` beside `x-comb-format` and the `x-comb-transform` it runs (`["trim", "uppercase"]` on `iso4217`), and its OpenAPI example is a value of the standard as written (`EUR` on `iso4217`), none where the column's own bounds leave that value out. It takes no `x-comb-text`, as no format's pattern admits U+0000 or a surrogate. The format sets the column's trim, case and length, so codegen refuses a `trim`, `lowercase` or `uppercase` beside it (`users.currency: an iso4217 text takes no lowercase: the format sets its case and trim`), a `min` or `max` outside its range, which the format already holds or which leaves no value (`users.currency: max 4 is past iso4217's length 3: the format holds it`), and an `email`, `url`, `password`, `imageUri` or `name` beside it, each a rule of its own (`users.currency: format and email contradict; declare one`). A bound inside the range, or a `pattern`, beside a format narrows it. `bcp47` sets no maximum, as RFC 5646's 35 characters are the least an implementation must hold, not a cap on a tag, so a column declares its own `max`. An id comb does not know is refused with the list it does (`users.currency: comb knows no format "iso-4217"; text formats: base64url, bcp47, …`).
|
|
413
|
+
|
|
414
|
+
Where the table names a predicate, a value must also be a member of the pinned registry it reads, which no pattern states: `HRK` matches `^[A-Z]{3}$` and is no `iso4217` value, withdrawn in 2023, and `Europe/Atlantis` matches the zone pattern and names no zone. Each registry is a snapshot of the release the table names, committed in comb's source: nothing fetches at build, test or run time, and a comb release moves to a newer one. The five predicates are root exports, each taking any string, exactly as written, and never throwing; a bundle that calls none of them carries none of their lists.
|
|
415
|
+
|
|
416
|
+
```ts
|
|
417
|
+
import { isIanaTimeZone, isIso3166Alpha2, isIso3166Alpha3, isIso4217, isIso639Alpha2 } from "@workerstack/database"
|
|
418
|
+
|
|
419
|
+
isIso4217("EUR") // true
|
|
420
|
+
isIso4217("HRK") // false: withdrawn in 2023
|
|
421
|
+
isIso4217("eur") // false: the DTO uppercases a value first, the predicate does not
|
|
422
|
+
isIso3166Alpha2("UK") // false: reserved, not assigned; GB is the code
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
A reader that validates from the JSON Schema alone checks such a value with the predicate, or with the release meta-contract §2.1 names beside the id. The `uts35-*` formats are the values of UTS #35's Unicode locale extension keys in CLDR 49, deprecated ones left out (`uts35-hc` is `h11`, `h12`, `h23`, `h24`, `c12` and `c24`); their pattern lists every value, so it is the whole rule, as on every format the table names no registry for.
|
|
426
|
+
|
|
427
|
+
On a `c.enum` the listed values stay the rule, and `format` says what they mean: codegen refuses a value the format does not take as written (`users.pay_currency: "usd" is no iso4217 value; write the standard's form, "USD"`, `users.pay_currency: "HRK" is no iso4217 value`), and every DTO of the column publishes its `enum` with `x-comb-format`.
|
|
428
|
+
|
|
429
|
+
`c.integer` takes the two integer formats: `iso8601-month`, 1 January to 12 December, and `iso8601-weekday`, 1 Monday to 7 Sunday (ISO 8601 §3.2.2), the numbering `Intl.Locale`'s `getWeekInfo().firstDay` returns. The write DTOs hold the value to the standard's range, or to a declared `min` or `max` inside it, published as `minimum` and `maximum` beside `x-comb-format`, and a value outside it gets zod's own `too_small` or `too_big`; codegen refuses a bound past the standard's (`users.week_start: max 8 is past iso8601-weekday's 7: the format holds it`) and a text format's id (`users.week_start: c.integer takes no format "iso4217"; integer formats: iso8601-month, iso8601-weekday`). `c.real`, `c.ref`, `c.json` and the timestamps take no format: TypeScript refuses one, and codegen refuses it from a tables module in JavaScript (`users.owner_id: c.ref takes no format`).
|
|
430
|
+
|
|
431
|
+
A text its format refuses gets exactly one issue, zod's `invalid_format` with the id as its `format` and `origin: "string"`, and no message of comb's, so zod words it in the locale you configure, and an error map can key a message of your own on the id:
|
|
432
|
+
|
|
433
|
+
```ts
|
|
434
|
+
import * as z from "zod"
|
|
435
|
+
|
|
436
|
+
// " eu " or "HRK" on an iso4217 field:
|
|
437
|
+
// { code: "invalid_format", format: "iso4217", origin: "string", path: ["currency"], message: "Invalid iso4217" }
|
|
438
|
+
|
|
439
|
+
z.config(z.locales.de()) // message: "Ungültig: iso4217"
|
|
440
|
+
|
|
441
|
+
z.config({
|
|
442
|
+
customError: (issue) =>
|
|
443
|
+
issue.code === "invalid_format" && issue.format === "iso4217" ? "Unknown currency" : undefined,
|
|
444
|
+
})
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
An error map that returns `undefined` leaves the issue to the locale, as that one does for every other issue.
|
|
448
|
+
|
|
449
|
+
### Column schemas
|
|
450
|
+
|
|
451
|
+
`schema` takes a rule comb has no key for: a zod schema the create and update DTOs pipe a `c.text`, `c.integer` or `c.real` value into after every rule of comb's, so it reads the value trimmed, cased and checked. Codegen imports it as it imports a `c.json` schema, so it must be exported, and it is best in a module of its own:
|
|
452
|
+
|
|
453
|
+
```ts
|
|
454
|
+
/* src/db/schemas.ts: a module of its own, so a DTO that imports a schema loads zod and this module alone */
|
|
455
|
+
import * as z from "zod"
|
|
456
|
+
|
|
457
|
+
export const dateOrder = z.enum(["iso", "dmy", "mdy"])
|
|
458
|
+
export const evenMinutes = z.int().refine((value) => value % 2 === 0, { error: "Must be even" })
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
The tables module imports both, `import { dateOrder, evenMinutes } from "./schemas.ts"`, and names each on its column:
|
|
354
462
|
|
|
355
|
-
|
|
463
|
+
<!-- ts-context
|
|
464
|
+
declare const dateOrder: import("zod").ZodType<string, string>
|
|
465
|
+
declare const evenMinutes: import("zod").ZodType<number, number>
|
|
466
|
+
-->
|
|
356
467
|
|
|
357
468
|
```ts
|
|
358
|
-
import {
|
|
469
|
+
import { sqliteTable } from "drizzle-orm/sqlite-core"
|
|
470
|
+
import { c } from "@workerstack/database/sqlite/columns"
|
|
471
|
+
|
|
472
|
+
export const users = sqliteTable("users", {
|
|
473
|
+
id: c.id("usr"),
|
|
474
|
+
date_order: c.text("date_order", { schema: dateOrder, trim: true }),
|
|
475
|
+
slot_minutes: c.integer("slot_minutes", { min: 0, schema: evenMinutes }),
|
|
476
|
+
})
|
|
477
|
+
```
|
|
359
478
|
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
479
|
+
A schema exported from the tables module works too, but then every DTO that imports it loads the tables module and drizzle with it, a frontend form's bundle among them, and as a `const` it must sit above the table that names it: one below throws a `ReferenceError` as the module loads, which codegen reports in the module's own words. An inline schema type-checks, yet codegen refuses it, as it can only import an export: `users.date_order: no module of the project exports the schema its schema key is given; export it, above the table that names it, so the generated DTOs can import it`, with the note of packages that would not load on their own that a `c.json` schema's error carries.
|
|
480
|
+
|
|
481
|
+
The text key takes a `z.ZodType<string, string>` and the number keys a `z.ZodType<number, number>`, so a `z.int()` on a text, or a transform to another type, fails to compile. The schema must come from the `zod` the generated DTOs import, one `zod` in the project's tree, as a `c.json` schema must. Codegen refuses a value that is no zod schema (`users.date_order: the constraint schema is no zod schema; comb's DTOs are zod 4, so the schema must be one`) and one with no JSON Schema form, such as a `z.custom()`, keeping zod's reason (`… has no JSON Schema form (zod: Custom types cannot be represented in JSON Schema); comb publishes every DTO rule`).
|
|
482
|
+
|
|
483
|
+
Its issues reach the caller exactly as zod yields them, and comb adds no code or message: `"x"` on `date_order` is the enum's own `invalid_value` with its values, and `3` on `slot_minutes` is the refine's `custom` issue, `"Must be even"`. A value comb's own rules refuse never reaches the schema, so `-3` there gets the `too_small` of `min: 0` alone.
|
|
484
|
+
|
|
485
|
+
The JSON Schema publishes the schema's export name as `x-comb-schema` and the schema's own input JSON Schema in an `allOf` beside it, which codegen computes with `z.toJSONSchema(schema, { io: "input" })` and writes as a literal; on a nullable field the `allOf` sits on the non-null member of its `anyOf`, as the schema's `type` would refuse the null the field takes. A nullable `date_order` publishes:
|
|
486
|
+
|
|
487
|
+
```text
|
|
488
|
+
{
|
|
489
|
+
"anyOf": [
|
|
490
|
+
{
|
|
491
|
+
"type": "string",
|
|
492
|
+
"allOf": [{ "type": "string", "enum": ["iso", "dmy", "mdy"] }],
|
|
493
|
+
"x-comb-schema": "dateOrder",
|
|
494
|
+
"x-comb-text": "no U+0000 or unpaired surrogate",
|
|
495
|
+
"x-comb-transform": ["trim"]
|
|
496
|
+
},
|
|
497
|
+
{ "type": "null" }
|
|
498
|
+
],
|
|
499
|
+
"x-comb-schema": "dateOrder",
|
|
500
|
+
"x-comb-text": "no U+0000 or unpaired surrogate",
|
|
501
|
+
"x-comb-transform": ["trim"]
|
|
502
|
+
}
|
|
364
503
|
```
|
|
365
504
|
|
|
505
|
+
A refine publishes nothing: `evenMinutes` publishes `{ "type": "integer" }` with a safe integer's range and no word of evenness. `x-comb-schema` is how a reader that runs no JavaScript learns the producer runs a rule it cannot see, and such a reader may take a value the server then refuses. A column with a `schema` carries no generated example, as a refine its JSON Schema does not state may refuse any value comb would pick. The read DTO runs no schema, as a read never refuses a stored row, and the manifest names the export as the column's `schema`. `c.enum`, `c.json`, `c.ref` and the timestamps take no `schema` (`users.status: c.enum takes no schema`): an enum's values and a `c.json` schema are rules of their own, and a reference's value is another table's key.
|
|
506
|
+
|
|
366
507
|
## Errors
|
|
367
508
|
|
|
368
509
|
### `CombError`
|
|
@@ -1057,28 +1198,27 @@ ULIDs use Crockford base32 with a 10-character timestamp and 16 characters of ra
|
|
|
1057
1198
|
|
|
1058
1199
|
## Package exports
|
|
1059
1200
|
|
|
1060
|
-
| Specifier
|
|
1061
|
-
|
|
|
1062
|
-
| `@workerstack/database` | `CombError`, `createUlidFactory`, `generateId`, `generateUlid`, `LruCache`, error keys and types
|
|
1063
|
-
| `@workerstack/database/types` | `StatusKey`, `ConstraintMap`, `DatabaseErrorHandler`, …
|
|
1064
|
-
| `@workerstack/database/cli` | `runCli(argv, io)`: run the CLI in process and get its exit code
|
|
1065
|
-
| `@workerstack/database/codegen` | `analyze`, `validateTables`, `fieldConstraintsOf`, analysis types
|
|
1066
|
-
| `@workerstack/database/codegen/generators` | every `generate*` function
|
|
1067
|
-
| `@workerstack/database/meta` | `combMeta`, `readCombMeta`, `readCombEntityMeta`, `readCombQueryMeta`, `carryCombMeta`, meta types
|
|
1068
|
-
| `@workerstack/database/migrate` | config, providers, apply, parse, Atlas helpers
|
|
1069
|
-
| `@workerstack/database/query` | filter, order, select, cursors, Zod builders
|
|
1070
|
-
| `@workerstack/database/query/sqlite` | SQL generation, FTS5, JSON helpers
|
|
1071
|
-
| `@workerstack/database/sqlite/columns` | `c` for SQLite
|
|
1072
|
-
| `@workerstack/database/sqlite/constraints` | constraint types
|
|
1073
|
-
| `@workerstack/database/sqlite/
|
|
1074
|
-
| `@workerstack/database/sqlite/
|
|
1075
|
-
| `@workerstack/database/sqlite/d1/
|
|
1076
|
-
| `@workerstack/database/sqlite/d1/
|
|
1077
|
-
| `@workerstack/database/sqlite/d1/
|
|
1078
|
-
| `@workerstack/database/
|
|
1079
|
-
| `@workerstack/database/
|
|
1080
|
-
| `@workerstack/database/pg/
|
|
1081
|
-
| `@workerstack/database/pg/error-handler` | `pgErrorHandler`, `classifyPgError` |
|
|
1201
|
+
| Specifier | Contents |
|
|
1202
|
+
| ------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
|
|
1203
|
+
| `@workerstack/database` | `CombError`, `createUlidFactory`, `generateId`, `generateUlid`, `LruCache`, the format registry predicates, error keys and types |
|
|
1204
|
+
| `@workerstack/database/types` | `StatusKey`, `ConstraintMap`, `DatabaseErrorHandler`, … |
|
|
1205
|
+
| `@workerstack/database/cli` | `runCli(argv, io)`: run the CLI in process and get its exit code |
|
|
1206
|
+
| `@workerstack/database/codegen` | `analyze`, `validateTables`, `fieldConstraintsOf`, analysis types |
|
|
1207
|
+
| `@workerstack/database/codegen/generators` | every `generate*` function |
|
|
1208
|
+
| `@workerstack/database/meta` | `combMeta`, `readCombMeta`, `readCombEntityMeta`, `readCombQueryMeta`, `carryCombMeta`, meta types |
|
|
1209
|
+
| `@workerstack/database/migrate` | config, providers, apply, parse, Atlas helpers |
|
|
1210
|
+
| `@workerstack/database/query` | filter, order, select, cursors, Zod builders |
|
|
1211
|
+
| `@workerstack/database/query/sqlite` | SQL generation, FTS5, JSON helpers |
|
|
1212
|
+
| `@workerstack/database/sqlite/columns` | `c` for SQLite |
|
|
1213
|
+
| `@workerstack/database/sqlite/constraints` | constraint types |
|
|
1214
|
+
| `@workerstack/database/sqlite/constraint-handler` | `sqliteErrorHandler`, `classifySqliteError`, `handleConstraintViolation` |
|
|
1215
|
+
| `@workerstack/database/sqlite/d1/session` | `openD1Session` |
|
|
1216
|
+
| `@workerstack/database/sqlite/d1/bookmark` | `BookmarkTracker` |
|
|
1217
|
+
| `@workerstack/database/sqlite/d1/error-handler` | `d1ErrorHandler`, `classifyD1Error` |
|
|
1218
|
+
| `@workerstack/database/sqlite/d1/sharding` | `ShardRouter`, `shardMap` |
|
|
1219
|
+
| `@workerstack/database/states` | `defineStateMachine`, `canTransition`, `assertTransition`, `nextStates`, `isState`, `isTerminal` |
|
|
1220
|
+
| `@workerstack/database/pg/columns` | `c` for PostgreSQL |
|
|
1221
|
+
| `@workerstack/database/pg/error-handler` | `pgErrorHandler`, `classifyPgError` |
|
|
1082
1222
|
|
|
1083
1223
|
## Repository layout
|
|
1084
1224
|
|
|
@@ -1126,6 +1266,30 @@ bun run test:bun # bun test — the bun:sqlite replay suite
|
|
|
1126
1266
|
|
|
1127
1267
|
Commits follow [Conventional Commits](https://www.conventionalcommits.org). `bun install` points `core.hooksPath` at `.githooks`, so the `commit-msg` hook enforces it locally; CI re-checks every commit on a pull request.
|
|
1128
1268
|
|
|
1269
|
+
## Upgrading to 0.22
|
|
1270
|
+
|
|
1271
|
+
What changes for a project on 0.21. Before deploying it:
|
|
1272
|
+
|
|
1273
|
+
1. **Know what changed in the API.** A test holds these three lists to the exports themselves.
|
|
1274
|
+
|
|
1275
|
+
New exports: `IntegerFormat`, `RealConstraints`, `RefConstraints`, `TextFormat`, `isIanaTimeZone`, `isIso3166Alpha2`, `isIso3166Alpha3`, `isIso4217`, `isIso639Alpha2`.
|
|
1276
|
+
|
|
1277
|
+
Removed: `patterns`, with its entry point, @workerstack/database/sqlite/patterns. **Fix:** declare the format that holds the standard a pattern of it stood for, as item 2 maps each one.
|
|
1278
|
+
|
|
1279
|
+
Changed: `AnalysisResult`, `CodegenConfig`, `DatabaseCodegen`, `EnumOptions`, `FieldConstraints`, `FieldConstraintsMeta`, `FieldMeta`, `GeneratorContext`, `IntegerConstraints`, `Manifest`, `ManifestColumn`, `ManifestEntity`, `OrmConfig`, `PgColumns`, `ShardMapColumns`, `ShardMapTable`, `SqliteColumns`, `TableMeta`, `TextConstraints`, `analyze`, `buildManifest`, `c`, `defineCodegenConfig`, `deriveEntityMeta`, `fieldConstraintsOf`, `generateColumns`, `generateConstraints`, `generateDiagrams`, `generateDtos`, `generateEntities`, `generateEnumChecks`, `generateFieldNames`, `generateFts`, `generateFtsSql`, `generateManifest`, `generateOrm`, `generateRelations`, `generateRows`, `generateTranslations`, `isExcludedFromUpdate`, `shardMap`, `validateTables`.
|
|
1280
|
+
|
|
1281
|
+
`TextConstraints` gains `format` and `schema`, `IntegerConstraints` gains `format` and `schema`, and `EnumOptions` gains `format`, each optional ([Formats](#formats), [Column schemas](#column-schemas)), a `format` standing where a regex of the removed `patterns` did (step 2). `c.real` takes `RealConstraints`, a real's bounds and `schema` with no `format`, where 0.21 typed it with `IntegerConstraints`, and `c.ref` takes `RefConstraints`, a text's keys but `format` and `schema`. The `c` of @workerstack/database/sqlite/columns, @workerstack/database/pg/columns and @workerstack/database/sqlite/d1/sharding changes with them, as do `SqliteColumns`, `PgColumns`, `ShardMapColumns`, `ShardMapTable` and `shardMap`. **Fix:** none: a call that compiled on 0.21, one passing a value typed `IntegerConstraints` to `c.real` or `TextConstraints` to `c.ref` among them, compiles on 0.22.
|
|
1282
|
+
|
|
1283
|
+
`ManifestColumn` gains `format`, the id of the standard a write checks the value against, and `schema`, the export name of the zod schema a text or number column declares, each null when the column declares none and `schema` null on a `c.json` column, whose schema the manifest already describes. `FieldConstraintsMeta`, which the package root, @workerstack/database/codegen, @workerstack/database/meta and @workerstack/database/query reach, gains `format`, and `FieldMeta` gains `schemaJson`, the schema's input JSON Schema, which the DTOs publish. Each is required, as every field of these types is (`ManifestColumn.format`, `ManifestColumn.schema`, `FieldConstraintsMeta.format`, `FieldMeta.schemaJson`), so a `ManifestColumn` or `FieldConstraintsMeta` you build fails to compile (TS2741). The manifest stays `v` 3, and a manifest reader meets the two fields as nulls on every column that declares neither. **Fix:** a `ManifestColumn` you build sets `format: null` and `schema: null`, a `FieldConstraintsMeta` sets `format: null`, and a `FieldMeta` sets `schemaJson: null`.
|
|
1284
|
+
|
|
1285
|
+
`fieldConstraintsOf` takes `values`, the column's enum values, each of which a `c.enum`'s format must take, or `null` for a column with none. **Fix:** a direct caller of `fieldConstraintsOf` passes `values`.
|
|
1286
|
+
|
|
1287
|
+
The rest of the list changes only through those types: `FieldConstraints` is `FieldConstraintsMeta`; `AnalysisResult`, `TableMeta`, `Manifest` and `ManifestEntity` hold a `FieldMeta` or a `ManifestColumn`; and `analyze`, `buildManifest`, `validateTables`, `deriveEntityMeta`, `isExcludedFromUpdate`, `CodegenConfig`, `DatabaseCodegen`, `GeneratorContext`, `OrmConfig`, `defineCodegenConfig` and each `generate*` function take or return one. **Fix:** none.
|
|
1288
|
+
|
|
1289
|
+
2. **Declare a `format` where a `patterns` regex stood.** Each of the four stood for a standard it held loosely: `COUNTRY_ALPHA2` took any two capitals, `UK` and `XX` among them; `E164_PHONE` refused a number of fewer than 8 digits; `IANA_TIMEZONE` took `a/b/c` and refused `Etc/GMT+5` and `America/Port-au-Prince`; `LOCALE` refused `zh-Hant-TW` and `es-419`. A format holds the standard itself, and a registry's format its pinned list: `iso3166-1-alpha-2` refuses `UK` and `XX`, `e164` takes 2 to 15 digits after the `+`, `iana-time-zone` takes the names of tzdata 2026f alone, and `bcp47` takes every well-formed tag. A format also trims the value and writes it in its standard's case before its check, so `" hr"` is stored as `HR` where the pattern refused it. A value it refuses gets zod's `invalid_format` with the id as its `format` and the locale's message, where a pattern's issue was `format: "regex"` with the pattern and an English message. **Fix:** replace `pattern: patterns.COUNTRY_ALPHA2` with `format: "iso3166-1-alpha-2"`, `pattern: patterns.E164_PHONE` with `format: "e164"`, `pattern: patterns.IANA_TIMEZONE` with `format: "iana-time-zone"` and `pattern: patterns.LOCALE` with `format: "bcp47"`; drop a `trim`, `lowercase` or `uppercase` beside it, and a `min` or `max` outside its range, which codegen refuses; a client that read a pattern's `format: "regex"` reads the id.
|
|
1290
|
+
|
|
1291
|
+
3. **Install `@workerstack/database` in place of `@lovrozagar/comb`.** The package moved to the workerstack org and the old name gets no new releases; `@workerstack/database@0.21.0` is `@lovrozagar/comb@0.21.0` under the new name, so you can switch before taking 0.22. The `comb` CLI, `comb.config.ts`, `CombError` and the migration tables keep their names. Generated DTOs and entities import from the package by name, so files generated on the old name still import `@lovrozagar/comb`. **Fix:** run `bun remove @lovrozagar/comb && bun add @workerstack/database`, replace every `@lovrozagar/comb` import prefix with `@workerstack/database`, and rerun `comb codegen`.
|
|
1292
|
+
|
|
1129
1293
|
## Upgrading to 0.21
|
|
1130
1294
|
|
|
1131
1295
|
What changes for a project on 0.20. Before deploying it:
|
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.0",
|
|
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",
|
|
@@ -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,
|