@opensaas/stack-core 0.26.0 → 0.27.1
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/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +54 -12
- package/dist/access/access-filter.d.ts +18 -1
- package/dist/access/access-filter.d.ts.map +1 -1
- package/dist/access/access-filter.js +62 -5
- package/dist/access/access-filter.js.map +1 -1
- package/dist/access/access-filter.test.d.ts +2 -0
- package/dist/access/access-filter.test.d.ts.map +1 -0
- package/dist/access/access-filter.test.js +116 -0
- package/dist/access/access-filter.test.js.map +1 -0
- package/dist/access/field-visibility.js.map +1 -1
- package/dist/access/index.d.ts +1 -1
- package/dist/access/index.d.ts.map +1 -1
- package/dist/access/index.js +1 -1
- package/dist/access/index.js.map +1 -1
- package/dist/access/multi-column-read-write.test.js.map +1 -1
- package/dist/config/label.d.ts +22 -0
- package/dist/config/label.d.ts.map +1 -0
- package/dist/config/label.js +48 -0
- package/dist/config/label.js.map +1 -0
- package/dist/config/plugin-engine.js.map +1 -1
- package/dist/config/types.d.ts +19 -5
- package/dist/config/types.d.ts.map +1 -1
- package/dist/context/index.d.ts +7 -0
- package/dist/context/index.d.ts.map +1 -1
- package/dist/context/index.js +42 -4
- package/dist/context/index.js.map +1 -1
- package/dist/context/nested-operations.js.map +1 -1
- package/dist/context/transaction-boundary.d.ts.map +1 -1
- package/dist/context/write-pipeline.d.ts.map +1 -1
- package/dist/context/write-pipeline.js.map +1 -1
- package/dist/fields/calendar-day.test.js +133 -7
- package/dist/fields/calendar-day.test.js.map +1 -1
- package/dist/fields/format-prisma-default.js.map +1 -1
- package/dist/fields/index.d.ts +7 -3
- package/dist/fields/index.d.ts.map +1 -1
- package/dist/fields/index.js +37 -8
- package/dist/fields/index.js.map +1 -1
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/index.js.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -1
- package/dist/query/index.d.ts.map +1 -1
- package/dist/query/relationship-options.d.ts +26 -0
- package/dist/query/relationship-options.d.ts.map +1 -0
- package/dist/query/relationship-options.js +43 -0
- package/dist/query/relationship-options.js.map +1 -0
- package/dist/query/relationship-options.test.d.ts +2 -0
- package/dist/query/relationship-options.test.d.ts.map +1 -0
- package/dist/query/relationship-options.test.js +121 -0
- package/dist/query/relationship-options.test.js.map +1 -0
- package/dist/utils/password.d.ts.map +1 -1
- package/dist/utils/password.js.map +1 -1
- package/dist/validation/schema.js.map +1 -1
- package/package.json +6 -5
- package/src/access/access-filter.test.ts +149 -0
- package/src/access/access-filter.ts +82 -9
- package/src/access/index.ts +5 -1
- package/src/config/label.ts +58 -0
- package/src/config/types.ts +20 -13
- package/src/context/index.ts +57 -5
- package/src/context/write-pipeline.ts +1 -2
- package/src/fields/calendar-day.test.ts +158 -7
- package/src/fields/index.ts +39 -11
- package/src/hooks/index.ts +1 -2
- package/src/index.ts +11 -0
- package/src/query/relationship-options.test.ts +157 -0
- package/src/query/relationship-options.ts +70 -0
- package/tests/context.test.ts +220 -0
- package/tests/label.test.ts +135 -0
- package/tsconfig.tsbuildinfo +1 -1
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,kEAAkE;AAClE,8CAA8C;AAC9C,EAAE;AACF,kEAAkE;AAClE,gEAAgE;AAChE,gEAAgE;AAChE,6DAA6D;AAC7D,yEAAyE;AACzE,kEAAkE;AAElE,kBAAkB;AAClB,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,mBAAmB,CAAA;AAuBhD,kBAAkB;AAClB,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAA;AAO/C,0EAA0E;AAC1E,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAA;AAE5E,gDAAgD;AAChD,OAAO,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAA;AAElD,uEAAuE;AACvE,4EAA4E;AAC5E,6EAA6E;AAC7E,oEAAoE;AACpE,OAAO,EAAE,mBAAmB,EAAE,oBAAoB,EAAE,MAAM,8BAA8B,CAAA;AAGxF,qEAAqE;AACrE,wEAAwE;AACxE,4EAA4E;AAC5E,6EAA6E;AAC7E,+EAA+E;AAC/E,gEAAgE;AAChE,OAAO,EAAE,cAAc,EAAE,QAAQ,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAA"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,kEAAkE;AAClE,8CAA8C;AAC9C,EAAE;AACF,kEAAkE;AAClE,gEAAgE;AAChE,gEAAgE;AAChE,6DAA6D;AAC7D,yEAAyE;AACzE,kEAAkE;AAElE,kBAAkB;AAClB,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,mBAAmB,CAAA;AAuBhD,kBAAkB;AAClB,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAA;AAO/C,0EAA0E;AAC1E,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAA;AAE5E,0EAA0E;AAC1E,yEAAyE;AACzE,2DAA2D;AAC3D,OAAO,EAAE,iBAAiB,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAEnE,gDAAgD;AAChD,OAAO,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAA;AAElD,uEAAuE;AACvE,4EAA4E;AAC5E,6EAA6E;AAC7E,oEAAoE;AACpE,OAAO,EAAE,mBAAmB,EAAE,oBAAoB,EAAE,MAAM,8BAA8B,CAAA;AAGxF,qEAAqE;AACrE,wEAAwE;AACxE,4EAA4E;AAC5E,6EAA6E;AAC7E,+EAA+E;AAC/E,gEAAgE;AAChE,OAAO,EAAE,cAAc,EAAE,QAAQ,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAA;AAGxE,qEAAqE;AACrE,6EAA6E;AAC7E,yEAAyE;AACzE,OAAO,EAAE,sBAAsB,EAAE,MAAM,iCAAiC,CAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/query/index.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/query/index.ts"],"names":[],"mappings":"AAMA;;;;;;;;GAQG;AACH,KAAK,UAAU,CAAC,CAAC,IAAI,WAAW,CAAC,CAAC,CAAC,SAAS,KAAK,CAAC,MAAM,CAAC,CAAC,GAAG,WAAW,CAAC,CAAC,CAAC,GAAG,WAAW,CAAC,CAAC,CAAC,CAAA;AAM5F;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AACH,MAAM,MAAM,gBAAgB,CAAC,QAAQ,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,IACjE,QAAQ,CAAC,QAAQ,EAAE,cAAc,CAAC,QAAQ,CAAC,CAAC,GAC5C;IACE,QAAQ,CAAC,KAAK,EAAE,QAAQ,CAAC,QAAQ,EAAE,cAAc,CAAC,QAAQ,CAAC,CAAC,CAAA;IAC5D,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IACxC,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,KAAK,GAAG,MAAM,CAAC,GAAG,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,KAAK,GAAG,MAAM,CAAC,CAAC,CAAA;IACzF,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CACvB,CAAA;AAEL;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,MAAM,cAAc,CAAC,CAAC,IAAI;IAC9B,QAAQ,EAAE,CAAC,IAAI,MAAM,CAAC,CAAC,CAAC,EAAE,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GACtE,gBAAgB,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,IAAI,GACzC,IAAI;CACT,CAAA;AAED;;;;;;;;;;;;;;;GAeG;AACH,MAAM,MAAM,QAAQ,CAAC,KAAK,EAAE,OAAO,SAAS,cAAc,CAAC,KAAK,CAAC,GAAG,cAAc,CAAC,KAAK,CAAC,IAAI;IAC3F,QAAQ,CAAC,KAAK,EAAE,UAAU,CAAA;IAC1B,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAA;CAC1B,CAAA;AAMD;;;;GAIG;AACH,KAAK,eAAe,CAAC,SAAS,IAC5B,SAAS,SAAS,QAAQ,CAAC,MAAM,KAAK,EAAE,MAAM,OAAO,CAAC,GAClD,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC,GACxB,SAAS,SAAS;IAAE,QAAQ,CAAC,KAAK,EAAE,QAAQ,CAAC,MAAM,KAAK,EAAE,MAAM,OAAO,CAAC,CAAA;CAAE,GACxE,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC,GACxB,KAAK,CAAA;AAEb;;;GAGG;AACH,KAAK,cAAc,CAAC,KAAK,EAAE,OAAO,SAAS,cAAc,CAAC,KAAK,CAAC,IAAI;KACjE,CAAC,IAAI,MAAM,OAAO,GAAG,MAAM,KAAK,GAAG,CAAC,eAAe,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,KAAK,CAAC,GAE7E,KAAK,CAAC,CAAC,CAAC,GAER,KAAK,CAAC,CAAC,CAAC,SAAS,KAAK,CAAC,OAAO,CAAC,GAC7B,QAAQ,CAAC,eAAe,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,GACvC,IAAI,SAAS,KAAK,CAAC,CAAC,CAAC,GACnB,QAAQ,CAAC,eAAe,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,IAAI,GAC5C,SAAS,SAAS,KAAK,CAAC,CAAC,CAAC,GACxB,QAAQ,CAAC,eAAe,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,SAAS,GACjD,QAAQ,CAAC,eAAe,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC;CAChD,CAAA;AAMD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,MAAM,QAAQ,CAAC,CAAC,IACpB,CAAC,SAAS,QAAQ,CAAC,MAAM,KAAK,EAAE,MAAM,OAAO,CAAC,GAAG,cAAc,CAAC,KAAK,EAAE,OAAO,CAAC,GAAG,KAAK,CAAA;AAEzF;;GAEG;AACH,MAAM,MAAM,SAAS,GAAG;IACtB,qFAAqF;IACrF,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAC/B,yFAAyF;IACzF,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,KAAK,GAAG,MAAM,CAAC,GAAG,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,KAAK,GAAG,MAAM,CAAC,CAAC,CAAA;IAChF,2CAA2C;IAC3C,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,kDAAkD;IAClD,IAAI,CAAC,EAAE,MAAM,CAAA;CACd,CAAA;AAED;;;GAGG;AACH,MAAM,WAAW,kBAAkB;IACjC,EAAE,EAAE;QACF,CAAC,GAAG,EAAE,MAAM,GAAG;YACb,QAAQ,EAAE,CAAC,IAAI,CAAC,EAAE,OAAO,KAAK,OAAO,CAAC,OAAO,EAAE,CAAC,CAAA;YAChD,SAAS,EAAE,CAAC,IAAI,CAAC,EAAE,OAAO,KAAK,OAAO,CAAC,OAAO,CAAC,CAAA;SAChD,CAAA;KACF,CAAA;CACF;AAMD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2DG;AACH,wBAAgB,cAAc,CAAC,KAAK,MACjB,OAAO,SAAS,cAAc,CAAC,KAAK,CAAC,UAC5C,OAAO,KACd,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC,CAG5B;AAMD,gBAAgB;AAChB,wBAAgB,UAAU,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,QAAQ,CAAC,OAAO,EAAE,cAAc,CAAC,OAAO,CAAC,CAAC,CAO9F;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,cAAc,CAAC,OAAO,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAwCjG;AAED;;;;;;;GAOG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,OAAO,SAAS,cAAc,CAAC,KAAK,CAAC,EACrE,IAAI,EAAE,KAAK,EACX,MAAM,EAAE,OAAO,GACd,cAAc,CAAC,KAAK,EAAE,OAAO,CAAC,CAiDhC;AAMD;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAsB,QAAQ,CAAC,KAAK,EAAE,OAAO,SAAS,cAAc,CAAC,KAAK,CAAC,EACzE,OAAO,EAAE,kBAAkB,EAC3B,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC,EAClC,IAAI,CAAC,EAAE,SAAS,GACf,OAAO,CAAC,cAAc,CAAC,KAAK,EAAE,OAAO,CAAC,EAAE,CAAC,CAmB3C;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAsB,WAAW,CAAC,KAAK,EAAE,OAAO,SAAS,cAAc,CAAC,KAAK,CAAC,EAC5E,OAAO,EAAE,kBAAkB,EAC3B,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC,EAClC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC7B,OAAO,CAAC,cAAc,CAAC,KAAK,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC,CAYhD"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { OpenSaasConfig } from '../config/types.js';
|
|
2
|
+
import { type QueryRunnerContext } from './index.js';
|
|
3
|
+
export interface RelationshipOption {
|
|
4
|
+
id: string;
|
|
5
|
+
label: string;
|
|
6
|
+
}
|
|
7
|
+
export interface RelationshipOptionsArgs {
|
|
8
|
+
/** Filters the label field via `contains` when it is a text field. */
|
|
9
|
+
search?: string;
|
|
10
|
+
/** Bounds the primary (search-scoped) window. @default 50 */
|
|
11
|
+
take?: number;
|
|
12
|
+
/** Always unioned into the result, even outside the search/take window. */
|
|
13
|
+
selectedIds?: string[];
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Bounded, projected fetch of `{ id, label }` options for a relationship
|
|
17
|
+
* editor — the read primitive behind the `relationshipOptions` serverAction
|
|
18
|
+
* op. Selects only `id` and the resolved label field (via
|
|
19
|
+
* {@link getLabelFieldName}), so the fragment carries no relation keys and
|
|
20
|
+
* `buildIncludeWithAccessControl`'s depth-5 auto-include never runs.
|
|
21
|
+
*
|
|
22
|
+
* Operation-level `query` access on `relatedListKey` still applies — a denied
|
|
23
|
+
* list resolves to `[]` (via the underlying access-controlled `findMany`).
|
|
24
|
+
*/
|
|
25
|
+
export declare function getRelationshipOptions(context: QueryRunnerContext, config: OpenSaasConfig, relatedListKey: string, args?: RelationshipOptionsArgs): Promise<RelationshipOption[]>;
|
|
26
|
+
//# sourceMappingURL=relationship-options.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"relationship-options.d.ts","sourceRoot":"","sources":["../../src/query/relationship-options.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAA;AAExD,OAAO,EAA4B,KAAK,kBAAkB,EAAE,MAAM,YAAY,CAAA;AAI9E,MAAM,WAAW,kBAAkB;IACjC,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,EAAE,MAAM,CAAA;CACd;AAED,MAAM,WAAW,uBAAuB;IACtC,sEAAsE;IACtE,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,6DAA6D;IAC7D,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,2EAA2E;IAC3E,WAAW,CAAC,EAAE,MAAM,EAAE,CAAA;CACvB;AAED;;;;;;;;;GASG;AACH,wBAAsB,sBAAsB,CAC1C,OAAO,EAAE,kBAAkB,EAC3B,MAAM,EAAE,cAAc,EACtB,cAAc,EAAE,MAAM,EACtB,IAAI,GAAE,uBAA4B,GACjC,OAAO,CAAC,kBAAkB,EAAE,CAAC,CAkC/B"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { getLabelFieldName, getItemLabel } from '../config/label.js';
|
|
2
|
+
import { defineFragment, runQuery } from './index.js';
|
|
3
|
+
const DEFAULT_TAKE = 50;
|
|
4
|
+
/**
|
|
5
|
+
* Bounded, projected fetch of `{ id, label }` options for a relationship
|
|
6
|
+
* editor — the read primitive behind the `relationshipOptions` serverAction
|
|
7
|
+
* op. Selects only `id` and the resolved label field (via
|
|
8
|
+
* {@link getLabelFieldName}), so the fragment carries no relation keys and
|
|
9
|
+
* `buildIncludeWithAccessControl`'s depth-5 auto-include never runs.
|
|
10
|
+
*
|
|
11
|
+
* Operation-level `query` access on `relatedListKey` still applies — a denied
|
|
12
|
+
* list resolves to `[]` (via the underlying access-controlled `findMany`).
|
|
13
|
+
*/
|
|
14
|
+
export async function getRelationshipOptions(context, config, relatedListKey, args = {}) {
|
|
15
|
+
const relatedListConfig = config.lists[relatedListKey];
|
|
16
|
+
if (!relatedListConfig)
|
|
17
|
+
return [];
|
|
18
|
+
const labelField = getLabelFieldName(relatedListConfig);
|
|
19
|
+
const fragment = defineFragment()({
|
|
20
|
+
id: true,
|
|
21
|
+
[labelField]: true,
|
|
22
|
+
});
|
|
23
|
+
const { search, take = DEFAULT_TAKE, selectedIds = [] } = args;
|
|
24
|
+
const labelFieldConfig = relatedListConfig.fields[labelField];
|
|
25
|
+
const where = search && labelFieldConfig?.type === 'text' ? { [labelField]: { contains: search } } : undefined;
|
|
26
|
+
const primary = await runQuery(context, relatedListKey, fragment, {
|
|
27
|
+
where,
|
|
28
|
+
orderBy: { [labelField]: 'asc' },
|
|
29
|
+
take,
|
|
30
|
+
});
|
|
31
|
+
const seenIds = new Set(primary.map((item) => item.id));
|
|
32
|
+
const missingSelectedIds = selectedIds.filter((id) => !seenIds.has(id));
|
|
33
|
+
const selected = missingSelectedIds.length
|
|
34
|
+
? await runQuery(context, relatedListKey, fragment, {
|
|
35
|
+
where: { id: { in: missingSelectedIds } },
|
|
36
|
+
})
|
|
37
|
+
: [];
|
|
38
|
+
return [...primary, ...selected].map((item) => ({
|
|
39
|
+
id: item.id,
|
|
40
|
+
label: getItemLabel(relatedListConfig, item),
|
|
41
|
+
}));
|
|
42
|
+
}
|
|
43
|
+
//# sourceMappingURL=relationship-options.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"relationship-options.js","sourceRoot":"","sources":["../../src/query/relationship-options.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,iBAAiB,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAA;AACpE,OAAO,EAAE,cAAc,EAAE,QAAQ,EAA2B,MAAM,YAAY,CAAA;AAE9E,MAAM,YAAY,GAAG,EAAE,CAAA;AAgBvB;;;;;;;;;GASG;AACH,MAAM,CAAC,KAAK,UAAU,sBAAsB,CAC1C,OAA2B,EAC3B,MAAsB,EACtB,cAAsB,EACtB,IAAI,GAA4B,EAAE;IAElC,MAAM,iBAAiB,GAAG,MAAM,CAAC,KAAK,CAAC,cAAc,CAAC,CAAA;IACtD,IAAI,CAAC,iBAAiB;QAAE,OAAO,EAAE,CAAA;IAEjC,MAAM,UAAU,GAAG,iBAAiB,CAAC,iBAAiB,CAAC,CAAA;IACvD,MAAM,QAAQ,GAAG,cAAc,EAA2B,CAAC;QACzD,EAAE,EAAE,IAAI;QACR,CAAC,UAAU,CAAC,EAAE,IAAI;KACnB,CAAC,CAAA;IAEF,MAAM,EAAE,MAAM,EAAE,IAAI,GAAG,YAAY,EAAE,WAAW,GAAG,EAAE,EAAE,GAAG,IAAI,CAAA;IAC9D,MAAM,gBAAgB,GAAG,iBAAiB,CAAC,MAAM,CAAC,UAAU,CAAkC,CAAA;IAC9F,MAAM,KAAK,GACT,MAAM,IAAI,gBAAgB,EAAE,IAAI,KAAK,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,UAAU,CAAC,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,CAAC,CAAC,CAAC,SAAS,CAAA;IAElG,MAAM,OAAO,GAAG,MAAM,QAAQ,CAAC,OAAO,EAAE,cAAc,EAAE,QAAQ,EAAE;QAChE,KAAK;QACL,OAAO,EAAE,EAAE,CAAC,UAAU,CAAC,EAAE,KAAK,EAAE;QAChC,IAAI;KACL,CAAC,CAAA;IAEF,MAAM,OAAO,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAE,IAAuB,CAAC,EAAE,CAAC,CAAC,CAAA;IAC3E,MAAM,kBAAkB,GAAG,WAAW,CAAC,MAAM,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAA;IAEvE,MAAM,QAAQ,GAAG,kBAAkB,CAAC,MAAM;QACxC,CAAC,CAAC,MAAM,QAAQ,CAAC,OAAO,EAAE,cAAc,EAAE,QAAQ,EAAE;YAChD,KAAK,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,kBAAkB,EAAE,EAAE;SAC1C,CAAC;QACJ,CAAC,CAAC,EAAE,CAAA;IAEN,OAAO,CAAC,GAAG,OAAO,EAAE,GAAG,QAAQ,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;QAC9C,EAAE,EAAG,IAAuB,CAAC,EAAE;QAC/B,KAAK,EAAE,YAAY,CAAC,iBAAiB,EAAE,IAA+B,CAAC;KACxE,CAAC,CAAC,CAAA;AACL,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"relationship-options.test.d.ts","sourceRoot":"","sources":["../../src/query/relationship-options.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import { getRelationshipOptions } from './relationship-options.js';
|
|
3
|
+
function makeDelegate(rows) {
|
|
4
|
+
return {
|
|
5
|
+
findMany: vi.fn(async (_args) => rows),
|
|
6
|
+
findFirst: vi.fn(async (_args) => rows[0] ?? null),
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
function makeContext(delegates) {
|
|
10
|
+
return { db: delegates };
|
|
11
|
+
}
|
|
12
|
+
const authors = [
|
|
13
|
+
{ id: 'a1', name: 'Ada Lovelace' },
|
|
14
|
+
{ id: 'a2', name: 'Alan Turing' },
|
|
15
|
+
{ id: 'a3', name: 'Grace Hopper' },
|
|
16
|
+
];
|
|
17
|
+
function makeConfig() {
|
|
18
|
+
return {
|
|
19
|
+
db: { provider: 'sqlite', url: 'file:./test.db' },
|
|
20
|
+
lists: {
|
|
21
|
+
Author: {
|
|
22
|
+
fields: {
|
|
23
|
+
name: { type: 'text' },
|
|
24
|
+
},
|
|
25
|
+
access: { operation: { query: () => true } },
|
|
26
|
+
},
|
|
27
|
+
NumericLabel: {
|
|
28
|
+
fields: {
|
|
29
|
+
rank: { type: 'integer' },
|
|
30
|
+
},
|
|
31
|
+
ui: { labelField: 'rank' },
|
|
32
|
+
access: { operation: { query: () => true } },
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
describe('getRelationshipOptions', () => {
|
|
38
|
+
it('returns { id, label }[] via a scalar-only fragment with no nested include', async () => {
|
|
39
|
+
const delegate = makeDelegate(authors);
|
|
40
|
+
const context = makeContext({ author: delegate });
|
|
41
|
+
const config = makeConfig();
|
|
42
|
+
const result = await getRelationshipOptions(context, config, 'Author', {});
|
|
43
|
+
expect(result).toEqual([
|
|
44
|
+
{ id: 'a1', label: 'Ada Lovelace' },
|
|
45
|
+
{ id: 'a2', label: 'Alan Turing' },
|
|
46
|
+
{ id: 'a3', label: 'Grace Hopper' },
|
|
47
|
+
]);
|
|
48
|
+
const call = delegate.findMany.mock.calls[0][0];
|
|
49
|
+
expect(call.include).toBeUndefined();
|
|
50
|
+
});
|
|
51
|
+
it('bounds the result by take', async () => {
|
|
52
|
+
const delegate = makeDelegate(authors.slice(0, 2));
|
|
53
|
+
const context = makeContext({ author: delegate });
|
|
54
|
+
const config = makeConfig();
|
|
55
|
+
await getRelationshipOptions(context, config, 'Author', { take: 2 });
|
|
56
|
+
expect(delegate.findMany).toHaveBeenCalledWith(expect.objectContaining({ take: 2 }));
|
|
57
|
+
});
|
|
58
|
+
it('orders by the label field ascending and filters via contains on a text label field', async () => {
|
|
59
|
+
const delegate = makeDelegate([authors[0]]);
|
|
60
|
+
const context = makeContext({ author: delegate });
|
|
61
|
+
const config = makeConfig();
|
|
62
|
+
await getRelationshipOptions(context, config, 'Author', { search: 'Ada' });
|
|
63
|
+
expect(delegate.findMany).toHaveBeenCalledWith(expect.objectContaining({
|
|
64
|
+
where: { name: { contains: 'Ada' } },
|
|
65
|
+
orderBy: { name: 'asc' },
|
|
66
|
+
}));
|
|
67
|
+
});
|
|
68
|
+
it('does not filter (first-N) when the label field is not a text field', async () => {
|
|
69
|
+
const rows = [{ id: 'n1', rank: 1 }];
|
|
70
|
+
const delegate = makeDelegate(rows);
|
|
71
|
+
const context = makeContext({ numericLabel: delegate });
|
|
72
|
+
const config = makeConfig();
|
|
73
|
+
await getRelationshipOptions(context, config, 'NumericLabel', { search: '1' });
|
|
74
|
+
const call = delegate.findMany.mock.calls[0][0];
|
|
75
|
+
expect(call.where).toBeUndefined();
|
|
76
|
+
expect(call.orderBy).toEqual({ rank: 'asc' });
|
|
77
|
+
});
|
|
78
|
+
it('unions currently-selected ids even when beyond take / not matching search', async () => {
|
|
79
|
+
// The bounded/search-scoped query only returns a1 (mimicking take:1 + search).
|
|
80
|
+
const primaryDelegate = makeDelegate([authors[0]]);
|
|
81
|
+
const context = makeContext({ author: primaryDelegate });
|
|
82
|
+
const config = makeConfig();
|
|
83
|
+
// The selected-ids query (a separate findMany call) resolves a3, which fell
|
|
84
|
+
// outside the primary window.
|
|
85
|
+
primaryDelegate.findMany.mockImplementationOnce(async () => [authors[0]]);
|
|
86
|
+
primaryDelegate.findMany.mockImplementationOnce(async () => [authors[2]]);
|
|
87
|
+
const result = await getRelationshipOptions(context, config, 'Author', {
|
|
88
|
+
take: 1,
|
|
89
|
+
selectedIds: ['a3'],
|
|
90
|
+
});
|
|
91
|
+
expect(result).toEqual([
|
|
92
|
+
{ id: 'a1', label: 'Ada Lovelace' },
|
|
93
|
+
{ id: 'a3', label: 'Grace Hopper' },
|
|
94
|
+
]);
|
|
95
|
+
const selectedCall = primaryDelegate.findMany.mock.calls[1][0];
|
|
96
|
+
expect(selectedCall.where).toEqual({ id: { in: ['a3'] } });
|
|
97
|
+
});
|
|
98
|
+
it('does not re-query when the selected id is already within the primary window', async () => {
|
|
99
|
+
const delegate = makeDelegate([authors[0]]);
|
|
100
|
+
const context = makeContext({ author: delegate });
|
|
101
|
+
const config = makeConfig();
|
|
102
|
+
await getRelationshipOptions(context, config, 'Author', { selectedIds: ['a1'] });
|
|
103
|
+
expect(delegate.findMany).toHaveBeenCalledTimes(1);
|
|
104
|
+
});
|
|
105
|
+
it('returns [] when the related list query access is denied (findMany returns [])', async () => {
|
|
106
|
+
const delegate = makeDelegate([]);
|
|
107
|
+
const context = makeContext({ author: delegate });
|
|
108
|
+
const config = makeConfig();
|
|
109
|
+
const result = await getRelationshipOptions(context, config, 'Author', {
|
|
110
|
+
selectedIds: ['a1'],
|
|
111
|
+
});
|
|
112
|
+
expect(result).toEqual([]);
|
|
113
|
+
});
|
|
114
|
+
it('returns [] when the related list does not exist in config', async () => {
|
|
115
|
+
const context = makeContext({});
|
|
116
|
+
const config = makeConfig();
|
|
117
|
+
const result = await getRelationshipOptions(context, config, 'Missing', {});
|
|
118
|
+
expect(result).toEqual([]);
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
//# sourceMappingURL=relationship-options.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"relationship-options.test.js","sourceRoot":"","sources":["../../src/query/relationship-options.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAA;AACjD,OAAO,EAAE,sBAAsB,EAAE,MAAM,2BAA2B,CAAA;AAIlE,SAAS,YAAY,CAAC,IAAoC;IACxD,OAAO;QACL,QAAQ,EAAE,EAAE,CAAC,EAAE,CAAC,KAAK,EAAE,KAAe,EAAE,EAAE,CAAC,IAAI,CAAC;QAChD,SAAS,EAAE,EAAE,CAAC,EAAE,CAAC,KAAK,EAAE,KAAe,EAAE,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC;KAC7D,CAAA;AACH,CAAC;AAED,SAAS,WAAW,CAClB,SAA0D;IAE1D,OAAO,EAAE,EAAE,EAAE,SAAS,EAAE,CAAA;AAC1B,CAAC;AAED,MAAM,OAAO,GAAG;IACd,EAAE,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,cAAc,EAAE;IAClC,EAAE,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,aAAa,EAAE;IACjC,EAAE,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,cAAc,EAAE;CACnC,CAAA;AAED,SAAS,UAAU;IACjB,OAAO;QACL,EAAE,EAAE,EAAE,QAAQ,EAAE,QAAQ,EAAE,GAAG,EAAE,gBAAgB,EAAE;QACjD,KAAK,EAAE;YACL,MAAM,EAAE;gBACN,MAAM,EAAE;oBACN,IAAI,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE;iBACvB;gBACD,MAAM,EAAE,EAAE,SAAS,EAAE,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC,IAAI,EAAE,EAAE;aAC7C;YACD,YAAY,EAAE;gBACZ,MAAM,EAAE;oBACN,IAAI,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE;iBAC1B;gBACD,EAAE,EAAE,EAAE,UAAU,EAAE,MAAM,EAAE;gBAC1B,MAAM,EAAE,EAAE,SAAS,EAAE,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC,IAAI,EAAE,EAAE;aAC7C;SACF;KAC2B,CAAA;AAChC,CAAC;AAED,QAAQ,CAAC,wBAAwB,EAAE,GAAG,EAAE;IACtC,EAAE,CAAC,2EAA2E,EAAE,KAAK,IAAI,EAAE;QACzF,MAAM,QAAQ,GAAG,YAAY,CAAC,OAAO,CAAC,CAAA;QACtC,MAAM,OAAO,GAAG,WAAW,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,CAAA;QACjD,MAAM,MAAM,GAAG,UAAU,EAAE,CAAA;QAE3B,MAAM,MAAM,GAAG,MAAM,sBAAsB,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,EAAE,CAAC,CAAA;QAE1E,MAAM,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC;YACrB,EAAE,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,cAAc,EAAE;YACnC,EAAE,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,aAAa,EAAE;YAClC,EAAE,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,cAAc,EAAE;SACpC,CAAC,CAAA;QAEF,MAAM,IAAI,GAAG,QAAQ,CAAC,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAA4B,CAAA;QAC1E,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,aAAa,EAAE,CAAA;IACtC,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,2BAA2B,EAAE,KAAK,IAAI,EAAE;QACzC,MAAM,QAAQ,GAAG,YAAY,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAA;QAClD,MAAM,OAAO,GAAG,WAAW,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,CAAA;QACjD,MAAM,MAAM,GAAG,UAAU,EAAE,CAAA;QAE3B,MAAM,sBAAsB,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC,CAAA;QAEpE,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC,oBAAoB,CAAC,MAAM,CAAC,gBAAgB,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC,CAAC,CAAA;IACtF,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,oFAAoF,EAAE,KAAK,IAAI,EAAE;QAClG,MAAM,QAAQ,GAAG,YAAY,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;QAC3C,MAAM,OAAO,GAAG,WAAW,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,CAAA;QACjD,MAAM,MAAM,GAAG,UAAU,EAAE,CAAA;QAE3B,MAAM,sBAAsB,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,CAAA;QAE1E,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC,oBAAoB,CAC5C,MAAM,CAAC,gBAAgB,CAAC;YACtB,KAAK,EAAE,EAAE,IAAI,EAAE,EAAE,QAAQ,EAAE,KAAK,EAAE,EAAE;YACpC,OAAO,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE;SACzB,CAAC,CACH,CAAA;IACH,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,oEAAoE,EAAE,KAAK,IAAI,EAAE;QAClF,MAAM,IAAI,GAAG,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC,CAAA;QACpC,MAAM,QAAQ,GAAG,YAAY,CAAC,IAAI,CAAC,CAAA;QACnC,MAAM,OAAO,GAAG,WAAW,CAAC,EAAE,YAAY,EAAE,QAAQ,EAAE,CAAC,CAAA;QACvD,MAAM,MAAM,GAAG,UAAU,EAAE,CAAA;QAE3B,MAAM,sBAAsB,CAAC,OAAO,EAAE,MAAM,EAAE,cAAc,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAA;QAE9E,MAAM,IAAI,GAAG,QAAQ,CAAC,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAA4B,CAAA;QAC1E,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,aAAa,EAAE,CAAA;QAClC,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,OAAO,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAA;IAC/C,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,2EAA2E,EAAE,KAAK,IAAI,EAAE;QACzF,+EAA+E;QAC/E,MAAM,eAAe,GAAG,YAAY,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;QAClD,MAAM,OAAO,GAAG,WAAW,CAAC,EAAE,MAAM,EAAE,eAAe,EAAE,CAAC,CAAA;QACxD,MAAM,MAAM,GAAG,UAAU,EAAE,CAAA;QAE3B,4EAA4E;QAC5E,8BAA8B;QAC9B,eAAe,CAAC,QAAQ,CAAC,sBAAsB,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;QACzE,eAAe,CAAC,QAAQ,CAAC,sBAAsB,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;QAEzE,MAAM,MAAM,GAAG,MAAM,sBAAsB,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE;YACrE,IAAI,EAAE,CAAC;YACP,WAAW,EAAE,CAAC,IAAI,CAAC;SACpB,CAAC,CAAA;QAEF,MAAM,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC;YACrB,EAAE,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,cAAc,EAAE;YACnC,EAAE,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,cAAc,EAAE;SACpC,CAAC,CAAA;QAEF,MAAM,YAAY,GAAG,eAAe,CAAC,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAA4B,CAAA;QACzF,MAAM,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC,OAAO,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,CAAC,IAAI,CAAC,EAAE,EAAE,CAAC,CAAA;IAC5D,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,6EAA6E,EAAE,KAAK,IAAI,EAAE;QAC3F,MAAM,QAAQ,GAAG,YAAY,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;QAC3C,MAAM,OAAO,GAAG,WAAW,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,CAAA;QACjD,MAAM,MAAM,GAAG,UAAU,EAAE,CAAA;QAE3B,MAAM,sBAAsB,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,EAAE,WAAW,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;QAEhF,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC,qBAAqB,CAAC,CAAC,CAAC,CAAA;IACpD,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,+EAA+E,EAAE,KAAK,IAAI,EAAE;QAC7F,MAAM,QAAQ,GAAG,YAAY,CAAC,EAAE,CAAC,CAAA;QACjC,MAAM,OAAO,GAAG,WAAW,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,CAAA;QACjD,MAAM,MAAM,GAAG,UAAU,EAAE,CAAA;QAE3B,MAAM,MAAM,GAAG,MAAM,sBAAsB,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE;YACrE,WAAW,EAAE,CAAC,IAAI,CAAC;SACpB,CAAC,CAAA;QAEF,MAAM,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC,CAAA;IAC5B,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,2DAA2D,EAAE,KAAK,IAAI,EAAE;QACzE,MAAM,OAAO,GAAG,WAAW,CAAC,EAAE,CAAC,CAAA;QAC/B,MAAM,MAAM,GAAG,UAAU,EAAE,CAAA;QAE3B,MAAM,MAAM,GAAG,MAAM,sBAAsB,CAAC,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,EAAE,CAAC,CAAA;QAE3E,MAAM,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC,CAAA;IAC5B,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"password.d.ts","sourceRoot":"","sources":["../../src/utils/password.ts"],"names":[],"mappings":"AASA;;;;;;;;;;;;GAYG;AACH,wBAAsB,YAAY,CAChC,aAAa,EAAE,MAAM,EACrB,UAAU,GAAE,MAA4B,GACvC,OAAO,CAAC,MAAM,CAAC,CAMjB;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,eAAe,CACnC,aAAa,EAAE,MAAM,EACrB,cAAc,EAAE,MAAM,GACrB,OAAO,CAAC,OAAO,CAAC,CAgBlB;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAMvD;AAED;;;;;;;;;;GAUG;AACH,qBAAa,cAAc;IACb,OAAO,CAAC,QAAQ,CAAC,IAAI;
|
|
1
|
+
{"version":3,"file":"password.d.ts","sourceRoot":"","sources":["../../src/utils/password.ts"],"names":[],"mappings":"AASA;;;;;;;;;;;;GAYG;AACH,wBAAsB,YAAY,CAChC,aAAa,EAAE,MAAM,EACrB,UAAU,GAAE,MAA4B,GACvC,OAAO,CAAC,MAAM,CAAC,CAMjB;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,eAAe,CACnC,aAAa,EAAE,MAAM,EACrB,cAAc,EAAE,MAAM,GACrB,OAAO,CAAC,OAAO,CAAC,CAgBlB;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAMvD;AAED;;;;;;;;;;GAUG;AACH,qBAAa,cAAc;IACb,OAAO,CAAC,QAAQ,CAAC,IAAI;IAAjC,YAA6B,IAAI,EAAE,MAAM,EAIxC;IAED;;;;;OAKG;IACG,OAAO,CAAC,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAErD;IAED;;;OAGG;IACH,QAAQ,IAAI,MAAM,CAEjB;IAED;;;OAGG;IACH,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAKzC;IAED;;;OAGG;IACH,MAAM,IAAI,MAAM,CAEf;IAED;;;OAGG;IACH,OAAO,IAAI,MAAM,CAEhB;CACF"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"password.js","sourceRoot":"","sources":["../../src/utils/password.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,MAAM,UAAU,CAAA;AAE7B;;;;GAIG;AACH,MAAM,mBAAmB,GAAG,EAAE,CAAA;AAE9B;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CAChC,aAAqB,EACrB,
|
|
1
|
+
{"version":3,"file":"password.js","sourceRoot":"","sources":["../../src/utils/password.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,MAAM,UAAU,CAAA;AAE7B;;;;GAIG;AACH,MAAM,mBAAmB,GAAG,EAAE,CAAA;AAE9B;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CAChC,aAAqB,EACrB,UAAU,GAAW,mBAAmB;IAExC,IAAI,OAAO,aAAa,KAAK,QAAQ,IAAI,aAAa,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACpE,MAAM,IAAI,KAAK,CAAC,qCAAqC,CAAC,CAAA;IACxD,CAAC;IAED,OAAO,MAAM,CAAC,IAAI,CAAC,aAAa,EAAE,UAAU,CAAC,CAAA;AAC/C,CAAC;AAED;;;;;;;;;;;;;;GAcG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CACnC,aAAqB,EACrB,cAAsB;IAEtB,IAAI,OAAO,aAAa,KAAK,QAAQ,IAAI,OAAO,cAAc,KAAK,QAAQ,EAAE,CAAC;QAC5E,OAAO,KAAK,CAAA;IACd,CAAC;IAED,IAAI,aAAa,CAAC,MAAM,KAAK,CAAC,IAAI,cAAc,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC9D,OAAO,KAAK,CAAA;IACd,CAAC;IAED,IAAI,CAAC;QACH,OAAO,MAAM,MAAM,CAAC,OAAO,CAAC,aAAa,EAAE,cAAc,CAAC,CAAA;IAC5D,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,4CAA4C;QAC5C,OAAO,CAAC,KAAK,CAAC,6BAA6B,EAAE,KAAK,CAAC,CAAA;QACnD,OAAO,KAAK,CAAA;IACd,CAAC;AACH,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,gBAAgB,CAAC,KAAa;IAC5C,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAA;IAE3C,uEAAuE;IACvE,uCAAuC;IACvC,OAAO,0BAA0B,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;AAC/C,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,OAAO,cAAc;IACI,IAAI;IAAjC,YAA6B,IAAY;oBAAZ,IAAI;QAC/B,IAAI,CAAC,IAAI,IAAI,OAAO,IAAI,KAAK,QAAQ,EAAE,CAAC;YACtC,MAAM,IAAI,KAAK,CAAC,iDAAiD,CAAC,CAAA;QACpE,CAAC;IACH,CAAC;IAED;;;;;OAKG;IACH,KAAK,CAAC,OAAO,CAAC,aAAqB;QACjC,OAAO,eAAe,CAAC,aAAa,EAAE,IAAI,CAAC,IAAI,CAAC,CAAA;IAClD,CAAC;IAED;;;OAGG;IACH,QAAQ;QACN,OAAO,IAAI,CAAC,IAAI,CAAA;IAClB,CAAC;IAED;;;OAGG;IACH,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC,IAAY;QAC/B,IAAI,IAAI,KAAK,QAAQ,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;YAC5C,OAAO,IAAI,CAAC,IAAI,CAAA;QAClB,CAAC;QACD,OAAO,IAAI,CAAC,IAAI,CAAA;IAClB,CAAC;IAED;;;OAGG;IACH,MAAM;QACJ,OAAO,IAAI,CAAC,IAAI,CAAA;IAClB,CAAC;IAED;;;OAGG;IACH,OAAO;QACL,OAAO,IAAI,CAAC,IAAI,CAAA;IAClB,CAAC;CACF"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"schema.js","sourceRoot":"","sources":["../../src/validation/schema.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAGvB;;GAEG;AACH,MAAM,UAAU,iBAAiB,CAC/B,YAAyC,EACzC,
|
|
1
|
+
{"version":3,"file":"schema.js","sourceRoot":"","sources":["../../src/validation/schema.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAGvB;;GAEG;AACH,MAAM,UAAU,iBAAiB,CAC/B,YAAyC,EACzC,SAAS,GAAwB,QAAQ;IAEzC,MAAM,KAAK,GAAiC,EAAE,CAAA;IAE9C,KAAK,MAAM,CAAC,SAAS,EAAE,WAAW,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,YAAY,CAAC,EAAE,CAAC;QACpE,wDAAwD;QACxD,+DAA+D;QAC/D,IACE,CAAC,IAAI,EAAE,WAAW,EAAE,WAAW,CAAC,CAAC,QAAQ,CAAC,SAAS,CAAC;YACpD,WAAW,CAAC,IAAI,KAAK,cAAc;YACnC,WAAW,CAAC,OAAO,EACnB,CAAC;YACD,SAAQ;QACV,CAAC;QAED,mCAAmC;QACnC,IAAI,WAAW,CAAC,YAAY,EAAE,CAAC;YAC7B,KAAK,CAAC,SAAS,CAAC,GAAG,WAAW,CAAC,YAAY,CAAC,SAAS,EAAE,SAAS,CAAC,CAAA;QACnE,CAAC;aAAM,CAAC;YACN,4DAA4D;YAC5D,KAAK,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE,CAAA;QACvC,CAAC;IACH,CAAC;IAED,OAAO,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;AACxB,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,eAAe,CAC7B,IAA6B,EAC7B,YAAyC,EACzC,SAAS,GAAwB,QAAQ;IAEzC,MAAM,MAAM,GAAG,iBAAiB,CAAC,YAAY,EAAE,SAAS,CAAC,CAAA;IAEzD,MAAM,MAAM,GAAG,MAAM,CAAC,SAAS,CAAC,IAAI,CAAC,CAAA;IAErC,IAAI,MAAM,CAAC,OAAO,EAAE,CAAC;QACnB,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,CAAA;IAC1B,CAAC;IAED,sDAAsD;IACtD,MAAM,MAAM,GAA2B,EAAE,CAAA;IACzC,KAAK,MAAM,KAAK,IAAI,MAAM,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC;QACxC,MAAM,SAAS,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;QACtC,MAAM,CAAC,SAAS,CAAC,GAAG,KAAK,CAAC,OAAO,CAAA;IACnC,CAAC;IAED,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,CAAA;AACnC,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opensaas/stack-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.27.1",
|
|
4
4
|
"description": "Core stack for OpenSaas - schema definition, access control, and runtime utilities",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -57,10 +57,11 @@
|
|
|
57
57
|
},
|
|
58
58
|
"devDependencies": {
|
|
59
59
|
"@prisma/client": "^7.8.0",
|
|
60
|
-
"@types/node": "^
|
|
61
|
-
"@
|
|
62
|
-
"
|
|
63
|
-
"
|
|
60
|
+
"@types/node": "^26.1.1",
|
|
61
|
+
"@typescript/native": "npm:typescript@^7.0.2",
|
|
62
|
+
"@vitest/coverage-v8": "^4.1.10",
|
|
63
|
+
"typescript": "npm:@typescript/typescript6@^6.0.2",
|
|
64
|
+
"vitest": "^4.1.10"
|
|
64
65
|
},
|
|
65
66
|
"scripts": {
|
|
66
67
|
"build": "tsc",
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { buildIncludeWithAccessControl, mergeIncludeWithAccessControl } from './access-filter.js'
|
|
3
|
+
import type { OpenSaasConfig, FieldConfig } from '../config/types.js'
|
|
4
|
+
import type { AccessContext } from './types.js'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Regression coverage for the cyclic readable-relationship auto-include.
|
|
8
|
+
*
|
|
9
|
+
* On a relationship graph that contains a cycle (A → B → C → A) the depth-first
|
|
10
|
+
* auto-include used to descend the cycle on every branch to `MAX_DEPTH`, and
|
|
11
|
+
* `mergeIncludeWithAccessControl` re-expanded any bare-`true` leaf back into that
|
|
12
|
+
* same auto-include. The resulting include tree was deep/large enough to
|
|
13
|
+
* stack-overflow downstream processing (the RSC serializer). The fix seeds a
|
|
14
|
+
* cycle guard with the root list and stops the walk at cycle back-edges, so a
|
|
15
|
+
* relation that closes a cycle comes back FLAT (own columns only).
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
// A relationship field pointing at another list.
|
|
19
|
+
function rel(ref: string, many = false): FieldConfig {
|
|
20
|
+
return { type: 'relationship', ref, many } as unknown as FieldConfig
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// A cyclic config: A → B → C → A (plus a scalar on each list).
|
|
24
|
+
function cyclicConfig(): OpenSaasConfig {
|
|
25
|
+
const allowQuery = () => true
|
|
26
|
+
return {
|
|
27
|
+
db: { provider: 'sqlite', url: 'file:./dev.db' },
|
|
28
|
+
lists: {
|
|
29
|
+
A: {
|
|
30
|
+
fields: { name: { type: 'text' } as FieldConfig, b: rel('B.a') },
|
|
31
|
+
access: { operation: { query: allowQuery } },
|
|
32
|
+
},
|
|
33
|
+
B: {
|
|
34
|
+
fields: { name: { type: 'text' } as FieldConfig, c: rel('C.b') },
|
|
35
|
+
access: { operation: { query: allowQuery } },
|
|
36
|
+
},
|
|
37
|
+
C: {
|
|
38
|
+
fields: { name: { type: 'text' } as FieldConfig, a: rel('A.c') },
|
|
39
|
+
access: { operation: { query: allowQuery } },
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- minimal config for unit test
|
|
43
|
+
} as any
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function makeContext(): AccessContext {
|
|
47
|
+
return {
|
|
48
|
+
session: null,
|
|
49
|
+
_isSudo: false,
|
|
50
|
+
_resolveOutputCounter: { depth: 0 },
|
|
51
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- minimal context for unit test
|
|
52
|
+
} as any
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Measure the maximum nesting depth of an include tree.
|
|
56
|
+
function includeDepth(include: unknown): number {
|
|
57
|
+
if (!include || typeof include !== 'object') return 0
|
|
58
|
+
let max = 0
|
|
59
|
+
for (const value of Object.values(include as Record<string, unknown>)) {
|
|
60
|
+
if (value && typeof value === 'object') {
|
|
61
|
+
const nested = (value as { include?: unknown }).include
|
|
62
|
+
if (nested) max = Math.max(max, 1 + includeDepth(nested))
|
|
63
|
+
else max = Math.max(max, 1)
|
|
64
|
+
} else {
|
|
65
|
+
max = Math.max(max, 1)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return max
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
describe('buildIncludeWithAccessControl — cyclic graph', () => {
|
|
72
|
+
it('stops re-descending a relationship cycle instead of walking to MAX_DEPTH', async () => {
|
|
73
|
+
const config = cyclicConfig()
|
|
74
|
+
const include = await buildIncludeWithAccessControl(
|
|
75
|
+
config.lists.A.fields,
|
|
76
|
+
{ session: null, context: makeContext() },
|
|
77
|
+
config,
|
|
78
|
+
0,
|
|
79
|
+
['A'],
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
// A → B → C, then C.a closes the cycle back to A → flat (no further nesting).
|
|
83
|
+
expect(include).toEqual({
|
|
84
|
+
b: { include: { c: { include: { a: true } } } },
|
|
85
|
+
})
|
|
86
|
+
// Three distinct lists → depth 3, not the old MAX_DEPTH=5 (which on a cycle
|
|
87
|
+
// could recurse A→B→C→A→B).
|
|
88
|
+
expect(includeDepth(include)).toBe(3)
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
it('flattens a self-referential relationship to a single level', async () => {
|
|
92
|
+
const allowQuery = () => true
|
|
93
|
+
const config = {
|
|
94
|
+
db: { provider: 'sqlite', url: 'file:./dev.db' },
|
|
95
|
+
lists: {
|
|
96
|
+
Category: {
|
|
97
|
+
fields: {
|
|
98
|
+
name: { type: 'text' } as FieldConfig,
|
|
99
|
+
parent: rel('Category.children'),
|
|
100
|
+
children: rel('Category.parent', true),
|
|
101
|
+
},
|
|
102
|
+
access: { operation: { query: allowQuery } },
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- minimal config for unit test
|
|
106
|
+
} as any as OpenSaasConfig
|
|
107
|
+
|
|
108
|
+
const include = await buildIncludeWithAccessControl(
|
|
109
|
+
config.lists.Category.fields,
|
|
110
|
+
{ session: null, context: makeContext() },
|
|
111
|
+
config,
|
|
112
|
+
0,
|
|
113
|
+
['Category'],
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
// Both self-references come back flat — no infinite parent/children descent.
|
|
117
|
+
expect(include).toEqual({ parent: true, children: true })
|
|
118
|
+
expect(includeDepth(include)).toBe(1)
|
|
119
|
+
})
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
describe('mergeIncludeWithAccessControl — bare-true leaf on a cyclic graph', () => {
|
|
123
|
+
it('does not re-expand a bare-true leaf beyond the cycle-bounded auto-include', async () => {
|
|
124
|
+
const config = cyclicConfig()
|
|
125
|
+
const accessControlledInclude = await buildIncludeWithAccessControl(
|
|
126
|
+
config.lists.A.fields,
|
|
127
|
+
{ session: null, context: makeContext() },
|
|
128
|
+
config,
|
|
129
|
+
0,
|
|
130
|
+
['A'],
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
// Caller asks for A → b with a bare-`true` leaf. The merge must keep the
|
|
134
|
+
// access-controlled (cycle-bounded) nested include rather than re-expanding
|
|
135
|
+
// the leaf into an unbounded auto-include.
|
|
136
|
+
const merged = mergeIncludeWithAccessControl(
|
|
137
|
+
{ b: true },
|
|
138
|
+
accessControlledInclude,
|
|
139
|
+
config.lists.A.fields,
|
|
140
|
+
config,
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
expect(merged).toEqual({
|
|
144
|
+
b: { include: { c: { include: { a: true } } } },
|
|
145
|
+
})
|
|
146
|
+
// Still bounded to the acyclic path length.
|
|
147
|
+
expect(includeDepth(merged)).toBeLessThanOrEqual(3)
|
|
148
|
+
})
|
|
149
|
+
})
|
|
@@ -30,6 +30,10 @@ export async function buildIncludeWithAccessControl(
|
|
|
30
30
|
},
|
|
31
31
|
config: OpenSaasConfig,
|
|
32
32
|
depth: number = 0,
|
|
33
|
+
// List names already on the path from the root to here. Used to detect
|
|
34
|
+
// relationship cycles (A → B → … → A) and stop the auto-include from
|
|
35
|
+
// re-descending them. Seed it with the root list name at the call site.
|
|
36
|
+
visitedLists: readonly string[] = [],
|
|
33
37
|
) {
|
|
34
38
|
const MAX_DEPTH = 5
|
|
35
39
|
if (depth >= MAX_DEPTH) {
|
|
@@ -75,16 +79,31 @@ export async function buildIncludeWithAccessControl(
|
|
|
75
79
|
includeEntry.where = accessResult
|
|
76
80
|
}
|
|
77
81
|
|
|
78
|
-
//
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
)
|
|
82
|
+
// Cycle guard: if the related list already appears on the path from the
|
|
83
|
+
// root, DO NOT auto-include its relationships again. On a cyclic
|
|
84
|
+
// readable-relationship graph (A → B → … → A) the depth-first walk would
|
|
85
|
+
// otherwise re-descend the cycle on every branch, and — combined with the
|
|
86
|
+
// bare-`true` leaf re-expansion in `mergeIncludeWithAccessControl` — build
|
|
87
|
+
// an include tree deep/large enough to overflow the call stack in
|
|
88
|
+
// downstream processing (the RSC serializer's recursive `Map.set`). This
|
|
89
|
+
// extends the SF-20 (#566) fix: a bare-`true` leaf must resolve to a
|
|
90
|
+
// genuine single-level fetch, not a re-expansion of the full auto-include.
|
|
91
|
+
// The relation itself is still included (as a FLAT fetch of its own
|
|
92
|
+
// columns); only its onward relationships are pruned at the back-edge.
|
|
93
|
+
const relatedListName = relatedConfig.listName
|
|
94
|
+
if (!visitedLists.includes(relatedListName)) {
|
|
95
|
+
// Recursively build nested includes
|
|
96
|
+
const nestedInclude = await buildIncludeWithAccessControl(
|
|
97
|
+
relatedConfig.listConfig.fields,
|
|
98
|
+
args,
|
|
99
|
+
config,
|
|
100
|
+
depth + 1,
|
|
101
|
+
[...visitedLists, relatedListName],
|
|
102
|
+
)
|
|
85
103
|
|
|
86
|
-
|
|
87
|
-
|
|
104
|
+
if (nestedInclude && Object.keys(nestedInclude).length > 0) {
|
|
105
|
+
includeEntry.include = nestedInclude
|
|
106
|
+
}
|
|
88
107
|
}
|
|
89
108
|
|
|
90
109
|
// Add to include object
|
|
@@ -251,3 +270,57 @@ export function mergeIncludeWithAccessControl(
|
|
|
251
270
|
|
|
252
271
|
return merged
|
|
253
272
|
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Remove keys that correspond to `virtual` fields from a Prisma `include`
|
|
276
|
+
* object, recursing into nested relationship includes using the related
|
|
277
|
+
* list's field configs.
|
|
278
|
+
*
|
|
279
|
+
* Virtual fields are computed in JavaScript (via `resolveOutput` in
|
|
280
|
+
* `field-visibility.ts`) and have no database column. Naming one in `include`
|
|
281
|
+
* type-checks — the generated `Include` type lists every field the config
|
|
282
|
+
* declares — but Prisma throws `Unknown field '<name>' for include statement`
|
|
283
|
+
* at runtime. This is applied as the final step on whatever `include` a read
|
|
284
|
+
* op ends up with (fragment-built, access-controlled merge, or sudo
|
|
285
|
+
* passthrough) so a virtual key never reaches the Prisma client, regardless
|
|
286
|
+
* of which path produced it. The virtual value is still computed
|
|
287
|
+
* unconditionally by `filterReadableFields`, so stripping the include key has
|
|
288
|
+
* no effect on whether the value appears in the result (#628).
|
|
289
|
+
*/
|
|
290
|
+
export function stripVirtualFieldsFromInclude(
|
|
291
|
+
include: Record<string, unknown> | undefined,
|
|
292
|
+
fieldConfigs: Record<string, FieldConfig>,
|
|
293
|
+
config: OpenSaasConfig,
|
|
294
|
+
): Record<string, unknown> | undefined {
|
|
295
|
+
if (!include) return include
|
|
296
|
+
|
|
297
|
+
const result: Record<string, unknown> = {}
|
|
298
|
+
|
|
299
|
+
for (const [key, value] of Object.entries(include)) {
|
|
300
|
+
const fieldConfig = fieldConfigs[key]
|
|
301
|
+
|
|
302
|
+
// Virtual fields have no database column — drop them from the include.
|
|
303
|
+
if (fieldConfig?.virtual) continue
|
|
304
|
+
|
|
305
|
+
const isDeclaredRelationship =
|
|
306
|
+
fieldConfig?.type === 'relationship' && 'ref' in fieldConfig && !!fieldConfig.ref
|
|
307
|
+
const entry = asEntryObject(value)
|
|
308
|
+
|
|
309
|
+
if (isDeclaredRelationship && entry?.include) {
|
|
310
|
+
const relatedConfig = getRelatedListConfig(fieldConfig.ref as string, config)
|
|
311
|
+
if (relatedConfig) {
|
|
312
|
+
const nestedInclude = stripVirtualFieldsFromInclude(
|
|
313
|
+
entry.include,
|
|
314
|
+
relatedConfig.listConfig.fields,
|
|
315
|
+
config,
|
|
316
|
+
)
|
|
317
|
+
result[key] = { ...(value as Record<string, unknown>), include: nestedInclude }
|
|
318
|
+
continue
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
result[key] = value
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
return result
|
|
326
|
+
}
|
package/src/access/index.ts
CHANGED
|
@@ -22,6 +22,10 @@ export {
|
|
|
22
22
|
// Canonical field-level access evaluation (shared by read and write paths).
|
|
23
23
|
export { checkFieldAccess, filterWritableFields } from './field-access.js'
|
|
24
24
|
// Phase 1 — Access Filter (pre-query row/relation scoping).
|
|
25
|
-
export {
|
|
25
|
+
export {
|
|
26
|
+
buildIncludeWithAccessControl,
|
|
27
|
+
mergeIncludeWithAccessControl,
|
|
28
|
+
stripVirtualFieldsFromInclude,
|
|
29
|
+
} from './access-filter.js'
|
|
26
30
|
// Phase 2 — Field Visibility (post-query field stripping + resolveOutput).
|
|
27
31
|
export { filterReadableFields } from './field-visibility.js'
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { ListConfig } from './types.js'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Resolve the field name that represents a list's rows as a single label —
|
|
5
|
+
* the projection half of the label seam. `getItemLabel` reads exactly the
|
|
6
|
+
* field this returns, so the field chosen for projection can never drift
|
|
7
|
+
* from the field used for rendering.
|
|
8
|
+
*
|
|
9
|
+
* Fallback order: configured `ui.labelField` → `name` → `title` → `id`
|
|
10
|
+
* (first field that exists on the list).
|
|
11
|
+
*
|
|
12
|
+
* @throws if `ui.labelField` is set but does not reference a declared,
|
|
13
|
+
* non-relationship field on the list.
|
|
14
|
+
*/
|
|
15
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- ListConfig must accept any TypeInfo
|
|
16
|
+
export function getLabelFieldName(listConfig: ListConfig<any>): string {
|
|
17
|
+
const configured = listConfig.ui?.labelField
|
|
18
|
+
|
|
19
|
+
if (configured !== undefined) {
|
|
20
|
+
const field = listConfig.fields[configured]
|
|
21
|
+
if (!field) {
|
|
22
|
+
throw new Error(
|
|
23
|
+
`ui.labelField "${configured}" does not reference a field declared on this list.`,
|
|
24
|
+
)
|
|
25
|
+
}
|
|
26
|
+
if (field.type === 'relationship') {
|
|
27
|
+
throw new Error(
|
|
28
|
+
`ui.labelField "${configured}" must reference a scalar field, not a relationship.`,
|
|
29
|
+
)
|
|
30
|
+
}
|
|
31
|
+
return configured
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if ('name' in listConfig.fields) return 'name'
|
|
35
|
+
if ('title' in listConfig.fields) return 'title'
|
|
36
|
+
return 'id'
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Resolve the display label for a single row — the render half of the label
|
|
41
|
+
* seam. Reads the field `getLabelFieldName` resolves, falling back to `id`
|
|
42
|
+
* when that field is missing from the row (e.g. stripped by field-level
|
|
43
|
+
* access).
|
|
44
|
+
*/
|
|
45
|
+
export function getItemLabel(
|
|
46
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- ListConfig must accept any TypeInfo
|
|
47
|
+
listConfig: ListConfig<any>,
|
|
48
|
+
item: Record<string, unknown>,
|
|
49
|
+
): string {
|
|
50
|
+
const fieldName = getLabelFieldName(listConfig)
|
|
51
|
+
const value = Object.prototype.hasOwnProperty.call(item, fieldName) ? item[fieldName] : undefined
|
|
52
|
+
|
|
53
|
+
if (value === undefined || value === null) {
|
|
54
|
+
return String(item.id)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return String(value)
|
|
58
|
+
}
|