@jskit-ai/crud 0.1.4

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.
Files changed (40) hide show
  1. package/package.descriptor.mjs +322 -0
  2. package/package.json +22 -0
  3. package/src/client/index.js +3 -0
  4. package/src/server/CrudServiceProvider.js +11 -0
  5. package/src/server/actionIds.js +22 -0
  6. package/src/server/actions.js +152 -0
  7. package/src/server/registerRoutes.js +235 -0
  8. package/src/server/repository.js +162 -0
  9. package/src/server/service.js +96 -0
  10. package/src/shared/crud/crudModuleConfig.js +310 -0
  11. package/src/shared/crud/crudResource.js +191 -0
  12. package/src/shared/index.js +12 -0
  13. package/templates/migrations/crud_initial.cjs +42 -0
  14. package/templates/src/elements/CreateElement.vue +115 -0
  15. package/templates/src/elements/EditElement.vue +140 -0
  16. package/templates/src/elements/ListElement.vue +88 -0
  17. package/templates/src/elements/ViewElement.vue +126 -0
  18. package/templates/src/elements/clientSupport.js +41 -0
  19. package/templates/src/local-package/client/index.js +4 -0
  20. package/templates/src/local-package/package.descriptor.mjs +83 -0
  21. package/templates/src/local-package/package.json +14 -0
  22. package/templates/src/local-package/server/CrudServiceProvider.js +87 -0
  23. package/templates/src/local-package/server/actionIds.js +9 -0
  24. package/templates/src/local-package/server/actions.js +151 -0
  25. package/templates/src/local-package/server/diTokens.js +4 -0
  26. package/templates/src/local-package/server/registerRoutes.js +196 -0
  27. package/templates/src/local-package/server/repository.js +1 -0
  28. package/templates/src/local-package/server/service.js +96 -0
  29. package/templates/src/local-package/shared/crudResource.js +1 -0
  30. package/templates/src/local-package/shared/index.js +8 -0
  31. package/templates/src/local-package/shared/moduleConfig.js +169 -0
  32. package/templates/src/pages/admin/crud/[recordId]/edit.vue +7 -0
  33. package/templates/src/pages/admin/crud/[recordId]/index.vue +7 -0
  34. package/templates/src/pages/admin/crud/index.vue +7 -0
  35. package/templates/src/pages/admin/crud/new.vue +7 -0
  36. package/test/crudModuleConfig.test.js +225 -0
  37. package/test/crudResource.test.js +41 -0
  38. package/test/crudServerGuards.test.js +61 -0
  39. package/test/crudService.test.js +83 -0
  40. package/test/routeInputContracts.test.js +211 -0
