@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.
- package/CHANGELOG.md +47 -23
- package/README.md +136 -180
- package/dist/base/d1/repository.d.ts +75 -66
- package/dist/base/d1/repository.d.ts.map +1 -1
- package/dist/base/d1/repository.js +134 -122
- package/dist/base/postgres/repository.d.ts +108 -86
- package/dist/base/postgres/repository.d.ts.map +1 -1
- package/dist/base/postgres/repository.js +175 -143
- package/dist/base/read-engine.d.ts +49 -0
- package/dist/base/read-engine.d.ts.map +1 -0
- package/dist/base/read-engine.js +93 -0
- package/dist/utils/filter.d.ts +40 -61
- package/dist/utils/filter.d.ts.map +1 -1
- package/dist/utils/filter.js +51 -85
- package/dist/utils/index.d.ts +3 -5
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +3 -5
- package/dist/utils/read.d.ts +86 -0
- package/dist/utils/read.d.ts.map +1 -0
- package/dist/utils/read.js +124 -0
- package/dist/utils/reads.d.ts +24 -0
- package/dist/utils/reads.d.ts.map +1 -0
- package/dist/utils/reads.js +23 -0
- package/dist/utils/relationships.d.ts +9 -6
- package/dist/utils/relationships.d.ts.map +1 -1
- package/dist/utils/relationships.js +26 -8
- package/package.json +2 -2
- package/dist/utils/assert-queryable-fields.d.ts +0 -27
- package/dist/utils/assert-queryable-fields.d.ts.map +0 -1
- package/dist/utils/assert-queryable-fields.js +0 -47
- package/dist/utils/constants.d.ts +0 -7
- package/dist/utils/constants.d.ts.map +0 -1
- package/dist/utils/constants.js +0 -6
- package/dist/utils/include.d.ts +0 -80
- package/dist/utils/include.d.ts.map +0 -1
- package/dist/utils/include.js +0 -132
- package/dist/utils/table-query.d.ts +0 -41
- package/dist/utils/table-query.d.ts.map +0 -1
- package/dist/utils/table-query.js +0 -1
- package/dist/utils/total-order.d.ts +0 -15
- package/dist/utils/total-order.d.ts.map +0 -1
- package/dist/utils/total-order.js +0 -35
package/dist/utils/filter.d.ts
CHANGED
|
@@ -1,18 +1,15 @@
|
|
|
1
1
|
import type { Column, SQL } from 'drizzle-orm';
|
|
2
|
-
import type {
|
|
3
|
-
import type { TableConfig as PgTableConfig } from 'drizzle-orm/pg-core';
|
|
4
|
-
import { PgTable } from 'drizzle-orm/pg-core';
|
|
5
|
-
import type { SQLiteTable, TableConfig as SQLiteTableConfig } from 'drizzle-orm/sqlite-core';
|
|
2
|
+
import type { FilterTree, FilterTreeGroup } from '@zeroxsolutions/query';
|
|
6
3
|
import { z } from 'zod';
|
|
7
4
|
/** The 15 comparison operators a filter clause may use; `filterOperatorSchema.enum.<op>` names one. */
|
|
8
5
|
export declare const filterOperatorSchema: z.ZodEnum<{
|
|
9
|
-
endsWith: "endsWith";
|
|
10
|
-
startsWith: "startsWith";
|
|
11
6
|
equals: "equals";
|
|
12
7
|
notEquals: "notEquals";
|
|
13
8
|
in: "in";
|
|
14
9
|
notIn: "notIn";
|
|
15
10
|
contains: "contains";
|
|
11
|
+
startsWith: "startsWith";
|
|
12
|
+
endsWith: "endsWith";
|
|
16
13
|
gt: "gt";
|
|
17
14
|
gte: "gte";
|
|
18
15
|
lt: "lt";
|
|
@@ -26,11 +23,11 @@ export declare const filterOperatorSchema: z.ZodEnum<{
|
|
|
26
23
|
export declare const filterValueSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
27
24
|
field: z.ZodString;
|
|
28
25
|
operator: z.ZodEnum<{
|
|
29
|
-
endsWith: "endsWith";
|
|
30
|
-
startsWith: "startsWith";
|
|
31
26
|
equals: "equals";
|
|
32
27
|
notEquals: "notEquals";
|
|
33
28
|
contains: "contains";
|
|
29
|
+
startsWith: "startsWith";
|
|
30
|
+
endsWith: "endsWith";
|
|
34
31
|
gt: "gt";
|
|
35
32
|
gte: "gte";
|
|
36
33
|
lt: "lt";
|
|
@@ -79,70 +76,52 @@ export type FilterOperator = z.infer<typeof filterOperatorSchema>;
|
|
|
79
76
|
/** A parsed filter clause, inferred from {@link filterValueSchema}. */
|
|
80
77
|
export type FilterValue = z.infer<typeof filterValueSchema>;
|
|
81
78
|
/**
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*/
|
|
85
|
-
export interface QueryableFields<TSelect> {
|
|
86
|
-
filterable: readonly (keyof TSelect & string)[];
|
|
87
|
-
sortable: readonly (keyof TSelect & string)[];
|
|
88
|
-
}
|
|
89
|
-
/** Any table this toolkit builds conditions against. */
|
|
90
|
-
export type FilterableTable = PgTable<PgTableConfig> | SQLiteTable<SQLiteTableConfig>;
|
|
91
|
-
/**
|
|
92
|
-
* A `filter` or `sort` named a field the table does not hold. Raised rather than skipped: skipping a
|
|
93
|
-
* condition widens the result and skipping a sort reorders it, both answering a question nobody asked.
|
|
79
|
+
* A `filter` or an `orderBy` named a field the table does not hold. Raised rather than skipped: skipping a condition
|
|
80
|
+
* widens the result and skipping an order reorders it, both answering a question nobody asked.
|
|
94
81
|
*/
|
|
95
82
|
export declare class UnknownQueryField extends Error {
|
|
96
83
|
constructor(field: string);
|
|
97
84
|
}
|
|
98
|
-
/**
|
|
99
|
-
export
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
* other name: a path permitted without an expression, or an expression for a path nobody permits, fails to compile.
|
|
103
|
-
*/
|
|
104
|
-
export type FilterResolvers<TTable extends FilterableTable, P> = {
|
|
105
|
-
readonly [F in Exclude<keyof FilterableOf<P> & string, keyof TTable['_']['columns'] & string>]: FilterResolver;
|
|
106
|
-
};
|
|
85
|
+
/** What a `virtual` name is decided against: the instant the finder was handed. */
|
|
86
|
+
export interface VirtualContext {
|
|
87
|
+
at: Date;
|
|
88
|
+
}
|
|
107
89
|
/**
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
* @throws Error on an operator other than `equals` and `in`, the only two a relationship filter grants
|
|
90
|
+
* A filter or an order name no column holds, as `READS` declares it: the expression a condition on it becomes, and the
|
|
91
|
+
* expression it orders by. A `filter` answering `undefined` contributes no condition, like an empty `in`.
|
|
112
92
|
*/
|
|
113
|
-
export
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
93
|
+
export interface VirtualField {
|
|
94
|
+
filter?: (condition: FilterValue, context: VirtualContext) => SQL | undefined;
|
|
95
|
+
order?: (context: VirtualContext) => SQL | Column;
|
|
96
|
+
}
|
|
97
|
+
/** What resolving a field needs: the table's columns, its relations to one row by their key, and its `virtual` names. */
|
|
98
|
+
export interface FieldContext {
|
|
99
|
+
columns: Readonly<Record<string, Column>>;
|
|
100
|
+
/** Each relation to one row, keyed by its name, mapped to the one column holding the related row's key. */
|
|
101
|
+
relationKeys: Readonly<Record<string, Column>>;
|
|
102
|
+
virtual: Readonly<Record<string, VirtualField>>;
|
|
103
|
+
at: Date;
|
|
104
|
+
/** Postgres matches text case-insensitively with `ILIKE`; SQLite has no `ILIKE`, and its `LIKE` already is. */
|
|
105
|
+
caseInsensitiveOperator: boolean;
|
|
119
106
|
}
|
|
120
107
|
/**
|
|
121
|
-
*
|
|
122
|
-
*
|
|
108
|
+
* One SQL condition from a filter tree. Nodes at the root join conjunctively; a group joins its children by its own
|
|
109
|
+
* conjunction. A field resolves to a `virtual` name first, then to a relation to one row, then to a column.
|
|
123
110
|
*
|
|
124
|
-
* @returns `undefined` when the tree contributes no condition - an absent or empty `filter`, an
|
|
125
|
-
*
|
|
126
|
-
* @throws {@link UnknownQueryField} when a clause names a
|
|
127
|
-
* @example
|
|
128
|
-
* ```ts
|
|
129
|
-
* buildFilterConditions({ table: assessments, ...parsedQuery });
|
|
130
|
-
* ```
|
|
111
|
+
* @returns `undefined` when the tree contributes no condition - an absent or empty `filter`, an empty group, or an
|
|
112
|
+
* `in`/`notIn` over an empty array
|
|
113
|
+
* @throws {@link UnknownQueryField} when a clause names a field nothing resolves
|
|
131
114
|
*/
|
|
132
|
-
export declare function
|
|
115
|
+
export declare function whereOf(context: FieldContext, filter: readonly FilterNode[] | undefined): SQL | undefined;
|
|
133
116
|
/**
|
|
134
|
-
*
|
|
117
|
+
* The `order by` clauses for an order, followed by each primary key column it does not name, ascending: rows that tie
|
|
118
|
+
* on the requested order have none of their own, and two pages cut from two orders of the same rows repeat one and
|
|
119
|
+
* skip another. A field resolves to a `virtual` name first, then to a column.
|
|
135
120
|
*
|
|
136
|
-
* @
|
|
137
|
-
* @returns an empty array for an absent or empty `sort`, which orders nothing
|
|
138
|
-
* @throws {@link UnknownQueryField} when a clause names a column the table does not hold and no
|
|
139
|
-
* resolver answers it
|
|
140
|
-
* @example
|
|
141
|
-
* ```ts
|
|
142
|
-
* const orderClauses = buildOrderBy(users, [{ field: 'createdAt', direction: 'desc' }]);
|
|
143
|
-
* ```
|
|
121
|
+
* @throws {@link UnknownQueryField} when an entry names a field nothing resolves
|
|
144
122
|
*/
|
|
145
|
-
export declare function
|
|
146
|
-
|
|
147
|
-
|
|
123
|
+
export declare function orderOf(context: FieldContext, orderBy: readonly {
|
|
124
|
+
field: string;
|
|
125
|
+
direction: 'asc' | 'desc';
|
|
126
|
+
}[] | undefined, primaryKey: readonly string[]): SQL[];
|
|
148
127
|
//# sourceMappingURL=filter.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"filter.d.ts","sourceRoot":"","sources":["../../src/utils/filter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"filter.d.ts","sourceRoot":"","sources":["../../src/utils/filter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,aAAa,CAAC;AAmB/C,OAAO,KAAK,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AACzE,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,uGAAuG;AACvG,eAAO,MAAM,oBAAoB;;;;;;;;;;;;;;;;EAgB/B,CAAC;AAIH,iHAAiH;AACjH,eAAO,MAAM,iBAAiB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;+BAiC5B,CAAC;AAEH,gGAAgG;AAChG,MAAM,MAAM,WAAW,GAAG,eAAe,CAAC,WAAW,CAAC,CAAC;AAEvD,wEAAwE;AACxE,MAAM,MAAM,UAAU,GAAG,UAAU,CAAC,WAAW,CAAC,CAAC;AAEjD,eAAO,MAAM,iBAAiB,EAAE,CAAC,CAAC,OAAO,CAAC,WAAW,CAKnD,CAAC;AAEH,kFAAkF;AAClF,eAAO,MAAM,gBAAgB,EAAE,CAAC,CAAC,OAAO,CAAC,UAAU,CAAmD,CAAC;AAEvG,mEAAmE;AACnE,wBAAgB,aAAa,CAAC,IAAI,EAAE,UAAU,GAAG,IAAI,IAAI,WAAW,CAEnE;AAED,qEAAqE;AACrE,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC;AAClE,uEAAuE;AACvE,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAW5D;;;GAGG;AACH,qBAAa,iBAAkB,SAAQ,KAAK;gBAC9B,KAAK,EAAE,MAAM;CAG1B;AAED,mFAAmF;AACnF,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,IAAI,CAAC;CACV;AAED;;;GAGG;AACH,MAAM,WAAW,YAAY;IAC3B,MAAM,CAAC,EAAE,CAAC,SAAS,EAAE,WAAW,EAAE,OAAO,EAAE,cAAc,KAAK,GAAG,GAAG,SAAS,CAAC;IAC9E,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,cAAc,KAAK,GAAG,GAAG,MAAM,CAAC;CACnD;AAED,yHAAyH;AACzH,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IAC1C,2GAA2G;IAC3G,YAAY,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IAC/C,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC,CAAC;IAChD,EAAE,EAAE,IAAI,CAAC;IACT,+GAA+G;IAC/G,uBAAuB,EAAE,OAAO,CAAC;CAClC;AA+FD;;;;;;;GAOG;AACH,wBAAgB,OAAO,CAAC,OAAO,EAAE,YAAY,EAAE,MAAM,EAAE,SAAS,UAAU,EAAE,GAAG,SAAS,GAAG,GAAG,GAAG,SAAS,CAIzG;AAED;;;;;;GAMG;AACH,wBAAgB,OAAO,CACrB,OAAO,EAAE,YAAY,EACrB,OAAO,EAAE,SAAS;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,KAAK,GAAG,MAAM,CAAA;CAAE,EAAE,GAAG,SAAS,EAC5E,UAAU,EAAE,SAAS,MAAM,EAAE,GAC5B,GAAG,EAAE,CAeP"}
|
package/dist/utils/filter.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import { and, asc, desc, eq,
|
|
2
|
-
import { PgTable } from 'drizzle-orm/pg-core';
|
|
1
|
+
import { and, asc, desc, eq, gt, gte, inArray, isNotNull, isNull, lt, lte, ne, notBetween, notInArray, or, sql, } from 'drizzle-orm';
|
|
3
2
|
import { z } from 'zod';
|
|
4
3
|
/** The 15 comparison operators a filter clause may use; `filterOperatorSchema.enum.<op>` names one. */
|
|
5
4
|
export const filterOperatorSchema = z.enum([
|
|
@@ -62,10 +61,7 @@ export const filterGroupSchema = z.object({
|
|
|
62
61
|
},
|
|
63
62
|
});
|
|
64
63
|
/** A filter tree node: {@link filterValueSchema} or {@link filterGroupSchema}. */
|
|
65
|
-
export const filterNodeSchema = z.union([
|
|
66
|
-
filterValueSchema,
|
|
67
|
-
filterGroupSchema,
|
|
68
|
-
]);
|
|
64
|
+
export const filterNodeSchema = z.union([filterValueSchema, filterGroupSchema]);
|
|
69
65
|
/** Whether a node joins children rather than comparing a field. */
|
|
70
66
|
export function isFilterGroup(node) {
|
|
71
67
|
return 'conjunction' in node;
|
|
@@ -80,29 +76,14 @@ function coerceForColumn(column, value) {
|
|
|
80
76
|
return value;
|
|
81
77
|
}
|
|
82
78
|
/**
|
|
83
|
-
* A `filter` or `
|
|
84
|
-
*
|
|
79
|
+
* A `filter` or an `orderBy` named a field the table does not hold. Raised rather than skipped: skipping a condition
|
|
80
|
+
* widens the result and skipping an order reorders it, both answering a question nobody asked.
|
|
85
81
|
*/
|
|
86
82
|
export class UnknownQueryField extends Error {
|
|
87
83
|
constructor(field) {
|
|
88
84
|
super(`No column resolves the field "${field}".`);
|
|
89
85
|
}
|
|
90
86
|
}
|
|
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
|
-
}
|
|
106
87
|
/**
|
|
107
88
|
* The character that makes the next one literal. It is its own escape, so the character class below
|
|
108
89
|
* must list it first: escaping `%` before `\` would re-escape the backslash the first pass added.
|
|
@@ -111,24 +92,33 @@ const LIKE_ESCAPE = '\\';
|
|
|
111
92
|
function escapeLikePattern(value) {
|
|
112
93
|
return value.replace(/[\\%_]/g, (character) => `${LIKE_ESCAPE}${character}`);
|
|
113
94
|
}
|
|
114
|
-
/**
|
|
115
|
-
* Case-insensitive text match on either dialect. Postgres `LIKE` is case-sensitive and needs `ILIKE`;
|
|
116
|
-
* SQLite has no `ILIKE` at all and its `LIKE` is already case-insensitive for ASCII, so emitting one
|
|
117
|
-
* operator for both made these three operators a syntax error on D1.
|
|
118
|
-
*/
|
|
119
95
|
function textMatch(context, column, pattern) {
|
|
120
96
|
return context.caseInsensitiveOperator
|
|
121
97
|
? sql `${column} ilike ${pattern} escape '\\'`
|
|
122
98
|
: sql `${column} like ${pattern} escape '\\'`;
|
|
123
99
|
}
|
|
100
|
+
/** A relationship filter compares the related row's id, which the relation's key column holds. */
|
|
101
|
+
function relationCondition(column, filter) {
|
|
102
|
+
if (filter.operator === 'equals')
|
|
103
|
+
return eq(column, filter.value);
|
|
104
|
+
if (filter.operator === 'in')
|
|
105
|
+
return filter.value.length > 0 ? inArray(column, filter.value) : undefined;
|
|
106
|
+
throw new Error(`A relationship filter answers equals and in, not "${filter.operator}".`);
|
|
107
|
+
}
|
|
124
108
|
function buildCondition(context, filter) {
|
|
125
|
-
//
|
|
126
|
-
|
|
127
|
-
if (
|
|
128
|
-
|
|
109
|
+
// Own keys only: the field is client-written, and `constructor` would otherwise answer from Object.prototype.
|
|
110
|
+
const virtual = Object.hasOwn(context.virtual, filter.field) ? context.virtual[filter.field] : undefined;
|
|
111
|
+
if (virtual?.filter) {
|
|
112
|
+
// Parenthesized because the expression may be a bare `a or b`, and `and()` joins its operands without grouping
|
|
113
|
+
// them: beside a sibling or the soft-delete condition it would read `a or (b and deleted_at is null)`.
|
|
114
|
+
const expression = virtual.filter(filter, { at: context.at });
|
|
129
115
|
return expression === undefined ? undefined : sql `(${expression})`;
|
|
130
116
|
}
|
|
131
|
-
|
|
117
|
+
const relationKey = Object.hasOwn(context.relationKeys, filter.field)
|
|
118
|
+
? context.relationKeys[filter.field]
|
|
119
|
+
: undefined;
|
|
120
|
+
if (relationKey)
|
|
121
|
+
return relationCondition(relationKey, filter);
|
|
132
122
|
const column = Object.hasOwn(context.columns, filter.field) ? context.columns[filter.field] : undefined;
|
|
133
123
|
if (!column)
|
|
134
124
|
throw new UnknownQueryField(filter.field);
|
|
@@ -156,12 +146,8 @@ function buildCondition(context, filter) {
|
|
|
156
146
|
case 'lte':
|
|
157
147
|
return lte(column, coerceForColumn(column, filter.value));
|
|
158
148
|
case 'between': {
|
|
159
|
-
const from = filter.value.from !== undefined
|
|
160
|
-
|
|
161
|
-
: undefined;
|
|
162
|
-
const to = filter.value.to !== undefined
|
|
163
|
-
? lte(column, coerceForColumn(column, filter.value.to))
|
|
164
|
-
: undefined;
|
|
149
|
+
const from = filter.value.from !== undefined ? gte(column, coerceForColumn(column, filter.value.from)) : undefined;
|
|
150
|
+
const to = filter.value.to !== undefined ? lte(column, coerceForColumn(column, filter.value.to)) : undefined;
|
|
165
151
|
return from && to ? and(from, to) : (from ?? to);
|
|
166
152
|
}
|
|
167
153
|
case 'notBetween':
|
|
@@ -175,9 +161,7 @@ function buildCondition(context, filter) {
|
|
|
175
161
|
function buildNodes(context, nodes) {
|
|
176
162
|
const conditions = [];
|
|
177
163
|
for (const node of nodes) {
|
|
178
|
-
const condition = isFilterGroup(node)
|
|
179
|
-
? buildGroup(context, node)
|
|
180
|
-
: buildCondition(context, node);
|
|
164
|
+
const condition = isFilterGroup(node) ? buildGroup(context, node) : buildCondition(context, node);
|
|
181
165
|
if (condition)
|
|
182
166
|
conditions.push(condition);
|
|
183
167
|
}
|
|
@@ -190,57 +174,39 @@ function buildGroup(context, group) {
|
|
|
190
174
|
return group.conjunction === 'or' ? or(...children) : and(...children);
|
|
191
175
|
}
|
|
192
176
|
/**
|
|
193
|
-
*
|
|
194
|
-
*
|
|
177
|
+
* One SQL condition from a filter tree. Nodes at the root join conjunctively; a group joins its children by its own
|
|
178
|
+
* conjunction. A field resolves to a `virtual` name first, then to a relation to one row, then to a column.
|
|
195
179
|
*
|
|
196
|
-
* @returns `undefined` when the tree contributes no condition - an absent or empty `filter`, an
|
|
197
|
-
*
|
|
198
|
-
* @throws {@link UnknownQueryField} when a clause names a
|
|
199
|
-
* @example
|
|
200
|
-
* ```ts
|
|
201
|
-
* buildFilterConditions({ table: assessments, ...parsedQuery });
|
|
202
|
-
* ```
|
|
180
|
+
* @returns `undefined` when the tree contributes no condition - an absent or empty `filter`, an empty group, or an
|
|
181
|
+
* `in`/`notIn` over an empty array
|
|
182
|
+
* @throws {@link UnknownQueryField} when a clause names a field nothing resolves
|
|
203
183
|
*/
|
|
204
|
-
export function
|
|
184
|
+
export function whereOf(context, filter) {
|
|
205
185
|
if (!filter || filter.length === 0)
|
|
206
186
|
return undefined;
|
|
207
|
-
const conditions = buildNodes(
|
|
208
|
-
columns: getTableColumns(table),
|
|
209
|
-
caseInsensitiveOperator: is(table, PgTable),
|
|
210
|
-
resolvers: resolvers ?? {},
|
|
211
|
-
}, filter);
|
|
187
|
+
const conditions = buildNodes(context, filter);
|
|
212
188
|
return conditions.length > 0 ? and(...conditions) : undefined;
|
|
213
189
|
}
|
|
214
190
|
/**
|
|
215
|
-
*
|
|
191
|
+
* The `order by` clauses for an order, followed by each primary key column it does not name, ascending: rows that tie
|
|
192
|
+
* on the requested order have none of their own, and two pages cut from two orders of the same rows repeat one and
|
|
193
|
+
* skip another. A field resolves to a `virtual` name first, then to a column.
|
|
216
194
|
*
|
|
217
|
-
* @
|
|
218
|
-
* @returns an empty array for an absent or empty `sort`, which orders nothing
|
|
219
|
-
* @throws {@link UnknownQueryField} when a clause names a column the table does not hold and no
|
|
220
|
-
* resolver answers it
|
|
221
|
-
* @example
|
|
222
|
-
* ```ts
|
|
223
|
-
* const orderClauses = buildOrderBy(users, [{ field: 'createdAt', direction: 'desc' }]);
|
|
224
|
-
* ```
|
|
195
|
+
* @throws {@link UnknownQueryField} when an entry names a field nothing resolves
|
|
225
196
|
*/
|
|
226
|
-
export function
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
const
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
: undefined;
|
|
197
|
+
export function orderOf(context, orderBy, primaryKey) {
|
|
198
|
+
const requested = orderBy ?? [];
|
|
199
|
+
const named = new Set(requested.map((entry) => entry.field));
|
|
200
|
+
const entries = [
|
|
201
|
+
...requested,
|
|
202
|
+
...primaryKey.filter((key) => !named.has(key)).map((key) => ({ field: key, direction: 'asc' })),
|
|
203
|
+
];
|
|
204
|
+
return entries.map((entry) => {
|
|
205
|
+
const virtual = Object.hasOwn(context.virtual, entry.field) ? context.virtual[entry.field] : undefined;
|
|
206
|
+
const expression = virtual?.order?.({ at: context.at }) ??
|
|
207
|
+
(Object.hasOwn(context.columns, entry.field) ? context.columns[entry.field] : undefined);
|
|
238
208
|
if (!expression)
|
|
239
|
-
throw new UnknownQueryField(
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
else
|
|
243
|
-
clauses.push(asc(expression));
|
|
244
|
-
}
|
|
245
|
-
return clauses;
|
|
209
|
+
throw new UnknownQueryField(entry.field);
|
|
210
|
+
return entry.direction === 'desc' ? desc(expression) : asc(expression);
|
|
211
|
+
});
|
|
246
212
|
}
|
package/dist/utils/index.d.ts
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
|
-
export
|
|
2
|
-
export * from './constants.js';
|
|
3
|
-
export * from './filter.js';
|
|
4
|
-
export * from './include.js';
|
|
1
|
+
export { filterGroupSchema, filterNodeSchema, filterOperatorSchema, filterValueSchema, isFilterGroup, UnknownQueryField, type FilterGroup, type FilterNode, type FilterOperator, type FilterValue, type VirtualContext, type VirtualField, } from './filter.js';
|
|
5
2
|
export * from './omit.js';
|
|
3
|
+
export { UnknownReadName, type ColumnName, type MatchOptions, type OrderEntry, type Read, type RelationRead, } from './read.js';
|
|
4
|
+
export * from './reads.js';
|
|
6
5
|
export * from './relationships.js';
|
|
7
|
-
export * from './table-query.js';
|
|
8
6
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAAA,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,iBAAiB,EACjB,gBAAgB,EAChB,oBAAoB,EACpB,iBAAiB,EACjB,aAAa,EACb,iBAAiB,EACjB,KAAK,WAAW,EAChB,KAAK,UAAU,EACf,KAAK,cAAc,EACnB,KAAK,WAAW,EAChB,KAAK,cAAc,EACnB,KAAK,YAAY,GAClB,MAAM,aAAa,CAAC;AACrB,cAAc,WAAW,CAAC;AAC1B,OAAO,EACL,eAAe,EACf,KAAK,UAAU,EACf,KAAK,YAAY,EACjB,KAAK,UAAU,EACf,KAAK,IAAI,EACT,KAAK,YAAY,GAClB,MAAM,WAAW,CAAC;AACnB,cAAc,YAAY,CAAC;AAC3B,cAAc,oBAAoB,CAAC"}
|
package/dist/utils/index.js
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
export
|
|
2
|
-
export * from './constants.js';
|
|
3
|
-
export * from './filter.js';
|
|
4
|
-
export * from './include.js';
|
|
1
|
+
export { filterGroupSchema, filterNodeSchema, filterOperatorSchema, filterValueSchema, isFilterGroup, UnknownQueryField, } from './filter.js';
|
|
5
2
|
export * from './omit.js';
|
|
3
|
+
export { UnknownReadName, } from './read.js';
|
|
4
|
+
export * from './reads.js';
|
|
6
5
|
export * from './relationships.js';
|
|
7
|
-
export * from './table-query.js';
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { type Column, type ExtractTablesWithRelations, type Relation, type SQL, type TablesRelationalConfig } from 'drizzle-orm';
|
|
2
|
+
import type { FilterNode } from './filter.js';
|
|
3
|
+
/** The tables-with-relations view a drizzle schema expands into. */
|
|
4
|
+
type RelationalSchema<TSchema extends Record<string, unknown>> = ExtractTablesWithRelations<TSchema>;
|
|
5
|
+
/**
|
|
6
|
+
* A column name of `TTableName`, offered by the editor and checked by the finder: any other string still compiles,
|
|
7
|
+
* because a read built from a client's query names its columns at run time, and the finder refuses one the table does
|
|
8
|
+
* not hold.
|
|
9
|
+
*/
|
|
10
|
+
export type ColumnName<TSchema extends Record<string, unknown>, TTableName extends keyof RelationalSchema<TSchema>> = (keyof RelationalSchema<TSchema>[TTableName]['columns'] & string) | (string & {});
|
|
11
|
+
/** One order: a field, which a column or a `virtual` name of `READS` answers, and its direction. */
|
|
12
|
+
export interface OrderEntry<TField extends string = string> {
|
|
13
|
+
field: TField;
|
|
14
|
+
direction: 'asc' | 'desc';
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* What one level of a read loads.
|
|
18
|
+
*
|
|
19
|
+
* - `columns` absent loads every column. On the root table, given columns load with its primary key beside them; on a
|
|
20
|
+
* relation, exactly the given columns load.
|
|
21
|
+
* - `relations` names each relation to load, under its own read. A relation not named does not load.
|
|
22
|
+
*/
|
|
23
|
+
export interface Read<TColumn extends string = string> {
|
|
24
|
+
columns?: readonly TColumn[];
|
|
25
|
+
relations?: Readonly<Record<string, RelationRead>>;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* A relation's read: its columns and relations, the order its rows load in where it holds many, and whether its
|
|
29
|
+
* soft-deleted rows load too. The order defaults to the one `READS` declares for its table, and its marked rows to
|
|
30
|
+
* left out.
|
|
31
|
+
*/
|
|
32
|
+
export interface RelationRead extends Read {
|
|
33
|
+
orderBy?: readonly OrderEntry[];
|
|
34
|
+
softDeleted?: boolean;
|
|
35
|
+
}
|
|
36
|
+
/** The finder options that change which rows match, which is all a count reads. */
|
|
37
|
+
export interface MatchOptions {
|
|
38
|
+
/** The condition tree a row must match. Root nodes join by `and`. */
|
|
39
|
+
filter?: readonly FilterNode[];
|
|
40
|
+
/** Whether marked rows of the root table match. */
|
|
41
|
+
softDeleted?: boolean;
|
|
42
|
+
/** The instant a `virtual` name of `READS` is decided at; the moment of the call where absent. */
|
|
43
|
+
at?: Date;
|
|
44
|
+
}
|
|
45
|
+
/** A read naming a column or a relation its table does not hold. */
|
|
46
|
+
export declare class UnknownReadName extends Error {
|
|
47
|
+
readonly path: string;
|
|
48
|
+
constructor(path: string);
|
|
49
|
+
}
|
|
50
|
+
/** One table as drizzle's relational config holds it. */
|
|
51
|
+
type TableConfig = TablesRelationalConfig[string];
|
|
52
|
+
/** The relational config `drizzle({ schema })` computed, and the map from a table's unique name to its export name. */
|
|
53
|
+
export interface RelationalMetadata {
|
|
54
|
+
tables: TablesRelationalConfig;
|
|
55
|
+
tableNamesMap: Record<string, string>;
|
|
56
|
+
}
|
|
57
|
+
/** The order a table's rows load in as a relation, keyed by the table's export name. */
|
|
58
|
+
export type RelationOrders = Readonly<Record<string, readonly OrderEntry[] | undefined>>;
|
|
59
|
+
/** One level of drizzle's relational config, as a finder hands it to `findFirst` / `findMany`. */
|
|
60
|
+
export interface RelationalConfig {
|
|
61
|
+
columns?: Record<string, true>;
|
|
62
|
+
with?: Record<string, RelationalConfig>;
|
|
63
|
+
where?: (fields: Record<string, Column>) => SQL | undefined;
|
|
64
|
+
orderBy?: (fields: Record<string, Column>) => SQL[];
|
|
65
|
+
}
|
|
66
|
+
/** The property keys of a table's primary key columns, in the order its relational config lists them. */
|
|
67
|
+
export declare function primaryKeyOf(table: TableConfig): string[];
|
|
68
|
+
/** The export name of the table a relation reaches, which is how the relational config keys it. */
|
|
69
|
+
export declare function targetOf(metadata: RelationalMetadata, relation: Relation): string;
|
|
70
|
+
/**
|
|
71
|
+
* A read resolved into drizzle's relational config for the table exported as `rootKey`: the root's columns with its
|
|
72
|
+
* primary key, and each relation under its own read, its marked rows left out unless it asks for them and, where it
|
|
73
|
+
* holds many, in its order.
|
|
74
|
+
*
|
|
75
|
+
* @throws {@link UnknownReadName} on a column or a relation the table it names does not hold
|
|
76
|
+
*/
|
|
77
|
+
export declare function relationalConfig(metadata: RelationalMetadata, orders: RelationOrders, rootKey: string, read: Read | undefined): Pick<RelationalConfig, 'columns' | 'with'>;
|
|
78
|
+
/**
|
|
79
|
+
* The columns a read of a table with no relational config selects: those it names with the primary key beside them,
|
|
80
|
+
* or every column.
|
|
81
|
+
*
|
|
82
|
+
* @throws {@link UnknownReadName} on a column the table does not hold, and on any relation, which needs the config
|
|
83
|
+
*/
|
|
84
|
+
export declare function plainColumns<TSelectable extends Record<string, unknown>>(selectable: TSelectable, primaryKey: readonly string[], read: Read | undefined, name: string): TSelectable;
|
|
85
|
+
export {};
|
|
86
|
+
//# sourceMappingURL=read.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"read.d.ts","sourceRoot":"","sources":["../../src/utils/read.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { asc, desc, getTableUniqueName, isNull, One, is, } from 'drizzle-orm';
|
|
2
|
+
/** A read naming a column or a relation its table does not hold. */
|
|
3
|
+
export class UnknownReadName extends Error {
|
|
4
|
+
path;
|
|
5
|
+
constructor(path) {
|
|
6
|
+
super(`No column or relation answers "${path}".`);
|
|
7
|
+
this.path = path;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
/** The property keys of a table's primary key columns, in the order its relational config lists them. */
|
|
11
|
+
export function primaryKeyOf(table) {
|
|
12
|
+
const names = new Set(table.primaryKey.map((column) => column.name));
|
|
13
|
+
return Object.keys(table.columns).filter((key) => names.has(table.columns[key]?.name ?? ''));
|
|
14
|
+
}
|
|
15
|
+
/** The export name of the table a relation reaches, which is how the relational config keys it. */
|
|
16
|
+
export function targetOf(metadata, relation) {
|
|
17
|
+
const unique = getTableUniqueName(relation.referencedTable);
|
|
18
|
+
return metadata.tableNamesMap[unique] ?? relation.referencedTableName;
|
|
19
|
+
}
|
|
20
|
+
const selecting = (keys) => Object.fromEntries(keys.map((key) => [key, true]));
|
|
21
|
+
/** The table at `key`, or the refusal naming the path that reached it. */
|
|
22
|
+
function tableAt(metadata, key, path) {
|
|
23
|
+
const table = Object.hasOwn(metadata.tables, key) ? metadata.tables[key] : undefined;
|
|
24
|
+
if (!table)
|
|
25
|
+
throw new UnknownReadName(path);
|
|
26
|
+
return table;
|
|
27
|
+
}
|
|
28
|
+
/** Refuses a column the table does not hold, reading own keys only: the name may have come from a client. */
|
|
29
|
+
function assertColumns(table, columns, path) {
|
|
30
|
+
for (const name of columns) {
|
|
31
|
+
if (!Object.hasOwn(table.columns, name))
|
|
32
|
+
throw new UnknownReadName(`${path}.${name}`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* The order entries of a relation that holds many, its own or the one `READS` declares for its table, with each primary
|
|
37
|
+
* key column it does not name appended, so two reads of one row load the set in one order.
|
|
38
|
+
*/
|
|
39
|
+
function relationOrder(table, entries, path) {
|
|
40
|
+
const requested = entries ?? [];
|
|
41
|
+
for (const entry of requested) {
|
|
42
|
+
if (!Object.hasOwn(table.columns, entry.field))
|
|
43
|
+
throw new UnknownReadName(`${path}.${entry.field}`);
|
|
44
|
+
}
|
|
45
|
+
const named = new Set(requested.map((entry) => entry.field));
|
|
46
|
+
const appended = primaryKeyOf(table)
|
|
47
|
+
.filter((key) => !named.has(key))
|
|
48
|
+
.map((key) => ({ field: key, direction: 'asc' }));
|
|
49
|
+
const all = [...requested, ...appended];
|
|
50
|
+
return (fields) => all.map((entry) => entry.direction === 'desc' ? desc(fields[entry.field]) : asc(fields[entry.field]));
|
|
51
|
+
}
|
|
52
|
+
/** A relation's level of the relational config, walked from its own read. */
|
|
53
|
+
function relationLevel(metadata, orders, relation, read, path) {
|
|
54
|
+
const key = targetOf(metadata, relation);
|
|
55
|
+
const table = tableAt(metadata, key, path);
|
|
56
|
+
if (read.columns !== undefined)
|
|
57
|
+
assertColumns(table, read.columns, path);
|
|
58
|
+
if (read.columns !== undefined && read.columns.length === 0 && !read.relations)
|
|
59
|
+
return undefined;
|
|
60
|
+
const level = {};
|
|
61
|
+
if (read.columns !== undefined && read.columns.length > 0)
|
|
62
|
+
level.columns = selecting(read.columns);
|
|
63
|
+
if (!read.softDeleted && Object.hasOwn(table.columns, 'deletedAt')) {
|
|
64
|
+
level.where = (fields) => isNull(fields['deletedAt']);
|
|
65
|
+
}
|
|
66
|
+
if (!is(relation, One))
|
|
67
|
+
level.orderBy = relationOrder(table, read.orderBy ?? orders[key], path);
|
|
68
|
+
const nested = relationsOf(metadata, orders, table, read.relations, path);
|
|
69
|
+
if (nested)
|
|
70
|
+
level.with = nested;
|
|
71
|
+
return level;
|
|
72
|
+
}
|
|
73
|
+
function relationsOf(metadata, orders, table, relations, path) {
|
|
74
|
+
if (!relations)
|
|
75
|
+
return undefined;
|
|
76
|
+
const levels = {};
|
|
77
|
+
for (const [name, read] of Object.entries(relations)) {
|
|
78
|
+
const relation = Object.hasOwn(table.relations, name) ? table.relations[name] : undefined;
|
|
79
|
+
if (!relation)
|
|
80
|
+
throw new UnknownReadName(`${path}.${name}`);
|
|
81
|
+
const level = relationLevel(metadata, orders, relation, read, `${path}.${name}`);
|
|
82
|
+
if (level)
|
|
83
|
+
levels[name] = level;
|
|
84
|
+
}
|
|
85
|
+
return Object.keys(levels).length > 0 ? levels : undefined;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* A read resolved into drizzle's relational config for the table exported as `rootKey`: the root's columns with its
|
|
89
|
+
* primary key, and each relation under its own read, its marked rows left out unless it asks for them and, where it
|
|
90
|
+
* holds many, in its order.
|
|
91
|
+
*
|
|
92
|
+
* @throws {@link UnknownReadName} on a column or a relation the table it names does not hold
|
|
93
|
+
*/
|
|
94
|
+
export function relationalConfig(metadata, orders, rootKey, read) {
|
|
95
|
+
const table = tableAt(metadata, rootKey, rootKey);
|
|
96
|
+
const config = {};
|
|
97
|
+
if (read?.columns !== undefined) {
|
|
98
|
+
assertColumns(table, read.columns, rootKey);
|
|
99
|
+
config.columns = selecting([...primaryKeyOf(table), ...read.columns]);
|
|
100
|
+
}
|
|
101
|
+
const nested = relationsOf(metadata, orders, table, read?.relations, rootKey);
|
|
102
|
+
if (nested)
|
|
103
|
+
config.with = nested;
|
|
104
|
+
return config;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* The columns a read of a table with no relational config selects: those it names with the primary key beside them,
|
|
108
|
+
* or every column.
|
|
109
|
+
*
|
|
110
|
+
* @throws {@link UnknownReadName} on a column the table does not hold, and on any relation, which needs the config
|
|
111
|
+
*/
|
|
112
|
+
export function plainColumns(selectable, primaryKey, read, name) {
|
|
113
|
+
if (read?.relations && Object.keys(read.relations).length > 0) {
|
|
114
|
+
throw new UnknownReadName(`${name}.${Object.keys(read.relations)[0]}`);
|
|
115
|
+
}
|
|
116
|
+
if (read?.columns === undefined)
|
|
117
|
+
return selectable;
|
|
118
|
+
for (const column of read.columns) {
|
|
119
|
+
if (!Object.hasOwn(selectable, column))
|
|
120
|
+
throw new UnknownReadName(`${name}.${column}`);
|
|
121
|
+
}
|
|
122
|
+
const kept = new Set([...primaryKey, ...read.columns]);
|
|
123
|
+
return Object.fromEntries(Object.entries(selectable).filter(([key]) => kept.has(key)));
|
|
124
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { type ExtractTablesWithRelations } from 'drizzle-orm';
|
|
2
|
+
import type { VirtualField } from './filter.js';
|
|
3
|
+
import type { ColumnName, OrderEntry } from './read.js';
|
|
4
|
+
/** What holds for one table whichever read reaches it. */
|
|
5
|
+
export interface TableReads<TColumn extends string = string> {
|
|
6
|
+
/** The order its rows load in as a relation that holds many; a relation's own `orderBy` replaces it. */
|
|
7
|
+
orderBy?: readonly OrderEntry<TColumn>[];
|
|
8
|
+
/** The filter and order names no column of it holds, each with the expression it becomes. */
|
|
9
|
+
virtual?: Readonly<Record<string, VirtualField>>;
|
|
10
|
+
}
|
|
11
|
+
/** A context's reads, keyed by each table's EXPORT name in the schema. */
|
|
12
|
+
export type Reads<TSchema extends Record<string, unknown> = Record<string, unknown>> = {
|
|
13
|
+
readonly [K in keyof ExtractTablesWithRelations<TSchema> & string]?: TableReads<ColumnName<TSchema, K>>;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Declares, once per context, what holds for each table whichever read reaches it: the order its rows load in as a
|
|
17
|
+
* relation, and its `virtual` filter and order names. Every repository of the context hands the same value to its base.
|
|
18
|
+
*
|
|
19
|
+
* @param schema - the schema barrel the repositories are typed by; each key is checked against it
|
|
20
|
+
* @throws Error at declaration on a key that is no table export of `schema`, or an order naming a column its table
|
|
21
|
+
* does not hold
|
|
22
|
+
*/
|
|
23
|
+
export declare function defineReads<TSchema extends Record<string, unknown>>(schema: TSchema, reads: Reads<TSchema>): Reads<TSchema>;
|
|
24
|
+
//# sourceMappingURL=reads.d.ts.map
|