@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.
- package/README.md +2 -2
- package/dist/adapter.schema.json +16 -0
- package/dist/check.d.ts +2 -27
- package/dist/check.d.ts.map +1 -1
- package/dist/check.js +2 -27
- package/dist/check.js.map +1 -1
- package/dist/crud.d.ts +21 -81
- package/dist/crud.d.ts.map +1 -1
- package/dist/crud.js +58 -102
- package/dist/crud.js.map +1 -1
- package/dist/ddl.d.ts +8 -53
- package/dist/ddl.d.ts.map +1 -1
- package/dist/ddl.js +16 -55
- package/dist/ddl.js.map +1 -1
- package/dist/dialect.d.ts +7 -50
- package/dist/dialect.d.ts.map +1 -1
- package/dist/dialect.js +2 -5
- package/dist/dialect.js.map +1 -1
- package/dist/diff.d.ts +6 -44
- package/dist/diff.d.ts.map +1 -1
- package/dist/diff.js +20 -50
- package/dist/diff.js.map +1 -1
- package/dist/drift.d.ts +29 -0
- package/dist/drift.d.ts.map +1 -0
- package/dist/drift.js +53 -0
- package/dist/drift.js.map +1 -0
- package/dist/fields.d.ts +10 -9
- package/dist/fields.d.ts.map +1 -1
- package/dist/fields.js +8 -1
- package/dist/fields.js.map +1 -1
- package/dist/index.d.ts +6 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/order.d.ts +23 -0
- package/dist/order.d.ts.map +1 -0
- package/dist/order.js +40 -0
- package/dist/order.js.map +1 -0
- package/dist/query.d.ts +2 -17
- package/dist/query.d.ts.map +1 -1
- package/dist/query.js +1 -11
- package/dist/query.js.map +1 -1
- package/dist/setup.d.ts +8 -36
- package/dist/setup.d.ts.map +1 -1
- package/dist/setup.js +11 -21
- package/dist/setup.js.map +1 -1
- package/dist/sqlite.d.ts.map +1 -1
- package/dist/sqlite.js +12 -20
- package/dist/sqlite.js.map +1 -1
- package/dist/step.d.ts +4 -33
- package/dist/step.d.ts.map +1 -1
- package/dist/step.js +11 -46
- package/dist/step.js.map +1 -1
- package/dist/table.d.ts +15 -77
- package/dist/table.d.ts.map +1 -1
- package/dist/table.js +34 -146
- package/dist/table.js.map +1 -1
- package/dist/values.d.ts +1 -17
- package/dist/values.d.ts.map +1 -1
- package/dist/values.js +2 -7
- package/dist/values.js.map +1 -1
- package/package.json +4 -4
- package/src/adapter.schema.json +16 -0
- package/src/check.ts +2 -27
- package/src/crud.ts +66 -105
- package/src/ddl.ts +14 -55
- package/src/dialect.ts +7 -50
- package/src/diff.ts +18 -50
- package/src/drift.ts +75 -0
- package/src/fields.ts +17 -8
- package/src/index.ts +5 -3
- package/src/order.ts +63 -0
- package/src/query.ts +2 -17
- package/src/setup.ts +19 -49
- package/src/sqlite.ts +13 -20
- package/src/step.ts +12 -54
- package/src/table.ts +42 -185
- 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
|
|
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`
|
|
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;
|
package/dist/check.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"check.d.ts","sourceRoot":"","sources":["../src/check.ts"],"names":[],"mappings":"AAAA
|
|
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
|
|
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
|
|
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>,
|
|
55
|
-
/** The Kysely instance this storage wraps — no
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
package/dist/crud.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"crud.d.ts","sourceRoot":"","sources":["../src/crud.ts"],"names":[],"mappings":"AACA
|
|
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
|
|
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 {
|
|
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,
|
|
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
|
|
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
|
|
150
|
-
return this.pk.names.reduce((q, name) => q.where(this.column(name), '=', this.write(name,
|
|
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]) =>
|
|
165
|
-
|
|
166
|
-
|
|
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
|
-
*
|
|
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
|
|
272
|
-
if (
|
|
273
|
-
|
|
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
|
|
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
|
-
*
|
|
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);
|