@getstrata/core 0.5.41 → 0.5.42
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/core/database/baseRepository.d.ts +1 -0
- package/dist/core/queue/failedJobRepository.d.ts +1 -0
- package/dist/entries/audit/exportAuditLogs.js +18 -0
- package/dist/entries/auth/sessionGuard.js +443 -0
- package/dist/entries/database/baseRepository.js +1388 -0
- package/dist/entries/database/bindConnection.js +22 -0
- package/dist/entries/database/boundConnection.js +19 -0
- package/dist/entries/database/connection.js +12 -0
- package/dist/entries/database/errors.js +128 -0
- package/dist/entries/database/model.js +948 -0
- package/dist/entries/database/query.js +436 -0
- package/dist/entries/database/relationships.js +162 -0
- package/dist/entries/database/table.js +8 -0
- package/dist/entries/database/transaction.js +129 -0
- package/dist/entries/http/authMiddleware.js +47 -0
- package/dist/entries/http/authorizeMiddleware.js +104 -0
- package/dist/entries/http/metricsMiddleware.js +91 -0
- package/dist/entries/http/parseMultipartUpload.js +144 -0
- package/dist/entries/http/securedRouteModelBinding.js +6 -0
- package/dist/entries/http/webErrorResponse.js +443 -0
- package/dist/entries/http/webFormRequest.js +6 -0
- package/dist/entries/jobs/dispatchWebhookJob.js +18 -0
- package/dist/entries/queue/createAppQueue.js +449 -0
- package/dist/entries/queue/failedJobRepository.js +2306 -0
- package/dist/entries/queue/publicQueue.js +449 -0
- package/dist/entries/queue/queueMetrics.js +449 -0
- package/dist/entries/queue/redisQueue.js +232 -0
- package/dist/entries/security/scimTenantTokens.js +51 -0
- package/dist/entries/tenant/tenantDatabaseScope.js +113 -0
- package/dist/entries/view.js +443 -0
- package/package.json +92 -2
|
@@ -0,0 +1,948 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// ../../src/core/errors/http.ts
|
|
3
|
+
class HttpError extends Error {
|
|
4
|
+
status;
|
|
5
|
+
details;
|
|
6
|
+
constructor(status, message, details) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.name = new.target.name;
|
|
9
|
+
this.status = status;
|
|
10
|
+
this.details = details;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
class BadRequestError extends HttpError {
|
|
15
|
+
constructor(message = "Bad Request", details) {
|
|
16
|
+
super(400, message, details);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
class NotFoundError extends HttpError {
|
|
21
|
+
constructor(message = "Not Found", details) {
|
|
22
|
+
super(404, message, details);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
class ConflictError extends HttpError {
|
|
27
|
+
constructor(message = "Conflict", details) {
|
|
28
|
+
super(409, message, details);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
class UnprocessableEntityError extends HttpError {
|
|
33
|
+
constructor(message = "Unprocessable Entity", details) {
|
|
34
|
+
super(422, message, details);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
class ValidationError extends HttpError {
|
|
39
|
+
constructor(message = "Validation failed", details) {
|
|
40
|
+
super(422, message, details);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
class ForbiddenError extends HttpError {
|
|
45
|
+
constructor(message = "Forbidden", details) {
|
|
46
|
+
super(403, message, details);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
class UnauthorizedError extends HttpError {
|
|
51
|
+
constructor(message = "Unauthorized", details) {
|
|
52
|
+
super(401, message, details);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
class PayloadTooLargeError extends HttpError {
|
|
57
|
+
constructor(message = "Payload Too Large", details) {
|
|
58
|
+
super(413, message, details);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
class PreconditionFailedError extends HttpError {
|
|
63
|
+
constructor(message = "Precondition Failed", details) {
|
|
64
|
+
super(412, message, details);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ../../src/core/database/query.ts
|
|
69
|
+
function quoteIdentifier(identifier) {
|
|
70
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(identifier)) {
|
|
71
|
+
throw new Error(`Invalid SQL identifier: ${identifier}`);
|
|
72
|
+
}
|
|
73
|
+
return `"${identifier}"`;
|
|
74
|
+
}
|
|
75
|
+
function qualifyColumn(tableName, column) {
|
|
76
|
+
return `${quoteIdentifier(tableName)}.${quoteIdentifier(column)}`;
|
|
77
|
+
}
|
|
78
|
+
function resolveQualifiedColumn(defaultTable, columnName) {
|
|
79
|
+
if (columnName.includes(".")) {
|
|
80
|
+
const [table, column] = columnName.split(".", 2);
|
|
81
|
+
if (!table || !column) {
|
|
82
|
+
throw new Error(`Invalid qualified column: ${columnName}`);
|
|
83
|
+
}
|
|
84
|
+
return qualifyColumn(table, column);
|
|
85
|
+
}
|
|
86
|
+
return qualifyColumn(defaultTable, columnName);
|
|
87
|
+
}
|
|
88
|
+
function parseQualifiedColumn(reference) {
|
|
89
|
+
const [table, column] = reference.split(".", 2);
|
|
90
|
+
if (!table || !column) {
|
|
91
|
+
throw new Error(`Join columns must be qualified as table.column: ${reference}`);
|
|
92
|
+
}
|
|
93
|
+
return { table, column };
|
|
94
|
+
}
|
|
95
|
+
function normalizeDirection(direction = "ASC") {
|
|
96
|
+
return direction.toUpperCase() === "DESC" ? "DESC" : "ASC";
|
|
97
|
+
}
|
|
98
|
+
function isQueryOperator(value) {
|
|
99
|
+
return value !== null && !Array.isArray(value) && !(value instanceof Date) && typeof value === "object";
|
|
100
|
+
}
|
|
101
|
+
function pushParam(values, value) {
|
|
102
|
+
values.push(value);
|
|
103
|
+
return `$${values.length}`;
|
|
104
|
+
}
|
|
105
|
+
function buildInClause(column, values, params) {
|
|
106
|
+
if (values.length === 0) {
|
|
107
|
+
return "1 = 0";
|
|
108
|
+
}
|
|
109
|
+
const placeholders = values.map((value) => pushParam(params, value)).join(", ");
|
|
110
|
+
return `${column} IN (${placeholders})`;
|
|
111
|
+
}
|
|
112
|
+
function buildOperatorClauses(column, operator, params) {
|
|
113
|
+
const clauses = [];
|
|
114
|
+
if (operator.isNull === true) {
|
|
115
|
+
clauses.push(`${column} IS NULL`);
|
|
116
|
+
}
|
|
117
|
+
if (operator.isNull === false) {
|
|
118
|
+
clauses.push(`${column} IS NOT NULL`);
|
|
119
|
+
}
|
|
120
|
+
if (operator.eq !== undefined) {
|
|
121
|
+
if (operator.eq === null) {
|
|
122
|
+
clauses.push(`${column} IS NULL`);
|
|
123
|
+
} else {
|
|
124
|
+
clauses.push(`${column} = ${pushParam(params, operator.eq)}`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if (operator.in !== undefined) {
|
|
128
|
+
clauses.push(buildInClause(column, operator.in, params));
|
|
129
|
+
}
|
|
130
|
+
if (operator.gt !== undefined) {
|
|
131
|
+
clauses.push(`${column} > ${pushParam(params, operator.gt)}`);
|
|
132
|
+
}
|
|
133
|
+
if (operator.gte !== undefined) {
|
|
134
|
+
clauses.push(`${column} >= ${pushParam(params, operator.gte)}`);
|
|
135
|
+
}
|
|
136
|
+
if (operator.lt !== undefined) {
|
|
137
|
+
clauses.push(`${column} < ${pushParam(params, operator.lt)}`);
|
|
138
|
+
}
|
|
139
|
+
if (operator.lte !== undefined) {
|
|
140
|
+
clauses.push(`${column} <= ${pushParam(params, operator.lte)}`);
|
|
141
|
+
}
|
|
142
|
+
if (operator.ilike !== undefined) {
|
|
143
|
+
clauses.push(`${column} ILIKE ${pushParam(params, `%${operator.ilike}%`)}`);
|
|
144
|
+
}
|
|
145
|
+
if (operator.tsMatch !== undefined) {
|
|
146
|
+
clauses.push(`${column} @@ plainto_tsquery('english', ${pushParam(params, operator.tsMatch)})`);
|
|
147
|
+
}
|
|
148
|
+
return clauses;
|
|
149
|
+
}
|
|
150
|
+
function appendWhereParts(tableName, where, params) {
|
|
151
|
+
const clauses = [];
|
|
152
|
+
for (const [columnName, filterValue] of Object.entries(where)) {
|
|
153
|
+
if (filterValue === undefined) {
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
const column = resolveQualifiedColumn(tableName, columnName);
|
|
157
|
+
if (Array.isArray(filterValue)) {
|
|
158
|
+
clauses.push(buildInClause(column, filterValue, params));
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
if (isQueryOperator(filterValue)) {
|
|
162
|
+
clauses.push(...buildOperatorClauses(column, filterValue, params));
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
if (filterValue === null) {
|
|
166
|
+
clauses.push(`${column} IS NULL`);
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
clauses.push(`${column} = ${pushParam(params, filterValue)}`);
|
|
170
|
+
}
|
|
171
|
+
return clauses.join(" AND ");
|
|
172
|
+
}
|
|
173
|
+
function buildWhereClause(tableName, where = {}) {
|
|
174
|
+
const params = [];
|
|
175
|
+
const body = appendWhereParts(tableName, where, params);
|
|
176
|
+
return {
|
|
177
|
+
clause: body.length > 0 ? ` WHERE ${body}` : "",
|
|
178
|
+
params
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
function buildWhereNodeClause(tableName, node, params) {
|
|
182
|
+
if ("where" in node) {
|
|
183
|
+
return appendWhereParts(tableName, node.where, params);
|
|
184
|
+
}
|
|
185
|
+
const grouped = buildWhereGroupClause(tableName, node.group, params);
|
|
186
|
+
if (!grouped) {
|
|
187
|
+
return "";
|
|
188
|
+
}
|
|
189
|
+
return grouped.includes(" OR ") ? `(${grouped})` : grouped;
|
|
190
|
+
}
|
|
191
|
+
function buildWhereGroupClause(tableName, nodes, params) {
|
|
192
|
+
let result = "";
|
|
193
|
+
for (const node of nodes) {
|
|
194
|
+
const part = buildWhereNodeClause(tableName, node, params);
|
|
195
|
+
if (!part) {
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
if (!result) {
|
|
199
|
+
result = part;
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
result = node.kind === "or" ? `${result} OR ${part}` : `${result} AND ${part}`;
|
|
203
|
+
}
|
|
204
|
+
if (!result) {
|
|
205
|
+
return "";
|
|
206
|
+
}
|
|
207
|
+
return result;
|
|
208
|
+
}
|
|
209
|
+
function buildAdvancedWhereClause(tableName, where = {}, whereNodes = [], params = []) {
|
|
210
|
+
const nodes = [];
|
|
211
|
+
if (Object.keys(where).length > 0) {
|
|
212
|
+
nodes.push({ kind: "and", where });
|
|
213
|
+
}
|
|
214
|
+
nodes.push(...whereNodes);
|
|
215
|
+
const combined = buildWhereGroupClause(tableName, nodes, params);
|
|
216
|
+
return {
|
|
217
|
+
clause: combined ? ` WHERE ${combined}` : "",
|
|
218
|
+
params
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
function resolveSoftDeleteColumn(table) {
|
|
222
|
+
if (!table.softDeletes) {
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
if (table.softDeletes === true) {
|
|
226
|
+
return "deleted_at";
|
|
227
|
+
}
|
|
228
|
+
return table.softDeletes.column ?? "deleted_at";
|
|
229
|
+
}
|
|
230
|
+
function appendSoftDeleteScope(table, options, clauses) {
|
|
231
|
+
const column = resolveSoftDeleteColumn(table);
|
|
232
|
+
if (!column) {
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
const qualifiedColumn = qualifyColumn(table.name, column);
|
|
236
|
+
if (options.onlyTrashed) {
|
|
237
|
+
clauses.push(`${qualifiedColumn} IS NOT NULL`);
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
if (!options.withTrashed) {
|
|
241
|
+
clauses.push(`${qualifiedColumn} IS NULL`);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
function buildQueryWhereClause(table, options = {}, whereNodes = [], params = []) {
|
|
245
|
+
const { clause, params: whereParams } = buildAdvancedWhereClause(table.name, options.where ?? {}, whereNodes, params);
|
|
246
|
+
const softDeleteClauses = [];
|
|
247
|
+
appendSoftDeleteScope(table, options, softDeleteClauses);
|
|
248
|
+
if (softDeleteClauses.length === 0) {
|
|
249
|
+
return { clause, params: whereParams };
|
|
250
|
+
}
|
|
251
|
+
const base = clause.replace(/^ WHERE /, "");
|
|
252
|
+
const scope = softDeleteClauses.join(" AND ");
|
|
253
|
+
return {
|
|
254
|
+
clause: base ? ` WHERE (${base}) AND ${scope}` : ` WHERE ${scope}`,
|
|
255
|
+
params: whereParams
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
function isQueryOrder(value) {
|
|
259
|
+
return "column" in value;
|
|
260
|
+
}
|
|
261
|
+
function normalizeOrderBy(orderBy) {
|
|
262
|
+
if (!orderBy) {
|
|
263
|
+
return [];
|
|
264
|
+
}
|
|
265
|
+
if (Array.isArray(orderBy)) {
|
|
266
|
+
return orderBy;
|
|
267
|
+
}
|
|
268
|
+
if (isQueryOrder(orderBy)) {
|
|
269
|
+
return [orderBy];
|
|
270
|
+
}
|
|
271
|
+
return Object.entries(orderBy).map(([column, direction]) => ({
|
|
272
|
+
column,
|
|
273
|
+
direction
|
|
274
|
+
}));
|
|
275
|
+
}
|
|
276
|
+
function buildOrderByClause(tableName, orderBy) {
|
|
277
|
+
const parts = normalizeOrderBy(orderBy).map(({ column, direction }) => {
|
|
278
|
+
return `${resolveQualifiedColumn(tableName, column)} ${normalizeDirection(direction)}`;
|
|
279
|
+
});
|
|
280
|
+
return parts.length > 0 ? ` ORDER BY ${parts.join(", ")}` : "";
|
|
281
|
+
}
|
|
282
|
+
function buildGroupByClause(tableName, groupBy) {
|
|
283
|
+
if (!groupBy) {
|
|
284
|
+
return "";
|
|
285
|
+
}
|
|
286
|
+
const columns = (Array.isArray(groupBy) ? groupBy : [groupBy]).map((column) => String(column));
|
|
287
|
+
const parts = columns.map((column) => resolveQualifiedColumn(tableName, column));
|
|
288
|
+
return parts.length > 0 ? ` GROUP BY ${parts.join(", ")}` : "";
|
|
289
|
+
}
|
|
290
|
+
function buildHavingClause(tableName, having, params) {
|
|
291
|
+
if (!having) {
|
|
292
|
+
return "";
|
|
293
|
+
}
|
|
294
|
+
const body = appendWhereParts(tableName, having, params);
|
|
295
|
+
return body.length > 0 ? ` HAVING ${body}` : "";
|
|
296
|
+
}
|
|
297
|
+
function buildJoinClause(joins = []) {
|
|
298
|
+
return joins.map((join) => {
|
|
299
|
+
const joinType = join.type === "left" ? "LEFT JOIN" : "INNER JOIN";
|
|
300
|
+
const onClause = join.on.map(({ left, right }) => `${qualifyColumn(left.table, left.column)} = ${qualifyColumn(right.table, right.column)}`).join(" AND ");
|
|
301
|
+
return ` ${joinType} ${quoteIdentifier(join.table)} ON ${onClause}`;
|
|
302
|
+
}).join("");
|
|
303
|
+
}
|
|
304
|
+
function buildLimitClause(limit) {
|
|
305
|
+
if (limit === undefined) {
|
|
306
|
+
return "";
|
|
307
|
+
}
|
|
308
|
+
if (!Number.isInteger(limit) || limit <= 0) {
|
|
309
|
+
throw new Error("Query limit must be a positive integer.");
|
|
310
|
+
}
|
|
311
|
+
return ` LIMIT ${limit}`;
|
|
312
|
+
}
|
|
313
|
+
function buildOffsetClause(offset) {
|
|
314
|
+
if (offset === undefined) {
|
|
315
|
+
return "";
|
|
316
|
+
}
|
|
317
|
+
if (!Number.isInteger(offset) || offset < 0) {
|
|
318
|
+
throw new Error("Query offset must be a non-negative integer.");
|
|
319
|
+
}
|
|
320
|
+
return ` OFFSET ${offset}`;
|
|
321
|
+
}
|
|
322
|
+
function buildReturningColumns(table) {
|
|
323
|
+
return table.columns.map((column) => qualifyColumn(table.name, column)).join(", ");
|
|
324
|
+
}
|
|
325
|
+
function buildSelectList(table, select, params = []) {
|
|
326
|
+
if (!select || select.length === 0) {
|
|
327
|
+
return buildReturningColumns(table);
|
|
328
|
+
}
|
|
329
|
+
return select.map((item) => {
|
|
330
|
+
if (item.kind === "column") {
|
|
331
|
+
const column2 = qualifyColumn(item.table, item.column);
|
|
332
|
+
return item.as ? `${column2} AS ${quoteIdentifier(item.as)}` : column2;
|
|
333
|
+
}
|
|
334
|
+
if (item.kind === "literalText") {
|
|
335
|
+
return `${pushParam(params, item.value)}::text AS ${quoteIdentifier(item.as)}`;
|
|
336
|
+
}
|
|
337
|
+
const column = qualifyColumn(item.table, item.column);
|
|
338
|
+
const placeholder = pushParam(params, item.query);
|
|
339
|
+
return `ts_rank(${column}, plainto_tsquery('english', ${placeholder})) AS ${quoteIdentifier(item.as)}`;
|
|
340
|
+
}).join(", ");
|
|
341
|
+
}
|
|
342
|
+
function getDefinedColumnEntries(table, values, options = {}) {
|
|
343
|
+
const record = values;
|
|
344
|
+
const excluded = new Set(options.exclude ?? []);
|
|
345
|
+
return table.columns.flatMap((column) => {
|
|
346
|
+
if (excluded.has(column) || !Object.hasOwn(record, column)) {
|
|
347
|
+
return [];
|
|
348
|
+
}
|
|
349
|
+
const value = record[column];
|
|
350
|
+
if (value === undefined) {
|
|
351
|
+
return [];
|
|
352
|
+
}
|
|
353
|
+
return [[column, value]];
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
function buildSelectQuery(table, options = {}, whereNodes = []) {
|
|
357
|
+
const params = [];
|
|
358
|
+
const columns = buildSelectList(table, options.select, params);
|
|
359
|
+
const { clause } = buildQueryWhereClause(table, options, whereNodes, params);
|
|
360
|
+
const joins = buildJoinClause(options.joins);
|
|
361
|
+
const groupBy = buildGroupByClause(table.name, options.groupBy);
|
|
362
|
+
const havingClause = buildHavingClause(table.name, options.having, params);
|
|
363
|
+
const orderBy = buildOrderByClause(table.name, options.orderBy ?? table.defaultOrderBy);
|
|
364
|
+
const limit = buildLimitClause(options.limit);
|
|
365
|
+
const offset = buildOffsetClause(options.offset);
|
|
366
|
+
return {
|
|
367
|
+
text: `SELECT ${columns} FROM ${quoteIdentifier(table.name)}${joins}${clause}${groupBy}${havingClause}${orderBy}${limit}${offset}`,
|
|
368
|
+
params
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
function buildCountQuery(table, where = {}, options = {}, whereNodes = []) {
|
|
372
|
+
const params = [];
|
|
373
|
+
const { clause, params: whereParams } = buildQueryWhereClause(table, {
|
|
374
|
+
where,
|
|
375
|
+
withTrashed: options.withTrashed,
|
|
376
|
+
onlyTrashed: options.onlyTrashed
|
|
377
|
+
}, whereNodes);
|
|
378
|
+
params.push(...whereParams);
|
|
379
|
+
const joins = buildJoinClause(options.joins);
|
|
380
|
+
const groupBy = buildGroupByClause(table.name, options.groupBy);
|
|
381
|
+
return {
|
|
382
|
+
text: `SELECT COUNT(*) AS count FROM ${quoteIdentifier(table.name)}${joins}${clause}${groupBy}`,
|
|
383
|
+
params
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
function buildProjectionQuery(table, expression, alias, options = {}, whereNodes = []) {
|
|
387
|
+
assertSafeProjectionExpression(expression);
|
|
388
|
+
const params = [];
|
|
389
|
+
const { clause, params: whereParams } = buildQueryWhereClause(table, options, whereNodes);
|
|
390
|
+
params.push(...whereParams);
|
|
391
|
+
const joins = buildJoinClause(options.joins);
|
|
392
|
+
const groupBy = buildGroupByClause(table.name, options.groupBy);
|
|
393
|
+
const orderBy = buildOrderByClause(table.name, options.orderBy);
|
|
394
|
+
const limit = buildLimitClause(options.limit);
|
|
395
|
+
return {
|
|
396
|
+
text: `SELECT ${expression} AS ${quoteIdentifier(alias)} FROM ${quoteIdentifier(table.name)}${joins}${clause}${groupBy}${orderBy}${limit}`,
|
|
397
|
+
params
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
var SAFE_PROJECTION_EXPRESSION = /^(COUNT\(\*\)|(?:"[\w_]+"(?:\."[\w_]+")?)|(?:AVG|SUM|MIN|MAX)\((?:"[\w_]+"(?:\."[\w_]+")?)\))$/;
|
|
401
|
+
function assertSafeProjectionExpression(expression) {
|
|
402
|
+
if (!SAFE_PROJECTION_EXPRESSION.test(expression.trim())) {
|
|
403
|
+
throw new Error(`Unsafe projection expression: ${expression}`);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
function buildGroupedCountQuery(table, column, where = {}, options = {}) {
|
|
407
|
+
const qualifiedColumn = qualifyColumn(table.name, column);
|
|
408
|
+
const { clause, params } = buildQueryWhereClause(table, {
|
|
409
|
+
where,
|
|
410
|
+
...options
|
|
411
|
+
});
|
|
412
|
+
return {
|
|
413
|
+
text: `SELECT ${qualifiedColumn} AS ${quoteIdentifier("value")}, COUNT(*) AS ${quoteIdentifier("count")} FROM ${quoteIdentifier(table.name)}${clause} GROUP BY ${qualifiedColumn} ORDER BY ${qualifiedColumn} ASC`,
|
|
414
|
+
params
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
function buildInsertQuery(table, values) {
|
|
418
|
+
const entries = getDefinedColumnEntries(table, values);
|
|
419
|
+
if (entries.length === 0) {
|
|
420
|
+
throw new Error(`Cannot insert into ${table.name} without any column values.`);
|
|
421
|
+
}
|
|
422
|
+
const params = [];
|
|
423
|
+
const columns = entries.map(([column]) => quoteIdentifier(column)).join(", ");
|
|
424
|
+
const placeholders = entries.map(([, value]) => pushParam(params, value)).join(", ");
|
|
425
|
+
const returningColumns = buildReturningColumns(table);
|
|
426
|
+
return {
|
|
427
|
+
text: `INSERT INTO ${quoteIdentifier(table.name)} (${columns}) VALUES (${placeholders}) RETURNING ${returningColumns}`,
|
|
428
|
+
params
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
function buildUpdateQuery(table, id, changes) {
|
|
432
|
+
const entries = getDefinedColumnEntries(table, changes, {
|
|
433
|
+
exclude: [table.primaryKey]
|
|
434
|
+
});
|
|
435
|
+
if (entries.length === 0) {
|
|
436
|
+
throw new Error(`Cannot update ${table.name} without any changed column values.`);
|
|
437
|
+
}
|
|
438
|
+
const params = [];
|
|
439
|
+
const setClause = entries.map(([column, value]) => `${quoteIdentifier(column)} = ${pushParam(params, value)}`).join(", ");
|
|
440
|
+
const primaryKeyPlaceholder = pushParam(params, id);
|
|
441
|
+
const returningColumns = buildReturningColumns(table);
|
|
442
|
+
const scopeClauses = [];
|
|
443
|
+
appendSoftDeleteScope(table, {}, scopeClauses);
|
|
444
|
+
const scopeSuffix = scopeClauses.length > 0 ? ` AND ${scopeClauses.join(" AND ")}` : "";
|
|
445
|
+
return {
|
|
446
|
+
text: `UPDATE ${quoteIdentifier(table.name)} SET ${setClause} WHERE ${quoteIdentifier(table.primaryKey)} = ${primaryKeyPlaceholder}${scopeSuffix} RETURNING ${returningColumns}`,
|
|
447
|
+
params
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
function buildSoftDeleteByIdQuery(table, id, deletedAt) {
|
|
451
|
+
const deletedAtColumn = resolveSoftDeleteColumn(table);
|
|
452
|
+
if (!deletedAtColumn) {
|
|
453
|
+
throw new Error(`Table ${table.name} does not support soft deletes.`);
|
|
454
|
+
}
|
|
455
|
+
const returningColumns = buildReturningColumns(table);
|
|
456
|
+
const scopeClauses = [];
|
|
457
|
+
appendSoftDeleteScope(table, {}, scopeClauses);
|
|
458
|
+
const scopeSuffix = scopeClauses.length > 0 ? ` AND ${scopeClauses.join(" AND ")}` : "";
|
|
459
|
+
return {
|
|
460
|
+
text: `UPDATE ${quoteIdentifier(table.name)} SET ${quoteIdentifier(deletedAtColumn)} = $1 WHERE ${quoteIdentifier(table.primaryKey)} = $2${scopeSuffix} RETURNING ${returningColumns}`,
|
|
461
|
+
params: [deletedAt, id]
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
function buildRestoreByIdQuery(table, id) {
|
|
465
|
+
const deletedAtColumn = resolveSoftDeleteColumn(table);
|
|
466
|
+
if (!deletedAtColumn) {
|
|
467
|
+
throw new Error(`Table ${table.name} does not support soft deletes.`);
|
|
468
|
+
}
|
|
469
|
+
const returningColumns = buildReturningColumns(table);
|
|
470
|
+
return {
|
|
471
|
+
text: `UPDATE ${quoteIdentifier(table.name)} SET ${quoteIdentifier(deletedAtColumn)} = $1 WHERE ${quoteIdentifier(table.primaryKey)} = $2 AND ${qualifyColumn(table.name, deletedAtColumn)} IS NOT NULL RETURNING ${returningColumns}`,
|
|
472
|
+
params: [null, id]
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
function buildDeleteByIdQuery(table, id) {
|
|
476
|
+
return {
|
|
477
|
+
text: `DELETE FROM ${quoteIdentifier(table.name)} WHERE ${quoteIdentifier(table.primaryKey)} = $1 RETURNING ${quoteIdentifier(table.primaryKey)} AS ${quoteIdentifier("deleted_id")}`,
|
|
478
|
+
params: [id]
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// ../../src/core/database/relationships.ts
|
|
483
|
+
function hasMany(definition) {
|
|
484
|
+
return {
|
|
485
|
+
type: "hasMany",
|
|
486
|
+
...definition
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
function hasOne(definition) {
|
|
490
|
+
return {
|
|
491
|
+
type: "hasOne",
|
|
492
|
+
...definition
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
function belongsTo(definition) {
|
|
496
|
+
return {
|
|
497
|
+
type: "belongsTo",
|
|
498
|
+
...definition
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
function belongsToMany(definition) {
|
|
502
|
+
return {
|
|
503
|
+
type: "belongsToMany",
|
|
504
|
+
...definition
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
function indexHasManyRelation(parents, children, relation) {
|
|
508
|
+
const groups = new Map;
|
|
509
|
+
for (const parent of parents) {
|
|
510
|
+
groups.set(parent[relation.localKey], []);
|
|
511
|
+
}
|
|
512
|
+
for (const child of children) {
|
|
513
|
+
const key = child[relation.foreignKey];
|
|
514
|
+
const group = groups.get(key);
|
|
515
|
+
if (!group) {
|
|
516
|
+
continue;
|
|
517
|
+
}
|
|
518
|
+
group.push(child);
|
|
519
|
+
}
|
|
520
|
+
return groups;
|
|
521
|
+
}
|
|
522
|
+
function indexHasOneRelation(parents, children, relation) {
|
|
523
|
+
const grouped = indexHasManyRelation(parents, children, relation);
|
|
524
|
+
const result = new Map;
|
|
525
|
+
for (const parent of parents) {
|
|
526
|
+
const matches = grouped.get(parent[relation.localKey]) ?? [];
|
|
527
|
+
result.set(parent[relation.localKey], matches[0]);
|
|
528
|
+
}
|
|
529
|
+
return result;
|
|
530
|
+
}
|
|
531
|
+
function indexBelongsToRelation(children, parents, relation) {
|
|
532
|
+
const parentsById = new Map;
|
|
533
|
+
for (const parent of parents) {
|
|
534
|
+
parentsById.set(parent[relation.ownerKey], parent);
|
|
535
|
+
}
|
|
536
|
+
const result = new Map;
|
|
537
|
+
for (const child of children) {
|
|
538
|
+
const foreignKey = child[relation.foreignKey];
|
|
539
|
+
const parent = parentsById.get(foreignKey);
|
|
540
|
+
if (parent) {
|
|
541
|
+
result.set(foreignKey, parent);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
return result;
|
|
545
|
+
}
|
|
546
|
+
function indexBelongsToManyRelation(parents, pivotRows, relatedRows, relation) {
|
|
547
|
+
const relatedById = new Map;
|
|
548
|
+
for (const related of relatedRows) {
|
|
549
|
+
relatedById.set(related[relation.relatedKey], related);
|
|
550
|
+
}
|
|
551
|
+
const groups = new Map;
|
|
552
|
+
for (const parent of parents) {
|
|
553
|
+
groups.set(parent[relation.parentKey], []);
|
|
554
|
+
}
|
|
555
|
+
for (const pivot of pivotRows) {
|
|
556
|
+
const parentId = pivot[relation.foreignPivotKey];
|
|
557
|
+
const relatedId = pivot[relation.relatedPivotKey];
|
|
558
|
+
const group = groups.get(parentId);
|
|
559
|
+
const related = relatedById.get(relatedId);
|
|
560
|
+
if (!group || !related) {
|
|
561
|
+
continue;
|
|
562
|
+
}
|
|
563
|
+
group.push(related);
|
|
564
|
+
}
|
|
565
|
+
return groups;
|
|
566
|
+
}
|
|
567
|
+
function morphMany(definition) {
|
|
568
|
+
return {
|
|
569
|
+
type: "morphMany",
|
|
570
|
+
...definition
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
function morphOne(definition) {
|
|
574
|
+
return {
|
|
575
|
+
type: "morphOne",
|
|
576
|
+
...definition
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
function morphTo(definition) {
|
|
580
|
+
return {
|
|
581
|
+
type: "morphTo",
|
|
582
|
+
...definition
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
function indexMorphManyRelation(parents, children, relation) {
|
|
586
|
+
const groups = new Map;
|
|
587
|
+
for (const parent of parents) {
|
|
588
|
+
groups.set(parent[relation.localKey], []);
|
|
589
|
+
}
|
|
590
|
+
for (const child of children) {
|
|
591
|
+
if (child[relation.morphTypeKey] !== relation.morphType) {
|
|
592
|
+
continue;
|
|
593
|
+
}
|
|
594
|
+
const key = child[relation.morphIdKey];
|
|
595
|
+
const group = groups.get(key);
|
|
596
|
+
if (!group) {
|
|
597
|
+
continue;
|
|
598
|
+
}
|
|
599
|
+
group.push(child);
|
|
600
|
+
}
|
|
601
|
+
return groups;
|
|
602
|
+
}
|
|
603
|
+
function indexMorphOneRelation(parents, children, relation) {
|
|
604
|
+
const grouped = indexMorphManyRelation(parents, children, relation);
|
|
605
|
+
const result = new Map;
|
|
606
|
+
for (const parent of parents) {
|
|
607
|
+
const matches = grouped.get(parent[relation.localKey]) ?? [];
|
|
608
|
+
result.set(parent[relation.localKey], matches[0]);
|
|
609
|
+
}
|
|
610
|
+
return result;
|
|
611
|
+
}
|
|
612
|
+
function indexMorphToRelation(children, parentsByType, relation) {
|
|
613
|
+
const result = new Map;
|
|
614
|
+
for (const child of children) {
|
|
615
|
+
const morphType = String(child[relation.morphTypeKey]);
|
|
616
|
+
const parents = parentsByType.get(morphType);
|
|
617
|
+
if (!parents) {
|
|
618
|
+
continue;
|
|
619
|
+
}
|
|
620
|
+
const parent = parents.get(child[relation.morphIdKey]);
|
|
621
|
+
if (parent) {
|
|
622
|
+
result.set(child[relation.morphIdKey], parent);
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
return result;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
// ../../src/core/database/model.ts
|
|
629
|
+
var modelRepositories = new WeakMap;
|
|
630
|
+
var modelGlobalScopes = new WeakMap;
|
|
631
|
+
var modelBooted = new WeakSet;
|
|
632
|
+
function resolveModelRepository(model) {
|
|
633
|
+
const repository = modelRepositories.get(model);
|
|
634
|
+
if (!repository) {
|
|
635
|
+
throw new Error(`${model.name}.repository() is not implemented.`);
|
|
636
|
+
}
|
|
637
|
+
return repository;
|
|
638
|
+
}
|
|
639
|
+
function modelStatics(model) {
|
|
640
|
+
return model;
|
|
641
|
+
}
|
|
642
|
+
function ensureBooted(model) {
|
|
643
|
+
if (modelBooted.has(model)) {
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
646
|
+
modelBooted.add(model);
|
|
647
|
+
const boot = model.boot;
|
|
648
|
+
if (typeof boot === "function") {
|
|
649
|
+
boot.call(model);
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
function getGlobalScopes(model) {
|
|
653
|
+
return modelGlobalScopes.get(model) ?? [];
|
|
654
|
+
}
|
|
655
|
+
function hydrateValue(value, cast) {
|
|
656
|
+
if (value === null || value === undefined) {
|
|
657
|
+
return value;
|
|
658
|
+
}
|
|
659
|
+
switch (cast) {
|
|
660
|
+
case "date":
|
|
661
|
+
case "datetime":
|
|
662
|
+
return value instanceof Date ? value : new Date(String(value));
|
|
663
|
+
case "json":
|
|
664
|
+
return typeof value === "string" ? JSON.parse(value) : value;
|
|
665
|
+
case "bool":
|
|
666
|
+
case "boolean":
|
|
667
|
+
return value === true || value === 1 || value === "1" || value === "true";
|
|
668
|
+
default:
|
|
669
|
+
return value;
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
function dehydrateValue(value, cast) {
|
|
673
|
+
if (value === null || value === undefined) {
|
|
674
|
+
return value;
|
|
675
|
+
}
|
|
676
|
+
switch (cast) {
|
|
677
|
+
case "date":
|
|
678
|
+
case "datetime":
|
|
679
|
+
return value instanceof Date ? value : new Date(String(value));
|
|
680
|
+
case "json":
|
|
681
|
+
return typeof value === "string" ? value : JSON.stringify(value);
|
|
682
|
+
case "bool":
|
|
683
|
+
case "boolean":
|
|
684
|
+
return Boolean(value);
|
|
685
|
+
default:
|
|
686
|
+
return value;
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
function filterMassAssignable(fillable, guarded, input) {
|
|
690
|
+
const resolvedGuarded = guarded ?? true;
|
|
691
|
+
if (fillable && fillable.length > 0) {
|
|
692
|
+
const allowed = new Set(fillable);
|
|
693
|
+
return Object.fromEntries(Object.entries(input).filter(([key]) => allowed.has(key)));
|
|
694
|
+
}
|
|
695
|
+
if (resolvedGuarded === true || resolvedGuarded.includes("*")) {
|
|
696
|
+
return {};
|
|
697
|
+
}
|
|
698
|
+
const blocked = new Set(resolvedGuarded);
|
|
699
|
+
return Object.fromEntries(Object.entries(input).filter(([key]) => !blocked.has(key)));
|
|
700
|
+
}
|
|
701
|
+
function applyCasts(values, casts, direction) {
|
|
702
|
+
if (Object.keys(casts).length === 0) {
|
|
703
|
+
return values;
|
|
704
|
+
}
|
|
705
|
+
const result = { ...values };
|
|
706
|
+
const castFn = direction === "hydrate" ? hydrateValue : dehydrateValue;
|
|
707
|
+
for (const [key, cast] of Object.entries(casts)) {
|
|
708
|
+
if (key in result && cast) {
|
|
709
|
+
result[key] = castFn(result[key], cast);
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
return result;
|
|
713
|
+
}
|
|
714
|
+
function applyTimestampsOnCreate(columns, values, enabled) {
|
|
715
|
+
if (!enabled) {
|
|
716
|
+
return values;
|
|
717
|
+
}
|
|
718
|
+
const now = new Date;
|
|
719
|
+
const result = { ...values };
|
|
720
|
+
if (columns.includes("created_at")) {
|
|
721
|
+
result.created_at = now;
|
|
722
|
+
}
|
|
723
|
+
if (columns.includes("updated_at")) {
|
|
724
|
+
result.updated_at = now;
|
|
725
|
+
}
|
|
726
|
+
return result;
|
|
727
|
+
}
|
|
728
|
+
function applyTimestampsOnUpdate(columns, values, enabled) {
|
|
729
|
+
if (!enabled) {
|
|
730
|
+
return values;
|
|
731
|
+
}
|
|
732
|
+
const result = { ...values };
|
|
733
|
+
if (columns.includes("updated_at")) {
|
|
734
|
+
result.updated_at = new Date;
|
|
735
|
+
}
|
|
736
|
+
return result;
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
class Model {
|
|
740
|
+
attributes;
|
|
741
|
+
repository;
|
|
742
|
+
static $fillable;
|
|
743
|
+
static $guarded;
|
|
744
|
+
static $casts = {};
|
|
745
|
+
static $timestamps = true;
|
|
746
|
+
_exists;
|
|
747
|
+
constructor(attributes, repository, exists = true) {
|
|
748
|
+
this.attributes = attributes;
|
|
749
|
+
this.repository = repository;
|
|
750
|
+
this._exists = exists;
|
|
751
|
+
}
|
|
752
|
+
get $exists() {
|
|
753
|
+
return this._exists;
|
|
754
|
+
}
|
|
755
|
+
get(key) {
|
|
756
|
+
return this.attributes[key];
|
|
757
|
+
}
|
|
758
|
+
get id() {
|
|
759
|
+
return this.attributes[this.primaryKey()];
|
|
760
|
+
}
|
|
761
|
+
toObject() {
|
|
762
|
+
return { ...this.attributes };
|
|
763
|
+
}
|
|
764
|
+
primaryKey() {
|
|
765
|
+
throw new Error(`${this.constructor.name}.primaryKey() is not implemented.`);
|
|
766
|
+
}
|
|
767
|
+
static primaryKeyField() {
|
|
768
|
+
return resolveModelRepository(this).getTable().primaryKey;
|
|
769
|
+
}
|
|
770
|
+
static hydrateAttributes(attributes) {
|
|
771
|
+
const casts = modelStatics(this).$casts ?? {};
|
|
772
|
+
return applyCasts(attributes, casts, "hydrate");
|
|
773
|
+
}
|
|
774
|
+
static dehydrateAttributes(attributes) {
|
|
775
|
+
const casts = modelStatics(this).$casts ?? {};
|
|
776
|
+
return applyCasts(attributes, casts, "dehydrate");
|
|
777
|
+
}
|
|
778
|
+
static fromRecord(record, repository, exists = true) {
|
|
779
|
+
const statics = modelStatics(this);
|
|
780
|
+
const hydrated = statics.hydrateAttributes(record);
|
|
781
|
+
return new statics(hydrated, repository, exists);
|
|
782
|
+
}
|
|
783
|
+
static boot() {}
|
|
784
|
+
static addGlobalScope(_name, scope) {
|
|
785
|
+
ensureBooted(this);
|
|
786
|
+
const existing = modelGlobalScopes.get(this) ?? [];
|
|
787
|
+
modelGlobalScopes.set(this, [
|
|
788
|
+
...existing,
|
|
789
|
+
scope
|
|
790
|
+
]);
|
|
791
|
+
}
|
|
792
|
+
static repository() {
|
|
793
|
+
return resolveModelRepository(this);
|
|
794
|
+
}
|
|
795
|
+
static query() {
|
|
796
|
+
ensureBooted(this);
|
|
797
|
+
const repository = resolveModelRepository(this);
|
|
798
|
+
let query = repository.query();
|
|
799
|
+
for (const scope of getGlobalScopes(this)) {
|
|
800
|
+
query = scope(query);
|
|
801
|
+
}
|
|
802
|
+
return query;
|
|
803
|
+
}
|
|
804
|
+
static async create(attributes) {
|
|
805
|
+
const statics = modelStatics(this);
|
|
806
|
+
ensureBooted(this);
|
|
807
|
+
const repository = resolveModelRepository(this);
|
|
808
|
+
const table = repository.getTable();
|
|
809
|
+
const timestamps = statics.$timestamps ?? true;
|
|
810
|
+
const assignable = filterMassAssignable(statics.$fillable, statics.$guarded, attributes);
|
|
811
|
+
const withTimestamps = applyTimestampsOnCreate(table.columns, assignable, timestamps);
|
|
812
|
+
const payload = statics.dehydrateAttributes(withTimestamps);
|
|
813
|
+
const record = await repository.create(payload);
|
|
814
|
+
return statics.fromRecord(record, repository, true);
|
|
815
|
+
}
|
|
816
|
+
static async find(id) {
|
|
817
|
+
const statics = modelStatics(this);
|
|
818
|
+
const repository = resolveModelRepository(this);
|
|
819
|
+
const primaryKey = repository.getTable().primaryKey;
|
|
820
|
+
const record = await Model.query.call(this).where({ [primaryKey]: id }).first();
|
|
821
|
+
return record ? statics.fromRecord(record, repository, true) : null;
|
|
822
|
+
}
|
|
823
|
+
static async findOrFail(id, errorFactory) {
|
|
824
|
+
const model = await Model.find.call(this, id);
|
|
825
|
+
if (model) {
|
|
826
|
+
return model;
|
|
827
|
+
}
|
|
828
|
+
throw errorFactory?.(id) ?? new NotFoundError(`${this.name} ${String(id)} not found.`);
|
|
829
|
+
}
|
|
830
|
+
static async all(options = {}) {
|
|
831
|
+
const statics = modelStatics(this);
|
|
832
|
+
const repository = resolveModelRepository(this);
|
|
833
|
+
let query = Model.query.call(this);
|
|
834
|
+
if (options.orderBy) {
|
|
835
|
+
query = query.orderBy(options.orderBy);
|
|
836
|
+
}
|
|
837
|
+
if (options.limit !== undefined) {
|
|
838
|
+
query = query.limit(options.limit);
|
|
839
|
+
}
|
|
840
|
+
const rows = await query.get();
|
|
841
|
+
return rows.map((row) => statics.fromRecord(row, repository, true));
|
|
842
|
+
}
|
|
843
|
+
static async firstWhere(where, options = {}) {
|
|
844
|
+
const statics = modelStatics(this);
|
|
845
|
+
const repository = resolveModelRepository(this);
|
|
846
|
+
let query = Model.query.call(this).where(where);
|
|
847
|
+
if (options.orderBy) {
|
|
848
|
+
query = query.orderBy(options.orderBy);
|
|
849
|
+
}
|
|
850
|
+
const record = await query.first();
|
|
851
|
+
return record ? statics.fromRecord(record, repository, true) : null;
|
|
852
|
+
}
|
|
853
|
+
async save() {
|
|
854
|
+
const ModelClass = modelStatics(this.constructor);
|
|
855
|
+
const timestamps = ModelClass.$timestamps ?? true;
|
|
856
|
+
const casts = ModelClass.$casts ?? {};
|
|
857
|
+
const table = this.repository.getTable();
|
|
858
|
+
if (this.$exists) {
|
|
859
|
+
const changes = applyTimestampsOnUpdate(table.columns, applyCasts(this.attributes, casts, "dehydrate"), timestamps);
|
|
860
|
+
const record2 = await this.repository.updateByIdOrThrow(this.id, changes);
|
|
861
|
+
this.attributes = ModelClass.hydrateAttributes(record2);
|
|
862
|
+
return this;
|
|
863
|
+
}
|
|
864
|
+
const assignable = filterMassAssignable(ModelClass.$fillable, ModelClass.$guarded, this.attributes);
|
|
865
|
+
const withTimestamps = applyTimestampsOnCreate(table.columns, assignable, timestamps);
|
|
866
|
+
const payload = ModelClass.dehydrateAttributes(withTimestamps);
|
|
867
|
+
const record = await this.repository.create(payload);
|
|
868
|
+
this.attributes = ModelClass.hydrateAttributes(record);
|
|
869
|
+
this._exists = true;
|
|
870
|
+
return this;
|
|
871
|
+
}
|
|
872
|
+
async update(changes) {
|
|
873
|
+
const ModelClass = modelStatics(this.constructor);
|
|
874
|
+
const assignable = filterMassAssignable(ModelClass.$fillable, ModelClass.$guarded, changes);
|
|
875
|
+
Object.assign(this.attributes, assignable);
|
|
876
|
+
return await this.save();
|
|
877
|
+
}
|
|
878
|
+
async delete() {
|
|
879
|
+
if (resolveSoftDeleteColumn(this.repository.getTable())) {
|
|
880
|
+
return await this.repository.deleteById(this.id);
|
|
881
|
+
}
|
|
882
|
+
return await this.repository.forceDeleteById(this.id);
|
|
883
|
+
}
|
|
884
|
+
async forceDelete() {
|
|
885
|
+
return await this.repository.forceDeleteById(this.id);
|
|
886
|
+
}
|
|
887
|
+
async restore() {
|
|
888
|
+
const ModelClass = modelStatics(this.constructor);
|
|
889
|
+
const record = await this.repository.restoreById(this.id);
|
|
890
|
+
if (!record) {
|
|
891
|
+
return null;
|
|
892
|
+
}
|
|
893
|
+
this.attributes = ModelClass.hydrateAttributes(record);
|
|
894
|
+
return this;
|
|
895
|
+
}
|
|
896
|
+
async loadHasMany(as, relation, childRepository, options = {}) {
|
|
897
|
+
const grouped = await childRepository.withConnection(this.repository.getConnection()).loadHasManyForParents([this.attributes], relation, options);
|
|
898
|
+
const loaded = grouped.get(this.attributes[relation.localKey]) ?? [];
|
|
899
|
+
return Object.assign(this, { [as]: loaded });
|
|
900
|
+
}
|
|
901
|
+
async loadHasOne(as, relation, childRepository, options = {}) {
|
|
902
|
+
const loaded = await this.loadHasMany(as, relation, childRepository, { ...options, limit: 1 });
|
|
903
|
+
const value = loaded[as]?.[0];
|
|
904
|
+
return Object.assign(this, { [as]: value });
|
|
905
|
+
}
|
|
906
|
+
async loadBelongsTo(as, relation, parentRepository, options = {}) {
|
|
907
|
+
const grouped = await this.repository.loadBelongsToForParents([this.attributes], relation, parentRepository, options);
|
|
908
|
+
const loaded = grouped.get(this.attributes[relation.foreignKey]);
|
|
909
|
+
return Object.assign(this, { [as]: loaded });
|
|
910
|
+
}
|
|
911
|
+
async loadBelongsToMany(as, relation, relatedRepository, options = {}) {
|
|
912
|
+
const connection = this.repository.getConnection();
|
|
913
|
+
const parentId = this.attributes[relation.parentKey];
|
|
914
|
+
const pivotRows = await connection.unsafe(`SELECT * FROM ${relation.pivotTable} WHERE ${String(relation.foreignPivotKey)} = $1`, [parentId]);
|
|
915
|
+
if (pivotRows.length === 0) {
|
|
916
|
+
return Object.assign(this, { [as]: [] });
|
|
917
|
+
}
|
|
918
|
+
const relatedIds = [
|
|
919
|
+
...new Set(pivotRows.map((row) => row[relation.relatedPivotKey]))
|
|
920
|
+
];
|
|
921
|
+
const relatedRows = await relatedRepository.withConnection(connection).findAll({
|
|
922
|
+
...options,
|
|
923
|
+
where: {
|
|
924
|
+
[relation.relatedKey]: relatedIds
|
|
925
|
+
}
|
|
926
|
+
});
|
|
927
|
+
const grouped = indexBelongsToManyRelation([this.attributes], pivotRows, relatedRows, relation);
|
|
928
|
+
const loaded = grouped.get(parentId) ?? [];
|
|
929
|
+
return Object.assign(this, { [as]: loaded });
|
|
930
|
+
}
|
|
931
|
+
mergeAttributes(patch) {
|
|
932
|
+
Object.assign(this.attributes, patch);
|
|
933
|
+
return this;
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
function registerModelRepository(model, repository) {
|
|
937
|
+
modelRepositories.set(model, repository);
|
|
938
|
+
ensureBooted(model);
|
|
939
|
+
return model;
|
|
940
|
+
}
|
|
941
|
+
export {
|
|
942
|
+
registerModelRepository,
|
|
943
|
+
hydrateValue,
|
|
944
|
+
filterMassAssignable,
|
|
945
|
+
dehydrateValue,
|
|
946
|
+
applyCasts,
|
|
947
|
+
Model
|
|
948
|
+
};
|