@jskit-ai/crud-server-generator 0.1.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,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,109 @@
1
+ import { Type } from "typebox";
2
+ __JSKIT_CRUD_RESOURCE_DATABASE_RUNTIME_IMPORT__
3
+ import {
4
+ normalizeObjectInput,
5
+ createCursorListValidator
6
+ } from "@jskit-ai/kernel/shared/validators";
7
+ __JSKIT_CRUD_RESOURCE_NORMALIZE_SUPPORT_IMPORT__
8
+ __JSKIT_CRUD_RESOURCE_JSON_IMPORT__
9
+ function normalizeRecordInput(payload = {}) {
10
+ const source = normalizeObjectInput(payload);
11
+ const normalized = {};
12
+
13
+ __JSKIT_CRUD_RESOURCE_INPUT_NORMALIZATION_LINES__
14
+
15
+ return normalized;
16
+ }
17
+
18
+ function normalizeRecordOutput(payload = {}) {
19
+ const source = normalizeObjectInput(payload);
20
+
21
+ return {
22
+ __JSKIT_CRUD_RESOURCE_OUTPUT_NORMALIZATION_LINES__
23
+ };
24
+ }
25
+
26
+ const recordOutputSchema = Type.Object(
27
+ {
28
+ __JSKIT_CRUD_RESOURCE_OUTPUT_SCHEMA_PROPERTIES__
29
+ },
30
+ { additionalProperties: false }
31
+ );
32
+
33
+ const createBodySchema = Type.Object(
34
+ {
35
+ __JSKIT_CRUD_RESOURCE_CREATE_SCHEMA_PROPERTIES__
36
+ },
37
+ {
38
+ additionalProperties: false,
39
+ required: __JSKIT_CRUD_RESOURCE_CREATE_REQUIRED_FIELDS__
40
+ }
41
+ );
42
+
43
+ const patchBodySchema = Type.Partial(createBodySchema, {
44
+ additionalProperties: false
45
+ });
46
+
47
+ const recordOutputValidator = Object.freeze({
48
+ schema: recordOutputSchema,
49
+ normalize: normalizeRecordOutput
50
+ });
51
+
52
+ const ${option:namespace|singular|camel}Resource = {
53
+ resource: "${option:namespace|snake}",
54
+ messages: {
55
+ validation: "Fix invalid values and try again.",
56
+ saveSuccess: "Record saved.",
57
+ saveError: "Unable to save record.",
58
+ deleteSuccess: "Record deleted.",
59
+ deleteError: "Unable to delete record."
60
+ },
61
+ operations: {
62
+ list: {
63
+ method: "GET",
64
+ outputValidator: createCursorListValidator(recordOutputValidator)
65
+ },
66
+ view: {
67
+ method: "GET",
68
+ outputValidator: recordOutputValidator
69
+ },
70
+ create: {
71
+ method: "POST",
72
+ bodyValidator: {
73
+ schema: createBodySchema,
74
+ normalize: normalizeRecordInput
75
+ },
76
+ outputValidator: recordOutputValidator
77
+ },
78
+ patch: {
79
+ method: "PATCH",
80
+ bodyValidator: {
81
+ schema: patchBodySchema,
82
+ normalize: normalizeRecordInput
83
+ },
84
+ outputValidator: recordOutputValidator
85
+ },
86
+ delete: {
87
+ method: "DELETE",
88
+ outputValidator: {
89
+ schema: Type.Object(
90
+ {
91
+ id: Type.Integer({ minimum: 1 }),
92
+ deleted: Type.Literal(true)
93
+ },
94
+ { additionalProperties: false }
95
+ ),
96
+ normalize(payload = {}) {
97
+ const source = normalizeObjectInput(payload);
98
+
99
+ return {
100
+ id: Number(source.id),
101
+ deleted: true
102
+ };
103
+ }
104
+ }
105
+ }
106
+ }
107
+ };
108
+
109
+ export { ${option:namespace|singular|camel}Resource };
@@ -0,0 +1,3 @@
1
+ export {
2
+ ${option:namespace|singular|camel}Resource
3
+ } from "./${option:namespace|singular|camel}Resource.js";
@@ -0,0 +1,256 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+
4
+ import { buildTemplateContext, __testables } from "../src/server/buildTemplateContext.js";
5
+
6
+ function createSnapshot({
7
+ tableName = "contacts",
8
+ hasWorkspaceOwnerColumn = true,
9
+ hasUserOwnerColumn = true
10
+ } = {}) {
11
+ return Object.freeze({
12
+ tableName,
13
+ idColumn: "id",
14
+ primaryKeyColumns: Object.freeze(["id"]),
15
+ hasWorkspaceOwnerColumn,
16
+ hasUserOwnerColumn,
17
+ columns: Object.freeze([
18
+ Object.freeze({
19
+ name: "id",
20
+ key: "id",
21
+ dataType: "int",
22
+ columnType: "int unsigned",
23
+ typeKind: "integer",
24
+ nullable: false,
25
+ hasDefault: false,
26
+ defaultValue: null,
27
+ autoIncrement: true,
28
+ unsigned: true,
29
+ extra: "",
30
+ maxLength: null,
31
+ numericPrecision: 10,
32
+ numericScale: 0,
33
+ enumValues: Object.freeze([])
34
+ }),
35
+ Object.freeze({
36
+ name: "workspace_owner_id",
37
+ key: "workspaceOwnerId",
38
+ dataType: "int",
39
+ columnType: "int unsigned",
40
+ typeKind: "integer",
41
+ nullable: true,
42
+ hasDefault: false,
43
+ defaultValue: null,
44
+ autoIncrement: false,
45
+ unsigned: true,
46
+ extra: "",
47
+ maxLength: null,
48
+ numericPrecision: 10,
49
+ numericScale: 0,
50
+ enumValues: Object.freeze([])
51
+ }),
52
+ Object.freeze({
53
+ name: "user_owner_id",
54
+ key: "userOwnerId",
55
+ dataType: "int",
56
+ columnType: "int unsigned",
57
+ typeKind: "integer",
58
+ nullable: true,
59
+ hasDefault: false,
60
+ defaultValue: null,
61
+ autoIncrement: false,
62
+ unsigned: true,
63
+ extra: "",
64
+ maxLength: null,
65
+ numericPrecision: 10,
66
+ numericScale: 0,
67
+ enumValues: Object.freeze([])
68
+ }),
69
+ Object.freeze({
70
+ name: "first_name",
71
+ key: "firstName",
72
+ dataType: "varchar",
73
+ columnType: "varchar(160)",
74
+ typeKind: "string",
75
+ nullable: false,
76
+ hasDefault: false,
77
+ defaultValue: null,
78
+ autoIncrement: false,
79
+ unsigned: false,
80
+ extra: "",
81
+ maxLength: 160,
82
+ numericPrecision: null,
83
+ numericScale: null,
84
+ enumValues: Object.freeze([])
85
+ }),
86
+ Object.freeze({
87
+ name: "updated_at",
88
+ key: "updatedAt",
89
+ dataType: "datetime",
90
+ columnType: "datetime",
91
+ typeKind: "datetime",
92
+ nullable: false,
93
+ hasDefault: true,
94
+ defaultValue: "CURRENT_TIMESTAMP",
95
+ autoIncrement: false,
96
+ unsigned: false,
97
+ extra: "on update current_timestamp",
98
+ maxLength: null,
99
+ numericPrecision: null,
100
+ numericScale: null,
101
+ enumValues: Object.freeze([])
102
+ })
103
+ ]),
104
+ indexes: Object.freeze([])
105
+ });
106
+ }
107
+
108
+ test("resolveOwnershipFilterForGeneration infers ownership filter for table introspection mode", () => {
109
+ const snapshotBoth = createSnapshot({
110
+ hasWorkspaceOwnerColumn: true,
111
+ hasUserOwnerColumn: true
112
+ });
113
+ const snapshotWorkspaceOnly = createSnapshot({
114
+ hasWorkspaceOwnerColumn: true,
115
+ hasUserOwnerColumn: false
116
+ });
117
+ const snapshotUserOnly = createSnapshot({
118
+ hasWorkspaceOwnerColumn: false,
119
+ hasUserOwnerColumn: true
120
+ });
121
+ const snapshotPublic = createSnapshot({
122
+ hasWorkspaceOwnerColumn: false,
123
+ hasUserOwnerColumn: false
124
+ });
125
+
126
+ assert.equal(
127
+ __testables.resolveOwnershipFilterForGeneration(snapshotBoth, "auto", {
128
+ enforceTableColumns: true
129
+ }),
130
+ "workspace_user"
131
+ );
132
+ assert.equal(
133
+ __testables.resolveOwnershipFilterForGeneration(snapshotWorkspaceOnly, "auto", {
134
+ enforceTableColumns: true
135
+ }),
136
+ "workspace"
137
+ );
138
+ assert.equal(
139
+ __testables.resolveOwnershipFilterForGeneration(snapshotUserOnly, "auto", {
140
+ enforceTableColumns: true
141
+ }),
142
+ "user"
143
+ );
144
+ assert.equal(
145
+ __testables.resolveOwnershipFilterForGeneration(snapshotPublic, "auto", {
146
+ enforceTableColumns: true
147
+ }),
148
+ "public"
149
+ );
150
+ });
151
+
152
+ test("resolveOwnershipFilterForGeneration rejects explicit ownership filters when required columns are missing", () => {
153
+ const snapshotPublic = createSnapshot({
154
+ hasWorkspaceOwnerColumn: false,
155
+ hasUserOwnerColumn: false
156
+ });
157
+
158
+ assert.throws(
159
+ () =>
160
+ __testables.resolveOwnershipFilterForGeneration(snapshotPublic, "workspace", {
161
+ enforceTableColumns: true
162
+ }),
163
+ /requires column "workspace_owner_id"/
164
+ );
165
+ assert.throws(
166
+ () =>
167
+ __testables.resolveOwnershipFilterForGeneration(snapshotPublic, "user", {
168
+ enforceTableColumns: true
169
+ }),
170
+ /requires column "user_owner_id"/
171
+ );
172
+ });
173
+
174
+ test("buildTemplateContext requires table-name", async () => {
175
+ await assert.rejects(
176
+ buildTemplateContext({
177
+ appRoot: process.cwd(),
178
+ options: {
179
+ namespace: "contacts"
180
+ }
181
+ }),
182
+ /requires option "table-name"/
183
+ );
184
+ });
185
+
186
+ test("buildReplacementsFromSnapshot builds deterministic template replacement payload", () => {
187
+ const snapshot = createSnapshot();
188
+ const replacements = __testables.buildReplacementsFromSnapshot({
189
+ namespace: "contacts",
190
+ snapshot,
191
+ resolvedOwnershipFilter: "workspace_user"
192
+ });
193
+
194
+ assert.equal(replacements.__JSKIT_CRUD_TABLE_NAME__, "\"contacts\"");
195
+ assert.equal(replacements.__JSKIT_CRUD_ID_COLUMN__, "\"id\"");
196
+ assert.equal(replacements.__JSKIT_CRUD_RESOLVED_OWNERSHIP_FILTER__, "workspace_user");
197
+ assert.match(replacements.__JSKIT_CRUD_MIGRATION_COLUMN_LINES__, /table\.increments\("id"\)/);
198
+ assert.match(replacements.__JSKIT_CRUD_MIGRATION_COLUMN_LINES__, /table\.string\("first_name", 160\)/);
199
+ assert.match(replacements.__JSKIT_CRUD_REPOSITORY_OUTPUT_KEYS__, /"firstName"/);
200
+ assert.match(replacements.__JSKIT_CRUD_REPOSITORY_WRITE_KEYS__, /"firstName"/);
201
+ assert.equal(replacements.__JSKIT_CRUD_REPOSITORY_COLUMN_OVERRIDES__, "{}");
202
+ assert.match(replacements.__JSKIT_CRUD_RESOURCE_OUTPUT_SCHEMA_PROPERTIES__, /updatedAt: Type\.String/);
203
+ assert.match(
204
+ replacements.__JSKIT_CRUD_RESOURCE_OUTPUT_SCHEMA_PROPERTIES__,
205
+ /id: Type\.Integer\(\{ minimum: 1 \}\),/
206
+ );
207
+ assert.match(replacements.__JSKIT_CRUD_RESOURCE_CREATE_SCHEMA_PROPERTIES__, /firstName: Type\.String/);
208
+ assert.match(
209
+ replacements.__JSKIT_CRUD_RESOURCE_INPUT_NORMALIZATION_LINES__,
210
+ /normalizeIfInSource\(source, normalized, "firstName", normalizeText\);/
211
+ );
212
+ assert.doesNotMatch(
213
+ replacements.__JSKIT_CRUD_RESOURCE_INPUT_NORMALIZATION_LINES__,
214
+ /\(value\) =>/
215
+ );
216
+ assert.doesNotMatch(
217
+ replacements.__JSKIT_CRUD_RESOURCE_INPUT_NORMALIZATION_LINES__,
218
+ /value == null/
219
+ );
220
+ assert.match(
221
+ replacements.__JSKIT_CRUD_RESOURCE_OUTPUT_NORMALIZATION_LINES__,
222
+ /firstName: normalizeIfPresent\(source\.firstName, normalizeText\),/
223
+ );
224
+ assert.doesNotMatch(
225
+ replacements.__JSKIT_CRUD_RESOURCE_OUTPUT_NORMALIZATION_LINES__,
226
+ /== null \?/
227
+ );
228
+ assert.equal(replacements.__JSKIT_CRUD_RESOURCE_CREATE_REQUIRED_FIELDS__, "[\"firstName\"]");
229
+ });
230
+
231
+ test("renderMigrationColumnLine ignores SQL NULL string defaults", () => {
232
+ const line = __testables.renderMigrationColumnLine(
233
+ {
234
+ name: "workspace_owner_id",
235
+ dataType: "int",
236
+ columnType: "int unsigned",
237
+ typeKind: "integer",
238
+ nullable: true,
239
+ hasDefault: true,
240
+ defaultValue: "NULL",
241
+ autoIncrement: false,
242
+ unsigned: true,
243
+ extra: "",
244
+ maxLength: null,
245
+ numericPrecision: 10,
246
+ numericScale: 0,
247
+ enumValues: []
248
+ },
249
+ {
250
+ idColumn: "id",
251
+ primaryKeyColumns: ["id"]
252
+ }
253
+ );
254
+
255
+ assert.equal(line.includes(".defaultTo("), false);
256
+ });
@@ -0,0 +1,225 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import {
4
+ resolveCrudConfig,
5
+ resolveCrudSurfacePolicy,
6
+ resolveCrudConfigFromModules,
7
+ resolveCrudConfigsFromModules
8
+ } from "../src/server/crudModuleConfig.js";
9
+
10
+ test("resolveCrudConfig throws when namespace is missing", () => {
11
+ assert.throws(
12
+ () => resolveCrudConfig({}),
13
+ /requires a non-empty namespace/
14
+ );
15
+ });
16
+
17
+ test("resolveCrudConfig normalizes namespaced public settings", () => {
18
+ const config = resolveCrudConfig({
19
+ namespace: "CRM Team",
20
+ ownershipFilter: "public"
21
+ });
22
+
23
+ assert.equal(config.namespace, "crm-team");
24
+ assert.equal(config.ownershipFilter, "public");
25
+ assert.equal(config.workspaceScoped, false);
26
+ assert.equal(config.namespacePath, "/crm-team");
27
+ assert.equal(config.relativePath, "/crm-team");
28
+ assert.equal(config.apiBasePath, "/api/crm-team");
29
+ assert.equal(config.tableName, "crud_crm_team");
30
+ assert.equal(config.actionIdPrefix, "crud.crm_team");
31
+ assert.equal(config.contributorId, "crud.crm_team");
32
+ assert.equal(config.domain, "crud");
33
+ });
34
+
35
+ test("resolveCrudConfigsFromModules returns only crud module entries", () => {
36
+ const configs = resolveCrudConfigsFromModules({
37
+ "crud.customers": {
38
+ module: "crud",
39
+ namespace: "customers",
40
+ ownershipFilter: "workspace"
41
+ },
42
+ "crud.dragons": {
43
+ module: "crud",
44
+ namespace: "dragons",
45
+ ownershipFilter: "public"
46
+ },
47
+ "users.default": {
48
+ module: "users",
49
+ namespace: "ignored"
50
+ }
51
+ });
52
+
53
+ assert.deepEqual(configs.map((entry) => entry.namespace), ["customers", "dragons"]);
54
+ assert.deepEqual(configs.map((entry) => entry.ownershipFilter), ["workspace", "public"]);
55
+ });
56
+
57
+ test("resolveCrudConfigFromModules resolves explicit namespace", () => {
58
+ const config = resolveCrudConfigFromModules(
59
+ {
60
+ "crud.customers": {
61
+ module: "crud",
62
+ namespace: "customers",
63
+ ownershipFilter: "workspace"
64
+ },
65
+ "crud.dragons": {
66
+ module: "crud",
67
+ namespace: "dragons",
68
+ ownershipFilter: "workspace_user"
69
+ }
70
+ },
71
+ {
72
+ namespace: "dragons"
73
+ }
74
+ );
75
+
76
+ assert.ok(config);
77
+ assert.equal(config.namespace, "dragons");
78
+ assert.equal(config.ownershipFilter, "workspace_user");
79
+ });
80
+
81
+ test("resolveCrudConfigFromModules returns null without namespace when multiple crud entries exist", () => {
82
+ const config = resolveCrudConfigFromModules({
83
+ "crud.customers": {
84
+ module: "crud",
85
+ namespace: "customers",
86
+ ownershipFilter: "workspace"
87
+ },
88
+ "crud.dragons": {
89
+ module: "crud",
90
+ namespace: "dragons",
91
+ ownershipFilter: "workspace"
92
+ }
93
+ });
94
+
95
+ assert.equal(config, null);
96
+ });
97
+
98
+ test("resolveCrudConfigsFromModules rejects duplicate normalized namespaces", () => {
99
+ assert.throws(
100
+ () =>
101
+ resolveCrudConfigsFromModules({
102
+ "crud.customers": {
103
+ module: "crud",
104
+ namespace: "customers",
105
+ ownershipFilter: "workspace"
106
+ },
107
+ "crud.customers-copy": {
108
+ module: "crud",
109
+ namespace: "Customers",
110
+ ownershipFilter: "public"
111
+ }
112
+ }),
113
+ /Duplicate CRUD namespace/
114
+ );
115
+ });
116
+
117
+ test("resolveCrudConfigsFromModules rejects module entries without namespace", () => {
118
+ assert.throws(
119
+ () =>
120
+ resolveCrudConfigsFromModules({
121
+ "crud.invalid": {
122
+ module: "crud",
123
+ namespace: "",
124
+ ownershipFilter: "workspace"
125
+ }
126
+ }),
127
+ /requires a non-empty namespace/
128
+ );
129
+ });
130
+
131
+ test("resolveCrudSurfacePolicy resolves auto ownership filter from workspace surface metadata", () => {
132
+ const policy = resolveCrudSurfacePolicy(
133
+ {
134
+ surface: "admin",
135
+ ownershipFilter: "auto",
136
+ relativePath: "/crm/customers"
137
+ },
138
+ {
139
+ surfaceDefinitions: {
140
+ admin: { requiresWorkspace: true, requiresAuth: true, enabled: true }
141
+ },
142
+ defaultSurfaceId: "admin"
143
+ }
144
+ );
145
+
146
+ assert.equal(policy.surfaceId, "admin");
147
+ assert.equal(policy.ownershipFilter, "workspace");
148
+ assert.equal(policy.workspaceScoped, true);
149
+ assert.equal(policy.relativePath, "/crm/customers");
150
+ });
151
+
152
+ test("resolveCrudSurfacePolicy resolves auto ownership filter from auth-only surface metadata", () => {
153
+ const policy = resolveCrudSurfacePolicy(
154
+ {
155
+ surface: "console",
156
+ ownershipFilter: "auto",
157
+ relativePath: "/crm/customers"
158
+ },
159
+ {
160
+ surfaceDefinitions: {
161
+ console: { requiresWorkspace: false, requiresAuth: true, enabled: true }
162
+ },
163
+ defaultSurfaceId: "console"
164
+ }
165
+ );
166
+
167
+ assert.equal(policy.surfaceId, "console");
168
+ assert.equal(policy.ownershipFilter, "user");
169
+ assert.equal(policy.workspaceScoped, false);
170
+ });
171
+
172
+ test("resolveCrudSurfacePolicy rejects explicit workspace ownership filter on non-workspace surfaces", () => {
173
+ assert.throws(
174
+ () =>
175
+ resolveCrudSurfacePolicy(
176
+ {
177
+ surface: "console",
178
+ ownershipFilter: "workspace",
179
+ relativePath: "/crm/customers"
180
+ },
181
+ {
182
+ surfaceDefinitions: {
183
+ console: { requiresWorkspace: false, requiresAuth: true, enabled: true }
184
+ }
185
+ }
186
+ ),
187
+ /requires a workspace-enabled surface/
188
+ );
189
+ });
190
+
191
+ test("resolveCrudSurfacePolicy rejects unknown or disabled surfaces", () => {
192
+ assert.throws(
193
+ () =>
194
+ resolveCrudSurfacePolicy(
195
+ {
196
+ surface: "missing",
197
+ ownershipFilter: "auto",
198
+ relativePath: "/crm/customers"
199
+ },
200
+ {
201
+ surfaceDefinitions: {
202
+ console: { requiresWorkspace: false, requiresAuth: true, enabled: true }
203
+ }
204
+ }
205
+ ),
206
+ /cannot resolve surface "missing"/
207
+ );
208
+
209
+ assert.throws(
210
+ () =>
211
+ resolveCrudSurfacePolicy(
212
+ {
213
+ surface: "console",
214
+ ownershipFilter: "auto",
215
+ relativePath: "/crm/customers"
216
+ },
217
+ {
218
+ surfaceDefinitions: {
219
+ console: { requiresWorkspace: false, requiresAuth: true, enabled: false }
220
+ }
221
+ }
222
+ ),
223
+ /surface "console" is disabled/
224
+ );
225
+ });