@atscript/moost-db 0.1.116 → 0.1.118

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.
@@ -0,0 +1,27 @@
1
+ import { DEFAULT_DB_SPACE as DEFAULT_DB_SPACE$1, DbSpace } from "@atscript/db";
2
+
3
+ //#region src/db-space-registry.d.ts
4
+ /**
5
+ * Registers a {@link DbSpace} for token-based controller binding
6
+ * (`@TableController(Model)`), keyed by `name` (defaults to
7
+ * {@link DEFAULT_DB_SPACE}).
8
+ *
9
+ * Call before `app.init()` — token resolution happens lazily when controllers
10
+ * are instantiated during init. Registering the same name twice replaces the
11
+ * previous space (used by tests).
12
+ *
13
+ * ```ts
14
+ * provideDbSpace(db) // "default"
15
+ * provideDbSpace(analyticsDb, "analytics")
16
+ * ```
17
+ */
18
+ declare function provideDbSpace(space: DbSpace, name?: string): void;
19
+ /**
20
+ * Returns the space registered under `name`, or throws with wiring guidance.
21
+ * Used by the token-form controller decorators; exported for advanced setups.
22
+ */
23
+ declare function resolveDbSpace(name?: string): DbSpace;
24
+ /** Removes all registered spaces. Intended for test teardown. */
25
+ declare function clearDbSpaces(): void;
26
+ //#endregion
27
+ export { resolveDbSpace as i, clearDbSpaces as n, provideDbSpace as r, DEFAULT_DB_SPACE$1 as t };
@@ -0,0 +1,27 @@
1
+ import { DEFAULT_DB_SPACE, DbSpace } from "@atscript/db";
2
+
3
+ //#region src/db-space-registry.d.ts
4
+ /**
5
+ * Registers a {@link DbSpace} for token-based controller binding
6
+ * (`@TableController(Model)`), keyed by `name` (defaults to
7
+ * {@link DEFAULT_DB_SPACE}).
8
+ *
9
+ * Call before `app.init()` — token resolution happens lazily when controllers
10
+ * are instantiated during init. Registering the same name twice replaces the
11
+ * previous space (used by tests).
12
+ *
13
+ * ```ts
14
+ * provideDbSpace(db) // "default"
15
+ * provideDbSpace(analyticsDb, "analytics")
16
+ * ```
17
+ */
18
+ declare function provideDbSpace(space: DbSpace, name?: string): void;
19
+ /**
20
+ * Returns the space registered under `name`, or throws with wiring guidance.
21
+ * Used by the token-form controller decorators; exported for advanced setups.
22
+ */
23
+ declare function resolveDbSpace(name?: string): DbSpace;
24
+ /** Removes all registered spaces. Intended for test teardown. */
25
+ declare function clearDbSpaces(): void;
26
+ //#endregion
27
+ export { resolveDbSpace as i, clearDbSpaces as n, provideDbSpace as r, DEFAULT_DB_SPACE as t };
@@ -0,0 +1,48 @@
1
+ import { DEFAULT_DB_SPACE, DEFAULT_DB_SPACE as DEFAULT_DB_SPACE$1 } from "@atscript/db";
2
+ //#region src/db-space-registry.ts
3
+ /**
4
+ * Ambient registry of {@link DbSpace} instances, keyed by name.
5
+ *
6
+ * WHY module-level and not Moost DI: provide factories in the DI container
7
+ * take no arguments (they cannot resolve sibling tokens), and controllers that
8
+ * declare their own constructors never execute the base class's `@Inject`
9
+ * decorations — so a DI-carried space would silently miss both paths. The
10
+ * ambient registry works for every binding form and can be superseded by a
11
+ * DI-native path later without breaking this API.
12
+ */
13
+ const spaces = /* @__PURE__ */ new Map();
14
+ /**
15
+ * Registers a {@link DbSpace} for token-based controller binding
16
+ * (`@TableController(Model)`), keyed by `name` (defaults to
17
+ * {@link DEFAULT_DB_SPACE}).
18
+ *
19
+ * Call before `app.init()` — token resolution happens lazily when controllers
20
+ * are instantiated during init. Registering the same name twice replaces the
21
+ * previous space (used by tests).
22
+ *
23
+ * ```ts
24
+ * provideDbSpace(db) // "default"
25
+ * provideDbSpace(analyticsDb, "analytics")
26
+ * ```
27
+ */
28
+ function provideDbSpace(space, name = DEFAULT_DB_SPACE) {
29
+ spaces.set(name, space);
30
+ }
31
+ /**
32
+ * Returns the space registered under `name`, or throws with wiring guidance.
33
+ * Used by the token-form controller decorators; exported for advanced setups.
34
+ */
35
+ function resolveDbSpace(name = DEFAULT_DB_SPACE) {
36
+ const space = spaces.get(name);
37
+ if (!space) {
38
+ const known = [...spaces.keys()];
39
+ throw new Error(`[moost-db] No DbSpace registered under "${name}". Call provideDbSpace(space${name === DEFAULT_DB_SPACE ? "" : `, "${name}"`}) before app.init(). ` + (known.length ? `Registered spaces: ${known.join(", ")}.` : "No spaces registered yet."));
40
+ }
41
+ return space;
42
+ }
43
+ /** Removes all registered spaces. Intended for test teardown. */
44
+ function clearDbSpaces() {
45
+ spaces.clear();
46
+ }
47
+ //#endregion
48
+ export { resolveDbSpace as i, clearDbSpaces as n, provideDbSpace as r, DEFAULT_DB_SPACE$1 as t };
@@ -0,0 +1,65 @@
1
+ let _atscript_db = require("@atscript/db");
2
+ //#region src/db-space-registry.ts
3
+ /**
4
+ * Ambient registry of {@link DbSpace} instances, keyed by name.
5
+ *
6
+ * WHY module-level and not Moost DI: provide factories in the DI container
7
+ * take no arguments (they cannot resolve sibling tokens), and controllers that
8
+ * declare their own constructors never execute the base class's `@Inject`
9
+ * decorations — so a DI-carried space would silently miss both paths. The
10
+ * ambient registry works for every binding form and can be superseded by a
11
+ * DI-native path later without breaking this API.
12
+ */
13
+ const spaces = /* @__PURE__ */ new Map();
14
+ /**
15
+ * Registers a {@link DbSpace} for token-based controller binding
16
+ * (`@TableController(Model)`), keyed by `name` (defaults to
17
+ * {@link DEFAULT_DB_SPACE}).
18
+ *
19
+ * Call before `app.init()` — token resolution happens lazily when controllers
20
+ * are instantiated during init. Registering the same name twice replaces the
21
+ * previous space (used by tests).
22
+ *
23
+ * ```ts
24
+ * provideDbSpace(db) // "default"
25
+ * provideDbSpace(analyticsDb, "analytics")
26
+ * ```
27
+ */
28
+ function provideDbSpace(space, name = _atscript_db.DEFAULT_DB_SPACE) {
29
+ spaces.set(name, space);
30
+ }
31
+ /**
32
+ * Returns the space registered under `name`, or throws with wiring guidance.
33
+ * Used by the token-form controller decorators; exported for advanced setups.
34
+ */
35
+ function resolveDbSpace(name = _atscript_db.DEFAULT_DB_SPACE) {
36
+ const space = spaces.get(name);
37
+ if (!space) {
38
+ const known = [...spaces.keys()];
39
+ throw new Error(`[moost-db] No DbSpace registered under "${name}". Call provideDbSpace(space${name === _atscript_db.DEFAULT_DB_SPACE ? "" : `, "${name}"`}) before app.init(). ` + (known.length ? `Registered spaces: ${known.join(", ")}.` : "No spaces registered yet."));
40
+ }
41
+ return space;
42
+ }
43
+ /** Removes all registered spaces. Intended for test teardown. */
44
+ function clearDbSpaces() {
45
+ spaces.clear();
46
+ }
47
+ //#endregion
48
+ Object.defineProperty(exports, "clearDbSpaces", {
49
+ enumerable: true,
50
+ get: function() {
51
+ return clearDbSpaces;
52
+ }
53
+ });
54
+ Object.defineProperty(exports, "provideDbSpace", {
55
+ enumerable: true,
56
+ get: function() {
57
+ return provideDbSpace;
58
+ }
59
+ });
60
+ Object.defineProperty(exports, "resolveDbSpace", {
61
+ enumerable: true,
62
+ get: function() {
63
+ return resolveDbSpace;
64
+ }
65
+ });
package/dist/index.cjs CHANGED
@@ -1,4 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ const require_db_space_registry = require("./db-space-registry-DqKr5Zdk.cjs");
2
3
  let _atscript_typescript_utils = require("@atscript/typescript/utils");
