@type32/tauri-sqlite-orm 0.3.0 → 0.4.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/dist/aggregates.d.ts +12 -0
- package/dist/aggregates.js +9 -0
- package/dist/builders/delete.d.ts +23 -0
- package/dist/builders/delete.js +73 -0
- package/dist/builders/index.d.ts +7 -0
- package/dist/builders/index.js +7 -0
- package/dist/builders/insert.d.ts +31 -0
- package/dist/builders/insert.js +141 -0
- package/dist/builders/query-base.d.ts +1 -0
- package/dist/builders/query-base.js +1 -0
- package/dist/builders/relations.d.ts +11 -0
- package/dist/builders/relations.js +1 -0
- package/dist/builders/select.d.ts +54 -0
- package/dist/builders/select.js +427 -0
- package/dist/builders/update.d.ts +30 -0
- package/dist/builders/update.js +124 -0
- package/dist/builders/with.d.ts +17 -0
- package/dist/builders/with.js +34 -0
- package/dist/column-helpers.d.ts +22 -0
- package/dist/column-helpers.js +17 -0
- package/dist/dialect.d.ts +21 -0
- package/dist/dialect.js +67 -0
- package/dist/errors.d.ts +30 -0
- package/dist/errors.js +66 -0
- package/dist/index.d.mts +11 -6
- package/dist/index.d.ts +11 -6
- package/dist/operators.d.ts +30 -0
- package/dist/operators.js +84 -0
- package/dist/orm.d.ts +180 -0
- package/dist/orm.js +556 -0
- package/dist/relational-types.d.ts +87 -0
- package/dist/relational-types.js +1 -0
- package/dist/relations-v2.d.ts +77 -0
- package/dist/relations-v2.js +157 -0
- package/dist/serialization.d.ts +14 -0
- package/dist/serialization.js +135 -0
- package/dist/subquery.d.ts +5 -0
- package/dist/subquery.js +6 -0
- package/dist/types.d.ts +58 -0
- package/dist/types.js +1 -0
- package/package.json +2 -1
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
import { sql } from 'kysely';
|
|
2
|
+
import { and } from '../operators';
|
|
3
|
+
import { deserializeValue } from '../serialization';
|
|
4
|
+
/** Map DB column names to TypeScript property names for a table */
|
|
5
|
+
function getDbNameToTsName(table) {
|
|
6
|
+
const map = {};
|
|
7
|
+
for (const [tsName, col] of Object.entries(table._.columns)) {
|
|
8
|
+
map[col._.name] = tsName;
|
|
9
|
+
}
|
|
10
|
+
return map;
|
|
11
|
+
}
|
|
12
|
+
/** Normalize result key - SQLite may return quoted aliases like "users.id" */
|
|
13
|
+
function normalizeRowKey(key) {
|
|
14
|
+
if (key.startsWith('"') && key.endsWith('"')) {
|
|
15
|
+
return key.slice(1, -1);
|
|
16
|
+
}
|
|
17
|
+
return key;
|
|
18
|
+
}
|
|
19
|
+
/** Resolve which columns to select for a relation. Always includes primary keys for deduplication. */
|
|
20
|
+
function resolveRelationColumns(table, include) {
|
|
21
|
+
const allEntries = Object.entries(table._.columns);
|
|
22
|
+
if (include === true || typeof include !== 'object') {
|
|
23
|
+
return allEntries.map(([, col]) => col);
|
|
24
|
+
}
|
|
25
|
+
const cols = include.columns;
|
|
26
|
+
if (!cols) {
|
|
27
|
+
return allEntries.map(([, col]) => col);
|
|
28
|
+
}
|
|
29
|
+
const names = Array.isArray(cols)
|
|
30
|
+
? cols
|
|
31
|
+
: Object.entries(cols)
|
|
32
|
+
.filter(([, v]) => v)
|
|
33
|
+
.map(([k]) => k);
|
|
34
|
+
const pkNames = allEntries
|
|
35
|
+
.filter(([, c]) => c.options.primaryKey)
|
|
36
|
+
.map(([k]) => k);
|
|
37
|
+
const combined = new Set([...names, ...pkNames]);
|
|
38
|
+
return allEntries
|
|
39
|
+
.filter(([tsName]) => combined.has(tsName))
|
|
40
|
+
.map(([, col]) => col);
|
|
41
|
+
}
|
|
42
|
+
export class SelectQueryBuilder {
|
|
43
|
+
kysely;
|
|
44
|
+
_builder;
|
|
45
|
+
_table;
|
|
46
|
+
_columns;
|
|
47
|
+
_includeRelations = {};
|
|
48
|
+
_manualJoins = [];
|
|
49
|
+
_isDistinct = false;
|
|
50
|
+
_includedColumnAliases = [];
|
|
51
|
+
constructor(kysely, table, columns) {
|
|
52
|
+
this.kysely = kysely;
|
|
53
|
+
this._table = table;
|
|
54
|
+
this._columns = columns;
|
|
55
|
+
const selected = columns
|
|
56
|
+
? columns.map((c) => table._.columns[c])
|
|
57
|
+
: Object.values(table._.columns);
|
|
58
|
+
const colSelections = selected.map((col) => `${table._.name}.${col._.name} as "${table._.name}.${col._.name}"`);
|
|
59
|
+
this._includedColumnAliases = colSelections;
|
|
60
|
+
this._builder = kysely.selectFrom(table._.name).select(colSelections);
|
|
61
|
+
}
|
|
62
|
+
distinct() {
|
|
63
|
+
this._isDistinct = true;
|
|
64
|
+
this._builder = this._builder.distinct();
|
|
65
|
+
return this;
|
|
66
|
+
}
|
|
67
|
+
where(condition) {
|
|
68
|
+
this._builder = this._builder.where(condition);
|
|
69
|
+
return this;
|
|
70
|
+
}
|
|
71
|
+
orderBy(column, direction = 'asc') {
|
|
72
|
+
if ('toOperationNode' in column) {
|
|
73
|
+
this._builder = this._builder.orderBy(column, direction);
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
this._builder = this._builder.orderBy(sql.ref(column._.name), direction);
|
|
77
|
+
}
|
|
78
|
+
return this;
|
|
79
|
+
}
|
|
80
|
+
limit(count) {
|
|
81
|
+
this._builder = this._builder.limit(count);
|
|
82
|
+
return this;
|
|
83
|
+
}
|
|
84
|
+
offset(count) {
|
|
85
|
+
this._builder = this._builder.offset(count);
|
|
86
|
+
return this;
|
|
87
|
+
}
|
|
88
|
+
groupBy(...columns) {
|
|
89
|
+
for (const col of columns) {
|
|
90
|
+
this._builder = this._builder.groupBy(sql `${sql.ref(this._table._.name)}.${sql.ref(col._.name)}`);
|
|
91
|
+
}
|
|
92
|
+
return this;
|
|
93
|
+
}
|
|
94
|
+
having(condition) {
|
|
95
|
+
this._builder = this._builder.having(condition);
|
|
96
|
+
return this;
|
|
97
|
+
}
|
|
98
|
+
leftJoin(table, condition, alias) {
|
|
99
|
+
this._manualJoins.push({ type: 'LEFT', table, condition, alias });
|
|
100
|
+
const aliasedCols = Object.values(table._.columns).map((col) => `${alias}.${col._.name} as "${alias}.${col._.name}"`);
|
|
101
|
+
this._builder = this._builder
|
|
102
|
+
.leftJoin(`${table._.name} as ${alias}`, (join) => join.on(condition))
|
|
103
|
+
.select(aliasedCols);
|
|
104
|
+
return this;
|
|
105
|
+
}
|
|
106
|
+
innerJoin(table, condition, alias) {
|
|
107
|
+
this._manualJoins.push({ type: 'INNER', table, condition, alias });
|
|
108
|
+
const aliasedCols = Object.values(table._.columns).map((col) => `${alias}.${col._.name} as "${alias}.${col._.name}"`);
|
|
109
|
+
this._builder = this._builder
|
|
110
|
+
.innerJoin(`${table._.name} as ${alias}`, (join) => join.on(condition))
|
|
111
|
+
.select(aliasedCols);
|
|
112
|
+
return this;
|
|
113
|
+
}
|
|
114
|
+
include(relations) {
|
|
115
|
+
this._includeRelations = { ...this._includeRelations, ...relations };
|
|
116
|
+
return this;
|
|
117
|
+
}
|
|
118
|
+
applyIncludes() {
|
|
119
|
+
const processRelations = (parentTable, parentAlias, relations, depth = 0) => {
|
|
120
|
+
if (depth > 10) {
|
|
121
|
+
console.warn('[Tauri-ORM] Maximum relation depth (10) exceeded.');
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
for (const [relationName, include] of Object.entries(relations)) {
|
|
125
|
+
if (!include)
|
|
126
|
+
continue;
|
|
127
|
+
const relation = parentTable.relations[relationName];
|
|
128
|
+
if (!relation) {
|
|
129
|
+
console.warn(`[Tauri-ORM] Relation "${relationName}" not found on table "${parentTable._.name}". Skipping.`);
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
const foreignTable = relation.foreignTable;
|
|
133
|
+
const foreignAlias = `${parentAlias}_${relationName}`;
|
|
134
|
+
const selectedCols = resolveRelationColumns(foreignTable, include);
|
|
135
|
+
const aliasedCols = selectedCols.map((col) => `${foreignAlias}.${col._.name} as "${foreignAlias}.${col._.name}"`);
|
|
136
|
+
if (relation.type === 'one' && relation.fields && relation.references) {
|
|
137
|
+
const onCondition = sql `${sql.join(relation.fields.map((field, i) => sql `${sql.ref(`${parentAlias}.${field._.name}`)} = ${sql.ref(`${foreignAlias}.${relation.references[i]._.name}`)}`), sql ` AND `)}`;
|
|
138
|
+
this._builder = this._builder
|
|
139
|
+
.leftJoin(`${foreignTable._.name} as ${foreignAlias}`, (join) => join.on(onCondition))
|
|
140
|
+
.select(aliasedCols);
|
|
141
|
+
}
|
|
142
|
+
else if (relation.type === 'many') {
|
|
143
|
+
// Many-to-many via through(): parent -> junction -> foreign
|
|
144
|
+
if (relation.junctionTable && relation.fromJunction && relation.toJunction) {
|
|
145
|
+
const junctionTable = relation.junctionTable;
|
|
146
|
+
const junctionAlias = `${foreignAlias}_jn`;
|
|
147
|
+
const fromJ = relation.fromJunction;
|
|
148
|
+
const toJ = relation.toJunction;
|
|
149
|
+
const join1 = sql `${sql.ref(`${parentAlias}.${fromJ.column._.name}`)} = ${sql.ref(`${junctionAlias}.${fromJ.junctionColumn._.name}`)}`;
|
|
150
|
+
let join2 = sql `${sql.ref(`${junctionAlias}.${toJ.junctionColumn._.name}`)} = ${sql.ref(`${foreignAlias}.${toJ.column._.name}`)}`;
|
|
151
|
+
if (relation.where) {
|
|
152
|
+
join2 = and(join2, relation.where(foreignAlias));
|
|
153
|
+
}
|
|
154
|
+
this._builder = this._builder
|
|
155
|
+
.leftJoin(`${junctionTable._.name} as ${junctionAlias}`, (join) => join.on(join1))
|
|
156
|
+
.leftJoin(`${foreignTable._.name} as ${foreignAlias}`, (join) => join.on(join2))
|
|
157
|
+
.select(aliasedCols);
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
// v2: explicit fields/references, or v1: infer from reverse one relation
|
|
161
|
+
let fields = relation.fields;
|
|
162
|
+
let references = relation.references;
|
|
163
|
+
if (!fields || !references) {
|
|
164
|
+
const refRelation = Object.entries(foreignTable.relations).find(([, r]) => r.foreignTable === parentTable);
|
|
165
|
+
if (refRelation && refRelation[1].fields && refRelation[1].references) {
|
|
166
|
+
const [, relationConfig] = refRelation;
|
|
167
|
+
fields = relationConfig.fields;
|
|
168
|
+
references = relationConfig.references;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
if (fields && references) {
|
|
172
|
+
let onCondition = sql `${sql.join(fields.map((field, i) => sql `${sql.ref(`${foreignAlias}.${field._.name}`)} = ${sql.ref(`${parentAlias}.${references[i]._.name}`)}`), sql ` AND `)}`;
|
|
173
|
+
if (relation.where) {
|
|
174
|
+
onCondition = and(onCondition, relation.where(foreignAlias));
|
|
175
|
+
}
|
|
176
|
+
this._builder = this._builder
|
|
177
|
+
.leftJoin(`${foreignTable._.name} as ${foreignAlias}`, (join) => join.on(onCondition))
|
|
178
|
+
.select(aliasedCols);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
if (typeof include === 'object' && include.with) {
|
|
183
|
+
processRelations(foreignTable, foreignAlias, include.with, depth + 1);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
processRelations(this._table, this._table._.name, this._includeRelations, 0);
|
|
188
|
+
}
|
|
189
|
+
async execute() {
|
|
190
|
+
this.applyIncludes();
|
|
191
|
+
const rawResults = await this._builder.execute();
|
|
192
|
+
const hasIncludes = Object.values(this._includeRelations).some((i) => i);
|
|
193
|
+
if (hasIncludes) {
|
|
194
|
+
return this.processRelationResults(rawResults);
|
|
195
|
+
}
|
|
196
|
+
const hasManualJoins = this._manualJoins.length > 0;
|
|
197
|
+
if (hasManualJoins) {
|
|
198
|
+
return rawResults;
|
|
199
|
+
}
|
|
200
|
+
const prefix = `${this._table._.name}.`;
|
|
201
|
+
const dbNameToTs = getDbNameToTsName(this._table);
|
|
202
|
+
return rawResults.map((row) => {
|
|
203
|
+
const out = {};
|
|
204
|
+
for (const key in row) {
|
|
205
|
+
const normKey = normalizeRowKey(key);
|
|
206
|
+
const dbColName = normKey.startsWith(prefix) ? normKey.slice(prefix.length) : normKey;
|
|
207
|
+
const tsName = dbNameToTs[dbColName] ?? dbColName;
|
|
208
|
+
const column = this._table._.columns[tsName];
|
|
209
|
+
out[tsName] = column ? deserializeValue(row[key], column) : row[key];
|
|
210
|
+
}
|
|
211
|
+
return out;
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
processRelationResults(rawResults) {
|
|
215
|
+
if (!rawResults.length)
|
|
216
|
+
return [];
|
|
217
|
+
const mainTablePks = Object.values(this._table._.columns)
|
|
218
|
+
.filter((c) => c.options.primaryKey)
|
|
219
|
+
.map((c) => c._.name);
|
|
220
|
+
if (mainTablePks.length === 0)
|
|
221
|
+
return rawResults;
|
|
222
|
+
const groupedResults = new Map();
|
|
223
|
+
const parseRelationPath = (tableAlias, baseAlias) => {
|
|
224
|
+
if (!tableAlias.startsWith(baseAlias + '_'))
|
|
225
|
+
return [];
|
|
226
|
+
return tableAlias.slice(baseAlias.length + 1).split('_');
|
|
227
|
+
};
|
|
228
|
+
const setNestedValue = (obj, path, columnName, value) => {
|
|
229
|
+
let cur = obj;
|
|
230
|
+
for (let i = 0; i < path.length; i++) {
|
|
231
|
+
const key = path[i];
|
|
232
|
+
if (i === path.length - 1) {
|
|
233
|
+
if (!cur[key])
|
|
234
|
+
cur[key] = {};
|
|
235
|
+
cur[key][columnName] = value;
|
|
236
|
+
}
|
|
237
|
+
else {
|
|
238
|
+
if (!cur[key])
|
|
239
|
+
cur[key] = {};
|
|
240
|
+
cur = cur[key];
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
const getNestedRelation = (table, path) => {
|
|
245
|
+
let current = table;
|
|
246
|
+
let relation = null;
|
|
247
|
+
for (const name of path) {
|
|
248
|
+
relation = current.relations[name];
|
|
249
|
+
if (!relation)
|
|
250
|
+
return null;
|
|
251
|
+
current = relation.foreignTable;
|
|
252
|
+
}
|
|
253
|
+
return relation;
|
|
254
|
+
};
|
|
255
|
+
for (const row of rawResults) {
|
|
256
|
+
const getVal = (logicalKey) => {
|
|
257
|
+
const quoted = `"${logicalKey}"`;
|
|
258
|
+
return row[quoted] ?? row[logicalKey];
|
|
259
|
+
};
|
|
260
|
+
const mainTableKey = mainTablePks
|
|
261
|
+
.map((pk) => getVal(`${this._table._.name}.${pk}`) ?? getVal(pk))
|
|
262
|
+
.join('_');
|
|
263
|
+
if (!groupedResults.has(mainTableKey)) {
|
|
264
|
+
groupedResults.set(mainTableKey, {});
|
|
265
|
+
}
|
|
266
|
+
const result = groupedResults.get(mainTableKey);
|
|
267
|
+
const relations = {};
|
|
268
|
+
for (const [key, value] of Object.entries(row)) {
|
|
269
|
+
const normKey = normalizeRowKey(key);
|
|
270
|
+
if (!normKey.includes('.')) {
|
|
271
|
+
const mainDbToTs = getDbNameToTsName(this._table);
|
|
272
|
+
const tsName = mainDbToTs[normKey] ?? normKey;
|
|
273
|
+
const column = this._table._.columns[tsName];
|
|
274
|
+
result[tsName] = column ? deserializeValue(value, column) : value;
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
const dotIndex = normKey.indexOf('.');
|
|
278
|
+
const tableAlias = normKey.slice(0, dotIndex);
|
|
279
|
+
const columnName = normKey.slice(dotIndex + 1);
|
|
280
|
+
if (tableAlias === this._table._.name) {
|
|
281
|
+
const mainDbToTs = getDbNameToTsName(this._table);
|
|
282
|
+
const tsName = mainDbToTs[columnName] ?? columnName;
|
|
283
|
+
const column = this._table._.columns[tsName];
|
|
284
|
+
result[tsName] = column ? deserializeValue(value, column) : value;
|
|
285
|
+
}
|
|
286
|
+
else {
|
|
287
|
+
// Skip junction table alias (used for through() many-to-many)
|
|
288
|
+
if (tableAlias.endsWith('_jn'))
|
|
289
|
+
continue;
|
|
290
|
+
const path = parseRelationPath(tableAlias, this._table._.name);
|
|
291
|
+
if (path.length > 0) {
|
|
292
|
+
const relationConfig = getNestedRelation(this._table, path);
|
|
293
|
+
const foreignTable = relationConfig?.foreignTable;
|
|
294
|
+
const foreignDbToTs = foreignTable ? getDbNameToTsName(foreignTable) : {};
|
|
295
|
+
const tsName = foreignDbToTs[columnName] ?? columnName;
|
|
296
|
+
const col = foreignTable?._.columns?.[tsName];
|
|
297
|
+
setNestedValue(relations, path, tsName, col ? deserializeValue(value, col) : value);
|
|
298
|
+
}
|
|
299
|
+
else {
|
|
300
|
+
if (!result[tableAlias])
|
|
301
|
+
result[tableAlias] = {};
|
|
302
|
+
result[tableAlias][columnName] = value;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
const attachRelations = (target, relData, table) => {
|
|
307
|
+
for (const [relName, data] of Object.entries(relData)) {
|
|
308
|
+
const relationConfig = table.relations[relName];
|
|
309
|
+
if (!relationConfig)
|
|
310
|
+
continue;
|
|
311
|
+
const directData = {};
|
|
312
|
+
const nestedData = {};
|
|
313
|
+
if (typeof data === 'object' && data !== null) {
|
|
314
|
+
for (const [k, v] of Object.entries(data)) {
|
|
315
|
+
if (typeof v === 'object' && v !== null) {
|
|
316
|
+
nestedData[k] = v;
|
|
317
|
+
}
|
|
318
|
+
else {
|
|
319
|
+
directData[k] = v;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
const hasData = Object.values(directData).some((v) => v !== null && v !== undefined && v !== '');
|
|
324
|
+
if (relationConfig.type === 'many') {
|
|
325
|
+
if (!target[relName])
|
|
326
|
+
target[relName] = [];
|
|
327
|
+
if (hasData) {
|
|
328
|
+
const relPks = Object.values(relationConfig.foreignTable._.columns)
|
|
329
|
+
.filter((c) => c.options.primaryKey)
|
|
330
|
+
.map((c) => c._.name);
|
|
331
|
+
const key = relPks.map((pk) => directData[pk]).join('_');
|
|
332
|
+
if (relPks.length === 0 ||
|
|
333
|
+
!target[relName].some((r) => relPks.map((pk) => r[pk]).join('_') === key)) {
|
|
334
|
+
const newItem = { ...directData };
|
|
335
|
+
if (Object.keys(nestedData).length > 0) {
|
|
336
|
+
attachRelations(newItem, nestedData, relationConfig.foreignTable);
|
|
337
|
+
}
|
|
338
|
+
target[relName].push(newItem);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
else {
|
|
343
|
+
if (hasData || Object.keys(nestedData).length > 0) {
|
|
344
|
+
target[relName] = { ...directData };
|
|
345
|
+
if (Object.keys(nestedData).length > 0) {
|
|
346
|
+
attachRelations(target[relName], nestedData, relationConfig.foreignTable);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
};
|
|
352
|
+
attachRelations(result, relations, this._table);
|
|
353
|
+
}
|
|
354
|
+
return Array.from(groupedResults.values());
|
|
355
|
+
}
|
|
356
|
+
async all() {
|
|
357
|
+
return this.execute();
|
|
358
|
+
}
|
|
359
|
+
async get() {
|
|
360
|
+
this.limit(1);
|
|
361
|
+
const result = await this.execute();
|
|
362
|
+
return result[0];
|
|
363
|
+
}
|
|
364
|
+
async first() {
|
|
365
|
+
return this.get();
|
|
366
|
+
}
|
|
367
|
+
async exists() {
|
|
368
|
+
this.applyIncludes();
|
|
369
|
+
const compiledResult = await this._builder
|
|
370
|
+
.clearSelect()
|
|
371
|
+
.select(sql.raw('1').as('__exists__'))
|
|
372
|
+
.limit(1)
|
|
373
|
+
.execute();
|
|
374
|
+
return compiledResult.length > 0;
|
|
375
|
+
}
|
|
376
|
+
async count() {
|
|
377
|
+
this.applyIncludes();
|
|
378
|
+
const result = await this._builder
|
|
379
|
+
.clearSelect()
|
|
380
|
+
.select(sql `COUNT(*)`.as('count'))
|
|
381
|
+
.execute();
|
|
382
|
+
const row = result[0];
|
|
383
|
+
const val = row ? (row['"count"'] ?? row.count) : undefined;
|
|
384
|
+
return Number(val ?? 0);
|
|
385
|
+
}
|
|
386
|
+
async pluck(column) {
|
|
387
|
+
this.applyIncludes();
|
|
388
|
+
const col = this._table._.columns[column];
|
|
389
|
+
const alias = col._.name;
|
|
390
|
+
const results = await this._builder
|
|
391
|
+
.clearSelect()
|
|
392
|
+
.select(sql.raw(`${this._table._.name}.${alias}`).as(alias))
|
|
393
|
+
.execute();
|
|
394
|
+
return results.map((row) => {
|
|
395
|
+
const val = row['"' + alias + '"'] ?? row[alias];
|
|
396
|
+
return col ? deserializeValue(val, col) : val;
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
async paginate(page = 1, pageSize = 10) {
|
|
400
|
+
if (page < 1)
|
|
401
|
+
page = 1;
|
|
402
|
+
if (pageSize < 1)
|
|
403
|
+
pageSize = 10;
|
|
404
|
+
const total = await this.count();
|
|
405
|
+
const totalPages = Math.ceil(total / pageSize);
|
|
406
|
+
const offset = (page - 1) * pageSize;
|
|
407
|
+
const data = await this.limit(pageSize).offset(offset).all();
|
|
408
|
+
return {
|
|
409
|
+
data,
|
|
410
|
+
total,
|
|
411
|
+
page,
|
|
412
|
+
pageSize,
|
|
413
|
+
totalPages,
|
|
414
|
+
hasNextPage: page < totalPages,
|
|
415
|
+
hasPrevPage: page > 1,
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
toSQL() {
|
|
419
|
+
this.applyIncludes();
|
|
420
|
+
const compiled = this._builder.compile();
|
|
421
|
+
return { sql: compiled.sql, params: [...compiled.parameters] };
|
|
422
|
+
}
|
|
423
|
+
toKyselyExpression() {
|
|
424
|
+
this.applyIncludes();
|
|
425
|
+
return this._builder;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { Kysely } from 'kysely';
|
|
2
|
+
import { InferInsertModel } from '../orm';
|
|
3
|
+
import { AnyTable, InferSelectModel } from '../types';
|
|
4
|
+
import { Condition } from '../operators';
|
|
5
|
+
export declare class UpdateQueryBuilder<T extends AnyTable> {
|
|
6
|
+
private readonly kysely;
|
|
7
|
+
private _builder;
|
|
8
|
+
private _table;
|
|
9
|
+
private _updateData;
|
|
10
|
+
private _returningColumns;
|
|
11
|
+
private _hasWhereClause;
|
|
12
|
+
private _allowGlobal;
|
|
13
|
+
private _incrementDecrementOps;
|
|
14
|
+
constructor(kysely: Kysely<any>, table: T);
|
|
15
|
+
set(data: Partial<InferInsertModel<T>>): this;
|
|
16
|
+
where(condition: Condition): this;
|
|
17
|
+
increment(column: keyof T['_']['columns'], value?: number): this;
|
|
18
|
+
decrement(column: keyof T['_']['columns'], value?: number): this;
|
|
19
|
+
allowGlobalOperation(): this;
|
|
20
|
+
returning(...columns: (keyof T['_']['columns'])[]): this;
|
|
21
|
+
private mapReturningRows;
|
|
22
|
+
private buildSetClause;
|
|
23
|
+
execute(): Promise<T extends AnyTable ? (InferSelectModel<T> & Record<string, any>)[] : never>;
|
|
24
|
+
returningAll(): Promise<InferSelectModel<T>[]>;
|
|
25
|
+
returningFirst(): Promise<InferSelectModel<T> | undefined>;
|
|
26
|
+
toSQL(): {
|
|
27
|
+
sql: string;
|
|
28
|
+
params: any[];
|
|
29
|
+
};
|
|
30
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { sql } from 'kysely';
|
|
2
|
+
import { MissingWhereClauseError, UpdateValidationError, ColumnNotFoundError } from '../errors';
|
|
3
|
+
import { serializeValue, deserializeValue } from '../serialization';
|
|
4
|
+
export class UpdateQueryBuilder {
|
|
5
|
+
kysely;
|
|
6
|
+
_builder;
|
|
7
|
+
_table;
|
|
8
|
+
_updateData = {};
|
|
9
|
+
_returningColumns = [];
|
|
10
|
+
_hasWhereClause = false;
|
|
11
|
+
_allowGlobal = false;
|
|
12
|
+
_incrementDecrementOps = [];
|
|
13
|
+
constructor(kysely, table) {
|
|
14
|
+
this.kysely = kysely;
|
|
15
|
+
this._table = table;
|
|
16
|
+
this._builder = kysely.updateTable(table._.name);
|
|
17
|
+
}
|
|
18
|
+
set(data) {
|
|
19
|
+
this._updateData = { ...this._updateData, ...data };
|
|
20
|
+
return this;
|
|
21
|
+
}
|
|
22
|
+
where(condition) {
|
|
23
|
+
this._hasWhereClause = true;
|
|
24
|
+
this._builder = this._builder.where(condition);
|
|
25
|
+
return this;
|
|
26
|
+
}
|
|
27
|
+
increment(column, value = 1) {
|
|
28
|
+
const col = this._table._.columns[column];
|
|
29
|
+
if (!col)
|
|
30
|
+
throw new ColumnNotFoundError(String(column), this._table._.name);
|
|
31
|
+
this._incrementDecrementOps.push({ column: col._.name, op: 'increment', value });
|
|
32
|
+
return this;
|
|
33
|
+
}
|
|
34
|
+
decrement(column, value = 1) {
|
|
35
|
+
const col = this._table._.columns[column];
|
|
36
|
+
if (!col)
|
|
37
|
+
throw new ColumnNotFoundError(String(column), this._table._.name);
|
|
38
|
+
this._incrementDecrementOps.push({ column: col._.name, op: 'decrement', value });
|
|
39
|
+
return this;
|
|
40
|
+
}
|
|
41
|
+
allowGlobalOperation() {
|
|
42
|
+
this._allowGlobal = true;
|
|
43
|
+
return this;
|
|
44
|
+
}
|
|
45
|
+
returning(...columns) {
|
|
46
|
+
this._returningColumns.push(...columns);
|
|
47
|
+
return this;
|
|
48
|
+
}
|
|
49
|
+
mapReturningRows(rows) {
|
|
50
|
+
const dbNameToTs = {};
|
|
51
|
+
for (const [tsName, col] of Object.entries(this._table._.columns)) {
|
|
52
|
+
dbNameToTs[col._.name] = tsName;
|
|
53
|
+
}
|
|
54
|
+
const norm = (k) => (k.startsWith('"') && k.endsWith('"') ? k.slice(1, -1) : k);
|
|
55
|
+
return rows.map((row) => {
|
|
56
|
+
const out = {};
|
|
57
|
+
for (const [dbKey, value] of Object.entries(row)) {
|
|
58
|
+
const logicalKey = norm(dbKey);
|
|
59
|
+
const tsName = dbNameToTs[logicalKey] ?? logicalKey;
|
|
60
|
+
const column = this._table._.columns[tsName];
|
|
61
|
+
out[tsName] = column ? deserializeValue(value, column) : value;
|
|
62
|
+
}
|
|
63
|
+
return out;
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
buildSetClause() {
|
|
67
|
+
const finalData = { ...this._updateData };
|
|
68
|
+
for (const [key, column] of Object.entries(this._table._.columns)) {
|
|
69
|
+
if (finalData[key] === undefined && column.options.$onUpdateFn) {
|
|
70
|
+
;
|
|
71
|
+
finalData[key] = column.options.$onUpdateFn();
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
const entries = Object.entries(finalData);
|
|
75
|
+
const hasSetData = entries.length > 0;
|
|
76
|
+
const hasOps = this._incrementDecrementOps.length > 0;
|
|
77
|
+
if (!hasSetData && !hasOps) {
|
|
78
|
+
throw new UpdateValidationError('Cannot execute an update query without a .set(), .increment(), or .decrement() call.');
|
|
79
|
+
}
|
|
80
|
+
const setMap = {};
|
|
81
|
+
for (const [key, value] of entries) {
|
|
82
|
+
const column = this._table._.columns[key];
|
|
83
|
+
if (!column)
|
|
84
|
+
throw new ColumnNotFoundError(key, this._table._.name);
|
|
85
|
+
setMap[column._.name] = serializeValue(value, column);
|
|
86
|
+
}
|
|
87
|
+
for (const op of this._incrementDecrementOps) {
|
|
88
|
+
const sign = op.op === 'increment' ? '+' : '-';
|
|
89
|
+
setMap[op.column] = sql.raw(`${op.column} ${sign} ${op.value}`);
|
|
90
|
+
}
|
|
91
|
+
return setMap;
|
|
92
|
+
}
|
|
93
|
+
async execute() {
|
|
94
|
+
if (!this._hasWhereClause && !this._allowGlobal) {
|
|
95
|
+
throw new MissingWhereClauseError('UPDATE', this._table._.name);
|
|
96
|
+
}
|
|
97
|
+
const setMap = this.buildSetClause();
|
|
98
|
+
let builder = this._builder.set(setMap);
|
|
99
|
+
if (this._returningColumns.length > 0) {
|
|
100
|
+
const cols = this._returningColumns.map((k) => this._table._.columns[k]._.name);
|
|
101
|
+
const rows = await builder.returning(cols).execute();
|
|
102
|
+
return this.mapReturningRows(rows);
|
|
103
|
+
}
|
|
104
|
+
const result = await builder.executeTakeFirst();
|
|
105
|
+
return [{ rowsAffected: Number(result?.numUpdatedRows ?? 0) }];
|
|
106
|
+
}
|
|
107
|
+
async returningAll() {
|
|
108
|
+
const allCols = Object.keys(this._table._.columns);
|
|
109
|
+
return this.returning(...allCols).execute();
|
|
110
|
+
}
|
|
111
|
+
async returningFirst() {
|
|
112
|
+
const results = await this.returningAll();
|
|
113
|
+
return results[0];
|
|
114
|
+
}
|
|
115
|
+
toSQL() {
|
|
116
|
+
const setMap = this.buildSetClause();
|
|
117
|
+
let builder = this._builder.set(setMap);
|
|
118
|
+
if (this._returningColumns.length > 0) {
|
|
119
|
+
builder = builder.returning(this._returningColumns.map((k) => this._table._.columns[k]._.name));
|
|
120
|
+
}
|
|
121
|
+
const compiled = builder.compile();
|
|
122
|
+
return { sql: compiled.sql, params: [...compiled.parameters] };
|
|
123
|
+
}
|
|
124
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Kysely } from 'kysely';
|
|
2
|
+
import { SelectQueryBuilder } from './select';
|
|
3
|
+
import { InsertQueryBuilder } from './insert';
|
|
4
|
+
import { UpdateQueryBuilder } from './update';
|
|
5
|
+
import { DeleteQueryBuilder } from './delete';
|
|
6
|
+
import { AnyTable } from '../types';
|
|
7
|
+
export declare class WithQueryBuilder {
|
|
8
|
+
private readonly kysely;
|
|
9
|
+
private _ctes;
|
|
10
|
+
constructor(kysely: Kysely<any>);
|
|
11
|
+
with(alias: string, query: SelectQueryBuilder<any, any>): this;
|
|
12
|
+
private applyWith;
|
|
13
|
+
select<T extends AnyTable, C extends (keyof T['_']['columns'])[] | undefined = undefined>(table: T, columns?: C): SelectQueryBuilder<T, C>;
|
|
14
|
+
insert<T extends AnyTable>(table: T): InsertQueryBuilder<T>;
|
|
15
|
+
update<T extends AnyTable>(table: T): UpdateQueryBuilder<T>;
|
|
16
|
+
delete<T extends AnyTable>(table: T): DeleteQueryBuilder<T>;
|
|
17
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { SelectQueryBuilder } from './select';
|
|
2
|
+
import { InsertQueryBuilder } from './insert';
|
|
3
|
+
import { UpdateQueryBuilder } from './update';
|
|
4
|
+
import { DeleteQueryBuilder } from './delete';
|
|
5
|
+
export class WithQueryBuilder {
|
|
6
|
+
kysely;
|
|
7
|
+
_ctes = [];
|
|
8
|
+
constructor(kysely) {
|
|
9
|
+
this.kysely = kysely;
|
|
10
|
+
}
|
|
11
|
+
with(alias, query) {
|
|
12
|
+
this._ctes.push({ alias, query: query.toKyselyExpression() });
|
|
13
|
+
return this;
|
|
14
|
+
}
|
|
15
|
+
applyWith(builder) {
|
|
16
|
+
let b = builder;
|
|
17
|
+
for (const { alias, query } of this._ctes) {
|
|
18
|
+
b = b.with(alias, () => query);
|
|
19
|
+
}
|
|
20
|
+
return b;
|
|
21
|
+
}
|
|
22
|
+
select(table, columns) {
|
|
23
|
+
return new SelectQueryBuilder(this.kysely, table, columns);
|
|
24
|
+
}
|
|
25
|
+
insert(table) {
|
|
26
|
+
return new InsertQueryBuilder(this.kysely, table);
|
|
27
|
+
}
|
|
28
|
+
update(table) {
|
|
29
|
+
return new UpdateQueryBuilder(this.kysely, table);
|
|
30
|
+
}
|
|
31
|
+
delete(table) {
|
|
32
|
+
return new DeleteQueryBuilder(this.kysely, table);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { SQLiteColumn } from './orm';
|
|
2
|
+
import { Mode } from './types';
|
|
3
|
+
export declare function text<TName extends string>(name: TName): SQLiteColumn<TName, 'TEXT', 'default', false, false, false, never, never>;
|
|
4
|
+
export declare function text<TName extends string, const TConfig extends {
|
|
5
|
+
mode?: 'default' | 'json';
|
|
6
|
+
enum?: readonly string[];
|
|
7
|
+
}>(name: TName, config: TConfig): SQLiteColumn<TName, 'TEXT', TConfig['mode'] extends 'json' ? 'json' : 'default', false, false, false, TConfig['enum'] extends readonly string[] ? TConfig['enum'] : never, never>;
|
|
8
|
+
export declare function integer<TName extends string>(name: TName): SQLiteColumn<TName, 'INTEGER', 'default', false, false, false, never, never>;
|
|
9
|
+
export declare function integer<TName extends string, const TConfig extends {
|
|
10
|
+
mode: Mode;
|
|
11
|
+
}>(name: TName, config: TConfig): SQLiteColumn<TName, 'INTEGER', TConfig['mode'], false, false, false, never, never>;
|
|
12
|
+
export declare const real: <TName extends string>(name: TName) => SQLiteColumn<TName, "REAL", "default", false, false, false, never, never>;
|
|
13
|
+
export declare function blob<TName extends string>(name: TName): SQLiteColumn<TName, 'BLOB', 'default', false, false, false, never, never>;
|
|
14
|
+
export declare function blob<TName extends string, const TConfig extends {
|
|
15
|
+
mode: 'json' | 'bigint' | 'default';
|
|
16
|
+
}>(name: TName, config: TConfig): SQLiteColumn<TName, 'BLOB', TConfig['mode'], false, false, false, never, never>;
|
|
17
|
+
export declare const boolean: <TName extends string>(name: TName) => SQLiteColumn<TName, "BOOLEAN", "default", false, false, false, never, never>;
|
|
18
|
+
export declare function numeric<TName extends string>(name: TName): SQLiteColumn<TName, 'NUMERIC', 'default', false, false, false, never, never>;
|
|
19
|
+
export declare function numeric<TName extends string, const TConfig extends {
|
|
20
|
+
mode: 'bigint' | 'default';
|
|
21
|
+
}>(name: TName, config: TConfig): SQLiteColumn<TName, 'NUMERIC', TConfig['mode'], false, false, false, never, never>;
|
|
22
|
+
export declare const enumType: <TName extends string, TValues extends readonly [string, ...string[]]>(name: TName, values: TValues) => SQLiteColumn<TName, "TEXT", "default", false, false, false, TValues extends readonly string[] ? TValues : never, never>;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// Column helpers
|
|
2
|
+
import { SQLiteColumn } from './orm';
|
|
3
|
+
export function text(name, config) {
|
|
4
|
+
return new SQLiteColumn(name, 'TEXT', config);
|
|
5
|
+
}
|
|
6
|
+
export function integer(name, config) {
|
|
7
|
+
return new SQLiteColumn(name, 'INTEGER', config);
|
|
8
|
+
}
|
|
9
|
+
export const real = (name) => new SQLiteColumn(name, 'REAL');
|
|
10
|
+
export function blob(name, config) {
|
|
11
|
+
return new SQLiteColumn(name, 'BLOB', config);
|
|
12
|
+
}
|
|
13
|
+
export const boolean = (name) => new SQLiteColumn(name, 'BOOLEAN');
|
|
14
|
+
export function numeric(name, config) {
|
|
15
|
+
return new SQLiteColumn(name, 'NUMERIC', config);
|
|
16
|
+
}
|
|
17
|
+
export const enumType = (name, values) => text(name, { enum: values });
|