@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.
- package/package.descriptor.mjs +218 -0
- package/package.json +22 -0
- package/src/server/CrudServiceProvider.js +11 -0
- package/src/server/actionIds.js +22 -0
- package/src/server/actions.js +152 -0
- package/src/server/buildTemplateContext.js +871 -0
- package/src/server/crudModuleConfig.js +1 -0
- package/src/server/registerRoutes.js +234 -0
- package/src/server/repository.js +162 -0
- package/src/server/service.js +96 -0
- package/src/shared/crud/crudResource.js +191 -0
- package/src/shared/index.js +1 -0
- package/templates/migrations/crud_initial.cjs +17 -0
- package/templates/src/local-package/package.descriptor.mjs +67 -0
- package/templates/src/local-package/package.json +10 -0
- package/templates/src/local-package/server/CrudServiceProvider.js +84 -0
- package/templates/src/local-package/server/actionIds.js +9 -0
- package/templates/src/local-package/server/actions.js +151 -0
- package/templates/src/local-package/server/registerRoutes.js +197 -0
- package/templates/src/local-package/server/repository.js +161 -0
- package/templates/src/local-package/server/service.js +96 -0
- package/templates/src/local-package/shared/crudResource.js +109 -0
- package/templates/src/local-package/shared/index.js +3 -0
- package/test/buildTemplateContext.test.js +256 -0
- package/test/crudModuleConfig.test.js +225 -0
- package/test/crudResource.test.js +41 -0
- package/test/crudServerGuards.test.js +61 -0
- package/test/crudService.test.js +83 -0
- package/test/routeInputContracts.test.js +215 -0
|
@@ -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,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
|
+
});
|