@@ -0,0 +1,151 @@
1
+ import {
2
+ cursorPaginationQueryValidator,
3
+ recordIdParamsValidator
4
+ } from "@jskit-ai/kernel/shared/validators";
5
+ import { workspaceSlugParamsValidator } from "@jskit-ai/users-core/server/validators/routeParamsValidator";
6
+ import { crudResource } from "../shared/${option:namespace|singular|camel}Resource.js";
7
+ import { actionIds } from "./actionIds.js";
8
+
9
+ function requireActionSurface(surface = "") {
10
+ const normalizedSurface = String(surface || "").trim().toLowerCase();
11
+ if (!normalizedSurface) {
12
+ throw new TypeError("createActions requires a non-empty surface.");
13
+ }
14
+
15
+ return normalizedSurface;
16
+ }
17
+
18
+ function createActions({ surface = "" } = {}) {
19
+ const actionSurface = requireActionSurface(surface);
20
+
21
+ return Object.freeze([
22
+ {
23
+ id: actionIds.list,
24
+ version: 1,
25
+ kind: "query",
26
+ channels: ["api", "automation", "internal"],
27
+ surfaces: [actionSurface],
28
+ permission: {
29
+ require: "authenticated"
30
+ },
31
+ inputValidator: [workspaceSlugParamsValidator, cursorPaginationQueryValidator],
32
+ outputValidator: crudResource.operations.list.outputValidator,
33
+ idempotency: "none",
34
+ audit: {
35
+ actionName: actionIds.list
36
+ },
37
+ observability: {},
38
+ async execute(input, context, deps) {
39
+ return deps.${option:namespace|camel}Service.listRecords(input, {
40
+ context,
41
+ visibilityContext: context?.visibilityContext
42
+ });
43
+ }
44
+ },
45
+ {
46
+ id: actionIds.view,
47
+ version: 1,
48
+ kind: "query",
49
+ channels: ["api", "automation", "internal"],
50
+ surfaces: [actionSurface],
51
+ permission: {
52
+ require: "authenticated"
53
+ },
54
+ inputValidator: [workspaceSlugParamsValidator, recordIdParamsValidator],
55
+ outputValidator: crudResource.operations.view.outputValidator,
56
+ idempotency: "none",
57
+ audit: {
58
+ actionName: actionIds.view
59
+ },
60
+ observability: {},
61
+ async execute(input, context, deps) {
62
+ return deps.${option:namespace|camel}Service.getRecord(input.recordId, {
63
+ context,
64
+ visibilityContext: context?.visibilityContext
65
+ });
66
+ }
67
+ },
68
+ {
69
+ id: actionIds.create,
70
+ version: 1,
71
+ kind: "command",
72
+ channels: ["api", "automation", "internal"],
73
+ surfaces: [actionSurface],
74
+ permission: {
75
+ require: "authenticated"
76
+ },
77
+ inputValidator: [
78
+ workspaceSlugParamsValidator,
79
+ {
80
+ payload: crudResource.operations.create.bodyValidator
81
+ }
82
+ ],
83
+ outputValidator: crudResource.operations.create.outputValidator,
84
+ idempotency: "optional",
85
+ audit: {
86
+ actionName: actionIds.create
87
+ },
88
+ observability: {},
89
+ async execute(input, context, deps) {
90
+ return deps.${option:namespace|camel}Service.createRecord(input.payload, {
91
+ context,
92
+ visibilityContext: context?.visibilityContext
93
+ });
94
+ }
95
+ },
96
+ {
97
+ id: actionIds.update,
98
+ version: 1,
99
+ kind: "command",
100
+ channels: ["api", "automation", "internal"],
101
+ surfaces: [actionSurface],
102
+ permission: {
103
+ require: "authenticated"
104
+ },
105
+ inputValidator: [
106
+ workspaceSlugParamsValidator,
107
+ recordIdParamsValidator,
108
+ {
109
+ patch: crudResource.operations.patch.bodyValidator
110
+ }
111
+ ],
112
+ outputValidator: crudResource.operations.patch.outputValidator,
113
+ idempotency: "optional",
114
+ audit: {
115
+ actionName: actionIds.update
116
+ },
117
+ observability: {},
118
+ async execute(input, context, deps) {
119
+ return deps.${option:namespace|camel}Service.updateRecord(input.recordId, input.patch, {
120
+ context,
121
+ visibilityContext: context?.visibilityContext
122
+ });
123
+ }
124
+ },
125
+ {
126
+ id: actionIds.delete,
127
+ version: 1,
128
+ kind: "command",
129
+ channels: ["api", "automation", "internal"],
130
+ surfaces: [actionSurface],
131
+ permission: {
132
+ require: "authenticated"
133
+ },
134
+ inputValidator: [workspaceSlugParamsValidator, recordIdParamsValidator],
135
+ outputValidator: crudResource.operations.delete.outputValidator,
136
+ idempotency: "optional",
137
+ audit: {
138
+ actionName: actionIds.delete
139
+ },
140
+ observability: {},
141
+ async execute(input, context, deps) {
142
+ return deps.${option:namespace|camel}Service.deleteRecord(input.recordId, {
143
+ context,
144
+ visibilityContext: context?.visibilityContext
145
+ });
146
+ }
147
+ }
148
+ ]);
149
+ }
150
+
151
+ export { createActions };
@@ -0,0 +1,4 @@
1
+ const NAMESPACE_${option:namespace|snake|upper}_SERVICE_TOKEN = "crud.${option:namespace|snake}";
2
+ const NAMESPACE_${option:namespace|snake|upper}_REPOSITORY_TOKEN = "repository.${option:namespace|snake}";
3
+
4
+ export { NAMESPACE_${option:namespace|snake|upper}_SERVICE_TOKEN, NAMESPACE_${option:namespace|snake|upper}_REPOSITORY_TOKEN };
@@ -0,0 +1,196 @@
1
+ import { withStandardErrorResponses } from "@jskit-ai/http-runtime/shared/validators/errorResponses";
2
+ import { normalizeSurfaceId } from "@jskit-ai/kernel/shared/surface/registry";
3
+ import { KERNEL_TOKENS } from "@jskit-ai/kernel/shared/support/tokens";
4
+ import {
5
+ cursorPaginationQueryValidator,
6
+ recordIdParamsValidator
7
+ } from "@jskit-ai/kernel/shared/validators";
8
+ import { routeParamsValidator } from "@jskit-ai/users-core/server/validators/routeParamsValidator";
9
+ import { normalizeScopedRouteVisibility } from "@jskit-ai/users-core/shared/support/usersVisibility";
10
+ import { buildWorkspaceInputFromRouteParams } from "@jskit-ai/users-core/server/support/workspaceRouteInput";
11
+ import { resolveApiBasePath } from "@jskit-ai/users-core/shared/support/usersApiPaths";
12
+ import { actionIds } from "./actionIds.js";
13
+ import { crudResource } from "../shared/${option:namespace|singular|camel}Resource.js";
14
+ import { crudModuleConfig } from "../shared/moduleConfig.js";
15
+
16
+ function registerRoutes(
17
+ app,
18
+ {
19
+ routeOwnershipFilter = "public",
20
+ routeSurface = "",
21
+ routeSurfaceRequiresWorkspace = false,
22
+ routeRelativePath = crudModuleConfig.relativePath
23
+ } = {}
24
+ ) {
25
+ if (!app || typeof app.make !== "function") {
26
+ throw new Error("registerRoutes requires application make().");
27
+ }
28
+
29
+ const router = app.make(KERNEL_TOKENS.HttpRouter);
30
+ const routeVisibility = normalizeScopedRouteVisibility(routeOwnershipFilter, {
31
+ fallback: "public"
32
+ });
33
+ const normalizedRouteSurface = normalizeSurfaceId(routeSurface);
34
+ const routeBase = resolveApiBasePath({
35
+ surfaceRequiresWorkspace: routeSurfaceRequiresWorkspace === true,
36
+ relativePath: routeRelativePath
37
+ });
38
+
39
+ router.register(
40
+ "GET",
41
+ routeBase,
42
+ {
43
+ auth: "required",
44
+ surface: normalizedRouteSurface,
45
+ visibility: routeVisibility,
46
+ meta: {
47
+ tags: ["crud"],
48
+ summary: "List records."
49
+ },
50
+ paramsValidator: routeParamsValidator,
51
+ queryValidator: cursorPaginationQueryValidator,
52
+ responseValidators: withStandardErrorResponses({
53
+ 200: crudResource.operations.list.outputValidator
54
+ })
55
+ },
56
+ async function (request, reply) {
57
+ const listInput = {
58
+ ...buildWorkspaceInputFromRouteParams(request.input.params)
59
+ };
60
+ if (request.input.query.cursor != null) {
61
+ listInput.cursor = request.input.query.cursor;
62
+ }
63
+ if (request.input.query.limit != null) {
64
+ listInput.limit = request.input.query.limit;
65
+ }
66
+ const response = await request.executeAction({
67
+ actionId: actionIds.list,
68
+ input: listInput
69
+ });
70
+ reply.code(200).send(response);
71
+ }
72
+ );
73
+
74
+ router.register(
75
+ "GET",
76
+ `${routeBase}/:recordId`,
77
+ {
78
+ auth: "required",
79
+ surface: normalizedRouteSurface,
80
+ visibility: routeVisibility,
81
+ meta: {
82
+ tags: ["crud"],
83
+ summary: "View a record."
84
+ },
85
+ paramsValidator: [routeParamsValidator, recordIdParamsValidator],
86
+ responseValidators: withStandardErrorResponses({
87
+ 200: crudResource.operations.view.outputValidator
88
+ })
89
+ },
90
+ async function (request, reply) {
91
+ const response = await request.executeAction({
92
+ actionId: actionIds.view,
93
+ input: {
94
+ ...buildWorkspaceInputFromRouteParams(request.input.params),
95
+ recordId: request.input.params.recordId
96
+ }
97
+ });
98
+ reply.code(200).send(response);
99
+ }
100
+ );
101
+
102
+ router.register(
103
+ "POST",
104
+ routeBase,
105
+ {
106
+ auth: "required",
107
+ surface: normalizedRouteSurface,
108
+ visibility: routeVisibility,
109
+ meta: {
110
+ tags: ["crud"],
111
+ summary: "Create a record."
112
+ },
113
+ paramsValidator: routeParamsValidator,
114
+ bodyValidator: crudResource.operations.create.bodyValidator,
115
+ responseValidators: withStandardErrorResponses(
116
+ {
117
+ 201: crudResource.operations.create.outputValidator
118
+ },
119
+ { includeValidation400: true }
120
+ )
121
+ },
122
+ async function (request, reply) {
123
+ const response = await request.executeAction({
124
+ actionId: actionIds.create,
125
+ input: {
126
+ ...buildWorkspaceInputFromRouteParams(request.input.params),
127
+ payload: request.input.body
128
+ }
129
+ });
130
+ reply.code(201).send(response);
131
+ }
132
+ );
133
+
134
+ router.register(
135
+ "PATCH",
136
+ `${routeBase}/:recordId`,
137
+ {
138
+ auth: "required",
139
+ surface: normalizedRouteSurface,
140
+ visibility: routeVisibility,
141
+ meta: {
142
+ tags: ["crud"],
143
+ summary: "Update a record."
144
+ },
145
+ paramsValidator: [routeParamsValidator, recordIdParamsValidator],
146
+ bodyValidator: crudResource.operations.patch.bodyValidator,
147
+ responseValidators: withStandardErrorResponses(
148
+ {
149
+ 200: crudResource.operations.patch.outputValidator
150
+ },
151
+ { includeValidation400: true }
152
+ )
153
+ },
154
+ async function (request, reply) {
155
+ const response = await request.executeAction({
156
+ actionId: actionIds.update,
157
+ input: {
158
+ ...buildWorkspaceInputFromRouteParams(request.input.params),
159
+ recordId: request.input.params.recordId,
160
+ patch: request.input.body
161
+ }
162
+ });
163
+ reply.code(200).send(response);
164
+ }
165
+ );
166
+
167
+ router.register(
168
+ "DELETE",
169
+ `${routeBase}/:recordId`,
170
+ {
171
+ auth: "required",
172
+ surface: normalizedRouteSurface,
173
+ visibility: routeVisibility,
174
+ meta: {
175
+ tags: ["crud"],
176
+ summary: "Delete a record."
177
+ },
178
+ paramsValidator: [routeParamsValidator, recordIdParamsValidator],
179
+ responseValidators: withStandardErrorResponses({
180
+ 200: crudResource.operations.delete.outputValidator
181
+ })
182
+ },
183
+ async function (request, reply) {
184
+ const response = await request.executeAction({
185
+ actionId: actionIds.delete,
186
+ input: {
187
+ ...buildWorkspaceInputFromRouteParams(request.input.params),
188
+ recordId: request.input.params.recordId
189
+ }
190
+ });
191
+ reply.code(200).send(response);
192
+ }
193
+ );
194
+ }
195
+
196
+ export { registerRoutes };
@@ -0,0 +1 @@
1
+ export { createRepository } from "../../../../src/server/repository.js";
@@ -0,0 +1,96 @@
1
+ import { AppError } from "@jskit-ai/kernel/server/runtime/errors";
2
+
3
+ const serviceEvents = Object.freeze({
4
+ createRecord: Object.freeze([
5
+ Object.freeze({
6
+ type: "entity.changed",
7
+ source: "crud",
8
+ entity: "record",
9
+ operation: "created",
10
+ entityId: ({ result }) => result?.id,
11
+ realtime: Object.freeze({
12
+ event: "${option:namespace|snake}.record.changed",
13
+ audience: "event_scope"
14
+ })
15
+ })
16
+ ]),
17
+ updateRecord: Object.freeze([
18
+ Object.freeze({
19
+ type: "entity.changed",
20
+ source: "crud",
21
+ entity: "record",
22
+ operation: "updated",
23
+ entityId: ({ result }) => result?.id,
24
+ realtime: Object.freeze({
25
+ event: "${option:namespace|snake}.record.changed",
26
+ audience: "event_scope"
27
+ })
28
+ })
29
+ ]),
30
+ deleteRecord: Object.freeze([
31
+ Object.freeze({
32
+ type: "entity.changed",
33
+ source: "crud",
34
+ entity: "record",
35
+ operation: "deleted",
36
+ entityId: ({ result }) => result?.id,
37
+ realtime: Object.freeze({
38
+ event: "${option:namespace|snake}.record.changed",
39
+ audience: "event_scope"
40
+ })
41
+ })
42
+ ])
43
+ });
44
+
45
+ function createService({ ${option:namespace|camel}Repository } = {}) {
46
+ if (!${option:namespace|camel}Repository) {
47
+ throw new Error("${option:namespace|camel}Service requires ${option:namespace|camel}Repository.");
48
+ }
49
+
50
+ async function listRecords(query = {}, options = {}) {
51
+ return ${option:namespace|camel}Repository.list(query, options);
52
+ }
53
+
54
+ async function getRecord(recordId, options = {}) {
55
+ const record = await ${option:namespace|camel}Repository.findById(recordId, options);
56
+ if (!record) {
57
+ throw new AppError(404, "Record not found.");
58
+ }
59
+
60
+ return record;
61
+ }
62
+
63
+ async function createRecord(payload = {}, options = {}) {
64
+ const record = await ${option:namespace|camel}Repository.create(payload, options);
65
+ if (!record) {
66
+ throw new Error("${option:namespace|camel}Service could not load the created record.");
67
+ }
68
+ return record;
69
+ }
70
+
71
+ async function updateRecord(recordId, payload = {}, options = {}) {
72
+ const record = await ${option:namespace|camel}Repository.updateById(recordId, payload, options);
73
+ if (!record) {
74
+ throw new AppError(404, "Record not found.");
75
+ }
76
+ return record;
77
+ }
78
+
79
+ async function deleteRecord(recordId, options = {}) {
80
+ const deleted = await ${option:namespace|camel}Repository.deleteById(recordId, options);
81
+ if (!deleted) {
82
+ throw new AppError(404, "Record not found.");
83
+ }
84
+ return deleted;
85
+ }
86
+
87
+ return Object.freeze({
88
+ listRecords,
89
+ getRecord,
90
+ createRecord,
91
+ updateRecord,
92
+ deleteRecord
93
+ });
94
+ }
95
+
96
+ export { createService, serviceEvents };
@@ -0,0 +1 @@
1
+ export { crudResource } from "../../../../src/shared/crud/crudResource.js";
@@ -0,0 +1,8 @@
1
+ export { crudResource } from "./${option:namespace|singular|camel}Resource.js";
2
+ export {
3
+ CRUD_MODULE_OWNERSHIP_FILTER_AUTO,
4
+ crudModuleConfig,
5
+ resolveCrudModulePolicy,
6
+ resolveCrudModulePolicyFromAppConfig,
7
+ resolveCrudModulePolicyFromPlacementContext
8
+ } from "./moduleConfig.js";
@@ -0,0 +1,169 @@
1
+ import { normalizeSurfaceId } from "@jskit-ai/kernel/shared/surface/registry";
2
+ import { normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
3
+ import {
4
+ USERS_ROUTE_VISIBILITY_LEVELS,
5
+ normalizeScopedRouteVisibility,
6
+ isWorkspaceVisibility
7
+ } from "@jskit-ai/users-core/shared/support/usersVisibility";
8
+
9
+ const CRUD_MODULE_OWNERSHIP_FILTER_AUTO = "auto";
10
+ const CRUD_MODULE_OWNERSHIP_FILTER_SET = new Set([
11
+ ...USERS_ROUTE_VISIBILITY_LEVELS,
12
+ CRUD_MODULE_OWNERSHIP_FILTER_AUTO
13
+ ]);
14
+
15
+ const crudModuleConfig = Object.freeze({
16
+ namespace: "${option:namespace|snake}",
17
+ surface: "${option:surface|lower}",
18
+ ownershipFilter: "${option:ownership-filter}",
19
+ relativePath: "/${option:directory-prefix|pathprefix}${option:namespace|kebab}"
20
+ });
21
+
22
+ function asRecord(value) {
23
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
24
+ return {};
25
+ }
26
+
27
+ return value;
28
+ }
29
+
30
+ function normalizeCrudOwnershipFilter(value, { fallback = CRUD_MODULE_OWNERSHIP_FILTER_AUTO } = {}) {
31
+ const normalized = normalizeText(value).toLowerCase();
32
+ if (CRUD_MODULE_OWNERSHIP_FILTER_SET.has(normalized)) {
33
+ return normalized;
34
+ }
35
+
36
+ const normalizedFallback = normalizeText(fallback).toLowerCase();
37
+ if (CRUD_MODULE_OWNERSHIP_FILTER_SET.has(normalizedFallback)) {
38
+ return normalizedFallback;
39
+ }
40
+
41
+ return CRUD_MODULE_OWNERSHIP_FILTER_AUTO;
42
+ }
43
+
44
+ function normalizeCrudRelativePath(value, { context = "resolveCrudModulePolicy" } = {}) {
45
+ const normalized = normalizeText(value);
46
+ if (!normalized) {
47
+ throw new TypeError(`${context} requires a non-empty relativePath.`);
48
+ }
49
+
50
+ const withLeadingSlash = normalized.startsWith("/") ? normalized : `/${normalized}`;
51
+ const compacted = withLeadingSlash.replace(/\/{2,}/g, "/");
52
+ return compacted === "/" ? "/" : compacted.replace(/\/+$/, "") || "/";
53
+ }
54
+
55
+ function normalizeCrudSurfaceDefinitions(sourceDefinitions = {}) {
56
+ const definitions = asRecord(sourceDefinitions);
57
+ const normalized = {};
58
+
59
+ for (const [key, value] of Object.entries(definitions)) {
60
+ const definition = asRecord(value);
61
+ const surfaceId = normalizeSurfaceId(definition.id || key);
62
+ if (!surfaceId) {
63
+ continue;
64
+ }
65
+
66
+ normalized[surfaceId] = Object.freeze({
67
+ ...definition,
68
+ id: surfaceId,
69
+ enabled: definition.enabled !== false,
70
+ requiresAuth: definition.requiresAuth === true,
71
+ requiresWorkspace: definition.requiresWorkspace === true
72
+ });
73
+ }
74
+
75
+ return Object.freeze(normalized);
76
+ }
77
+
78
+ function resolveOwnershipFilterForSurfaceDefinition(definition = {}) {
79
+ if (definition.requiresWorkspace === true) {
80
+ return "workspace";
81
+ }
82
+ if (definition.requiresAuth === true) {
83
+ return "user";
84
+ }
85
+ return "public";
86
+ }
87
+
88
+ function resolveCrudModulePolicy({
89
+ moduleConfig = crudModuleConfig,
90
+ surfaceDefinitions = {},
91
+ defaultSurfaceId = "",
92
+ context = "resolveCrudModulePolicy"
93
+ } = {}) {
94
+ const config = asRecord(moduleConfig);
95
+ const normalizedDefinitions = normalizeCrudSurfaceDefinitions(surfaceDefinitions);
96
+ const requestedSurfaceId = normalizeSurfaceId(config.surface);
97
+ const fallbackSurfaceId = normalizeSurfaceId(defaultSurfaceId);
98
+ const selectedSurfaceId = requestedSurfaceId || fallbackSurfaceId;
99
+ if (!selectedSurfaceId) {
100
+ throw new Error(
101
+ `${context} requires crudModuleConfig.surface or an app default surface id.`
102
+ );
103
+ }
104
+
105
+ const surfaceDefinition = normalizedDefinitions[selectedSurfaceId];
106
+ if (!surfaceDefinition) {
107
+ throw new Error(
108
+ `${context} cannot resolve surface "${selectedSurfaceId}" from surface definitions.`
109
+ );
110
+ }
111
+ if (surfaceDefinition.enabled === false) {
112
+ throw new Error(`${context} surface "${selectedSurfaceId}" is disabled.`);
113
+ }
114
+
115
+ const requestedOwnershipFilter = normalizeCrudOwnershipFilter(config.ownershipFilter);
116
+ const resolvedOwnershipFilter =
117
+ requestedOwnershipFilter === CRUD_MODULE_OWNERSHIP_FILTER_AUTO
118
+ ? resolveOwnershipFilterForSurfaceDefinition(surfaceDefinition)
119
+ : normalizeScopedRouteVisibility(requestedOwnershipFilter, {
120
+ fallback: "public"
121
+ });
122
+
123
+ if (isWorkspaceVisibility(resolvedOwnershipFilter) && surfaceDefinition.requiresWorkspace !== true) {
124
+ throw new Error(
125
+ `${context} ownershipFilter "${resolvedOwnershipFilter}" requires a workspace-enabled surface.`
126
+ );
127
+ }
128
+
129
+ const relativePath = normalizeCrudRelativePath(config.relativePath, {
130
+ context
131
+ });
132
+
133
+ return Object.freeze({
134
+ namespace: normalizeText(config.namespace).toLowerCase(),
135
+ relativePath,
136
+ surfaceId: selectedSurfaceId,
137
+ requestedOwnershipFilter,
138
+ ownershipFilter: resolvedOwnershipFilter,
139
+ workspaceScoped: isWorkspaceVisibility(resolvedOwnershipFilter),
140
+ surfaceDefinition
141
+ });
142
+ }
143
+
144
+ function resolveCrudModulePolicyFromAppConfig(appConfig = {}, options = {}) {
145
+ const config = asRecord(appConfig);
146
+ return resolveCrudModulePolicy({
147
+ ...asRecord(options),
148
+ surfaceDefinitions: config.surfaceDefinitions,
149
+ defaultSurfaceId: config.surfaceDefaultId
150
+ });
151
+ }
152
+
153
+ function resolveCrudModulePolicyFromPlacementContext(placementContext = null, options = {}) {
154
+ const context = asRecord(placementContext);
155
+ const surfaceConfig = asRecord(context.surfaceConfig);
156
+ return resolveCrudModulePolicy({
157
+ ...asRecord(options),
158
+ surfaceDefinitions: surfaceConfig.surfacesById,
159
+ defaultSurfaceId: surfaceConfig.defaultSurfaceId
160
+ });
161
+ }
162
+
163
+ export {
164
+ CRUD_MODULE_OWNERSHIP_FILTER_AUTO,
165
+ crudModuleConfig,
166
+ resolveCrudModulePolicy,
167
+ resolveCrudModulePolicyFromAppConfig,
168
+ resolveCrudModulePolicyFromPlacementContext
169
+ };
@@ -0,0 +1,7 @@
1
+ <template>
2
+ <Edit${option:namespace|singular|pascal}Element />
3
+ </template>
4
+
5
+ <script setup>
6
+ import Edit${option:namespace|singular|pascal}Element from "@local/${option:namespace|kebab}/client/Edit${option:namespace|singular|pascal}Element";
7
+ </script>
@@ -0,0 +1,7 @@
1
+ <template>
2
+ <View${option:namespace|singular|pascal}Element />
3
+ </template>
4
+
5
+ <script setup>
6
+ import View${option:namespace|singular|pascal}Element from "@local/${option:namespace|kebab}/client/View${option:namespace|singular|pascal}Element";
7
+ </script>
@@ -0,0 +1,7 @@
1
+ <template>
2
+ <List${option:namespace|plural|pascal}Element />
3
+ </template>
4
+
5
+ <script setup>
6
+ import List${option:namespace|plural|pascal}Element from "@local/${option:namespace|kebab}/client/List${option:namespace|plural|pascal}Element";
7
+ </script>
@@ -0,0 +1,7 @@
1
+ <template>
2
+ <Create${option:namespace|singular|pascal}Element />
3
+ </template>
4
+
5
+ <script setup>
6
+ import Create${option:namespace|singular|pascal}Element from "@local/${option:namespace|kebab}/client/Create${option:namespace|singular|pascal}Element";
7
+ </script>