@atscript/moost-db 0.1.34 → 0.1.36

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/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2025 Atscript
3
+ Copyright (c) 2025-present Artem Maltsev
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/dist/index.cjs CHANGED
@@ -26,19 +26,32 @@ const __atscript_typescript_utils = __toESM(require("@atscript/typescript/utils"
26
26
  const __moostjs_event_http = __toESM(require("@moostjs/event-http"));
27
27
  const moost = __toESM(require("moost"));
28
28
  const __uniqu_url = __toESM(require("@uniqu/url"));
29
+ const __atscript_utils_db = __toESM(require("@atscript/utils-db"));
29
30
 
30
31
  //#region packages/moost-db/src/decorators.ts
31
- const TABLE_DEF = "__atscript_db_table_def";
32
+ const READABLE_DEF = "__atscript_db_readable_def";
33
+ const TABLE_DEF = READABLE_DEF;
32
34
  const TableController = (table, prefix) => (0, moost.ApplyDecorators)((0, moost.Provide)(TABLE_DEF, () => table), (0, moost.Controller)(prefix || table.tableName), (0, moost.Inherit)());
35
+ const ReadableController = (readable, prefix) => (0, moost.ApplyDecorators)((0, moost.Provide)(READABLE_DEF, () => readable), (0, moost.Controller)(prefix || readable.tableName), (0, moost.Inherit)());
36
+ const ViewController = ReadableController;
33
37
 
34
38
  //#endregion
35
39
  //#region packages/moost-db/src/validation-interceptor.ts
40
+ const dbErrorCodeToStatus = { CONFLICT: 409 };
36
41
  function transformValidationError(error, reply) {
37
42
  if (error instanceof __atscript_typescript_utils.ValidatorError) reply(new __moostjs_event_http.HttpError(400, {
38
43
  message: error.message,
39
44
  statusCode: 400,
40
45
  errors: error.errors
41
46
  }));
47
+ else if (error instanceof __atscript_utils_db.DbError) {
48
+ const statusCode = dbErrorCodeToStatus[error.code] ?? 400;
49
+ reply(new __moostjs_event_http.HttpError(statusCode, {
50
+ message: error.message,
51
+ statusCode,
52
+ errors: error.errors
53
+ }));
54
+ }
42
55
  }
43
56
  const validationErrorTransform = () => (0, moost.defineInterceptor)({ error: transformValidationError }, moost.TInterceptorPriority.CATCH_ERROR);
44
57
  const UseValidationErrorTransform = () => (0, moost.Intercept)(validationErrorTransform());
@@ -145,7 +158,7 @@ _define_property$1(SelectControlDto, "id", "SelectControlDto");
145
158
  (0, __atscript_typescript_utils.defineAnnotatedType)("object", SelectControlDto).propPattern(/./, (0, __atscript_typescript_utils.defineAnnotatedType)("union").item((0, __atscript_typescript_utils.defineAnnotatedType)().designType("number").value(1).$type).item((0, __atscript_typescript_utils.defineAnnotatedType)().designType("number").value(0).$type).$type);
146
159
 
147
160
  //#endregion
148
- //#region packages/moost-db/src/as-db.controller.ts
161
+ //#region packages/moost-db/src/as-db-readable.controller.ts
149
162
  function _define_property(obj, key, value) {
150
163
  if (key in obj) Object.defineProperty(obj, key, {
151
164
  value,
@@ -156,21 +169,21 @@ function _define_property(obj, key, value) {
156
169
  else obj[key] = value;
157
170
  return obj;
158
171
  }
159
- function _ts_decorate(decorators, target, key, desc) {
172
+ function _ts_decorate$1(decorators, target, key, desc) {
160
173
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
161
174
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
162
175
  else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
163
176
  return c > 3 && r && Object.defineProperty(target, key, r), r;
164
177
  }
165
- function _ts_metadata(k, v) {
178
+ function _ts_metadata$1(k, v) {
166
179
  if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
167
180
  }
168
- function _ts_param(paramIndex, decorator) {
181
+ function _ts_param$1(paramIndex, decorator) {
169
182
  return function(target, key) {
170
183
  decorator(target, key, paramIndex);
171
184
  };
172
185
  }
173
- var AsDbController = class {
186
+ var AsDbReadableController = class {
174
187
  /**
175
188
  * One-time initialization hook. Override to seed data, register watchers, etc.
176
189
  */ init() {}
@@ -192,7 +205,7 @@ var AsDbController = class {
192
205
  return undefined;
193
206
  }
194
207
  validateInsights(insights) {
195
- for (const key of insights.keys()) if (!this.table.flatMap.has(key)) return `Unknown field "${key}"`;
208
+ for (const key of insights.keys()) if (!this.readable.flatMap.has(key)) return `Unknown field "${key}"`;
196
209
  return undefined;
197
210
  }
198
211
  validateParsed(parsed, type) {
@@ -202,6 +215,18 @@ var AsDbController = class {
202
215
  const insightsError = this.validateInsights(parsed.insights);
203
216
  if (insightsError) return new __moostjs_event_http.HttpError(400, insightsError);
204
217
  }
218
+ const withRelations = parsed.controls.$with;
219
+ if (withRelations?.length) {
220
+ const relations = this.readable.relations;
221
+ for (const rel of withRelations) if (!rel.name.includes(".") && !relations.has(rel.name)) return new __moostjs_event_http.HttpError(400, {
222
+ message: `Unknown relation "${rel.name}" in $with. Available relations: ${[...relations.keys()].join(", ") || "(none)"}`,
223
+ statusCode: 400,
224
+ errors: [{
225
+ path: "$with",
226
+ message: `Unknown relation "${rel.name}"`
227
+ }]
228
+ });
229
+ }
205
230
  return undefined;
206
231
  }
207
232
  /**
@@ -214,16 +239,6 @@ var AsDbController = class {
214
239
  */ transformProjection(projection) {
215
240
  return projection;
216
241
  }
217
- /**
218
- * Intercepts write operations. Return `undefined` to abort.
219
- */ onWrite(action, data) {
220
- return data;
221
- }
222
- /**
223
- * Intercepts delete operations. Return `undefined` to abort.
224
- */ onRemove(id) {
225
- return id;
226
- }
227
242
  parseQueryString(url) {
228
243
  const idx = url.indexOf("?");
229
244
  return (0, __uniqu_url.parseUrl)(idx >= 0 ? url.slice(idx + 1) : "");
@@ -234,6 +249,38 @@ var AsDbController = class {
234
249
  return item;
235
250
  }
236
251
  /**
252
+ * Extracts a composite identifier object from query params.
253
+ * Tries composite primary key first, then compound unique indexes.
254
+ */ extractCompositeId(query) {
255
+ const pkFields = this.readable.primaryKeys;
256
+ if (pkFields.length > 1) {
257
+ const idObj = {};
258
+ let allPresent = true;
259
+ for (const field of pkFields) {
260
+ if (query[field] === undefined) {
261
+ allPresent = false;
262
+ break;
263
+ }
264
+ idObj[field] = query[field];
265
+ }
266
+ if (allPresent) return idObj;
267
+ }
268
+ for (const index of this.readable.indexes.values()) {
269
+ if (index.type !== "unique" || index.fields.length < 2) continue;
270
+ const idObj = {};
271
+ let allPresent = true;
272
+ for (const indexField of index.fields) {
273
+ if (query[indexField.name] === undefined) {
274
+ allPresent = false;
275
+ break;
276
+ }
277
+ idObj[indexField.name] = query[indexField.name];
278
+ }
279
+ if (allPresent) return idObj;
280
+ }
281
+ return new __moostjs_event_http.HttpError(400, "Query params do not match any composite primary key or compound unique index");
282
+ }
283
+ /**
237
284
  * **GET /query** — returns an array of records or a count.
238
285
  */ async query(url) {
239
286
  const parsed = this.parseQueryString(url);
@@ -242,7 +289,7 @@ var AsDbController = class {
242
289
  const controls = parsed.controls;
243
290
  const filter = this.transformFilter(parsed.filter);
244
291
  const select = this.transformProjection(controls.$select);
245
- if (controls.$count) return this.table.count({
292
+ if (controls.$count) return this.readable.count({
246
293
  filter,
247
294
  controls: {
248
295
  ...controls,
@@ -251,7 +298,7 @@ var AsDbController = class {
251
298
  });
252
299
  const searchTerm = controls.$search;
253
300
  const indexName = controls.$index;
254
- if (searchTerm && this.table.isSearchable()) return this.table.search(searchTerm, {
301
+ if (searchTerm && this.readable.isSearchable()) return this.readable.search(searchTerm, {
255
302
  filter,
256
303
  controls: {
257
304
  ...controls,
@@ -259,7 +306,7 @@ var AsDbController = class {
259
306
  $limit: controls.$limit || 1e3
260
307
  }
261
308
  }, indexName);
262
- return this.table.findMany({
309
+ return this.readable.findMany({
263
310
  filter,
264
311
  controls: {
265
312
  ...controls,
@@ -292,8 +339,8 @@ var AsDbController = class {
292
339
  }
293
340
  };
294
341
  let result;
295
- if (searchTerm && this.table.isSearchable()) result = await this.table.searchWithCount(searchTerm, query, indexName);
296
- else result = await this.table.findManyWithCount(query);
342
+ if (searchTerm && this.readable.isSearchable()) result = await this.readable.searchWithCount(searchTerm, query, indexName);
343
+ else result = await this.readable.findManyWithCount(query);
297
344
  return {
298
345
  data: result.data,
299
346
  page,
@@ -310,31 +357,164 @@ else result = await this.table.findManyWithCount(query);
310
357
  const error = this.validateParsed(parsed, "getOne");
311
358
  if (error) return error;
312
359
  const select = this.transformProjection(parsed.controls.$select);
313
- return this.returnOne(this.table.findById(id, { $select: select }));
360
+ const controls = {
361
+ ...parsed.controls,
362
+ $select: select
363
+ };
364
+ return this.returnOne(this.readable.findById(id, { controls }));
365
+ }
366
+ /**
367
+ * **GET /one?field1=val1&field2=val2** — retrieves a single record by composite key
368
+ * (composite primary key or compound unique index).
369
+ */ async getOneComposite(query, url) {
370
+ const idObj = this.extractCompositeId(query);
371
+ if (idObj instanceof __moostjs_event_http.HttpError) return idObj;
372
+ const parsed = this.parseQueryString(url);
373
+ const select = this.transformProjection(parsed.controls.$select);
374
+ const controls = {
375
+ ...parsed.controls,
376
+ $select: select
377
+ };
378
+ return this.returnOne(this.readable.findById(idObj, { controls }));
379
+ }
380
+ /**
381
+ * **GET /meta** — returns table/view metadata for UI.
382
+ */ meta() {
383
+ return {
384
+ searchable: this.readable.isSearchable(),
385
+ searchIndexes: this._searchIndexes,
386
+ type: this._serializedType
387
+ };
388
+ }
389
+ constructor(readable, app) {
390
+ /** Reference to the underlying readable (table or view). */ _define_property(this, "readable", void 0);
391
+ /** Application-scoped logger. */ _define_property(this, "logger", void 0);
392
+ /** Cached serialized type definition (static, computed once). */ _define_property(this, "_serializedType", void 0);
393
+ /** Cached search index list (static, computed once). */ _define_property(this, "_searchIndexes", void 0);
394
+ _define_property(this, "_queryControlsValidator", void 0);
395
+ _define_property(this, "_pagesControlsValidator", void 0);
396
+ _define_property(this, "_getOneControlsValidator", void 0);
397
+ this.readable = readable;
398
+ this._serializedType = (0, __atscript_typescript_utils.serializeAnnotatedType)(readable.type);
399
+ this._searchIndexes = readable.getSearchIndexes();
400
+ this.logger = app.getLogger(`db [${readable.tableName}]`);
401
+ this.logger.info(`Initializing ${readable.isView ? "view" : "table"} controller`);
402
+ try {
403
+ const p = this.init();
404
+ if (p instanceof Promise) p.catch((error) => {
405
+ this.logger.error(error);
406
+ });
407
+ } catch (error) {
408
+ this.logger.error(error);
409
+ throw error;
410
+ }
411
+ }
412
+ };
413
+ _ts_decorate$1([
414
+ (0, __moostjs_event_http.Get)("query"),
415
+ _ts_param$1(0, (0, __moostjs_event_http.Url)()),
416
+ _ts_metadata$1("design:type", Function),
417
+ _ts_metadata$1("design:paramtypes", [String]),
418
+ _ts_metadata$1("design:returntype", Promise)
419
+ ], AsDbReadableController.prototype, "query", null);
420
+ _ts_decorate$1([
421
+ (0, __moostjs_event_http.Get)("pages"),
422
+ _ts_param$1(0, (0, __moostjs_event_http.Url)()),
423
+ _ts_metadata$1("design:type", Function),
424
+ _ts_metadata$1("design:paramtypes", [String]),
425
+ _ts_metadata$1("design:returntype", Promise)
426
+ ], AsDbReadableController.prototype, "pages", null);
427
+ _ts_decorate$1([
428
+ (0, __moostjs_event_http.Get)("one/:id"),
429
+ _ts_param$1(0, (0, moost.Param)("id")),
430
+ _ts_param$1(1, (0, __moostjs_event_http.Url)()),
431
+ _ts_metadata$1("design:type", Function),
432
+ _ts_metadata$1("design:paramtypes", [String, String]),
433
+ _ts_metadata$1("design:returntype", Promise)
434
+ ], AsDbReadableController.prototype, "getOne", null);
435
+ _ts_decorate$1([
436
+ (0, __moostjs_event_http.Get)("one"),
437
+ _ts_param$1(0, (0, __moostjs_event_http.Query)()),
438
+ _ts_param$1(1, (0, __moostjs_event_http.Url)()),
439
+ _ts_metadata$1("design:type", Function),
440
+ _ts_metadata$1("design:paramtypes", [typeof Record === "undefined" ? Object : Record, String]),
441
+ _ts_metadata$1("design:returntype", Promise)
442
+ ], AsDbReadableController.prototype, "getOneComposite", null);
443
+ _ts_decorate$1([
444
+ (0, __moostjs_event_http.Get)("meta"),
445
+ _ts_metadata$1("design:type", Function),
446
+ _ts_metadata$1("design:paramtypes", []),
447
+ _ts_metadata$1("design:returntype", void 0)
448
+ ], AsDbReadableController.prototype, "meta", null);
449
+ AsDbReadableController = _ts_decorate$1([
450
+ UseValidationErrorTransform(),
451
+ _ts_param$1(0, (0, moost.Inject)(READABLE_DEF)),
452
+ _ts_metadata$1("design:type", Function),
453
+ _ts_metadata$1("design:paramtypes", [typeof AtscriptDbReadable === "undefined" ? Object : AtscriptDbReadable, typeof moost.Moost === "undefined" ? Object : moost.Moost])
454
+ ], AsDbReadableController);
455
+
456
+ //#endregion
457
+ //#region packages/moost-db/src/as-db.controller.ts
458
+ function _ts_decorate(decorators, target, key, desc) {
459
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
460
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
461
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
462
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
463
+ }
464
+ function _ts_metadata(k, v) {
465
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
466
+ }
467
+ function _ts_param(paramIndex, decorator) {
468
+ return function(target, key) {
469
+ decorator(target, key, paramIndex);
470
+ };
471
+ }
472
+ var AsDbController = class extends AsDbReadableController {
473
+ /** Reference to the underlying table (typed for write access). */ get table() {
474
+ return this.readable;
475
+ }
476
+ /**
477
+ * Intercepts write operations. Return `undefined` to abort.
478
+ */ onWrite(action, data) {
479
+ return data;
480
+ }
481
+ /**
482
+ * Intercepts delete operations. Return `undefined` to abort.
483
+ */ onRemove(id) {
484
+ return id;
314
485
  }
315
486
  /**
316
487
  * **POST /** — inserts one or many records.
317
488
  */ async insert(payload) {
318
- const arr = Array.isArray(payload) ? payload : [payload];
319
- if (arr.length === 1) {
320
- const data$1 = await this.onWrite("insert", arr[0]);
489
+ if (Array.isArray(payload)) {
490
+ const data$1 = await this.onWrite("insertMany", payload);
321
491
  if (data$1 === undefined) return new __moostjs_event_http.HttpError(500, "Not saved");
322
- return await this.table.insertOne(data$1);
492
+ return await this.table.insertMany(data$1);
323
493
  }
324
- const data = await this.onWrite("insertMany", arr);
494
+ const data = await this.onWrite("insert", payload);
325
495
  if (data === undefined) return new __moostjs_event_http.HttpError(500, "Not saved");
326
- return await this.table.insertMany(data);
496
+ return await this.table.insertOne(data);
327
497
  }
328
498
  /**
329
- * **PUT /** — fully replaces a record matched by primary key.
499
+ * **PUT /** — fully replaces one or many records matched by primary key.
330
500
  */ async replace(payload) {
501
+ if (Array.isArray(payload)) {
502
+ const data$1 = await this.onWrite("replaceMany", payload);
503
+ if (data$1 === undefined) return new __moostjs_event_http.HttpError(500, "Not saved");
504
+ return await this.table.bulkReplace(data$1);
505
+ }
331
506
  const data = await this.onWrite("replace", payload);
332
507
  if (data === undefined) return new __moostjs_event_http.HttpError(500, "Not saved");
333
508
  return await this.table.replaceOne(data);
334
509
  }
335
510
  /**
336
- * **PATCH /** — partially updates a record matched by primary key.
511
+ * **PATCH /** — partially updates one or many records matched by primary key.
337
512
  */ async update(payload) {
513
+ if (Array.isArray(payload)) {
514
+ const data$1 = await this.onWrite("updateMany", payload);
515
+ if (data$1 === undefined) return new __moostjs_event_http.HttpError(500, "Not saved");
516
+ return await this.table.bulkUpdate(data$1);
517
+ }
338
518
  const data = await this.onWrite("update", payload);
339
519
  if (data === undefined) return new __moostjs_event_http.HttpError(500, "Not saved");
340
520
  return await this.table.updateOne(data);
@@ -349,60 +529,21 @@ else result = await this.table.findManyWithCount(query);
349
529
  return result;
350
530
  }
351
531
  /**
352
- * **GET /meta** — returns table metadata for UI.
353
- */ meta() {
354
- return {
355
- searchable: this.table.isSearchable(),
356
- searchIndexes: this._searchIndexes,
357
- type: this._serializedType
358
- };
532
+ * **DELETE /?field1=val1&field2=val2** — removes a record by composite key
533
+ * (composite primary key or compound unique index).
534
+ */ async removeComposite(query) {
535
+ const idObj = this.extractCompositeId(query);
536
+ if (idObj instanceof __moostjs_event_http.HttpError) return idObj;
537
+ const resolvedId = await this.onRemove(idObj);
538
+ if (resolvedId === undefined) return new __moostjs_event_http.HttpError(500, "Not deleted");
539
+ const result = await this.table.deleteOne(resolvedId);
540
+ if (result.deletedCount < 1) return new __moostjs_event_http.HttpError(404);
541
+ return result;
359
542
  }
360
543
  constructor(table, app) {
361
- /** Reference to the underlying table. */ _define_property(this, "table", void 0);
362
- /** Application-scoped logger. */ _define_property(this, "logger", void 0);
363
- /** Cached serialized type definition (static, computed once). */ _define_property(this, "_serializedType", void 0);
364
- /** Cached search index list (static, computed once). */ _define_property(this, "_searchIndexes", void 0);
365
- _define_property(this, "_queryControlsValidator", void 0);
366
- _define_property(this, "_pagesControlsValidator", void 0);
367
- _define_property(this, "_getOneControlsValidator", void 0);
368
- this.table = table;
369
- this._serializedType = (0, __atscript_typescript_utils.serializeAnnotatedType)(table.type);
370
- this._searchIndexes = table.getSearchIndexes();
371
- this.logger = app.getLogger(`db [${table.tableName}]`);
372
- this.logger.info("Initializing table controller");
373
- try {
374
- const p = this.init();
375
- if (p instanceof Promise) p.catch((error) => {
376
- this.logger.error(error);
377
- });
378
- } catch (error) {
379
- this.logger.error(error);
380
- throw error;
381
- }
544
+ super(table, app);
382
545
  }
383
546
  };
384
- _ts_decorate([
385
- (0, __moostjs_event_http.Get)("query"),
386
- _ts_param(0, (0, __moostjs_event_http.Url)()),
387
- _ts_metadata("design:type", Function),
388
- _ts_metadata("design:paramtypes", [String]),
389
- _ts_metadata("design:returntype", Promise)
390
- ], AsDbController.prototype, "query", null);
391
- _ts_decorate([
392
- (0, __moostjs_event_http.Get)("pages"),
393
- _ts_param(0, (0, __moostjs_event_http.Url)()),
394
- _ts_metadata("design:type", Function),
395
- _ts_metadata("design:paramtypes", [String]),
396
- _ts_metadata("design:returntype", Promise)
397
- ], AsDbController.prototype, "pages", null);
398
- _ts_decorate([
399
- (0, __moostjs_event_http.Get)("one/:id"),
400
- _ts_param(0, (0, moost.Param)("id")),
401
- _ts_param(1, (0, __moostjs_event_http.Url)()),
402
- _ts_metadata("design:type", Function),
403
- _ts_metadata("design:paramtypes", [String, String]),
404
- _ts_metadata("design:returntype", Promise)
405
- ], AsDbController.prototype, "getOne", null);
406
547
  _ts_decorate([
407
548
  (0, __moostjs_event_http.Post)(""),
408
549
  _ts_param(0, (0, __moostjs_event_http.Body)()),
@@ -432,13 +573,14 @@ _ts_decorate([
432
573
  _ts_metadata("design:returntype", Promise)
433
574
  ], AsDbController.prototype, "remove", null);
434
575
  _ts_decorate([
435
- (0, __moostjs_event_http.Get)("meta"),
576
+ (0, __moostjs_event_http.Delete)(""),
577
+ _ts_param(0, (0, __moostjs_event_http.Query)()),
436
578
  _ts_metadata("design:type", Function),
437
- _ts_metadata("design:paramtypes", []),
438
- _ts_metadata("design:returntype", void 0)
439
- ], AsDbController.prototype, "meta", null);
579
+ _ts_metadata("design:paramtypes", [typeof Record === "undefined" ? Object : Record]),
580
+ _ts_metadata("design:returntype", Promise)
581
+ ], AsDbController.prototype, "removeComposite", null);
440
582
  AsDbController = _ts_decorate([
441
- UseValidationErrorTransform(),
583
+ (0, moost.Inherit)(),
442
584
  _ts_param(0, (0, moost.Inject)(TABLE_DEF)),
443
585
  _ts_metadata("design:type", Function),
444
586
  _ts_metadata("design:paramtypes", [typeof AtscriptDbTable === "undefined" ? Object : AtscriptDbTable, typeof moost.Moost === "undefined" ? Object : moost.Moost])
@@ -451,7 +593,16 @@ Object.defineProperty(exports, 'AsDbController', {
451
593
  return AsDbController;
452
594
  }
453
595
  });
596
+ Object.defineProperty(exports, 'AsDbReadableController', {
597
+ enumerable: true,
598
+ get: function () {
599
+ return AsDbReadableController;
600
+ }
601
+ });
602
+ exports.READABLE_DEF = READABLE_DEF
603
+ exports.ReadableController = ReadableController
454
604
  exports.TABLE_DEF = TABLE_DEF
455
605
  exports.TableController = TableController
456
606
  exports.UseValidationErrorTransform = UseValidationErrorTransform
607
+ exports.ViewController = ViewController
457
608
  exports.validationErrorTransform = validationErrorTransform
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import * as _atscript_typescript_serialize from '@atscript/typescript/serialize';
2
2
  import * as _atscript_utils_db from '@atscript/utils-db';
3
- import { AtscriptDbTable, Uniquery, FilterExpr, UniqueryControls } from '@atscript/utils-db';
3
+ import { AtscriptDbReadable, Uniquery, FilterExpr, UniqueryControls, AtscriptDbTable } from '@atscript/utils-db';
4
4
  import * as _uniqu_url from '@uniqu/url';
5
5
  import { TAtscriptAnnotatedType, TAtscriptDataType, Validator } from '@atscript/typescript/utils';
6
6
  import { HttpError } from '@moostjs/event-http';
@@ -8,27 +8,22 @@ import * as moost from 'moost';
8
8
  import { TConsoleBase, Moost } from 'moost';
9
9
 
10
10
  /**
11
- * Generic database controller for Moost that works with any `AtscriptDbTable` +
12
- * `BaseDbAdapter`. All CRUD routes through the generic table layer — no
13
- * adapter-specific imports.
11
+ * Read-only database controller for Moost that works with any `AtscriptDbReadable`
12
+ * (tables or views). Provides query, pages, getOne, and meta endpoints.
14
13
  *
15
- * Subclass and provide the table via DI:
16
- * ```ts
17
- * ‎@Provide(TABLE_DEF, () => driver.getTable(MyType))
18
- * ‎@TableController(MyType)
19
- * export class MyController extends AsDbController<typeof MyType> {}
20
- * ```
14
+ * For write operations (insert, replace, update, delete), use {@link AsDbController}.
15
+ * For views, use {@link AsDbViewController}.
21
16
  */
22
- declare class AsDbController<T extends TAtscriptAnnotatedType = TAtscriptAnnotatedType, DataType = TAtscriptDataType<T>> {
23
- /** Reference to the underlying table. */
24
- protected table: AtscriptDbTable<T>;
17
+ declare class AsDbReadableController<T extends TAtscriptAnnotatedType = TAtscriptAnnotatedType, DataType = TAtscriptDataType<T>> {
18
+ /** Reference to the underlying readable (table or view). */
19
+ protected readable: AtscriptDbReadable<T>;
25
20
  /** Application-scoped logger. */
26
21
  protected logger: TConsoleBase;
27
22
  /** Cached serialized type definition (static, computed once). */
28
23
  private _serializedType;
29
24
  /** Cached search index list (static, computed once). */
30
25
  private _searchIndexes;
31
- constructor(table: AtscriptDbTable<T>, app: Moost);
26
+ constructor(readable: AtscriptDbReadable<T>, app: Moost);
32
27
  /**
33
28
  * One-time initialization hook. Override to seed data, register watchers, etc.
34
29
  */
@@ -50,16 +45,13 @@ declare class AsDbController<T extends TAtscriptAnnotatedType = TAtscriptAnnotat
50
45
  * Transform projection before querying.
51
46
  */
52
47
  protected transformProjection(projection?: UniqueryControls['$select']): UniqueryControls['$select'] | undefined;
53
- /**
54
- * Intercepts write operations. Return `undefined` to abort.
55
- */
56
- protected onWrite(action: 'insert' | 'insertMany' | 'replace' | 'update', data: unknown): unknown | Promise<unknown | undefined>;
57
- /**
58
- * Intercepts delete operations. Return `undefined` to abort.
59
- */
60
- protected onRemove(id: unknown): unknown | Promise<unknown | undefined>;
61
48
  protected parseQueryString(url: string): _uniqu_url.UrlQuery;
62
49
  protected returnOne(result: Promise<DataType | null>): Promise<DataType | HttpError>;
50
+ /**
51
+ * Extracts a composite identifier object from query params.
52
+ * Tries composite primary key first, then compound unique indexes.
53
+ */
54
+ protected extractCompositeId(query: Record<string, string>): Record<string, unknown> | HttpError;
63
55
  /**
64
56
  * **GET /query** — returns an array of records or a count.
65
57
  */
@@ -78,16 +70,53 @@ declare class AsDbController<T extends TAtscriptAnnotatedType = TAtscriptAnnotat
78
70
  * **GET /one/:id** — retrieves a single record by ID or unique property.
79
71
  */
80
72
  getOne(id: string, url: string): Promise<DataType | HttpError>;
73
+ /**
74
+ * **GET /one?field1=val1&field2=val2** — retrieves a single record by composite key
75
+ * (composite primary key or compound unique index).
76
+ */
77
+ getOneComposite(query: Record<string, string>, url: string): Promise<DataType | HttpError>;
78
+ /**
79
+ * **GET /meta** — returns table/view metadata for UI.
80
+ */
81
+ meta(): {
82
+ searchable: boolean;
83
+ searchIndexes: _atscript_utils_db.TSearchIndexInfo[];
84
+ type: _atscript_typescript_serialize.TSerializedAnnotatedType;
85
+ };
86
+ }
87
+
88
+ /**
89
+ * Full CRUD database controller for Moost that works with any `AtscriptDbTable` +
90
+ * `BaseDbAdapter`. Extends {@link AsDbReadableController} with write operations.
91
+ *
92
+ * Subclass and provide the table via DI:
93
+ * ```ts
94
+ * ‎@TableController(usersTable)
95
+ * export class UsersController extends AsDbController<typeof UserModel> {}
96
+ * ```
97
+ */
98
+ declare class AsDbController<T extends TAtscriptAnnotatedType = TAtscriptAnnotatedType, DataType = TAtscriptDataType<T>> extends AsDbReadableController<T, DataType> {
99
+ /** Reference to the underlying table (typed for write access). */
100
+ protected get table(): AtscriptDbTable<T>;
101
+ constructor(table: AtscriptDbTable<T>, app: Moost);
102
+ /**
103
+ * Intercepts write operations. Return `undefined` to abort.
104
+ */
105
+ protected onWrite(action: 'insert' | 'insertMany' | 'replace' | 'replaceMany' | 'update' | 'updateMany', data: unknown): unknown | Promise<unknown | undefined>;
106
+ /**
107
+ * Intercepts delete operations. Return `undefined` to abort.
108
+ */
109
+ protected onRemove(id: unknown): unknown | Promise<unknown | undefined>;
81
110
  /**
82
111
  * **POST /** — inserts one or many records.
83
112
  */
84
113
  insert(payload: unknown): Promise<HttpError | unknown>;
85
114
  /**
86
- * **PUT /** — fully replaces a record matched by primary key.
115
+ * **PUT /** — fully replaces one or many records matched by primary key.
87
116
  */
88
117
  replace(payload: unknown): Promise<HttpError | unknown>;
89
118
  /**
90
- * **PATCH /** — partially updates a record matched by primary key.
119
+ * **PATCH /** — partially updates one or many records matched by primary key.
91
120
  */
92
121
  update(payload: unknown): Promise<HttpError | unknown>;
93
122
  /**
@@ -95,20 +124,23 @@ declare class AsDbController<T extends TAtscriptAnnotatedType = TAtscriptAnnotat
95
124
  */
96
125
  remove(id: string): Promise<HttpError | unknown>;
97
126
  /**
98
- * **GET /meta** — returns table metadata for UI.
127
+ * **DELETE /?field1=val1&field2=val2** — removes a record by composite key
128
+ * (composite primary key or compound unique index).
99
129
  */
100
- meta(): {
101
- searchable: boolean;
102
- searchIndexes: _atscript_utils_db.TSearchIndexInfo[];
103
- type: _atscript_typescript_serialize.TSerializedAnnotatedType;
104
- };
130
+ removeComposite(query: Record<string, string>): Promise<HttpError | unknown>;
105
131
  }
106
132
 
133
+ /**
134
+ * DI token under which the {@link AtscriptDbReadable} instance
135
+ * is exposed to the readable controller's constructor via `@Inject`.
136
+ */
137
+ declare const READABLE_DEF = "__atscript_db_readable_def";
107
138
  /**
108
139
  * DI token under which the {@link AtscriptDbTable} instance
109
140
  * is exposed to the controller's constructor via `@Inject`.
141
+ * Points to the same token as READABLE_DEF for backward compatibility.
110
142
  */
111
- declare const TABLE_DEF = "__atscript_db_table_def";
143
+ declare const TABLE_DEF = "__atscript_db_readable_def";
112
144
  /**
113
145
  * Combines the boilerplate needed to turn an {@link AsDbController}
114
146
  * subclass into a fully wired HTTP controller for a given `@db.table` model.
@@ -130,8 +162,32 @@ declare const TABLE_DEF = "__atscript_db_table_def";
130
162
  * ```
131
163
  */
132
164
  declare const TableController: (table: AtscriptDbTable, prefix?: string) => MethodDecorator & ClassDecorator & ParameterDecorator & PropertyDecorator;
165
+ /**
166
+ * Combines the boilerplate needed to turn an {@link AsDbReadableController}
167
+ * subclass into a fully wired HTTP controller for a given `@db.view` or `@db.table` model.
168
+ *
169
+ * @param readable The {@link AtscriptDbReadable} instance (table or view).
170
+ * @param prefix Optional route prefix. Defaults to `readable.tableName`.
171
+ *
172
+ * @example
173
+ * ```ts
174
+ * ‎@ReadableController(activeTasksView)
175
+ * export class ActiveTasksController extends AsDbReadableController<typeof ActiveTasks> {}
176
+ * ```
177
+ */
178
+ declare const ReadableController: (readable: AtscriptDbReadable, prefix?: string) => MethodDecorator & ClassDecorator & ParameterDecorator & PropertyDecorator;
179
+ /**
180
+ * Alias for {@link ReadableController} — use with view-backed controllers.
181
+ *
182
+ * @example
183
+ * ```ts
184
+ * ‎@ViewController(activeTasksView)
185
+ * export class ActiveTasksController extends AsDbReadableController<typeof ActiveTasks> {}
186
+ * ```
187
+ */
188
+ declare const ViewController: (readable: AtscriptDbReadable, prefix?: string) => MethodDecorator & ClassDecorator & ParameterDecorator & PropertyDecorator;
133
189
 
134
190
  declare const validationErrorTransform: () => moost.TInterceptorDef;
135
191
  declare const UseValidationErrorTransform: () => ClassDecorator & MethodDecorator;
136
192
 
137
- export { AsDbController, TABLE_DEF, TableController, UseValidationErrorTransform, validationErrorTransform };
193
+ export { AsDbController, AsDbReadableController, READABLE_DEF, ReadableController, TABLE_DEF, TableController, UseValidationErrorTransform, ViewController, validationErrorTransform };
package/dist/index.mjs CHANGED
@@ -1,20 +1,33 @@
1
1
  import { ValidatorError, defineAnnotatedType, serializeAnnotatedType, throwFeatureDisabled } from "@atscript/typescript/utils";
2
- import { Body, Delete, Get, HttpError, Patch, Post, Put, Url } from "@moostjs/event-http";
2
+ import { Body, Delete, Get, HttpError, Patch, Post, Put, Query, Url } from "@moostjs/event-http";
3
3
  import { ApplyDecorators, Controller, Inherit, Inject, Intercept, Moost, Param, Provide, TInterceptorPriority, defineInterceptor } from "moost";
4
4
  import { parseUrl } from "@uniqu/url";
5
+ import { DbError } from "@atscript/utils-db";
5
6
 
6
7
  //#region packages/moost-db/src/decorators.ts
7
- const TABLE_DEF = "__atscript_db_table_def";
8
+ const READABLE_DEF = "__atscript_db_readable_def";
9
+ const TABLE_DEF = READABLE_DEF;
8
10
  const TableController = (table, prefix) => ApplyDecorators(Provide(TABLE_DEF, () => table), Controller(prefix || table.tableName), Inherit());
11
+ const ReadableController = (readable, prefix) => ApplyDecorators(Provide(READABLE_DEF, () => readable), Controller(prefix || readable.tableName), Inherit());
12
+ const ViewController = ReadableController;
9
13
 
10
14
  //#endregion
11
15
  //#region packages/moost-db/src/validation-interceptor.ts
16
+ const dbErrorCodeToStatus = { CONFLICT: 409 };
12
17
  function transformValidationError(error, reply) {
13
18
  if (error instanceof ValidatorError) reply(new HttpError(400, {
14
19
  message: error.message,
15
20
  statusCode: 400,
16
21
  errors: error.errors
17
22
  }));
23
+ else if (error instanceof DbError) {
24
+ const statusCode = dbErrorCodeToStatus[error.code] ?? 400;
25
+ reply(new HttpError(statusCode, {
26
+ message: error.message,
27
+ statusCode,
28
+ errors: error.errors
29
+ }));
30
+ }
18
31
  }
19
32
  const validationErrorTransform = () => defineInterceptor({ error: transformValidationError }, TInterceptorPriority.CATCH_ERROR);
20
33
  const UseValidationErrorTransform = () => Intercept(validationErrorTransform());
@@ -121,7 +134,7 @@ defineAnnotatedType("object", SortControlDto).propPattern(/./, defineAnnotatedTy
121
134
  defineAnnotatedType("object", SelectControlDto).propPattern(/./, defineAnnotatedType("union").item(defineAnnotatedType().designType("number").value(1).$type).item(defineAnnotatedType().designType("number").value(0).$type).$type);
122
135
 
123
136
  //#endregion
124
- //#region packages/moost-db/src/as-db.controller.ts
137
+ //#region packages/moost-db/src/as-db-readable.controller.ts
125
138
  function _define_property(obj, key, value) {
126
139
  if (key in obj) Object.defineProperty(obj, key, {
127
140
  value,
@@ -132,21 +145,21 @@ function _define_property(obj, key, value) {
132
145
  else obj[key] = value;
133
146
  return obj;
134
147
  }
135
- function _ts_decorate(decorators, target, key, desc) {
148
+ function _ts_decorate$1(decorators, target, key, desc) {
136
149
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
137
150
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
138
151
  else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
139
152
  return c > 3 && r && Object.defineProperty(target, key, r), r;
140
153
  }
141
- function _ts_metadata(k, v) {
154
+ function _ts_metadata$1(k, v) {
142
155
  if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
143
156
  }
144
- function _ts_param(paramIndex, decorator) {
157
+ function _ts_param$1(paramIndex, decorator) {
145
158
  return function(target, key) {
146
159
  decorator(target, key, paramIndex);
147
160
  };
148
161
  }
149
- var AsDbController = class {
162
+ var AsDbReadableController = class {
150
163
  /**
151
164
  * One-time initialization hook. Override to seed data, register watchers, etc.
152
165
  */ init() {}
@@ -168,7 +181,7 @@ var AsDbController = class {
168
181
  return undefined;
169
182
  }
170
183
  validateInsights(insights) {
171
- for (const key of insights.keys()) if (!this.table.flatMap.has(key)) return `Unknown field "${key}"`;
184
+ for (const key of insights.keys()) if (!this.readable.flatMap.has(key)) return `Unknown field "${key}"`;
172
185
  return undefined;
173
186
  }
174
187
  validateParsed(parsed, type) {
@@ -178,6 +191,18 @@ var AsDbController = class {
178
191
  const insightsError = this.validateInsights(parsed.insights);
179
192
  if (insightsError) return new HttpError(400, insightsError);
180
193
  }
194
+ const withRelations = parsed.controls.$with;
195
+ if (withRelations?.length) {
196
+ const relations = this.readable.relations;
197
+ for (const rel of withRelations) if (!rel.name.includes(".") && !relations.has(rel.name)) return new HttpError(400, {
198
+ message: `Unknown relation "${rel.name}" in $with. Available relations: ${[...relations.keys()].join(", ") || "(none)"}`,
199
+ statusCode: 400,
200
+ errors: [{
201
+ path: "$with",
202
+ message: `Unknown relation "${rel.name}"`
203
+ }]
204
+ });
205
+ }
181
206
  return undefined;
182
207
  }
183
208
  /**
@@ -190,16 +215,6 @@ var AsDbController = class {
190
215
  */ transformProjection(projection) {
191
216
  return projection;
192
217
  }
193
- /**
194
- * Intercepts write operations. Return `undefined` to abort.
195
- */ onWrite(action, data) {
196
- return data;
197
- }
198
- /**
199
- * Intercepts delete operations. Return `undefined` to abort.
200
- */ onRemove(id) {
201
- return id;
202
- }
203
218
  parseQueryString(url) {
204
219
  const idx = url.indexOf("?");
205
220
  return parseUrl(idx >= 0 ? url.slice(idx + 1) : "");
@@ -210,6 +225,38 @@ var AsDbController = class {
210
225
  return item;
211
226
  }
212
227
  /**
228
+ * Extracts a composite identifier object from query params.
229
+ * Tries composite primary key first, then compound unique indexes.
230
+ */ extractCompositeId(query) {
231
+ const pkFields = this.readable.primaryKeys;
232
+ if (pkFields.length > 1) {
233
+ const idObj = {};
234
+ let allPresent = true;
235
+ for (const field of pkFields) {
236
+ if (query[field] === undefined) {
237
+ allPresent = false;
238
+ break;
239
+ }
240
+ idObj[field] = query[field];
241
+ }
242
+ if (allPresent) return idObj;
243
+ }
244
+ for (const index of this.readable.indexes.values()) {
245
+ if (index.type !== "unique" || index.fields.length < 2) continue;
246
+ const idObj = {};
247
+ let allPresent = true;
248
+ for (const indexField of index.fields) {
249
+ if (query[indexField.name] === undefined) {
250
+ allPresent = false;
251
+ break;
252
+ }
253
+ idObj[indexField.name] = query[indexField.name];
254
+ }
255
+ if (allPresent) return idObj;
256
+ }
257
+ return new HttpError(400, "Query params do not match any composite primary key or compound unique index");
258
+ }
259
+ /**
213
260
  * **GET /query** — returns an array of records or a count.
214
261
  */ async query(url) {
215
262
  const parsed = this.parseQueryString(url);
@@ -218,7 +265,7 @@ var AsDbController = class {
218
265
  const controls = parsed.controls;
219
266
  const filter = this.transformFilter(parsed.filter);
220
267
  const select = this.transformProjection(controls.$select);
221
- if (controls.$count) return this.table.count({
268
+ if (controls.$count) return this.readable.count({
222
269
  filter,
223
270
  controls: {
224
271
  ...controls,
@@ -227,7 +274,7 @@ var AsDbController = class {
227
274
  });
228
275
  const searchTerm = controls.$search;
229
276
  const indexName = controls.$index;
230
- if (searchTerm && this.table.isSearchable()) return this.table.search(searchTerm, {
277
+ if (searchTerm && this.readable.isSearchable()) return this.readable.search(searchTerm, {
231
278
  filter,
232
279
  controls: {
233
280
  ...controls,
@@ -235,7 +282,7 @@ var AsDbController = class {
235
282
  $limit: controls.$limit || 1e3
236
283
  }
237
284
  }, indexName);
238
- return this.table.findMany({
285
+ return this.readable.findMany({
239
286
  filter,
240
287
  controls: {
241
288
  ...controls,
@@ -268,8 +315,8 @@ var AsDbController = class {
268
315
  }
269
316
  };
270
317
  let result;
271
- if (searchTerm && this.table.isSearchable()) result = await this.table.searchWithCount(searchTerm, query, indexName);
272
- else result = await this.table.findManyWithCount(query);
318
+ if (searchTerm && this.readable.isSearchable()) result = await this.readable.searchWithCount(searchTerm, query, indexName);
319
+ else result = await this.readable.findManyWithCount(query);
273
320
  return {
274
321
  data: result.data,
275
322
  page,
@@ -286,31 +333,164 @@ else result = await this.table.findManyWithCount(query);
286
333
  const error = this.validateParsed(parsed, "getOne");
287
334
  if (error) return error;
288
335
  const select = this.transformProjection(parsed.controls.$select);
289
- return this.returnOne(this.table.findById(id, { $select: select }));
336
+ const controls = {
337
+ ...parsed.controls,
338
+ $select: select
339
+ };
340
+ return this.returnOne(this.readable.findById(id, { controls }));
341
+ }
342
+ /**
343
+ * **GET /one?field1=val1&field2=val2** — retrieves a single record by composite key
344
+ * (composite primary key or compound unique index).
345
+ */ async getOneComposite(query, url) {
346
+ const idObj = this.extractCompositeId(query);
347
+ if (idObj instanceof HttpError) return idObj;
348
+ const parsed = this.parseQueryString(url);
349
+ const select = this.transformProjection(parsed.controls.$select);
350
+ const controls = {
351
+ ...parsed.controls,
352
+ $select: select
353
+ };
354
+ return this.returnOne(this.readable.findById(idObj, { controls }));
355
+ }
356
+ /**
357
+ * **GET /meta** — returns table/view metadata for UI.
358
+ */ meta() {
359
+ return {
360
+ searchable: this.readable.isSearchable(),
361
+ searchIndexes: this._searchIndexes,
362
+ type: this._serializedType
363
+ };
364
+ }
365
+ constructor(readable, app) {
366
+ /** Reference to the underlying readable (table or view). */ _define_property(this, "readable", void 0);
367
+ /** Application-scoped logger. */ _define_property(this, "logger", void 0);
368
+ /** Cached serialized type definition (static, computed once). */ _define_property(this, "_serializedType", void 0);
369
+ /** Cached search index list (static, computed once). */ _define_property(this, "_searchIndexes", void 0);
370
+ _define_property(this, "_queryControlsValidator", void 0);
371
+ _define_property(this, "_pagesControlsValidator", void 0);
372
+ _define_property(this, "_getOneControlsValidator", void 0);
373
+ this.readable = readable;
374
+ this._serializedType = serializeAnnotatedType(readable.type);
375
+ this._searchIndexes = readable.getSearchIndexes();
376
+ this.logger = app.getLogger(`db [${readable.tableName}]`);
377
+ this.logger.info(`Initializing ${readable.isView ? "view" : "table"} controller`);
378
+ try {
379
+ const p = this.init();
380
+ if (p instanceof Promise) p.catch((error) => {
381
+ this.logger.error(error);
382
+ });
383
+ } catch (error) {
384
+ this.logger.error(error);
385
+ throw error;
386
+ }
387
+ }
388
+ };
389
+ _ts_decorate$1([
390
+ Get("query"),
391
+ _ts_param$1(0, Url()),
392
+ _ts_metadata$1("design:type", Function),
393
+ _ts_metadata$1("design:paramtypes", [String]),
394
+ _ts_metadata$1("design:returntype", Promise)
395
+ ], AsDbReadableController.prototype, "query", null);
396
+ _ts_decorate$1([
397
+ Get("pages"),
398
+ _ts_param$1(0, Url()),
399
+ _ts_metadata$1("design:type", Function),
400
+ _ts_metadata$1("design:paramtypes", [String]),
401
+ _ts_metadata$1("design:returntype", Promise)
402
+ ], AsDbReadableController.prototype, "pages", null);
403
+ _ts_decorate$1([
404
+ Get("one/:id"),
405
+ _ts_param$1(0, Param("id")),
406
+ _ts_param$1(1, Url()),
407
+ _ts_metadata$1("design:type", Function),
408
+ _ts_metadata$1("design:paramtypes", [String, String]),
409
+ _ts_metadata$1("design:returntype", Promise)
410
+ ], AsDbReadableController.prototype, "getOne", null);
411
+ _ts_decorate$1([
412
+ Get("one"),
413
+ _ts_param$1(0, Query()),
414
+ _ts_param$1(1, Url()),
415
+ _ts_metadata$1("design:type", Function),
416
+ _ts_metadata$1("design:paramtypes", [typeof Record === "undefined" ? Object : Record, String]),
417
+ _ts_metadata$1("design:returntype", Promise)
418
+ ], AsDbReadableController.prototype, "getOneComposite", null);
419
+ _ts_decorate$1([
420
+ Get("meta"),
421
+ _ts_metadata$1("design:type", Function),
422
+ _ts_metadata$1("design:paramtypes", []),
423
+ _ts_metadata$1("design:returntype", void 0)
424
+ ], AsDbReadableController.prototype, "meta", null);
425
+ AsDbReadableController = _ts_decorate$1([
426
+ UseValidationErrorTransform(),
427
+ _ts_param$1(0, Inject(READABLE_DEF)),
428
+ _ts_metadata$1("design:type", Function),
429
+ _ts_metadata$1("design:paramtypes", [typeof AtscriptDbReadable === "undefined" ? Object : AtscriptDbReadable, typeof Moost === "undefined" ? Object : Moost])
430
+ ], AsDbReadableController);
431
+
432
+ //#endregion
433
+ //#region packages/moost-db/src/as-db.controller.ts
434
+ function _ts_decorate(decorators, target, key, desc) {
435
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
436
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
437
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
438
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
439
+ }
440
+ function _ts_metadata(k, v) {
441
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
442
+ }
443
+ function _ts_param(paramIndex, decorator) {
444
+ return function(target, key) {
445
+ decorator(target, key, paramIndex);
446
+ };
447
+ }
448
+ var AsDbController = class extends AsDbReadableController {
449
+ /** Reference to the underlying table (typed for write access). */ get table() {
450
+ return this.readable;
451
+ }
452
+ /**
453
+ * Intercepts write operations. Return `undefined` to abort.
454
+ */ onWrite(action, data) {
455
+ return data;
456
+ }
457
+ /**
458
+ * Intercepts delete operations. Return `undefined` to abort.
459
+ */ onRemove(id) {
460
+ return id;
290
461
  }
291
462
  /**
292
463
  * **POST /** — inserts one or many records.
293
464
  */ async insert(payload) {
294
- const arr = Array.isArray(payload) ? payload : [payload];
295
- if (arr.length === 1) {
296
- const data$1 = await this.onWrite("insert", arr[0]);
465
+ if (Array.isArray(payload)) {
466
+ const data$1 = await this.onWrite("insertMany", payload);
297
467
  if (data$1 === undefined) return new HttpError(500, "Not saved");
298
- return await this.table.insertOne(data$1);
468
+ return await this.table.insertMany(data$1);
299
469
  }
300
- const data = await this.onWrite("insertMany", arr);
470
+ const data = await this.onWrite("insert", payload);
301
471
  if (data === undefined) return new HttpError(500, "Not saved");
302
- return await this.table.insertMany(data);
472
+ return await this.table.insertOne(data);
303
473
  }
304
474
  /**
305
- * **PUT /** — fully replaces a record matched by primary key.
475
+ * **PUT /** — fully replaces one or many records matched by primary key.
306
476
  */ async replace(payload) {
477
+ if (Array.isArray(payload)) {
478
+ const data$1 = await this.onWrite("replaceMany", payload);
479
+ if (data$1 === undefined) return new HttpError(500, "Not saved");
480
+ return await this.table.bulkReplace(data$1);
481
+ }
307
482
  const data = await this.onWrite("replace", payload);
308
483
  if (data === undefined) return new HttpError(500, "Not saved");
309
484
  return await this.table.replaceOne(data);
310
485
  }
311
486
  /**
312
- * **PATCH /** — partially updates a record matched by primary key.
487
+ * **PATCH /** — partially updates one or many records matched by primary key.
313
488
  */ async update(payload) {
489
+ if (Array.isArray(payload)) {
490
+ const data$1 = await this.onWrite("updateMany", payload);
491
+ if (data$1 === undefined) return new HttpError(500, "Not saved");
492
+ return await this.table.bulkUpdate(data$1);
493
+ }
314
494
  const data = await this.onWrite("update", payload);
315
495
  if (data === undefined) return new HttpError(500, "Not saved");
316
496
  return await this.table.updateOne(data);
@@ -325,60 +505,21 @@ else result = await this.table.findManyWithCount(query);
325
505
  return result;
326
506
  }
327
507
  /**
328
- * **GET /meta** — returns table metadata for UI.
329
- */ meta() {
330
- return {
331
- searchable: this.table.isSearchable(),
332
- searchIndexes: this._searchIndexes,
333
- type: this._serializedType
334
- };
508
+ * **DELETE /?field1=val1&field2=val2** — removes a record by composite key
509
+ * (composite primary key or compound unique index).
510
+ */ async removeComposite(query) {
511
+ const idObj = this.extractCompositeId(query);
512
+ if (idObj instanceof HttpError) return idObj;
513
+ const resolvedId = await this.onRemove(idObj);
514
+ if (resolvedId === undefined) return new HttpError(500, "Not deleted");
515
+ const result = await this.table.deleteOne(resolvedId);
516
+ if (result.deletedCount < 1) return new HttpError(404);
517
+ return result;
335
518
  }
336
519
  constructor(table, app) {
337
- /** Reference to the underlying table. */ _define_property(this, "table", void 0);
338
- /** Application-scoped logger. */ _define_property(this, "logger", void 0);
339
- /** Cached serialized type definition (static, computed once). */ _define_property(this, "_serializedType", void 0);
340
- /** Cached search index list (static, computed once). */ _define_property(this, "_searchIndexes", void 0);
341
- _define_property(this, "_queryControlsValidator", void 0);
342
- _define_property(this, "_pagesControlsValidator", void 0);
343
- _define_property(this, "_getOneControlsValidator", void 0);
344
- this.table = table;
345
- this._serializedType = serializeAnnotatedType(table.type);
346
- this._searchIndexes = table.getSearchIndexes();
347
- this.logger = app.getLogger(`db [${table.tableName}]`);
348
- this.logger.info("Initializing table controller");
349
- try {
350
- const p = this.init();
351
- if (p instanceof Promise) p.catch((error) => {
352
- this.logger.error(error);
353
- });
354
- } catch (error) {
355
- this.logger.error(error);
356
- throw error;
357
- }
520
+ super(table, app);
358
521
  }
359
522
  };
360
- _ts_decorate([
361
- Get("query"),
362
- _ts_param(0, Url()),
363
- _ts_metadata("design:type", Function),
364
- _ts_metadata("design:paramtypes", [String]),
365
- _ts_metadata("design:returntype", Promise)
366
- ], AsDbController.prototype, "query", null);
367
- _ts_decorate([
368
- Get("pages"),
369
- _ts_param(0, Url()),
370
- _ts_metadata("design:type", Function),
371
- _ts_metadata("design:paramtypes", [String]),
372
- _ts_metadata("design:returntype", Promise)
373
- ], AsDbController.prototype, "pages", null);
374
- _ts_decorate([
375
- Get("one/:id"),
376
- _ts_param(0, Param("id")),
377
- _ts_param(1, Url()),
378
- _ts_metadata("design:type", Function),
379
- _ts_metadata("design:paramtypes", [String, String]),
380
- _ts_metadata("design:returntype", Promise)
381
- ], AsDbController.prototype, "getOne", null);
382
523
  _ts_decorate([
383
524
  Post(""),
384
525
  _ts_param(0, Body()),
@@ -408,17 +549,18 @@ _ts_decorate([
408
549
  _ts_metadata("design:returntype", Promise)
409
550
  ], AsDbController.prototype, "remove", null);
410
551
  _ts_decorate([
411
- Get("meta"),
552
+ Delete(""),
553
+ _ts_param(0, Query()),
412
554
  _ts_metadata("design:type", Function),
413
- _ts_metadata("design:paramtypes", []),
414
- _ts_metadata("design:returntype", void 0)
415
- ], AsDbController.prototype, "meta", null);
555
+ _ts_metadata("design:paramtypes", [typeof Record === "undefined" ? Object : Record]),
556
+ _ts_metadata("design:returntype", Promise)
557
+ ], AsDbController.prototype, "removeComposite", null);
416
558
  AsDbController = _ts_decorate([
417
- UseValidationErrorTransform(),
559
+ Inherit(),
418
560
  _ts_param(0, Inject(TABLE_DEF)),
419
561
  _ts_metadata("design:type", Function),
420
562
  _ts_metadata("design:paramtypes", [typeof AtscriptDbTable === "undefined" ? Object : AtscriptDbTable, typeof Moost === "undefined" ? Object : Moost])
421
563
  ], AsDbController);
422
564
 
423
565
  //#endregion
424
- export { AsDbController, TABLE_DEF, TableController, UseValidationErrorTransform, validationErrorTransform };
566
+ export { AsDbController, AsDbReadableController, READABLE_DEF, ReadableController, TABLE_DEF, TableController, UseValidationErrorTransform, ViewController, validationErrorTransform };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atscript/moost-db",
3
- "version": "0.1.34",
3
+ "version": "0.1.36",
4
4
  "description": "Generic database controller for Moost with Atscript.",
5
5
  "keywords": [
6
6
  "annotations",
@@ -13,7 +13,7 @@
13
13
  "bugs": {
14
14
  "url": "https://github.com/moostjs/atscript/issues"
15
15
  },
16
- "license": "ISC",
16
+ "license": "MIT",
17
17
  "author": "Artem Maltsev",
18
18
  "repository": {
19
19
  "type": "git",
@@ -35,20 +35,20 @@
35
35
  "./package.json": "./package.json"
36
36
  },
37
37
  "dependencies": {
38
- "@uniqu/url": "^0.0.4"
38
+ "@uniqu/url": "^0.0.6"
39
39
  },
40
40
  "devDependencies": {
41
41
  "@moostjs/event-http": "^0.6.2",
42
42
  "moost": "^0.6.2",
43
43
  "vitest": "3.2.4",
44
- "@atscript/core": "^0.1.34"
44
+ "@atscript/core": "^0.1.36"
45
45
  },
46
46
  "peerDependencies": {
47
47
  "@moostjs/event-http": "^0.6.2",
48
48
  "moost": "^0.6.2",
49
- "@uniqu/core": "^0.0.4",
50
- "@atscript/utils-db": "^0.1.34",
51
- "@atscript/typescript": "^0.1.34"
49
+ "@uniqu/core": "^0.0.6",
50
+ "@atscript/utils-db": "^0.1.36",
51
+ "@atscript/typescript": "^0.1.36"
52
52
  },
53
53
  "scripts": {
54
54
  "pub": "pnpm publish --access public",