@getstrata/core 2.0.9 → 2.2.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 +37 -0
- package/dist/core/cache/createCacheStore.d.ts +3 -1
- package/dist/core/cache/redisCacheStore.d.ts +21 -8
- package/dist/core/cache/repository.d.ts +3 -2
- package/dist/core/cache/store.d.ts +4 -1
- package/dist/core/cache/taggedCache.d.ts +1 -1
- package/dist/core/contracts/container.d.ts +28 -10
- package/dist/core/contracts/di.d.ts +7 -4
- package/dist/core/contracts/serviceTokens.d.ts +6 -1
- package/dist/core/database/baseRepository.d.ts +0 -1
- package/dist/core/database/index.d.ts +1 -1
- package/dist/core/database/model.d.ts +123 -42
- package/dist/core/database/repositoryQuery.d.ts +16 -1
- package/dist/core/database/types.d.ts +25 -2
- package/dist/core/database/whereBuilder.d.ts +5 -5
- package/dist/core/events/deferredModelEvents.d.ts +2 -1
- package/dist/core/http/loginThrottleMiddleware.d.ts +6 -5
- package/dist/core/http/memoryThrottleMiddleware.d.ts +11 -3
- package/dist/core/http/middleware.d.ts +2 -2
- package/dist/core/http/response.d.ts +2 -0
- package/dist/core/http/routeMiddleware.d.ts +3 -2
- package/dist/core/http/scimThrottleMiddleware.d.ts +12 -3
- package/dist/core/http/throttleMiddleware.d.ts +24 -6
- package/dist/core/lifecycle/gracefulShutdown.d.ts +33 -5
- package/dist/core/queue/index.d.ts +7 -0
- package/dist/core/queue/redisQueue.d.ts +1 -0
- package/dist/core/queue/resilientQueue.d.ts +2 -2
- package/dist/core/runtime/applicationRegistry.d.ts +2 -1
- package/dist/core/runtime/boundedThrottleStore.d.ts +29 -0
- package/dist/core/scheduler/leases.d.ts +40 -0
- package/dist/core/scheduler/schedule.d.ts +29 -5
- package/dist/core/tenant/tenantContext.d.ts +5 -4
- package/dist/core/tenant/tenantMiddleware.d.ts +7 -1
- package/dist/core/tracing/otel.d.ts +28 -26
- package/dist/core/tracing/tracingMiddleware.d.ts +4 -1
- package/dist/entries/audit/exportAuditLogs.js +14 -2
- package/dist/entries/auth/basicAuthGuard.js +80 -0
- package/dist/entries/auth/jwtGuard.js +80 -0
- package/dist/entries/auth/scimAuthMiddleware.js +17 -7
- package/dist/entries/auth/sessionGuard.js +80 -0
- package/dist/entries/cache/createCacheStore.js +365 -119
- package/dist/entries/cache/repository.js +5 -0
- package/dist/entries/contracts/container.js +8 -1
- package/dist/entries/contracts/serviceTokens.js +83 -0
- package/dist/entries/database/factory.js +0 -3
- package/dist/entries/database/model.js +1 -2465
- package/dist/entries/database/repositoryQuery.js +67 -9
- package/dist/entries/http/bodySizeLimitMiddleware.js +76 -10
- package/dist/entries/http/memoryThrottleMiddleware.js +276 -21
- package/dist/entries/http/routeMiddleware.js +1 -4
- package/dist/entries/http/scimThrottleMiddleware.js +576 -54
- package/dist/entries/http/throttleMiddleware.js +65 -16
- package/dist/entries/jobs/exportAuditLogsJob.js +44 -7
- package/dist/entries/jobs/invalidateCacheTagsJob.js +30 -5
- package/dist/entries/queue/createAppQueue.js +67 -6
- package/dist/entries/queue/publicQueue.js +67 -6
- package/dist/entries/queue/queueMetrics.js +67 -6
- package/dist/entries/queue/redisQueue.js +3 -0
- package/dist/entries/queue.js +30 -5
- package/dist/entries/scheduler/schedule.js +1 -72
- package/dist/entries/tenant/databaseTenantContext.js +14 -2
- package/dist/entries/tenant/tenantDatabaseScope.js +14 -2
- package/dist/entries/tracing/tracingMiddleware.js +186 -454
- package/dist/framework/public-api.d.ts +14 -10
- package/dist/index.js +1770 -638
- package/dist/types/services.d.ts +3 -3
- package/package.json +17 -8
|
@@ -1,2465 +1 @@
|
|
|
1
|
-
|
|
2
|
-
// ../../src/core/database/model.ts
|
|
3
|
-
import { ConflictError, NotFoundError } from "@getstrata/core/errors/http";
|
|
4
|
-
|
|
5
|
-
// ../../src/core/database/inflection.ts
|
|
6
|
-
function singularize(word) {
|
|
7
|
-
if (word.endsWith("ies") && word.length > 3) {
|
|
8
|
-
return `${word.slice(0, -3)}y`;
|
|
9
|
-
}
|
|
10
|
-
if (/(ses|xes|zes|ches|shes)$/i.test(word)) {
|
|
11
|
-
return word.slice(0, -2);
|
|
12
|
-
}
|
|
13
|
-
if (word.endsWith("s") && !word.endsWith("ss")) {
|
|
14
|
-
return word.slice(0, -1);
|
|
15
|
-
}
|
|
16
|
-
return word;
|
|
17
|
-
}
|
|
18
|
-
function foreignKeyFromTable(tableName) {
|
|
19
|
-
return `${singularize(tableName)}_id`;
|
|
20
|
-
}
|
|
21
|
-
function pivotTableName(leftTable, rightTable) {
|
|
22
|
-
return [singularize(leftTable), singularize(rightTable)].sort().join("_");
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
// ../../src/core/database/query.ts
|
|
26
|
-
import { currentSqlDialect } from "@getstrata/core/database/dialect";
|
|
27
|
-
function quoteIdentifier(identifier) {
|
|
28
|
-
return currentSqlDialect().quoteIdentifier(identifier);
|
|
29
|
-
}
|
|
30
|
-
function returningSuffix(columns) {
|
|
31
|
-
return currentSqlDialect().returningClause(columns);
|
|
32
|
-
}
|
|
33
|
-
function qualifyColumn(tableName, column) {
|
|
34
|
-
return `${quoteIdentifier(tableName)}.${quoteIdentifier(column)}`;
|
|
35
|
-
}
|
|
36
|
-
function resolveQualifiedColumn(defaultTable, columnName) {
|
|
37
|
-
if (columnName.includes(".")) {
|
|
38
|
-
const [table, column] = columnName.split(".", 2);
|
|
39
|
-
if (!table || !column) {
|
|
40
|
-
throw new Error(`Invalid qualified column: ${columnName}`);
|
|
41
|
-
}
|
|
42
|
-
return qualifyColumn(table, column);
|
|
43
|
-
}
|
|
44
|
-
return qualifyColumn(defaultTable, columnName);
|
|
45
|
-
}
|
|
46
|
-
function parseQualifiedColumn(reference) {
|
|
47
|
-
const [table, column] = reference.split(".", 2);
|
|
48
|
-
if (!table || !column) {
|
|
49
|
-
throw new Error(`Join columns must be qualified as table.column: ${reference}`);
|
|
50
|
-
}
|
|
51
|
-
return { table, column };
|
|
52
|
-
}
|
|
53
|
-
function normalizeDirection(direction = "ASC") {
|
|
54
|
-
return direction.toUpperCase() === "DESC" ? "DESC" : "ASC";
|
|
55
|
-
}
|
|
56
|
-
function isQueryOperator(value) {
|
|
57
|
-
return value !== null && !Array.isArray(value) && !(value instanceof Date) && typeof value === "object";
|
|
58
|
-
}
|
|
59
|
-
function pushParam(values, value) {
|
|
60
|
-
values.push(value);
|
|
61
|
-
return currentSqlDialect().placeholder(values.length);
|
|
62
|
-
}
|
|
63
|
-
var SUPPORTED_OPERATORS = new Set([
|
|
64
|
-
"eq",
|
|
65
|
-
"ne",
|
|
66
|
-
"in",
|
|
67
|
-
"notIn",
|
|
68
|
-
"gt",
|
|
69
|
-
"gte",
|
|
70
|
-
"lt",
|
|
71
|
-
"lte",
|
|
72
|
-
"isNull",
|
|
73
|
-
"ilike",
|
|
74
|
-
"tsMatch"
|
|
75
|
-
]);
|
|
76
|
-
function buildInClause(column, values, params, negated = false) {
|
|
77
|
-
if (values.length === 0) {
|
|
78
|
-
return negated ? "1 = 1" : "1 = 0";
|
|
79
|
-
}
|
|
80
|
-
const placeholders = values.map((value) => pushParam(params, value)).join(", ");
|
|
81
|
-
return `${column}${negated ? " NOT" : ""} IN (${placeholders})`;
|
|
82
|
-
}
|
|
83
|
-
function buildOperatorClauses(column, operator, params) {
|
|
84
|
-
const clauses = [];
|
|
85
|
-
const unsupported = Object.keys(operator).filter((key) => !SUPPORTED_OPERATORS.has(key));
|
|
86
|
-
if (unsupported.length > 0) {
|
|
87
|
-
throw new Error(`Unsupported query operator(s) for ${column}: ${unsupported.join(", ")}. Supported: ${[...SUPPORTED_OPERATORS].join(", ")}.`);
|
|
88
|
-
}
|
|
89
|
-
if (operator.isNull === true) {
|
|
90
|
-
clauses.push(`${column} IS NULL`);
|
|
91
|
-
}
|
|
92
|
-
if (operator.isNull === false) {
|
|
93
|
-
clauses.push(`${column} IS NOT NULL`);
|
|
94
|
-
}
|
|
95
|
-
if (operator.eq !== undefined) {
|
|
96
|
-
if (operator.eq === null) {
|
|
97
|
-
clauses.push(`${column} IS NULL`);
|
|
98
|
-
} else {
|
|
99
|
-
clauses.push(`${column} = ${pushParam(params, operator.eq)}`);
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
if (operator.ne !== undefined) {
|
|
103
|
-
if (operator.ne === null) {
|
|
104
|
-
clauses.push(`${column} IS NOT NULL`);
|
|
105
|
-
} else {
|
|
106
|
-
clauses.push(`${column} <> ${pushParam(params, operator.ne)}`);
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
if (operator.in !== undefined) {
|
|
110
|
-
clauses.push(buildInClause(column, operator.in, params));
|
|
111
|
-
}
|
|
112
|
-
if (operator.notIn !== undefined) {
|
|
113
|
-
clauses.push(buildInClause(column, operator.notIn, params, true));
|
|
114
|
-
}
|
|
115
|
-
if (operator.gt !== undefined) {
|
|
116
|
-
clauses.push(`${column} > ${pushParam(params, operator.gt)}`);
|
|
117
|
-
}
|
|
118
|
-
if (operator.gte !== undefined) {
|
|
119
|
-
clauses.push(`${column} >= ${pushParam(params, operator.gte)}`);
|
|
120
|
-
}
|
|
121
|
-
if (operator.lt !== undefined) {
|
|
122
|
-
clauses.push(`${column} < ${pushParam(params, operator.lt)}`);
|
|
123
|
-
}
|
|
124
|
-
if (operator.lte !== undefined) {
|
|
125
|
-
clauses.push(`${column} <= ${pushParam(params, operator.lte)}`);
|
|
126
|
-
}
|
|
127
|
-
if (operator.ilike !== undefined) {
|
|
128
|
-
clauses.push(`${column} ${currentSqlDialect().ilikeOperator()} ${pushParam(params, operator.ilike)}`);
|
|
129
|
-
}
|
|
130
|
-
if (operator.tsMatch !== undefined) {
|
|
131
|
-
if (currentSqlDialect().driver !== "pgsql") {
|
|
132
|
-
throw new Error("Full-text search (tsMatch) is only available on PostgreSQL.");
|
|
133
|
-
}
|
|
134
|
-
clauses.push(`${column} @@ plainto_tsquery('english', ${pushParam(params, operator.tsMatch)})`);
|
|
135
|
-
}
|
|
136
|
-
return clauses;
|
|
137
|
-
}
|
|
138
|
-
function appendWhereParts(tableName, where, params) {
|
|
139
|
-
const clauses = [];
|
|
140
|
-
for (const [columnName, filterValue] of Object.entries(where)) {
|
|
141
|
-
if (filterValue === undefined) {
|
|
142
|
-
continue;
|
|
143
|
-
}
|
|
144
|
-
const column = resolveQualifiedColumn(tableName, columnName);
|
|
145
|
-
if (Array.isArray(filterValue)) {
|
|
146
|
-
clauses.push(buildInClause(column, filterValue, params));
|
|
147
|
-
continue;
|
|
148
|
-
}
|
|
149
|
-
if (isQueryOperator(filterValue)) {
|
|
150
|
-
clauses.push(...buildOperatorClauses(column, filterValue, params));
|
|
151
|
-
continue;
|
|
152
|
-
}
|
|
153
|
-
if (filterValue === null) {
|
|
154
|
-
clauses.push(`${column} IS NULL`);
|
|
155
|
-
continue;
|
|
156
|
-
}
|
|
157
|
-
clauses.push(`${column} = ${pushParam(params, filterValue)}`);
|
|
158
|
-
}
|
|
159
|
-
return clauses.join(" AND ");
|
|
160
|
-
}
|
|
161
|
-
function buildWhereClause(tableName, where = {}) {
|
|
162
|
-
const params = [];
|
|
163
|
-
const body = appendWhereParts(tableName, where, params);
|
|
164
|
-
return {
|
|
165
|
-
clause: body.length > 0 ? ` WHERE ${body}` : "",
|
|
166
|
-
params
|
|
167
|
-
};
|
|
168
|
-
}
|
|
169
|
-
function remapExistsSql(sql, existsParams, params) {
|
|
170
|
-
const offset = params.length;
|
|
171
|
-
params.push(...existsParams);
|
|
172
|
-
return sql.replace(/\$(\d+)/g, (_match, index) => `$${offset + Number(index)}`);
|
|
173
|
-
}
|
|
174
|
-
function buildWhereNodeClause(tableName, node, params) {
|
|
175
|
-
if ("exists" in node) {
|
|
176
|
-
const body = remapExistsSql(node.exists.sql, node.exists.params, params);
|
|
177
|
-
return `${node.exists.not ? "NOT " : ""}EXISTS (${body})`;
|
|
178
|
-
}
|
|
179
|
-
if ("where" in node) {
|
|
180
|
-
return appendWhereParts(tableName, node.where, params);
|
|
181
|
-
}
|
|
182
|
-
const grouped = buildWhereGroupClause(tableName, node.group, params);
|
|
183
|
-
if (!grouped) {
|
|
184
|
-
return "";
|
|
185
|
-
}
|
|
186
|
-
return grouped.includes(" OR ") ? `(${grouped})` : grouped;
|
|
187
|
-
}
|
|
188
|
-
function buildWhereGroupClause(tableName, nodes, params) {
|
|
189
|
-
let result = "";
|
|
190
|
-
for (const node of nodes) {
|
|
191
|
-
const part = buildWhereNodeClause(tableName, node, params);
|
|
192
|
-
if (!part) {
|
|
193
|
-
continue;
|
|
194
|
-
}
|
|
195
|
-
if (!result) {
|
|
196
|
-
result = part;
|
|
197
|
-
continue;
|
|
198
|
-
}
|
|
199
|
-
result = node.kind === "or" ? `${result} OR ${part}` : `${result} AND ${part}`;
|
|
200
|
-
}
|
|
201
|
-
if (!result) {
|
|
202
|
-
return "";
|
|
203
|
-
}
|
|
204
|
-
return result;
|
|
205
|
-
}
|
|
206
|
-
function buildAdvancedWhereClause(tableName, where = {}, whereNodes = [], params = []) {
|
|
207
|
-
const nodes = [];
|
|
208
|
-
if (Object.keys(where).length > 0) {
|
|
209
|
-
nodes.push({ kind: "and", where });
|
|
210
|
-
}
|
|
211
|
-
nodes.push(...whereNodes);
|
|
212
|
-
const combined = buildWhereGroupClause(tableName, nodes, params);
|
|
213
|
-
return {
|
|
214
|
-
clause: combined ? ` WHERE ${combined}` : "",
|
|
215
|
-
params
|
|
216
|
-
};
|
|
217
|
-
}
|
|
218
|
-
function resolveSoftDeleteColumn(table) {
|
|
219
|
-
if (!table.softDeletes) {
|
|
220
|
-
return null;
|
|
221
|
-
}
|
|
222
|
-
if (table.softDeletes === true) {
|
|
223
|
-
return "deleted_at";
|
|
224
|
-
}
|
|
225
|
-
return table.softDeletes.column ?? "deleted_at";
|
|
226
|
-
}
|
|
227
|
-
function appendSoftDeleteScope(table, options, clauses) {
|
|
228
|
-
const column = resolveSoftDeleteColumn(table);
|
|
229
|
-
if (!column) {
|
|
230
|
-
return;
|
|
231
|
-
}
|
|
232
|
-
const qualifiedColumn = qualifyColumn(table.name, column);
|
|
233
|
-
if (options.onlyTrashed) {
|
|
234
|
-
clauses.push(`${qualifiedColumn} IS NOT NULL`);
|
|
235
|
-
return;
|
|
236
|
-
}
|
|
237
|
-
if (!options.withTrashed) {
|
|
238
|
-
clauses.push(`${qualifiedColumn} IS NULL`);
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
function buildQueryWhereClause(table, options = {}, whereNodes = [], params = []) {
|
|
242
|
-
const { clause, params: whereParams } = buildAdvancedWhereClause(table.name, options.where ?? {}, whereNodes, params);
|
|
243
|
-
const softDeleteClauses = [];
|
|
244
|
-
appendSoftDeleteScope(table, options, softDeleteClauses);
|
|
245
|
-
if (softDeleteClauses.length === 0) {
|
|
246
|
-
return { clause, params: whereParams };
|
|
247
|
-
}
|
|
248
|
-
const base = clause.replace(/^ WHERE /, "");
|
|
249
|
-
const scope = softDeleteClauses.join(" AND ");
|
|
250
|
-
return {
|
|
251
|
-
clause: base ? ` WHERE (${base}) AND ${scope}` : ` WHERE ${scope}`,
|
|
252
|
-
params: whereParams
|
|
253
|
-
};
|
|
254
|
-
}
|
|
255
|
-
function isQueryOrder(value) {
|
|
256
|
-
return "column" in value;
|
|
257
|
-
}
|
|
258
|
-
function normalizeOrderBy(orderBy) {
|
|
259
|
-
if (!orderBy) {
|
|
260
|
-
return [];
|
|
261
|
-
}
|
|
262
|
-
if (Array.isArray(orderBy)) {
|
|
263
|
-
return orderBy;
|
|
264
|
-
}
|
|
265
|
-
if (isQueryOrder(orderBy)) {
|
|
266
|
-
return [orderBy];
|
|
267
|
-
}
|
|
268
|
-
return Object.entries(orderBy).map(([column, direction]) => ({
|
|
269
|
-
column,
|
|
270
|
-
direction
|
|
271
|
-
}));
|
|
272
|
-
}
|
|
273
|
-
function buildOrderByClause(tableName, orderBy) {
|
|
274
|
-
const parts = normalizeOrderBy(orderBy).map(({ column, direction }) => {
|
|
275
|
-
return `${resolveQualifiedColumn(tableName, column)} ${normalizeDirection(direction)}`;
|
|
276
|
-
});
|
|
277
|
-
return parts.length > 0 ? ` ORDER BY ${parts.join(", ")}` : "";
|
|
278
|
-
}
|
|
279
|
-
function buildGroupByClause(tableName, groupBy) {
|
|
280
|
-
if (!groupBy) {
|
|
281
|
-
return "";
|
|
282
|
-
}
|
|
283
|
-
const columns = (Array.isArray(groupBy) ? groupBy : [groupBy]).map((column) => String(column));
|
|
284
|
-
const parts = columns.map((column) => resolveQualifiedColumn(tableName, column));
|
|
285
|
-
return parts.length > 0 ? ` GROUP BY ${parts.join(", ")}` : "";
|
|
286
|
-
}
|
|
287
|
-
function buildHavingClause(tableName, having, params) {
|
|
288
|
-
if (!having) {
|
|
289
|
-
return "";
|
|
290
|
-
}
|
|
291
|
-
const body = appendWhereParts(tableName, having, params);
|
|
292
|
-
return body.length > 0 ? ` HAVING ${body}` : "";
|
|
293
|
-
}
|
|
294
|
-
function buildJoinClause(joins = []) {
|
|
295
|
-
return joins.map((join) => {
|
|
296
|
-
const joinType = join.type === "left" ? "LEFT JOIN" : "INNER JOIN";
|
|
297
|
-
const onClause = join.on.map(({ left, right }) => `${qualifyColumn(left.table, left.column)} = ${qualifyColumn(right.table, right.column)}`).join(" AND ");
|
|
298
|
-
return ` ${joinType} ${quoteIdentifier(join.table)} ON ${onClause}`;
|
|
299
|
-
}).join("");
|
|
300
|
-
}
|
|
301
|
-
function buildLimitClause(limit) {
|
|
302
|
-
if (limit === undefined) {
|
|
303
|
-
return "";
|
|
304
|
-
}
|
|
305
|
-
if (!Number.isInteger(limit) || limit <= 0) {
|
|
306
|
-
throw new Error("Query limit must be a positive integer.");
|
|
307
|
-
}
|
|
308
|
-
return ` LIMIT ${limit}`;
|
|
309
|
-
}
|
|
310
|
-
function buildOffsetClause(offset) {
|
|
311
|
-
if (offset === undefined) {
|
|
312
|
-
return "";
|
|
313
|
-
}
|
|
314
|
-
if (!Number.isInteger(offset) || offset < 0) {
|
|
315
|
-
throw new Error("Query offset must be a non-negative integer.");
|
|
316
|
-
}
|
|
317
|
-
return ` OFFSET ${offset}`;
|
|
318
|
-
}
|
|
319
|
-
function buildReturningColumns(table) {
|
|
320
|
-
return table.columns.map((column) => qualifyColumn(table.name, column)).join(", ");
|
|
321
|
-
}
|
|
322
|
-
function buildSelectList(table, select, params = []) {
|
|
323
|
-
if (!select || select.length === 0) {
|
|
324
|
-
return buildReturningColumns(table);
|
|
325
|
-
}
|
|
326
|
-
return select.map((item) => {
|
|
327
|
-
if (item.kind === "column") {
|
|
328
|
-
const column = qualifyColumn(item.table, item.column);
|
|
329
|
-
return item.as ? `${column} AS ${quoteIdentifier(item.as)}` : column;
|
|
330
|
-
}
|
|
331
|
-
if (item.kind === "literalText") {
|
|
332
|
-
return `${currentSqlDialect().castToText(pushParam(params, item.value))} AS ${quoteIdentifier(item.as)}`;
|
|
333
|
-
}
|
|
334
|
-
if (item.kind === "subqueryCount") {
|
|
335
|
-
const body = remapExistsSql(item.sql, item.params, params);
|
|
336
|
-
return `(${body}) AS ${quoteIdentifier(item.as)}`;
|
|
337
|
-
}
|
|
338
|
-
const column = qualifyColumn(item.table, item.column);
|
|
339
|
-
const placeholder = pushParam(params, item.query);
|
|
340
|
-
return `ts_rank(${column}, plainto_tsquery('english', ${placeholder})) AS ${quoteIdentifier(item.as)}`;
|
|
341
|
-
}).join(", ");
|
|
342
|
-
}
|
|
343
|
-
function getDefinedColumnEntries(table, values, options = {}) {
|
|
344
|
-
const record = values;
|
|
345
|
-
const excluded = new Set(options.exclude ?? []);
|
|
346
|
-
return table.columns.flatMap((column) => {
|
|
347
|
-
if (excluded.has(column) || !Object.hasOwn(record, column)) {
|
|
348
|
-
return [];
|
|
349
|
-
}
|
|
350
|
-
const value = record[column];
|
|
351
|
-
if (value === undefined) {
|
|
352
|
-
return [];
|
|
353
|
-
}
|
|
354
|
-
return [[column, value]];
|
|
355
|
-
});
|
|
356
|
-
}
|
|
357
|
-
function buildSelectQuery(table, options = {}, whereNodes = []) {
|
|
358
|
-
const params = [];
|
|
359
|
-
const columns = buildSelectList(table, options.select, params);
|
|
360
|
-
const { clause } = buildQueryWhereClause(table, options, whereNodes, params);
|
|
361
|
-
const joins = buildJoinClause(options.joins);
|
|
362
|
-
const groupBy = buildGroupByClause(table.name, options.groupBy);
|
|
363
|
-
const havingClause = buildHavingClause(table.name, options.having, params);
|
|
364
|
-
const orderBy = buildOrderByClause(table.name, options.orderBy ?? table.defaultOrderBy);
|
|
365
|
-
const limit = buildLimitClause(options.limit);
|
|
366
|
-
const offset = buildOffsetClause(options.offset);
|
|
367
|
-
return {
|
|
368
|
-
text: `SELECT ${columns} FROM ${quoteIdentifier(table.name)}${joins}${clause}${groupBy}${havingClause}${orderBy}${limit}${offset}`,
|
|
369
|
-
params
|
|
370
|
-
};
|
|
371
|
-
}
|
|
372
|
-
function buildCountQuery(table, where = {}, options = {}, whereNodes = []) {
|
|
373
|
-
const params = [];
|
|
374
|
-
const { clause, params: whereParams } = buildQueryWhereClause(table, {
|
|
375
|
-
where,
|
|
376
|
-
withTrashed: options.withTrashed,
|
|
377
|
-
onlyTrashed: options.onlyTrashed
|
|
378
|
-
}, whereNodes);
|
|
379
|
-
params.push(...whereParams);
|
|
380
|
-
const joins = buildJoinClause(options.joins);
|
|
381
|
-
const groupBy = buildGroupByClause(table.name, options.groupBy);
|
|
382
|
-
return {
|
|
383
|
-
text: `SELECT COUNT(*) AS count FROM ${quoteIdentifier(table.name)}${joins}${clause}${groupBy}`,
|
|
384
|
-
params
|
|
385
|
-
};
|
|
386
|
-
}
|
|
387
|
-
function buildProjectionQuery(table, expression, alias, options = {}, whereNodes = []) {
|
|
388
|
-
assertSafeProjectionExpression(expression);
|
|
389
|
-
const params = [];
|
|
390
|
-
const { clause, params: whereParams } = buildQueryWhereClause(table, options, whereNodes);
|
|
391
|
-
params.push(...whereParams);
|
|
392
|
-
const joins = buildJoinClause(options.joins);
|
|
393
|
-
const groupBy = buildGroupByClause(table.name, options.groupBy);
|
|
394
|
-
const orderBy = buildOrderByClause(table.name, options.orderBy);
|
|
395
|
-
const limit = buildLimitClause(options.limit);
|
|
396
|
-
return {
|
|
397
|
-
text: `SELECT ${expression} AS ${quoteIdentifier(alias)} FROM ${quoteIdentifier(table.name)}${joins}${clause}${groupBy}${orderBy}${limit}`,
|
|
398
|
-
params
|
|
399
|
-
};
|
|
400
|
-
}
|
|
401
|
-
var SAFE_PROJECTION_EXPRESSION = /^(COUNT\(\*\)|(?:"[\w_]+"(?:\."[\w_]+")?)|(?:AVG|SUM|MIN|MAX)\((?:"[\w_]+"(?:\."[\w_]+")?)\))$/;
|
|
402
|
-
function assertSafeProjectionExpression(expression) {
|
|
403
|
-
if (!SAFE_PROJECTION_EXPRESSION.test(expression.trim())) {
|
|
404
|
-
throw new Error(`Unsafe projection expression: ${expression}`);
|
|
405
|
-
}
|
|
406
|
-
}
|
|
407
|
-
function buildGroupedCountQuery(table, column, where = {}, options = {}) {
|
|
408
|
-
const qualifiedColumn = qualifyColumn(table.name, column);
|
|
409
|
-
const { clause, params } = buildQueryWhereClause(table, {
|
|
410
|
-
where,
|
|
411
|
-
...options
|
|
412
|
-
});
|
|
413
|
-
return {
|
|
414
|
-
text: `SELECT ${qualifiedColumn} AS ${quoteIdentifier("value")}, COUNT(*) AS ${quoteIdentifier("count")} FROM ${quoteIdentifier(table.name)}${clause} GROUP BY ${qualifiedColumn} ORDER BY ${qualifiedColumn} ASC`,
|
|
415
|
-
params
|
|
416
|
-
};
|
|
417
|
-
}
|
|
418
|
-
function buildInsertQuery(table, values) {
|
|
419
|
-
const entries = getDefinedColumnEntries(table, values);
|
|
420
|
-
if (entries.length === 0) {
|
|
421
|
-
throw new Error(`Cannot insert into ${table.name} without any column values.`);
|
|
422
|
-
}
|
|
423
|
-
const params = [];
|
|
424
|
-
const columns = entries.map(([column]) => quoteIdentifier(column)).join(", ");
|
|
425
|
-
const placeholders = entries.map(([, value]) => pushParam(params, value)).join(", ");
|
|
426
|
-
const returningColumns = buildReturningColumns(table);
|
|
427
|
-
return {
|
|
428
|
-
text: `INSERT INTO ${quoteIdentifier(table.name)} (${columns}) VALUES (${placeholders})${returningSuffix(returningColumns)}`,
|
|
429
|
-
params
|
|
430
|
-
};
|
|
431
|
-
}
|
|
432
|
-
function buildUpsertQuery(table, values, conflictColumns, updateColumns) {
|
|
433
|
-
const entries = getDefinedColumnEntries(table, values);
|
|
434
|
-
if (entries.length === 0) {
|
|
435
|
-
throw new Error(`Cannot upsert into ${table.name} without any column values.`);
|
|
436
|
-
}
|
|
437
|
-
if (conflictColumns.length === 0) {
|
|
438
|
-
throw new Error(`Cannot upsert into ${table.name} without any conflict columns.`);
|
|
439
|
-
}
|
|
440
|
-
const insertable = new Set(entries.map(([column]) => column));
|
|
441
|
-
const conflict = new Set(conflictColumns);
|
|
442
|
-
const updatable = (updateColumns ?? entries.map(([column]) => column).filter((c) => !conflict.has(c))).filter((column) => insertable.has(column));
|
|
443
|
-
const params = [];
|
|
444
|
-
const columns = entries.map(([column]) => quoteIdentifier(column)).join(", ");
|
|
445
|
-
const placeholders = entries.map(([, value]) => pushParam(params, value)).join(", ");
|
|
446
|
-
const returningColumns = buildReturningColumns(table);
|
|
447
|
-
const suffix = currentSqlDialect().upsertSuffix(conflictColumns, updatable);
|
|
448
|
-
return {
|
|
449
|
-
text: `INSERT INTO ${quoteIdentifier(table.name)} (${columns}) VALUES (${placeholders})${suffix}${returningSuffix(returningColumns)}`,
|
|
450
|
-
params
|
|
451
|
-
};
|
|
452
|
-
}
|
|
453
|
-
function buildIncrementQuery(table, id, column, amount, extra = {}) {
|
|
454
|
-
if (!Number.isFinite(amount)) {
|
|
455
|
-
throw new Error("Increment amount must be a finite number.");
|
|
456
|
-
}
|
|
457
|
-
if (!table.columns.includes(column)) {
|
|
458
|
-
throw new Error(`Unknown column ${String(column)} on ${table.name}.`);
|
|
459
|
-
}
|
|
460
|
-
const params = [];
|
|
461
|
-
const target = quoteIdentifier(column);
|
|
462
|
-
const assignments = [`${target} = ${target} + ${pushParam(params, amount)}`];
|
|
463
|
-
for (const [name, value] of getDefinedColumnEntries(table, extra, {
|
|
464
|
-
exclude: [table.primaryKey, column]
|
|
465
|
-
})) {
|
|
466
|
-
assignments.push(`${quoteIdentifier(name)} = ${pushParam(params, value)}`);
|
|
467
|
-
}
|
|
468
|
-
const primaryKeyPlaceholder = pushParam(params, id);
|
|
469
|
-
const returningColumns = buildReturningColumns(table);
|
|
470
|
-
const scopeClauses = [];
|
|
471
|
-
appendSoftDeleteScope(table, {}, scopeClauses);
|
|
472
|
-
const scopeSuffix = scopeClauses.length > 0 ? ` AND ${scopeClauses.join(" AND ")}` : "";
|
|
473
|
-
return {
|
|
474
|
-
text: `UPDATE ${quoteIdentifier(table.name)} SET ${assignments.join(", ")} WHERE ${quoteIdentifier(table.primaryKey)} = ${primaryKeyPlaceholder}${scopeSuffix}${returningSuffix(returningColumns)}`,
|
|
475
|
-
params
|
|
476
|
-
};
|
|
477
|
-
}
|
|
478
|
-
function buildUpdateQuery(table, id, changes) {
|
|
479
|
-
const entries = getDefinedColumnEntries(table, changes, {
|
|
480
|
-
exclude: [table.primaryKey]
|
|
481
|
-
});
|
|
482
|
-
if (entries.length === 0) {
|
|
483
|
-
throw new Error(`Cannot update ${table.name} without any changed column values.`);
|
|
484
|
-
}
|
|
485
|
-
const params = [];
|
|
486
|
-
const setClause = entries.map(([column, value]) => `${quoteIdentifier(column)} = ${pushParam(params, value)}`).join(", ");
|
|
487
|
-
const primaryKeyPlaceholder = pushParam(params, id);
|
|
488
|
-
const returningColumns = buildReturningColumns(table);
|
|
489
|
-
const scopeClauses = [];
|
|
490
|
-
appendSoftDeleteScope(table, {}, scopeClauses);
|
|
491
|
-
const scopeSuffix = scopeClauses.length > 0 ? ` AND ${scopeClauses.join(" AND ")}` : "";
|
|
492
|
-
return {
|
|
493
|
-
text: `UPDATE ${quoteIdentifier(table.name)} SET ${setClause} WHERE ${quoteIdentifier(table.primaryKey)} = ${primaryKeyPlaceholder}${scopeSuffix}${returningSuffix(returningColumns)}`,
|
|
494
|
-
params
|
|
495
|
-
};
|
|
496
|
-
}
|
|
497
|
-
function buildSoftDeleteByIdQuery(table, id, deletedAt) {
|
|
498
|
-
const deletedAtColumn = resolveSoftDeleteColumn(table);
|
|
499
|
-
if (!deletedAtColumn) {
|
|
500
|
-
throw new Error(`Table ${table.name} does not support soft deletes.`);
|
|
501
|
-
}
|
|
502
|
-
const returningColumns = buildReturningColumns(table);
|
|
503
|
-
const scopeClauses = [];
|
|
504
|
-
appendSoftDeleteScope(table, {}, scopeClauses);
|
|
505
|
-
const scopeSuffix = scopeClauses.length > 0 ? ` AND ${scopeClauses.join(" AND ")}` : "";
|
|
506
|
-
const params = [];
|
|
507
|
-
const deletedAtPlaceholder = pushParam(params, deletedAt);
|
|
508
|
-
const idPlaceholder = pushParam(params, id);
|
|
509
|
-
return {
|
|
510
|
-
text: `UPDATE ${quoteIdentifier(table.name)} SET ${quoteIdentifier(deletedAtColumn)} = ${deletedAtPlaceholder} WHERE ${quoteIdentifier(table.primaryKey)} = ${idPlaceholder}${scopeSuffix}${returningSuffix(returningColumns)}`,
|
|
511
|
-
params
|
|
512
|
-
};
|
|
513
|
-
}
|
|
514
|
-
function buildRestoreByIdQuery(table, id) {
|
|
515
|
-
const deletedAtColumn = resolveSoftDeleteColumn(table);
|
|
516
|
-
if (!deletedAtColumn) {
|
|
517
|
-
throw new Error(`Table ${table.name} does not support soft deletes.`);
|
|
518
|
-
}
|
|
519
|
-
const returningColumns = buildReturningColumns(table);
|
|
520
|
-
const params = [];
|
|
521
|
-
const deletedAtPlaceholder = pushParam(params, null);
|
|
522
|
-
const idPlaceholder = pushParam(params, id);
|
|
523
|
-
return {
|
|
524
|
-
text: `UPDATE ${quoteIdentifier(table.name)} SET ${quoteIdentifier(deletedAtColumn)} = ${deletedAtPlaceholder} WHERE ${quoteIdentifier(table.primaryKey)} = ${idPlaceholder} AND ${qualifyColumn(table.name, deletedAtColumn)} IS NOT NULL${returningSuffix(returningColumns)}`,
|
|
525
|
-
params
|
|
526
|
-
};
|
|
527
|
-
}
|
|
528
|
-
function buildDeleteByIdQuery(table, id) {
|
|
529
|
-
const params = [];
|
|
530
|
-
const idPlaceholder = pushParam(params, id);
|
|
531
|
-
const returning = returningSuffix(`${quoteIdentifier(table.primaryKey)} AS ${quoteIdentifier("deleted_id")}`);
|
|
532
|
-
return {
|
|
533
|
-
text: `DELETE FROM ${quoteIdentifier(table.name)} WHERE ${quoteIdentifier(table.primaryKey)} = ${idPlaceholder}${returning}`,
|
|
534
|
-
params
|
|
535
|
-
};
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
// ../../src/core/database/pluck.ts
|
|
539
|
-
function uniqueColumnSelect(table, columns) {
|
|
540
|
-
const seen = new Set;
|
|
541
|
-
const select = [];
|
|
542
|
-
for (const column of columns) {
|
|
543
|
-
if (seen.has(column)) {
|
|
544
|
-
continue;
|
|
545
|
-
}
|
|
546
|
-
seen.add(column);
|
|
547
|
-
select.push({ kind: "column", table, column, as: column });
|
|
548
|
-
}
|
|
549
|
-
return select;
|
|
550
|
-
}
|
|
551
|
-
function projectPluck(rows, column, keyBy) {
|
|
552
|
-
if (keyBy === undefined) {
|
|
553
|
-
return rows.map((row) => row[column]);
|
|
554
|
-
}
|
|
555
|
-
const keyed = new Map;
|
|
556
|
-
for (const row of rows) {
|
|
557
|
-
keyed.set(row[keyBy], row[column]);
|
|
558
|
-
}
|
|
559
|
-
return keyed;
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
// ../../src/core/database/relationQuery.ts
|
|
563
|
-
function asWhere(where) {
|
|
564
|
-
return where;
|
|
565
|
-
}
|
|
566
|
-
function ownerId(owner, ownerKey) {
|
|
567
|
-
if (typeof owner.get === "function") {
|
|
568
|
-
return owner.get(ownerKey);
|
|
569
|
-
}
|
|
570
|
-
if (owner && typeof owner === "object" && "id" in owner && owner.id !== undefined) {
|
|
571
|
-
return owner.id;
|
|
572
|
-
}
|
|
573
|
-
throw new Error("belongsTo.associate() requires a related model or { id }.");
|
|
574
|
-
}
|
|
575
|
-
function thenGet(get, onfulfilled, onrejected) {
|
|
576
|
-
return get().then(onfulfilled ?? undefined, onrejected ?? undefined);
|
|
577
|
-
}
|
|
578
|
-
function pluckFromQuery(query, column, keyBy) {
|
|
579
|
-
if (!query) {
|
|
580
|
-
return Promise.resolve(keyBy === undefined ? [] : new Map);
|
|
581
|
-
}
|
|
582
|
-
return keyBy === undefined ? query.pluck(column) : query.pluck(column, keyBy);
|
|
583
|
-
}
|
|
584
|
-
|
|
585
|
-
class HasManyRelationQuery {
|
|
586
|
-
parent;
|
|
587
|
-
related;
|
|
588
|
-
relation;
|
|
589
|
-
kind = "hasMany";
|
|
590
|
-
extraWhere = {};
|
|
591
|
-
extraOptions = {};
|
|
592
|
-
constructor(parent, related, relation) {
|
|
593
|
-
this.parent = parent;
|
|
594
|
-
this.related = related;
|
|
595
|
-
this.relation = relation;
|
|
596
|
-
}
|
|
597
|
-
where(where) {
|
|
598
|
-
this.extraWhere = { ...this.extraWhere, ...where };
|
|
599
|
-
return this;
|
|
600
|
-
}
|
|
601
|
-
orderBy(orderBy) {
|
|
602
|
-
this.extraOptions = { ...this.extraOptions, orderBy };
|
|
603
|
-
return this;
|
|
604
|
-
}
|
|
605
|
-
limit(limit) {
|
|
606
|
-
this.extraOptions = { ...this.extraOptions, limit };
|
|
607
|
-
return this;
|
|
608
|
-
}
|
|
609
|
-
applyEagerLoad(query, alias) {
|
|
610
|
-
query.withHasMany(alias, this.relation, this.related.repository(), this.extraOptions);
|
|
611
|
-
}
|
|
612
|
-
hydrateEager(row, alias) {
|
|
613
|
-
const value = row[alias] ?? [];
|
|
614
|
-
const rows = Array.isArray(value) ? value : [];
|
|
615
|
-
return rows.map((item) => this.related.newFromRecord(item));
|
|
616
|
-
}
|
|
617
|
-
toExistsClause(parentTable) {
|
|
618
|
-
const childTable = this.related.repository().getTable().name;
|
|
619
|
-
const extra = buildAdvancedWhereClause(childTable, this.extraWhere, [], []);
|
|
620
|
-
const extraSql = extra.clause.replace(/^ WHERE /, "");
|
|
621
|
-
const sql = `SELECT 1 FROM ${quoteIdentifier(childTable)} WHERE ${qualifyColumn(childTable, this.relation.foreignKey)} = ${qualifyColumn(parentTable, this.relation.localKey)}${extraSql ? ` AND ${extraSql}` : ""}`;
|
|
622
|
-
return { sql, params: extra.params };
|
|
623
|
-
}
|
|
624
|
-
scopedQuery() {
|
|
625
|
-
const repository = this.related.repository().withConnection(this.parent.getRepository().getConnection());
|
|
626
|
-
let query = repository.query(asWhere({
|
|
627
|
-
[this.relation.foreignKey]: this.parent.get(this.relation.localKey),
|
|
628
|
-
...this.extraWhere
|
|
629
|
-
}));
|
|
630
|
-
if (this.extraOptions.orderBy) {
|
|
631
|
-
query = query.orderBy(this.extraOptions.orderBy);
|
|
632
|
-
}
|
|
633
|
-
if (this.extraOptions.limit !== undefined) {
|
|
634
|
-
query = query.limit(this.extraOptions.limit);
|
|
635
|
-
}
|
|
636
|
-
return query;
|
|
637
|
-
}
|
|
638
|
-
async get() {
|
|
639
|
-
const rows = await this.scopedQuery().get();
|
|
640
|
-
return rows.map((row) => this.related.newFromRecord(row));
|
|
641
|
-
}
|
|
642
|
-
async first() {
|
|
643
|
-
const rows = await this.limit(1).get();
|
|
644
|
-
return rows[0] ?? null;
|
|
645
|
-
}
|
|
646
|
-
async count() {
|
|
647
|
-
return this.scopedQuery().count();
|
|
648
|
-
}
|
|
649
|
-
async pluck(column, keyBy) {
|
|
650
|
-
return pluckFromQuery(this.scopedQuery(), column, keyBy);
|
|
651
|
-
}
|
|
652
|
-
async value(column) {
|
|
653
|
-
return this.scopedQuery().value(column);
|
|
654
|
-
}
|
|
655
|
-
then(onfulfilled, onrejected) {
|
|
656
|
-
return thenGet(() => this.get(), onfulfilled, onrejected);
|
|
657
|
-
}
|
|
658
|
-
async create(attributes = {}) {
|
|
659
|
-
return this.related.create(attributes, {
|
|
660
|
-
[this.relation.foreignKey]: this.parent.get(this.relation.localKey)
|
|
661
|
-
});
|
|
662
|
-
}
|
|
663
|
-
async save(related) {
|
|
664
|
-
const forced = {
|
|
665
|
-
[this.relation.foreignKey]: this.parent.get(this.relation.localKey)
|
|
666
|
-
};
|
|
667
|
-
const savable = related;
|
|
668
|
-
if (typeof savable.save === "function") {
|
|
669
|
-
savable.mergeAttributes?.(forced);
|
|
670
|
-
await savable.save();
|
|
671
|
-
return related;
|
|
672
|
-
}
|
|
673
|
-
return this.create(related);
|
|
674
|
-
}
|
|
675
|
-
async createMany(records) {
|
|
676
|
-
return Promise.all(records.map((attributes) => this.create(attributes)));
|
|
677
|
-
}
|
|
678
|
-
}
|
|
679
|
-
|
|
680
|
-
class HasOneRelationQuery {
|
|
681
|
-
relation;
|
|
682
|
-
kind = "hasOne";
|
|
683
|
-
inner;
|
|
684
|
-
constructor(parent, related, relation) {
|
|
685
|
-
this.relation = relation;
|
|
686
|
-
this.inner = new HasManyRelationQuery(parent, related, {
|
|
687
|
-
type: "hasMany",
|
|
688
|
-
name: relation.name,
|
|
689
|
-
localKey: relation.localKey,
|
|
690
|
-
foreignKey: relation.foreignKey
|
|
691
|
-
});
|
|
692
|
-
}
|
|
693
|
-
where(where) {
|
|
694
|
-
this.inner.where(where);
|
|
695
|
-
return this;
|
|
696
|
-
}
|
|
697
|
-
orderBy(orderBy) {
|
|
698
|
-
this.inner.orderBy(orderBy);
|
|
699
|
-
return this;
|
|
700
|
-
}
|
|
701
|
-
applyEagerLoad(query, alias) {
|
|
702
|
-
this.inner.limit(1).applyEagerLoad(query, alias);
|
|
703
|
-
}
|
|
704
|
-
hydrateEager(row, alias) {
|
|
705
|
-
const hydrated = this.inner.hydrateEager(row, alias);
|
|
706
|
-
return hydrated[0];
|
|
707
|
-
}
|
|
708
|
-
toExistsClause(parentTable) {
|
|
709
|
-
return this.inner.toExistsClause(parentTable);
|
|
710
|
-
}
|
|
711
|
-
async get() {
|
|
712
|
-
return this.inner.limit(1).first();
|
|
713
|
-
}
|
|
714
|
-
async first() {
|
|
715
|
-
return this.get();
|
|
716
|
-
}
|
|
717
|
-
async count() {
|
|
718
|
-
return this.inner.count();
|
|
719
|
-
}
|
|
720
|
-
async pluck(column, keyBy) {
|
|
721
|
-
const query = this.inner.limit(1);
|
|
722
|
-
return keyBy === undefined ? query.pluck(column) : query.pluck(column, keyBy);
|
|
723
|
-
}
|
|
724
|
-
async value(column) {
|
|
725
|
-
return this.inner.limit(1).value(column);
|
|
726
|
-
}
|
|
727
|
-
then(onfulfilled, onrejected) {
|
|
728
|
-
return thenGet(() => this.get(), onfulfilled, onrejected);
|
|
729
|
-
}
|
|
730
|
-
async create(attributes = {}) {
|
|
731
|
-
return this.inner.create(attributes);
|
|
732
|
-
}
|
|
733
|
-
async save(related) {
|
|
734
|
-
return this.inner.save(related);
|
|
735
|
-
}
|
|
736
|
-
}
|
|
737
|
-
|
|
738
|
-
class BelongsToRelationQuery {
|
|
739
|
-
parent;
|
|
740
|
-
related;
|
|
741
|
-
relation;
|
|
742
|
-
kind = "belongsTo";
|
|
743
|
-
extraWhere = {};
|
|
744
|
-
extraOptions = {};
|
|
745
|
-
constructor(parent, related, relation) {
|
|
746
|
-
this.parent = parent;
|
|
747
|
-
this.related = related;
|
|
748
|
-
this.relation = relation;
|
|
749
|
-
}
|
|
750
|
-
where(where) {
|
|
751
|
-
this.extraWhere = { ...this.extraWhere, ...where };
|
|
752
|
-
return this;
|
|
753
|
-
}
|
|
754
|
-
orderBy(orderBy) {
|
|
755
|
-
this.extraOptions = { ...this.extraOptions, orderBy };
|
|
756
|
-
return this;
|
|
757
|
-
}
|
|
758
|
-
applyEagerLoad(query, alias) {
|
|
759
|
-
query.withBelongsTo(alias, this.relation, this.related.repository(), this.extraOptions);
|
|
760
|
-
}
|
|
761
|
-
hydrateEager(row, alias) {
|
|
762
|
-
const value = row[alias];
|
|
763
|
-
return value ? this.related.newFromRecord(value) : value;
|
|
764
|
-
}
|
|
765
|
-
toExistsClause(parentTable) {
|
|
766
|
-
const relatedTable = this.related.repository().getTable().name;
|
|
767
|
-
const extra = buildAdvancedWhereClause(relatedTable, this.extraWhere, [], []);
|
|
768
|
-
const extraSql = extra.clause.replace(/^ WHERE /, "");
|
|
769
|
-
const sql = `SELECT 1 FROM ${quoteIdentifier(relatedTable)} WHERE ${qualifyColumn(relatedTable, this.relation.ownerKey)} = ${qualifyColumn(parentTable, this.relation.foreignKey)}${extraSql ? ` AND ${extraSql}` : ""}`;
|
|
770
|
-
return { sql, params: extra.params };
|
|
771
|
-
}
|
|
772
|
-
async get() {
|
|
773
|
-
const query = this.relatedQuery();
|
|
774
|
-
if (!query) {
|
|
775
|
-
return null;
|
|
776
|
-
}
|
|
777
|
-
const row = await query.first();
|
|
778
|
-
return row ? this.related.newFromRecord(row) : null;
|
|
779
|
-
}
|
|
780
|
-
async first() {
|
|
781
|
-
return this.get();
|
|
782
|
-
}
|
|
783
|
-
async pluck(column, keyBy) {
|
|
784
|
-
return pluckFromQuery(this.relatedQuery(), column, keyBy);
|
|
785
|
-
}
|
|
786
|
-
async value(column) {
|
|
787
|
-
return this.relatedQuery()?.value(column) ?? null;
|
|
788
|
-
}
|
|
789
|
-
relatedQuery() {
|
|
790
|
-
const foreign = this.parent.get(this.relation.foreignKey);
|
|
791
|
-
if (foreign === null || foreign === undefined) {
|
|
792
|
-
return null;
|
|
793
|
-
}
|
|
794
|
-
const repository = this.related.repository().withConnection(this.parent.getRepository().getConnection());
|
|
795
|
-
let query = repository.query(asWhere({ [this.relation.ownerKey]: foreign, ...this.extraWhere }));
|
|
796
|
-
if (this.extraOptions.orderBy) {
|
|
797
|
-
query = query.orderBy(this.extraOptions.orderBy);
|
|
798
|
-
}
|
|
799
|
-
return query;
|
|
800
|
-
}
|
|
801
|
-
then(onfulfilled, onrejected) {
|
|
802
|
-
return thenGet(() => this.get(), onfulfilled, onrejected);
|
|
803
|
-
}
|
|
804
|
-
async associate(owner) {
|
|
805
|
-
await this.parent.getRepository().updateById(this.parent.id, {
|
|
806
|
-
[this.relation.foreignKey]: ownerId(owner, this.relation.ownerKey)
|
|
807
|
-
});
|
|
808
|
-
}
|
|
809
|
-
async dissociate() {
|
|
810
|
-
await this.parent.getRepository().updateById(this.parent.id, {
|
|
811
|
-
[this.relation.foreignKey]: null
|
|
812
|
-
});
|
|
813
|
-
}
|
|
814
|
-
}
|
|
815
|
-
|
|
816
|
-
class BelongsToManyRelationQuery {
|
|
817
|
-
parent;
|
|
818
|
-
related;
|
|
819
|
-
relation;
|
|
820
|
-
kind = "belongsToMany";
|
|
821
|
-
extraWhere = {};
|
|
822
|
-
extraOptions = {};
|
|
823
|
-
pivotValues = {};
|
|
824
|
-
constructor(parent, related, relation) {
|
|
825
|
-
this.parent = parent;
|
|
826
|
-
this.related = related;
|
|
827
|
-
this.relation = relation;
|
|
828
|
-
}
|
|
829
|
-
where(where) {
|
|
830
|
-
this.extraWhere = { ...this.extraWhere, ...where };
|
|
831
|
-
return this;
|
|
832
|
-
}
|
|
833
|
-
orderBy(orderBy) {
|
|
834
|
-
this.extraOptions = { ...this.extraOptions, orderBy };
|
|
835
|
-
return this;
|
|
836
|
-
}
|
|
837
|
-
applyEagerLoad(query, alias) {
|
|
838
|
-
query.withBelongsToMany(alias, this.relation, this.related.repository(), this.extraOptions);
|
|
839
|
-
}
|
|
840
|
-
withPivotValues(values) {
|
|
841
|
-
this.pivotValues = { ...this.pivotValues, ...values };
|
|
842
|
-
return this;
|
|
843
|
-
}
|
|
844
|
-
hydrateEager(row, alias) {
|
|
845
|
-
const value = row[alias] ?? [];
|
|
846
|
-
const rows = Array.isArray(value) ? value : [];
|
|
847
|
-
return rows.map((item) => this.related.newFromRecord(item));
|
|
848
|
-
}
|
|
849
|
-
toExistsClause(parentTable) {
|
|
850
|
-
const relatedTable = this.related.repository().getTable().name;
|
|
851
|
-
const extra = buildAdvancedWhereClause(relatedTable, this.extraWhere, [], []);
|
|
852
|
-
const extraSql = extra.clause.replace(/^ WHERE /, "");
|
|
853
|
-
const sql = `SELECT 1 FROM ${quoteIdentifier(relatedTable)} INNER JOIN ${quoteIdentifier(this.relation.pivotTable)} ON ${qualifyColumn(this.relation.pivotTable, this.relation.relatedPivotKey)} = ${qualifyColumn(relatedTable, this.relation.relatedKey)} WHERE ${qualifyColumn(this.relation.pivotTable, this.relation.foreignPivotKey)} = ${qualifyColumn(parentTable, this.relation.parentKey)}${extraSql ? ` AND ${extraSql}` : ""}`;
|
|
854
|
-
return { sql, params: extra.params };
|
|
855
|
-
}
|
|
856
|
-
connection() {
|
|
857
|
-
return this.parent.getRepository().getConnection();
|
|
858
|
-
}
|
|
859
|
-
async get() {
|
|
860
|
-
const parentId = this.parent.get(this.relation.parentKey);
|
|
861
|
-
const pivotRows = await this.connection().unsafe(`SELECT * FROM ${quoteIdentifier(this.relation.pivotTable)} WHERE ${quoteIdentifier(String(this.relation.foreignPivotKey))} = $1`, [parentId]);
|
|
862
|
-
if (pivotRows.length === 0) {
|
|
863
|
-
return [];
|
|
864
|
-
}
|
|
865
|
-
const relatedIds = [
|
|
866
|
-
...new Set(pivotRows.map((row) => row[this.relation.relatedPivotKey]))
|
|
867
|
-
];
|
|
868
|
-
const repository = this.related.repository().withConnection(this.connection());
|
|
869
|
-
const rows = await repository.findAll({
|
|
870
|
-
...this.extraOptions,
|
|
871
|
-
where: asWhere({
|
|
872
|
-
[this.relation.relatedKey]: relatedIds,
|
|
873
|
-
...this.extraWhere
|
|
874
|
-
})
|
|
875
|
-
});
|
|
876
|
-
return rows.map((row) => this.related.newFromRecord(row));
|
|
877
|
-
}
|
|
878
|
-
async first() {
|
|
879
|
-
const rows = await this.get();
|
|
880
|
-
return rows[0] ?? null;
|
|
881
|
-
}
|
|
882
|
-
async count() {
|
|
883
|
-
const parentId = this.parent.get(this.relation.parentKey);
|
|
884
|
-
const rows = await this.connection().unsafe(`SELECT COUNT(*) AS count FROM ${quoteIdentifier(this.relation.pivotTable)} WHERE ${quoteIdentifier(String(this.relation.foreignPivotKey))} = $1`, [parentId]);
|
|
885
|
-
return Number(rows[0]?.count ?? 0);
|
|
886
|
-
}
|
|
887
|
-
async pluck(column, keyBy) {
|
|
888
|
-
return pluckFromQuery(await this.relatedQuery(), column, keyBy);
|
|
889
|
-
}
|
|
890
|
-
async value(column) {
|
|
891
|
-
const query = await this.relatedQuery();
|
|
892
|
-
return query ? query.value(column) : null;
|
|
893
|
-
}
|
|
894
|
-
async relatedQuery() {
|
|
895
|
-
const parentId = this.parent.get(this.relation.parentKey);
|
|
896
|
-
const pivotRows = await this.connection().unsafe(`SELECT * FROM ${quoteIdentifier(this.relation.pivotTable)} WHERE ${quoteIdentifier(String(this.relation.foreignPivotKey))} = $1`, [parentId]);
|
|
897
|
-
if (pivotRows.length === 0) {
|
|
898
|
-
return null;
|
|
899
|
-
}
|
|
900
|
-
const relatedIds = [
|
|
901
|
-
...new Set(pivotRows.map((row) => row[this.relation.relatedPivotKey]))
|
|
902
|
-
];
|
|
903
|
-
let query = this.related.repository().withConnection(this.connection()).query(asWhere({
|
|
904
|
-
[this.relation.relatedKey]: relatedIds,
|
|
905
|
-
...this.extraWhere
|
|
906
|
-
}));
|
|
907
|
-
if (this.extraOptions.orderBy) {
|
|
908
|
-
query = query.orderBy(this.extraOptions.orderBy);
|
|
909
|
-
}
|
|
910
|
-
return query;
|
|
911
|
-
}
|
|
912
|
-
then(onfulfilled, onrejected) {
|
|
913
|
-
return thenGet(() => this.get(), onfulfilled, onrejected);
|
|
914
|
-
}
|
|
915
|
-
async attach(ids) {
|
|
916
|
-
const list = Array.isArray(ids) ? ids : [ids];
|
|
917
|
-
const parentId = this.parent.get(this.relation.parentKey);
|
|
918
|
-
const extraKeys = Object.keys(this.pivotValues);
|
|
919
|
-
const extraColumns = extraKeys.length > 0 ? `, ${extraKeys.map((key) => quoteIdentifier(key)).join(", ")}` : "";
|
|
920
|
-
const extraPlaceholders = extraKeys.map((_, index) => `$${index + 3}`).join(", ");
|
|
921
|
-
const extraValues = extraKeys.map((key) => this.pivotValues[key]);
|
|
922
|
-
for (const id of list) {
|
|
923
|
-
await this.connection().unsafe(extraKeys.length > 0 ? `INSERT INTO ${quoteIdentifier(this.relation.pivotTable)} (${quoteIdentifier(String(this.relation.foreignPivotKey))}, ${quoteIdentifier(String(this.relation.relatedPivotKey))}${extraColumns}) VALUES ($1, $2, ${extraPlaceholders})` : `INSERT INTO ${quoteIdentifier(this.relation.pivotTable)} (${quoteIdentifier(String(this.relation.foreignPivotKey))}, ${quoteIdentifier(String(this.relation.relatedPivotKey))}) VALUES ($1, $2)`, [parentId, id, ...extraValues]);
|
|
924
|
-
}
|
|
925
|
-
}
|
|
926
|
-
async toggle(ids) {
|
|
927
|
-
const list = Array.isArray(ids) ? ids : [ids];
|
|
928
|
-
const parentId = this.parent.get(this.relation.parentKey);
|
|
929
|
-
for (const id of list) {
|
|
930
|
-
const existing = await this.connection().unsafe(`SELECT 1 FROM ${quoteIdentifier(this.relation.pivotTable)} WHERE ${quoteIdentifier(String(this.relation.foreignPivotKey))} = $1 AND ${quoteIdentifier(String(this.relation.relatedPivotKey))} = $2 LIMIT 1`, [parentId, id]);
|
|
931
|
-
if (existing.length > 0) {
|
|
932
|
-
await this.detach(id);
|
|
933
|
-
} else {
|
|
934
|
-
await this.attach(id);
|
|
935
|
-
}
|
|
936
|
-
}
|
|
937
|
-
}
|
|
938
|
-
async detach(ids) {
|
|
939
|
-
const parentId = this.parent.get(this.relation.parentKey);
|
|
940
|
-
if (ids === undefined) {
|
|
941
|
-
await this.connection().unsafe(`DELETE FROM ${quoteIdentifier(this.relation.pivotTable)} WHERE ${quoteIdentifier(String(this.relation.foreignPivotKey))} = $1`, [parentId]);
|
|
942
|
-
return;
|
|
943
|
-
}
|
|
944
|
-
const list = Array.isArray(ids) ? ids : [ids];
|
|
945
|
-
const placeholders = list.map((_, index) => `$${index + 2}`).join(", ");
|
|
946
|
-
await this.connection().unsafe(`DELETE FROM ${quoteIdentifier(this.relation.pivotTable)} WHERE ${quoteIdentifier(String(this.relation.foreignPivotKey))} = $1 AND ${quoteIdentifier(String(this.relation.relatedPivotKey))} IN (${placeholders})`, [parentId, ...list]);
|
|
947
|
-
}
|
|
948
|
-
async sync(ids) {
|
|
949
|
-
await this.detach();
|
|
950
|
-
if (ids.length > 0) {
|
|
951
|
-
await this.attach(ids);
|
|
952
|
-
}
|
|
953
|
-
}
|
|
954
|
-
async create(attributes = {}) {
|
|
955
|
-
const related = await this.related.create(attributes);
|
|
956
|
-
await this.attach(related.id);
|
|
957
|
-
return related;
|
|
958
|
-
}
|
|
959
|
-
}
|
|
960
|
-
|
|
961
|
-
class MorphManyRelationQuery {
|
|
962
|
-
parent;
|
|
963
|
-
related;
|
|
964
|
-
relation;
|
|
965
|
-
kind = "morphMany";
|
|
966
|
-
extraWhere = {};
|
|
967
|
-
extraOptions = {};
|
|
968
|
-
constructor(parent, related, relation) {
|
|
969
|
-
this.parent = parent;
|
|
970
|
-
this.related = related;
|
|
971
|
-
this.relation = relation;
|
|
972
|
-
}
|
|
973
|
-
where(where) {
|
|
974
|
-
this.extraWhere = { ...this.extraWhere, ...where };
|
|
975
|
-
return this;
|
|
976
|
-
}
|
|
977
|
-
applyEagerLoad(query, alias) {
|
|
978
|
-
query.withMorphMany(alias, this.relation, this.related.repository(), this.extraOptions);
|
|
979
|
-
}
|
|
980
|
-
hydrateEager(row, alias) {
|
|
981
|
-
const value = row[alias] ?? [];
|
|
982
|
-
const rows = Array.isArray(value) ? value : [];
|
|
983
|
-
return rows.map((item) => this.related.newFromRecord(item));
|
|
984
|
-
}
|
|
985
|
-
toExistsClause(parentTable) {
|
|
986
|
-
const childTable = this.related.repository().getTable().name;
|
|
987
|
-
const extra = buildAdvancedWhereClause(childTable, {
|
|
988
|
-
[this.relation.morphTypeKey]: this.relation.morphType,
|
|
989
|
-
...this.extraWhere
|
|
990
|
-
}, [], []);
|
|
991
|
-
const extraSql = extra.clause.replace(/^ WHERE /, "");
|
|
992
|
-
const sql = `SELECT 1 FROM ${quoteIdentifier(childTable)} WHERE ${qualifyColumn(childTable, this.relation.morphIdKey)} = ${qualifyColumn(parentTable, this.relation.localKey)}${extraSql ? ` AND ${extraSql}` : ""}`;
|
|
993
|
-
return { sql, params: extra.params };
|
|
994
|
-
}
|
|
995
|
-
scopedQuery() {
|
|
996
|
-
return this.related.repository().withConnection(this.parent.getRepository().getConnection()).query(asWhere({
|
|
997
|
-
[this.relation.morphTypeKey]: this.relation.morphType,
|
|
998
|
-
[this.relation.morphIdKey]: this.parent.get(this.relation.localKey),
|
|
999
|
-
...this.extraWhere
|
|
1000
|
-
}));
|
|
1001
|
-
}
|
|
1002
|
-
async get() {
|
|
1003
|
-
const rows = await this.scopedQuery().get();
|
|
1004
|
-
return rows.map((row) => this.related.newFromRecord(row));
|
|
1005
|
-
}
|
|
1006
|
-
async first() {
|
|
1007
|
-
const rows = await this.get();
|
|
1008
|
-
return rows[0] ?? null;
|
|
1009
|
-
}
|
|
1010
|
-
async count() {
|
|
1011
|
-
return this.scopedQuery().count();
|
|
1012
|
-
}
|
|
1013
|
-
async pluck(column, keyBy) {
|
|
1014
|
-
return pluckFromQuery(this.scopedQuery(), column, keyBy);
|
|
1015
|
-
}
|
|
1016
|
-
async value(column) {
|
|
1017
|
-
return this.scopedQuery().value(column);
|
|
1018
|
-
}
|
|
1019
|
-
then(onfulfilled, onrejected) {
|
|
1020
|
-
return thenGet(() => this.get(), onfulfilled, onrejected);
|
|
1021
|
-
}
|
|
1022
|
-
async create(attributes = {}) {
|
|
1023
|
-
return this.related.create(attributes, {
|
|
1024
|
-
[this.relation.morphTypeKey]: this.relation.morphType,
|
|
1025
|
-
[this.relation.morphIdKey]: this.parent.get(this.relation.localKey)
|
|
1026
|
-
});
|
|
1027
|
-
}
|
|
1028
|
-
}
|
|
1029
|
-
|
|
1030
|
-
class MorphOneRelationQuery {
|
|
1031
|
-
relation;
|
|
1032
|
-
kind = "morphOne";
|
|
1033
|
-
inner;
|
|
1034
|
-
constructor(parent, related, relation) {
|
|
1035
|
-
this.relation = relation;
|
|
1036
|
-
this.inner = new MorphManyRelationQuery(parent, related, {
|
|
1037
|
-
type: "morphMany",
|
|
1038
|
-
name: relation.name,
|
|
1039
|
-
localKey: relation.localKey,
|
|
1040
|
-
morphTypeKey: relation.morphTypeKey,
|
|
1041
|
-
morphIdKey: relation.morphIdKey,
|
|
1042
|
-
morphType: relation.morphType
|
|
1043
|
-
});
|
|
1044
|
-
}
|
|
1045
|
-
where(where) {
|
|
1046
|
-
this.inner.where(where);
|
|
1047
|
-
return this;
|
|
1048
|
-
}
|
|
1049
|
-
applyEagerLoad(query, alias) {
|
|
1050
|
-
this.inner.applyEagerLoad(query, alias);
|
|
1051
|
-
}
|
|
1052
|
-
hydrateEager(row, alias) {
|
|
1053
|
-
const hydrated = this.inner.hydrateEager(row, alias);
|
|
1054
|
-
return hydrated[0];
|
|
1055
|
-
}
|
|
1056
|
-
toExistsClause(parentTable) {
|
|
1057
|
-
return this.inner.toExistsClause(parentTable);
|
|
1058
|
-
}
|
|
1059
|
-
async get() {
|
|
1060
|
-
return this.inner.first();
|
|
1061
|
-
}
|
|
1062
|
-
async first() {
|
|
1063
|
-
return this.get();
|
|
1064
|
-
}
|
|
1065
|
-
async count() {
|
|
1066
|
-
return this.inner.count();
|
|
1067
|
-
}
|
|
1068
|
-
async pluck(column, keyBy) {
|
|
1069
|
-
return keyBy === undefined ? this.inner.pluck(column) : this.inner.pluck(column, keyBy);
|
|
1070
|
-
}
|
|
1071
|
-
async value(column) {
|
|
1072
|
-
return this.inner.value(column);
|
|
1073
|
-
}
|
|
1074
|
-
then(onfulfilled, onrejected) {
|
|
1075
|
-
return thenGet(() => this.get(), onfulfilled, onrejected);
|
|
1076
|
-
}
|
|
1077
|
-
async create(attributes = {}) {
|
|
1078
|
-
return this.inner.create(attributes);
|
|
1079
|
-
}
|
|
1080
|
-
}
|
|
1081
|
-
|
|
1082
|
-
class MorphToRelationQuery {
|
|
1083
|
-
parent;
|
|
1084
|
-
relatedByType;
|
|
1085
|
-
relation;
|
|
1086
|
-
kind = "morphTo";
|
|
1087
|
-
extraWhere = {};
|
|
1088
|
-
constructor(parent, relatedByType, relation) {
|
|
1089
|
-
this.parent = parent;
|
|
1090
|
-
this.relatedByType = relatedByType;
|
|
1091
|
-
this.relation = relation;
|
|
1092
|
-
}
|
|
1093
|
-
where(where) {
|
|
1094
|
-
this.extraWhere = { ...this.extraWhere, ...where };
|
|
1095
|
-
return this;
|
|
1096
|
-
}
|
|
1097
|
-
applyEagerLoad(query, alias) {
|
|
1098
|
-
const repositories = new Map(Object.entries(this.relatedByType).map(([type, model]) => [
|
|
1099
|
-
type,
|
|
1100
|
-
model.repository()
|
|
1101
|
-
]));
|
|
1102
|
-
query.withMorphTo(alias, this.relation, repositories);
|
|
1103
|
-
}
|
|
1104
|
-
hydrateEager(row, alias) {
|
|
1105
|
-
return row[alias];
|
|
1106
|
-
}
|
|
1107
|
-
toExistsClause(parentTable) {
|
|
1108
|
-
const type = String(this.parent.get(this.relation.morphTypeKey) ?? "");
|
|
1109
|
-
const related = this.relatedByType[type];
|
|
1110
|
-
if (!related) {
|
|
1111
|
-
return { sql: "SELECT 1 WHERE 1 = 0", params: [] };
|
|
1112
|
-
}
|
|
1113
|
-
const relatedTable = related.repository().getTable();
|
|
1114
|
-
const extra = buildAdvancedWhereClause(relatedTable.name, this.extraWhere, [], []);
|
|
1115
|
-
const extraSql = extra.clause.replace(/^ WHERE /, "");
|
|
1116
|
-
return {
|
|
1117
|
-
sql: `SELECT 1 FROM ${quoteIdentifier(relatedTable.name)} WHERE ${qualifyColumn(relatedTable.name, relatedTable.primaryKey)} = ${qualifyColumn(parentTable, this.relation.morphIdKey)}${extraSql ? ` AND ${extraSql}` : ""}`,
|
|
1118
|
-
params: extra.params
|
|
1119
|
-
};
|
|
1120
|
-
}
|
|
1121
|
-
async get() {
|
|
1122
|
-
const query = this.relatedQuery();
|
|
1123
|
-
if (!query) {
|
|
1124
|
-
return null;
|
|
1125
|
-
}
|
|
1126
|
-
const row = await query.first();
|
|
1127
|
-
return row ? this.relatedForCurrentType()?.newFromRecord(row) ?? null : null;
|
|
1128
|
-
}
|
|
1129
|
-
async pluck(column, keyBy) {
|
|
1130
|
-
return pluckFromQuery(this.relatedQuery(), column, keyBy);
|
|
1131
|
-
}
|
|
1132
|
-
async value(column) {
|
|
1133
|
-
return this.relatedQuery()?.value(column) ?? null;
|
|
1134
|
-
}
|
|
1135
|
-
relatedForCurrentType() {
|
|
1136
|
-
const type = String(this.parent.get(this.relation.morphTypeKey) ?? "");
|
|
1137
|
-
return this.relatedByType[type];
|
|
1138
|
-
}
|
|
1139
|
-
relatedQuery() {
|
|
1140
|
-
const id = this.parent.get(this.relation.morphIdKey);
|
|
1141
|
-
const related = this.relatedForCurrentType();
|
|
1142
|
-
if (!related || id === null || id === undefined) {
|
|
1143
|
-
return null;
|
|
1144
|
-
}
|
|
1145
|
-
const table = related.repository().getTable();
|
|
1146
|
-
return related.repository().withConnection(this.parent.getRepository().getConnection()).query(asWhere({ [table.primaryKey]: id, ...this.extraWhere }));
|
|
1147
|
-
}
|
|
1148
|
-
then(onfulfilled, onrejected) {
|
|
1149
|
-
return thenGet(() => this.get(), onfulfilled, onrejected);
|
|
1150
|
-
}
|
|
1151
|
-
}
|
|
1152
|
-
|
|
1153
|
-
class HasManyThroughRelationQuery {
|
|
1154
|
-
parent;
|
|
1155
|
-
related;
|
|
1156
|
-
relation;
|
|
1157
|
-
kind = "hasManyThrough";
|
|
1158
|
-
extraWhere = {};
|
|
1159
|
-
extraOptions = {};
|
|
1160
|
-
constructor(parent, related, relation) {
|
|
1161
|
-
this.parent = parent;
|
|
1162
|
-
this.related = related;
|
|
1163
|
-
this.relation = relation;
|
|
1164
|
-
}
|
|
1165
|
-
where(where) {
|
|
1166
|
-
this.extraWhere = { ...this.extraWhere, ...where };
|
|
1167
|
-
return this;
|
|
1168
|
-
}
|
|
1169
|
-
orderBy(orderBy) {
|
|
1170
|
-
this.extraOptions = { ...this.extraOptions, orderBy };
|
|
1171
|
-
return this;
|
|
1172
|
-
}
|
|
1173
|
-
limit(limit) {
|
|
1174
|
-
this.extraOptions = { ...this.extraOptions, limit };
|
|
1175
|
-
return this;
|
|
1176
|
-
}
|
|
1177
|
-
applyEagerLoad(query, alias) {
|
|
1178
|
-
query.withHasManyThrough(alias, this.relation, this.related.repository(), this.extraOptions);
|
|
1179
|
-
}
|
|
1180
|
-
hydrateEager(row, alias) {
|
|
1181
|
-
const value = row[alias] ?? [];
|
|
1182
|
-
const rows = Array.isArray(value) ? value : [];
|
|
1183
|
-
return rows.map((item) => this.related.newFromRecord(item));
|
|
1184
|
-
}
|
|
1185
|
-
toExistsClause(parentTable) {
|
|
1186
|
-
const farTable = this.related.repository().getTable().name;
|
|
1187
|
-
const extra = buildAdvancedWhereClause(farTable, this.extraWhere, [], []);
|
|
1188
|
-
const extraSql = extra.clause.replace(/^ WHERE /, "");
|
|
1189
|
-
const sql = `SELECT 1 FROM ${quoteIdentifier(farTable)} INNER JOIN ${quoteIdentifier(this.relation.throughTable)} ON ${qualifyColumn(this.relation.throughTable, this.relation.secondLocalKey)} = ${qualifyColumn(farTable, this.relation.secondKey)} WHERE ${qualifyColumn(this.relation.throughTable, this.relation.firstKey)} = ${qualifyColumn(parentTable, this.relation.localKey)}${extraSql ? ` AND ${extraSql}` : ""}`;
|
|
1190
|
-
return { sql, params: extra.params };
|
|
1191
|
-
}
|
|
1192
|
-
async get() {
|
|
1193
|
-
const rows = await this.farRows();
|
|
1194
|
-
return rows.map((row) => this.related.newFromRecord(row));
|
|
1195
|
-
}
|
|
1196
|
-
async first() {
|
|
1197
|
-
const rows = await this.limit(1).get();
|
|
1198
|
-
return rows[0] ?? null;
|
|
1199
|
-
}
|
|
1200
|
-
async count() {
|
|
1201
|
-
const rows = await this.get();
|
|
1202
|
-
return rows.length;
|
|
1203
|
-
}
|
|
1204
|
-
async pluck(column, keyBy) {
|
|
1205
|
-
const rows = await this.farRows();
|
|
1206
|
-
return keyBy === undefined ? projectPluck(rows, column) : projectPluck(rows, column, keyBy);
|
|
1207
|
-
}
|
|
1208
|
-
async value(column) {
|
|
1209
|
-
const values = await this.limit(1).pluck(column);
|
|
1210
|
-
return values[0] ?? null;
|
|
1211
|
-
}
|
|
1212
|
-
async farRows() {
|
|
1213
|
-
return this.related.repository().withConnection(this.parent.getRepository().getConnection()).findHasManyThrough(this.parent.get(this.relation.localKey), this.relation, {
|
|
1214
|
-
...this.extraOptions,
|
|
1215
|
-
where: this.extraWhere
|
|
1216
|
-
});
|
|
1217
|
-
}
|
|
1218
|
-
then(onfulfilled, onrejected) {
|
|
1219
|
-
return thenGet(() => this.get(), onfulfilled, onrejected);
|
|
1220
|
-
}
|
|
1221
|
-
}
|
|
1222
|
-
|
|
1223
|
-
// ../../src/core/database/relationships.ts
|
|
1224
|
-
function hasMany(definition) {
|
|
1225
|
-
return {
|
|
1226
|
-
type: "hasMany",
|
|
1227
|
-
...definition
|
|
1228
|
-
};
|
|
1229
|
-
}
|
|
1230
|
-
function hasOne(definition) {
|
|
1231
|
-
return {
|
|
1232
|
-
type: "hasOne",
|
|
1233
|
-
...definition
|
|
1234
|
-
};
|
|
1235
|
-
}
|
|
1236
|
-
function belongsTo(definition) {
|
|
1237
|
-
return {
|
|
1238
|
-
type: "belongsTo",
|
|
1239
|
-
...definition
|
|
1240
|
-
};
|
|
1241
|
-
}
|
|
1242
|
-
function hasManyThrough(definition) {
|
|
1243
|
-
return {
|
|
1244
|
-
type: "hasManyThrough",
|
|
1245
|
-
throughParentKey: "__through_parent_id",
|
|
1246
|
-
...definition
|
|
1247
|
-
};
|
|
1248
|
-
}
|
|
1249
|
-
function belongsToMany(definition) {
|
|
1250
|
-
return {
|
|
1251
|
-
type: "belongsToMany",
|
|
1252
|
-
...definition
|
|
1253
|
-
};
|
|
1254
|
-
}
|
|
1255
|
-
function relationMatchKey(value) {
|
|
1256
|
-
if (value === null || value === undefined) {
|
|
1257
|
-
return "";
|
|
1258
|
-
}
|
|
1259
|
-
if (typeof value === "bigint") {
|
|
1260
|
-
return value.toString();
|
|
1261
|
-
}
|
|
1262
|
-
if (typeof value === "number" && Number.isFinite(value)) {
|
|
1263
|
-
return String(value);
|
|
1264
|
-
}
|
|
1265
|
-
if (typeof value === "string" && /^-?\d+$/.test(value)) {
|
|
1266
|
-
return BigInt(value).toString();
|
|
1267
|
-
}
|
|
1268
|
-
return String(value);
|
|
1269
|
-
}
|
|
1270
|
-
var relationLookupCache = new WeakMap;
|
|
1271
|
-
function indexedRelationLookup(map) {
|
|
1272
|
-
const cached = relationLookupCache.get(map);
|
|
1273
|
-
if (cached) {
|
|
1274
|
-
return cached;
|
|
1275
|
-
}
|
|
1276
|
-
const indexed = new Map;
|
|
1277
|
-
for (const [existing, value] of map) {
|
|
1278
|
-
indexed.set(relationMatchKey(existing), value);
|
|
1279
|
-
}
|
|
1280
|
-
relationLookupCache.set(map, indexed);
|
|
1281
|
-
return indexed;
|
|
1282
|
-
}
|
|
1283
|
-
function getByRelationKey(map, key) {
|
|
1284
|
-
if (map.has(key)) {
|
|
1285
|
-
return map.get(key);
|
|
1286
|
-
}
|
|
1287
|
-
const want = relationMatchKey(key);
|
|
1288
|
-
if (want === "") {
|
|
1289
|
-
return;
|
|
1290
|
-
}
|
|
1291
|
-
return indexedRelationLookup(map).get(want);
|
|
1292
|
-
}
|
|
1293
|
-
function indexHasManyRelation(parents, children, relation) {
|
|
1294
|
-
const groups = new Map;
|
|
1295
|
-
const originalKeys = new Map;
|
|
1296
|
-
for (const parent of parents) {
|
|
1297
|
-
const key = relationMatchKey(parent[relation.localKey]);
|
|
1298
|
-
if (!groups.has(key)) {
|
|
1299
|
-
groups.set(key, []);
|
|
1300
|
-
originalKeys.set(key, parent[relation.localKey]);
|
|
1301
|
-
}
|
|
1302
|
-
}
|
|
1303
|
-
for (const child of children) {
|
|
1304
|
-
const group = groups.get(relationMatchKey(child[relation.foreignKey]));
|
|
1305
|
-
if (!group) {
|
|
1306
|
-
continue;
|
|
1307
|
-
}
|
|
1308
|
-
group.push(child);
|
|
1309
|
-
}
|
|
1310
|
-
const result = new Map;
|
|
1311
|
-
for (const [key, group] of groups) {
|
|
1312
|
-
const original = originalKeys.get(key);
|
|
1313
|
-
if (original !== undefined) {
|
|
1314
|
-
result.set(original, group);
|
|
1315
|
-
}
|
|
1316
|
-
}
|
|
1317
|
-
return result;
|
|
1318
|
-
}
|
|
1319
|
-
function indexHasOneRelation(parents, children, relation) {
|
|
1320
|
-
const grouped = indexHasManyRelation(parents, children, relation);
|
|
1321
|
-
const result = new Map;
|
|
1322
|
-
for (const parent of parents) {
|
|
1323
|
-
const matches = getByRelationKey(grouped, parent[relation.localKey]) ?? [];
|
|
1324
|
-
result.set(parent[relation.localKey], matches[0]);
|
|
1325
|
-
}
|
|
1326
|
-
return result;
|
|
1327
|
-
}
|
|
1328
|
-
function indexBelongsToRelation(children, parents, relation) {
|
|
1329
|
-
const parentsById = new Map;
|
|
1330
|
-
for (const parent of parents) {
|
|
1331
|
-
parentsById.set(relationMatchKey(parent[relation.ownerKey]), parent);
|
|
1332
|
-
}
|
|
1333
|
-
const result = new Map;
|
|
1334
|
-
for (const child of children) {
|
|
1335
|
-
const foreignKey = child[relation.foreignKey];
|
|
1336
|
-
const parent = parentsById.get(relationMatchKey(foreignKey));
|
|
1337
|
-
if (parent) {
|
|
1338
|
-
result.set(foreignKey, parent);
|
|
1339
|
-
}
|
|
1340
|
-
}
|
|
1341
|
-
return result;
|
|
1342
|
-
}
|
|
1343
|
-
function indexBelongsToManyRelation(parents, pivotRows, relatedRows, relation) {
|
|
1344
|
-
const relatedById = new Map;
|
|
1345
|
-
for (const related of relatedRows) {
|
|
1346
|
-
relatedById.set(relationMatchKey(related[relation.relatedKey]), related);
|
|
1347
|
-
}
|
|
1348
|
-
const groups = new Map;
|
|
1349
|
-
const originalKeys = new Map;
|
|
1350
|
-
for (const parent of parents) {
|
|
1351
|
-
const key = relationMatchKey(parent[relation.parentKey]);
|
|
1352
|
-
if (!groups.has(key)) {
|
|
1353
|
-
groups.set(key, []);
|
|
1354
|
-
originalKeys.set(key, parent[relation.parentKey]);
|
|
1355
|
-
}
|
|
1356
|
-
}
|
|
1357
|
-
for (const pivot of pivotRows) {
|
|
1358
|
-
const group = groups.get(relationMatchKey(pivot[relation.foreignPivotKey]));
|
|
1359
|
-
const related = relatedById.get(relationMatchKey(pivot[relation.relatedPivotKey]));
|
|
1360
|
-
if (!group || !related) {
|
|
1361
|
-
continue;
|
|
1362
|
-
}
|
|
1363
|
-
group.push(related);
|
|
1364
|
-
}
|
|
1365
|
-
const result = new Map;
|
|
1366
|
-
for (const [key, group] of groups) {
|
|
1367
|
-
const original = originalKeys.get(key);
|
|
1368
|
-
if (original !== undefined) {
|
|
1369
|
-
result.set(original, group);
|
|
1370
|
-
}
|
|
1371
|
-
}
|
|
1372
|
-
return result;
|
|
1373
|
-
}
|
|
1374
|
-
function morphMany(definition) {
|
|
1375
|
-
return {
|
|
1376
|
-
type: "morphMany",
|
|
1377
|
-
...definition
|
|
1378
|
-
};
|
|
1379
|
-
}
|
|
1380
|
-
function morphOne(definition) {
|
|
1381
|
-
return {
|
|
1382
|
-
type: "morphOne",
|
|
1383
|
-
...definition
|
|
1384
|
-
};
|
|
1385
|
-
}
|
|
1386
|
-
function morphTo(definition) {
|
|
1387
|
-
return {
|
|
1388
|
-
type: "morphTo",
|
|
1389
|
-
...definition
|
|
1390
|
-
};
|
|
1391
|
-
}
|
|
1392
|
-
function indexMorphManyRelation(parents, children, relation) {
|
|
1393
|
-
const groups = new Map;
|
|
1394
|
-
const originalKeys = new Map;
|
|
1395
|
-
for (const parent of parents) {
|
|
1396
|
-
const key = relationMatchKey(parent[relation.localKey]);
|
|
1397
|
-
if (!groups.has(key)) {
|
|
1398
|
-
groups.set(key, []);
|
|
1399
|
-
originalKeys.set(key, parent[relation.localKey]);
|
|
1400
|
-
}
|
|
1401
|
-
}
|
|
1402
|
-
for (const child of children) {
|
|
1403
|
-
if (child[relation.morphTypeKey] !== relation.morphType) {
|
|
1404
|
-
continue;
|
|
1405
|
-
}
|
|
1406
|
-
const group = groups.get(relationMatchKey(child[relation.morphIdKey]));
|
|
1407
|
-
if (!group) {
|
|
1408
|
-
continue;
|
|
1409
|
-
}
|
|
1410
|
-
group.push(child);
|
|
1411
|
-
}
|
|
1412
|
-
const result = new Map;
|
|
1413
|
-
for (const [key, group] of groups) {
|
|
1414
|
-
const original = originalKeys.get(key);
|
|
1415
|
-
if (original !== undefined) {
|
|
1416
|
-
result.set(original, group);
|
|
1417
|
-
}
|
|
1418
|
-
}
|
|
1419
|
-
return result;
|
|
1420
|
-
}
|
|
1421
|
-
function indexMorphOneRelation(parents, children, relation) {
|
|
1422
|
-
const grouped = indexMorphManyRelation(parents, children, relation);
|
|
1423
|
-
const result = new Map;
|
|
1424
|
-
for (const parent of parents) {
|
|
1425
|
-
const matches = getByRelationKey(grouped, parent[relation.localKey]) ?? [];
|
|
1426
|
-
result.set(parent[relation.localKey], matches[0]);
|
|
1427
|
-
}
|
|
1428
|
-
return result;
|
|
1429
|
-
}
|
|
1430
|
-
function indexHasManyThroughRelation(parents, children, relation) {
|
|
1431
|
-
const throughKey = relation.throughParentKey ?? "__through_parent_id";
|
|
1432
|
-
const grouped = new Map;
|
|
1433
|
-
for (const child of children) {
|
|
1434
|
-
const key = relationMatchKey(child[throughKey]);
|
|
1435
|
-
if (key === "") {
|
|
1436
|
-
continue;
|
|
1437
|
-
}
|
|
1438
|
-
const existing = grouped.get(key) ?? [];
|
|
1439
|
-
const { [throughKey]: _through, ...far } = child;
|
|
1440
|
-
existing.push(far);
|
|
1441
|
-
grouped.set(key, existing);
|
|
1442
|
-
}
|
|
1443
|
-
const result = new Map;
|
|
1444
|
-
for (const parent of parents) {
|
|
1445
|
-
const key = relationMatchKey(parent[relation.localKey]);
|
|
1446
|
-
result.set(parent[relation.localKey], grouped.get(key) ?? []);
|
|
1447
|
-
}
|
|
1448
|
-
return result;
|
|
1449
|
-
}
|
|
1450
|
-
function indexMorphToRelation(children, parentsByType, relation) {
|
|
1451
|
-
const result = new Map;
|
|
1452
|
-
for (const child of children) {
|
|
1453
|
-
const morphType = String(child[relation.morphTypeKey]);
|
|
1454
|
-
const parents = parentsByType.get(morphType);
|
|
1455
|
-
if (!parents) {
|
|
1456
|
-
continue;
|
|
1457
|
-
}
|
|
1458
|
-
const parent = getByRelationKey(parents, child[relation.morphIdKey]);
|
|
1459
|
-
if (parent) {
|
|
1460
|
-
result.set(child[relation.morphIdKey], parent);
|
|
1461
|
-
}
|
|
1462
|
-
}
|
|
1463
|
-
return result;
|
|
1464
|
-
}
|
|
1465
|
-
|
|
1466
|
-
// ../../src/core/database/model.ts
|
|
1467
|
-
var modelRepositories = new WeakMap;
|
|
1468
|
-
var namedModels = new Map;
|
|
1469
|
-
var modelGlobalScopes = new WeakMap;
|
|
1470
|
-
var modelObservers = new WeakMap;
|
|
1471
|
-
var modelBooted = new WeakSet;
|
|
1472
|
-
function flattenRelationNames(relations) {
|
|
1473
|
-
const names = [];
|
|
1474
|
-
for (const item of relations) {
|
|
1475
|
-
if (typeof item === "string") {
|
|
1476
|
-
if (item.length > 0) {
|
|
1477
|
-
names.push(item);
|
|
1478
|
-
}
|
|
1479
|
-
continue;
|
|
1480
|
-
}
|
|
1481
|
-
if (Array.isArray(item)) {
|
|
1482
|
-
for (const nested of item) {
|
|
1483
|
-
if (typeof nested === "string" && nested.length > 0) {
|
|
1484
|
-
names.push(nested);
|
|
1485
|
-
}
|
|
1486
|
-
}
|
|
1487
|
-
}
|
|
1488
|
-
}
|
|
1489
|
-
return names;
|
|
1490
|
-
}
|
|
1491
|
-
async function runObservers(model, hook) {
|
|
1492
|
-
const observers = modelObservers.get(model.constructor) ?? [];
|
|
1493
|
-
for (const observer of observers) {
|
|
1494
|
-
const handler = observer[hook];
|
|
1495
|
-
if (handler && await handler(model) === false) {
|
|
1496
|
-
return false;
|
|
1497
|
-
}
|
|
1498
|
-
}
|
|
1499
|
-
return true;
|
|
1500
|
-
}
|
|
1501
|
-
function accessorName(key) {
|
|
1502
|
-
const pascal = key.replace(/_([a-z])/g, (_match, letter) => letter.toUpperCase());
|
|
1503
|
-
return `get${pascal.charAt(0).toUpperCase()}${pascal.slice(1)}Attribute`;
|
|
1504
|
-
}
|
|
1505
|
-
function isLoadableModel(value) {
|
|
1506
|
-
return Boolean(value && typeof value === "object" && typeof value.load === "function" && typeof value.loaded === "function" && typeof value.setLoaded === "function");
|
|
1507
|
-
}
|
|
1508
|
-
async function eagerLoadOnModels(models, paths) {
|
|
1509
|
-
if (models.length === 0 || paths.length === 0) {
|
|
1510
|
-
return;
|
|
1511
|
-
}
|
|
1512
|
-
const grouped = new Map;
|
|
1513
|
-
for (const path of paths) {
|
|
1514
|
-
const [head, ...rest] = path.split(".");
|
|
1515
|
-
if (!head) {
|
|
1516
|
-
continue;
|
|
1517
|
-
}
|
|
1518
|
-
const nested = rest.join(".");
|
|
1519
|
-
const existing = grouped.get(head) ?? [];
|
|
1520
|
-
if (nested) {
|
|
1521
|
-
existing.push(nested);
|
|
1522
|
-
}
|
|
1523
|
-
grouped.set(head, existing);
|
|
1524
|
-
}
|
|
1525
|
-
await Promise.all([...grouped.entries()].map(async ([head, nested]) => {
|
|
1526
|
-
const unloaded = models.filter((model) => model.loaded(head) === undefined);
|
|
1527
|
-
if (unloaded.length > 0) {
|
|
1528
|
-
const first = unloaded[0];
|
|
1529
|
-
if (!first) {
|
|
1530
|
-
return;
|
|
1531
|
-
}
|
|
1532
|
-
const method = first[head];
|
|
1533
|
-
if (typeof method !== "function") {
|
|
1534
|
-
throw new Error(`${first.constructor.name} has no relation method ${head}().`);
|
|
1535
|
-
}
|
|
1536
|
-
const relationQuery = method.call(first);
|
|
1537
|
-
const query = first.getRepository().query();
|
|
1538
|
-
relationQuery.applyEagerLoad(query, head);
|
|
1539
|
-
const attached = await query.attachToRows(unloaded.map((model) => model.toObject()));
|
|
1540
|
-
for (const [index, model] of unloaded.entries()) {
|
|
1541
|
-
const row = attached[index] ?? model.toObject();
|
|
1542
|
-
model.setLoaded(head, relationQuery.hydrateEager(row, head));
|
|
1543
|
-
}
|
|
1544
|
-
}
|
|
1545
|
-
if (nested.length === 0) {
|
|
1546
|
-
return;
|
|
1547
|
-
}
|
|
1548
|
-
const children = models.flatMap((model) => {
|
|
1549
|
-
const loaded = model.loaded(head);
|
|
1550
|
-
return Array.isArray(loaded) ? loaded : loaded ? [loaded] : [];
|
|
1551
|
-
});
|
|
1552
|
-
await eagerLoadOnModels(children.filter(isLoadableModel), nested);
|
|
1553
|
-
}));
|
|
1554
|
-
}
|
|
1555
|
-
async function loadNested(model, path) {
|
|
1556
|
-
await eagerLoadOnModels([model], [path]);
|
|
1557
|
-
}
|
|
1558
|
-
function resolveModelRepository(model) {
|
|
1559
|
-
const repository = modelRepositories.get(model);
|
|
1560
|
-
if (!repository) {
|
|
1561
|
-
throw new Error(`${model.name}.repository() is not implemented.`);
|
|
1562
|
-
}
|
|
1563
|
-
return repository;
|
|
1564
|
-
}
|
|
1565
|
-
function registerModelClass(name, model) {
|
|
1566
|
-
namedModels.set(name, model);
|
|
1567
|
-
}
|
|
1568
|
-
function resolveRelated(related) {
|
|
1569
|
-
if (typeof related === "string") {
|
|
1570
|
-
const found = namedModels.get(related);
|
|
1571
|
-
if (!found) {
|
|
1572
|
-
throw new Error(`Model [${related}] is not registered. Call registerModelClass() first.`);
|
|
1573
|
-
}
|
|
1574
|
-
return found;
|
|
1575
|
-
}
|
|
1576
|
-
if (typeof related === "function" && typeof related.repository !== "function") {
|
|
1577
|
-
return related();
|
|
1578
|
-
}
|
|
1579
|
-
return related;
|
|
1580
|
-
}
|
|
1581
|
-
function inferRelationMethodName(callee) {
|
|
1582
|
-
const stack = new Error().stack ?? "";
|
|
1583
|
-
let seenCallee = false;
|
|
1584
|
-
for (const line of stack.split(`
|
|
1585
|
-
`)) {
|
|
1586
|
-
const match = /at (?:async )?(?:[^.\s]+\.)?(\w+)/.exec(line);
|
|
1587
|
-
const name = match?.[1];
|
|
1588
|
-
if (!name || name === "Error" || name === "inferRelationMethodName") {
|
|
1589
|
-
continue;
|
|
1590
|
-
}
|
|
1591
|
-
if (!seenCallee) {
|
|
1592
|
-
if (name === callee) {
|
|
1593
|
-
seenCallee = true;
|
|
1594
|
-
}
|
|
1595
|
-
continue;
|
|
1596
|
-
}
|
|
1597
|
-
if (name !== callee) {
|
|
1598
|
-
return name;
|
|
1599
|
-
}
|
|
1600
|
-
}
|
|
1601
|
-
return;
|
|
1602
|
-
}
|
|
1603
|
-
function morphClassOf(model) {
|
|
1604
|
-
const statics = modelStatics(model.constructor === Function ? model : model.constructor);
|
|
1605
|
-
return statics.$morphClass ?? (model.constructor === Function ? model.name : model.constructor.name);
|
|
1606
|
-
}
|
|
1607
|
-
function modelStatics(model) {
|
|
1608
|
-
return model;
|
|
1609
|
-
}
|
|
1610
|
-
function ensureBooted(model) {
|
|
1611
|
-
if (modelBooted.has(model)) {
|
|
1612
|
-
return;
|
|
1613
|
-
}
|
|
1614
|
-
modelBooted.add(model);
|
|
1615
|
-
const boot = model.boot;
|
|
1616
|
-
if (typeof boot === "function") {
|
|
1617
|
-
boot.call(model);
|
|
1618
|
-
}
|
|
1619
|
-
}
|
|
1620
|
-
function getGlobalScopes(model) {
|
|
1621
|
-
return modelGlobalScopes.get(model) ?? [];
|
|
1622
|
-
}
|
|
1623
|
-
var PASSWORD_HASH_PATTERN = /^\$(?:2[aby]?|argon2(?:i|d|id)?)\$/;
|
|
1624
|
-
function isAlreadyHashed(value) {
|
|
1625
|
-
return PASSWORD_HASH_PATTERN.test(value);
|
|
1626
|
-
}
|
|
1627
|
-
function hashCastValue(value) {
|
|
1628
|
-
const plain = String(value);
|
|
1629
|
-
if (isAlreadyHashed(plain)) {
|
|
1630
|
-
return plain;
|
|
1631
|
-
}
|
|
1632
|
-
return Bun.password.hashSync(plain, { algorithm: "bcrypt", cost: 12 });
|
|
1633
|
-
}
|
|
1634
|
-
function hydrateValue(value, cast) {
|
|
1635
|
-
if (value === null || value === undefined) {
|
|
1636
|
-
return value;
|
|
1637
|
-
}
|
|
1638
|
-
switch (cast) {
|
|
1639
|
-
case "date":
|
|
1640
|
-
case "datetime":
|
|
1641
|
-
return value instanceof Date ? value : new Date(String(value));
|
|
1642
|
-
case "json":
|
|
1643
|
-
return typeof value === "string" ? JSON.parse(value) : value;
|
|
1644
|
-
case "bool":
|
|
1645
|
-
case "boolean":
|
|
1646
|
-
return value === true || value === 1 || value === "1" || value === "true";
|
|
1647
|
-
case "integer":
|
|
1648
|
-
case "int":
|
|
1649
|
-
return value === "" ? null : Number(value);
|
|
1650
|
-
case "hashed":
|
|
1651
|
-
return value;
|
|
1652
|
-
default:
|
|
1653
|
-
return value;
|
|
1654
|
-
}
|
|
1655
|
-
}
|
|
1656
|
-
function dehydrateValue(value, cast) {
|
|
1657
|
-
if (value === null || value === undefined) {
|
|
1658
|
-
return value;
|
|
1659
|
-
}
|
|
1660
|
-
switch (cast) {
|
|
1661
|
-
case "date":
|
|
1662
|
-
case "datetime":
|
|
1663
|
-
return value instanceof Date ? value : new Date(String(value));
|
|
1664
|
-
case "json":
|
|
1665
|
-
return typeof value === "string" ? value : JSON.stringify(value);
|
|
1666
|
-
case "bool":
|
|
1667
|
-
case "boolean":
|
|
1668
|
-
return Boolean(value);
|
|
1669
|
-
case "integer":
|
|
1670
|
-
case "int":
|
|
1671
|
-
return value === "" ? null : Number(value);
|
|
1672
|
-
case "hashed":
|
|
1673
|
-
return hashCastValue(value);
|
|
1674
|
-
default:
|
|
1675
|
-
return value;
|
|
1676
|
-
}
|
|
1677
|
-
}
|
|
1678
|
-
function filterMassAssignable(fillable, guarded, input) {
|
|
1679
|
-
if (fillable === undefined && guarded === undefined && Object.keys(input).length > 0) {
|
|
1680
|
-
throw new Error("Mass assignment is not configured for this model. Declare static $fillable = [...] to allow specific columns, or static $guarded = [] to allow all of them.");
|
|
1681
|
-
}
|
|
1682
|
-
const resolvedGuarded = guarded ?? true;
|
|
1683
|
-
if (fillable && fillable.length > 0) {
|
|
1684
|
-
const allowed = new Set(fillable);
|
|
1685
|
-
return Object.fromEntries(Object.entries(input).filter(([key]) => allowed.has(key)));
|
|
1686
|
-
}
|
|
1687
|
-
if (resolvedGuarded === true || resolvedGuarded.includes("*")) {
|
|
1688
|
-
return {};
|
|
1689
|
-
}
|
|
1690
|
-
const blocked = new Set(resolvedGuarded);
|
|
1691
|
-
return Object.fromEntries(Object.entries(input).filter(([key]) => !blocked.has(key)));
|
|
1692
|
-
}
|
|
1693
|
-
function applyCasts(values, casts, direction) {
|
|
1694
|
-
if (Object.keys(casts).length === 0) {
|
|
1695
|
-
return values;
|
|
1696
|
-
}
|
|
1697
|
-
const result = { ...values };
|
|
1698
|
-
const castFn = direction === "hydrate" ? hydrateValue : dehydrateValue;
|
|
1699
|
-
for (const [key, cast] of Object.entries(casts)) {
|
|
1700
|
-
if (key in result && cast) {
|
|
1701
|
-
result[key] = castFn(result[key], cast);
|
|
1702
|
-
}
|
|
1703
|
-
}
|
|
1704
|
-
return result;
|
|
1705
|
-
}
|
|
1706
|
-
function castPluckedValue(modelClass, column, value) {
|
|
1707
|
-
const cast = modelStatics(modelClass).$casts?.[column];
|
|
1708
|
-
return cast ? hydrateValue(value, cast) : value;
|
|
1709
|
-
}
|
|
1710
|
-
function applyTimestampsOnCreate(columns, values, enabled) {
|
|
1711
|
-
if (!enabled) {
|
|
1712
|
-
return values;
|
|
1713
|
-
}
|
|
1714
|
-
const now = new Date;
|
|
1715
|
-
const result = { ...values };
|
|
1716
|
-
if (columns.includes("created_at")) {
|
|
1717
|
-
result.created_at = now;
|
|
1718
|
-
}
|
|
1719
|
-
if (columns.includes("updated_at")) {
|
|
1720
|
-
result.updated_at = now;
|
|
1721
|
-
}
|
|
1722
|
-
return result;
|
|
1723
|
-
}
|
|
1724
|
-
function applyTimestampsOnUpdate(columns, values, enabled) {
|
|
1725
|
-
if (!enabled) {
|
|
1726
|
-
return values;
|
|
1727
|
-
}
|
|
1728
|
-
const result = { ...values };
|
|
1729
|
-
if (columns.includes("updated_at")) {
|
|
1730
|
-
result.updated_at = new Date;
|
|
1731
|
-
}
|
|
1732
|
-
return result;
|
|
1733
|
-
}
|
|
1734
|
-
|
|
1735
|
-
class ModelQuery {
|
|
1736
|
-
modelClass;
|
|
1737
|
-
query;
|
|
1738
|
-
eager = [];
|
|
1739
|
-
constructor(modelClass, query) {
|
|
1740
|
-
this.modelClass = modelClass;
|
|
1741
|
-
this.query = query;
|
|
1742
|
-
}
|
|
1743
|
-
with(...relations) {
|
|
1744
|
-
const statics = modelStatics(this.modelClass);
|
|
1745
|
-
ensureBooted(this.modelClass);
|
|
1746
|
-
const dummy = statics.newFromRecord({}, false);
|
|
1747
|
-
for (const path of flattenRelationNames(relations)) {
|
|
1748
|
-
const name = path.split(".")[0] ?? path;
|
|
1749
|
-
const method = dummy[name];
|
|
1750
|
-
if (typeof method !== "function") {
|
|
1751
|
-
throw new Error(`${this.modelClass.name} has no relation method ${name}().`);
|
|
1752
|
-
}
|
|
1753
|
-
const relationQuery = method.call(dummy);
|
|
1754
|
-
this.eager.push({ name, path, relationQuery });
|
|
1755
|
-
relationQuery.applyEagerLoad(this.query, name);
|
|
1756
|
-
}
|
|
1757
|
-
return this;
|
|
1758
|
-
}
|
|
1759
|
-
where(input) {
|
|
1760
|
-
this.query.where(input);
|
|
1761
|
-
return this;
|
|
1762
|
-
}
|
|
1763
|
-
orWhere(input) {
|
|
1764
|
-
this.query.orWhere(input);
|
|
1765
|
-
return this;
|
|
1766
|
-
}
|
|
1767
|
-
orderBy(orderBy) {
|
|
1768
|
-
this.query.orderBy(orderBy);
|
|
1769
|
-
return this;
|
|
1770
|
-
}
|
|
1771
|
-
limit(limit) {
|
|
1772
|
-
this.query.limit(limit);
|
|
1773
|
-
return this;
|
|
1774
|
-
}
|
|
1775
|
-
offset(offset) {
|
|
1776
|
-
this.query.offset(offset);
|
|
1777
|
-
return this;
|
|
1778
|
-
}
|
|
1779
|
-
whereNull(column) {
|
|
1780
|
-
this.query.whereNull(column);
|
|
1781
|
-
return this;
|
|
1782
|
-
}
|
|
1783
|
-
whereNotNull(column) {
|
|
1784
|
-
this.query.whereNotNull(column);
|
|
1785
|
-
return this;
|
|
1786
|
-
}
|
|
1787
|
-
whereIn(column, values) {
|
|
1788
|
-
this.query.whereIn(column, values);
|
|
1789
|
-
return this;
|
|
1790
|
-
}
|
|
1791
|
-
whereNotIn(column, values) {
|
|
1792
|
-
this.query.whereNotIn(column, values);
|
|
1793
|
-
return this;
|
|
1794
|
-
}
|
|
1795
|
-
groupBy(groupBy) {
|
|
1796
|
-
this.query.groupBy(groupBy);
|
|
1797
|
-
return this;
|
|
1798
|
-
}
|
|
1799
|
-
having(having) {
|
|
1800
|
-
this.query.having(having);
|
|
1801
|
-
return this;
|
|
1802
|
-
}
|
|
1803
|
-
join(left, right) {
|
|
1804
|
-
this.query.join(left, right);
|
|
1805
|
-
return this;
|
|
1806
|
-
}
|
|
1807
|
-
leftJoin(left, right) {
|
|
1808
|
-
this.query.leftJoin(left, right);
|
|
1809
|
-
return this;
|
|
1810
|
-
}
|
|
1811
|
-
async paginate(options) {
|
|
1812
|
-
const { data, meta } = await this.query.paginate(options);
|
|
1813
|
-
return { data: await this.hydrateRows(data), meta };
|
|
1814
|
-
}
|
|
1815
|
-
whereExists(sql, params = []) {
|
|
1816
|
-
this.query.whereExists(sql, params);
|
|
1817
|
-
return this;
|
|
1818
|
-
}
|
|
1819
|
-
whereNotExists(sql, params = []) {
|
|
1820
|
-
this.query.whereNotExists(sql, params);
|
|
1821
|
-
return this;
|
|
1822
|
-
}
|
|
1823
|
-
whereHas(name, constrain) {
|
|
1824
|
-
return this.constrainExists(name, constrain, false);
|
|
1825
|
-
}
|
|
1826
|
-
has(name) {
|
|
1827
|
-
return this.constrainExists(name, undefined, false);
|
|
1828
|
-
}
|
|
1829
|
-
doesntHave(name) {
|
|
1830
|
-
return this.constrainExists(name, undefined, true);
|
|
1831
|
-
}
|
|
1832
|
-
whereDoesntHave(name, constrain) {
|
|
1833
|
-
return this.constrainExists(name, constrain, true);
|
|
1834
|
-
}
|
|
1835
|
-
withHasMany(...args) {
|
|
1836
|
-
this.query.withHasMany(...args);
|
|
1837
|
-
return this;
|
|
1838
|
-
}
|
|
1839
|
-
withBelongsTo(...args) {
|
|
1840
|
-
this.query.withBelongsTo(...args);
|
|
1841
|
-
return this;
|
|
1842
|
-
}
|
|
1843
|
-
withBelongsToMany(...args) {
|
|
1844
|
-
this.query.withBelongsToMany(...args);
|
|
1845
|
-
return this;
|
|
1846
|
-
}
|
|
1847
|
-
withMorphMany(...args) {
|
|
1848
|
-
this.query.withMorphMany(...args);
|
|
1849
|
-
return this;
|
|
1850
|
-
}
|
|
1851
|
-
withMorphOne(...args) {
|
|
1852
|
-
this.query.withMorphOne(...args);
|
|
1853
|
-
return this;
|
|
1854
|
-
}
|
|
1855
|
-
withMorphTo(...args) {
|
|
1856
|
-
this.query.withMorphTo(...args);
|
|
1857
|
-
return this;
|
|
1858
|
-
}
|
|
1859
|
-
withHasManyThrough(...args) {
|
|
1860
|
-
this.query.withHasManyThrough(...args);
|
|
1861
|
-
return this;
|
|
1862
|
-
}
|
|
1863
|
-
withTrashed() {
|
|
1864
|
-
this.query.withTrashed();
|
|
1865
|
-
return this;
|
|
1866
|
-
}
|
|
1867
|
-
onlyTrashed() {
|
|
1868
|
-
this.query.onlyTrashed();
|
|
1869
|
-
return this;
|
|
1870
|
-
}
|
|
1871
|
-
async get() {
|
|
1872
|
-
return await this.hydrateRows(await this.query.get());
|
|
1873
|
-
}
|
|
1874
|
-
async hydrateRows(rows) {
|
|
1875
|
-
const statics = modelStatics(this.modelClass);
|
|
1876
|
-
const models = [];
|
|
1877
|
-
for (const row of rows) {
|
|
1878
|
-
const model = statics.newFromRecord(row, true);
|
|
1879
|
-
for (const { name, relationQuery } of this.eager) {
|
|
1880
|
-
model.setLoaded(name, relationQuery.hydrateEager(row, name));
|
|
1881
|
-
}
|
|
1882
|
-
models.push(model);
|
|
1883
|
-
}
|
|
1884
|
-
await Promise.all(models.map((model) => runObservers(model, "retrieved")));
|
|
1885
|
-
const nested = this.eager.filter((item) => item.path.includes(".")).map((item) => item.path);
|
|
1886
|
-
await eagerLoadOnModels(models.filter(isLoadableModel), nested);
|
|
1887
|
-
return models;
|
|
1888
|
-
}
|
|
1889
|
-
async first() {
|
|
1890
|
-
this.query.limit(1);
|
|
1891
|
-
const models = await this.get();
|
|
1892
|
-
return models[0] ?? null;
|
|
1893
|
-
}
|
|
1894
|
-
async count() {
|
|
1895
|
-
return this.query.count();
|
|
1896
|
-
}
|
|
1897
|
-
async pluck(column, keyBy) {
|
|
1898
|
-
if (keyBy === undefined) {
|
|
1899
|
-
const values = await this.query.pluck(column);
|
|
1900
|
-
return values.map((value) => castPluckedValue(this.modelClass, column, value));
|
|
1901
|
-
}
|
|
1902
|
-
const keyed = await this.query.pluck(column, keyBy);
|
|
1903
|
-
const result = new Map;
|
|
1904
|
-
for (const [key, value] of keyed) {
|
|
1905
|
-
result.set(castPluckedValue(this.modelClass, keyBy, key), castPluckedValue(this.modelClass, column, value));
|
|
1906
|
-
}
|
|
1907
|
-
return result;
|
|
1908
|
-
}
|
|
1909
|
-
async value(column) {
|
|
1910
|
-
const value = await this.query.value(column);
|
|
1911
|
-
return castPluckedValue(this.modelClass, column, value);
|
|
1912
|
-
}
|
|
1913
|
-
async find(id) {
|
|
1914
|
-
const primaryKey = resolveModelRepository(this.modelClass).getTable().primaryKey;
|
|
1915
|
-
return this.where({ [primaryKey]: id }).first();
|
|
1916
|
-
}
|
|
1917
|
-
async findOrFail(id, errorFactory) {
|
|
1918
|
-
const model = await this.find(id);
|
|
1919
|
-
if (model) {
|
|
1920
|
-
return model;
|
|
1921
|
-
}
|
|
1922
|
-
throw errorFactory?.(id) ?? new NotFoundError(`${this.modelClass.name} ${String(id)} not found.`);
|
|
1923
|
-
}
|
|
1924
|
-
then(onfulfilled, onrejected) {
|
|
1925
|
-
return this.get().then(onfulfilled ?? undefined, onrejected ?? undefined);
|
|
1926
|
-
}
|
|
1927
|
-
withCount(name, alias = `${name}_count`) {
|
|
1928
|
-
const statics = modelStatics(this.modelClass);
|
|
1929
|
-
ensureBooted(this.modelClass);
|
|
1930
|
-
const repository = resolveModelRepository(this.modelClass);
|
|
1931
|
-
const dummy = statics.newFromRecord({});
|
|
1932
|
-
const method = dummy[name];
|
|
1933
|
-
if (typeof method !== "function") {
|
|
1934
|
-
throw new Error(`${this.modelClass.name} has no relation method ${name}().`);
|
|
1935
|
-
}
|
|
1936
|
-
const relationQuery = method.call(dummy);
|
|
1937
|
-
const exists = relationQuery.toExistsClause(repository.getTable().name);
|
|
1938
|
-
if (!exists.sql.startsWith("SELECT 1 ")) {
|
|
1939
|
-
throw new Error(`Cannot count relation ${name}: unexpected subquery shape.`);
|
|
1940
|
-
}
|
|
1941
|
-
this.query.withSubqueryCount(alias, `SELECT COUNT(*) ${exists.sql.slice("SELECT 1 ".length)}`, exists.params);
|
|
1942
|
-
return this;
|
|
1943
|
-
}
|
|
1944
|
-
constrainExists(name, constrain, not) {
|
|
1945
|
-
const statics = modelStatics(this.modelClass);
|
|
1946
|
-
ensureBooted(this.modelClass);
|
|
1947
|
-
const repository = resolveModelRepository(this.modelClass);
|
|
1948
|
-
const dummy = statics.newFromRecord({});
|
|
1949
|
-
const method = dummy[name];
|
|
1950
|
-
if (typeof method !== "function") {
|
|
1951
|
-
throw new Error(`${this.modelClass.name} has no relation method ${name}().`);
|
|
1952
|
-
}
|
|
1953
|
-
const relationQuery = method.call(dummy);
|
|
1954
|
-
constrain?.(relationQuery);
|
|
1955
|
-
const exists = relationQuery.toExistsClause(repository.getTable().name);
|
|
1956
|
-
return not ? this.whereNotExists(exists.sql, exists.params) : this.whereExists(exists.sql, exists.params);
|
|
1957
|
-
}
|
|
1958
|
-
}
|
|
1959
|
-
|
|
1960
|
-
class Model {
|
|
1961
|
-
attributes;
|
|
1962
|
-
repository;
|
|
1963
|
-
static $fillable;
|
|
1964
|
-
static $guarded;
|
|
1965
|
-
static $casts = {};
|
|
1966
|
-
static $timestamps = true;
|
|
1967
|
-
static $hidden;
|
|
1968
|
-
static $visible;
|
|
1969
|
-
static $appends;
|
|
1970
|
-
static $morphClass;
|
|
1971
|
-
_exists;
|
|
1972
|
-
loadedRelations = {};
|
|
1973
|
-
hiddenOverrides = [];
|
|
1974
|
-
visibleOverrides = [];
|
|
1975
|
-
appended = [];
|
|
1976
|
-
constructor(attributes, repository, exists = true) {
|
|
1977
|
-
this.attributes = attributes;
|
|
1978
|
-
this.repository = repository;
|
|
1979
|
-
this._exists = exists;
|
|
1980
|
-
this.attributes = modelStatics(this.constructor).hydrateAttributes(attributes);
|
|
1981
|
-
}
|
|
1982
|
-
getRepository() {
|
|
1983
|
-
return this.repository;
|
|
1984
|
-
}
|
|
1985
|
-
get $exists() {
|
|
1986
|
-
return this._exists;
|
|
1987
|
-
}
|
|
1988
|
-
get(key) {
|
|
1989
|
-
return this.attributes[key];
|
|
1990
|
-
}
|
|
1991
|
-
get id() {
|
|
1992
|
-
return this.attributes[this.primaryKey()];
|
|
1993
|
-
}
|
|
1994
|
-
toObject() {
|
|
1995
|
-
return { ...this.attributes };
|
|
1996
|
-
}
|
|
1997
|
-
toArray() {
|
|
1998
|
-
const ModelClass = modelStatics(this.constructor);
|
|
1999
|
-
const hidden = new Set([...ModelClass.$hidden ?? [], ...this.hiddenOverrides]);
|
|
2000
|
-
const visible = this.visibleOverrides.length > 0 ? this.visibleOverrides : ModelClass.$visible;
|
|
2001
|
-
const data = { ...this.attributes };
|
|
2002
|
-
if (visible && visible.length > 0) {
|
|
2003
|
-
for (const key of Object.keys(data)) {
|
|
2004
|
-
if (!visible.includes(key)) {
|
|
2005
|
-
delete data[key];
|
|
2006
|
-
}
|
|
2007
|
-
}
|
|
2008
|
-
}
|
|
2009
|
-
for (const key of hidden) {
|
|
2010
|
-
delete data[key];
|
|
2011
|
-
}
|
|
2012
|
-
for (const key of [...ModelClass.$appends ?? [], ...this.appended]) {
|
|
2013
|
-
const accessor = this[accessorName(key)];
|
|
2014
|
-
if (typeof accessor === "function") {
|
|
2015
|
-
data[key] = accessor.call(this);
|
|
2016
|
-
}
|
|
2017
|
-
}
|
|
2018
|
-
for (const [name, value] of Object.entries(this.loadedRelations)) {
|
|
2019
|
-
if (!hidden.has(name) && (!visible || visible.includes(name) || this.appended.includes(name))) {
|
|
2020
|
-
data[name] = value;
|
|
2021
|
-
}
|
|
2022
|
-
}
|
|
2023
|
-
return data;
|
|
2024
|
-
}
|
|
2025
|
-
toJSON() {
|
|
2026
|
-
return this.toArray();
|
|
2027
|
-
}
|
|
2028
|
-
makeHidden(...keys) {
|
|
2029
|
-
this.hiddenOverrides.push(...keys);
|
|
2030
|
-
return this;
|
|
2031
|
-
}
|
|
2032
|
-
makeVisible(...keys) {
|
|
2033
|
-
this.visibleOverrides.push(...keys);
|
|
2034
|
-
return this;
|
|
2035
|
-
}
|
|
2036
|
-
append(...keys) {
|
|
2037
|
-
this.appended.push(...keys);
|
|
2038
|
-
return this;
|
|
2039
|
-
}
|
|
2040
|
-
primaryKey() {
|
|
2041
|
-
return this.repository.getTable().primaryKey;
|
|
2042
|
-
}
|
|
2043
|
-
static primaryKeyField() {
|
|
2044
|
-
return resolveModelRepository(this).getTable().primaryKey;
|
|
2045
|
-
}
|
|
2046
|
-
static hydrateAttributes(attributes) {
|
|
2047
|
-
const casts = modelStatics(this).$casts ?? {};
|
|
2048
|
-
return applyCasts(attributes, casts, "hydrate");
|
|
2049
|
-
}
|
|
2050
|
-
static dehydrateAttributes(attributes) {
|
|
2051
|
-
const casts = modelStatics(this).$casts ?? {};
|
|
2052
|
-
return applyCasts(attributes, casts, "dehydrate");
|
|
2053
|
-
}
|
|
2054
|
-
static fromRecord(record, repository, exists = true) {
|
|
2055
|
-
const statics = modelStatics(this);
|
|
2056
|
-
const hydrated = statics.hydrateAttributes(record);
|
|
2057
|
-
return new statics(hydrated, repository, exists);
|
|
2058
|
-
}
|
|
2059
|
-
static boot() {}
|
|
2060
|
-
static observe(observer) {
|
|
2061
|
-
const existing = modelObservers.get(this) ?? [];
|
|
2062
|
-
modelObservers.set(this, [...existing, observer]);
|
|
2063
|
-
}
|
|
2064
|
-
static addGlobalScope(_name, scope) {
|
|
2065
|
-
ensureBooted(this);
|
|
2066
|
-
const existing = modelGlobalScopes.get(this) ?? [];
|
|
2067
|
-
modelGlobalScopes.set(this, [
|
|
2068
|
-
...existing,
|
|
2069
|
-
scope
|
|
2070
|
-
]);
|
|
2071
|
-
}
|
|
2072
|
-
static repository() {
|
|
2073
|
-
return resolveModelRepository(this);
|
|
2074
|
-
}
|
|
2075
|
-
static query() {
|
|
2076
|
-
ensureBooted(this);
|
|
2077
|
-
const repository = resolveModelRepository(this);
|
|
2078
|
-
let query = repository.query();
|
|
2079
|
-
for (const scope of getGlobalScopes(this)) {
|
|
2080
|
-
query = scope(query);
|
|
2081
|
-
}
|
|
2082
|
-
return new ModelQuery(this, query);
|
|
2083
|
-
}
|
|
2084
|
-
static newFromRecord(record, exists = true) {
|
|
2085
|
-
const repository = resolveModelRepository(this);
|
|
2086
|
-
return modelStatics(this).fromRecord(record, repository, exists);
|
|
2087
|
-
}
|
|
2088
|
-
static async create(attributes, forced = {}) {
|
|
2089
|
-
const statics = modelStatics(this);
|
|
2090
|
-
ensureBooted(this);
|
|
2091
|
-
const repository = resolveModelRepository(this);
|
|
2092
|
-
const table = repository.getTable();
|
|
2093
|
-
const timestamps = statics.$timestamps ?? true;
|
|
2094
|
-
const assignable = {
|
|
2095
|
-
...filterMassAssignable(statics.$fillable, statics.$guarded, attributes),
|
|
2096
|
-
...forced
|
|
2097
|
-
};
|
|
2098
|
-
const withTimestamps = applyTimestampsOnCreate(table.columns, assignable, timestamps);
|
|
2099
|
-
const payload = statics.dehydrateAttributes(withTimestamps);
|
|
2100
|
-
const pending = statics.newFromRecord({ ...payload }, false);
|
|
2101
|
-
if (await runObservers(pending, "saving") === false) {
|
|
2102
|
-
throw new Error(`${this.name}.create() was cancelled by an observer.`);
|
|
2103
|
-
}
|
|
2104
|
-
if (await runObservers(pending, "creating") === false) {
|
|
2105
|
-
throw new Error(`${this.name}.create() was cancelled by an observer.`);
|
|
2106
|
-
}
|
|
2107
|
-
const record = await repository.create(payload);
|
|
2108
|
-
const created = statics.fromRecord(record, repository, true);
|
|
2109
|
-
await runObservers(created, "created");
|
|
2110
|
-
await runObservers(created, "saved");
|
|
2111
|
-
return created;
|
|
2112
|
-
}
|
|
2113
|
-
static with(...relations) {
|
|
2114
|
-
return Model.query.call(this).with(...relations);
|
|
2115
|
-
}
|
|
2116
|
-
static withTrashed() {
|
|
2117
|
-
return Model.query.call(this).withTrashed();
|
|
2118
|
-
}
|
|
2119
|
-
static onlyTrashed() {
|
|
2120
|
-
return Model.query.call(this).onlyTrashed();
|
|
2121
|
-
}
|
|
2122
|
-
static async chunk(count, callback) {
|
|
2123
|
-
const statics = modelStatics(this);
|
|
2124
|
-
ensureBooted(this);
|
|
2125
|
-
await resolveModelRepository(this).chunk(count, async (rows) => {
|
|
2126
|
-
return await callback(rows.map((row) => statics.newFromRecord(row, true)));
|
|
2127
|
-
});
|
|
2128
|
-
}
|
|
2129
|
-
static async cursorPaginate(options) {
|
|
2130
|
-
const statics = modelStatics(this);
|
|
2131
|
-
ensureBooted(this);
|
|
2132
|
-
const page = await resolveModelRepository(this).cursorPaginate({
|
|
2133
|
-
perPage: options.perPage,
|
|
2134
|
-
cursor: options.cursor
|
|
2135
|
-
});
|
|
2136
|
-
return {
|
|
2137
|
-
data: page.data.map((row) => statics.newFromRecord(row, true)),
|
|
2138
|
-
meta: page.meta
|
|
2139
|
-
};
|
|
2140
|
-
}
|
|
2141
|
-
static whereHas(name, constrain) {
|
|
2142
|
-
return Model.query.call(this).whereHas(name, constrain);
|
|
2143
|
-
}
|
|
2144
|
-
static has(name) {
|
|
2145
|
-
return Model.query.call(this).has(name);
|
|
2146
|
-
}
|
|
2147
|
-
static doesntHave(name) {
|
|
2148
|
-
return Model.query.call(this).doesntHave(name);
|
|
2149
|
-
}
|
|
2150
|
-
static whereDoesntHave(name, constrain) {
|
|
2151
|
-
return Model.query.call(this).whereDoesntHave(name, constrain);
|
|
2152
|
-
}
|
|
2153
|
-
static async find(id) {
|
|
2154
|
-
const primaryKey = resolveModelRepository(this).getTable().primaryKey;
|
|
2155
|
-
return Model.query.call(this).where({ [primaryKey]: id }).first();
|
|
2156
|
-
}
|
|
2157
|
-
static async findOrFail(id, errorFactory) {
|
|
2158
|
-
const model = await Model.find.call(this, id);
|
|
2159
|
-
if (model) {
|
|
2160
|
-
return model;
|
|
2161
|
-
}
|
|
2162
|
-
throw errorFactory?.(id) ?? new NotFoundError(`${this.name} ${String(id)} not found.`);
|
|
2163
|
-
}
|
|
2164
|
-
static async all(options = {}) {
|
|
2165
|
-
let query = Model.query.call(this);
|
|
2166
|
-
if (options.orderBy) {
|
|
2167
|
-
query = query.orderBy(options.orderBy);
|
|
2168
|
-
}
|
|
2169
|
-
if (options.limit !== undefined) {
|
|
2170
|
-
query = query.limit(options.limit);
|
|
2171
|
-
}
|
|
2172
|
-
return query.get();
|
|
2173
|
-
}
|
|
2174
|
-
static where(where) {
|
|
2175
|
-
return Model.query.call(this).where(where);
|
|
2176
|
-
}
|
|
2177
|
-
static async count() {
|
|
2178
|
-
return Model.query.call(this).count();
|
|
2179
|
-
}
|
|
2180
|
-
static pluck(column, keyBy) {
|
|
2181
|
-
const query = Model.query.call(this);
|
|
2182
|
-
return keyBy === undefined ? query.pluck(column) : query.pluck(column, keyBy);
|
|
2183
|
-
}
|
|
2184
|
-
static value(column) {
|
|
2185
|
-
return Model.query.call(this).value(column);
|
|
2186
|
-
}
|
|
2187
|
-
static async firstWhere(where, options = {}) {
|
|
2188
|
-
let query = Model.query.call(this).where(where);
|
|
2189
|
-
if (options.orderBy) {
|
|
2190
|
-
query = query.orderBy(options.orderBy);
|
|
2191
|
-
}
|
|
2192
|
-
return query.first();
|
|
2193
|
-
}
|
|
2194
|
-
static async firstOrNew(where, values = {}) {
|
|
2195
|
-
const existing = await Model.firstWhere.call(this, where);
|
|
2196
|
-
if (existing) {
|
|
2197
|
-
return existing;
|
|
2198
|
-
}
|
|
2199
|
-
return modelStatics(this).newFromRecord({ ...where, ...values }, false);
|
|
2200
|
-
}
|
|
2201
|
-
static async firstOrCreate(where, values = {}) {
|
|
2202
|
-
const findExisting = () => Model.firstWhere.call(this, where);
|
|
2203
|
-
const existing = await findExisting();
|
|
2204
|
-
if (existing) {
|
|
2205
|
-
return existing;
|
|
2206
|
-
}
|
|
2207
|
-
try {
|
|
2208
|
-
return await Model.create.call(this, { ...where, ...values });
|
|
2209
|
-
} catch (error) {
|
|
2210
|
-
if (!(error instanceof ConflictError)) {
|
|
2211
|
-
throw error;
|
|
2212
|
-
}
|
|
2213
|
-
const raced = await findExisting();
|
|
2214
|
-
if (!raced) {
|
|
2215
|
-
throw error;
|
|
2216
|
-
}
|
|
2217
|
-
return raced;
|
|
2218
|
-
}
|
|
2219
|
-
}
|
|
2220
|
-
static async updateOrCreate(where, values = {}) {
|
|
2221
|
-
const existing = await Model.firstWhere.call(this, where);
|
|
2222
|
-
if (existing) {
|
|
2223
|
-
return existing.update(values);
|
|
2224
|
-
}
|
|
2225
|
-
return Model.create.call(this, { ...where, ...values });
|
|
2226
|
-
}
|
|
2227
|
-
async save() {
|
|
2228
|
-
const ModelClass = modelStatics(this.constructor);
|
|
2229
|
-
const timestamps = ModelClass.$timestamps ?? true;
|
|
2230
|
-
const casts = ModelClass.$casts ?? {};
|
|
2231
|
-
const table = this.repository.getTable();
|
|
2232
|
-
const updating = this.$exists;
|
|
2233
|
-
if (await runObservers(this, "saving") === false) {
|
|
2234
|
-
return this;
|
|
2235
|
-
}
|
|
2236
|
-
if (await runObservers(this, updating ? "updating" : "creating") === false) {
|
|
2237
|
-
return this;
|
|
2238
|
-
}
|
|
2239
|
-
if (updating) {
|
|
2240
|
-
const changes = applyTimestampsOnUpdate(table.columns, applyCasts(this.attributes, casts, "dehydrate"), timestamps);
|
|
2241
|
-
const record = await this.repository.updateByIdOrThrow(this.id, changes);
|
|
2242
|
-
this.attributes = ModelClass.hydrateAttributes(record);
|
|
2243
|
-
await runObservers(this, "updated");
|
|
2244
|
-
await runObservers(this, "saved");
|
|
2245
|
-
return this;
|
|
2246
|
-
}
|
|
2247
|
-
const assignable = filterMassAssignable(ModelClass.$fillable, ModelClass.$guarded, this.attributes);
|
|
2248
|
-
const withTimestamps = applyTimestampsOnCreate(table.columns, assignable, timestamps);
|
|
2249
|
-
const payload = ModelClass.dehydrateAttributes(withTimestamps);
|
|
2250
|
-
const record = await this.repository.create(payload);
|
|
2251
|
-
this.attributes = ModelClass.hydrateAttributes(record);
|
|
2252
|
-
this._exists = true;
|
|
2253
|
-
await runObservers(this, "created");
|
|
2254
|
-
await runObservers(this, "saved");
|
|
2255
|
-
return this;
|
|
2256
|
-
}
|
|
2257
|
-
async update(changes) {
|
|
2258
|
-
const ModelClass = modelStatics(this.constructor);
|
|
2259
|
-
const assignable = filterMassAssignable(ModelClass.$fillable, ModelClass.$guarded, changes);
|
|
2260
|
-
Object.assign(this.attributes, assignable);
|
|
2261
|
-
return await this.save();
|
|
2262
|
-
}
|
|
2263
|
-
async delete() {
|
|
2264
|
-
if (await runObservers(this, "deleting") === false) {
|
|
2265
|
-
return false;
|
|
2266
|
-
}
|
|
2267
|
-
const deleted = resolveSoftDeleteColumn(this.repository.getTable()) ? await this.repository.deleteById(this.id) : await this.repository.forceDeleteById(this.id);
|
|
2268
|
-
if (deleted) {
|
|
2269
|
-
await runObservers(this, "deleted");
|
|
2270
|
-
}
|
|
2271
|
-
return deleted;
|
|
2272
|
-
}
|
|
2273
|
-
async forceDelete() {
|
|
2274
|
-
return await this.repository.forceDeleteById(this.id);
|
|
2275
|
-
}
|
|
2276
|
-
async restore() {
|
|
2277
|
-
const ModelClass = modelStatics(this.constructor);
|
|
2278
|
-
const record = await this.repository.restoreById(this.id);
|
|
2279
|
-
if (!record) {
|
|
2280
|
-
return null;
|
|
2281
|
-
}
|
|
2282
|
-
this.attributes = ModelClass.hydrateAttributes(record);
|
|
2283
|
-
return this;
|
|
2284
|
-
}
|
|
2285
|
-
async loadHasMany(as, relation, childRepository, options = {}) {
|
|
2286
|
-
const grouped = await childRepository.withConnection(this.repository.getConnection()).loadHasManyForParents([this.attributes], relation, options);
|
|
2287
|
-
const loaded = getByRelationKey(grouped, this.attributes[relation.localKey]) ?? [];
|
|
2288
|
-
return Object.assign(this, { [as]: loaded });
|
|
2289
|
-
}
|
|
2290
|
-
async loadHasOne(as, relation, childRepository, options = {}) {
|
|
2291
|
-
const loaded = await this.loadHasMany(as, relation, childRepository, { ...options, limit: 1 });
|
|
2292
|
-
const value = loaded[as]?.[0];
|
|
2293
|
-
return Object.assign(this, { [as]: value });
|
|
2294
|
-
}
|
|
2295
|
-
async loadBelongsTo(as, relation, parentRepository, options = {}) {
|
|
2296
|
-
const grouped = await this.repository.loadBelongsToForParents([this.attributes], relation, parentRepository, options);
|
|
2297
|
-
const loaded = getByRelationKey(grouped, this.attributes[relation.foreignKey]);
|
|
2298
|
-
return Object.assign(this, { [as]: loaded });
|
|
2299
|
-
}
|
|
2300
|
-
async loadBelongsToMany(as, relation, relatedRepository, options = {}) {
|
|
2301
|
-
const connection = this.repository.getConnection();
|
|
2302
|
-
const parentId = this.attributes[relation.parentKey];
|
|
2303
|
-
const pivotRows = await connection.unsafe(`SELECT * FROM ${relation.pivotTable} WHERE ${String(relation.foreignPivotKey)} = $1`, [parentId]);
|
|
2304
|
-
if (pivotRows.length === 0) {
|
|
2305
|
-
return Object.assign(this, { [as]: [] });
|
|
2306
|
-
}
|
|
2307
|
-
const relatedIds = [
|
|
2308
|
-
...new Set(pivotRows.map((row) => row[relation.relatedPivotKey]))
|
|
2309
|
-
];
|
|
2310
|
-
const relatedRows = await relatedRepository.withConnection(connection).findAll({
|
|
2311
|
-
...options,
|
|
2312
|
-
where: {
|
|
2313
|
-
[relation.relatedKey]: relatedIds
|
|
2314
|
-
}
|
|
2315
|
-
});
|
|
2316
|
-
const grouped = indexBelongsToManyRelation([this.attributes], pivotRows, relatedRows, relation);
|
|
2317
|
-
const loaded = getByRelationKey(grouped, parentId) ?? [];
|
|
2318
|
-
return Object.assign(this, { [as]: loaded });
|
|
2319
|
-
}
|
|
2320
|
-
hasMany(related, foreignKey, localKey) {
|
|
2321
|
-
const table = this.repository.getTable();
|
|
2322
|
-
const relatedClass = resolveRelated(related);
|
|
2323
|
-
return new HasManyRelationQuery(this, relatedClass, hasMany({
|
|
2324
|
-
name: relatedClass.repository().getTable().name,
|
|
2325
|
-
localKey: localKey ?? table.primaryKey,
|
|
2326
|
-
foreignKey: foreignKey ?? foreignKeyFromTable(table.name)
|
|
2327
|
-
}));
|
|
2328
|
-
}
|
|
2329
|
-
hasOne(related, foreignKey, localKey) {
|
|
2330
|
-
const table = this.repository.getTable();
|
|
2331
|
-
const relatedClass = resolveRelated(related);
|
|
2332
|
-
return new HasOneRelationQuery(this, relatedClass, hasOne({
|
|
2333
|
-
name: relatedClass.repository().getTable().name,
|
|
2334
|
-
localKey: localKey ?? table.primaryKey,
|
|
2335
|
-
foreignKey: foreignKey ?? foreignKeyFromTable(table.name)
|
|
2336
|
-
}));
|
|
2337
|
-
}
|
|
2338
|
-
belongsTo(related, foreignKey, ownerKey) {
|
|
2339
|
-
const relatedClass = resolveRelated(related);
|
|
2340
|
-
const relatedTable = relatedClass.repository().getTable();
|
|
2341
|
-
return new BelongsToRelationQuery(this, relatedClass, belongsTo({
|
|
2342
|
-
name: relatedTable.name,
|
|
2343
|
-
foreignKey: foreignKey ?? foreignKeyFromTable(relatedTable.name),
|
|
2344
|
-
ownerKey: ownerKey ?? relatedTable.primaryKey
|
|
2345
|
-
}));
|
|
2346
|
-
}
|
|
2347
|
-
hasManyThrough(related, through, firstKey, secondKey, localKey, secondLocalKey) {
|
|
2348
|
-
const table = this.repository.getTable();
|
|
2349
|
-
const relatedClass = resolveRelated(related);
|
|
2350
|
-
const throughClass = resolveRelated(through);
|
|
2351
|
-
const throughTable = throughClass.repository().getTable();
|
|
2352
|
-
return new HasManyThroughRelationQuery(this, relatedClass, hasManyThrough({
|
|
2353
|
-
name: relatedClass.repository().getTable().name,
|
|
2354
|
-
throughTable: throughTable.name,
|
|
2355
|
-
localKey: localKey ?? table.primaryKey,
|
|
2356
|
-
firstKey: firstKey ?? foreignKeyFromTable(table.name),
|
|
2357
|
-
secondLocalKey: secondLocalKey ?? throughTable.primaryKey,
|
|
2358
|
-
secondKey: secondKey ?? foreignKeyFromTable(throughTable.name)
|
|
2359
|
-
}));
|
|
2360
|
-
}
|
|
2361
|
-
belongsToMany(related, pivotTable, foreignPivotKey, relatedPivotKey) {
|
|
2362
|
-
const table = this.repository.getTable();
|
|
2363
|
-
const relatedClass = resolveRelated(related);
|
|
2364
|
-
const relatedTable = relatedClass.repository().getTable();
|
|
2365
|
-
return new BelongsToManyRelationQuery(this, relatedClass, belongsToMany({
|
|
2366
|
-
name: relatedTable.name,
|
|
2367
|
-
pivotTable: pivotTable ?? pivotTableName(table.name, relatedTable.name),
|
|
2368
|
-
parentKey: table.primaryKey,
|
|
2369
|
-
relatedKey: relatedTable.primaryKey,
|
|
2370
|
-
foreignPivotKey: foreignPivotKey ?? foreignKeyFromTable(table.name),
|
|
2371
|
-
relatedPivotKey: relatedPivotKey ?? foreignKeyFromTable(relatedTable.name)
|
|
2372
|
-
}));
|
|
2373
|
-
}
|
|
2374
|
-
morphMany(related, morphName, typeKey, idKey, morphType) {
|
|
2375
|
-
const table = this.repository.getTable();
|
|
2376
|
-
const relatedClass = resolveRelated(related);
|
|
2377
|
-
return new MorphManyRelationQuery(this, relatedClass, morphMany({
|
|
2378
|
-
name: morphName,
|
|
2379
|
-
localKey: table.primaryKey,
|
|
2380
|
-
morphTypeKey: typeKey ?? `${morphName}_type`,
|
|
2381
|
-
morphIdKey: idKey ?? `${morphName}_id`,
|
|
2382
|
-
morphType: morphType ?? morphClassOf(this)
|
|
2383
|
-
}));
|
|
2384
|
-
}
|
|
2385
|
-
morphOne(related, morphName, typeKey, idKey, morphType) {
|
|
2386
|
-
const table = this.repository.getTable();
|
|
2387
|
-
const relatedClass = resolveRelated(related);
|
|
2388
|
-
return new MorphOneRelationQuery(this, relatedClass, morphOne({
|
|
2389
|
-
name: morphName,
|
|
2390
|
-
localKey: table.primaryKey,
|
|
2391
|
-
morphTypeKey: typeKey ?? `${morphName}_type`,
|
|
2392
|
-
morphIdKey: idKey ?? `${morphName}_id`,
|
|
2393
|
-
morphType: morphType ?? morphClassOf(this)
|
|
2394
|
-
}));
|
|
2395
|
-
}
|
|
2396
|
-
morphTo(relatedByType, morphName, typeKey, idKey) {
|
|
2397
|
-
const resolvedName = morphName ?? inferRelationMethodName("morphTo");
|
|
2398
|
-
if (!resolvedName) {
|
|
2399
|
-
throw new Error(`${this.constructor.name}.morphTo() needs an explicit morph name (the calling method name is not available at runtime).`);
|
|
2400
|
-
}
|
|
2401
|
-
const resolvedMap = Object.fromEntries(Object.entries(relatedByType).map(([type, related]) => [type, resolveRelated(related)]));
|
|
2402
|
-
return new MorphToRelationQuery(this, resolvedMap, morphTo({
|
|
2403
|
-
name: resolvedName,
|
|
2404
|
-
morphTypeKey: typeKey ?? `${resolvedName}_type`,
|
|
2405
|
-
morphIdKey: idKey ?? `${resolvedName}_id`
|
|
2406
|
-
}));
|
|
2407
|
-
}
|
|
2408
|
-
async load(...names) {
|
|
2409
|
-
for (const name of flattenRelationNames(names)) {
|
|
2410
|
-
if (name.includes(".")) {
|
|
2411
|
-
await loadNested(this, name);
|
|
2412
|
-
continue;
|
|
2413
|
-
}
|
|
2414
|
-
const method = this[name];
|
|
2415
|
-
if (typeof method !== "function") {
|
|
2416
|
-
throw new Error(`${this.constructor.name} has no relation method ${name}().`);
|
|
2417
|
-
}
|
|
2418
|
-
const relationQuery = method.call(this);
|
|
2419
|
-
this.loadedRelations[name] = await relationQuery.get();
|
|
2420
|
-
}
|
|
2421
|
-
return this;
|
|
2422
|
-
}
|
|
2423
|
-
loaded(name) {
|
|
2424
|
-
return this.loadedRelations[name];
|
|
2425
|
-
}
|
|
2426
|
-
setLoaded(name, value) {
|
|
2427
|
-
this.loadedRelations[name] = value;
|
|
2428
|
-
return this;
|
|
2429
|
-
}
|
|
2430
|
-
mergeAttributes(patch) {
|
|
2431
|
-
Object.assign(this.attributes, patch);
|
|
2432
|
-
return this;
|
|
2433
|
-
}
|
|
2434
|
-
}
|
|
2435
|
-
function registerModelRepository(model, repository) {
|
|
2436
|
-
modelRepositories.set(model, repository);
|
|
2437
|
-
const name = model.name;
|
|
2438
|
-
if (name) {
|
|
2439
|
-
namedModels.set(name, model);
|
|
2440
|
-
}
|
|
2441
|
-
const morphClass = model.$morphClass;
|
|
2442
|
-
if (morphClass) {
|
|
2443
|
-
namedModels.set(morphClass, model);
|
|
2444
|
-
}
|
|
2445
|
-
ensureBooted(model);
|
|
2446
|
-
return model;
|
|
2447
|
-
}
|
|
2448
|
-
export {
|
|
2449
|
-
BelongsToManyRelationQuery,
|
|
2450
|
-
BelongsToRelationQuery,
|
|
2451
|
-
HasManyRelationQuery,
|
|
2452
|
-
HasManyThroughRelationQuery,
|
|
2453
|
-
HasOneRelationQuery,
|
|
2454
|
-
Model,
|
|
2455
|
-
ModelQuery,
|
|
2456
|
-
MorphManyRelationQuery,
|
|
2457
|
-
MorphOneRelationQuery,
|
|
2458
|
-
MorphToRelationQuery,
|
|
2459
|
-
applyCasts,
|
|
2460
|
-
dehydrateValue,
|
|
2461
|
-
filterMassAssignable,
|
|
2462
|
-
hydrateValue,
|
|
2463
|
-
registerModelClass,
|
|
2464
|
-
registerModelRepository
|
|
2465
|
-
};
|
|
1
|
+
export * from "../../index.js";
|