@fougere/adapter-sql 0.6.0-alpha.0 → 0.8.0-alpha.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.
Files changed (78) hide show
  1. package/README.md +2 -2
  2. package/dist/adapter.schema.json +16 -0
  3. package/dist/check.d.ts +2 -27
  4. package/dist/check.d.ts.map +1 -1
  5. package/dist/check.js +2 -27
  6. package/dist/check.js.map +1 -1
  7. package/dist/crud.d.ts +21 -81
  8. package/dist/crud.d.ts.map +1 -1
  9. package/dist/crud.js +58 -102
  10. package/dist/crud.js.map +1 -1
  11. package/dist/ddl.d.ts +8 -53
  12. package/dist/ddl.d.ts.map +1 -1
  13. package/dist/ddl.js +16 -55
  14. package/dist/ddl.js.map +1 -1
  15. package/dist/dialect.d.ts +7 -50
  16. package/dist/dialect.d.ts.map +1 -1
  17. package/dist/dialect.js +2 -5
  18. package/dist/dialect.js.map +1 -1
  19. package/dist/diff.d.ts +6 -44
  20. package/dist/diff.d.ts.map +1 -1
  21. package/dist/diff.js +20 -50
  22. package/dist/diff.js.map +1 -1
  23. package/dist/drift.d.ts +29 -0
  24. package/dist/drift.d.ts.map +1 -0
  25. package/dist/drift.js +53 -0
  26. package/dist/drift.js.map +1 -0
  27. package/dist/fields.d.ts +10 -9
  28. package/dist/fields.d.ts.map +1 -1
  29. package/dist/fields.js +8 -1
  30. package/dist/fields.js.map +1 -1
  31. package/dist/index.d.ts +6 -2
  32. package/dist/index.d.ts.map +1 -1
  33. package/dist/index.js +3 -1
  34. package/dist/index.js.map +1 -1
  35. package/dist/order.d.ts +23 -0
  36. package/dist/order.d.ts.map +1 -0
  37. package/dist/order.js +40 -0
  38. package/dist/order.js.map +1 -0
  39. package/dist/query.d.ts +2 -17
  40. package/dist/query.d.ts.map +1 -1
  41. package/dist/query.js +1 -11
  42. package/dist/query.js.map +1 -1
  43. package/dist/setup.d.ts +8 -36
  44. package/dist/setup.d.ts.map +1 -1
  45. package/dist/setup.js +11 -21
  46. package/dist/setup.js.map +1 -1
  47. package/dist/sqlite.d.ts.map +1 -1
  48. package/dist/sqlite.js +12 -20
  49. package/dist/sqlite.js.map +1 -1
  50. package/dist/step.d.ts +4 -33
  51. package/dist/step.d.ts.map +1 -1
  52. package/dist/step.js +11 -46
  53. package/dist/step.js.map +1 -1
  54. package/dist/table.d.ts +15 -77
  55. package/dist/table.d.ts.map +1 -1
  56. package/dist/table.js +34 -146
  57. package/dist/table.js.map +1 -1
  58. package/dist/values.d.ts +1 -17
  59. package/dist/values.d.ts.map +1 -1
  60. package/dist/values.js +2 -7
  61. package/dist/values.js.map +1 -1
  62. package/package.json +4 -4
  63. package/src/adapter.schema.json +16 -0
  64. package/src/check.ts +2 -27
  65. package/src/crud.ts +66 -105
  66. package/src/ddl.ts +14 -55
  67. package/src/dialect.ts +7 -50
  68. package/src/diff.ts +18 -50
  69. package/src/drift.ts +75 -0
  70. package/src/fields.ts +17 -8
  71. package/src/index.ts +5 -3
  72. package/src/order.ts +63 -0
  73. package/src/query.ts +2 -17
  74. package/src/setup.ts +19 -49
  75. package/src/sqlite.ts +13 -20
  76. package/src/step.ts +12 -54
  77. package/src/table.ts +42 -185
  78. package/src/values.ts +3 -24
package/README.md CHANGED
@@ -3,9 +3,9 @@
3
3
  The SQL realization of the schema: the tables, the additive migration and the
4
4
  per-entity scoped storage. Four dialects — SQLite, PostgreSQL, MySQL, SQL Server.
5
5
 
6
- Validation judges, storage realizes: this storage decides nothing of its own — it applies
6
+ Validation decides, storage realizes: this storage decides nothing of its own — it applies
7
7
  the `lifecycle` rules on the way in. The shape is still held on this path, twice: the
8
- DDL emits a `CHECK` for `oneOf`, `min` and `max`, and core's `StorageGuard` judges every
8
+ DDL emits a `CHECK` for `oneOf`, `min` and `max`, and core's `StorageGuard` validates every
9
9
  write through the port. `storage.client` meets neither.
10
10
 
11
11
  ## Installation
