@zeroxsolutions/db 9.5.0 → 10.1.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 +57 -22
- package/README.md +149 -180
- package/dist/base/d1/entity.d.ts.map +1 -1
- package/dist/base/d1/outbox.d.ts.map +1 -1
- package/dist/base/d1/outbox.js +1 -3
- package/dist/base/d1/repository.d.ts +76 -66
- package/dist/base/d1/repository.d.ts.map +1 -1
- package/dist/base/d1/repository.js +134 -122
- package/dist/base/d1/unit-of-work.d.ts.map +1 -1
- package/dist/base/postgres/entity.d.ts.map +1 -1
- package/dist/base/postgres/outbox.d.ts.map +1 -1
- package/dist/base/postgres/outbox.js +1 -3
- package/dist/base/postgres/repository.d.ts +109 -86
- package/dist/base/postgres/repository.d.ts.map +1 -1
- package/dist/base/postgres/repository.js +182 -143
- package/dist/base/postgres/unit-of-work.d.ts.map +1 -1
- package/dist/base/read-engine.d.ts +48 -0
- package/dist/base/read-engine.d.ts.map +1 -0
- package/dist/base/read-engine.js +94 -0
- package/dist/utils/filter.d.ts +44 -59
- 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 +88 -0
- package/dist/utils/read.d.ts.map +1 -0
- package/dist/utils/read.js +124 -0
- package/dist/utils/reads.d.ts +30 -0
- package/dist/utils/reads.d.ts.map +1 -0
- package/dist/utils/reads.js +26 -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 +7 -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
|
@@ -1,17 +1,22 @@
|
|
|
1
|
-
import { and, count, eq, getTableColumns, getViewSelectedFields,
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
/** A Postgres repository over `TTable
|
|
1
|
+
import { and, count, eq, getTableColumns, getViewSelectedFields, inArray, sql, } from 'drizzle-orm';
|
|
2
|
+
import { orderOf, whereOf } from '../../utils/filter.js';
|
|
3
|
+
import { plainColumns } from '../../utils/read.js';
|
|
4
|
+
import { conditionOf, liveCondition, loadsRelations, orderClausesOf, primaryKeyFields, queryDelegateOf, relationalReadOf, } from '../read-engine.js';
|
|
5
|
+
/** A Postgres repository over `TTable`: reads that name what they load, soft delete, and writes keyed by id. */
|
|
6
6
|
export class BaseRepository {
|
|
7
7
|
db;
|
|
8
8
|
table;
|
|
9
9
|
queryKey;
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
reads;
|
|
11
|
+
/**
|
|
12
|
+
* @param queryKey - the table's EXPORT name in the schema; omitted, a read that loads a relation throws
|
|
13
|
+
* @param reads - the context's `READS`: each table's order as a relation, and its `virtual` names
|
|
14
|
+
*/
|
|
15
|
+
constructor(db, table, queryKey, reads = {}) {
|
|
12
16
|
this.db = db;
|
|
13
17
|
this.table = table;
|
|
14
18
|
this.queryKey = queryKey;
|
|
19
|
+
this.reads = reads;
|
|
15
20
|
}
|
|
16
21
|
getExecutor(tx) {
|
|
17
22
|
const executor = tx ?? this.db;
|
|
@@ -19,82 +24,111 @@ export class BaseRepository {
|
|
|
19
24
|
throw new Error('Database executor is not available');
|
|
20
25
|
return executor;
|
|
21
26
|
}
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
return
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
27
|
+
get source() {
|
|
28
|
+
return {
|
|
29
|
+
table: this.table,
|
|
30
|
+
queryKey: this.queryKey,
|
|
31
|
+
reads: this.reads,
|
|
32
|
+
caseInsensitiveOperator: true,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
idColumn(action) {
|
|
36
|
+
const column = getTableColumns(this.table)['id'];
|
|
37
|
+
if (!column)
|
|
38
|
+
throw new Error(`${action} by id is not supported for this table`);
|
|
39
|
+
return column;
|
|
40
|
+
}
|
|
41
|
+
async findOneById(id, options) {
|
|
42
|
+
return this.findOneWhere(eq(this.idColumn('Find one'), id), options);
|
|
43
|
+
}
|
|
44
|
+
async findAllById(ids, options) {
|
|
45
|
+
const column = this.idColumn('Find all');
|
|
46
|
+
if (ids.length === 0)
|
|
47
|
+
return [];
|
|
48
|
+
return this.findAllWhere(inArray(column, [...ids]), options);
|
|
49
|
+
}
|
|
50
|
+
async existsById(id, options) {
|
|
51
|
+
return this.existsWhere(eq(this.idColumn('Exists'), id), options);
|
|
52
|
+
}
|
|
53
|
+
async findAll(options) {
|
|
54
|
+
return this.findAllWhere(undefined, options);
|
|
55
|
+
}
|
|
56
|
+
async findAllAndCount(options) {
|
|
57
|
+
return this.findAllAndCountWhere(undefined, options);
|
|
58
|
+
}
|
|
59
|
+
async count(options) {
|
|
60
|
+
return this.countWhere(undefined, options);
|
|
61
|
+
}
|
|
62
|
+
/** The rows matching `where` and the options, in their order and page window. */
|
|
63
|
+
async findAllWhere(where, options) {
|
|
64
|
+
const executor = this.getExecutor();
|
|
65
|
+
const condition = conditionOf(this.source, executor, where, options);
|
|
66
|
+
const order = orderClausesOf(this.source, executor, options);
|
|
67
|
+
if (loadsRelations(options?.read)) {
|
|
68
|
+
const config = relationalReadOf(this.source, executor, options?.read);
|
|
69
|
+
return queryDelegateOf(executor, String(this.queryKey)).findMany({
|
|
70
|
+
...config,
|
|
71
|
+
where: condition,
|
|
72
|
+
orderBy: order,
|
|
73
|
+
limit: options?.limit,
|
|
74
|
+
offset: options?.offset,
|
|
49
75
|
});
|
|
50
76
|
}
|
|
51
|
-
|
|
52
|
-
.select(
|
|
77
|
+
let query = executor
|
|
78
|
+
.select(this.plainSelection(options?.read))
|
|
53
79
|
.from(this.table)
|
|
54
|
-
.
|
|
55
|
-
.
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
80
|
+
.where(condition)
|
|
81
|
+
.orderBy(...order)
|
|
82
|
+
.$dynamic();
|
|
83
|
+
if (options?.limit !== undefined)
|
|
84
|
+
query = query.limit(options.limit);
|
|
85
|
+
if (options?.offset !== undefined)
|
|
86
|
+
query = query.offset(options.offset);
|
|
87
|
+
return (await query);
|
|
59
88
|
}
|
|
60
|
-
|
|
61
|
-
|
|
89
|
+
/** {@link findAllWhere} and {@link countWhere} over the same `where` and filter, so the total matches the page. */
|
|
90
|
+
async findAllAndCountWhere(where, options) {
|
|
91
|
+
return await Promise.all([this.findAllWhere(where, options), this.countWhere(where, options)]);
|
|
62
92
|
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
throw new Error('queryKey must be provided to use the "with" option.');
|
|
73
|
-
const queries = executor.query;
|
|
74
|
-
const safeQueryKey = String(this.queryKey);
|
|
75
|
-
const queryDelegate = queries[safeQueryKey];
|
|
76
|
-
if (!queryDelegate)
|
|
77
|
-
throw new Error(`Invalid queryKey: ${safeQueryKey}`);
|
|
78
|
-
const result = await queryDelegate.findFirst({
|
|
79
|
-
where: _where,
|
|
80
|
-
columns: read.columns,
|
|
81
|
-
with: read.with,
|
|
93
|
+
/** The first row matching `where`, loaded as the options' `read` names. */
|
|
94
|
+
async findOneWhere(where, options) {
|
|
95
|
+
const executor = this.getExecutor();
|
|
96
|
+
const condition = and(where, liveCondition(this.table, options?.softDeleted));
|
|
97
|
+
if (loadsRelations(options?.read)) {
|
|
98
|
+
const config = relationalReadOf(this.source, executor, options?.read);
|
|
99
|
+
const row = await queryDelegateOf(executor, String(this.queryKey)).findFirst({
|
|
100
|
+
...config,
|
|
101
|
+
where: condition,
|
|
82
102
|
});
|
|
83
|
-
return
|
|
103
|
+
return row ?? null;
|
|
84
104
|
}
|
|
85
105
|
const rows = await executor
|
|
86
|
-
.select(
|
|
106
|
+
.select(this.plainSelection(options?.read))
|
|
87
107
|
.from(this.table)
|
|
88
|
-
.where(
|
|
108
|
+
.where(condition)
|
|
89
109
|
.limit(1);
|
|
90
110
|
return rows[0] ?? null;
|
|
91
111
|
}
|
|
92
|
-
|
|
112
|
+
/** How many rows match `where` and the options' filter. */
|
|
113
|
+
async countWhere(where, options) {
|
|
114
|
+
const result = await this.getExecutor()
|
|
115
|
+
.select({ count: count() })
|
|
116
|
+
.from(this.table)
|
|
117
|
+
.where(conditionOf(this.source, this.getExecutor(), where, options));
|
|
118
|
+
return result[0]?.count ?? 0;
|
|
119
|
+
}
|
|
120
|
+
/** Whether any row matches `where` and the options' filter. */
|
|
121
|
+
async existsWhere(where, options) {
|
|
122
|
+
const result = await this.getExecutor()
|
|
123
|
+
.select({ value: sql `1` })
|
|
124
|
+
.from(this.table)
|
|
125
|
+
.where(conditionOf(this.source, this.getExecutor(), where, options))
|
|
126
|
+
.limit(1);
|
|
127
|
+
return result.length > 0;
|
|
128
|
+
}
|
|
129
|
+
plainSelection(read) {
|
|
93
130
|
const columns = getTableColumns(this.table);
|
|
94
|
-
|
|
95
|
-
throw new Error('Find one by id is not supported for this table');
|
|
96
|
-
}
|
|
97
|
-
return this.findOne(eq(columns.id, id), options);
|
|
131
|
+
return plainColumns(columns, primaryKeyFields(this.table), read, String(this.queryKey ?? 'table'));
|
|
98
132
|
}
|
|
99
133
|
async create(data, options) {
|
|
100
134
|
const rows = await this.getExecutor(options?.tx).insert(this.table).values(data).returning();
|
|
@@ -104,53 +138,38 @@ export class BaseRepository {
|
|
|
104
138
|
const rows = await this.getExecutor(options?.tx).insert(this.table).values(data).returning();
|
|
105
139
|
return rows;
|
|
106
140
|
}
|
|
107
|
-
async update(where, data, options) {
|
|
108
|
-
const rows = await this.getExecutor(options?.tx).update(this.table).set(data).where(where).returning();
|
|
109
|
-
return rows[0];
|
|
110
|
-
}
|
|
111
141
|
async updateById(id, data, options) {
|
|
112
|
-
|
|
113
|
-
if (!columns.id) {
|
|
114
|
-
throw new Error('Update by id is not supported for this table');
|
|
115
|
-
}
|
|
116
|
-
return await this.update(eq(columns.id, id), data, options);
|
|
117
|
-
}
|
|
118
|
-
async delete(where, options) {
|
|
119
|
-
await this.getExecutor(options?.tx).delete(this.table).where(where);
|
|
142
|
+
return await this.updateWhere(eq(this.idColumn('Update'), id), data, options);
|
|
120
143
|
}
|
|
121
144
|
async deleteById(id, options) {
|
|
122
|
-
|
|
123
|
-
if (!columns.id) {
|
|
124
|
-
throw new Error('Delete by id is not supported for this table');
|
|
125
|
-
}
|
|
126
|
-
await this.delete(eq(columns.id, id), options);
|
|
145
|
+
await this.deleteWhere(eq(this.idColumn('Delete'), id), options);
|
|
127
146
|
}
|
|
128
|
-
async
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
_where = and(where, isNull(columns.deletedAt));
|
|
134
|
-
const result = await executor
|
|
135
|
-
.select({ count: count() })
|
|
136
|
-
.from(this.table)
|
|
137
|
-
.where(_where);
|
|
138
|
-
return result[0]?.count ?? 0;
|
|
147
|
+
async deleteAllById(ids, options) {
|
|
148
|
+
const column = this.idColumn('Delete all');
|
|
149
|
+
if (ids.length === 0)
|
|
150
|
+
return;
|
|
151
|
+
await this.deleteWhere(inArray(column, [...ids]), options);
|
|
139
152
|
}
|
|
140
|
-
async
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
.from(this.table)
|
|
149
|
-
.where(_where)
|
|
150
|
-
.limit(1);
|
|
151
|
-
return result.length > 0;
|
|
153
|
+
async softDeleteById(id, options) {
|
|
154
|
+
await this.softDeleteWhere(eq(this.idColumn('Soft delete'), id), options);
|
|
155
|
+
}
|
|
156
|
+
async softDeleteAllById(ids, options) {
|
|
157
|
+
const column = this.idColumn('Soft delete all');
|
|
158
|
+
if (ids.length === 0)
|
|
159
|
+
return;
|
|
160
|
+
await this.softDeleteWhere(inArray(column, [...ids]), options);
|
|
152
161
|
}
|
|
153
|
-
|
|
162
|
+
/** Updates the rows matching `where`, answering the first. */
|
|
163
|
+
async updateWhere(where, data, options) {
|
|
164
|
+
const rows = await this.getExecutor(options?.tx).update(this.table).set(data).where(where).returning();
|
|
165
|
+
return rows[0] ?? null;
|
|
166
|
+
}
|
|
167
|
+
/** Removes the rows matching `where`. */
|
|
168
|
+
async deleteWhere(where, options) {
|
|
169
|
+
await this.getExecutor(options?.tx).delete(this.table).where(where);
|
|
170
|
+
}
|
|
171
|
+
/** Marks the rows matching `where` deleted. */
|
|
172
|
+
async softDeleteWhere(where, options) {
|
|
154
173
|
const columns = getTableColumns(this.table);
|
|
155
174
|
if (!columns.deletedAt) {
|
|
156
175
|
throw new Error('Soft delete is not supported for this table');
|
|
@@ -160,13 +179,7 @@ export class BaseRepository {
|
|
|
160
179
|
.set({ deletedAt: new Date() })
|
|
161
180
|
.where(where);
|
|
162
181
|
}
|
|
163
|
-
|
|
164
|
-
const columns = getTableColumns(this.table);
|
|
165
|
-
if (!columns.id || !columns.deletedAt) {
|
|
166
|
-
throw new Error('Soft delete by id is not supported for this table');
|
|
167
|
-
}
|
|
168
|
-
await this.softDelete(eq(columns.id, id), options);
|
|
169
|
-
}
|
|
182
|
+
/** Sets `field` to null on every row whose `field` holds `value`. */
|
|
170
183
|
async nullifyBy(field, value, options) {
|
|
171
184
|
const columns = getTableColumns(this.table);
|
|
172
185
|
const column = columns[field];
|
|
@@ -180,15 +193,18 @@ export class BaseRepository {
|
|
|
180
193
|
}
|
|
181
194
|
}
|
|
182
195
|
/**
|
|
183
|
-
* A read-only repository over a Postgres materialized view
|
|
184
|
-
*
|
|
196
|
+
* A read-only repository over a Postgres materialized view. A view has no `deletedAt` and no relational config, and it
|
|
197
|
+
* is read back with no order of its own, so every order ends on the columns of its unique index.
|
|
185
198
|
*/
|
|
186
199
|
export class BaseViewRepository {
|
|
187
200
|
db;
|
|
188
201
|
viewTable;
|
|
189
|
-
|
|
202
|
+
key;
|
|
203
|
+
/** @param key - the columns of the view's unique index, appended to every order as a table's primary key is */
|
|
204
|
+
constructor(db, viewTable, key) {
|
|
190
205
|
this.db = db;
|
|
191
206
|
this.viewTable = viewTable;
|
|
207
|
+
this.key = key;
|
|
192
208
|
}
|
|
193
209
|
getExecutor(tx) {
|
|
194
210
|
const executor = tx ?? this.db;
|
|
@@ -196,40 +212,63 @@ export class BaseViewRepository {
|
|
|
196
212
|
throw new Error('Database executor is not available');
|
|
197
213
|
return executor;
|
|
198
214
|
}
|
|
199
|
-
/** The view's selected fields,
|
|
200
|
-
|
|
201
|
-
const read = resolveRelationalRead(undefined, undefined, this.viewTable, { fields: options?.fields });
|
|
215
|
+
/** The view's selected fields, which a filter and an order resolve against. */
|
|
216
|
+
get selectable() {
|
|
202
217
|
// drizzle types a view's selection as `ColumnsSelection`, values `unknown`; it is the object its own `select()` reads.
|
|
203
|
-
return
|
|
218
|
+
return getViewSelectedFields(this.viewTable);
|
|
204
219
|
}
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
220
|
+
get fields() {
|
|
221
|
+
return {
|
|
222
|
+
columns: this.selectable,
|
|
223
|
+
relationKeys: {},
|
|
224
|
+
virtual: {},
|
|
225
|
+
at: new Date(),
|
|
226
|
+
context: undefined,
|
|
227
|
+
caseInsensitiveOperator: true,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
async findAll(options) {
|
|
231
|
+
return this.findAllWhere(undefined, options);
|
|
232
|
+
}
|
|
233
|
+
async findAllAndCount(options) {
|
|
234
|
+
return this.findAllAndCountWhere(undefined, options);
|
|
235
|
+
}
|
|
236
|
+
async count(options) {
|
|
237
|
+
return this.countWhere(undefined, options);
|
|
238
|
+
}
|
|
239
|
+
/** The view rows matching `where` and the options, in their order and page window. */
|
|
240
|
+
async findAllWhere(where, options) {
|
|
241
|
+
let query = this.getExecutor()
|
|
242
|
+
.select(plainColumns(this.selectable, this.key, options?.read, 'view'))
|
|
210
243
|
.from(this.viewTable)
|
|
211
|
-
.
|
|
212
|
-
.
|
|
213
|
-
|
|
244
|
+
.where(and(where, whereOf(this.fields, options?.filter)))
|
|
245
|
+
.orderBy(...orderOf(this.fields, options?.orderBy, this.key))
|
|
246
|
+
.$dynamic();
|
|
247
|
+
if (options?.limit !== undefined)
|
|
248
|
+
query = query.limit(options.limit);
|
|
249
|
+
if (options?.offset !== undefined)
|
|
250
|
+
query = query.offset(options.offset);
|
|
251
|
+
return (await query);
|
|
214
252
|
}
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
const rows = await
|
|
218
|
-
.select(this.
|
|
253
|
+
/** The first view row matching `where`. */
|
|
254
|
+
async findOneWhere(where, options) {
|
|
255
|
+
const rows = await this.getExecutor()
|
|
256
|
+
.select(plainColumns(this.selectable, this.key, options?.read, 'view'))
|
|
219
257
|
.from(this.viewTable)
|
|
220
258
|
.where(where)
|
|
221
259
|
.limit(1);
|
|
222
260
|
return rows[0] ?? null;
|
|
223
261
|
}
|
|
224
|
-
|
|
225
|
-
|
|
262
|
+
/** {@link findAllWhere} and {@link countWhere} over the same `where` and filter. */
|
|
263
|
+
async findAllAndCountWhere(where, options) {
|
|
264
|
+
return await Promise.all([this.findAllWhere(where, options), this.countWhere(where, options)]);
|
|
226
265
|
}
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
const result = await
|
|
266
|
+
/** How many view rows match `where` and the options' filter. */
|
|
267
|
+
async countWhere(where, options) {
|
|
268
|
+
const result = await this.getExecutor()
|
|
230
269
|
.select({ count: count() })
|
|
231
270
|
.from(this.viewTable)
|
|
232
|
-
.where(where);
|
|
271
|
+
.where(and(where, whereOf(this.fields, options?.filter)));
|
|
233
272
|
return result[0]?.count ?? 0;
|
|
234
273
|
}
|
|
235
274
|
async refresh(options) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"unit-of-work.d.ts","sourceRoot":"","sources":["../../../src/base/postgres/unit-of-work.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AAC1D,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAClE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAEnD;;;;GAIG;AACH,qBAAa,iBAAiB,
|
|
1
|
+
{"version":3,"file":"unit-of-work.d.ts","sourceRoot":"","sources":["../../../src/base/postgres/unit-of-work.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AAC1D,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAClE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAEnD;;;;GAIG;AACH,qBAAa,iBAAiB,CAC5B,OAAO,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACvC,aAAa,CACb,YAAW,WAAW,CAAC,aAAa,CAAC;IAEnC,OAAO,CAAC,QAAQ,CAAC,EAAE;IACnB,OAAO,CAAC,QAAQ,CAAC,gBAAgB;gBADhB,EAAE,EAAE,kBAAkB,CAAC,OAAO,CAAC,EAC/B,gBAAgB,EAAE,CAAC,EAAE,EAAE,WAAW,CAAC,OAAO,CAAC,KAAK,aAAa;IAGhF,GAAG,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,YAAY,EAAE,aAAa,KAAK,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC;CAGtE"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { type SQL, type Table } from 'drizzle-orm';
|
|
2
|
+
import { type FieldContext } from '../utils/filter.js';
|
|
3
|
+
import { type MatchOptions, type Read, type RelationalConfig, type RelationalMetadata } from '../utils/read.js';
|
|
4
|
+
import type { TableReads } from '../utils/reads.js';
|
|
5
|
+
/**
|
|
6
|
+
* What every base repository reads with, whichever engine runs it: the table, its export name in the schema, and the
|
|
7
|
+
* context's `READS`.
|
|
8
|
+
*/
|
|
9
|
+
export interface ReadSource {
|
|
10
|
+
table: Table;
|
|
11
|
+
queryKey: string | undefined;
|
|
12
|
+
reads: Readonly<Record<string, TableReads | undefined>>;
|
|
13
|
+
/** Postgres matches text with `ILIKE`; SQLite has none. */
|
|
14
|
+
caseInsensitiveOperator: boolean;
|
|
15
|
+
}
|
|
16
|
+
/** The relational config a drizzle client was built with, or `undefined` for one built with no schema. */
|
|
17
|
+
export declare function metadataOf(executor: unknown): RelationalMetadata | undefined;
|
|
18
|
+
/**
|
|
19
|
+
* The property keys of the table's primary key columns, in the order the key declares them.
|
|
20
|
+
*
|
|
21
|
+
* A composite key sets no column's `primary` flag, so it is read off the table config instead, and matched back by SQL
|
|
22
|
+
* name: the key holds its own copies of the columns, so its column objects are not the table's (drizzle-orm@0.45.1).
|
|
23
|
+
*/
|
|
24
|
+
export declare function primaryKeyFields(table: Table): string[];
|
|
25
|
+
/** What resolving a filter or an order name of this table needs, decided at `at` against `context`. */
|
|
26
|
+
export declare function fieldContextOf(source: ReadSource, executor: unknown, options: Pick<MatchOptions, 'at' | 'context'> | undefined): FieldContext;
|
|
27
|
+
/** The root table's soft-delete condition, unless the read asks for marked rows or the table marks none. */
|
|
28
|
+
export declare function liveCondition(table: Table, softDeleted: boolean | undefined): SQL | undefined;
|
|
29
|
+
/** A finder's own `where`, the caller's filter and the soft-delete condition, joined. */
|
|
30
|
+
export declare function conditionOf(source: ReadSource, executor: unknown, where: SQL | undefined, options: MatchOptions | undefined): SQL | undefined;
|
|
31
|
+
/** The order a read of many rows runs in, the primary key appended. */
|
|
32
|
+
export declare function orderClausesOf(source: ReadSource, executor: unknown, options: ({
|
|
33
|
+
orderBy?: readonly {
|
|
34
|
+
field: string;
|
|
35
|
+
direction: 'asc' | 'desc';
|
|
36
|
+
}[];
|
|
37
|
+
} & Pick<MatchOptions, 'at' | 'context'>) | undefined): SQL[];
|
|
38
|
+
/** Whether a read loads any relation, which only drizzle's relational query can. */
|
|
39
|
+
export declare function loadsRelations(read: Read | undefined): boolean;
|
|
40
|
+
/**
|
|
41
|
+
* The relational config a read resolves into, for drizzle's `findFirst` / `findMany`.
|
|
42
|
+
*
|
|
43
|
+
* @throws Error when the repository has no `queryKey` or its client no schema, which a relation needs
|
|
44
|
+
*/
|
|
45
|
+
export declare function relationalReadOf(source: ReadSource, executor: unknown, read: Read | undefined): Pick<RelationalConfig, 'columns' | 'with'>;
|
|
46
|
+
/** The relational query delegate drizzle builds for the table exported as `queryKey`. */
|
|
47
|
+
export declare function queryDelegateOf<TDelegate>(executor: unknown, queryKey: string): TDelegate;
|
|
48
|
+
//# sourceMappingURL=read-engine.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"read-engine.d.ts","sourceRoot":"","sources":["../../src/base/read-engine.ts"],"names":[],"mappings":"AAAA,OAAO,EAAsD,KAAK,GAAG,EAAE,KAAK,KAAK,EAAE,MAAM,aAAa,CAAC;AAGvG,OAAO,EAAoB,KAAK,YAAY,EAAE,MAAM,oBAAoB,CAAC;AACzE,OAAO,EAEL,KAAK,YAAY,EACjB,KAAK,IAAI,EACT,KAAK,gBAAgB,EACrB,KAAK,kBAAkB,EAExB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAEpD;;;GAGG;AACH,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,KAAK,CAAC;IACb,QAAQ,EAAE,MAAM,GAAG,SAAS,CAAC;IAC7B,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,UAAU,GAAG,SAAS,CAAC,CAAC,CAAC;IACxD,2DAA2D;IAC3D,uBAAuB,EAAE,OAAO,CAAC;CAClC;AAED,0GAA0G;AAC1G,wBAAgB,UAAU,CAAC,QAAQ,EAAE,OAAO,GAAG,kBAAkB,GAAG,SAAS,CAM5E;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,KAAK,GAAG,MAAM,EAAE,CAOvD;AAiBD,uGAAuG;AACvG,wBAAgB,cAAc,CAC5B,MAAM,EAAE,UAAU,EAClB,QAAQ,EAAE,OAAO,EACjB,OAAO,EAAE,IAAI,CAAC,YAAY,EAAE,IAAI,GAAG,SAAS,CAAC,GAAG,SAAS,GACxD,YAAY,CAUd;AAED,4GAA4G;AAC5G,wBAAgB,aAAa,CAAC,KAAK,EAAE,KAAK,EAAE,WAAW,EAAE,OAAO,GAAG,SAAS,GAAG,GAAG,GAAG,SAAS,CAG7F;AAED,yFAAyF;AACzF,wBAAgB,WAAW,CACzB,MAAM,EAAE,UAAU,EAClB,QAAQ,EAAE,OAAO,EACjB,KAAK,EAAE,GAAG,GAAG,SAAS,EACtB,OAAO,EAAE,YAAY,GAAG,SAAS,GAChC,GAAG,GAAG,SAAS,CAGjB;AAED,uEAAuE;AACvE,wBAAgB,cAAc,CAC5B,MAAM,EAAE,UAAU,EAClB,QAAQ,EAAE,OAAO,EACjB,OAAO,EACH,CAAC;IAAE,OAAO,CAAC,EAAE,SAAS;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,KAAK,GAAG,MAAM,CAAA;KAAE,EAAE,CAAA;CAAE,GAAG,IAAI,CAAC,YAAY,EAAE,IAAI,GAAG,SAAS,CAAC,CAAC,GAC9G,SAAS,GACZ,GAAG,EAAE,CAEP;AAED,oFAAoF;AACpF,wBAAgB,cAAc,CAAC,IAAI,EAAE,IAAI,GAAG,SAAS,GAAG,OAAO,CAE9D;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,UAAU,EAClB,QAAQ,EAAE,OAAO,EACjB,IAAI,EAAE,IAAI,GAAG,SAAS,GACrB,IAAI,CAAC,gBAAgB,EAAE,SAAS,GAAG,MAAM,CAAC,CAS5C;AAED,yFAAyF;AACzF,wBAAgB,eAAe,CAAC,SAAS,EAAE,QAAQ,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,GAAG,SAAS,CAKzF"}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { and, getTableColumns, is, isNull, One } 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
|
+
import { orderOf, whereOf } from '../utils/filter.js';
|
|
5
|
+
import { relationalConfig, } from '../utils/read.js';
|
|
6
|
+
/** The relational config a drizzle client was built with, or `undefined` for one built with no schema. */
|
|
7
|
+
export function metadataOf(executor) {
|
|
8
|
+
const internals = executor._;
|
|
9
|
+
if (!internals?.schema || !internals.tableNamesMap)
|
|
10
|
+
return undefined;
|
|
11
|
+
return { tables: internals.schema, tableNamesMap: internals.tableNamesMap };
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* The property keys of the table's primary key columns, in the order the key declares them.
|
|
15
|
+
*
|
|
16
|
+
* A composite key sets no column's `primary` flag, so it is read off the table config instead, and matched back by SQL
|
|
17
|
+
* name: the key holds its own copies of the columns, so its column objects are not the table's (drizzle-orm@0.45.1).
|
|
18
|
+
*/
|
|
19
|
+
export function primaryKeyFields(table) {
|
|
20
|
+
const columns = getTableColumns(table);
|
|
21
|
+
const fieldOf = new Map(Object.entries(columns).map(([field, column]) => [column.name, field]));
|
|
22
|
+
const declared = Object.values(columns).filter((column) => column.primary);
|
|
23
|
+
if (declared.length > 0)
|
|
24
|
+
return declared.flatMap((column) => fieldOf.get(column.name) ?? []);
|
|
25
|
+
const config = is(table, PgTable) ? getPgTableConfig(table) : getSQLiteTableConfig(table);
|
|
26
|
+
return (config.primaryKeys[0]?.columns ?? []).flatMap((column) => fieldOf.get(column.name) ?? []);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Each relation of the table that reaches one row through a column the table itself holds, mapped to that column: a
|
|
30
|
+
* relationship filter compares the related row's id, which is that column's value.
|
|
31
|
+
*/
|
|
32
|
+
function relationKeysOf(source, metadata) {
|
|
33
|
+
if (!metadata || source.queryKey === undefined)
|
|
34
|
+
return {};
|
|
35
|
+
const relations = metadata.tables[source.queryKey]?.relations ?? {};
|
|
36
|
+
const keys = {};
|
|
37
|
+
for (const [name, relation] of Object.entries(relations)) {
|
|
38
|
+
const fields = is(relation, One) ? relation.config?.fields : undefined;
|
|
39
|
+
if (fields?.length === 1 && fields[0])
|
|
40
|
+
keys[name] = fields[0];
|
|
41
|
+
}
|
|
42
|
+
return keys;
|
|
43
|
+
}
|
|
44
|
+
/** What resolving a filter or an order name of this table needs, decided at `at` against `context`. */
|
|
45
|
+
export function fieldContextOf(source, executor, options) {
|
|
46
|
+
const metadata = metadataOf(executor);
|
|
47
|
+
return {
|
|
48
|
+
columns: getTableColumns(source.table),
|
|
49
|
+
relationKeys: relationKeysOf(source, metadata),
|
|
50
|
+
virtual: (source.queryKey !== undefined ? source.reads[source.queryKey]?.virtual : undefined) ?? {},
|
|
51
|
+
at: options?.at ?? new Date(),
|
|
52
|
+
context: options?.context,
|
|
53
|
+
caseInsensitiveOperator: source.caseInsensitiveOperator,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
/** The root table's soft-delete condition, unless the read asks for marked rows or the table marks none. */
|
|
57
|
+
export function liveCondition(table, softDeleted) {
|
|
58
|
+
const columns = getTableColumns(table);
|
|
59
|
+
return !softDeleted && columns['deletedAt'] ? isNull(columns['deletedAt']) : undefined;
|
|
60
|
+
}
|
|
61
|
+
/** A finder's own `where`, the caller's filter and the soft-delete condition, joined. */
|
|
62
|
+
export function conditionOf(source, executor, where, options) {
|
|
63
|
+
const context = fieldContextOf(source, executor, options);
|
|
64
|
+
return and(where, whereOf(context, options?.filter), liveCondition(source.table, options?.softDeleted));
|
|
65
|
+
}
|
|
66
|
+
/** The order a read of many rows runs in, the primary key appended. */
|
|
67
|
+
export function orderClausesOf(source, executor, options) {
|
|
68
|
+
return orderOf(fieldContextOf(source, executor, options), options?.orderBy, primaryKeyFields(source.table));
|
|
69
|
+
}
|
|
70
|
+
/** Whether a read loads any relation, which only drizzle's relational query can. */
|
|
71
|
+
export function loadsRelations(read) {
|
|
72
|
+
return read?.relations !== undefined && Object.keys(read.relations).length > 0;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* The relational config a read resolves into, for drizzle's `findFirst` / `findMany`.
|
|
76
|
+
*
|
|
77
|
+
* @throws Error when the repository has no `queryKey` or its client no schema, which a relation needs
|
|
78
|
+
*/
|
|
79
|
+
export function relationalReadOf(source, executor, read) {
|
|
80
|
+
const metadata = metadataOf(executor);
|
|
81
|
+
if (source.queryKey === undefined || !metadata) {
|
|
82
|
+
throw new Error('A read loading a relation needs a queryKey and a client built with its schema.');
|
|
83
|
+
}
|
|
84
|
+
const orders = Object.fromEntries(Object.entries(source.reads).map(([key, declared]) => [key, declared?.orderBy]));
|
|
85
|
+
return relationalConfig(metadata, orders, source.queryKey, read);
|
|
86
|
+
}
|
|
87
|
+
/** The relational query delegate drizzle builds for the table exported as `queryKey`. */
|
|
88
|
+
export function queryDelegateOf(executor, queryKey) {
|
|
89
|
+
const queries = executor.query;
|
|
90
|
+
const delegate = Object.hasOwn(queries, queryKey) ? queries[queryKey] : undefined;
|
|
91
|
+
if (!delegate)
|
|
92
|
+
throw new Error(`Invalid queryKey: ${queryKey}`);
|
|
93
|
+
return delegate;
|
|
94
|
+
}
|