@zeroxsolutions/db 9.5.0 → 10.0.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 (42) hide show
  1. package/CHANGELOG.md +47 -23
  2. package/README.md +136 -180
  3. package/dist/base/d1/repository.d.ts +75 -66
  4. package/dist/base/d1/repository.d.ts.map +1 -1
  5. package/dist/base/d1/repository.js +134 -122
  6. package/dist/base/postgres/repository.d.ts +108 -86
  7. package/dist/base/postgres/repository.d.ts.map +1 -1
  8. package/dist/base/postgres/repository.js +175 -143
  9. package/dist/base/read-engine.d.ts +49 -0
  10. package/dist/base/read-engine.d.ts.map +1 -0
  11. package/dist/base/read-engine.js +93 -0
  12. package/dist/utils/filter.d.ts +40 -61
  13. package/dist/utils/filter.d.ts.map +1 -1
  14. package/dist/utils/filter.js +51 -85
  15. package/dist/utils/index.d.ts +3 -5
  16. package/dist/utils/index.d.ts.map +1 -1
  17. package/dist/utils/index.js +3 -5
  18. package/dist/utils/read.d.ts +86 -0
  19. package/dist/utils/read.d.ts.map +1 -0
  20. package/dist/utils/read.js +124 -0
  21. package/dist/utils/reads.d.ts +24 -0
  22. package/dist/utils/reads.d.ts.map +1 -0
  23. package/dist/utils/reads.js +23 -0
  24. package/dist/utils/relationships.d.ts +9 -6
  25. package/dist/utils/relationships.d.ts.map +1 -1
  26. package/dist/utils/relationships.js +26 -8
  27. package/package.json +2 -2
  28. package/dist/utils/assert-queryable-fields.d.ts +0 -27
  29. package/dist/utils/assert-queryable-fields.d.ts.map +0 -1
  30. package/dist/utils/assert-queryable-fields.js +0 -47
  31. package/dist/utils/constants.d.ts +0 -7
  32. package/dist/utils/constants.d.ts.map +0 -1
  33. package/dist/utils/constants.js +0 -6
  34. package/dist/utils/include.d.ts +0 -80
  35. package/dist/utils/include.d.ts.map +0 -1
  36. package/dist/utils/include.js +0 -132
  37. package/dist/utils/table-query.d.ts +0 -41
  38. package/dist/utils/table-query.d.ts.map +0 -1
  39. package/dist/utils/table-query.js +0 -1
  40. package/dist/utils/total-order.d.ts +0 -15
  41. package/dist/utils/total-order.d.ts.map +0 -1
  42. package/dist/utils/total-order.js +0 -35
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reads.d.ts","sourceRoot":"","sources":["../../src/utils/reads.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,KAAK,0BAA0B,EAChC,MAAM,aAAa,CAAC;AACrB,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAChD,OAAO,KAAK,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAExD,0DAA0D;AAC1D,MAAM,WAAW,UAAU,CAAC,OAAO,SAAS,MAAM,GAAG,MAAM;IACzD,wGAAwG;IACxG,OAAO,CAAC,EAAE,SAAS,UAAU,CAAC,OAAO,CAAC,EAAE,CAAC;IACzC,6FAA6F;IAC7F,OAAO,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC,CAAC;CAClD;AAED,0EAA0E;AAC1E,MAAM,MAAM,KAAK,CAAC,OAAO,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,IAAI;IACrF,QAAQ,EAAE,CAAC,IAAI,MAAM,0BAA0B,CAAC,OAAO,CAAC,GAAG,MAAM,CAAC,CAAC,EAAE,UAAU,CAAC,UAAU,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC;CACxG,CAAC;AAEF;;;;;;;GAOG;AACH,wBAAgB,WAAW,CAAC,OAAO,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACjE,MAAM,EAAE,OAAO,EACf,KAAK,EAAE,KAAK,CAAC,OAAO,CAAC,GACpB,KAAK,CAAC,OAAO,CAAC,CAYhB"}
@@ -0,0 +1,23 @@
1
+ import { createTableRelationsHelpers, extractTablesRelationalConfig, } from 'drizzle-orm';
2
+ /**
3
+ * Declares, once per context, what holds for each table whichever read reaches it: the order its rows load in as a
4
+ * relation, and its `virtual` filter and order names. Every repository of the context hands the same value to its base.
5
+ *
6
+ * @param schema - the schema barrel the repositories are typed by; each key is checked against it
7
+ * @throws Error at declaration on a key that is no table export of `schema`, or an order naming a column its table
8
+ * does not hold
9
+ */
10
+ export function defineReads(schema, reads) {
11
+ const { tables } = extractTablesRelationalConfig(schema, createTableRelationsHelpers);
12
+ for (const [key, declared] of Object.entries(reads)) {
13
+ const table = Object.hasOwn(tables, key) ? tables[key] : undefined;
14
+ if (!table)
15
+ throw new Error(`"${key}" is no table export of the schema.`);
16
+ for (const entry of declared?.orderBy ?? []) {
17
+ if (!Object.hasOwn(table.columns, entry.field)) {
18
+ throw new Error(`"${key}" holds no column "${entry.field}" to order by.`);
19
+ }
20
+ }
21
+ }
22
+ return reads;
23
+ }
@@ -1,14 +1,17 @@
1
1
  import { type ExtractTablesWithRelations } from 'drizzle-orm';