3
4
  let _moostjs_event_http = require("@moostjs/event-http");
4
5
  let moost = require("moost");
@@ -913,57 +914,140 @@ const READABLE_DEF = "__atscript_db_readable_def";
913
914
  * Points to the same token as READABLE_DEF for backward compatibility.
914
915
  */
915
916
  const TABLE_DEF = READABLE_DEF;
917
+ function normalizeOptions(prefixOrOptions) {
918
+ if (typeof prefixOrOptions === "string") return { prefix: prefixOrOptions };
919
+ return prefixOrOptions ?? {};
920
+ }
921
+ /**
922
+ * Builds the shared binding metadata for a decorator invocation: classifies
923
+ * the binding form, computes the static route prefix, and packages a uniform
924
+ * `resolve()` used by both the DI provide factory and the base controller's
925
+ * `super(undefined, app)` fallback.
926
+ */
927
+ function buildBinding(binding, options, decoratorName) {
928
+ if ((0, _atscript_typescript_utils.isAnnotatedType)(binding)) {
929
+ const model = binding;
930
+ const space = options.space ?? model.metadata.get("db.space");
931
+ const prefix = options.prefix || model.metadata.get("db.http.path") || model.metadata.get("db.table") || model.metadata.get("db.view") || model.id || "";
932
+ if (!prefix) throw new Error(`[moost-db] @${decoratorName}: cannot derive a route prefix from the model token (no @db.http.path / @db.table / @db.view and no type id). Pass an explicit prefix.`);
933
+ return {
934
+ meta: {
935
+ model,
936
+ resolve: () => require_db_space_registry.resolveDbSpace(space).get(model)
937
+ },
938
+ prefix
939
+ };
940
+ }
941
+ if (typeof binding === "function") {
942
+ if (!options.prefix) throw new Error(`[moost-db] @${decoratorName}: the lazy factory form needs an explicit route prefix (the readable is not created until app.init()). Pass a prefix, or use the model token form which derives it from @db.http.path / @db.table.`);
943
+ return {
944
+ meta: { resolve: binding },
945
+ prefix: options.prefix
946
+ };
947
+ }
948
+ const readable = binding;
949
+ const prefix = options.prefix || readable.type.metadata.get("db.http.path") || readable.tableName;
950
+ return {
951
+ meta: {
952
+ model: readable.type,
953
+ resolve: () => readable
954
+ },
955
+ prefix
956
+ };
957
+ }
958
+ function bindReadableController(binding, prefixOrOptions, decoratorName) {
959
+ const { meta, prefix } = buildBinding(binding, normalizeOptions(prefixOrOptions), decoratorName);
960
+ return (0, moost.ApplyDecorators)(getAtscriptDbMate().decorate((classMeta) => {
961
+ classMeta.atscript_db_readable_binding = meta;
962
+ return classMeta;
963
+ }), (0, moost.Provide)(READABLE_DEF, () => meta.resolve()), (0, moost.Controller)(prefix), (0, moost.Inherit)());
964
+ }
916
965
  /**
917
966
  * Combines the boilerplate needed to turn an {@link AsDbController}
918
967
  * subclass into a fully wired HTTP controller for a given `@db.table` model.
919
968
  *
920
969
  * Internally applies three decorators:
921
- * 1. **Provide** — registers the table instance under {@link TABLE_DEF}.
970
+ * 1. **Provide** — registers the readable resolver under {@link TABLE_DEF}.
922
971
  * 2. **Controller** — registers the class as a Moost HTTP controller
923
- * with an optional route prefix. Defaults to `table.tableName`.
972
+ * with an optional route prefix (defaults to `@db.http.path`, then the
973
+ * table name).
924
974
  * 3. **Inherit** — copies metadata (routes, guards, etc.) from the
925
975
  * parent class so they stay active in the derived controller.
926
976
  *
927
- * @param table The {@link AtscriptDbTable} instance for this controller.
928
- * @param prefix Optional route prefix. Defaults to `table.tableName`.
977
+ * All three binding forms are supported (see {@link TReadableBinding}):
929
978
  *
930
- * @example
931
979
  * ```ts
932
- * ‎@TableController(usersTable)
933
- * export class UsersController extends AsDbController<typeof UserModel> {}
980
+ * ‎@TableController(User) // model token (preferred)
981
+ * ‎@TableController(User, { space: "analytics" })
982
+ * ‎@TableController(() => db.getTable(User), "users") // lazy factory
983
+ * ‎@TableController(usersTable) // instance (legacy)
984
+ * export class UsersController extends AsDbController<typeof User> {}
934
985
  * ```
986
+ *
987
+ * Token and factory forms resolve lazily at first controller instantiation
988
+ * (during `app.init()`), so the `DbSpace` does not have to exist when the
989
+ * controller module is imported. For the token form, register the space with
990
+ * `provideDbSpace(db)` before `app.init()`.
991
+ *
992
+ * @param binding Model token, lazy factory, or {@link AtscriptDbTable} instance.
993
+ * @param prefixOrOptions Route prefix string, or {@link TControllerBindingOptions}.
935
994
  */
