@zeroxsolutions/db 9.1.0 → 9.3.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/CHANGELOG.md CHANGED
@@ -1,3 +1,42 @@
1
+ ## 9.3.0 (2026-09-28)
2
+
3
+ ### 🚀 Features
4
+
5
+ - **db:** read a table's relationship names off the schema ([2a8045f](https://github.com/zeroxsolutions/zeroxsolutions/commit/2a8045f))
6
+
7
+ ### ❤️ Thank You
8
+
9
+ - Claude Opus 5.5 (1M context)
10
+ - Lương Văn Tú
11
+
12
+ ## 9.2.0 (2026-09-28)
13
+
14
+ ### 🚀 Features
15
+
16
+ - **db:** resolve a relationship filter against its foreign-key column ([32903ba](https://github.com/zeroxsolutions/zeroxsolutions/commit/32903ba))
17
+
18
+ ### ❤️ Thank You
19
+
20
+ - Claude Opus 5.5 (1M context)
21
+ - Lương Văn Tú
22
+
23
+ ## 9.1.0 (2026-09-27)
24
+
25
+ ### 🚀 Features
26
+
27
+ - **db:** order by a path no column holds, and admit resolved names ([bc10882](https://github.com/zeroxsolutions/zeroxsolutions/commit/bc10882))
28
+ - **db:** resolve a filter path no column holds through its own expression ([4627742](https://github.com/zeroxsolutions/zeroxsolutions/commit/4627742))
29
+
30
+ ### 🩹 Fixes
31
+
32
+ - **db:** parenthesize a resolved condition and read resolvers by own key ([56bb1e4](https://github.com/zeroxsolutions/zeroxsolutions/commit/56bb1e4))
33
+
34
+ ### ❤️ Thank You
35
+
36
+ - Claude Opus 5.5 (1M context)
37
+ - Claude Sonnet 5
38
+ - Lương Văn Tú
39
+
1
40
  ## 9.0.1 (2026-09-27)
2
41
 
3
42
  ### 🩹 Fixes
package/README.md CHANGED
@@ -361,6 +361,36 @@ All 15, as `filterOperatorSchema` declares them:
361
361
 
362
362
  A `%` or `_` in a text value is escaped, so it matches literally rather than as a wildcard.
363
363
 
364
+ #### Relationship Filters
365
+
366
+ A relationship named in a permit list is no column - its id sits in a foreign-key column instead, which
367
+ `buildFilterConditions` cannot resolve on its own. Declare the permit through `permitsFor`'s own
368
+ `relationship(name, operators)` (`@zeroxsolutions/query`), then hand `columnResolver` the foreign-key
369
+ column under that same name in `resolvers`:
370
+
371
+ ```ts
372
+ import { permitsFor } from '@zeroxsolutions/query';
373
+ import { buildFilterConditions, columnResolver } from '@zeroxsolutions/db/utils';
374
+
375
+ const placesQueryPermits = permitsFor(placeAttributes, { relationships: places.relationships }).relationship(
376
+ 'region',
377
+ ['equals', 'in'],
378
+ );
379
+
380
+ const condition = buildFilterConditions({
381
+ table: places,
382
+ filter: query.filter,
383
+ resolvers: { region: columnResolver(places.regionId) },
384
+ });
385
+ ```
386
+
387
+ `columnResolver` answers `equals` and `in` - the only two operators a relationship filter grants -
388
+ comparing the id against the column it closes over, and throws on any other operator.
389
+
390
+ - `relationshipsOf(schema, '<table export>')` - the relations the schema declares on that table, as the map
391
+ `permitsFor(schema, { relationships })` takes, so a relationship filter names a relation the schema holds.
392
+ Throws on a key that is no table export (a table's SQL name is not one).
393
+
364
394
  ### Order Builder
365
395
 
366
396
  ```ts
@@ -104,6 +104,13 @@ export type FilterResolver = (leaf: FilterValue) => SQL | undefined;
104
104
  export type FilterResolvers<TTable extends FilterableTable, P> = {
105
105
  readonly [F in Exclude<keyof FilterableOf<P> & string, keyof TTable['_']['columns'] & string>]: FilterResolver;
106
106
  };
107
+ /**
108
+ * The resolver for a relationship filter: the relationship's name is no column, and the id it compares is held in its
109
+ * foreign-key column. `{ region: columnResolver(places.regionId) }` declares one.
110
+ *
111
+ * @throws Error on an operator other than `equals` and `in`, the only two a relationship filter grants
112
+ */
113
+ export declare function columnResolver(column: Column): FilterResolver;
107
114
  /** What a filter may reach: the table to resolve fields against, the tree to build, and the paths no column holds. */
108
115
  export interface FilterQuery<TTable extends FilterableTable = FilterableTable> {
109
116
  table: TTable;
@@ -1 +1 @@
1
- {"version":3,"file":"filter.d.ts","sourceRoot":"","sources":["../../src/utils/filter.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"filter.d.ts","sourceRoot":"","sources":["../../src/utils/filter.ts"],"names":[],"mappings":"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"}
@@ -88,6 +88,21 @@ export class UnknownQueryField extends Error {
88
88
  super(`No column resolves the field "${field}".`);
89
89
  }
90
90
  }
91
+ /**
92
+ * The resolver for a relationship filter: the relationship's name is no column, and the id it compares is held in its
93
+ * foreign-key column. `{ region: columnResolver(places.regionId) }` declares one.
94
+ *
95
+ * @throws Error on an operator other than `equals` and `in`, the only two a relationship filter grants
96
+ */
97
+ export function columnResolver(column) {
98
+ return (leaf) => {
99
+ if (leaf.operator === 'equals')
100
+ return eq(column, leaf.value);
101
+ if (leaf.operator === 'in')
102
+ return leaf.value.length > 0 ? inArray(column, leaf.value) : undefined;
103
+ throw new Error(`A relationship filter answers equals and in, not "${leaf.operator}".`);
104
+ };
105
+ }
91
106
  /**
92
107
  * The character that makes the next one literal. It is its own escape, so the character class below
93
108
  * must list it first: escaping `%` before `\` would re-escape the backslash the first pass added.
@@ -3,5 +3,6 @@ export * from './constants.js';
3
3
  export * from './filter.js';
4
4
  export * from './include.js';
5
5
  export * from './omit.js';
6
+ export * from './relationships.js';
6
7
  export * from './table-query.js';
7
8
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAAA,cAAc,8BAA8B,CAAC;AAC7C,cAAc,gBAAgB,CAAC;AAC/B,cAAc,aAAa,CAAC;AAC5B,cAAc,cAAc,CAAC;AAC7B,cAAc,WAAW,CAAC;AAC1B,cAAc,kBAAkB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAAA,cAAc,8BAA8B,CAAC;AAC7C,cAAc,gBAAgB,CAAC;AAC/B,cAAc,aAAa,CAAC;AAC5B,cAAc,cAAc,CAAC;AAC7B,cAAc,WAAW,CAAC;AAC1B,cAAc,oBAAoB,CAAC;AACnC,cAAc,kBAAkB,CAAC"}
@@ -3,4 +3,5 @@ export * from './constants.js';
3
3
  export * from './filter.js';
4
4
  export * from './include.js';
5
5
  export * from './omit.js';
6
+ export * from './relationships.js';
6
7
  export * from './table-query.js';
@@ -0,0 +1,14 @@
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>;
5
+ };
6
+ /**
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.
9
+ *
10
+ * @param table - the table's EXPORT name in `schema`, not its SQL name
11
+ * @throws Error when `table` is no table export of `schema`
12
+ */
13
+ export declare function relationshipsOf<TSchema extends Record<string, unknown>, K extends keyof ExtractTablesWithRelations<TSchema> & string>(schema: TSchema, table: K): RelationshipsOf<TSchema, K>;
14
+ //# sourceMappingURL=relationships.d.ts.map
@@ -0,0 +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"}
@@ -0,0 +1,15 @@
1
+ import { createTableRelationsHelpers, extractTablesRelationalConfig } from 'drizzle-orm';
2
+ /**
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.
5
+ *
6
+ * @param table - the table's EXPORT name in `schema`, not its SQL name
7
+ * @throws Error when `table` is no table export of `schema`
8
+ */
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)
13
+ throw new Error(`"${table}" is no table export of the schema.`);
14
+ return Object.fromEntries(Object.keys(config.relations).map((name) => [name, {}]));
15
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zeroxsolutions/db",
3
- "version": "9.1.0",
3
+ "version": "9.3.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",
@@ -38,7 +38,7 @@
38
38
  "tslib": "^2.3.0",
39
39
  "uuid": "^14.0.1",
40
40
  "@zeroxsolutions/cosmic": "0.3.4",
41
- "@zeroxsolutions/query": "0.6.0"
41
+ "@zeroxsolutions/query": "0.7.0"
42
42
  },
43
43
  "peerDependencies": {
44
44
  "@testcontainers/postgresql": "^12.1.0",