@@ -0,0 +1,16 @@
1
+ {
2
+ "type": "object",
3
+ "properties": {
4
+ "columnType": {
5
+ "type": "object",
6
+ "properties": {
7
+ "sqlite": { "type": "string" },
8
+ "pg": { "type": "string" },
9
+ "mysql": { "type": "string" },
10
+ "mssql": { "type": "string" }
11
+ },
12
+ "additionalProperties": false
13
+ }
14
+ },
15
+ "additionalProperties": false
16
+ }
package/dist/check.d.ts CHANGED
@@ -1,24 +1,4 @@
1
- /**
2
- * Shape → CHECK constraints.
3
- *
4
- * `oneOf`, `min`, `max` were declared on the field and read by the façade alone: a
5
- * handler writing through the storage put `status: 'brouillon'` in a column that declares
6
- * two values, and nothing said a word. The rule was in the schema; no one held it on
7
- * that path.
8
- *
9
- * So the storage learns what the shape already says. `validate` judges what a client
10
- * proposes; this holds what anyone writes — including us.
11
- *
12
- * Only the keywords a database can decide alone. `pattern` and `format` are left to
13
- * the façade: regex dialects diverge (POSIX here, PCRE there, nothing in SQLite
14
- * without an extension), and a constraint that means something different per engine
15
- * is worse than none.
16
- *
17
- * Every value is inlined with `sql.lit`, not bound: SQLite answers `parameters
18
- * prohibited in CHECK constraints`, and a constraint is part of the schema rather
19
- * than of a query. The values are the author's own literals — `oneOf('draft', …)`,
20
- * `max: 160` — never anything a request carried, and Kysely escapes them.
21
- */
1
+ /** Shape → CHECK constraints. */
22
2
  import { type Expression, type SqlBool } from 'kysely';
23
3
  import type { ColumnDef } from './table.js';
24
4
  /** What a column's shape can be checked for, beyond its type. */
@@ -29,12 +9,7 @@ export interface ShapeBounds {
29
9
  minimum?: number;
30
10
  maximum?: number;
31
11
  }
32
- /**
33
- * The CHECK expression for one column, or nothing when its shape bounds nothing.
34
- *
35
- * A nullable column passes when it holds `null`: `NOT NULL` is the axis that decides
36
- * presence, and stacking the two would make `optional()` unwritable.
37
- */
12
+ /** The CHECK expression for one column, or nothing when its shape bounds nothing. */
38
13
  export declare function checkFor(column: ColumnDef): Expression<SqlBool> | undefined;
39
14
  /** Read the bounds off a field's base shape — the keywords a database can decide. */
40
15
  export declare function boundsOf(shape: Record<string, unknown> | undefined): ShapeBounds | undefined;