936
- const TableController = (table, prefix) => {
937
- const resolvedPath = prefix || table.type.metadata.get("db.http.path");
938
- return (0, moost.ApplyDecorators)((0, moost.Provide)(TABLE_DEF, () => table), (0, moost.Controller)(resolvedPath || table.tableName), (0, moost.Inherit)());
939
- };
995
+ const TableController = (binding, prefixOrOptions) => bindReadableController(binding, prefixOrOptions, "TableController");
940
996
  /**
941
997
  * Combines the boilerplate needed to turn an {@link AsDbReadableController}
942
998
  * subclass into a fully wired HTTP controller for a given `@db.view` or `@db.table` model.
943
999
  *
944
- * @param readable The {@link AtscriptDbReadable} instance (table or view).
945
- * @param prefix Optional route prefix. Defaults to `readable.tableName`.
1000
+ * Accepts the same three binding forms as {@link TableController}.
1001
+ *
1002
+ * @param binding Model token, lazy factory, or {@link AtscriptDbReadable} instance.
1003
+ * @param prefixOrOptions Route prefix string, or {@link TControllerBindingOptions}.
946
1004
  *
947
1005
  * @example
948
1006
  * ```ts
949
- * ‎@ReadableController(activeTasksView)
1007
+ * ‎@ReadableController(ActiveTasks)
950
1008
  * export class ActiveTasksController extends AsDbReadableController<typeof ActiveTasks> {}
951
1009
  * ```
952
1010
  */
