@happyvertical/smrt-core 0.57.46 → 0.57.48
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/AGENTS.md +11 -3
- package/agents/browser-entry.md +16 -0
- package/agents/build-knowledge.md +58 -14
- package/agents/change-feed.md +20 -0
- package/agents/generators.md +69 -3
- package/agents/schema-paths.md +82 -0
- package/dist/_virtual/_rolldown/runtime.js +22 -1
- package/dist/browser.d.ts +2 -2
- package/dist/browser.d.ts.map +1 -1
- package/dist/browser.js +2 -2
- package/dist/cascade.d.ts +46 -0
- package/dist/cascade.d.ts.map +1 -1
- package/dist/cascade.js +96 -7
- package/dist/cascade.js.map +1 -1
- package/dist/change-feed.d.ts.map +1 -1
- package/dist/change-feed.js +7 -4
- package/dist/change-feed.js.map +1 -1
- package/dist/class.d.ts.map +1 -1
- package/dist/class.js +3 -3
- package/dist/class.js.map +1 -1
- package/dist/consumer-plugin/index.d.ts +13 -0
- package/dist/consumer-plugin/index.d.ts.map +1 -1
- package/dist/consumer-plugin/index.js +69 -13
- package/dist/consumer-plugin/index.js.map +1 -1
- package/dist/consumer-plugin.js +2 -2
- package/dist/database.d.ts.map +1 -1
- package/dist/database.js +3 -3
- package/dist/database.js.map +1 -1
- package/dist/dispatch/bus.d.ts.map +1 -1
- package/dist/dispatch/bus.js +1 -4
- package/dist/dispatch/bus.js.map +1 -1
- package/dist/dispatch/index.d.ts +1 -1
- package/dist/dispatch/index.d.ts.map +1 -1
- package/dist/dispatch/types.d.ts +22 -3
- package/dist/dispatch/types.d.ts.map +1 -1
- package/dist/generators/changes-route.d.ts.map +1 -1
- package/dist/generators/changes-route.js +2 -2
- package/dist/generators/changes-route.js.map +1 -1
- package/dist/generators/custom-action.d.ts +11 -0
- package/dist/generators/custom-action.d.ts.map +1 -1
- package/dist/generators/custom-action.js +23 -4
- package/dist/generators/custom-action.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/lazy-config.d.ts +120 -3
- package/dist/lazy-config.d.ts.map +1 -1
- package/dist/lazy-config.js +81 -10
- package/dist/lazy-config.js.map +1 -1
- package/dist/manifest/generator.d.ts +8 -2
- package/dist/manifest/generator.d.ts.map +1 -1
- package/dist/manifest/generator.js +6 -5
- package/dist/manifest/generator.js.map +1 -1
- package/dist/manifest/index.d.ts +1 -0
- package/dist/manifest/index.d.ts.map +1 -1
- package/dist/manifest/index.js +2 -1
- package/dist/manifest/index.js.map +1 -1
- package/dist/manifest/local-manifest-reader.d.ts +13 -2
- package/dist/manifest/local-manifest-reader.d.ts.map +1 -1
- package/dist/manifest/local-manifest-reader.js +38 -6
- package/dist/manifest/local-manifest-reader.js.map +1 -1
- package/dist/manifest/manifest-loader.d.ts +9 -8
- package/dist/manifest/manifest-loader.d.ts.map +1 -1
- package/dist/manifest/manifest-loader.js +67 -27
- package/dist/manifest/manifest-loader.js.map +1 -1
- package/dist/manifest/object-declarations.d.ts +116 -0
- package/dist/manifest/object-declarations.d.ts.map +1 -0
- package/dist/manifest/object-declarations.js +161 -0
- package/dist/manifest/object-declarations.js.map +1 -0
- package/dist/manifest/static-manifest.d.ts.map +1 -1
- package/dist/manifest/static-manifest.js +5 -3
- package/dist/manifest/static-manifest.js.map +1 -1
- package/dist/manifest/store.d.ts.map +1 -1
- package/dist/manifest/store.js +18 -2
- package/dist/manifest/store.js.map +1 -1
- package/dist/manifest/test-artifacts.d.ts +57 -0
- package/dist/manifest/test-artifacts.d.ts.map +1 -0
- package/dist/manifest/test-artifacts.js +111 -0
- package/dist/manifest/test-artifacts.js.map +1 -0
- package/dist/manifest.js +2 -1
- package/dist/manifest.json +5 -3
- package/dist/migrations/differ.d.ts.map +1 -1
- package/dist/migrations/differ.js +11 -3
- package/dist/migrations/differ.js.map +1 -1
- package/dist/migrations/index.d.ts +2 -0
- package/dist/migrations/index.d.ts.map +1 -1
- package/dist/migrations/index.js +3 -1
- package/dist/migrations/qualified-name-aliases.d.ts.map +1 -1
- package/dist/migrations/qualified-name-aliases.js +1 -1
- package/dist/migrations/qualified-name-aliases.js.map +1 -1
- package/dist/migrations.js +3 -1
- package/dist/object.d.ts.map +1 -1
- package/dist/object.js +2 -2
- package/dist/object.js.map +1 -1
- package/dist/postgres-permissions.d.ts.map +1 -1
- package/dist/postgres-permissions.js +4 -3
- package/dist/postgres-permissions.js.map +1 -1
- package/dist/postgres-timeouts.d.ts +30 -3
- package/dist/postgres-timeouts.d.ts.map +1 -1
- package/dist/postgres-timeouts.js +39 -2
- package/dist/postgres-timeouts.js.map +1 -1
- package/dist/prebuild/index.d.ts.map +1 -1
- package/dist/prebuild/index.js +14 -11
- package/dist/prebuild/index.js.map +1 -1
- package/dist/registry/class-registration.d.ts.map +1 -1
- package/dist/registry/class-registration.js +24 -2
- package/dist/registry/class-registration.js.map +1 -1
- package/dist/registry/collection-exposure.d.ts +27 -0
- package/dist/registry/collection-exposure.d.ts.map +1 -1
- package/dist/registry/collection-exposure.js +74 -1
- package/dist/registry/collection-exposure.js.map +1 -1
- package/dist/registry/collection-resolution.d.ts.map +1 -1
- package/dist/registry/collection-resolution.js +1 -0
- package/dist/registry/collection-resolution.js.map +1 -1
- package/dist/registry/framework-base-classes.d.ts.map +1 -1
- package/dist/registry/framework-base-classes.js +5 -0
- package/dist/registry/framework-base-classes.js.map +1 -1
- package/dist/registry/schema-builder.d.ts +8 -1
- package/dist/registry/schema-builder.d.ts.map +1 -1
- package/dist/registry/schema-builder.js +17 -4
- package/dist/registry/schema-builder.js.map +1 -1
- package/dist/registry/types.d.ts +12 -0
- package/dist/registry/types.d.ts.map +1 -1
- package/dist/registry.d.ts +2 -1
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +30 -10
- package/dist/registry.js.map +1 -1
- package/dist/scanner/manifest-generator.d.ts.map +1 -1
- package/dist/scanner/manifest-generator.js +10 -8
- package/dist/scanner/manifest-generator.js.map +1 -1
- package/dist/scanner/types.d.ts +29 -0
- package/dist/scanner/types.d.ts.map +1 -1
- package/dist/scanner/types.js.map +1 -1
- package/dist/schema/generator.d.ts +32 -0
- package/dist/schema/generator.d.ts.map +1 -1
- package/dist/schema/generator.js +59 -0
- package/dist/schema/generator.js.map +1 -1
- package/dist/schema/live-parity.d.ts.map +1 -1
- package/dist/schema/live-parity.js +4 -2
- package/dist/schema/live-parity.js.map +1 -1
- package/dist/scripts/browser-unbundled-dev.mjs +90 -0
- package/dist/scripts/generate-test-manifest.js +9 -49
- package/dist/smrt-knowledge.json +16 -16
- package/dist/system/compatibility.d.ts.map +1 -1
- package/dist/system/compatibility.js +12 -4
- package/dist/system/compatibility.js.map +1 -1
- package/dist/system/schema.d.ts +70 -8
- package/dist/system/schema.d.ts.map +1 -1
- package/dist/system/schema.js +231 -24
- package/dist/system/schema.js.map +1 -1
- package/dist/testing/database.d.ts.map +1 -1
- package/dist/testing/database.js +14 -2
- package/dist/testing/database.js.map +1 -1
- package/dist/tools/tool-generator.d.ts.map +1 -1
- package/dist/tools/tool-generator.js +2 -2
- package/dist/tools/tool-generator.js.map +1 -1
- package/dist/utils/import-workspace-module.d.ts.map +1 -1
- package/dist/utils/import-workspace-module.js +23 -0
- package/dist/utils/import-workspace-module.js.map +1 -1
- package/dist/utils/naming.js +5 -3
- package/dist/utils/naming.js.map +1 -1
- package/dist/vendor/pluralize/pluralize.js +421 -0
- package/dist/vendor/pluralize/pluralize.js.map +1 -0
- package/dist/vite-plugin/api-client-entries.d.ts +5 -1
- package/dist/vite-plugin/api-client-entries.d.ts.map +1 -1
- package/dist/vite-plugin/api-client-entries.js +11 -6
- package/dist/vite-plugin/api-client-entries.js.map +1 -1
- package/dist/vite-plugin/consumer-import.d.ts +28 -0
- package/dist/vite-plugin/consumer-import.d.ts.map +1 -0
- package/dist/vite-plugin/consumer-import.js +42 -0
- package/dist/vite-plugin/consumer-import.js.map +1 -0
- package/dist/vite-plugin/dev-plane-route.d.ts +6 -1
- package/dist/vite-plugin/dev-plane-route.d.ts.map +1 -1
- package/dist/vite-plugin/dev-plane-route.js +8 -9
- package/dist/vite-plugin/dev-plane-route.js.map +1 -1
- package/dist/vite-plugin/index.d.ts.map +1 -1
- package/dist/vite-plugin/index.js +12 -9
- package/dist/vite-plugin/index.js.map +1 -1
- package/dist/vite-plugin/resources-route.d.ts.map +1 -1
- package/dist/vite-plugin/resources-route.js +3 -8
- package/dist/vite-plugin/resources-route.js.map +1 -1
- package/dist/vite-plugin/sveltekit-generator.d.ts.map +1 -1
- package/dist/vite-plugin/sveltekit-generator.js +7 -11
- package/dist/vite-plugin/sveltekit-generator.js.map +1 -1
- package/dist/vite-plugin/sync-apply-route.d.ts +16 -0
- package/dist/vite-plugin/sync-apply-route.d.ts.map +1 -1
- package/dist/vite-plugin/sync-apply-route.js +48 -6
- package/dist/vite-plugin/sync-apply-route.js.map +1 -1
- package/package.json +12 -12
|
@@ -75,6 +75,33 @@ export declare function inheritedCollectionSurface(surface: ExposureSurface, ite
|
|
|
75
75
|
* Returns an empty object when nothing changes.
|
|
76
76
|
*/
|
|
77
77
|
export declare function inheritCollectionExposure(collectionConfig: SmartObjectConfig, itemConfig: SmartObjectConfig, methods?: CollectionMethodNames): Partial<Pick<SmartObjectConfig, ExposureSurface>>;
|
|
78
|
+
/**
|
|
79
|
+
* `exclude: ['*']` was written to mean "nothing on this surface" (smrt-profiles
|
|
80
|
+
* `MagicLinkToken`), but no generator ever read `'*'` as a wildcard, so it
|
|
81
|
+
* excluded nothing and left every operation open. Read it fail-closed: a
|
|
82
|
+
* surface whose `exclude` names `'*'` becomes `include: []`, keeping its other
|
|
83
|
+
* options. Returns the config unchanged (same object) when no surface does
|
|
84
|
+
* (#3468).
|
|
85
|
+
*/
|
|
86
|
+
export declare function closeWildcardExclusions(config: SmartObjectConfig): SmartObjectConfig;
|
|
87
|
+
/**
|
|
88
|
+
* Combine a surface the scanner copied into a collection's manifest entry with
|
|
89
|
+
* the value its model's runtime config now implies, never opening more than
|
|
90
|
+
* either: `false` wins, `include` lists intersect, `exclude` lists union, and
|
|
91
|
+
* the copied value's other options (routes, `skipApiCheck`) are kept. A model
|
|
92
|
+
* the runtime leaves open (`undefined`) keeps the copied value, so a later
|
|
93
|
+
* registration cannot widen what the build closed (#3468).
|
|
94
|
+
*/
|
|
95
|
+
export declare function narrowInheritedCollectionSurface(copied: SmartObjectConfig[ExposureSurface] | undefined, current: SmartObjectConfig[ExposureSurface] | undefined): SmartObjectConfig[ExposureSurface] | undefined;
|
|
96
|
+
/**
|
|
97
|
+
* Close the generated CRUD operations a collection's surface opens on a
|
|
98
|
+
* collection-CRUD transport (`mcp`, `cli`) that its model's effective surface
|
|
99
|
+
* does not, by adding them to the surface's `exclude`. Custom-method names are
|
|
100
|
+
* untouched: a collection still opens its own methods by listing them. Returns
|
|
101
|
+
* the value unchanged when nothing exceeds the model, and for an abstract
|
|
102
|
+
* model, whose collection surface the build already narrows.
|
|
103
|
+
*/
|
|
104
|
+
export declare function capCollectionCrudToModel(surface: ExposureSurface, collectionValue: SmartObjectConfig[ExposureSurface] | undefined, itemConfig: SmartObjectConfig): SmartObjectConfig[ExposureSurface] | undefined;
|
|
78
105
|
/** A CRUD write a collection opens on a transport where its model does not. */
|
|
79
106
|
export interface CollectionWriteEscalation {
|
|
80
107
|
surface: ExposureSurface;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"collection-exposure.d.ts","sourceRoot":"","sources":["../../src/registry/collection-exposure.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,YAAS,CAAC;AAEjD,qEAAqE;AACrE,MAAM,MAAM,eAAe,GAAG,KAAK,GAAG,KAAK,GAAG,KAAK,CAAC;AAEpD,oDAAoD;AACpD,eAAO,MAAM,iBAAiB,EAAE,SAAS,eAAe,EAIvD,CAAC;AAEF,QAAA,MAAM,UAAU,wDAAyD,CAAC;AAE1E,kCAAkC;AAClC,MAAM,MAAM,QAAQ,GAAG,CAAC,OAAO,UAAU,CAAC,CAAC,MAAM,CAAC,CAAC;AAEnD,4CAA4C;AAC5C,eAAO,MAAM,gBAAgB,EAAE,SAAS,QAAQ,EAI/C,CAAC;AAIF;;;;GAIG;AACH,eAAO,MAAM,wBAAwB,EAAE,SAAS,eAAe,EAG9D,CAAC;AAEF;;;;GAIG;AACH,MAAM,WAAW,qBAAqB;IACpC,UAAU,CAAC,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IACjC,IAAI,CAAC,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;CAC5B;AAMD;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,GAAG,QAAQ,EAAE,CAa3D;AAED;;;;GAIG;AACH,wBAAgB,0BAA0B,CACxC,OAAO,EAAE,eAAe,EACxB,UAAU,EAAE,iBAAiB,EAC7B,OAAO,GAAE,qBAA0B,GAClC,iBAAiB,CAAC,eAAe,CAAC,GAAG,SAAS,CAmFhD;AAED;;;;GAIG;AACH,wBAAgB,yBAAyB,CACvC,gBAAgB,EAAE,iBAAiB,EACnC,UAAU,EAAE,iBAAiB,EAC7B,OAAO,GAAE,qBAA0B,GAClC,OAAO,CAAC,IAAI,CAAC,iBAAiB,EAAE,eAAe,CAAC,CAAC,CAUnD;AAED,+EAA+E;AAC/E,MAAM,WAAW,yBAAyB;IACxC,OAAO,EAAE,eAAe,CAAC;IACzB,KAAK,EAAE,QAAQ,EAAE,CAAC;CACnB;AAED;;;;;GAKG;AACH,wBAAgB,8BAA8B,CAC5C,gBAAgB,EAAE,iBAAiB,EACnC,UAAU,EAAE,iBAAiB,GAC5B,yBAAyB,EAAE,CAY7B;AAED;;GAEG;AACH,wBAAgB,gCAAgC,CAC9C,QAAQ,EAAE,KAAK,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,yBAAyB,EAAE,CAAC;CAC1C,CAAC,GACD,MAAM,CAmBR"}
|
|
1
|
+
{"version":3,"file":"collection-exposure.d.ts","sourceRoot":"","sources":["../../src/registry/collection-exposure.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,YAAS,CAAC;AAEjD,qEAAqE;AACrE,MAAM,MAAM,eAAe,GAAG,KAAK,GAAG,KAAK,GAAG,KAAK,CAAC;AAEpD,oDAAoD;AACpD,eAAO,MAAM,iBAAiB,EAAE,SAAS,eAAe,EAIvD,CAAC;AAEF,QAAA,MAAM,UAAU,wDAAyD,CAAC;AAE1E,kCAAkC;AAClC,MAAM,MAAM,QAAQ,GAAG,CAAC,OAAO,UAAU,CAAC,CAAC,MAAM,CAAC,CAAC;AAEnD,4CAA4C;AAC5C,eAAO,MAAM,gBAAgB,EAAE,SAAS,QAAQ,EAI/C,CAAC;AAIF;;;;GAIG;AACH,eAAO,MAAM,wBAAwB,EAAE,SAAS,eAAe,EAG9D,CAAC;AAEF;;;;GAIG;AACH,MAAM,WAAW,qBAAqB;IACpC,UAAU,CAAC,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IACjC,IAAI,CAAC,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;CAC5B;AAMD;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,GAAG,QAAQ,EAAE,CAa3D;AAED;;;;GAIG;AACH,wBAAgB,0BAA0B,CACxC,OAAO,EAAE,eAAe,EACxB,UAAU,EAAE,iBAAiB,EAC7B,OAAO,GAAE,qBAA0B,GAClC,iBAAiB,CAAC,eAAe,CAAC,GAAG,SAAS,CAmFhD;AAED;;;;GAIG;AACH,wBAAgB,yBAAyB,CACvC,gBAAgB,EAAE,iBAAiB,EACnC,UAAU,EAAE,iBAAiB,EAC7B,OAAO,GAAE,qBAA0B,GAClC,OAAO,CAAC,IAAI,CAAC,iBAAiB,EAAE,eAAe,CAAC,CAAC,CAUnD;AAED;;;;;;;GAOG;AACH,wBAAgB,uBAAuB,CACrC,MAAM,EAAE,iBAAiB,GACxB,iBAAiB,CAanB;AAED;;;;;;;GAOG;AACH,wBAAgB,gCAAgC,CAC9C,MAAM,EAAE,iBAAiB,CAAC,eAAe,CAAC,GAAG,SAAS,EACtD,OAAO,EAAE,iBAAiB,CAAC,eAAe,CAAC,GAAG,SAAS,GACtD,iBAAiB,CAAC,eAAe,CAAC,GAAG,SAAS,CA4BhD;AAED;;;;;;;GAOG;AACH,wBAAgB,wBAAwB,CACtC,OAAO,EAAE,eAAe,EACxB,eAAe,EAAE,iBAAiB,CAAC,eAAe,CAAC,GAAG,SAAS,EAC/D,UAAU,EAAE,iBAAiB,GAC5B,iBAAiB,CAAC,eAAe,CAAC,GAAG,SAAS,CAwBhD;AAED,+EAA+E;AAC/E,MAAM,WAAW,yBAAyB;IACxC,OAAO,EAAE,eAAe,CAAC;IACzB,KAAK,EAAE,QAAQ,EAAE,CAAC;CACnB;AAED;;;;;GAKG;AACH,wBAAgB,8BAA8B,CAC5C,gBAAgB,EAAE,iBAAiB,EACnC,UAAU,EAAE,iBAAiB,GAC5B,yBAAyB,EAAE,CAY7B;AAED;;GAEG;AACH,wBAAgB,gCAAgC,CAC9C,QAAQ,EAAE,KAAK,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,yBAAyB,EAAE,CAAC;CAC1C,CAAC,GACD,MAAM,CAmBR"}
|
|
@@ -95,6 +95,79 @@ function inheritCollectionExposure(collectionConfig, itemConfig, methods = {}) {
|
|
|
95
95
|
return inherited;
|
|
96
96
|
}
|
|
97
97
|
/**
|
|
98
|
+
* `exclude: ['*']` was written to mean "nothing on this surface" (smrt-profiles
|
|
99
|
+
* `MagicLinkToken`), but no generator ever read `'*'` as a wildcard, so it
|
|
100
|
+
* excluded nothing and left every operation open. Read it fail-closed: a
|
|
101
|
+
* surface whose `exclude` names `'*'` becomes `include: []`, keeping its other
|
|
102
|
+
* options. Returns the config unchanged (same object) when no surface does
|
|
103
|
+
* (#3468).
|
|
104
|
+
*/
|
|
105
|
+
function closeWildcardExclusions(config) {
|
|
106
|
+
let next;
|
|
107
|
+
for (const surface of EXPOSURE_SURFACES) {
|
|
108
|
+
const value = config[surface];
|
|
109
|
+
if (!value || typeof value !== "object") continue;
|
|
110
|
+
const { exclude } = value;
|
|
111
|
+
if (!Array.isArray(exclude) || !exclude.includes("*")) continue;
|
|
112
|
+
const closed = {
|
|
113
|
+
...value,
|
|
114
|
+
include: []
|
|
115
|
+
};
|
|
116
|
+
delete closed.exclude;
|
|
117
|
+
next ??= { ...config };
|
|
118
|
+
next[surface] = closed;
|
|
119
|
+
}
|
|
120
|
+
return next ?? config;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Combine a surface the scanner copied into a collection's manifest entry with
|
|
124
|
+
* the value its model's runtime config now implies, never opening more than
|
|
125
|
+
* either: `false` wins, `include` lists intersect, `exclude` lists union, and
|
|
126
|
+
* the copied value's other options (routes, `skipApiCheck`) are kept. A model
|
|
127
|
+
* the runtime leaves open (`undefined`) keeps the copied value, so a later
|
|
128
|
+
* registration cannot widen what the build closed (#3468).
|
|
129
|
+
*/
|
|
130
|
+
function narrowInheritedCollectionSurface(copied, current) {
|
|
131
|
+
if (copied === false || current === false) return false;
|
|
132
|
+
if (current === void 0 || current === true) return copied;
|
|
133
|
+
if (copied === void 0 || copied === true) return current;
|
|
134
|
+
const left = copied;
|
|
135
|
+
const right = current;
|
|
136
|
+
const strings = (value) => Array.isArray(value) ? value.filter((entry) => typeof entry === "string") : void 0;
|
|
137
|
+
const leftInclude = strings(left.include);
|
|
138
|
+
const rightInclude = strings(right.include);
|
|
139
|
+
const include = leftInclude && rightInclude ? leftInclude.filter((entry) => rightInclude.includes(entry)) : leftInclude ?? rightInclude;
|
|
140
|
+
const exclude = [.../* @__PURE__ */ new Set([...strings(left.exclude) ?? [], ...strings(right.exclude) ?? []])];
|
|
141
|
+
const narrowed = { ...left };
|
|
142
|
+
delete narrowed.include;
|
|
143
|
+
delete narrowed.exclude;
|
|
144
|
+
if (include !== void 0) narrowed.include = include;
|
|
145
|
+
if (exclude.length > 0) narrowed.exclude = exclude;
|
|
146
|
+
return narrowed;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Close the generated CRUD operations a collection's surface opens on a
|
|
150
|
+
* collection-CRUD transport (`mcp`, `cli`) that its model's effective surface
|
|
151
|
+
* does not, by adding them to the surface's `exclude`. Custom-method names are
|
|
152
|
+
* untouched: a collection still opens its own methods by listing them. Returns
|
|
153
|
+
* the value unchanged when nothing exceeds the model, and for an abstract
|
|
154
|
+
* model, whose collection surface the build already narrows.
|
|
155
|
+
*/
|
|
156
|
+
function capCollectionCrudToModel(surface, collectionValue, itemConfig) {
|
|
157
|
+
if (!COLLECTION_CRUD_SURFACES.includes(surface)) return collectionValue;
|
|
158
|
+
if (collectionValue === false || itemConfig.abstract === true) return collectionValue;
|
|
159
|
+
let modelVerbs = exposedCrudVerbs(itemConfig[surface]);
|
|
160
|
+
if (surface === "mcp" && itemConfig.sensitive === true) modelVerbs = modelVerbs.filter((verb) => CRUD_READ_VERBS.includes(verb));
|
|
161
|
+
const extra = exposedCrudVerbs(collectionValue).filter((verb) => !modelVerbs.includes(verb));
|
|
162
|
+
if (extra.length === 0) return collectionValue;
|
|
163
|
+
const base = collectionValue && typeof collectionValue === "object" ? collectionValue : {};
|
|
164
|
+
const excluded = Array.isArray(base.exclude) ? base.exclude.filter((entry) => typeof entry === "string") : [];
|
|
165
|
+
return {
|
|
166
|
+
...base,
|
|
167
|
+
exclude: [.../* @__PURE__ */ new Set([...excluded, ...extra])]
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
98
171
|
* CRUD writes the collection's effective config opens that the model's does
|
|
99
172
|
* not, per transport on which collections get CRUD operations. Pass the
|
|
100
173
|
* collection config AFTER inheritance; an undecorated collection then never
|
|
@@ -130,6 +203,6 @@ function formatCollectionWriteEscalations(findings) {
|
|
|
130
203
|
].join("\n");
|
|
131
204
|
}
|
|
132
205
|
//#endregion
|
|
133
|
-
export { COLLECTION_CRUD_SURFACES, CRUD_WRITE_VERBS, EXPOSURE_SURFACES, exposedCrudVerbs, findCollectionWriteEscalations, formatCollectionWriteEscalations, inheritCollectionExposure, inheritedCollectionSurface };
|
|
206
|
+
export { COLLECTION_CRUD_SURFACES, CRUD_WRITE_VERBS, EXPOSURE_SURFACES, capCollectionCrudToModel, closeWildcardExclusions, exposedCrudVerbs, findCollectionWriteEscalations, formatCollectionWriteEscalations, inheritCollectionExposure, inheritedCollectionSurface, narrowInheritedCollectionSurface };
|
|
134
207
|
|
|
135
208
|
//# sourceMappingURL=collection-exposure.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"collection-exposure.js","names":[],"sources":["../../src/registry/collection-exposure.ts"],"sourcesContent":["/**\n * Generated-surface (api/mcp/cli) exposure that a collection class takes from\n * its model.\n *\n * A collection class is registered as a resource of its own, so every\n * transport generates operations for it: `<name>collection_create` MCP tools,\n * `<name>collection:create` CLI commands, and routes for its public methods.\n * Those operations reach the same rows as the model's, so a collection must\n * never publish more than its model does. A surface the collection does not\n * configure itself therefore inherits the model's:\n *\n * - `false` stays `false`.\n * - An `include` list keeps its CRUD verbs, plus any name that only the\n * collection declares (the model publishing a collection-hosted action,\n * with its `api.routes` entry). A name the model declares is the model's\n * own method; a collection method that happens to share it is a different\n * operation, so the collection's custom methods stay closed until the\n * collection lists them itself.\n * - An `exclude` list is copied as is.\n * - A model with no list (or `true`) leaves the collection's surface unset,\n * which is the historical open default.\n * - A `sensitive: true` model's collection gets read-only MCP (`list`/`get`\n * where the model has them) unless the collection configures `mcp` itself.\n *\n * A collection that configures a surface keeps its own value; that is how a\n * collection opts in to its own custom methods (for example\n * `@smrt({ api: { include: ['resolveBatch'] } })`). Its CRUD writes are then\n * checked against the model by {@link findCollectionWriteEscalations}.\n *\n * Pure functions over config objects: the manifest generator applies them at\n * build time and `ObjectRegistry.getConfig()` applies them again at runtime, so\n * a manifest built before this rule existed is held to it too.\n */\nimport type { SmartObjectConfig } from './types';\n\n/** The generated transports whose exposure a collection inherits. */\nexport type ExposureSurface = 'api' | 'mcp' | 'cli';\n\n/** Every generated transport, in a stable order. */\nexport const EXPOSURE_SURFACES: readonly ExposureSurface[] = [\n 'api',\n 'mcp',\n 'cli',\n];\n\nconst CRUD_VERBS = ['list', 'get', 'create', 'update', 'delete'] as const;\n\n/** A generated CRUD operation. */\nexport type CrudVerb = (typeof CRUD_VERBS)[number];\n\n/** The CRUD operations that change rows. */\nexport const CRUD_WRITE_VERBS: readonly CrudVerb[] = [\n 'create',\n 'update',\n 'delete',\n];\n\nconst CRUD_READ_VERBS: readonly CrudVerb[] = ['list', 'get'];\n\n/**\n * Transports on which a collection class gets generated CRUD operations of its\n * own. REST serves collection CRUD through the model's routes and gives a\n * collection class only its custom-method routes, so `api` is not one.\n */\nexport const COLLECTION_CRUD_SURFACES: readonly ExposureSurface[] = [\n 'mcp',\n 'cli',\n];\n\n/**\n * Method names of a collection class and of its model. When given, an entry in\n * the model's `include` list that names a method only the collection declares\n * carries over to the collection (see {@link inheritedCollectionSurface}).\n */\nexport interface CollectionMethodNames {\n collection?: ReadonlySet<string>;\n item?: ReadonlySet<string>;\n}\n\nfunction isCrudVerb(name: unknown): name is CrudVerb {\n return (CRUD_VERBS as readonly unknown[]).includes(name);\n}\n\n/**\n * The CRUD verbs a surface config opens, with the same reading the MCP and CLI\n * generators use: `false` opens none, an absent value or `true` opens all, an\n * object opens `include` (or everything when it has no `include`) minus\n * `exclude`.\n */\nexport function exposedCrudVerbs(value: unknown): CrudVerb[] {\n if (value === false) return [];\n if (!value || typeof value !== 'object') return [...CRUD_VERBS];\n const { include, exclude } = value as {\n include?: unknown;\n exclude?: unknown;\n };\n const included = Array.isArray(include) ? include : undefined;\n const excluded = Array.isArray(exclude) ? exclude : [];\n return CRUD_VERBS.filter(\n (verb) =>\n (!included || included.includes(verb)) && !excluded.includes(verb),\n );\n}\n\n/**\n * The value a collection inherits for one surface from its model, or\n * `undefined` when the model leaves that surface open (the collection keeps\n * its historical default).\n */\nexport function inheritedCollectionSurface(\n surface: ExposureSurface,\n itemConfig: SmartObjectConfig,\n methods: CollectionMethodNames = {},\n): SmartObjectConfig[ExposureSurface] | undefined {\n const itemValue = itemConfig[surface];\n if (itemValue === false) return false;\n\n if (surface === 'mcp' && itemConfig.sensitive === true) {\n return {\n include: exposedCrudVerbs(itemValue).filter((verb) =>\n CRUD_READ_VERBS.includes(verb),\n ),\n };\n }\n\n if (!itemValue || typeof itemValue !== 'object') return undefined;\n\n const source = itemValue as {\n include?: unknown;\n exclude?: unknown;\n skipApiCheck?: unknown;\n };\n const inherited: {\n include?: string[];\n exclude?: string[];\n skipApiCheck?: boolean | string[];\n routes?: Record<string, unknown>;\n principalContext?: unknown;\n } = {};\n const hosted: string[] = [];\n if (Array.isArray(source.include)) {\n inherited.include = source.include.filter((entry) => {\n if (isCrudVerb(entry)) return true;\n // A name only the collection declares can mean nothing else: the\n // model is publishing a collection-hosted action (the legacy\n // `api.routes` `scope: 'collection'` form). A name the model also\n // declares is the model's own method and stays off the collection.\n if (\n typeof entry === 'string' &&\n methods.collection?.has(entry) &&\n !methods.item?.has(entry)\n ) {\n hosted.push(entry);\n return true;\n }\n return false;\n });\n }\n if (surface === 'api' && hosted.length > 0) {\n const api = itemValue as {\n routes?: Record<string, unknown>;\n principalContext?: unknown;\n };\n const routes = Object.fromEntries(\n Object.entries(api.routes ?? {}).filter(([name]) =>\n hosted.includes(name),\n ),\n );\n if (Object.keys(routes).length > 0) inherited.routes = routes;\n if (api.principalContext !== undefined) {\n inherited.principalContext = api.principalContext;\n }\n }\n if (Array.isArray(source.exclude)) {\n inherited.exclude = source.exclude.filter(\n (entry): entry is string => typeof entry === 'string',\n );\n }\n if (inherited.include === undefined && inherited.exclude === undefined) {\n // Route or context options alone do not narrow the model's surface.\n return undefined;\n }\n if (surface === 'cli') {\n // The model's acknowledgement that a command has no API route carries\n // over for the CRUD verbs the collection inherits.\n if (source.skipApiCheck === true) {\n inherited.skipApiCheck = true;\n } else if (Array.isArray(source.skipApiCheck)) {\n const verbs = source.skipApiCheck.filter(\n (entry): entry is CrudVerb =>\n isCrudVerb(entry) && (inherited.include?.includes(entry) ?? true),\n );\n if (verbs.length > 0) inherited.skipApiCheck = verbs;\n }\n }\n return inherited as SmartObjectConfig[ExposureSurface];\n}\n\n/**\n * The surfaces a collection inherits from its model: every surface the\n * collection leaves unset and the model narrows (see the module comment).\n * Returns an empty object when nothing changes.\n */\nexport function inheritCollectionExposure(\n collectionConfig: SmartObjectConfig,\n itemConfig: SmartObjectConfig,\n methods: CollectionMethodNames = {},\n): Partial<Pick<SmartObjectConfig, ExposureSurface>> {\n const inherited: Partial<Pick<SmartObjectConfig, ExposureSurface>> = {};\n for (const surface of EXPOSURE_SURFACES) {\n if (collectionConfig[surface] !== undefined) continue;\n const value = inheritedCollectionSurface(surface, itemConfig, methods);\n if (value !== undefined) {\n (inherited as Record<ExposureSurface, unknown>)[surface] = value;\n }\n }\n return inherited;\n}\n\n/** A CRUD write a collection opens on a transport where its model does not. */\nexport interface CollectionWriteEscalation {\n surface: ExposureSurface;\n verbs: CrudVerb[];\n}\n\n/**\n * CRUD writes the collection's effective config opens that the model's does\n * not, per transport on which collections get CRUD operations. Pass the\n * collection config AFTER inheritance; an undecorated collection then never\n * escalates, so a finding always points at the collection's own `@smrt()`.\n */\nexport function findCollectionWriteEscalations(\n collectionConfig: SmartObjectConfig,\n itemConfig: SmartObjectConfig,\n): CollectionWriteEscalation[] {\n const escalations: CollectionWriteEscalation[] = [];\n for (const surface of COLLECTION_CRUD_SURFACES) {\n const modelWrites = new Set(\n itemConfig.abstract === true ? [] : exposedCrudVerbs(itemConfig[surface]),\n );\n const verbs = exposedCrudVerbs(collectionConfig[surface]).filter(\n (verb) => CRUD_WRITE_VERBS.includes(verb) && !modelWrites.has(verb),\n );\n if (verbs.length > 0) escalations.push({ surface, verbs });\n }\n return escalations;\n}\n\n/**\n * Build-time error text for collections that open writes their model closes.\n */\nexport function formatCollectionWriteEscalations(\n findings: Array<{\n collection: string;\n model: string;\n escalations: CollectionWriteEscalation[];\n }>,\n): string {\n const lines = findings.flatMap(({ collection, model, escalations }) =>\n escalations.map(\n ({ surface, verbs }) =>\n ` - ${collection} opens ${surface} ${verbs.join('/')}, which ${model} does not.`,\n ),\n );\n return [\n '[smrt] A collection class publishes generated writes its model closes:',\n ...lines,\n '',\n 'A collection reaches the same rows as its model, so it may not open a CRUD',\n 'write the model keeps closed. Either:',\n \" - remove the verb from the collection's own @smrt() surface (or drop that\",\n \" surface key so the collection inherits the model's), or\",\n ' - open the verb on the model itself, if it is meant to be public.',\n \"Custom collection methods are not affected: list them in the collection's\",\n 'own include to expose them.',\n ].join('\\n');\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"collection-exposure.js","names":[],"sources":["../../src/registry/collection-exposure.ts"],"sourcesContent":["/**\n * Generated-surface (api/mcp/cli) exposure that a collection class takes from\n * its model.\n *\n * A collection class is registered as a resource of its own, so every\n * transport generates operations for it: `<name>collection_create` MCP tools,\n * `<name>collection:create` CLI commands, and routes for its public methods.\n * Those operations reach the same rows as the model's, so a collection must\n * never publish more than its model does. A surface the collection does not\n * configure itself therefore inherits the model's:\n *\n * - `false` stays `false`.\n * - An `include` list keeps its CRUD verbs, plus any name that only the\n * collection declares (the model publishing a collection-hosted action,\n * with its `api.routes` entry). A name the model declares is the model's\n * own method; a collection method that happens to share it is a different\n * operation, so the collection's custom methods stay closed until the\n * collection lists them itself.\n * - An `exclude` list is copied as is.\n * - A model with no list (or `true`) leaves the collection's surface unset,\n * which is the historical open default.\n * - A `sensitive: true` model's collection gets read-only MCP (`list`/`get`\n * where the model has them) unless the collection configures `mcp` itself.\n *\n * A collection that configures a surface keeps its own value; that is how a\n * collection opts in to its own custom methods (for example\n * `@smrt({ api: { include: ['resolveBatch'] } })`). Its CRUD writes are then\n * checked against the model by {@link findCollectionWriteEscalations}.\n *\n * Pure functions over config objects: the manifest generator applies them at\n * build time and `ObjectRegistry.getConfig()` applies them again at runtime, so\n * a manifest built before this rule existed is held to it too.\n */\nimport type { SmartObjectConfig } from './types';\n\n/** The generated transports whose exposure a collection inherits. */\nexport type ExposureSurface = 'api' | 'mcp' | 'cli';\n\n/** Every generated transport, in a stable order. */\nexport const EXPOSURE_SURFACES: readonly ExposureSurface[] = [\n 'api',\n 'mcp',\n 'cli',\n];\n\nconst CRUD_VERBS = ['list', 'get', 'create', 'update', 'delete'] as const;\n\n/** A generated CRUD operation. */\nexport type CrudVerb = (typeof CRUD_VERBS)[number];\n\n/** The CRUD operations that change rows. */\nexport const CRUD_WRITE_VERBS: readonly CrudVerb[] = [\n 'create',\n 'update',\n 'delete',\n];\n\nconst CRUD_READ_VERBS: readonly CrudVerb[] = ['list', 'get'];\n\n/**\n * Transports on which a collection class gets generated CRUD operations of its\n * own. REST serves collection CRUD through the model's routes and gives a\n * collection class only its custom-method routes, so `api` is not one.\n */\nexport const COLLECTION_CRUD_SURFACES: readonly ExposureSurface[] = [\n 'mcp',\n 'cli',\n];\n\n/**\n * Method names of a collection class and of its model. When given, an entry in\n * the model's `include` list that names a method only the collection declares\n * carries over to the collection (see {@link inheritedCollectionSurface}).\n */\nexport interface CollectionMethodNames {\n collection?: ReadonlySet<string>;\n item?: ReadonlySet<string>;\n}\n\nfunction isCrudVerb(name: unknown): name is CrudVerb {\n return (CRUD_VERBS as readonly unknown[]).includes(name);\n}\n\n/**\n * The CRUD verbs a surface config opens, with the same reading the MCP and CLI\n * generators use: `false` opens none, an absent value or `true` opens all, an\n * object opens `include` (or everything when it has no `include`) minus\n * `exclude`.\n */\nexport function exposedCrudVerbs(value: unknown): CrudVerb[] {\n if (value === false) return [];\n if (!value || typeof value !== 'object') return [...CRUD_VERBS];\n const { include, exclude } = value as {\n include?: unknown;\n exclude?: unknown;\n };\n const included = Array.isArray(include) ? include : undefined;\n const excluded = Array.isArray(exclude) ? exclude : [];\n return CRUD_VERBS.filter(\n (verb) =>\n (!included || included.includes(verb)) && !excluded.includes(verb),\n );\n}\n\n/**\n * The value a collection inherits for one surface from its model, or\n * `undefined` when the model leaves that surface open (the collection keeps\n * its historical default).\n */\nexport function inheritedCollectionSurface(\n surface: ExposureSurface,\n itemConfig: SmartObjectConfig,\n methods: CollectionMethodNames = {},\n): SmartObjectConfig[ExposureSurface] | undefined {\n const itemValue = itemConfig[surface];\n if (itemValue === false) return false;\n\n if (surface === 'mcp' && itemConfig.sensitive === true) {\n return {\n include: exposedCrudVerbs(itemValue).filter((verb) =>\n CRUD_READ_VERBS.includes(verb),\n ),\n };\n }\n\n if (!itemValue || typeof itemValue !== 'object') return undefined;\n\n const source = itemValue as {\n include?: unknown;\n exclude?: unknown;\n skipApiCheck?: unknown;\n };\n const inherited: {\n include?: string[];\n exclude?: string[];\n skipApiCheck?: boolean | string[];\n routes?: Record<string, unknown>;\n principalContext?: unknown;\n } = {};\n const hosted: string[] = [];\n if (Array.isArray(source.include)) {\n inherited.include = source.include.filter((entry) => {\n if (isCrudVerb(entry)) return true;\n // A name only the collection declares can mean nothing else: the\n // model is publishing a collection-hosted action (the legacy\n // `api.routes` `scope: 'collection'` form). A name the model also\n // declares is the model's own method and stays off the collection.\n if (\n typeof entry === 'string' &&\n methods.collection?.has(entry) &&\n !methods.item?.has(entry)\n ) {\n hosted.push(entry);\n return true;\n }\n return false;\n });\n }\n if (surface === 'api' && hosted.length > 0) {\n const api = itemValue as {\n routes?: Record<string, unknown>;\n principalContext?: unknown;\n };\n const routes = Object.fromEntries(\n Object.entries(api.routes ?? {}).filter(([name]) =>\n hosted.includes(name),\n ),\n );\n if (Object.keys(routes).length > 0) inherited.routes = routes;\n if (api.principalContext !== undefined) {\n inherited.principalContext = api.principalContext;\n }\n }\n if (Array.isArray(source.exclude)) {\n inherited.exclude = source.exclude.filter(\n (entry): entry is string => typeof entry === 'string',\n );\n }\n if (inherited.include === undefined && inherited.exclude === undefined) {\n // Route or context options alone do not narrow the model's surface.\n return undefined;\n }\n if (surface === 'cli') {\n // The model's acknowledgement that a command has no API route carries\n // over for the CRUD verbs the collection inherits.\n if (source.skipApiCheck === true) {\n inherited.skipApiCheck = true;\n } else if (Array.isArray(source.skipApiCheck)) {\n const verbs = source.skipApiCheck.filter(\n (entry): entry is CrudVerb =>\n isCrudVerb(entry) && (inherited.include?.includes(entry) ?? true),\n );\n if (verbs.length > 0) inherited.skipApiCheck = verbs;\n }\n }\n return inherited as SmartObjectConfig[ExposureSurface];\n}\n\n/**\n * The surfaces a collection inherits from its model: every surface the\n * collection leaves unset and the model narrows (see the module comment).\n * Returns an empty object when nothing changes.\n */\nexport function inheritCollectionExposure(\n collectionConfig: SmartObjectConfig,\n itemConfig: SmartObjectConfig,\n methods: CollectionMethodNames = {},\n): Partial<Pick<SmartObjectConfig, ExposureSurface>> {\n const inherited: Partial<Pick<SmartObjectConfig, ExposureSurface>> = {};\n for (const surface of EXPOSURE_SURFACES) {\n if (collectionConfig[surface] !== undefined) continue;\n const value = inheritedCollectionSurface(surface, itemConfig, methods);\n if (value !== undefined) {\n (inherited as Record<ExposureSurface, unknown>)[surface] = value;\n }\n }\n return inherited;\n}\n\n/**\n * `exclude: ['*']` was written to mean \"nothing on this surface\" (smrt-profiles\n * `MagicLinkToken`), but no generator ever read `'*'` as a wildcard, so it\n * excluded nothing and left every operation open. Read it fail-closed: a\n * surface whose `exclude` names `'*'` becomes `include: []`, keeping its other\n * options. Returns the config unchanged (same object) when no surface does\n * (#3468).\n */\nexport function closeWildcardExclusions(\n config: SmartObjectConfig,\n): SmartObjectConfig {\n let next: SmartObjectConfig | undefined;\n for (const surface of EXPOSURE_SURFACES) {\n const value = config[surface];\n if (!value || typeof value !== 'object') continue;\n const { exclude } = value as { exclude?: unknown };\n if (!Array.isArray(exclude) || !exclude.includes('*')) continue;\n const closed: Record<string, unknown> = { ...value, include: [] };\n delete closed.exclude;\n next ??= { ...config };\n (next as Record<string, unknown>)[surface] = closed;\n }\n return next ?? config;\n}\n\n/**\n * Combine a surface the scanner copied into a collection's manifest entry with\n * the value its model's runtime config now implies, never opening more than\n * either: `false` wins, `include` lists intersect, `exclude` lists union, and\n * the copied value's other options (routes, `skipApiCheck`) are kept. A model\n * the runtime leaves open (`undefined`) keeps the copied value, so a later\n * registration cannot widen what the build closed (#3468).\n */\nexport function narrowInheritedCollectionSurface(\n copied: SmartObjectConfig[ExposureSurface] | undefined,\n current: SmartObjectConfig[ExposureSurface] | undefined,\n): SmartObjectConfig[ExposureSurface] | undefined {\n if (copied === false || current === false) return false;\n if (current === undefined || current === true) return copied;\n if (copied === undefined || copied === true) return current;\n const left = copied as { include?: unknown; exclude?: unknown };\n const right = current as { include?: unknown; exclude?: unknown };\n const strings = (value: unknown): string[] | undefined =>\n Array.isArray(value)\n ? value.filter((entry): entry is string => typeof entry === 'string')\n : undefined;\n const leftInclude = strings(left.include);\n const rightInclude = strings(right.include);\n const include =\n leftInclude && rightInclude\n ? leftInclude.filter((entry) => rightInclude.includes(entry))\n : (leftInclude ?? rightInclude);\n const exclude = [\n ...new Set([\n ...(strings(left.exclude) ?? []),\n ...(strings(right.exclude) ?? []),\n ]),\n ];\n const narrowed: Record<string, unknown> = { ...left };\n delete narrowed.include;\n delete narrowed.exclude;\n if (include !== undefined) narrowed.include = include;\n if (exclude.length > 0) narrowed.exclude = exclude;\n return narrowed as SmartObjectConfig[ExposureSurface];\n}\n\n/**\n * Close the generated CRUD operations a collection's surface opens on a\n * collection-CRUD transport (`mcp`, `cli`) that its model's effective surface\n * does not, by adding them to the surface's `exclude`. Custom-method names are\n * untouched: a collection still opens its own methods by listing them. Returns\n * the value unchanged when nothing exceeds the model, and for an abstract\n * model, whose collection surface the build already narrows.\n */\nexport function capCollectionCrudToModel(\n surface: ExposureSurface,\n collectionValue: SmartObjectConfig[ExposureSurface] | undefined,\n itemConfig: SmartObjectConfig,\n): SmartObjectConfig[ExposureSurface] | undefined {\n if (!COLLECTION_CRUD_SURFACES.includes(surface)) return collectionValue;\n if (collectionValue === false || itemConfig.abstract === true) {\n return collectionValue;\n }\n let modelVerbs = exposedCrudVerbs(itemConfig[surface]);\n if (surface === 'mcp' && itemConfig.sensitive === true) {\n modelVerbs = modelVerbs.filter((verb) => CRUD_READ_VERBS.includes(verb));\n }\n const extra = exposedCrudVerbs(collectionValue).filter(\n (verb) => !modelVerbs.includes(verb),\n );\n if (extra.length === 0) return collectionValue;\n const base =\n collectionValue && typeof collectionValue === 'object'\n ? (collectionValue as { exclude?: unknown })\n : {};\n const excluded = Array.isArray(base.exclude)\n ? base.exclude.filter((entry): entry is string => typeof entry === 'string')\n : [];\n return {\n ...base,\n exclude: [...new Set([...excluded, ...extra])],\n } as SmartObjectConfig[ExposureSurface];\n}\n\n/** A CRUD write a collection opens on a transport where its model does not. */\nexport interface CollectionWriteEscalation {\n surface: ExposureSurface;\n verbs: CrudVerb[];\n}\n\n/**\n * CRUD writes the collection's effective config opens that the model's does\n * not, per transport on which collections get CRUD operations. Pass the\n * collection config AFTER inheritance; an undecorated collection then never\n * escalates, so a finding always points at the collection's own `@smrt()`.\n */\nexport function findCollectionWriteEscalations(\n collectionConfig: SmartObjectConfig,\n itemConfig: SmartObjectConfig,\n): CollectionWriteEscalation[] {\n const escalations: CollectionWriteEscalation[] = [];\n for (const surface of COLLECTION_CRUD_SURFACES) {\n const modelWrites = new Set(\n itemConfig.abstract === true ? [] : exposedCrudVerbs(itemConfig[surface]),\n );\n const verbs = exposedCrudVerbs(collectionConfig[surface]).filter(\n (verb) => CRUD_WRITE_VERBS.includes(verb) && !modelWrites.has(verb),\n );\n if (verbs.length > 0) escalations.push({ surface, verbs });\n }\n return escalations;\n}\n\n/**\n * Build-time error text for collections that open writes their model closes.\n */\nexport function formatCollectionWriteEscalations(\n findings: Array<{\n collection: string;\n model: string;\n escalations: CollectionWriteEscalation[];\n }>,\n): string {\n const lines = findings.flatMap(({ collection, model, escalations }) =>\n escalations.map(\n ({ surface, verbs }) =>\n ` - ${collection} opens ${surface} ${verbs.join('/')}, which ${model} does not.`,\n ),\n );\n return [\n '[smrt] A collection class publishes generated writes its model closes:',\n ...lines,\n '',\n 'A collection reaches the same rows as its model, so it may not open a CRUD',\n 'write the model keeps closed. Either:',\n \" - remove the verb from the collection's own @smrt() surface (or drop that\",\n \" surface key so the collection inherits the model's), or\",\n ' - open the verb on the model itself, if it is meant to be public.',\n \"Custom collection methods are not affected: list them in the collection's\",\n 'own include to expose them.',\n ].join('\\n');\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"collection-resolution.d.ts","sourceRoot":"","sources":["../../src/registry/collection-resolution.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAS,CAAC;AAE/C,MAAM,WAAW,4BAA4B;IAC3C,SAAS,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,eAAe,GAAG,SAAS,CAAC;IACzD,kBAAkB,CAAC,EAAE,CACnB,WAAW,EAAE,MAAM,EACnB,SAAS,EAAE,MAAM,KACd,eAAe,GAAG,SAAS,CAAC;IACjC,mBAAmB,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,MAAM,EAAE,CAAC;CACtD;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AACH,eAAO,MAAM,0BAA0B,yFAK7B,CAAC;AAEX,wBAAgB,2BAA2B,CACzC,WAAW,EAAE,MAAM,GAAG,SAAS,GAC9B,OAAO,CAOT;AAED,wBAAgB,0BAA0B,CACxC,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,eAAe,EACvB,MAAM,EAAE,4BAA4B,GACnC,eAAe,GAAG,SAAS,CAa7B;AAED,wBAAgB,+BAA+B,CAC7C,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,eAAe,EAC3B,MAAM,EAAE,4BAA4B,GACnC,eAAe,EAAE,CAiBnB;AAED,wBAAgB,wBAAwB,CACtC,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,eAAe,EAC3B,MAAM,EAAE,4BAA4B,GACnC,OAAO,
|
|
1
|
+
{"version":3,"file":"collection-resolution.d.ts","sourceRoot":"","sources":["../../src/registry/collection-resolution.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAS,CAAC;AAE/C,MAAM,WAAW,4BAA4B;IAC3C,SAAS,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,eAAe,GAAG,SAAS,CAAC;IACzD,kBAAkB,CAAC,EAAE,CACnB,WAAW,EAAE,MAAM,EACnB,SAAS,EAAE,MAAM,KACd,eAAe,GAAG,SAAS,CAAC;IACjC,mBAAmB,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,MAAM,EAAE,CAAC;CACtD;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AACH,eAAO,MAAM,0BAA0B,yFAK7B,CAAC;AAEX,wBAAgB,2BAA2B,CACzC,WAAW,EAAE,MAAM,GAAG,SAAS,GAC9B,OAAO,CAOT;AAED,wBAAgB,0BAA0B,CACxC,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,eAAe,EACvB,MAAM,EAAE,4BAA4B,GACnC,eAAe,GAAG,SAAS,CAa7B;AAED,wBAAgB,+BAA+B,CAC7C,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,eAAe,EAC3B,MAAM,EAAE,4BAA4B,GACnC,eAAe,EAAE,CAiBnB;AAED,wBAAgB,wBAAwB,CACtC,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,eAAe,EAC3B,MAAM,EAAE,4BAA4B,GACnC,OAAO,CA2BT;AA+DD,wBAAgB,8BAA8B,CAC5C,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,eAAe,EAC3B,MAAM,EAAE,4BAA4B,GACnC,MAAM,GAAG,SAAS,CAmCpB"}
|
|
@@ -64,6 +64,7 @@ function getCollectionChainRegistrations(className, registered, lookup) {
|
|
|
64
64
|
return registrations;
|
|
65
65
|
}
|
|
66
66
|
function isCollectionRegistration(className, registered, lookup) {
|
|
67
|
+
if (registered.collectionOf) return true;
|
|
67
68
|
if (isSmrtCollectionExtendsName(registered.extends)) return true;
|
|
68
69
|
let prototype = typeof registered.constructor === "function" ? registered.constructor.prototype : void 0;
|
|
69
70
|
for (let depth = 0; prototype && depth < 64; depth += 1) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"collection-resolution.js","names":[],"sources":["../../src/registry/collection-resolution.ts"],"sourcesContent":["import { SmrtCollection } from '../collection.js';\nimport type { RegisteredClass } from './types';\n\nexport interface CollectionRegistrationLookup {\n findClass: (name: string) => RegisteredClass | undefined;\n findClassInPackage?: (\n packageName: string,\n className: string,\n ) => RegisteredClass | undefined;\n getInheritanceChain: (className: string) => string[];\n}\n\n/**\n * Framework base classes that ARE collections (extend `SmrtCollection`).\n *\n * `SmrtJunction` and `SmrtJunctionBase` are abstract collection bases — junction collections declare\n * `extends SmrtJunction<Item>`, and the manifest records that *direct* parent.\n * `SmrtJunction` is itself never registered in a consumer's registry, so the\n * inheritance-chain walk in `isCollectionRegistration()` cannot reach\n * `SmrtCollection` through it. Recognizing the junction base by name here is\n * what keeps a junction Collection classified as a collection (and therefore\n * redirected to its item class for schema generation) regardless of\n * registration order. Without it, a junction Collection that is iterated\n * before its item class is mistaken for a regular table-bearing class and\n * creates a base-columns-only table, dropping the junction's FK columns\n * (#1342 — `place_assets` vs `profile_assets` asymmetry).\n *\n * `SmrtReportCollection` (`@happyvertical/smrt-reports`) is the same case: a\n * framework collection base that is never registered, so a report collection\n * (`class XCollection extends SmrtReportCollection<X>`) was classified as a\n * table-bearing class and claimed its item's table. The schema planner then\n * rejected the pair as two unrelated owners of one table (#3110).\n *\n * Keep in sync with the collection bases that extend `SmrtCollection` in\n * `packages/core/src/junction.ts` and `packages/reports/src/report.ts`. (`SmrtHierarchical` and\n * `SmrtPolymorphicAssociation` extend `SmrtObject`, not `SmrtCollection`, so\n * they deliberately do NOT belong here.)\n *\n * A new framework collection base must also be added to the other parallel\n * framework-base lists so it is recognized everywhere it matters:\n * - `FRAMEWORK_BASE_CLASSES` in\n * `packages/scanner/src/inheritance-resolver.ts` (scanner chain termination).\n * - `FRAMEWORK_ABSTRACT_BASE_NAMES` in\n * `packages/core/src/scanner/manifest-generator.ts` (field-merge skipping).\n *\n * Downstream tooling that needs this detector (e.g. @happyvertical/smrt-vitest)\n * should IMPORT it from `@happyvertical/smrt-core` rather than keep a copy — a\n * duplicate detector is exactly how the #1342 junction schema-drop bug\n * half-survived the original core fix.\n */\nexport const SMRT_COLLECTION_BASE_NAMES = [\n 'SmrtCollection',\n 'SmrtJunction',\n 'SmrtJunctionBase',\n 'SmrtReportCollection',\n] as const;\n\nexport function isSmrtCollectionExtendsName(\n extendsName: string | undefined,\n): boolean {\n if (!extendsName) {\n return false;\n }\n return SMRT_COLLECTION_BASE_NAMES.some(\n (base) => extendsName === base || extendsName.endsWith(`:${base}`),\n );\n}\n\nexport function resolveRelatedRegistration(\n className: string,\n origin: RegisteredClass,\n lookup: CollectionRegistrationLookup,\n): RegisteredClass | undefined {\n if (className.includes(':')) {\n return lookup.findClass(className);\n }\n\n if (origin.packageName && lookup.findClassInPackage) {\n return (\n lookup.findClassInPackage(origin.packageName, className) ||\n lookup.findClass(className)\n );\n }\n\n return lookup.findClass(className);\n}\n\nexport function getCollectionChainRegistrations(\n className: string,\n registered: RegisteredClass,\n lookup: CollectionRegistrationLookup,\n): RegisteredClass[] {\n const lookupName = registered.qualifiedName || registered.name || className;\n const chain = lookup.getInheritanceChain(lookupName);\n const registrations: RegisteredClass[] = [];\n\n for (const chainName of chain) {\n const chainEntry = resolveRelatedRegistration(\n chainName,\n registered,\n lookup,\n );\n if (chainEntry) {\n registrations.push(chainEntry);\n }\n }\n\n return registrations;\n}\n\nexport function isCollectionRegistration(\n className: string,\n registered: RegisteredClass,\n lookup: CollectionRegistrationLookup,\n): boolean {\n if (isSmrtCollectionExtendsName(registered.extends)) {\n return true;\n }\n\n // A concrete collection may extend an undecorated domain collection whose\n // metadata was never registered. Match real prototype identity, not names or\n // an _itemClass property that an unrelated model could also declare.\n let prototype: object | null | undefined =\n typeof registered.constructor === 'function'\n ? registered.constructor.prototype\n : undefined;\n for (let depth = 0; prototype && depth < 64; depth += 1) {\n if (prototype === SmrtCollection.prototype) return true;\n prototype = Object.getPrototypeOf(prototype);\n }\n\n return getCollectionChainRegistrations(className, registered, lookup).some(\n (chainEntry) => isSmrtCollectionExtendsName(chainEntry.extends),\n );\n}\n\nfunction getStaticItemClassName(\n registered: RegisteredClass,\n): string | undefined {\n const itemClass = (\n registered.constructor as unknown as {\n _itemClass?: { name?: string };\n }\n )._itemClass;\n return itemClass?.name;\n}\n\nfunction getCollectionItemNameCandidates(className: string): string[] {\n const candidates: string[] = [];\n const simpleName = className.includes(':')\n ? className.slice(className.lastIndexOf(':') + 1)\n : className;\n\n const addCandidate = (candidate: string): void => {\n if (\n candidate &&\n candidate !== simpleName &&\n !candidates.includes(candidate)\n ) {\n candidates.push(candidate);\n }\n };\n\n if (simpleName.endsWith('Collection')) {\n addCandidate(simpleName.slice(0, -'Collection'.length));\n }\n\n if (simpleName.endsWith('ies')) {\n addCandidate(`${simpleName.slice(0, -3)}y`);\n } else if (simpleName.endsWith('s')) {\n addCandidate(simpleName.slice(0, -1));\n }\n\n return candidates;\n}\n\nfunction inferCollectionItemClassName(\n className: string,\n registered: RegisteredClass,\n lookup: CollectionRegistrationLookup,\n): string | undefined {\n const lookupName = registered.name || className;\n\n for (const candidate of getCollectionItemNameCandidates(lookupName)) {\n const candidateRegistration = resolveRelatedRegistration(\n candidate,\n registered,\n lookup,\n );\n if (candidateRegistration) {\n return candidate;\n }\n }\n\n return undefined;\n}\n\nexport function resolveCollectionItemClassName(\n className: string,\n registered: RegisteredClass,\n lookup: CollectionRegistrationLookup,\n): string | undefined {\n const staticItemClassName = getStaticItemClassName(registered);\n if (staticItemClassName) {\n return staticItemClassName;\n }\n\n if (registered.extendsTypeArg) {\n return registered.extendsTypeArg;\n }\n\n const inferredItemClassName = inferCollectionItemClassName(\n className,\n registered,\n lookup,\n );\n if (inferredItemClassName) {\n return inferredItemClassName;\n }\n\n const chain = getCollectionChainRegistrations(\n className,\n registered,\n lookup,\n ).reverse();\n\n for (const chainEntry of chain) {\n if (chainEntry === registered) {\n continue;\n }\n if (chainEntry.extendsTypeArg) {\n return chainEntry.extendsTypeArg;\n }\n }\n\n return undefined;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkDA,IAAa,6BAA6B;CACxC;CACA;CACA;CACA;AACF;AAEA,SAAgB,4BACd,aACS;CACT,IAAI,CAAC,aACH,OAAO;CAET,OAAO,2BAA2B,MAC/B,SAAS,gBAAgB,QAAQ,YAAY,SAAS,IAAI,MAAM,CACnE;AACF;AAEA,SAAgB,2BACd,WACA,QACA,QAC6B;CAC7B,IAAI,UAAU,SAAS,GAAG,GACxB,OAAO,OAAO,UAAU,SAAS;CAGnC,IAAI,OAAO,eAAe,OAAO,oBAC/B,OACE,OAAO,mBAAmB,OAAO,aAAa,SAAS,KACvD,OAAO,UAAU,SAAS;CAI9B,OAAO,OAAO,UAAU,SAAS;AACnC;AAEA,SAAgB,gCACd,WACA,YACA,QACmB;CACnB,MAAM,aAAa,WAAW,iBAAiB,WAAW,QAAQ;CAClE,MAAM,QAAQ,OAAO,oBAAoB,UAAU;CACnD,MAAM,gBAAmC,CAAC;CAE1C,KAAK,MAAM,aAAa,OAAO;EAC7B,MAAM,aAAa,2BACjB,WACA,YACA,MACF;EACA,IAAI,YACF,cAAc,KAAK,UAAU;CAEjC;CAEA,OAAO;AACT;AAEA,SAAgB,yBACd,WACA,YACA,QACS;
|
|
1
|
+
{"version":3,"file":"collection-resolution.js","names":[],"sources":["../../src/registry/collection-resolution.ts"],"sourcesContent":["import { SmrtCollection } from '../collection.js';\nimport type { RegisteredClass } from './types';\n\nexport interface CollectionRegistrationLookup {\n findClass: (name: string) => RegisteredClass | undefined;\n findClassInPackage?: (\n packageName: string,\n className: string,\n ) => RegisteredClass | undefined;\n getInheritanceChain: (className: string) => string[];\n}\n\n/**\n * Framework base classes that ARE collections (extend `SmrtCollection`).\n *\n * `SmrtJunction` and `SmrtJunctionBase` are abstract collection bases — junction collections declare\n * `extends SmrtJunction<Item>`, and the manifest records that *direct* parent.\n * `SmrtJunction` is itself never registered in a consumer's registry, so the\n * inheritance-chain walk in `isCollectionRegistration()` cannot reach\n * `SmrtCollection` through it. Recognizing the junction base by name here is\n * what keeps a junction Collection classified as a collection (and therefore\n * redirected to its item class for schema generation) regardless of\n * registration order. Without it, a junction Collection that is iterated\n * before its item class is mistaken for a regular table-bearing class and\n * creates a base-columns-only table, dropping the junction's FK columns\n * (#1342 — `place_assets` vs `profile_assets` asymmetry).\n *\n * `SmrtReportCollection` (`@happyvertical/smrt-reports`) is the same case: a\n * framework collection base that is never registered, so a report collection\n * (`class XCollection extends SmrtReportCollection<X>`) was classified as a\n * table-bearing class and claimed its item's table. The schema planner then\n * rejected the pair as two unrelated owners of one table (#3110).\n *\n * Keep in sync with the collection bases that extend `SmrtCollection` in\n * `packages/core/src/junction.ts` and `packages/reports/src/report.ts`. (`SmrtHierarchical` and\n * `SmrtPolymorphicAssociation` extend `SmrtObject`, not `SmrtCollection`, so\n * they deliberately do NOT belong here.)\n *\n * A new framework collection base must also be added to the other parallel\n * framework-base lists so it is recognized everywhere it matters:\n * - `FRAMEWORK_BASE_CLASSES` in\n * `packages/scanner/src/inheritance-resolver.ts` (scanner chain termination).\n * - `FRAMEWORK_ABSTRACT_BASE_NAMES` in\n * `packages/core/src/scanner/manifest-generator.ts` (field-merge skipping).\n *\n * Downstream tooling that needs this detector (e.g. @happyvertical/smrt-vitest)\n * should IMPORT it from `@happyvertical/smrt-core` rather than keep a copy — a\n * duplicate detector is exactly how the #1342 junction schema-drop bug\n * half-survived the original core fix.\n */\nexport const SMRT_COLLECTION_BASE_NAMES = [\n 'SmrtCollection',\n 'SmrtJunction',\n 'SmrtJunctionBase',\n 'SmrtReportCollection',\n] as const;\n\nexport function isSmrtCollectionExtendsName(\n extendsName: string | undefined,\n): boolean {\n if (!extendsName) {\n return false;\n }\n return SMRT_COLLECTION_BASE_NAMES.some(\n (base) => extendsName === base || extendsName.endsWith(`:${base}`),\n );\n}\n\nexport function resolveRelatedRegistration(\n className: string,\n origin: RegisteredClass,\n lookup: CollectionRegistrationLookup,\n): RegisteredClass | undefined {\n if (className.includes(':')) {\n return lookup.findClass(className);\n }\n\n if (origin.packageName && lookup.findClassInPackage) {\n return (\n lookup.findClassInPackage(origin.packageName, className) ||\n lookup.findClass(className)\n );\n }\n\n return lookup.findClass(className);\n}\n\nexport function getCollectionChainRegistrations(\n className: string,\n registered: RegisteredClass,\n lookup: CollectionRegistrationLookup,\n): RegisteredClass[] {\n const lookupName = registered.qualifiedName || registered.name || className;\n const chain = lookup.getInheritanceChain(lookupName);\n const registrations: RegisteredClass[] = [];\n\n for (const chainName of chain) {\n const chainEntry = resolveRelatedRegistration(\n chainName,\n registered,\n lookup,\n );\n if (chainEntry) {\n registrations.push(chainEntry);\n }\n }\n\n return registrations;\n}\n\nexport function isCollectionRegistration(\n className: string,\n registered: RegisteredClass,\n lookup: CollectionRegistrationLookup,\n): boolean {\n // The scanner resolved the chain at build time with every source in view;\n // its classification holds even when this runtime cannot resolve the\n // parent registration or holds a different core copy's SmrtCollection\n // (#3956).\n if (registered.collectionOf) {\n return true;\n }\n if (isSmrtCollectionExtendsName(registered.extends)) {\n return true;\n }\n\n // A concrete collection may extend an undecorated domain collection whose\n // metadata was never registered. Match real prototype identity, not names or\n // an _itemClass property that an unrelated model could also declare.\n let prototype: object | null | undefined =\n typeof registered.constructor === 'function'\n ? registered.constructor.prototype\n : undefined;\n for (let depth = 0; prototype && depth < 64; depth += 1) {\n if (prototype === SmrtCollection.prototype) return true;\n prototype = Object.getPrototypeOf(prototype);\n }\n\n return getCollectionChainRegistrations(className, registered, lookup).some(\n (chainEntry) => isSmrtCollectionExtendsName(chainEntry.extends),\n );\n}\n\nfunction getStaticItemClassName(\n registered: RegisteredClass,\n): string | undefined {\n const itemClass = (\n registered.constructor as unknown as {\n _itemClass?: { name?: string };\n }\n )._itemClass;\n return itemClass?.name;\n}\n\nfunction getCollectionItemNameCandidates(className: string): string[] {\n const candidates: string[] = [];\n const simpleName = className.includes(':')\n ? className.slice(className.lastIndexOf(':') + 1)\n : className;\n\n const addCandidate = (candidate: string): void => {\n if (\n candidate &&\n candidate !== simpleName &&\n !candidates.includes(candidate)\n ) {\n candidates.push(candidate);\n }\n };\n\n if (simpleName.endsWith('Collection')) {\n addCandidate(simpleName.slice(0, -'Collection'.length));\n }\n\n if (simpleName.endsWith('ies')) {\n addCandidate(`${simpleName.slice(0, -3)}y`);\n } else if (simpleName.endsWith('s')) {\n addCandidate(simpleName.slice(0, -1));\n }\n\n return candidates;\n}\n\nfunction inferCollectionItemClassName(\n className: string,\n registered: RegisteredClass,\n lookup: CollectionRegistrationLookup,\n): string | undefined {\n const lookupName = registered.name || className;\n\n for (const candidate of getCollectionItemNameCandidates(lookupName)) {\n const candidateRegistration = resolveRelatedRegistration(\n candidate,\n registered,\n lookup,\n );\n if (candidateRegistration) {\n return candidate;\n }\n }\n\n return undefined;\n}\n\nexport function resolveCollectionItemClassName(\n className: string,\n registered: RegisteredClass,\n lookup: CollectionRegistrationLookup,\n): string | undefined {\n const staticItemClassName = getStaticItemClassName(registered);\n if (staticItemClassName) {\n return staticItemClassName;\n }\n\n if (registered.extendsTypeArg) {\n return registered.extendsTypeArg;\n }\n\n const inferredItemClassName = inferCollectionItemClassName(\n className,\n registered,\n lookup,\n );\n if (inferredItemClassName) {\n return inferredItemClassName;\n }\n\n const chain = getCollectionChainRegistrations(\n className,\n registered,\n lookup,\n ).reverse();\n\n for (const chainEntry of chain) {\n if (chainEntry === registered) {\n continue;\n }\n if (chainEntry.extendsTypeArg) {\n return chainEntry.extendsTypeArg;\n }\n }\n\n return undefined;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkDA,IAAa,6BAA6B;CACxC;CACA;CACA;CACA;AACF;AAEA,SAAgB,4BACd,aACS;CACT,IAAI,CAAC,aACH,OAAO;CAET,OAAO,2BAA2B,MAC/B,SAAS,gBAAgB,QAAQ,YAAY,SAAS,IAAI,MAAM,CACnE;AACF;AAEA,SAAgB,2BACd,WACA,QACA,QAC6B;CAC7B,IAAI,UAAU,SAAS,GAAG,GACxB,OAAO,OAAO,UAAU,SAAS;CAGnC,IAAI,OAAO,eAAe,OAAO,oBAC/B,OACE,OAAO,mBAAmB,OAAO,aAAa,SAAS,KACvD,OAAO,UAAU,SAAS;CAI9B,OAAO,OAAO,UAAU,SAAS;AACnC;AAEA,SAAgB,gCACd,WACA,YACA,QACmB;CACnB,MAAM,aAAa,WAAW,iBAAiB,WAAW,QAAQ;CAClE,MAAM,QAAQ,OAAO,oBAAoB,UAAU;CACnD,MAAM,gBAAmC,CAAC;CAE1C,KAAK,MAAM,aAAa,OAAO;EAC7B,MAAM,aAAa,2BACjB,WACA,YACA,MACF;EACA,IAAI,YACF,cAAc,KAAK,UAAU;CAEjC;CAEA,OAAO;AACT;AAEA,SAAgB,yBACd,WACA,YACA,QACS;CAKT,IAAI,WAAW,cACb,OAAO;CAET,IAAI,4BAA4B,WAAW,OAAO,GAChD,OAAO;CAMT,IAAI,YACF,OAAO,WAAW,gBAAgB,aAC9B,WAAW,YAAY,YACvB,KAAA;CACN,KAAK,IAAI,QAAQ,GAAG,aAAa,QAAQ,IAAI,SAAS,GAAG;EACvD,IAAI,cAAc,eAAe,WAAW,OAAO;EACnD,YAAY,OAAO,eAAe,SAAS;CAC7C;CAEA,OAAO,gCAAgC,WAAW,YAAY,MAAM,CAAC,CAAC,MACnE,eAAe,4BAA4B,WAAW,OAAO,CAChE;AACF;AAEA,SAAS,uBACP,YACoB;CAMpB,OAJE,WAAW,YAGX,YACgB;AACpB;AAEA,SAAS,gCAAgC,WAA6B;CACpE,MAAM,aAAuB,CAAC;CAC9B,MAAM,aAAa,UAAU,SAAS,GAAG,IACrC,UAAU,MAAM,UAAU,YAAY,GAAG,IAAI,CAAC,IAC9C;CAEJ,MAAM,gBAAgB,cAA4B;EAChD,IACE,aACA,cAAc,cACd,CAAC,WAAW,SAAS,SAAS,GAE9B,WAAW,KAAK,SAAS;CAE7B;CAEA,IAAI,WAAW,SAAS,YAAY,GAClC,aAAa,WAAW,MAAM,GAAG,GAAoB,CAAC;CAGxD,IAAI,WAAW,SAAS,KAAK,GAC3B,aAAa,GAAG,WAAW,MAAM,GAAG,EAAE,EAAE,EAAE;MACrC,IAAI,WAAW,SAAS,GAAG,GAChC,aAAa,WAAW,MAAM,GAAG,EAAE,CAAC;CAGtC,OAAO;AACT;AAEA,SAAS,6BACP,WACA,YACA,QACoB;CACpB,MAAM,aAAa,WAAW,QAAQ;CAEtC,KAAK,MAAM,aAAa,gCAAgC,UAAU,GAMhE,IAL8B,2BAC5B,WACA,YACA,MAEE,GACF,OAAO;AAKb;AAEA,SAAgB,+BACd,WACA,YACA,QACoB;CACpB,MAAM,sBAAsB,uBAAuB,UAAU;CAC7D,IAAI,qBACF,OAAO;CAGT,IAAI,WAAW,gBACb,OAAO,WAAW;CAGpB,MAAM,wBAAwB,6BAC5B,WACA,YACA,MACF;CACA,IAAI,uBACF,OAAO;CAGT,MAAM,QAAQ,gCACZ,WACA,YACA,MACF,CAAC,CAAC,QAAQ;CAEV,KAAK,MAAM,cAAc,OAAO;EAC9B,IAAI,eAAe,YACjB;EAEF,IAAI,WAAW,gBACb,OAAO,WAAW;CAEtB;AAGF"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"framework-base-classes.d.ts","sourceRoot":"","sources":["../../src/registry/framework-base-classes.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"framework-base-classes.d.ts","sourceRoot":"","sources":["../../src/registry/framework-base-classes.ts"],"names":[],"mappings":"AAsFA;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAClC,SAAS,EAAE,MAAM,GAAG,SAAS,EAC7B,WAAW,EAAE,MAAM,GAAG,SAAS,GAC9B,OAAO,CAMT"}
|
|
@@ -18,6 +18,9 @@
|
|
|
18
18
|
* This is the SINGLE shared source of truth for that identity check within
|
|
19
19
|
* `@happyvertical/smrt-core` and its consumers — imported by:
|
|
20
20
|
* - `./schema-builder.ts` (skips table/DDL generation for these classes)
|
|
21
|
+
* - `./table-mappings.ts`, `./class-registration.ts` and
|
|
22
|
+
* `../migrations/qualified-name-aliases.ts` (registry table, parent and
|
|
23
|
+
* alias bookkeeping skip these classes)
|
|
21
24
|
* - `../knowledge.ts` (the `.smrt/smrt-knowledge.json` surface projection)
|
|
22
25
|
* - `../generators/mcp.ts` (`MCPGenerator.generateTools()`)
|
|
23
26
|
* - `../generators/rest.ts` (`APIGenerator.findClassByCollectionSegment()`)
|
|
@@ -25,6 +28,8 @@
|
|
|
25
28
|
* - `../vite-plugin/sync-apply-route.ts` (`collectSyncApplyTargets()`)
|
|
26
29
|
* - `../vite-plugin/web-collections.ts` (the `smrt-virt-web` client's
|
|
27
30
|
* collection and WebMCP tool selectors)
|
|
31
|
+
* - `../vite-plugin/api-client-entries.ts` (`selectApiClientEntries()`, the
|
|
32
|
+
* generated `@smrt/client` runtime module and declarations, #3295)
|
|
28
33
|
* - `@happyvertical/smrt-cli`'s `cli-generator.ts` (the live local `smrt`
|
|
29
34
|
* binary's object command generation), via core's public export
|
|
30
35
|
* - `../testing/database.ts` (`getTestDatabase()` skips table creation for
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"framework-base-classes.js","names":[],"sources":["../../src/registry/framework-base-classes.ts"],"sourcesContent":["/**\n * The framework's own abstract base classes, keyed by simple class name to\n * their declaring package.\n *\n * These are scaffolding every application model descends from — they carry\n * no `@smrt()` decorator and have no independent existence as a resource.\n * A foundation package like `@happyvertical/smrt-core` declares them as real\n * local classes (there is no other package for them to live in), so\n * `loadAllManifests()` registers them exactly like any genuine domain class\n * when a consumer installs `@happyvertical/smrt-core` — with no decoration\n * or framework-base filter of its own (#2642). The registration criterion\n * this framework was designed around is descent from `SmrtObject`, not the\n * presence of a decorator — undecorated domain classes (e.g. a hand-written\n * `SmrtCollection` subclass) keep their surfaces; only the framework's own\n * bases are excluded here, checked by class identity rather than decoration.\n *\n * This is the SINGLE shared source of truth for that identity check within\n * `@happyvertical/smrt-core` and its consumers — imported by:\n * - `./schema-builder.ts` (skips table/DDL generation for these classes)\n * - `../knowledge.ts` (the `.smrt/smrt-knowledge.json` surface projection)\n * - `../generators/mcp.ts` (`MCPGenerator.generateTools()`)\n * - `../generators/rest.ts` (`APIGenerator.findClassByCollectionSegment()`)\n * - `../vite-plugin/sveltekit-generator.ts` (REST route generation)\n * - `../vite-plugin/sync-apply-route.ts` (`collectSyncApplyTargets()`)\n * - `../vite-plugin/web-collections.ts` (the `smrt-virt-web` client's\n * collection and WebMCP tool selectors)\n * - `@happyvertical/smrt-cli`'s `cli-generator.ts` (the live local `smrt`\n * binary's object command generation), via core's public export\n * - `../testing/database.ts` (`getTestDatabase()` skips table creation for\n * these classes, but only for an implicit `classes` list — an explicit\n * `classes: [...]` caller naming a framework base class is still honored,\n * unlike every unconditional filter above) (#2645)\n *\n * Do not add another hardcoded copy of this list at a new call site — import\n * from here instead.\n *\n * `packages/scanner/src/inheritance-resolver.ts` keeps its own separate\n * `FRAMEWORK_BASE_CLASSES` set: that package is a lower-level AST layer this\n * one has no reason to otherwise depend on, and that set controls a\n * different concern (scanner-level extends-chain termination and stub\n * resolution for packages that don't declare these classes locally), not\n * resource exposure.\n *\n * `scanner/manifest-generator.ts`'s `FRAMEWORK_METHOD_BASE_NAMES` is a\n * DIFFERENT, narrower (3-name: `SmrtObject`/`SmrtClass`/`SmrtCollection`) set\n * answering a different question — not \"is this class a resource?\" but\n * \"should this ancestor's METHODS merge into a subclass that doesn't declare\n * them?\" `SmrtReportCollection` and `SmrtJunction` are deliberately in this\n * resource set but NOT in that 3-name method-merge set: their declared\n * methods (`list`/`get` overrides, `attach`/`detach`/`byLeft`/`byRight`/\n * `setLinks`) are real, subclass-inherited API, not generic object-lifecycle\n * plumbing. Unifying the two sets — applying this one to method merging too —\n * strips those methods from every subclass; that regression actually\n * happened during #2624 and was caught only by a\n * `packages/template-sveltekit` snapshot test. Do not merge these lists.\n *\n * Most of these live in `@happyvertical/smrt-core` itself, but\n * `SmrtReport`/`SmrtReportCollection` are declared in\n * `@happyvertical/smrt-reports` — the owning package is per-name, not a\n * single blanket package check. A new framework base must be added here AND\n * to the scanner's `FRAMEWORK_BASE_CLASSES`.\n *\n * Deliberately NOT exported: only `isFrameworkBaseClass()` below needs to be\n * public. A `Map` survives TypeScript's `ReadonlyMap` typing only at the\n * type-check layer — a plain-JS or `as Map` caller could still `.set()`/\n * `.clear()` an exported instance at runtime and change what the framework\n * considers a base class. Keeping the map itself module-private closes that\n * off entirely rather than relying on callers not to do that.\n */\nconst FRAMEWORK_BASE_CLASS_PACKAGES: ReadonlyMap<string, string> = new Map([\n ['SmrtObject', '@happyvertical/smrt-core'],\n ['SmrtClass', '@happyvertical/smrt-core'],\n ['SmrtCollection', '@happyvertical/smrt-core'],\n ['SmrtJunction', '@happyvertical/smrt-core'],\n ['SmrtJunctionBase', '@happyvertical/smrt-core'],\n ['SmrtHierarchical', '@happyvertical/smrt-core'],\n ['SmrtPolymorphicAssociation', '@happyvertical/smrt-core'],\n ['SmrtReport', '@happyvertical/smrt-reports'],\n ['SmrtReportCollection', '@happyvertical/smrt-reports'],\n]);\n\n/**\n * True when `className`/`packageName` identify one of the framework's own\n * abstract base classes rather than a genuine resource.\n *\n * Both a class name AND its declaring package must match: a consuming\n * package is free to declare its own local class named e.g. `SmrtReport`\n * without colliding with the framework base of the same name.\n */\nexport function isFrameworkBaseClass(\n className: string | undefined,\n packageName: string | undefined,\n): boolean {\n return (\n className !== undefined &&\n packageName !== undefined &&\n FRAMEWORK_BASE_CLASS_PACKAGES.get(className) === packageName\n );\n}\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"framework-base-classes.js","names":[],"sources":["../../src/registry/framework-base-classes.ts"],"sourcesContent":["/**\n * The framework's own abstract base classes, keyed by simple class name to\n * their declaring package.\n *\n * These are scaffolding every application model descends from — they carry\n * no `@smrt()` decorator and have no independent existence as a resource.\n * A foundation package like `@happyvertical/smrt-core` declares them as real\n * local classes (there is no other package for them to live in), so\n * `loadAllManifests()` registers them exactly like any genuine domain class\n * when a consumer installs `@happyvertical/smrt-core` — with no decoration\n * or framework-base filter of its own (#2642). The registration criterion\n * this framework was designed around is descent from `SmrtObject`, not the\n * presence of a decorator — undecorated domain classes (e.g. a hand-written\n * `SmrtCollection` subclass) keep their surfaces; only the framework's own\n * bases are excluded here, checked by class identity rather than decoration.\n *\n * This is the SINGLE shared source of truth for that identity check within\n * `@happyvertical/smrt-core` and its consumers — imported by:\n * - `./schema-builder.ts` (skips table/DDL generation for these classes)\n * - `./table-mappings.ts`, `./class-registration.ts` and\n * `../migrations/qualified-name-aliases.ts` (registry table, parent and\n * alias bookkeeping skip these classes)\n * - `../knowledge.ts` (the `.smrt/smrt-knowledge.json` surface projection)\n * - `../generators/mcp.ts` (`MCPGenerator.generateTools()`)\n * - `../generators/rest.ts` (`APIGenerator.findClassByCollectionSegment()`)\n * - `../vite-plugin/sveltekit-generator.ts` (REST route generation)\n * - `../vite-plugin/sync-apply-route.ts` (`collectSyncApplyTargets()`)\n * - `../vite-plugin/web-collections.ts` (the `smrt-virt-web` client's\n * collection and WebMCP tool selectors)\n * - `../vite-plugin/api-client-entries.ts` (`selectApiClientEntries()`, the\n * generated `@smrt/client` runtime module and declarations, #3295)\n * - `@happyvertical/smrt-cli`'s `cli-generator.ts` (the live local `smrt`\n * binary's object command generation), via core's public export\n * - `../testing/database.ts` (`getTestDatabase()` skips table creation for\n * these classes, but only for an implicit `classes` list — an explicit\n * `classes: [...]` caller naming a framework base class is still honored,\n * unlike every unconditional filter above) (#2645)\n *\n * Do not add another hardcoded copy of this list at a new call site — import\n * from here instead.\n *\n * `packages/scanner/src/inheritance-resolver.ts` keeps its own separate\n * `FRAMEWORK_BASE_CLASSES` set: that package is a lower-level AST layer this\n * one has no reason to otherwise depend on, and that set controls a\n * different concern (scanner-level extends-chain termination and stub\n * resolution for packages that don't declare these classes locally), not\n * resource exposure.\n *\n * `scanner/manifest-generator.ts`'s `FRAMEWORK_METHOD_BASE_NAMES` is a\n * DIFFERENT, narrower (3-name: `SmrtObject`/`SmrtClass`/`SmrtCollection`) set\n * answering a different question — not \"is this class a resource?\" but\n * \"should this ancestor's METHODS merge into a subclass that doesn't declare\n * them?\" `SmrtReportCollection` and `SmrtJunction` are deliberately in this\n * resource set but NOT in that 3-name method-merge set: their declared\n * methods (`list`/`get` overrides, `attach`/`detach`/`byLeft`/`byRight`/\n * `setLinks`) are real, subclass-inherited API, not generic object-lifecycle\n * plumbing. Unifying the two sets — applying this one to method merging too —\n * strips those methods from every subclass; that regression actually\n * happened during #2624 and was caught only by a\n * `packages/template-sveltekit` snapshot test. Do not merge these lists.\n *\n * Most of these live in `@happyvertical/smrt-core` itself, but\n * `SmrtReport`/`SmrtReportCollection` are declared in\n * `@happyvertical/smrt-reports` — the owning package is per-name, not a\n * single blanket package check. A new framework base must be added here AND\n * to the scanner's `FRAMEWORK_BASE_CLASSES`.\n *\n * Deliberately NOT exported: only `isFrameworkBaseClass()` below needs to be\n * public. A `Map` survives TypeScript's `ReadonlyMap` typing only at the\n * type-check layer — a plain-JS or `as Map` caller could still `.set()`/\n * `.clear()` an exported instance at runtime and change what the framework\n * considers a base class. Keeping the map itself module-private closes that\n * off entirely rather than relying on callers not to do that.\n */\nconst FRAMEWORK_BASE_CLASS_PACKAGES: ReadonlyMap<string, string> = new Map([\n ['SmrtObject', '@happyvertical/smrt-core'],\n ['SmrtClass', '@happyvertical/smrt-core'],\n ['SmrtCollection', '@happyvertical/smrt-core'],\n ['SmrtJunction', '@happyvertical/smrt-core'],\n ['SmrtJunctionBase', '@happyvertical/smrt-core'],\n ['SmrtHierarchical', '@happyvertical/smrt-core'],\n ['SmrtPolymorphicAssociation', '@happyvertical/smrt-core'],\n ['SmrtReport', '@happyvertical/smrt-reports'],\n ['SmrtReportCollection', '@happyvertical/smrt-reports'],\n]);\n\n/**\n * True when `className`/`packageName` identify one of the framework's own\n * abstract base classes rather than a genuine resource.\n *\n * Both a class name AND its declaring package must match: a consuming\n * package is free to declare its own local class named e.g. `SmrtReport`\n * without colliding with the framework base of the same name.\n */\nexport function isFrameworkBaseClass(\n className: string | undefined,\n packageName: string | undefined,\n): boolean {\n return (\n className !== undefined &&\n packageName !== undefined &&\n FRAMEWORK_BASE_CLASS_PACKAGES.get(className) === packageName\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA0EA,IAAM,gDAA6D,IAAI,IAAI;CACzE,CAAC,cAAc,0BAA0B;CACzC,CAAC,aAAa,0BAA0B;CACxC,CAAC,kBAAkB,0BAA0B;CAC7C,CAAC,gBAAgB,0BAA0B;CAC3C,CAAC,oBAAoB,0BAA0B;CAC/C,CAAC,oBAAoB,0BAA0B;CAC/C,CAAC,8BAA8B,0BAA0B;CACzD,CAAC,cAAc,6BAA6B;CAC5C,CAAC,wBAAwB,6BAA6B;AACxD,CAAC;;;;;;;;;AAUD,SAAgB,qBACd,WACA,aACS;CACT,OACE,cAAc,KAAA,KACd,gBAAgB,KAAA,KAChB,8BAA8B,IAAI,SAAS,MAAM;AAErD"}
|
|
@@ -80,7 +80,14 @@ export declare function getAllSchemas(engine?: DatabaseEngine): Record<string, {
|
|
|
80
80
|
*
|
|
81
81
|
* @returns Map of tableName to SchemaDefinition
|
|
82
82
|
*/
|
|
83
|
-
export declare function getAllSchemasAsDefinitions(
|
|
83
|
+
export declare function getAllSchemasAsDefinitions(options?: {
|
|
84
|
+
/**
|
|
85
|
+
* Build only these tables and the tables their foreign keys reach. Table
|
|
86
|
+
* ownership is still enforced for every table built; collisions among
|
|
87
|
+
* tables outside that closure do not abort the build (#3956).
|
|
88
|
+
*/
|
|
89
|
+
tables?: Iterable<string>;
|
|
90
|
+
}): Record<string, SchemaDefinition>;
|
|
84
91
|
/**
|
|
85
92
|
* Generate DDL CREATE TABLE statement from columns
|
|
86
93
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"schema-builder.d.ts","sourceRoot":"","sources":["../../src/registry/schema-builder.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAO3D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAc7D,OAAO,KAAK,EACV,gBAAgB,EAEhB,gBAAgB,EAChB,WAAW,EACZ,MAAM,oBAAoB,CAAC;AA+L5B;;;;;;;;;;;GAWG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,gBAAgB,GAAG,SAAS,CAIpE;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,YAAY,CAC1B,IAAI,EAAE,MAAM,EACZ,MAAM,CAAC,EAAE,cAAc,GACtB,MAAM,GAAG,SAAS,CAWpB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAkB7D;
|
|
1
|
+
{"version":3,"file":"schema-builder.d.ts","sourceRoot":"","sources":["../../src/registry/schema-builder.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAO3D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAc7D,OAAO,KAAK,EACV,gBAAgB,EAEhB,gBAAgB,EAChB,WAAW,EACZ,MAAM,oBAAoB,CAAC;AA+L5B;;;;;;;;;;;GAWG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,gBAAgB,GAAG,SAAS,CAIpE;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,YAAY,CAC1B,IAAI,EAAE,MAAM,EACZ,MAAM,CAAC,EAAE,cAAc,GACtB,MAAM,GAAG,SAAS,CAWpB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAkB7D;AAiiBD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,aAAa,CAC3B,MAAM,CAAC,EAAE,cAAc,GACtB,MAAM,CAAC,MAAM,EAAE;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,EAAE,CAAA;CAAE,CAAC,CAuFxE;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,0BAA0B,CAAC,OAAO,CAAC,EAAE;IACnD;;;;OAIG;IACH,MAAM,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC;CAC3B,GAAG,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CA8CnC;AAED;;;;;;;;;GASG;AACH,wBAAgB,sBAAsB,CACpC,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,EACzC,KAAK,UAAQ,EACb,MAAM,CAAC,EAAE,cAAc,GACtB,MAAM,CA2DR;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAEvE;AAED;;GAEG;AACH,MAAM,WAAW,sBAAsB;IACrC;;;;;;;;OAQG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B;;;;OAIG;IACH,eAAe,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CACrC;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,eAAe,CAC7B,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,eAAe,CAAC,EACpC,OAAO,CAAC,EAAE,sBAAsB,GAC/B,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAiHlC;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAC/B,SAAS,EAAE,eAAe,CAAC,MAAM,CAAC,GACjC,WAAW,CAqBb"}
|
|
@@ -376,7 +376,7 @@ function sortTableContributors(contributors) {
|
|
|
376
376
|
* both produce the same merged shape. The result depends only on what is
|
|
377
377
|
* registered, never on the order it was registered in.
|
|
378
378
|
*/
|
|
379
|
-
function buildMergedTableSchemas() {
|
|
379
|
+
function buildMergedTableSchemas(scope) {
|
|
380
380
|
assertRuntimeTableBindings();
|
|
381
381
|
const contributorsByTable = /* @__PURE__ */ new Map();
|
|
382
382
|
for (const [className, registered] of getClasses()) {
|
|
@@ -389,7 +389,11 @@ function buildMergedTableSchemas() {
|
|
|
389
389
|
else contributorsByTable.set(resolved.tableName, [resolved.contributor]);
|
|
390
390
|
}
|
|
391
391
|
const tableSchemas = {};
|
|
392
|
-
|
|
392
|
+
const pending = scope ? [...scope].filter((tableName) => contributorsByTable.has(tableName)) : [...contributorsByTable.keys()];
|
|
393
|
+
const queued = new Set(pending);
|
|
394
|
+
for (let next = 0; next < pending.length; next++) {
|
|
395
|
+
const tableName = pending[next];
|
|
396
|
+
const contributors = contributorsByTable.get(tableName) ?? [];
|
|
393
397
|
assertSingleTableFamily(tableName, contributors);
|
|
394
398
|
const orderedContributors = sortTableContributors(contributors);
|
|
395
399
|
for (const contributor of orderedContributors) {
|
|
@@ -446,6 +450,15 @@ function buildMergedTableSchemas() {
|
|
|
446
450
|
if (existing >= 0) tableSchema.indexes[existing] = childIndex;
|
|
447
451
|
else tableSchema.indexes.push(childIndex);
|
|
448
452
|
}
|
|
453
|
+
if (scope) for (const foreignKey of schemaForeignKeys({
|
|
454
|
+
columns: tableSchema.columns,
|
|
455
|
+
foreignKeys: []
|
|
456
|
+
})) {
|
|
457
|
+
const dependency = foreignKey.referencesTable;
|
|
458
|
+
if (queued.has(dependency) || !contributorsByTable.has(dependency)) continue;
|
|
459
|
+
queued.add(dependency);
|
|
460
|
+
pending.push(dependency);
|
|
461
|
+
}
|
|
449
462
|
}
|
|
450
463
|
return tableSchemas;
|
|
451
464
|
}
|
|
@@ -513,8 +526,8 @@ function getAllSchemas(engine) {
|
|
|
513
526
|
*
|
|
514
527
|
* @returns Map of tableName to SchemaDefinition
|
|
515
528
|
*/
|
|
516
|
-
function getAllSchemasAsDefinitions() {
|
|
517
|
-
const tableSchemas = buildMergedTableSchemas();
|
|
529
|
+
function getAllSchemasAsDefinitions(options) {
|
|
530
|
+
const tableSchemas = buildMergedTableSchemas(options?.tables ? new Set(options.tables) : void 0);
|
|
518
531
|
const schemas = {};
|
|
519
532
|
for (const [tableName, tableSchema] of Object.entries(tableSchemas)) {
|
|
520
533
|
if (Object.keys(tableSchema.columns).length === 0) continue;
|