@@ -1 +1 @@
1
- {"version":3,"file":"check.d.ts","sourceRoot":"","sources":["../src/check.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,OAAO,EAAO,KAAK,UAAU,EAAE,KAAK,OAAO,EAAE,MAAM,QAAQ,CAAC;AAC5D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAE5C,iEAAiE;AACjE,MAAM,WAAW,WAAW;IAC1B,IAAI,CAAC,EAAE,SAAS,CAAC,MAAM,GAAG,MAAM,CAAC,EAAE,CAAC;IACpC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;;;;GAKG;AACH,wBAAgB,QAAQ,CAAC,MAAM,EAAE,SAAS,GAAG,UAAU,CAAC,OAAO,CAAC,GAAG,SAAS,CAuB3E;AAED,qFAAqF;AACrF,wBAAgB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,GAAG,WAAW,GAAG,SAAS,CAU5F"}
1
+ {"version":3,"file":"check.d.ts","sourceRoot":"","sources":["../src/check.ts"],"names":[],"mappings":"AAAA,iCAAiC;AACjC,OAAO,EAAO,KAAK,UAAU,EAAE,KAAK,OAAO,EAAE,MAAM,QAAQ,CAAC;AAC5D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAE5C,iEAAiE;AACjE,MAAM,WAAW,WAAW;IAC1B,IAAI,CAAC,EAAE,SAAS,CAAC,MAAM,GAAG,MAAM,CAAC,EAAE,CAAC;IACpC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,qFAAqF;AACrF,wBAAgB,QAAQ,CAAC,MAAM,EAAE,SAAS,GAAG,UAAU,CAAC,OAAO,CAAC,GAAG,SAAS,CAuB3E;AAED,qFAAqF;AACrF,wBAAgB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,GAAG,WAAW,GAAG,SAAS,CAU5F"}
package/dist/check.js CHANGED
@@ -1,31 +1,6 @@
1
- /**
2
- * Shape → CHECK constraints.
3
- *
4
- * `oneOf`, `min`, `max` were declared on the field and read by the façade alone: a
5
- * handler writing through the storage put `status: 'brouillon'` in a column that declares
6
- * two values, and nothing said a word. The rule was in the schema; no one held it on
7
- * that path.
8
- *
9
- * So the storage learns what the shape already says. `validate` judges what a client
10
- * proposes; this holds what anyone writes — including us.
11
- *
12
- * Only the keywords a database can decide alone. `pattern` and `format` are left to
13
- * the façade: regex dialects diverge (POSIX here, PCRE there, nothing in SQLite
14
- * without an extension), and a constraint that means something different per engine
15
- * is worse than none.
16
- *
17
- * Every value is inlined with `sql.lit`, not bound: SQLite answers `parameters
18
- * prohibited in CHECK constraints`, and a constraint is part of the schema rather
19
- * than of a query. The values are the author's own literals — `oneOf('draft', …)`,
20
- * `max: 160` — never anything a request carried, and Kysely escapes them.
21
- */
1
+ /** Shape → CHECK constraints. */
22
2
  import { sql } from 'kysely';
23
- /**
24
- * The CHECK expression for one column, or nothing when its shape bounds nothing.
25
- *
26
- * A nullable column passes when it holds `null`: `NOT NULL` is the axis that decides
27
- * presence, and stacking the two would make `optional()` unwritable.
28
- */
3
+ /** The CHECK expression for one column, or nothing when its shape bounds nothing. */
29
4
  export function checkFor(column) {
30
5
  const bounds = column.bounds;
31
6
  if (!bounds)
package/dist/check.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"check.js","sourceRoot":"","sources":["../src/check.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,OAAO,EAAE,GAAG,EAAiC,MAAM,QAAQ,CAAC;AAY5D;;;;;GAKG;AACH,MAAM,UAAU,QAAQ,CAAC,MAAiB;IACxC,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC;IAC7B,IAAI,CAAC,MAAM;QAAE,OAAO,SAAS,CAAC;IAE9B,MAAM,GAAG,GAAG,GAAG,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;IACjC,MAAM,KAAK,GAA0B,EAAE,CAAC;IAExC,IAAI,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,CAAC;QACxB,KAAK,CAAC,IAAI,CAAC,GAAG,CAAS,GAAG,GAAG,QAAQ,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;IACxF,CAAC;IACD,kFAAkF;IAClF,+EAA+E;IAC/E,wEAAwE;IACxE,IAAI,MAAM,CAAC,SAAS,KAAK,SAAS;QAAE,KAAK,CAAC,IAAI,CAAC,GAAG,CAAS,UAAU,GAAG,QAAQ,GAAG,CAAC,GAAG,CAAC,MAAM,CAAC,SAAS,CAAC,EAAE,CAAC,CAAC;IAC7G,IAAI,MAAM,CAAC,SAAS,KAAK,SAAS;QAAE,KAAK,CAAC,IAAI,CAAC,GAAG,CAAS,UAAU,GAAG,QAAQ,GAAG,CAAC,GAAG,CAAC,MAAM,CAAC,SAAS,CAAC,EAAE,CAAC,CAAC;IAC7G,IAAI,MAAM,CAAC,OAAO,KAAK,SAAS;QAAE,KAAK,CAAC,IAAI,CAAC,GAAG,CAAS,GAAG,GAAG,OAAO,GAAG,CAAC,GAAG,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;IACjG,IAAI,MAAM,CAAC,OAAO,KAAK,SAAS;QAAE,KAAK,CAAC,IAAI,CAAC,GAAG,CAAS,GAAG,GAAG,OAAO,GAAG,CAAC,GAAG,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;IAEjG,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,SAAS,CAAC;IAEzC,MAAM,GAAG,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC,GAAG,CAAS,GAAG,IAAI,QAAQ,KAAK,EAAE,CAAC,CAAC;IAE9E,OAAO,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAS,GAAG,GAAG,gBAAgB,GAAG,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC;AAC1E,CAAC;AAED,qFAAqF;AACrF,MAAM,UAAU,QAAQ,CAAC,KAA0C;IACjE,IAAI,CAAC,KAAK;QAAE,OAAO,SAAS,CAAC;IAE7B,MAAM,MAAM,GAAgB,EAAE,CAAC;IAC/B,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,KAAK,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC;QAAE,MAAM,CAAC,IAAI,GAAG,KAAK,CAAC,IAAgB,CAAC;IAC7F,KAAK,MAAM,GAAG,IAAI,CAAC,WAAW,EAAE,WAAW,EAAE,SAAS,EAAE,SAAS,CAAU,EAAE,CAAC;QAC5E,IAAI,OAAO,KAAK,CAAC,GAAG,CAAC,KAAK,QAAQ;YAAE,MAAM,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC,GAAG,CAAW,CAAC;IACzE,CAAC;IAED,OAAO,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC;AAC7D,CAAC"}
1
+ {"version":3,"file":"check.js","sourceRoot":"","sources":["../src/check.ts"],"names":[],"mappings":"AAAA,iCAAiC;AACjC,OAAO,EAAE,GAAG,EAAiC,MAAM,QAAQ,CAAC;AAY5D,qFAAqF;AACrF,MAAM,UAAU,QAAQ,CAAC,MAAiB;IACxC,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC;IAC7B,IAAI,CAAC,MAAM;QAAE,OAAO,SAAS,CAAC;IAE9B,MAAM,GAAG,GAAG,GAAG,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;IACjC,MAAM,KAAK,GAA0B,EAAE,CAAC;IAExC,IAAI,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,CAAC;QACxB,KAAK,CAAC,IAAI,CAAC,GAAG,CAAS,GAAG,GAAG,QAAQ,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;IACxF,CAAC;IACD,kFAAkF;IAClF,+EAA+E;IAC/E,wEAAwE;IACxE,IAAI,MAAM,CAAC,SAAS,KAAK,SAAS;QAAE,KAAK,CAAC,IAAI,CAAC,GAAG,CAAS,UAAU,GAAG,QAAQ,GAAG,CAAC,GAAG,CAAC,MAAM,CAAC,SAAS,CAAC,EAAE,CAAC,CAAC;IAC7G,IAAI,MAAM,CAAC,SAAS,KAAK,SAAS;QAAE,KAAK,CAAC,IAAI,CAAC,GAAG,CAAS,UAAU,GAAG,QAAQ,GAAG,CAAC,GAAG,CAAC,MAAM,CAAC,SAAS,CAAC,EAAE,CAAC,CAAC;IAC7G,IAAI,MAAM,CAAC,OAAO,KAAK,SAAS;QAAE,KAAK,CAAC,IAAI,CAAC,GAAG,CAAS,GAAG,GAAG,OAAO,GAAG,CAAC,GAAG,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;IACjG,IAAI,MAAM,CAAC,OAAO,KAAK,SAAS;QAAE,KAAK,CAAC,IAAI,CAAC,GAAG,CAAS,GAAG,GAAG,OAAO,GAAG,CAAC,GAAG,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;IAEjG,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,SAAS,CAAC;IAEzC,MAAM,GAAG,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC,GAAG,CAAS,GAAG,IAAI,QAAQ,KAAK,EAAE,CAAC,CAAC;IAE9E,OAAO,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAS,GAAG,GAAG,gBAAgB,GAAG,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC;AAC1E,CAAC;AAED,qFAAqF;AACrF,MAAM,UAAU,QAAQ,CAAC,KAA0C;IACjE,IAAI,CAAC,KAAK;QAAE,OAAO,SAAS,CAAC;IAE7B,MAAM,MAAM,GAAgB,EAAE,CAAC;IAC/B,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,KAAK,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC;QAAE,MAAM,CAAC,IAAI,GAAG,KAAK,CAAC,IAAgB,CAAC;IAC7F,KAAK,MAAM,GAAG,IAAI,CAAC,WAAW,EAAE,WAAW,EAAE,SAAS,EAAE,SAAS,CAAU,EAAE,CAAC;QAC5E,IAAI,OAAO,KAAK,CAAC,GAAG,CAAC,KAAK,QAAQ;YAAE,MAAM,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC,GAAG,CAAW,CAAC;IACzE,CAAC;IAED,OAAO,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC;AAC7D,CAAC"}
package/dist/crud.d.ts CHANGED
@@ -1,19 +1,6 @@
1
- /**
2
- * SqlStorage — per-entity storage over Kysely, one implementation for every engine.
3
- *
4
- * Structurally matches @fougere/core's Storage (duck typed, no dep). There is
5
- * no generated table object: Kysely addresses tables and columns by name, so the
6
- * entity stays the only description. The field↔column mapping is explicit rather
7
- * than a global plugin — auth tables carry their own naming and must not be
8
- * rewritten behind the caller's back.
9
- *
10
- * `create` and `update` re-read the row instead of using `RETURNING`: the
11
- * contract is to hand back the COMPLETE row, including defaults realised by SQL.
12
- * That also makes the code identical on MySQL and SQL Server, which have no
13
- * `RETURNING` clause.
14
- */
1
+ /** SqlStorage — per-entity storage over Kysely, one implementation for every engine. */
15
2
  import { type Kysely } from 'kysely';
16
- import { type SchemaView, type SchemaOrCard } from '@fougere/schema';
3
+ import { type SchemaView } from '@fougere/schema';
17
4
  import { type DialectName } from './dialect.js';
18
5
  /** ListOptions — duplicated from @fougere/core to avoid a runtime dep. */
19
6
  interface ListOptions {
@@ -51,8 +38,8 @@ export declare class SqlStorage {
51
38
  private maxBindings;
52
39
  /** How this engine spells an upsert, or `false` when it cannot — see `Dialect.upsert`. */
53
40
  private upsertClause;
54
- constructor(db: Kysely<any>, source: SchemaOrCard, tableName: string, selectFields?: Set<string>, dialect?: DialectName);
55
- /** The Kysely instance this storage wraps — no judge sits behind it. See Storage.client. */
41
+ constructor(db: Kysely<any>, entity: SchemaView, tableName: string, selectFields?: Set<string>, dialect?: DialectName);
42
+ /** The Kysely instance this storage wraps — no validator sits behind it. See Storage.client. */
56
43
  get client(): Kysely<any>;
57
44
  /** Returns a scoped storage that restricts all read results to the fields of the given schema. */
58
45
  output(schema: SchemaView): SqlStorage;
@@ -64,60 +51,28 @@ export declare class SqlStorage {
64
51
  private toRow;
65
52
  /** Column keys → entity keys, and column values → the values the entity declares. */
66
53
  private fromRow;
67
- /**
68
- * Apply a primary-key filter (simple or composite).
69
- *
70
- * The key crosses to the column exactly like every other value — `whereAll` states
71
- * the rule two lines below and this did not follow it. It cost nothing while every
72
- * generated key was a string; a key that holds a Date (`primary(created())`) inserted
73
- * fine and then failed its own re-read, with the row already persisted.
74
- */
54
+ /** Apply a primary-key filter (simple or composite). */
75
55
  private wherePk;
76
56
  private whereAll;
57
+ /**
58
+ * What a bare value and a set could not say. Every comparison a criterion names is
59
+ * AND-ed, which is the rule two criteria already follow — `{ gte: 100, lte: 400 }` is
60
+ * one range, not two answers.
61
+ */
62
+ private compared;
77
63
  list(options?: ListOptions & SelectOption & {
78
64
  where?: Record<string, unknown>;
79
65
  }): Promise<ListResult<Record<string, unknown>>>;
80
66
  /**
81
- * One query for N keys, never N queries — what every page-level read stands on:
82
- * a computed field, a relation, a resolver on the other side of a wire.
83
- *
84
- * A composite key has no list form: it is refused by name rather than answering a
85
- * partial result that reads as complete.
67
+ * One query for N keys, never N queries — what every page-level read stands on: a computed
68
+ * field, a relation, a resolver on the other side of a wire.
86
69
  */
87
70
  findByKeys(ids: readonly string[], options?: SelectOption): Promise<Map<string, Record<string, unknown>>>;
88
- /**
89
- * The other direction of a relation, in one query — see the port's `findAllByKeys`.
90
- *
91
- * The grouping key is read off the ROW rather than trusted from the request: a codec
92
- * may write a value one way and read it back another, and a group keyed on the
93
- * request's spelling would then be empty while the rows sit there.
94
- */
71
+ /** The other direction of a relation, in one query — see the port's `findAllByKeys`. */
95
72
  findAllByKeys(field: string, keys: readonly string[], options?: SelectOption): Promise<Map<string, Record<string, unknown>[]>>;
96
- /**
97
- * Write the row, or make the existing one look like this — one statement.
98
- *
99
- * The gesture an import needs and the port did not have: `create` throws on the
100
- * second run (`UNIQUE constraint failed`), so re-reading anything meant deleting
101
- * first. Measured pulling 500 rows from an API twice.
102
- *
103
- * Both lifecycles are realized, each on the side it belongs to: `applyCreate` fills
104
- * what a first write owes (a generated key, `created()`, a declared default) and
105
- * `applyUpdate` stamps what every write owes (`updated()`). On conflict the key and
106
- * the creation stamps are left alone — a row keeps the moment it appeared, whatever
107
- * later overwrites say.
108
- */
73
+ /** Write the row, or make the existing one look like this — one statement. */
109
74
  upsert(input: Record<string, unknown>, options?: SelectOption): Promise<Record<string, unknown>>;
110
- /**
111
- * Upsert a whole page in one statement — what an import writes through.
112
- *
113
- * Row by row, 500 rows were 500 statements (measured pulling an API); the shape of
114
- * an import is a page, so the write should be one too. Sliced like every other batch,
115
- * but by rows × COLUMNS: a statement binds values, not rows, so the ceiling divides.
116
- *
117
- * Answers how many rows were written and not the rows themselves. `create` hands back
118
- * the complete row because a caller acts on it; an import acts on none of them, and
119
- * re-reading a page to satisfy a symmetry nobody uses would double the work.
120
- */
75
+ /** Upsert a whole page in one statement — what an import writes through. */
121
76
  upsertAll(inputs: readonly Record<string, unknown>[], _options?: SelectOption): Promise<number>;
122
77
  /**
123
78
  * The COLUMNS a later write must not touch: the key, and a stamp that is create-only.
@@ -127,23 +82,14 @@ export declare class SqlStorage {
127
82
  findById(id: string | Record<string, unknown>, options?: SelectOption): Promise<Record<string, unknown> | undefined>;
128
83
  findBy(criteria: Record<string, unknown>, options?: SelectOption): Promise<Record<string, unknown> | undefined>;
129
84
  findAllBy(criteria: Record<string, unknown>, options?: SelectOption): Promise<Record<string, unknown>[]>;
130
- /**
131
- * One criteria object per statement — the oversized set is the one that splits.
132
- *
133
- * Only ONE criterion may be split: two split sets would need their cross product,
134
- * which is a different query, so the second is refused rather than silently wrong.
135
- */
85
+ /** One criteria object per statement — the oversized set is the one that splits. */
136
86
  private refuseOversized;
137
87
  private splitCriteria;
138
88
  create(input: Record<string, unknown>, options?: SelectOption): Promise<Record<string, unknown>>;
139
89
  update(id: string | Record<string, unknown>, input: Record<string, unknown>, options?: SelectOption): Promise<Record<string, unknown>>;
140
90
  /**
141
- * A duplicate is an ANSWER, not a failure — so it leaves as `CONFLICT` and not as the
142
- * blank `Internal error` a caller used to get. The engine's own wording never travels:
143
- * it names a table and a constraint, which is our schema and not the caller's business.
144
- *
145
- * Only the dialect can recognize it; this method knows no engine, which is the rule
146
- * `dialect.ts` exists to keep.
91
+ * A duplicate is an ANSWER, not a failure — so it leaves as `CONFLICT` and not as the blank
92
+ * `Internal error` a caller used to get.
147
93
  */
148
94
  private refusal;
149
95
  delete(id: string | Record<string, unknown>): Promise<boolean>;
@@ -152,13 +98,7 @@ export interface StorageFactoryOptions {
152
98
  /** Override table name resolution. Default: camelCase → snake_case + 's'. */
153
99
  tableName?: (entityName: string) => string;
154
100
  }
155
- /**
156
- * Create a StorageFactory backed by Kysely — same call shape on every engine.
157
- *
158
- * ```ts
159
- * const app = await createApp({ createContainer, storageFactory: createStorageFactory(db) });
160
- * ```
161
- */
162
- export declare function createStorageFactory(db: Kysely<any>, options?: StorageFactoryOptions, dialect?: DialectName): (entity: SchemaOrCard, name: string) => SqlStorage;
101
+ /** Create a StorageFactory backed by Kysely — same call shape on every engine. */
102
+ export declare function createStorageFactory(db: Kysely<any>, options?: StorageFactoryOptions, dialect?: DialectName): (entity: SchemaView, name: string) => SqlStorage;
163
103
  export {};
164
104
  //# sourceMappingURL=crud.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"crud.d.ts","sourceRoot":"","sources":["../src/crud.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"crud.d.ts","sourceRoot":"","sources":["../src/crud.ts"],"names":[],"mappings":"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"}
package/dist/crud.js CHANGED
@@ -1,34 +1,14 @@
1
1
  import { Lifecycle, Role } from '@fougere/schema';
2
- /**
3
- * SqlStorage — per-entity storage over Kysely, one implementation for every engine.
4
- *
5
- * Structurally matches @fougere/core's Storage (duck typed, no dep). There is
6
- * no generated table object: Kysely addresses tables and columns by name, so the
7
- * entity stays the only description. The field↔column mapping is explicit rather
8
- * than a global plugin — auth tables carry their own naming and must not be
9
- * rewritten behind the caller's back.
10
- *
11
- * `create` and `update` re-read the row instead of using `RETURNING`: the
12
- * contract is to hand back the COMPLETE row, including defaults realised by SQL.
13
- * That also makes the code identical on MySQL and SQL Server, which have no
14
- * `RETURNING` clause.
15
- */
2
+ /** SqlStorage — per-entity storage over Kysely, one implementation for every engine. */
16
3
  import { sql } from 'kysely';
17
- import { applyCreate, applyUpdate, schemaOf } from '@fougere/schema';
4
+ import { applyCreate, applyUpdate } from '@fougere/schema';
18
5
  import { toTable, toTableName } from './table.js';
19
6
  import { resolveDialect } from './dialect.js';
20
7
  // The contract entry and not the main one: `FougereError` crosses a process boundary and
21
8
  // lives there for that reason, and this package must not drag the boot to raise one.
22
- import { FougereError, ErrorCode } from '@fougere/core/contract';
9
+ import { comparisonOf, comparisonsIn, ErrorCode, FougereError } from '@fougere/core/contract';
23
10
  import { codecsOf } from './values.js';
24
- /**
25
- * The primary key, read off the role axis.
26
- *
27
- * Used to answer "what identifies a row" — where to point a WHERE, what a cursor
28
- * carries. The generated ids and managed timestamps that used to be computed here
29
- * moved to `applyCreate`/`applyUpdate` (`@fougere/schema`): nothing in them was about
30
- * SQL, and every other storage was re-deriving them from scratch.
31
- */
11
+ /** The primary key, read off the role axis. */
32
12
  function analyzeFields(entity) {
33
13
  const pkNames = Object.entries(entity.getFields())
34
14
  .filter(([, field]) => Role.of(field).isPrimary)
@@ -78,16 +58,12 @@ export class SqlStorage {
78
58
  maxBindings;
79
59
  /** How this engine spells an upsert, or `false` when it cannot — see `Dialect.upsert`. */
80
60
  upsertClause;
81
- constructor(db, source, tableName, selectFields, dialect = 'sqlite') {
61
+ constructor(db, entity, tableName, selectFields, dialect = 'sqlite') {
82
62
  this.db = db;
83
63
  const resolved = resolveDialect(dialect);
84
64
  this.dialect = resolved;
85
65
  this.maxBindings = resolved.maxBindings;
86
66
  this.upsertClause = resolved.upsert;
87
- // Normalized once: the table projection and the axis analysis below both read the
88
- // schema, and a card handed to each separately would be rebuilt twice into two
89
- // unrelated field objects. Past this line nothing knows which form arrived.
90
- const entity = schemaOf(source);
91
67
  this.table = toTable(tableName, entity);
92
68
  for (const column of this.table.columns) {
93
69
  this.toColumn.set(column.field, column.name);
@@ -98,7 +74,7 @@ export class SqlStorage {
98
74
  this.fields = entity.getFields();
99
75
  this.selectFields = selectFields;
100
76
  }
101
- /** The Kysely instance this storage wraps — no judge sits behind it. See Storage.client. */
77
+ /** The Kysely instance this storage wraps — no validator sits behind it. See Storage.client. */
102
78
  get client() {
103
79
  return this.db;
104
80
  }
@@ -136,18 +112,11 @@ export class SqlStorage {
136
112
  }
137
113
  return data;
138
114
  }
139
- /**
140
- * Apply a primary-key filter (simple or composite).
141
- *
142
- * The key crosses to the column exactly like every other value — `whereAll` states
143
- * the rule two lines below and this did not follow it. It cost nothing while every
144
- * generated key was a string; a key that holds a Date (`primary(created())`) inserted
145
- * fine and then failed its own re-read, with the row already persisted.
146
- */
115
+ /** Apply a primary-key filter (simple or composite). */
147
116
  wherePk(query, id) {
148
117
  if (this.pk.isComposite) {
149
- const obj = id;
150
- return this.pk.names.reduce((q, name) => q.where(this.column(name), '=', this.write(name, obj[name])), query);
118
+ const composite = id;
119
+ return this.pk.names.reduce((q, name) => q.where(this.column(name), '=', this.write(name, composite[name])), query);
151
120
  }
152
121
  const name = this.pk.names[0];
153
122
  return query.where(this.column(name), '=', this.write(name, id));
@@ -161,9 +130,42 @@ export class SqlStorage {
161
130
  // dual already went through this same door. An empty set matches nothing, said in
162
131
  // SQL rather than by returning the whole table.
163
132
  whereAll(query, criteria) {
164
- return Object.entries(criteria).reduce((q, [key, value]) => Array.isArray(value)
165
- ? q.where(this.column(key), 'in', [...new Set(value)].map((v) => this.write(key, v)))
166
- : q.where(this.column(key), '=', this.write(key, value)), query);
133
+ return Object.entries(criteria).reduce((q, [key, value]) => {
134
+ const comparison = comparisonOf(this.fields[key], value);
135
+ if (comparison)
136
+ return this.compared(q, key, comparison);
137
+ return Array.isArray(value)
138
+ ? q.where(this.column(key), 'in', [...new Set(value)].map((v) => this.write(key, v)))
139
+ : q.where(this.column(key), '=', this.write(key, value));
140
+ }, query);
141
+ }
142
+ /**
143
+ * What a bare value and a set could not say. Every comparison a criterion names is
144
+ * AND-ed, which is the rule two criteria already follow — `{ gte: 100, lte: 400 }` is
145
+ * one range, not two answers.
146
+ */
147
+ compared(query, key, comparison) {
148
+ const column = this.column(key);
149
+ const bound = (value) => this.write(key, value);
150
+ return comparisonsIn(comparison).reduce((q, [name, value]) => {
151
+ switch (name) {
152
+ case 'gte': return q.where(column, '>=', bound(value));
153
+ case 'lte': return q.where(column, '<=', bound(value));
154
+ case 'gt': return q.where(column, '>', bound(value));
155
+ case 'lt': return q.where(column, '<', bound(value));
156
+ case 'ne': return q.where(column, '!=', bound(value));
157
+ case 'contains': return q.where(column, 'like', `%${String(value)}%`);
158
+ case 'notIn': {
159
+ const values = [...new Set(value)].map(bound);
160
+ return values.length ? q.where(column, 'not in', values) : q;
161
+ }
162
+ case 'isNull': return q.where(column, value ? 'is' : 'is not', null);
163
+ case 'between': {
164
+ const [low, high] = value;
165
+ return q.where(column, '>=', bound(low)).where(column, '<=', bound(high));
166
+ }
167
+ }
168
+ }, query);
167
169
  }
168
170
  async list(options) {
169
171
  let query = this.db.selectFrom(this.table.name).selectAll();
@@ -224,11 +226,8 @@ export class SqlStorage {
224
226
  return sel ? pickList(result, sel) : result;
225
227
  }
226
228
  /**
227
- * One query for N keys, never N queries — what every page-level read stands on:
228
- * a computed field, a relation, a resolver on the other side of a wire.
229
- *
230
- * A composite key has no list form: it is refused by name rather than answering a
231
- * partial result that reads as complete.
229
+ * One query for N keys, never N queries — what every page-level read stands on: a computed
230
+ * field, a relation, a resolver on the other side of a wire.
232
231
  */
233
232
  async findByKeys(ids, options) {
234
233
  if (this.pk.isComposite) {
@@ -254,13 +253,7 @@ export class SqlStorage {
254
253
  }
255
254
  return found;
256
255
  }
257
- /**
258
- * The other direction of a relation, in one query — see the port's `findAllByKeys`.
259
- *
260
- * The grouping key is read off the ROW rather than trusted from the request: a codec
261
- * may write a value one way and read it back another, and a group keyed on the
262
- * request's spelling would then be empty while the rows sit there.
263
- */
256
+ /** The other direction of a relation, in one query — see the port's `findAllByKeys`. */
264
257
  async findAllByKeys(field, keys, options) {
265
258
  const grouped = new Map();
266
259
  if (keys.length === 0)
@@ -268,27 +261,15 @@ export class SqlStorage {
268
261
  const rows = await this.findAllBy({ [field]: [...keys] }, options);
269
262
  for (const row of rows) {
270
263
  const key = String(row[field]);
271
- const held = grouped.get(key);
272
- if (held)
273
- held.push(row);
264
+ const bucket = grouped.get(key);
265
+ if (bucket)
266
+ bucket.push(row);
274
267
  else
275
268
  grouped.set(key, [row]);
276
269
  }
277
270
  return grouped;
278
271
  }
279
- /**
280
- * Write the row, or make the existing one look like this — one statement.
281
- *
282
- * The gesture an import needs and the port did not have: `create` throws on the
283
- * second run (`UNIQUE constraint failed`), so re-reading anything meant deleting
284
- * first. Measured pulling 500 rows from an API twice.
285
- *
286
- * Both lifecycles are realized, each on the side it belongs to: `applyCreate` fills
287
- * what a first write owes (a generated key, `created()`, a declared default) and
288
- * `applyUpdate` stamps what every write owes (`updated()`). On conflict the key and
289
- * the creation stamps are left alone — a row keeps the moment it appeared, whatever
290
- * later overwrites say.
291
- */
272
+ /** Write the row, or make the existing one look like this — one statement. */
292
273
  async upsert(input, options) {
293
274
  if (this.upsertClause === false) {
294
275
  throw new Error(`${this.table.name}.upsert(): this engine has no upsert clause — read with findById ` +
@@ -313,17 +294,7 @@ export class SqlStorage {
313
294
  : data[this.pk.names[0]];
314
295
  return (await this.findById(id, options));
315
296
  }
316
- /**
317
- * Upsert a whole page in one statement — what an import writes through.
318
- *
319
- * Row by row, 500 rows were 500 statements (measured pulling an API); the shape of
320
- * an import is a page, so the write should be one too. Sliced like every other batch,
321
- * but by rows × COLUMNS: a statement binds values, not rows, so the ceiling divides.
322
- *
323
- * Answers how many rows were written and not the rows themselves. `create` hands back
324
- * the complete row because a caller acts on it; an import acts on none of them, and
325
- * re-reading a page to satisfy a symmetry nobody uses would double the work.
326
- */
297
+ /** Upsert a whole page in one statement — what an import writes through. */
327
298
  async upsertAll(inputs, _options) {
328
299
  if (this.upsertClause === false) {
329
300
  throw new Error(`${this.table.name}.upsertAll(): this engine has no upsert clause — write the rows ` +
@@ -391,12 +362,7 @@ export class SqlStorage {
391
362
  }
392
363
  return out;
393
364
  }
394
- /**
395
- * One criteria object per statement — the oversized set is the one that splits.
396
- *
397
- * Only ONE criterion may be split: two split sets would need their cross product,
398
- * which is a different query, so the second is refused rather than silently wrong.
399
- */
365
+ /** One criteria object per statement — the oversized set is the one that splits. */
400
366
  refuseOversized(criteria, op) {
401
367
  for (const [key, value] of Object.entries(criteria)) {
402
368
  if (!Array.isArray(value) || new Set(value).size <= this.maxBindings)
@@ -425,7 +391,7 @@ export class SqlStorage {
425
391
  // writer that is not us, the way a CHECK does; nothing depends on it any more.
426
392
  const data = applyCreate(this.fields, input);
427
393
  await this.refusal(() => this.db.insertInto(this.table.name).values(this.toRow(data)).execute());
428
- // Contract: create returns the COMPLETE row (validation judges absence, it
394
+ // Contract: create returns the COMPLETE row (validation validates absence, it
429
395
  // never fills) — re-read so SQL-realised defaults appear. Same move as update().
430
396
  const id = this.pk.isComposite
431
397
  ? Object.fromEntries(this.pk.names.map((n) => [n, data[n]]))
@@ -444,12 +410,8 @@ export class SqlStorage {
444
410
  return sel ? pick(result, sel) : result;
445
411
  }
446
412
  /**
447
- * A duplicate is an ANSWER, not a failure — so it leaves as `CONFLICT` and not as the
448
- * blank `Internal error` a caller used to get. The engine's own wording never travels:
449
- * it names a table and a constraint, which is our schema and not the caller's business.
450
- *
451
- * Only the dialect can recognize it; this method knows no engine, which is the rule
452
- * `dialect.ts` exists to keep.
413
+ * A duplicate is an ANSWER, not a failure — so it leaves as `CONFLICT` and not as the blank
414
+ * `Internal error` a caller used to get.
453
415
  */
454
416
  async refusal(write) {
455
417
  try {
@@ -473,13 +435,7 @@ export class SqlStorage {
473
435
  return true;
474
436
  }
475
437
  }
476
- /**
477
- * Create a StorageFactory backed by Kysely — same call shape on every engine.
478
- *
479
- * ```ts
480
- * const app = await createApp({ createContainer, storageFactory: createStorageFactory(db) });
481
- * ```
482
- */
438
+ /** Create a StorageFactory backed by Kysely — same call shape on every engine. */
483
439
  export function createStorageFactory(db, options, dialect = 'sqlite') {
484
440
  const resolve = options?.tableName ?? toTableName;
485
441
  return (entity, name) => new SqlStorage(db, entity, resolve(name), undefined, dialect);