953
- const ReadableController = (readable, prefix) => {
954
- const resolvedPath = prefix || readable.type.metadata.get("db.http.path");
955
- return (0, moost.ApplyDecorators)((0, moost.Provide)(READABLE_DEF, () => readable), (0, moost.Controller)(resolvedPath || readable.tableName), (0, moost.Inherit)());
956
- };
1011
+ const ReadableController = (binding, prefixOrOptions) => bindReadableController(binding, prefixOrOptions, "ReadableController");
957
1012
  /**
958
1013
  * Alias for {@link ReadableController} — use with view-backed controllers.
959
1014
  *
960
1015
  * @example
961
1016
  * ```ts
962
- * ‎@ViewController(activeTasksView)
1017
+ * ‎@ViewController(ActiveTasks)
963
1018
  * export class ActiveTasksController extends AsDbReadableController<typeof ActiveTasks> {}
964
1019
  * ```
965
1020
  */
966
1021
  const ViewController = ReadableController;
1022
+ /**
1023
+ * Finds the readable binding written by `@TableController` /
1024
+ * `@ReadableController` / `@ViewController` on a controller class, walking the
1025
+ * prototype chain so intermediate undecorated classes don't hide the binding
1026
+ * (nearest decorated ancestor wins).
1027
+ */
1028
+ function findReadableBinding(ctor) {
1029
+ const mate = getAtscriptDbMate();
1030
+ let current = ctor;
1031
+ while (typeof current === "function") {
1032
+ const binding = mate.read(current)?.atscript_db_readable_binding;
1033
+ if (binding) return binding;
1034
+ current = Object.getPrototypeOf(current);
1035
+ }
1036
+ }
1037
+ /**
1038
+ * Resolves the readable bound to a controller class via
1039
+ * {@link findReadableBinding}, throwing with wiring guidance when none is
1040
+ * found.
1041
+ *
1042
+ * Used by {@link AsDbReadableController}'s constructor when `readable` is
1043
+ * `undefined` — i.e. a subclass with its own constructor called
1044
+ * `super(undefined, app)` instead of forwarding an injected instance.
1045
+ */
1046
+ function resolveBoundReadable(ctor) {
1047
+ const binding = findReadableBinding(ctor);
1048
+ if (binding) return binding.resolve();
1049
+ throw new Error(`[moost-db] ${ctor?.name || "controller"}: no readable bound. Either pass a table/view to super(...), or decorate the class with @TableController / @ReadableController (model token, lazy factory, or instance form).`);
1050
+ }
967
1051
  //#endregion