2
- /** The relation names a schema declares on one table, as the map `permitsFor` takes for its relationship filters. */
3
- export type RelationshipsOf<TSchema extends Record<string, unknown>, K extends keyof ExtractTablesWithRelations<TSchema>> = {
4
- readonly [R in keyof ExtractTablesWithRelations<TSchema>[K]['relations'] & string]: Record<never, never>;
2
+ import type { RelationTarget } from '@zeroxsolutions/query';
3
+ /** The relations a schema declares on one table, keyed by name, each with the type it reaches. */
4
+ export type RelationsOf<TSchema extends Record<string, unknown>, K extends keyof ExtractTablesWithRelations<TSchema>> = {
5
+ readonly [R in keyof ExtractTablesWithRelations<TSchema>[K]['relations'] & string]: RelationTarget;
5
6
  };
6
7
  /**
7
- * The relations `schema` declares on the table exported as `table`, read the way `drizzle({ schema })` reads them, so a
8
- * relation added to the schema is a relationship a permit list can name without a second list to edit.
8
+ * The relations `schema` declares on the table exported as `table`, read the way `drizzle({ schema })` reads them: each
9
+ * with the type its fieldset is keyed by (its table's SQL name), its primary key, and its own relations, read when a
10
+ * path walks into them. A permit list names its relationships off it, so a relation added to the schema is one a
11
+ * permit list can name without a second list to edit.
9
12
  *
10
13
  * @param table - the table's EXPORT name in `schema`, not its SQL name
11
14
  * @throws Error when `table` is no table export of `schema`
12
15
  */
13
- export declare function relationshipsOf<TSchema extends Record<string, unknown>, K extends keyof ExtractTablesWithRelations<TSchema> & string>(schema: TSchema, table: K): RelationshipsOf<TSchema, K>;
16
+ export declare function relationsOf<TSchema extends Record<string, unknown>, K extends keyof ExtractTablesWithRelations<TSchema> & string>(schema: TSchema, table: K): RelationsOf<TSchema, K>;
14
17
  //# sourceMappingURL=relationships.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"relationships.d.ts","sourceRoot":"","sources":["../../src/utils/relationships.ts"],"names":[],"mappings":"AAAA,OAAO,EAA8D,KAAK,0BAA0B,EAAE,MAAM,aAAa,CAAC;AAE1H,qHAAqH;AACrH,MAAM,MAAM,eAAe,CACzB,OAAO,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACvC,CAAC,SAAS,MAAM,0BAA0B,CAAC,OAAO,CAAC,IACjD;IAAE,QAAQ,EAAE,CAAC,IAAI,MAAM,0BAA0B,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC,GAAG,MAAM,GAAG,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC;CAAE,CAAC;AAEjH;;;;;;GAMG;AACH,wBAAgB,eAAe,CAC7B,OAAO,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACvC,CAAC,SAAS,MAAM,0BAA0B,CAAC,OAAO,CAAC,GAAG,MAAM,EAC5D,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC,GAAG,eAAe,CAAC,OAAO,EAAE,CAAC,CAAC,CAKxD"}
1
+ {"version":3,"file":"relationships.d.ts","sourceRoot":"","sources":["../../src/utils/relationships.ts"],"names":[],"mappings":"AAAA,OAAO,EAIL,KAAK,0BAA0B,EAChC,MAAM,aAAa,CAAC;AACrB,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAG5D,kGAAkG;AAClG,MAAM,MAAM,WAAW,CACrB,OAAO,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACvC,CAAC,SAAS,MAAM,0BAA0B,CAAC,OAAO,CAAC,IACjD;IAAE,QAAQ,EAAE,CAAC,IAAI,MAAM,0BAA0B,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC,GAAG,MAAM,GAAG,cAAc;CAAE,CAAC;AAE3G;;;;;;;;GAQG;AACH,wBAAgB,WAAW,CACzB,OAAO,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACvC,CAAC,SAAS,MAAM,0BAA0B,CAAC,OAAO,CAAC,GAAG,MAAM,EAC5D,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC,GAAG,WAAW,CAAC,OAAO,EAAE,CAAC,CAAC,CAwBpD"}
@@ -1,15 +1,33 @@
1
- import { createTableRelationsHelpers, extractTablesRelationalConfig } from 'drizzle-orm';
1
+ import { createTableRelationsHelpers, extractTablesRelationalConfig, getTableUniqueName, } from 'drizzle-orm';
2
+ import { primaryKeyOf } from './read.js';
2
3
  /**
3
- * The relations `schema` declares on the table exported as `table`, read the way `drizzle({ schema })` reads them, so a
4
- * relation added to the schema is a relationship a permit list can name without a second list to edit.
4
+ * The relations `schema` declares on the table exported as `table`, read the way `drizzle({ schema })` reads them: each
5
+ * with the type its fieldset is keyed by (its table's SQL name), its primary key, and its own relations, read when a
6
+ * path walks into them. A permit list names its relationships off it, so a relation added to the schema is one a
7
+ * permit list can name without a second list to edit.
5
8
  *
6
9
  * @param table - the table's EXPORT name in `schema`, not its SQL name
7
10
  * @throws Error when `table` is no table export of `schema`
8
11
  */
9
- export function relationshipsOf(schema, table) {
10
- const { tables } = extractTablesRelationalConfig(schema, createTableRelationsHelpers);
11
- const config = Object.hasOwn(tables, table) ? tables[table] : undefined;
12
- if (!config)
12
+ export function relationsOf(schema, table) {
13
+ const { tables, tableNamesMap } = extractTablesRelationalConfig(schema, createTableRelationsHelpers);
14
+ if (!Object.hasOwn(tables, table))
13
15
  throw new Error(`"${table}" is no table export of the schema.`);
14
- return Object.fromEntries(Object.keys(config.relations).map((name) => [name, {}]));
16
+ const relationsAt = (key) => Object.fromEntries(Object.entries(tables[key]?.relations ?? {}).map(([name, relation]) => {
17
+ const target = tableNamesMap[getTableUniqueName(relation.referencedTable)] ?? relation.referencedTableName;
18
+ return [name, targetAt(target)];
19
+ }));
20
+ // `relations` is a getter: a schema's relations come back to the tables they left, so building them eagerly
21
+ // would never end.
22
+ const targetAt = (key) => {
23
+ const config = tables[key];
24
+ return {
25
+ type: config?.dbName ?? key,
26
+ primaryKey: config ? primaryKeyOf(config) : [],
27
+ get relations() {
28
+ return relationsAt(key);
29
+ },
30
+ };
31
+ };
32
+ return relationsAt(table);
15
33
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zeroxsolutions/db",
3
- "version": "9.5.0",
3
+ "version": "10.0.0",
4
4
  "description": "Generic Drizzle toolkit - the id column and audit timestamps a table takes, a base repository, the unit-of-work and transactional outbox impls, and the filter/sort/pagination query helpers, for PostgreSQL (postgres-js) and Cloudflare D1; ships no domain schema. Shared across ZeroXSolutions services.",
5
5
  "private": false,
6
6
  "type": "module",
@@ -42,7 +42,7 @@
42
42
  "dependencies": {
43
43
  "tslib": "^2.3.0",
44
44
  "uuid": "^14.0.1",
45
- "@zeroxsolutions/query": "0.7.3",
45
+ "@zeroxsolutions/query": "0.7.4",
46
46
  "@zeroxsolutions/cosmic": "0.3.4"
47
47
  },
48
48
  "peerDependencies": {
@@ -1,27 +0,0 @@
1
- import type { GuardedCollectionQuery } from '@zeroxsolutions/query';
2
- import type { FilterValue, QueryableFields } from './filter.js';
3
- /** A `filter` targeted a field outside the entity's filterable whitelist. */
4
- export declare class InvalidQueryFilter extends Error {
5
- constructor(field: string);
6
- }
7
- /** A `sort` targeted a field outside the entity's sortable whitelist. */
8
- export declare class InvalidQuerySort extends Error {
9
- constructor(field: string);
10
- }
11
- /**
12
- * Rejects a `filter`/`sort` on any field outside a listable entity's {@link QueryableFields} whitelist,
13
- * so a client can never filter or order by a hidden column. A repository finder calls this before
14
- * building SQL.
15
- *
16
- * @param query - the parsed wire query, read for its `filter` / `sort` (a no-op when `undefined`)
17
- * @param resolved - the names a resolver answers, which no row key could list
18
- * @throws {@link InvalidQueryFilter} naming the first filtered field outside `allow.filterable` and
19
- * `resolved.filterable`
20
- * @throws {@link InvalidQuerySort} naming the first sorted field outside `allow.sortable` and
21
- * `resolved.sortable`
22
- */
23
- export declare function assertQueryableFields<TSelect>(query: Partial<Pick<GuardedCollectionQuery<FilterValue>, 'filter' | 'sort'>> | undefined, allow: QueryableFields<TSelect>, resolved?: {
24
- filterable?: readonly string[];
25
- sortable?: readonly string[];
26
- }): void;
27
- //# sourceMappingURL=assert-queryable-fields.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"assert-queryable-fields.d.ts","sourceRoot":"","sources":["../../src/utils/assert-queryable-fields.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,uBAAuB,CAAC;AACpE,OAAO,KAAK,EAAc,WAAW,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAG5E,6EAA6E;AAC7E,qBAAa,kBAAmB,SAAQ,KAAK;gBAC/B,KAAK,EAAE,MAAM;CAG1B;AAED,yEAAyE;AACzE,qBAAa,gBAAiB,SAAQ,KAAK;gBAC7B,KAAK,EAAE,MAAM;CAG1B;AAaD;;;;;;;;;;;GAWG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAC3C,KAAK,EAAE,OAAO,CAAC,IAAI,CAAC,sBAAsB,CAAC,WAAW,CAAC,EAAE,QAAQ,GAAG,MAAM,CAAC,CAAC,GAAG,SAAS,EACxF,KAAK,EAAE,eAAe,CAAC,OAAO,CAAC,EAC/B,QAAQ,GAAE;IAAE,UAAU,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAAC,QAAQ,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;CAAO,GAC9E,IAAI,CAUN"}
@@ -1,47 +0,0 @@
1
- import { isFilterGroup } from './filter.js';
2
- /** A `filter` targeted a field outside the entity's filterable whitelist. */
3
- export class InvalidQueryFilter extends Error {
4
- constructor(field) {
5
- super(`Filtering by "${field}" is not supported.`);
6
- }
7
- }
8
- /** A `sort` targeted a field outside the entity's sortable whitelist. */
9
- export class InvalidQuerySort extends Error {
10
- constructor(field) {
11
- super(`Sorting by "${field}" is not supported.`);
12
- }
13
- }
14
- /** Walks the tree so a condition nested in a group is checked too, not just the root nodes. */
15
- function assertFilterNodes(nodes, filterable) {
16
- for (const node of nodes) {
17
- if (isFilterGroup(node)) {
18
- assertFilterNodes(node.children, filterable);
19
- }
20
- else if (!filterable.includes(node.field)) {
21
- throw new InvalidQueryFilter(node.field);
22
- }
23
- }
24
- }
25
- /**
26
- * Rejects a `filter`/`sort` on any field outside a listable entity's {@link QueryableFields} whitelist,
27
- * so a client can never filter or order by a hidden column. A repository finder calls this before
28
- * building SQL.
29
- *
30
- * @param query - the parsed wire query, read for its `filter` / `sort` (a no-op when `undefined`)
31
- * @param resolved - the names a resolver answers, which no row key could list
32
- * @throws {@link InvalidQueryFilter} naming the first filtered field outside `allow.filterable` and
33
- * `resolved.filterable`
34
- * @throws {@link InvalidQuerySort} naming the first sorted field outside `allow.sortable` and
35
- * `resolved.sortable`
36
- */
37
- export function assertQueryableFields(query, allow, resolved = {}) {
38
- // Widened to `string`: the membership check runs against client-supplied names, not keys of TSelect.
39
- const filterable = [...allow.filterable, ...(resolved.filterable ?? [])];
40
- const sortable = [...allow.sortable, ...(resolved.sortable ?? [])];
41
- assertFilterNodes(query?.filter ?? [], filterable);
42
- for (const order of query?.sort ?? []) {
43
- if (!sortable.includes(order.field)) {
44
- throw new InvalidQuerySort(order.field);
45
- }
46
- }
47
- }
@@ -1,7 +0,0 @@
1
- /** Hard cap on a page's row count - a larger `limit` is silently clamped to this, not rejected. */
2
- export declare const MAX_PAGE_SIZE = 100;
3
- /** The offset applied when a query omits `offset` - starts at the first row. */
4
- export declare const DEFAULT_OFFSET = 0;
5
- /** The page size applied when a query omits `limit`. */
6
- export declare const DEFAULT_PAGE_SIZE = 20;
7
- //# sourceMappingURL=constants.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../../src/utils/constants.ts"],"names":[],"mappings":"AAAA,mGAAmG;AACnG,eAAO,MAAM,aAAa,MAAM,CAAC;AAEjC,gFAAgF;AAChF,eAAO,MAAM,cAAc,IAAI,CAAC;AAEhC,wDAAwD;AACxD,eAAO,MAAM,iBAAiB,KAAK,CAAC"}
@@ -1,6 +0,0 @@
1
- /** Hard cap on a page's row count - a larger `limit` is silently clamped to this, not rejected. */
2
- export const MAX_PAGE_SIZE = 100;
3
- /** The offset applied when a query omits `offset` - starts at the first row. */
4
- export const DEFAULT_OFFSET = 0;
5
- /** The page size applied when a query omits `limit`. */
6
- export const DEFAULT_PAGE_SIZE = 20;
@@ -1,80 +0,0 @@
1
- import { type Table, type View } from 'drizzle-orm';
2
- /** An include path named a relation the schema does not hold. */
3
- export declare class UnknownIncludePath extends Error {
4
- readonly path: string;
5
- constructor(path: string);
6
- }
7
- /** One relation, narrowed to what walking a path needs. */
8
- interface RelationTarget {
9
- referencedTableName: string;
10
- }
11
- /** A column, narrowed to what picking it needs. */
12
- interface ColumnTarget {
13
- name: string;
14
- }
15
- /** The relational metadata drizzle computes when the client is built (`db._.schema`), narrowed the same way. */
16
- export interface RelationalTables {
17
- [exportName: string]: {
18
- dbName: string;
19
- columns: Record<string, ColumnTarget>;
20
- primaryKey: readonly ColumnTarget[];
21
- relations: Record<string, RelationTarget>;
22
- };
23
- }
24
- /** What a finder was asked to load, as its options carry it. */
25
- export interface RelationalReadOptions {
26
- /** Fieldsets keyed by table name; a table with none loads every column. */
27
- fields?: Readonly<Record<string, readonly string[] | undefined>>;
28
- /** Relation paths, dot-separated as the wire states them. */
29
- include?: readonly string[];
30
- /** Root columns loaded on top of the root fieldset; never narrows it. */
31
- columns?: Readonly<Record<string, boolean | undefined>>;
32
- /** A relational config the caller spells out, winning over `include` wherever both name a relation. */
33
- with?: unknown;
34
- }
35
- /** One level of drizzle's relational config: the columns it selects, and the relations it loads under it. */
36
- export interface RelationalRead {
37
- /** Absent where every column loads. */
38
- columns?: Record<string, true>;
39
- /** Absent where no relation loads, which is what lets a finder take the plain `select()` path. */
40
- with?: Record<string, unknown>;
41
- }
42
- /**
43
- * A single table or view described the way {@link RelationalTables} describes one, for a read given no schema.
44
- *
45
- * A view declares no primary key of its own; the key it reads under is whichever selected column is a primary key of
46
- * the table it was selected from.
47
- */
48
- export declare function describeTable(table: Table | View): RelationalTables[string];
49
- /**
50
- * `selectable` narrowed to the keys a {@link RelationalRead} level selects, for a plain `select()`.
51
- *
52
- * @param columns - a level's `columns`; absent, every one of `selectable` is kept
53
- */
54
- export declare function selectedColumns<TSelectable extends Record<string, unknown>>(selectable: TSelectable, columns: Readonly<Record<string, true>> | undefined): TSelectable;
55
- /**
56
- * What one finder call loads: resolved against the relational schema `executor` was built with where the repository
57
- * has a `queryKey`, and against its own table alone where it has not - so a fieldset still narrows the row.
58
- *
59
- * @param executor - the drizzle client or transaction the call runs on; its `_.schema` is drizzle's relational config
60
- * @throws when `include` is given and the repository has no `queryKey`, or its client no schema
61
- * @throws {@link UnknownIncludePath} as {@link buildRelationalRead} does
62
- */
63
- export declare function resolveRelationalRead(executor: unknown, queryKey: PropertyKey | undefined, table: Table | View, options: RelationalReadOptions | undefined): RelationalRead;
64
- /**
65
- * Resolves a read into drizzle's relational config: `fields` into each level's `columns`, `include` into nested
66
- * `with`, then `columns` and `with` as the caller spelled them.
67
- *
68
- * - A fieldset selects its columns and the table's primary key; a relation it names loads that relation's primary keys,
69
- * which is the linkage a relationship publishes. A name that is neither loads nothing: the permit-list and the
70
- * `TableQuery` type are what refuse one.
71
- * - An included relation replaces any linkage the fieldset gave it, and loads under its own table's fieldset.
72
- *
73
- * @param rootTable - the EXPORT name of the table the read starts from, not its table name
74
- * @throws {@link UnknownIncludePath} naming the path up to and including the segment that failed - drizzle@0.45.1
75
- * raises on a relation it does not hold too, but as `Cannot read properties of undefined (reading
76
- * 'referencedTable')`, naming neither the path nor the relation
77
- */
78
- export declare function buildRelationalRead(tables: RelationalTables, rootTable: string, options: RelationalReadOptions): RelationalRead;
79
- export {};
80
- //# sourceMappingURL=include.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"include.d.ts","sourceRoot":"","sources":["../../src/utils/include.ts"],"names":[],"mappings":"AAAA,OAAO,EAA6E,KAAK,KAAK,EAAE,KAAK,IAAI,EAAE,MAAM,aAAa,CAAC;AAE/H,iEAAiE;AACjE,qBAAa,kBAAmB,SAAQ,KAAK;IAC/B,QAAQ,CAAC,IAAI,EAAE,MAAM;gBAAZ,IAAI,EAAE,MAAM;CAGlC;AAED,2DAA2D;AAC3D,UAAU,cAAc;IACtB,mBAAmB,EAAE,MAAM,CAAC;CAC7B;AAED,mDAAmD;AACnD,UAAU,YAAY;IACpB,IAAI,EAAE,MAAM,CAAC;CACd;AAED,gHAAgH;AAChH,MAAM,WAAW,gBAAgB;IAC/B,CAAC,UAAU,EAAE,MAAM,GAAG;QACpB,MAAM,EAAE,MAAM,CAAC;QACf,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;QACtC,UAAU,EAAE,SAAS,YAAY,EAAE,CAAC;QACpC,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC;KAC3C,CAAC;CACH;AAED,gEAAgE;AAChE,MAAM,WAAW,qBAAqB;IACpC,2EAA2E;IAC3E,MAAM,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,GAAG,SAAS,CAAC,CAAC,CAAC;IACjE,6DAA6D;IAC7D,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC5B,yEAAyE;IACzE,OAAO,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,GAAG,SAAS,CAAC,CAAC,CAAC;IACxD,uGAAuG;IACvG,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AAED,6GAA6G;AAC7G,MAAM,WAAW,cAAc;IAC7B,uCAAuC;IACvC,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;IAC/B,kGAAkG;IAClG,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAChC;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI,GAAG,gBAAgB,CAAC,MAAM,CAAC,CAY3E;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,WAAW,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACzE,UAAU,EAAE,WAAW,EACvB,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC,GAAG,SAAS,GAClD,WAAW,CAGb;AAED;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CACnC,QAAQ,EAAE,OAAO,EACjB,QAAQ,EAAE,WAAW,GAAG,SAAS,EACjC,KAAK,EAAE,KAAK,GAAG,IAAI,EACnB,OAAO,EAAE,qBAAqB,GAAG,SAAS,GACzC,cAAc,CAShB;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,gBAAgB,EACxB,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,qBAAqB,GAC7B,cAAc,CAmEhB"}
@@ -1,132 +0,0 @@
1
- import { getTableColumns, getTableName, getViewName, getViewSelectedFields, isView } from 'drizzle-orm';
2
- /** An include path named a relation the schema does not hold. */
3
- export class UnknownIncludePath extends Error {
4
- path;
5
- constructor(path) {
6
- super(`Including "${path}" is not supported.`);
7
- this.path = path;
8
- }
9
- }
10
- /**
11
- * A single table or view described the way {@link RelationalTables} describes one, for a read given no schema.
12
- *
13
- * A view declares no primary key of its own; the key it reads under is whichever selected column is a primary key of
14
- * the table it was selected from.
15
- */
16
- export function describeTable(table) {
17
- const view = isView(table);
18
- const columns = (view ? getViewSelectedFields(table) : getTableColumns(table));
19
- return {
20
- dbName: view ? getViewName(table) : getTableName(table),
21
- columns,
22
- primaryKey: Object.values(columns).filter((column) => column.primary),
23
- relations: {},
24
- };
25
- }
26
- /**
27
- * `selectable` narrowed to the keys a {@link RelationalRead} level selects, for a plain `select()`.
28
- *
29
- * @param columns - a level's `columns`; absent, every one of `selectable` is kept
30
- */
31
- export function selectedColumns(selectable, columns) {
32
- if (!columns)
33
- return selectable;
34
- return Object.fromEntries(Object.entries(selectable).filter(([key]) => columns[key]));
35
- }
36
- /**
37
- * What one finder call loads: resolved against the relational schema `executor` was built with where the repository
38
- * has a `queryKey`, and against its own table alone where it has not - so a fieldset still narrows the row.
39
- *
40
- * @param executor - the drizzle client or transaction the call runs on; its `_.schema` is drizzle's relational config
41
- * @throws when `include` is given and the repository has no `queryKey`, or its client no schema
42
- * @throws {@link UnknownIncludePath} as {@link buildRelationalRead} does
43
- */
44
- export function resolveRelationalRead(executor, queryKey, table, options) {
45
- if (!options)
46
- return {};
47
- const tables = executor?._?.schema;
48
- if (queryKey !== undefined && tables)
49
- return buildRelationalRead(tables, String(queryKey), options);
50
- if (options.include?.length) {
51
- throw new Error('"include" needs a queryKey and a client built with its schema.');
52
- }
53
- const described = describeTable(table);
54
- return buildRelationalRead({ [described.dbName]: described }, described.dbName, options);
55
- }
56
- /**
57
- * Resolves a read into drizzle's relational config: `fields` into each level's `columns`, `include` into nested
58
- * `with`, then `columns` and `with` as the caller spelled them.
59
- *
60
- * - A fieldset selects its columns and the table's primary key; a relation it names loads that relation's primary keys,
61
- * which is the linkage a relationship publishes. A name that is neither loads nothing: the permit-list and the
62
- * `TableQuery` type are what refuse one.
63
- * - An included relation replaces any linkage the fieldset gave it, and loads under its own table's fieldset.
64
- *
65
- * @param rootTable - the EXPORT name of the table the read starts from, not its table name
66
- * @throws {@link UnknownIncludePath} naming the path up to and including the segment that failed - drizzle@0.45.1
67
- * raises on a relation it does not hold too, but as `Cannot read properties of undefined (reading
68
- * 'referencedTable')`, naming neither the path nor the relation
69
- */
70
- export function buildRelationalRead(tables, rootTable, options) {
71
- // A relation names its target by TABLE name while `tables` is keyed by EXPORT name; looking the
72
- // target up directly reads as "no such relation" wherever the two differ.
73
- const byTableName = new Map(Object.entries(tables).map(([key, table]) => [table.dbName, key]));
74
- const exportNameOf = (relation) => byTableName.get(relation.referencedTableName) ?? relation.referencedTableName;
75
- const primaryKeyOf = (tableKey) => {
76
- const table = tables[tableKey];
77
- if (!table)
78
- return [];
79
- const names = new Set(table.primaryKey.map((column) => column.name));
80
- return Object.keys(table.columns).filter((key) => names.has(table.columns[key]?.name ?? ''));
81
- };
82
- const selecting = (keys) => Object.fromEntries(keys.map((key) => [key, true]));
83
- const levelOf = (tableKey) => {
84
- const table = tables[tableKey];
85
- const fieldset = table ? options.fields?.[table.dbName] : undefined;
86
- if (!table || fieldset === undefined)
87
- return {};
88
- const level = {
89
- columns: selecting([...primaryKeyOf(tableKey), ...fieldset.filter((name) => name in table.columns)]),
90
- };
91
- for (const name of fieldset) {
92
- const relation = table.relations[name];
93
- if (!relation)
94
- continue;
95
- level.with ??= {};
96
- level.with[name] = { columns: selecting(primaryKeyOf(exportNameOf(relation))) };
97
- }
98
- return level;
99
- };
100
- const root = levelOf(rootTable);
101
- const included = new Set();
102
- for (const path of options.include ?? []) {
103
- let tableKey = rootTable;
104
- let level = root;
105
- const walked = [];
106
- for (const segment of path.split('.')) {
107
- walked.push(segment);
108
- const relation = tables[tableKey]?.relations[segment];
109
- if (!relation)
110
- throw new UnknownIncludePath(walked.join('.'));
111
- tableKey = exportNameOf(relation);
112
- level.with ??= {};
113
- let next = level.with[segment];
114
- if (next === undefined || !included.has(next)) {
115
- next = levelOf(tableKey);
116
- included.add(next);
117
- level.with[segment] = next;
118
- }
119
- level = next;
120
- }
121
- }
122
- if (root.columns) {
123
- for (const [key, loaded] of Object.entries(options.columns ?? {})) {
124
- if (loaded)
125
- root.columns[key] = true;
126
- }
127
- }
128
- if (options.with !== undefined) {
129
- root.with = { ...root.with, ...options.with };
130
- }
131
- return root;
132
- }
@@ -1,41 +0,0 @@
1
- import type { ExtractTablesWithRelations, FindTableByDBName, TableRelationalConfig, TablesRelationalConfig } from 'drizzle-orm';
2
- /** How many relations deep {@link IncludePath} spells a path; a deeper one is spelled out with `with`. */
3
- type IncludeDepth = 3;
4
- /** Indexed by a depth, the depth one relation further in. */
5
- type OneLess = [never, 0, 1, 2];
6
- /**
7
- * Every dotted relation path that starts at `TTable`, up to three relations deep.
8
- *
9
- * A path may come back to a table it passed through (`author.books` from a book), so depth is what keeps the type
10
- * finite, not the tables visited.
11
- */
12
- export type IncludePath<TSchema extends TablesRelationalConfig, TTable extends TableRelationalConfig, TDepth extends number = IncludeDepth> = [TDepth] extends [0] ? never : TTable extends TableRelationalConfig ? {
13
- [K in keyof TTable['relations'] & string]: K | `${K}.${IncludePath<TSchema, FindTableByDBName<TSchema, TTable['relations'][K]['referencedTableName']>, OneLess[TDepth]>}`;
14
- }[keyof TTable['relations'] & string] : never;
15
- /**
16
- * A JSON:API read of one table, typed off its drizzle schema: fieldsets keyed by TABLE name holding column and relation
17
- * names, include paths walked by relation name.
18
- *
19
- * Its shape is the query `@zeroxsolutions/query` guards, so a guarded query whose permit-list was declared `as const`
20
- * and checked with `satisfies TableQuery<typeof schema, '<export name>'>` passes to a finder without a cast. A
21
- * repository whose schema is not given types it by name only.
22
- */
23
- export type TableQuery<TSchema extends Record<string, unknown>, TTableName extends keyof ExtractTablesWithRelations<TSchema>> = [TTableName] extends [never] ? {
24
- /** As the schema-typed branch below: keyed by table name, holding column and relation names. */
25
- fields?: Readonly<Record<string, readonly string[]>>;
26
- /** As the schema-typed branch below: relation paths, dot-separated. */
27
- include?: readonly string[];
28
- } : {
29
- /**
30
- * Fieldsets keyed by table name. A table given one loads those columns and its primary key, and each relation it
31
- * names loads the related primary keys; an empty one loads the primary key alone; a table given none loads every
32
- * column.
33
- */
34
- fields?: {
35
- [K in keyof ExtractTablesWithRelations<TSchema> as ExtractTablesWithRelations<TSchema>[K]['dbName']]?: readonly ((keyof ExtractTablesWithRelations<TSchema>[K]['columns'] | keyof ExtractTablesWithRelations<TSchema>[K]['relations']) & string)[];
36
- };
37
- /** Relation paths, dot-separated; each relation loads under its own table's fieldset. Needs a `queryKey`. */
38
- include?: readonly IncludePath<ExtractTablesWithRelations<TSchema>, ExtractTablesWithRelations<TSchema>[TTableName]>[];
39
- };
40
- export {};
41
- //# sourceMappingURL=table-query.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"table-query.d.ts","sourceRoot":"","sources":["../../src/utils/table-query.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,0BAA0B,EAC1B,iBAAiB,EACjB,qBAAqB,EACrB,sBAAsB,EACvB,MAAM,aAAa,CAAC;AAErB,0GAA0G;AAC1G,KAAK,YAAY,GAAG,CAAC,CAAC;AAEtB,6DAA6D;AAC7D,KAAK,OAAO,GAAG,CAAC,KAAK,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;AAEhC;;;;;GAKG;AACH,MAAM,MAAM,WAAW,CACrB,OAAO,SAAS,sBAAsB,EACtC,MAAM,SAAS,qBAAqB,EACpC,MAAM,SAAS,MAAM,GAAG,YAAY,IAClC,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,GACpB,KAAK,GACL,MAAM,SAAS,qBAAqB,GAClC;KACG,CAAC,IAAI,MAAM,MAAM,CAAC,WAAW,CAAC,GAAG,MAAM,GACpC,CAAC,GACD,GAAG,CAAC,IAAI,WAAW,CAAC,OAAO,EAAE,iBAAiB,CAAC,OAAO,EAAE,MAAM,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC,qBAAqB,CAAC,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC,EAAE;CAC/H,CAAC,MAAM,MAAM,CAAC,WAAW,CAAC,GAAG,MAAM,CAAC,GACrC,KAAK,CAAC;AAEZ;;;;;;;GAOG;AACH,MAAM,MAAM,UAAU,CACpB,OAAO,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACvC,UAAU,SAAS,MAAM,0BAA0B,CAAC,OAAO,CAAC,IAC1D,CAAC,UAAU,CAAC,SAAS,CAAC,KAAK,CAAC,GAC5B;IACE,gGAAgG;IAChG,MAAM,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC,CAAC,CAAC;IACrD,uEAAuE;IACvE,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAC7B,GACD;IACE;;;;OAIG;IACH,MAAM,CAAC,EAAE;SACN,CAAC,IAAI,MAAM,0BAA0B,CAAC,OAAO,CAAC,IAAI,0BAA0B,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,SAAS,CAAC,CAC7G,MAAM,0BAA0B,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,GACvD,MAAM,0BAA0B,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC,CAC5D,GACC,MAAM,CAAC,EAAE;KACZ,CAAC;IACF,6GAA6G;IAC7G,OAAO,CAAC,EAAE,SAAS,WAAW,CAAC,0BAA0B,CAAC,OAAO,CAAC,EAAE,0BAA0B,CAAC,OAAO,CAAC,CAAC,UAAU,CAAC,CAAC,EAAE,CAAC;CACxH,CAAC"}
@@ -1 +0,0 @@
1
- export {};
@@ -1,15 +0,0 @@
1
- import type { SortField } from '@zeroxsolutions/query';
2
- import { PgTable, type TableConfig as PgTableConfig } from 'drizzle-orm/pg-core';
3
- import { type SQLiteTable, type TableConfig as SQLiteTableConfig } from 'drizzle-orm/sqlite-core';
4
- type OrderableTable = PgTable<PgTableConfig> | SQLiteTable<SQLiteTableConfig>;
5
- /**
6
- * The caller's sort followed by each primary key column it does not already name, ascending, so no
7
- * two rows tie and every page of a walk is cut from the same order.
8
- *
9
- * Kept apart from `buildOrderBy`, which a consumer applies one clause at a time to build its own
10
- * order: a key appended to each clause would land in the middle of that order. Kept out of
11
- * `utils/index.ts` too, which re-exports everything it names.
12
- */
13
- export declare function totalOrder(table: OrderableTable, sort: SortField[] | undefined): SortField[];
14
- export {};
15
- //# sourceMappingURL=total-order.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"total-order.d.ts","sourceRoot":"","sources":["../../src/utils/total-order.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAEvD,OAAO,EAAsC,OAAO,EAAE,KAAK,WAAW,IAAI,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACrH,OAAO,EAEL,KAAK,WAAW,EAChB,KAAK,WAAW,IAAI,iBAAiB,EACtC,MAAM,yBAAyB,CAAC;AAEjC,KAAK,cAAc,GAAG,OAAO,CAAC,aAAa,CAAC,GAAG,WAAW,CAAC,iBAAiB,CAAC,CAAC;AAE9E;;;;;;;GAOG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,cAAc,EAAE,IAAI,EAAE,SAAS,EAAE,GAAG,SAAS,GAAG,SAAS,EAAE,CAO5F"}
@@ -1,35 +0,0 @@
1
- import { getTableColumns, is } from 'drizzle-orm';
2
- import { getTableConfig as getPgTableConfig, PgTable } from 'drizzle-orm/pg-core';
3
- import { getTableConfig as getSQLiteTableConfig, } from 'drizzle-orm/sqlite-core';
4
- /**
5
- * The caller's sort followed by each primary key column it does not already name, ascending, so no
6
- * two rows tie and every page of a walk is cut from the same order.
7
- *
8
- * Kept apart from `buildOrderBy`, which a consumer applies one clause at a time to build its own
9
- * order: a key appended to each clause would land in the middle of that order. Kept out of
10
- * `utils/index.ts` too, which re-exports everything it names.
11
- */
12
- export function totalOrder(table, sort) {
13
- const requested = sort ?? [];
14
- const named = new Set(requested.map((item) => item.field));
15
- const appended = primaryKeyFields(table)
16
- .filter((field) => !named.has(field))
17
- .map((field) => ({ field, direction: 'asc' }));
18
- return [...requested, ...appended];
19
- }
20
- /**
21
- * The property keys of the table's primary key columns, in the order the key declares them.
22
- *
23
- * A property key and not the SQL name, because a `SortField` names a column the way
24
- * `getTableColumns` keys it. A composite key sets no column's `primary` flag, so it is read off the
25
- * table config instead, and matched back by SQL name: pg-core hands the key its own copies of the
26
- * columns, so the key's column objects are not the table's (drizzle-orm@0.45.1).
27
- */
28
- function primaryKeyFields(table) {
29
- const columns = getTableColumns(table);
30
- const fieldOf = new Map(Object.entries(columns).map(([field, column]) => [column.name, field]));
31
- const declared = Object.values(columns).filter((column) => column.primary);
32
- const composite = (is(table, PgTable) ? getPgTableConfig(table) : getSQLiteTableConfig(table)).primaryKeys[0]?.columns;
33
- const keyColumns = declared.length > 0 ? declared : (composite ?? []);
34
- return keyColumns.flatMap((column) => fieldOf.get(column.name) ?? []);
35
- }