webspresso 0.0.6 → 0.0.8
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/README.md +506 -2
- package/bin/webspresso.js +357 -10
- package/core/applySchema.js +1 -0
- package/core/compileSchema.js +1 -0
- package/core/orm/eager-loader.js +232 -0
- package/core/orm/index.js +148 -0
- package/core/orm/migrations/index.js +205 -0
- package/core/orm/migrations/scaffold.js +312 -0
- package/core/orm/model.js +178 -0
- package/core/orm/query-builder.js +430 -0
- package/core/orm/repository.js +346 -0
- package/core/orm/schema-helpers.js +416 -0
- package/core/orm/scopes.js +183 -0
- package/core/orm/seeder.js +585 -0
- package/core/orm/transaction.js +69 -0
- package/core/orm/types.js +237 -0
- package/core/orm/utils.js +127 -0
- package/index.js +13 -1
- package/package.json +24 -3
- package/src/plugin-manager.js +1 -0
- package/utils/schemaCache.js +1 -0
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Webspresso ORM - Schema Helpers
|
|
3
|
+
* Wraps Zod with database metadata helpers
|
|
4
|
+
* @module core/orm/schema-helpers
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const METADATA_MARKER = '__wdb__';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Encode column metadata into a JSON string for Zod .describe()
|
|
11
|
+
* @param {import('./types').ColumnMeta} meta - Column metadata
|
|
12
|
+
* @returns {string} JSON-encoded metadata
|
|
13
|
+
*/
|
|
14
|
+
function encodeColumnMeta(meta) {
|
|
15
|
+
return JSON.stringify({ [METADATA_MARKER]: true, meta });
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Decode column metadata from Zod .describe() string
|
|
20
|
+
* @param {string} description - Zod description string
|
|
21
|
+
* @returns {import('./types').ColumnMeta|null} Decoded metadata or null
|
|
22
|
+
*/
|
|
23
|
+
function decodeColumnMeta(description) {
|
|
24
|
+
if (!description) return null;
|
|
25
|
+
try {
|
|
26
|
+
const parsed = JSON.parse(description);
|
|
27
|
+
if (parsed && parsed[METADATA_MARKER]) {
|
|
28
|
+
return parsed.meta;
|
|
29
|
+
}
|
|
30
|
+
} catch {
|
|
31
|
+
// Not our metadata, ignore
|
|
32
|
+
}
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Check if a Zod schema has ORM column metadata
|
|
38
|
+
* @param {import('zod').ZodTypeAny} schema - Zod schema
|
|
39
|
+
* @returns {boolean}
|
|
40
|
+
*/
|
|
41
|
+
function hasColumnMeta(schema) {
|
|
42
|
+
return decodeColumnMeta(schema.description) !== null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Get column metadata from a Zod schema
|
|
47
|
+
* @param {import('zod').ZodTypeAny} schema - Zod schema
|
|
48
|
+
* @returns {import('./types').ColumnMeta|null}
|
|
49
|
+
*/
|
|
50
|
+
function getColumnMeta(schema) {
|
|
51
|
+
return decodeColumnMeta(schema.description);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Create schema helpers bound to a Zod instance
|
|
56
|
+
* @param {typeof import('zod').z} z - Zod instance
|
|
57
|
+
* @returns {Object} Schema helpers (zdb)
|
|
58
|
+
*/
|
|
59
|
+
function createSchemaHelpers(z) {
|
|
60
|
+
/**
|
|
61
|
+
* Apply metadata to a Zod schema
|
|
62
|
+
* @param {import('zod').ZodTypeAny} schema - Base Zod schema
|
|
63
|
+
* @param {import('./types').ColumnMeta} meta - Column metadata
|
|
64
|
+
* @returns {import('zod').ZodTypeAny}
|
|
65
|
+
*/
|
|
66
|
+
function withMeta(schema, meta) {
|
|
67
|
+
return schema.describe(encodeColumnMeta(meta));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
/**
|
|
72
|
+
* Primary key column (bigint, auto-increment)
|
|
73
|
+
* @param {Partial<import('./types').ColumnMeta>} [options={}]
|
|
74
|
+
* @returns {import('zod').ZodNumber}
|
|
75
|
+
*/
|
|
76
|
+
id(options = {}) {
|
|
77
|
+
const schema = z.number().int().positive().optional();
|
|
78
|
+
return withMeta(schema, {
|
|
79
|
+
type: 'bigint',
|
|
80
|
+
primary: true,
|
|
81
|
+
autoIncrement: true,
|
|
82
|
+
...options,
|
|
83
|
+
});
|
|
84
|
+
},
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* UUID primary key column
|
|
88
|
+
* @param {Partial<import('./types').ColumnMeta>} [options={}]
|
|
89
|
+
* @returns {import('zod').ZodString}
|
|
90
|
+
*/
|
|
91
|
+
uuid(options = {}) {
|
|
92
|
+
const schema = z.string().uuid().optional();
|
|
93
|
+
return withMeta(schema, {
|
|
94
|
+
type: 'uuid',
|
|
95
|
+
primary: true,
|
|
96
|
+
...options,
|
|
97
|
+
});
|
|
98
|
+
},
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* String column (varchar)
|
|
102
|
+
* @param {Partial<import('./types').ColumnMeta>} [options={}]
|
|
103
|
+
* @returns {import('zod').ZodString}
|
|
104
|
+
*/
|
|
105
|
+
string(options = {}) {
|
|
106
|
+
const { maxLength = 255, nullable = false, ...rest } = options;
|
|
107
|
+
let schema = z.string().max(maxLength);
|
|
108
|
+
if (nullable) {
|
|
109
|
+
schema = schema.nullable().optional();
|
|
110
|
+
}
|
|
111
|
+
return withMeta(schema, {
|
|
112
|
+
type: 'string',
|
|
113
|
+
maxLength,
|
|
114
|
+
nullable,
|
|
115
|
+
...rest,
|
|
116
|
+
});
|
|
117
|
+
},
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Text column (unlimited length)
|
|
121
|
+
* @param {Partial<import('./types').ColumnMeta>} [options={}]
|
|
122
|
+
* @returns {import('zod').ZodString}
|
|
123
|
+
*/
|
|
124
|
+
text(options = {}) {
|
|
125
|
+
const { nullable = false, ...rest } = options;
|
|
126
|
+
let schema = z.string();
|
|
127
|
+
if (nullable) {
|
|
128
|
+
schema = schema.nullable().optional();
|
|
129
|
+
}
|
|
130
|
+
return withMeta(schema, {
|
|
131
|
+
type: 'text',
|
|
132
|
+
nullable,
|
|
133
|
+
...rest,
|
|
134
|
+
});
|
|
135
|
+
},
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Integer column
|
|
139
|
+
* @param {Partial<import('./types').ColumnMeta>} [options={}]
|
|
140
|
+
* @returns {import('zod').ZodNumber}
|
|
141
|
+
*/
|
|
142
|
+
integer(options = {}) {
|
|
143
|
+
const { nullable = false, ...rest } = options;
|
|
144
|
+
let schema = z.number().int();
|
|
145
|
+
if (nullable) {
|
|
146
|
+
schema = schema.nullable().optional();
|
|
147
|
+
}
|
|
148
|
+
return withMeta(schema, {
|
|
149
|
+
type: 'integer',
|
|
150
|
+
nullable,
|
|
151
|
+
...rest,
|
|
152
|
+
});
|
|
153
|
+
},
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Big integer column
|
|
157
|
+
* @param {Partial<import('./types').ColumnMeta>} [options={}]
|
|
158
|
+
* @returns {import('zod').ZodNumber}
|
|
159
|
+
*/
|
|
160
|
+
bigint(options = {}) {
|
|
161
|
+
const { nullable = false, ...rest } = options;
|
|
162
|
+
let schema = z.number().int();
|
|
163
|
+
if (nullable) {
|
|
164
|
+
schema = schema.nullable().optional();
|
|
165
|
+
}
|
|
166
|
+
return withMeta(schema, {
|
|
167
|
+
type: 'bigint',
|
|
168
|
+
nullable,
|
|
169
|
+
...rest,
|
|
170
|
+
});
|
|
171
|
+
},
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Float column
|
|
175
|
+
* @param {Partial<import('./types').ColumnMeta>} [options={}]
|
|
176
|
+
* @returns {import('zod').ZodNumber}
|
|
177
|
+
*/
|
|
178
|
+
float(options = {}) {
|
|
179
|
+
const { nullable = false, ...rest } = options;
|
|
180
|
+
let schema = z.number();
|
|
181
|
+
if (nullable) {
|
|
182
|
+
schema = schema.nullable().optional();
|
|
183
|
+
}
|
|
184
|
+
return withMeta(schema, {
|
|
185
|
+
type: 'float',
|
|
186
|
+
nullable,
|
|
187
|
+
...rest,
|
|
188
|
+
});
|
|
189
|
+
},
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Decimal column
|
|
193
|
+
* @param {Partial<import('./types').ColumnMeta>} [options={}]
|
|
194
|
+
* @returns {import('zod').ZodNumber}
|
|
195
|
+
*/
|
|
196
|
+
decimal(options = {}) {
|
|
197
|
+
const { precision = 10, scale = 2, nullable = false, ...rest } = options;
|
|
198
|
+
let schema = z.number();
|
|
199
|
+
if (nullable) {
|
|
200
|
+
schema = schema.nullable().optional();
|
|
201
|
+
}
|
|
202
|
+
return withMeta(schema, {
|
|
203
|
+
type: 'decimal',
|
|
204
|
+
precision,
|
|
205
|
+
scale,
|
|
206
|
+
nullable,
|
|
207
|
+
...rest,
|
|
208
|
+
});
|
|
209
|
+
},
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Boolean column
|
|
213
|
+
* @param {Partial<import('./types').ColumnMeta>} [options={}]
|
|
214
|
+
* @returns {import('zod').ZodBoolean}
|
|
215
|
+
*/
|
|
216
|
+
boolean(options = {}) {
|
|
217
|
+
const { nullable = false, default: defaultValue, ...rest } = options;
|
|
218
|
+
let schema = z.boolean();
|
|
219
|
+
if (defaultValue !== undefined) {
|
|
220
|
+
schema = schema.default(defaultValue);
|
|
221
|
+
}
|
|
222
|
+
if (nullable) {
|
|
223
|
+
schema = schema.nullable().optional();
|
|
224
|
+
}
|
|
225
|
+
return withMeta(schema, {
|
|
226
|
+
type: 'boolean',
|
|
227
|
+
nullable,
|
|
228
|
+
default: defaultValue,
|
|
229
|
+
...rest,
|
|
230
|
+
});
|
|
231
|
+
},
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Date column (date only, no time)
|
|
235
|
+
* @param {Partial<import('./types').ColumnMeta>} [options={}]
|
|
236
|
+
* @returns {import('zod').ZodDate}
|
|
237
|
+
*/
|
|
238
|
+
date(options = {}) {
|
|
239
|
+
const { nullable = false, ...rest } = options;
|
|
240
|
+
let schema = z.coerce.date();
|
|
241
|
+
if (nullable) {
|
|
242
|
+
schema = schema.nullable().optional();
|
|
243
|
+
}
|
|
244
|
+
return withMeta(schema, {
|
|
245
|
+
type: 'date',
|
|
246
|
+
nullable,
|
|
247
|
+
...rest,
|
|
248
|
+
});
|
|
249
|
+
},
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Datetime column
|
|
253
|
+
* @param {Partial<import('./types').ColumnMeta>} [options={}]
|
|
254
|
+
* @returns {import('zod').ZodDate}
|
|
255
|
+
*/
|
|
256
|
+
datetime(options = {}) {
|
|
257
|
+
const { nullable = false, ...rest } = options;
|
|
258
|
+
let schema = z.coerce.date();
|
|
259
|
+
if (nullable) {
|
|
260
|
+
schema = schema.nullable().optional();
|
|
261
|
+
}
|
|
262
|
+
return withMeta(schema, {
|
|
263
|
+
type: 'datetime',
|
|
264
|
+
nullable,
|
|
265
|
+
...rest,
|
|
266
|
+
});
|
|
267
|
+
},
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Timestamp column (with optional auto behavior)
|
|
271
|
+
* @param {Partial<import('./types').ColumnMeta>} [options={}]
|
|
272
|
+
* @returns {import('zod').ZodDate}
|
|
273
|
+
*/
|
|
274
|
+
timestamp(options = {}) {
|
|
275
|
+
const { nullable = false, auto, ...rest } = options;
|
|
276
|
+
let schema = z.coerce.date();
|
|
277
|
+
// Auto timestamps are always optional in input
|
|
278
|
+
if (nullable || auto) {
|
|
279
|
+
schema = schema.nullable().optional();
|
|
280
|
+
}
|
|
281
|
+
return withMeta(schema, {
|
|
282
|
+
type: 'timestamp',
|
|
283
|
+
nullable: nullable || !!auto,
|
|
284
|
+
auto,
|
|
285
|
+
...rest,
|
|
286
|
+
});
|
|
287
|
+
},
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* JSON column
|
|
291
|
+
* @param {Partial<import('./types').ColumnMeta>} [options={}]
|
|
292
|
+
* @returns {import('zod').ZodUnknown}
|
|
293
|
+
*/
|
|
294
|
+
json(options = {}) {
|
|
295
|
+
const { nullable = false, ...rest } = options;
|
|
296
|
+
let schema = z.unknown();
|
|
297
|
+
if (nullable) {
|
|
298
|
+
schema = schema.nullable().optional();
|
|
299
|
+
}
|
|
300
|
+
return withMeta(schema, {
|
|
301
|
+
type: 'json',
|
|
302
|
+
nullable,
|
|
303
|
+
...rest,
|
|
304
|
+
});
|
|
305
|
+
},
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Enum column
|
|
309
|
+
* @param {string[]} values - Allowed enum values
|
|
310
|
+
* @param {Partial<import('./types').ColumnMeta>} [options={}]
|
|
311
|
+
* @returns {import('zod').ZodEnum}
|
|
312
|
+
*/
|
|
313
|
+
enum(values, options = {}) {
|
|
314
|
+
const { nullable = false, default: defaultValue, ...rest } = options;
|
|
315
|
+
let schema = z.enum(values);
|
|
316
|
+
if (defaultValue !== undefined) {
|
|
317
|
+
schema = schema.default(defaultValue);
|
|
318
|
+
}
|
|
319
|
+
if (nullable) {
|
|
320
|
+
schema = schema.nullable().optional();
|
|
321
|
+
}
|
|
322
|
+
return withMeta(schema, {
|
|
323
|
+
type: 'enum',
|
|
324
|
+
enumValues: values,
|
|
325
|
+
nullable,
|
|
326
|
+
default: defaultValue,
|
|
327
|
+
...rest,
|
|
328
|
+
});
|
|
329
|
+
},
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Foreign key column (references another table)
|
|
333
|
+
* @param {string} references - Referenced table name
|
|
334
|
+
* @param {Partial<import('./types').ColumnMeta>} [options={}]
|
|
335
|
+
* @returns {import('zod').ZodNumber}
|
|
336
|
+
*/
|
|
337
|
+
foreignKey(references, options = {}) {
|
|
338
|
+
const { referenceColumn = 'id', nullable = false, ...rest } = options;
|
|
339
|
+
let schema = z.number().int().positive();
|
|
340
|
+
if (nullable) {
|
|
341
|
+
schema = schema.nullable().optional();
|
|
342
|
+
}
|
|
343
|
+
return withMeta(schema, {
|
|
344
|
+
type: 'bigint',
|
|
345
|
+
references,
|
|
346
|
+
referenceColumn,
|
|
347
|
+
nullable,
|
|
348
|
+
...rest,
|
|
349
|
+
});
|
|
350
|
+
},
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* UUID foreign key column
|
|
354
|
+
* @param {string} references - Referenced table name
|
|
355
|
+
* @param {Partial<import('./types').ColumnMeta>} [options={}]
|
|
356
|
+
* @returns {import('zod').ZodString}
|
|
357
|
+
*/
|
|
358
|
+
foreignUuid(references, options = {}) {
|
|
359
|
+
const { referenceColumn = 'id', nullable = false, ...rest } = options;
|
|
360
|
+
let schema = z.string().uuid();
|
|
361
|
+
if (nullable) {
|
|
362
|
+
schema = schema.nullable().optional();
|
|
363
|
+
}
|
|
364
|
+
return withMeta(schema, {
|
|
365
|
+
type: 'uuid',
|
|
366
|
+
references,
|
|
367
|
+
referenceColumn,
|
|
368
|
+
nullable,
|
|
369
|
+
...rest,
|
|
370
|
+
});
|
|
371
|
+
},
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Extract all column metadata from a Zod object schema
|
|
377
|
+
* @param {import('zod').ZodObject} schema - Zod object schema
|
|
378
|
+
* @returns {Map<string, import('./types').ColumnMeta>}
|
|
379
|
+
*/
|
|
380
|
+
function extractColumnsFromSchema(schema) {
|
|
381
|
+
const columns = new Map();
|
|
382
|
+
const shape = schema.shape;
|
|
383
|
+
|
|
384
|
+
for (const [key, fieldSchema] of Object.entries(shape)) {
|
|
385
|
+
// Unwrap optional/nullable wrappers to get to the base schema
|
|
386
|
+
let current = fieldSchema;
|
|
387
|
+
while (current._def) {
|
|
388
|
+
if (current._def.innerType) {
|
|
389
|
+
current = current._def.innerType;
|
|
390
|
+
} else if (current._def.schema) {
|
|
391
|
+
current = current._def.schema;
|
|
392
|
+
} else {
|
|
393
|
+
break;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// Check the original field schema for metadata
|
|
398
|
+
const meta = getColumnMeta(fieldSchema) || getColumnMeta(current);
|
|
399
|
+
if (meta) {
|
|
400
|
+
columns.set(key, meta);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
return columns;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
module.exports = {
|
|
408
|
+
createSchemaHelpers,
|
|
409
|
+
encodeColumnMeta,
|
|
410
|
+
decodeColumnMeta,
|
|
411
|
+
hasColumnMeta,
|
|
412
|
+
getColumnMeta,
|
|
413
|
+
extractColumnsFromSchema,
|
|
414
|
+
METADATA_MARKER,
|
|
415
|
+
};
|
|
416
|
+
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Webspresso ORM - Scope Modifiers
|
|
3
|
+
* Global scopes for soft delete, timestamps, and multi-tenancy
|
|
4
|
+
* @module core/orm/scopes
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Apply soft delete scope to a query builder
|
|
9
|
+
* @param {import('knex').Knex.QueryBuilder} qb - Knex query builder
|
|
10
|
+
* @param {import('./types').ScopeContext} context - Scope context
|
|
11
|
+
* @param {import('./types').ModelDefinition} model - Model definition
|
|
12
|
+
* @returns {import('knex').Knex.QueryBuilder}
|
|
13
|
+
*/
|
|
14
|
+
function applySoftDeleteScope(qb, context, model) {
|
|
15
|
+
if (!model.scopes.softDelete) {
|
|
16
|
+
return qb;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// If onlyTrashed is set, only return deleted records
|
|
20
|
+
if (context.onlyTrashed) {
|
|
21
|
+
return qb.whereNotNull('deleted_at');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// If withTrashed is set, return all records (no filter)
|
|
25
|
+
if (context.withTrashed) {
|
|
26
|
+
return qb;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Default: only return non-deleted records
|
|
30
|
+
return qb.whereNull('deleted_at');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Apply tenant scope to a query builder
|
|
35
|
+
* @param {import('knex').Knex.QueryBuilder} qb - Knex query builder
|
|
36
|
+
* @param {import('./types').ScopeContext} context - Scope context
|
|
37
|
+
* @param {import('./types').ModelDefinition} model - Model definition
|
|
38
|
+
* @returns {import('knex').Knex.QueryBuilder}
|
|
39
|
+
*/
|
|
40
|
+
function applyTenantScope(qb, context, model) {
|
|
41
|
+
const tenantColumn = model.scopes.tenant;
|
|
42
|
+
if (!tenantColumn || context.tenantId === undefined) {
|
|
43
|
+
return qb;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return qb.where(tenantColumn, context.tenantId);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Apply all global scopes to a query builder
|
|
51
|
+
* @param {import('knex').Knex.QueryBuilder} qb - Knex query builder
|
|
52
|
+
* @param {import('./types').ScopeContext} context - Scope context
|
|
53
|
+
* @param {import('./types').ModelDefinition} model - Model definition
|
|
54
|
+
* @returns {import('knex').Knex.QueryBuilder}
|
|
55
|
+
*/
|
|
56
|
+
function applyScopes(qb, context, model) {
|
|
57
|
+
let builder = qb;
|
|
58
|
+
builder = applySoftDeleteScope(builder, context, model);
|
|
59
|
+
builder = applyTenantScope(builder, context, model);
|
|
60
|
+
return builder;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Apply timestamp values for insert operations
|
|
65
|
+
* @param {Object} data - Data to insert
|
|
66
|
+
* @param {import('./types').ModelDefinition} model - Model definition
|
|
67
|
+
* @returns {Object} Data with timestamps applied
|
|
68
|
+
*/
|
|
69
|
+
function applyInsertTimestamps(data, model) {
|
|
70
|
+
if (!model.scopes.timestamps) {
|
|
71
|
+
return data;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const now = new Date();
|
|
75
|
+
return {
|
|
76
|
+
...data,
|
|
77
|
+
created_at: data.created_at || now,
|
|
78
|
+
updated_at: data.updated_at || now,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Apply timestamp values for update operations
|
|
84
|
+
* @param {Object} data - Data to update
|
|
85
|
+
* @param {import('./types').ModelDefinition} model - Model definition
|
|
86
|
+
* @returns {Object} Data with timestamps applied
|
|
87
|
+
*/
|
|
88
|
+
function applyUpdateTimestamps(data, model) {
|
|
89
|
+
if (!model.scopes.timestamps) {
|
|
90
|
+
return data;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
...data,
|
|
95
|
+
updated_at: new Date(),
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Apply tenant ID for insert operations
|
|
101
|
+
* @param {Object} data - Data to insert
|
|
102
|
+
* @param {import('./types').ScopeContext} context - Scope context
|
|
103
|
+
* @param {import('./types').ModelDefinition} model - Model definition
|
|
104
|
+
* @returns {Object} Data with tenant ID applied
|
|
105
|
+
*/
|
|
106
|
+
function applyInsertTenant(data, context, model) {
|
|
107
|
+
const tenantColumn = model.scopes.tenant;
|
|
108
|
+
if (!tenantColumn || context.tenantId === undefined) {
|
|
109
|
+
return data;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
...data,
|
|
114
|
+
[tenantColumn]: data[tenantColumn] || context.tenantId,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Get soft delete data (for UPDATE instead of DELETE)
|
|
120
|
+
* @returns {Object} Soft delete update data
|
|
121
|
+
*/
|
|
122
|
+
function getSoftDeleteData() {
|
|
123
|
+
return { deleted_at: new Date() };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Get restore data (to undo soft delete)
|
|
128
|
+
* @returns {Object} Restore update data
|
|
129
|
+
*/
|
|
130
|
+
function getRestoreData() {
|
|
131
|
+
return { deleted_at: null };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Apply all insert modifiers (timestamps, tenant)
|
|
136
|
+
* @param {Object} data - Data to insert
|
|
137
|
+
* @param {import('./types').ScopeContext} context - Scope context
|
|
138
|
+
* @param {import('./types').ModelDefinition} model - Model definition
|
|
139
|
+
* @returns {Object} Modified data
|
|
140
|
+
*/
|
|
141
|
+
function applyInsertModifiers(data, context, model) {
|
|
142
|
+
let modified = { ...data };
|
|
143
|
+
modified = applyInsertTimestamps(modified, model);
|
|
144
|
+
modified = applyInsertTenant(modified, context, model);
|
|
145
|
+
return modified;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Apply all update modifiers (timestamps)
|
|
150
|
+
* @param {Object} data - Data to update
|
|
151
|
+
* @param {import('./types').ModelDefinition} model - Model definition
|
|
152
|
+
* @returns {Object} Modified data
|
|
153
|
+
*/
|
|
154
|
+
function applyUpdateModifiers(data, model) {
|
|
155
|
+
return applyUpdateTimestamps(data, model);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Create default scope context
|
|
160
|
+
* @returns {import('./types').ScopeContext}
|
|
161
|
+
*/
|
|
162
|
+
function createScopeContext() {
|
|
163
|
+
return {
|
|
164
|
+
tenantId: undefined,
|
|
165
|
+
withTrashed: false,
|
|
166
|
+
onlyTrashed: false,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
module.exports = {
|
|
171
|
+
applySoftDeleteScope,
|
|
172
|
+
applyTenantScope,
|
|
173
|
+
applyScopes,
|
|
174
|
+
applyInsertTimestamps,
|
|
175
|
+
applyUpdateTimestamps,
|
|
176
|
+
applyInsertTenant,
|
|
177
|
+
applyInsertModifiers,
|
|
178
|
+
applyUpdateModifiers,
|
|
179
|
+
getSoftDeleteData,
|
|
180
|
+
getRestoreData,
|
|
181
|
+
createScopeContext,
|
|
182
|
+
};
|
|
183
|
+
|