968
1052
  //#region src/permissions/crud-controls.ts
969
1053
  /**
@@ -1013,11 +1097,12 @@ let AsDbReadableController = class AsDbReadableController extends AsReadableCont
1013
1097
  /** Paths the adapter vetoes for filtering (e.g. JSON storage on SQL). Symmetric with `/meta` `filterable: false`. */
1014
1098
  _adapterNonFilterable;
1015
1099
  constructor(readable, app) {
1016
- super(readable.type, readable.tableName, app, readable.isView ? "view" : "table");
1017
- this.readable = readable;
1100
+ const resolved = readable ?? resolveBoundReadable(new.target);
1101
+ super(resolved.type, resolved.tableName, app, resolved.isView ? "view" : "table");
1102
+ this.readable = resolved;
1018
1103
  this._adapterNonFilterable = this._collectAdapterNonFilterable();
1019
1104
  this._gates = this._buildGates();
1020
- this._preferredIdSet = new Set(readable.preferredId ?? []);
1105
+ this._preferredIdSet = new Set(resolved.preferredId ?? []);
1021
1106
  this._quantityRefByPath = this._collectQuantityRefs();
1022
1107
  const defaultOverlay = AsReadableController.prototype.applyMetaOverlay;
1023
1108
  this._overlayIsNoOp = this.applyMetaOverlay === defaultOverlay;
@@ -1674,6 +1759,7 @@ __decorate([
1674
1759
  AsDbReadableController = __decorate([
1675
1760
  (0, moost.Inherit)(),
1676
1761
  __decorateParam(0, (0, moost.Inject)(READABLE_DEF)),
1762
+ __decorateParam(0, (0, moost.Optional)()),
1677
1763
  __decorateMetadata("design:paramtypes", [Object, typeof moost.Moost === "undefined" ? Object : moost.Moost])
1678
1764
  ], AsDbReadableController);
1679
1765
  registerAsDbReadableController(AsDbReadableController);
@@ -1871,6 +1957,7 @@ __decorate([
1871
1957
  AsDbController = __decorate([
1872
1958
  (0, moost.Inherit)(),
1873
1959
  __decorateParam(0, (0, moost.Inject)(TABLE_DEF)),
1960
+ __decorateParam(0, (0, moost.Optional)()),
1874
1961
  __decorateMetadata("design:paramtypes", [Object, typeof moost.Moost === "undefined" ? Object : moost.Moost])
1875
1962
  ], AsDbController);
1876
1963
  //#endregion
@@ -2139,6 +2226,44 @@ AsJsonValueHelpController = __decorate([(0, moost.Inherit)(), __decorateMetadata
2139
2226
  String
2140
2227
  ])], AsJsonValueHelpController);
2141
2228
  //#endregion
2229
+ //#region src/assert-exposed.ts
2230
+ /**
2231
+ * Dev-mode wiring assertion: warns for every model annotated with
2232
+ * `@db.http.path` that has no registered db controller.
2233
+ *
2234
+ * Call AFTER `await app.init()` (controller bindings are collected during
2235
+ * init). Detection reads the binding metadata written by `@TableController` /
2236
+ * `@ReadableController` / `@ViewController`, so it covers the model-token and
2237
+ * instance forms; lazy-factory bindings can't name their model until resolved
2238
+ * and are ignored.
2239
+ *
2240
+ * Returns the list of unexposed models so callers can escalate (e.g. throw in
2241
+ * CI):
2242
+ *
2243
+ * ```ts
2244
+ * await app.init()
2245
+ * const missing = assertExposed(app, atscriptModels)
2246
+ * if (missing.length && process.env.CI) throw new Error("unexposed models")
2247
+ * ```
2248
+ */
2249
+ function assertExposed(app, models, options) {
2250
+ const logger = options?.logger ?? console;
2251
+ const exposed = /* @__PURE__ */ new Set();
2252
+ for (const overview of app.getControllersOverview()) {
2253
+ const model = findReadableBinding(overview.type)?.model;
2254
+ if (model) exposed.add(model);
2255
+ }
2256
+ const missing = [];
2257
+ for (const model of models) {
2258
+ if (!model.metadata.has("db.http.path")) continue;
2259
+ if (exposed.has(model)) continue;
2260
+ missing.push(model);
2261
+ const path = model.metadata.get("db.http.path");
2262
+ logger.warn(`[moost-db] Model "${model.id ?? path}" declares @db.http.path "${path}" but no registered controller is bound to it. Did you forget to register its controller?`);
2263
+ }
2264
+ return missing;
2265
+ }
2266
+ //#endregion
2142
2267
  //#region src/actions/action-disabled-error.ts
2143
2268
  function buildMessage(action, ids) {
2144
2269
  if (ids !== void 0) return `Action "${action}" is disabled for ${ids.length} of the selected rows`;
@@ -2759,6 +2884,12 @@ Object.defineProperty(exports, "AsValueHelpController", {
2759
2884
  return AsValueHelpController;
2760
2885
  }
2761
2886
  });
2887
+ Object.defineProperty(exports, "DEFAULT_DB_SPACE", {
2888
+ enumerable: true,
2889
+ get: function() {
2890
+ return _atscript_db.DEFAULT_DB_SPACE;
2891
+ }
2892
+ });
2762
2893
  exports.DbAction = DbAction;
2763
2894
  exports.DbActionDefault = DbActionDefault;
2764
2895
  exports.DbActionID = DbActionID;
@@ -2779,12 +2910,18 @@ exports.TABLE_DEF = TABLE_DEF;
2779
2910
  exports.TableController = TableController;
2780
2911
  exports.UseValidationErrorTransform = UseValidationErrorTransform;
2781
2912
  exports.ViewController = ViewController;
2913
+ exports.assertExposed = assertExposed;
2914
+ exports.clearDbSpaces = require_db_space_registry.clearDbSpaces;
2782
2915
  exports.dbActionBodySlot = dbActionBodySlot;
2783
2916
  exports.dbActionInputSlot = dbActionInputSlot;
2784
2917
  exports.discoverActions = discoverActions;
2918
+ exports.findReadableBinding = findReadableBinding;
2785
2919
  exports.getAtscriptDbMate = getAtscriptDbMate;
2786
2920
  exports.getControllerFormType = getControllerFormType;
2787
2921
  exports.perRow = perRow;
2922
+ exports.provideDbSpace = require_db_space_registry.provideDbSpace;
2923
+ exports.resolveBoundReadable = resolveBoundReadable;
2924
+ exports.resolveDbSpace = require_db_space_registry.resolveDbSpace;
2788
2925
  exports.useDbActionId = useDbActionId;
2789
2926
  exports.useDbActionIds = useDbActionIds;
2790
2927
  exports.useDbActionInput = useDbActionInput;