@jskit-ai/crud-server-generator 0.1.31 → 0.1.33
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 +21 -9
- package/package.json +6 -6
- package/src/server/buildTemplateContext.js +42 -1
- package/templates/src/local-package/server/actions.js +21 -7
- package/templates/src/local-package/server/listConfig.js +5 -0
- package/templates/src/local-package/server/registerRoutes.js +4 -2
- package/templates/src/local-package/server/repository.js +1 -23
- package/templates/src/local-package/server/service.js +15 -62
- package/templates/src/local-package/shared/crudResource.js +1 -4
- package/test/buildTemplateContext.test.js +141 -8
- package/test/crudServerGuards.test.js +35 -0
- package/test/crudService.test.js +43 -0
- package/test-support/templateServerFixture.js +10 -2
package/package.descriptor.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export default Object.freeze({
|
|
2
2
|
packageVersion: 1,
|
|
3
3
|
packageId: "@jskit-ai/crud-server-generator",
|
|
4
|
-
version: "0.1.
|
|
4
|
+
version: "0.1.33",
|
|
5
5
|
kind: "generator",
|
|
6
6
|
description: "CRUD server generator with routes, actions, and persistence scaffolding.",
|
|
7
7
|
options: {
|
|
@@ -134,13 +134,13 @@ export default Object.freeze({
|
|
|
134
134
|
mutations: {
|
|
135
135
|
dependencies: {
|
|
136
136
|
runtime: {
|
|
137
|
-
"@jskit-ai/auth-core": "0.1.
|
|
138
|
-
"@jskit-ai/crud-core": "0.1.
|
|
139
|
-
"@jskit-ai/database-runtime": "0.1.
|
|
140
|
-
"@jskit-ai/http-runtime": "0.1.
|
|
141
|
-
"@jskit-ai/kernel": "0.1.
|
|
142
|
-
"@jskit-ai/realtime": "0.1.
|
|
143
|
-
"@jskit-ai/users-core": "0.1.
|
|
137
|
+
"@jskit-ai/auth-core": "0.1.24",
|
|
138
|
+
"@jskit-ai/crud-core": "0.1.33",
|
|
139
|
+
"@jskit-ai/database-runtime": "0.1.25",
|
|
140
|
+
"@jskit-ai/http-runtime": "0.1.24",
|
|
141
|
+
"@jskit-ai/kernel": "0.1.25",
|
|
142
|
+
"@jskit-ai/realtime": "0.1.24",
|
|
143
|
+
"@jskit-ai/users-core": "0.1.35",
|
|
144
144
|
"@local/${option:namespace|kebab}": "file:packages/${option:namespace|kebab}",
|
|
145
145
|
"typebox": "^1.0.81"
|
|
146
146
|
},
|
|
@@ -247,6 +247,18 @@ export default Object.freeze({
|
|
|
247
247
|
}
|
|
248
248
|
}
|
|
249
249
|
],
|
|
250
|
-
text: [
|
|
250
|
+
text: [
|
|
251
|
+
{
|
|
252
|
+
op: "append-text",
|
|
253
|
+
file: "config/roles.js",
|
|
254
|
+
position: "bottom",
|
|
255
|
+
skipIfContains: "\"crud.${option:namespace|snake}.list\"",
|
|
256
|
+
value:
|
|
257
|
+
"\nroleCatalog.roles.member.permissions.push(\n \"crud.${option:namespace|snake}.list\",\n \"crud.${option:namespace|snake}.view\",\n \"crud.${option:namespace|snake}.create\",\n \"crud.${option:namespace|snake}.update\",\n \"crud.${option:namespace|snake}.delete\"\n);\n",
|
|
258
|
+
reason: "Grant generated CRUD action permissions to the default member role in the app-owned role catalog.",
|
|
259
|
+
category: "crud",
|
|
260
|
+
id: "crud-role-catalog-permissions-${option:namespace|snake}"
|
|
261
|
+
}
|
|
262
|
+
]
|
|
251
263
|
}
|
|
252
264
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jskit-ai/crud-server-generator",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.33",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"test": "node --test"
|
|
@@ -13,11 +13,11 @@
|
|
|
13
13
|
},
|
|
14
14
|
"dependencies": {
|
|
15
15
|
"@babel/parser": "^7.29.2",
|
|
16
|
-
"@jskit-ai/crud-core": "0.1.
|
|
17
|
-
"@jskit-ai/database-runtime": "0.1.
|
|
18
|
-
"@jskit-ai/http-runtime": "0.1.
|
|
19
|
-
"@jskit-ai/kernel": "0.1.
|
|
20
|
-
"@jskit-ai/users-core": "0.1.
|
|
16
|
+
"@jskit-ai/crud-core": "0.1.33",
|
|
17
|
+
"@jskit-ai/database-runtime": "0.1.25",
|
|
18
|
+
"@jskit-ai/http-runtime": "0.1.24",
|
|
19
|
+
"@jskit-ai/kernel": "0.1.25",
|
|
20
|
+
"@jskit-ai/users-core": "0.1.35",
|
|
21
21
|
"recast": "^0.23.11",
|
|
22
22
|
"typebox": "^1.0.81"
|
|
23
23
|
}
|
|
@@ -369,7 +369,9 @@ function renderResourceFieldSchema(column, { forOutput = false } = {}) {
|
|
|
369
369
|
} else if (typeKind === "date") {
|
|
370
370
|
schemaExpression = 'Type.String({ format: "date", minLength: 1 })';
|
|
371
371
|
} else if (typeKind === "time") {
|
|
372
|
-
|
|
372
|
+
return column.nullable === true
|
|
373
|
+
? "NULLABLE_HTML_TIME_STRING_SCHEMA"
|
|
374
|
+
: "HTML_TIME_STRING_SCHEMA";
|
|
373
375
|
} else if (typeKind === "json") {
|
|
374
376
|
schemaExpression = "Type.Any()";
|
|
375
377
|
}
|
|
@@ -380,6 +382,17 @@ function renderResourceFieldSchema(column, { forOutput = false } = {}) {
|
|
|
380
382
|
return schemaExpression;
|
|
381
383
|
}
|
|
382
384
|
|
|
385
|
+
function renderResourceValidatorsImport({ needsHtmlTimeSchemas = false } = {}) {
|
|
386
|
+
const imports = [
|
|
387
|
+
"normalizeObjectInput",
|
|
388
|
+
"createCursorListValidator"
|
|
389
|
+
];
|
|
390
|
+
if (needsHtmlTimeSchemas) {
|
|
391
|
+
imports.push("HTML_TIME_STRING_SCHEMA", "NULLABLE_HTML_TIME_STRING_SCHEMA");
|
|
392
|
+
}
|
|
393
|
+
return `import {\n ${imports.join(",\n ")}\n} from "@jskit-ai/kernel/shared/validators";`;
|
|
394
|
+
}
|
|
395
|
+
|
|
383
396
|
function renderInputNormalizer(column) {
|
|
384
397
|
const typeKind = String(column.typeKind || "");
|
|
385
398
|
const nullable = column?.nullable === true;
|
|
@@ -1064,6 +1077,29 @@ function renderResourceFieldMetaPushLines(entries = []) {
|
|
|
1064
1077
|
return sourceEntries.map((entry) => renderFieldMetaEntryLines(entry)).join("\n\n");
|
|
1065
1078
|
}
|
|
1066
1079
|
|
|
1080
|
+
function renderRepositoryListConfigLines(snapshot = {}) {
|
|
1081
|
+
const commentLines = [
|
|
1082
|
+
" // defaultLimit: 20,",
|
|
1083
|
+
" // maxLimit: 100,",
|
|
1084
|
+
" // searchColumns: [\"name\"],"
|
|
1085
|
+
];
|
|
1086
|
+
const sourceColumns = Array.isArray(snapshot?.columns) ? snapshot.columns : [];
|
|
1087
|
+
const hasCreatedAtColumn = sourceColumns.some((column = {}) => normalizeText(column?.name) === "created_at");
|
|
1088
|
+
if (!hasCreatedAtColumn) {
|
|
1089
|
+
return commentLines.join("\n");
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
return [
|
|
1093
|
+
...commentLines,
|
|
1094
|
+
" orderBy: [",
|
|
1095
|
+
" {",
|
|
1096
|
+
" column: \"created_at\",",
|
|
1097
|
+
" direction: \"desc\"",
|
|
1098
|
+
" }",
|
|
1099
|
+
" ]"
|
|
1100
|
+
].join("\n");
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1067
1103
|
function buildReplacementsFromSnapshot({
|
|
1068
1104
|
snapshot,
|
|
1069
1105
|
resolvedOwnershipFilter
|
|
@@ -1090,6 +1126,7 @@ function buildReplacementsFromSnapshot({
|
|
|
1090
1126
|
const needsNullableDateInput = writableColumns.some(
|
|
1091
1127
|
(column) => column.typeKind === "date" && column.nullable === true
|
|
1092
1128
|
);
|
|
1129
|
+
const needsHtmlTimeSchemas = resourceColumns.some((column) => column.typeKind === "time");
|
|
1093
1130
|
const needsDate = resourceColumns.some((column) => column.typeKind === "date");
|
|
1094
1131
|
const needsJson = resourceColumns.some((column) => column.typeKind === "json");
|
|
1095
1132
|
const needsNormalizeText = resourceColumns.some((column) =>
|
|
@@ -1107,6 +1144,9 @@ function buildReplacementsFromSnapshot({
|
|
|
1107
1144
|
__JSKIT_CRUD_TABLE_NAME__: JSON.stringify(snapshot.tableName),
|
|
1108
1145
|
__JSKIT_CRUD_ID_COLUMN__: JSON.stringify(snapshot.idColumn || DEFAULT_ID_COLUMN),
|
|
1109
1146
|
__JSKIT_CRUD_RESOLVED_OWNERSHIP_FILTER__: resolvedOwnershipFilter,
|
|
1147
|
+
__JSKIT_CRUD_RESOURCE_VALIDATORS_IMPORT__: renderResourceValidatorsImport({
|
|
1148
|
+
needsHtmlTimeSchemas
|
|
1149
|
+
}),
|
|
1110
1150
|
__JSKIT_CRUD_RESOURCE_DATABASE_RUNTIME_IMPORT__: renderResourceDatabaseRuntimeImport({
|
|
1111
1151
|
needsToIsoString: needsDateTimeOutput || needsDate,
|
|
1112
1152
|
needsToDatabaseDateTimeUtc: needsDateTimeInput
|
|
@@ -1133,6 +1173,7 @@ function buildReplacementsFromSnapshot({
|
|
|
1133
1173
|
__JSKIT_CRUD_RESOURCE_OUTPUT_NORMALIZATION_LINES__: renderResourceOutputNormalizationLines(outputColumns),
|
|
1134
1174
|
__JSKIT_CRUD_RESOURCE_CREATE_REQUIRED_FIELDS__: JSON.stringify(createRequiredFieldKeys),
|
|
1135
1175
|
__JSKIT_CRUD_RESOURCE_FIELD_META_PUSH_LINES__: renderResourceFieldMetaPushLines(fieldMetaEntries),
|
|
1176
|
+
__JSKIT_CRUD_LIST_CONFIG_LINES__: renderRepositoryListConfigLines(snapshot),
|
|
1136
1177
|
__JSKIT_CRUD_MIGRATION_COLUMN_LINES__: renderMigrationColumnLines(snapshot),
|
|
1137
1178
|
__JSKIT_CRUD_MIGRATION_INDEX_LINES__: renderMigrationIndexLines(snapshot),
|
|
1138
1179
|
__JSKIT_CRUD_MIGRATION_FOREIGN_KEY_LINES__: renderMigrationForeignKeyLines(snapshot)
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import {
|
|
2
|
-
cursorPaginationQueryValidator,
|
|
3
2
|
recordIdParamsValidator
|
|
4
3
|
} from "@jskit-ai/kernel/shared/validators";
|
|
5
4
|
import {
|
|
5
|
+
createCrudCursorPaginationQueryValidator,
|
|
6
6
|
listSearchQueryValidator,
|
|
7
7
|
lookupIncludeQueryValidator,
|
|
8
8
|
createCrudParentFilterQueryValidator
|
|
@@ -10,8 +10,17 @@ import {
|
|
|
10
10
|
import { workspaceSlugParamsValidator } from "@jskit-ai/users-core/server/validators/routeParamsValidator";
|
|
11
11
|
import { resource } from "../shared/${option:namespace|singular|camel}Resource.js";
|
|
12
12
|
import { actionIds } from "./actionIds.js";
|
|
13
|
+
import { LIST_CONFIG } from "./listConfig.js";
|
|
13
14
|
|
|
15
|
+
const listCursorPaginationQueryValidator = createCrudCursorPaginationQueryValidator(LIST_CONFIG);
|
|
14
16
|
const listParentFilterQueryValidator = createCrudParentFilterQueryValidator(resource);
|
|
17
|
+
const actionPermissions = Object.freeze({
|
|
18
|
+
list: "crud.${option:namespace|snake}.list",
|
|
19
|
+
view: "crud.${option:namespace|snake}.view",
|
|
20
|
+
create: "crud.${option:namespace|snake}.create",
|
|
21
|
+
update: "crud.${option:namespace|snake}.update",
|
|
22
|
+
delete: "crud.${option:namespace|snake}.delete"
|
|
23
|
+
});
|
|
15
24
|
|
|
16
25
|
function requireActionSurface(surface = "") {
|
|
17
26
|
const normalizedSurface = String(surface || "").trim().toLowerCase();
|
|
@@ -33,11 +42,12 @@ function createActions({ surface = "" } = {}) {
|
|
|
33
42
|
channels: ["api", "automation", "internal"],
|
|
34
43
|
surfaces: [actionSurface],
|
|
35
44
|
permission: {
|
|
36
|
-
require: "
|
|
45
|
+
require: "all",
|
|
46
|
+
permissions: [actionPermissions.list]
|
|
37
47
|
},
|
|
38
48
|
inputValidator: [
|
|
39
49
|
workspaceSlugParamsValidator,
|
|
40
|
-
|
|
50
|
+
listCursorPaginationQueryValidator,
|
|
41
51
|
listSearchQueryValidator,
|
|
42
52
|
listParentFilterQueryValidator,
|
|
43
53
|
lookupIncludeQueryValidator
|
|
@@ -62,7 +72,8 @@ function createActions({ surface = "" } = {}) {
|
|
|
62
72
|
channels: ["api", "automation", "internal"],
|
|
63
73
|
surfaces: [actionSurface],
|
|
64
74
|
permission: {
|
|
65
|
-
require: "
|
|
75
|
+
require: "all",
|
|
76
|
+
permissions: [actionPermissions.view]
|
|
66
77
|
},
|
|
67
78
|
inputValidator: [workspaceSlugParamsValidator, recordIdParamsValidator, lookupIncludeQueryValidator],
|
|
68
79
|
outputValidator: resource.operations.view.outputValidator,
|
|
@@ -86,7 +97,8 @@ function createActions({ surface = "" } = {}) {
|
|
|
86
97
|
channels: ["api", "automation", "internal"],
|
|
87
98
|
surfaces: [actionSurface],
|
|
88
99
|
permission: {
|
|
89
|
-
require: "
|
|
100
|
+
require: "all",
|
|
101
|
+
permissions: [actionPermissions.create]
|
|
90
102
|
},
|
|
91
103
|
inputValidator: [
|
|
92
104
|
workspaceSlugParamsValidator,
|
|
@@ -114,7 +126,8 @@ function createActions({ surface = "" } = {}) {
|
|
|
114
126
|
channels: ["api", "automation", "internal"],
|
|
115
127
|
surfaces: [actionSurface],
|
|
116
128
|
permission: {
|
|
117
|
-
require: "
|
|
129
|
+
require: "all",
|
|
130
|
+
permissions: [actionPermissions.update]
|
|
118
131
|
},
|
|
119
132
|
inputValidator: [
|
|
120
133
|
workspaceSlugParamsValidator,
|
|
@@ -143,7 +156,8 @@ function createActions({ surface = "" } = {}) {
|
|
|
143
156
|
channels: ["api", "automation", "internal"],
|
|
144
157
|
surfaces: [actionSurface],
|
|
145
158
|
permission: {
|
|
146
|
-
require: "
|
|
159
|
+
require: "all",
|
|
160
|
+
permissions: [actionPermissions.delete]
|
|
147
161
|
},
|
|
148
162
|
inputValidator: [workspaceSlugParamsValidator, recordIdParamsValidator],
|
|
149
163
|
outputValidator: resource.operations.delete.outputValidator,
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { withStandardErrorResponses } from "@jskit-ai/http-runtime/shared/validators/errorResponses";
|
|
2
2
|
import { normalizeSurfaceId } from "@jskit-ai/kernel/shared/surface/registry";
|
|
3
3
|
import {
|
|
4
|
+
createCrudCursorPaginationQueryValidator,
|
|
4
5
|
listSearchQueryValidator,
|
|
5
6
|
lookupIncludeQueryValidator,
|
|
6
7
|
createCrudParentFilterQueryValidator
|
|
7
8
|
} from "@jskit-ai/crud-core/server/listQueryValidators";
|
|
8
9
|
import {
|
|
9
|
-
cursorPaginationQueryValidator,
|
|
10
10
|
recordIdParamsValidator
|
|
11
11
|
} from "@jskit-ai/kernel/shared/validators";
|
|
12
12
|
import { routeParamsValidator } from "@jskit-ai/users-core/server/validators/routeParamsValidator";
|
|
@@ -15,7 +15,9 @@ import { buildWorkspaceInputFromRouteParams } from "@jskit-ai/users-core/server/
|
|
|
15
15
|
import { resolveApiBasePath } from "@jskit-ai/users-core/shared/support/usersApiPaths";
|
|
16
16
|
import { actionIds } from "./actionIds.js";
|
|
17
17
|
import { resource } from "../shared/${option:namespace|singular|camel}Resource.js";
|
|
18
|
+
import { LIST_CONFIG } from "./listConfig.js";
|
|
18
19
|
|
|
20
|
+
const listCursorPaginationQueryValidator = createCrudCursorPaginationQueryValidator(LIST_CONFIG);
|
|
19
21
|
const listParentFilterQueryValidator = createCrudParentFilterQueryValidator(resource);
|
|
20
22
|
|
|
21
23
|
function registerRoutes(
|
|
@@ -47,7 +49,7 @@ function registerRoutes(
|
|
|
47
49
|
},
|
|
48
50
|
paramsValidator: routeParamsValidator,
|
|
49
51
|
queryValidator: [
|
|
50
|
-
|
|
52
|
+
listCursorPaginationQueryValidator,
|
|
51
53
|
listSearchQueryValidator,
|
|
52
54
|
listParentFilterQueryValidator,
|
|
53
55
|
lookupIncludeQueryValidator
|
|
@@ -3,18 +3,12 @@ import {
|
|
|
3
3
|
crudRepositoryList,
|
|
4
4
|
crudRepositoryFindById,
|
|
5
5
|
crudRepositoryListByIds,
|
|
6
|
-
crudRepositoryListByForeignIds,
|
|
7
6
|
crudRepositoryCreate,
|
|
8
7
|
crudRepositoryUpdateById,
|
|
9
8
|
crudRepositoryDeleteById
|
|
10
9
|
} from "@jskit-ai/crud-core/server/repositoryMethods";
|
|
11
10
|
import { resource } from "../shared/${option:namespace|singular|camel}Resource.js";
|
|
12
|
-
|
|
13
|
-
const LIST_CONFIG = Object.freeze({
|
|
14
|
-
// defaultLimit: 20,
|
|
15
|
-
// maxLimit: 100,
|
|
16
|
-
// searchColumns: ["name"]
|
|
17
|
-
});
|
|
11
|
+
import { LIST_CONFIG } from "./listConfig.js";
|
|
18
12
|
|
|
19
13
|
const repositoryRuntime = createCrudRepositoryRuntime(resource, {
|
|
20
14
|
context: "${option:namespace|snake} repository",
|
|
@@ -22,10 +16,6 @@ const repositoryRuntime = createCrudRepositoryRuntime(resource, {
|
|
|
22
16
|
});
|
|
23
17
|
|
|
24
18
|
function createRepository(knex, options = {}) {
|
|
25
|
-
if (typeof knex !== "function") {
|
|
26
|
-
throw new TypeError("crudRepository requires knex.");
|
|
27
|
-
}
|
|
28
|
-
|
|
29
19
|
async function list(query = {}, callOptions = {}) {
|
|
30
20
|
return crudRepositoryList(repositoryRuntime, knex, query, options, callOptions);
|
|
31
21
|
}
|
|
@@ -38,17 +28,6 @@ function createRepository(knex, options = {}) {
|
|
|
38
28
|
return crudRepositoryListByIds(repositoryRuntime, knex, ids, options, callOptions);
|
|
39
29
|
}
|
|
40
30
|
|
|
41
|
-
async function listByForeignIds(ids = [], foreignKey = "", callOptions = {}) {
|
|
42
|
-
return crudRepositoryListByForeignIds(
|
|
43
|
-
repositoryRuntime,
|
|
44
|
-
knex,
|
|
45
|
-
ids,
|
|
46
|
-
foreignKey,
|
|
47
|
-
options,
|
|
48
|
-
callOptions
|
|
49
|
-
);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
31
|
async function create(payload = {}, callOptions = {}) {
|
|
53
32
|
return crudRepositoryCreate(repositoryRuntime, knex, payload, options, callOptions);
|
|
54
33
|
}
|
|
@@ -65,7 +44,6 @@ function createRepository(knex, options = {}) {
|
|
|
65
44
|
list,
|
|
66
45
|
findById,
|
|
67
46
|
listByIds,
|
|
68
|
-
listByForeignIds,
|
|
69
47
|
create,
|
|
70
48
|
updateById,
|
|
71
49
|
deleteById
|
|
@@ -1,12 +1,18 @@
|
|
|
1
|
-
import { AppError } from "@jskit-ai/kernel/server/runtime/errors";
|
|
2
1
|
import { createCrudServiceEvents } from "@jskit-ai/crud-core/server/serviceEvents";
|
|
3
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
createCrudServiceRuntime,
|
|
4
|
+
crudServiceListRecords,
|
|
5
|
+
crudServiceGetRecord,
|
|
6
|
+
crudServiceCreateRecord,
|
|
7
|
+
crudServiceUpdateRecord,
|
|
8
|
+
crudServiceDeleteRecord
|
|
9
|
+
} from "@jskit-ai/crud-core/server/serviceMethods";
|
|
4
10
|
import { resource } from "../shared/${option:namespace|singular|camel}Resource.js";
|
|
5
11
|
|
|
6
|
-
const
|
|
12
|
+
const serviceRuntime = createCrudServiceRuntime(resource, {
|
|
7
13
|
context: "${option:namespace|camel}Service"
|
|
8
14
|
});
|
|
9
|
-
const
|
|
15
|
+
const baseServiceEvents = createCrudServiceEvents(resource, {
|
|
10
16
|
context: "${option:namespace|camel}Service"
|
|
11
17
|
});
|
|
12
18
|
|
|
@@ -37,77 +43,24 @@ const DEFAULT_FIELD_ACCESS = Object.freeze({
|
|
|
37
43
|
});
|
|
38
44
|
|
|
39
45
|
function createService({ ${option:namespace|camel}Repository, fieldAccess = DEFAULT_FIELD_ACCESS } = {}) {
|
|
40
|
-
if (!${option:namespace|camel}Repository) {
|
|
41
|
-
throw new Error("${option:namespace|camel}Service requires ${option:namespace|camel}Repository.");
|
|
42
|
-
}
|
|
43
|
-
|
|
44
46
|
async function listRecords(query = {}, options = {}) {
|
|
45
|
-
|
|
46
|
-
return fieldAccessRuntime.filterReadableListResult(result, fieldAccess, {
|
|
47
|
-
action: "list",
|
|
48
|
-
query,
|
|
49
|
-
options,
|
|
50
|
-
context: options?.context
|
|
51
|
-
});
|
|
47
|
+
return crudServiceListRecords(serviceRuntime, ${option:namespace|camel}Repository, fieldAccess, query, options);
|
|
52
48
|
}
|
|
53
49
|
|
|
54
50
|
async function getRecord(recordId, options = {}) {
|
|
55
|
-
|
|
56
|
-
if (!record) {
|
|
57
|
-
throw new AppError(404, "Record not found.");
|
|
58
|
-
}
|
|
59
|
-
return fieldAccessRuntime.filterReadableRecord(record, fieldAccess, {
|
|
60
|
-
action: "view",
|
|
61
|
-
recordId,
|
|
62
|
-
options,
|
|
63
|
-
context: options?.context
|
|
64
|
-
});
|
|
51
|
+
return crudServiceGetRecord(serviceRuntime, ${option:namespace|camel}Repository, fieldAccess, recordId, options);
|
|
65
52
|
}
|
|
66
53
|
|
|
67
54
|
async function createRecord(payload = {}, options = {}) {
|
|
68
|
-
|
|
69
|
-
action: "create",
|
|
70
|
-
payload,
|
|
71
|
-
options,
|
|
72
|
-
context: options?.context
|
|
73
|
-
});
|
|
74
|
-
const record = await ${option:namespace|camel}Repository.create(writablePayload, options);
|
|
75
|
-
if (!record) {
|
|
76
|
-
throw new Error("${option:namespace|camel}Service could not load the created record.");
|
|
77
|
-
}
|
|
78
|
-
return fieldAccessRuntime.filterReadableRecord(record, fieldAccess, {
|
|
79
|
-
action: "create",
|
|
80
|
-
options,
|
|
81
|
-
context: options?.context
|
|
82
|
-
});
|
|
55
|
+
return crudServiceCreateRecord(serviceRuntime, ${option:namespace|camel}Repository, fieldAccess, payload, options);
|
|
83
56
|
}
|
|
84
57
|
|
|
85
58
|
async function updateRecord(recordId, payload = {}, options = {}) {
|
|
86
|
-
|
|
87
|
-
action: "update",
|
|
88
|
-
recordId,
|
|
89
|
-
payload,
|
|
90
|
-
options,
|
|
91
|
-
context: options?.context
|
|
92
|
-
});
|
|
93
|
-
const record = await ${option:namespace|camel}Repository.updateById(recordId, writablePayload, options);
|
|
94
|
-
if (!record) {
|
|
95
|
-
throw new AppError(404, "Record not found.");
|
|
96
|
-
}
|
|
97
|
-
return fieldAccessRuntime.filterReadableRecord(record, fieldAccess, {
|
|
98
|
-
action: "update",
|
|
99
|
-
recordId,
|
|
100
|
-
options,
|
|
101
|
-
context: options?.context
|
|
102
|
-
});
|
|
59
|
+
return crudServiceUpdateRecord(serviceRuntime, ${option:namespace|camel}Repository, fieldAccess, recordId, payload, options);
|
|
103
60
|
}
|
|
104
61
|
|
|
105
62
|
async function deleteRecord(recordId, options = {}) {
|
|
106
|
-
|
|
107
|
-
if (!deleted) {
|
|
108
|
-
throw new AppError(404, "Record not found.");
|
|
109
|
-
}
|
|
110
|
-
return deleted;
|
|
63
|
+
return crudServiceDeleteRecord(serviceRuntime, ${option:namespace|camel}Repository, fieldAccess, recordId, options);
|
|
111
64
|
}
|
|
112
65
|
|
|
113
66
|
return Object.freeze({
|
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
import { Type } from "typebox";
|
|
2
2
|
__JSKIT_CRUD_RESOURCE_DATABASE_RUNTIME_IMPORT__
|
|
3
|
-
|
|
4
|
-
normalizeObjectInput,
|
|
5
|
-
createCursorListValidator
|
|
6
|
-
} from "@jskit-ai/kernel/shared/validators";
|
|
3
|
+
__JSKIT_CRUD_RESOURCE_VALIDATORS_IMPORT__
|
|
7
4
|
__JSKIT_CRUD_RESOURCE_NORMALIZE_SUPPORT_IMPORT__
|
|
8
5
|
__JSKIT_CRUD_RESOURCE_JSON_IMPORT__
|
|
9
6
|
|
|
@@ -9,8 +9,30 @@ import { buildTemplateContext, __testables } from "../src/server/buildTemplateCo
|
|
|
9
9
|
function createSnapshot({
|
|
10
10
|
tableName = "contacts",
|
|
11
11
|
hasWorkspaceOwnerColumn = true,
|
|
12
|
-
hasUserOwnerColumn = true
|
|
12
|
+
hasUserOwnerColumn = true,
|
|
13
|
+
hasCreatedAtColumn = true
|
|
13
14
|
} = {}) {
|
|
15
|
+
const createdAtColumn = hasCreatedAtColumn
|
|
16
|
+
? [
|
|
17
|
+
Object.freeze({
|
|
18
|
+
name: "created_at",
|
|
19
|
+
key: "createdAt",
|
|
20
|
+
dataType: "datetime",
|
|
21
|
+
columnType: "datetime",
|
|
22
|
+
typeKind: "datetime",
|
|
23
|
+
nullable: false,
|
|
24
|
+
hasDefault: true,
|
|
25
|
+
defaultValue: "CURRENT_TIMESTAMP",
|
|
26
|
+
autoIncrement: false,
|
|
27
|
+
unsigned: false,
|
|
28
|
+
extra: "",
|
|
29
|
+
maxLength: null,
|
|
30
|
+
numericPrecision: null,
|
|
31
|
+
numericScale: null,
|
|
32
|
+
enumValues: Object.freeze([])
|
|
33
|
+
})
|
|
34
|
+
]
|
|
35
|
+
: [];
|
|
14
36
|
return Object.freeze({
|
|
15
37
|
tableName,
|
|
16
38
|
idColumn: "id",
|
|
@@ -86,6 +108,7 @@ function createSnapshot({
|
|
|
86
108
|
numericScale: null,
|
|
87
109
|
enumValues: Object.freeze([])
|
|
88
110
|
}),
|
|
111
|
+
...createdAtColumn,
|
|
89
112
|
Object.freeze({
|
|
90
113
|
name: "updated_at",
|
|
91
114
|
key: "updatedAt",
|
|
@@ -223,6 +246,14 @@ test("buildReplacementsFromSnapshot builds deterministic template replacement pa
|
|
|
223
246
|
replacements.__JSKIT_CRUD_RESOURCE_OUTPUT_NORMALIZATION_LINES__,
|
|
224
247
|
/firstName: normalizeIfPresent\(source\.firstName, normalizeText\),/
|
|
225
248
|
);
|
|
249
|
+
assert.match(
|
|
250
|
+
replacements.__JSKIT_CRUD_LIST_CONFIG_LINES__,
|
|
251
|
+
/orderBy: \[\s+{\s+column: "created_at",\s+direction: "desc"\s+}\s+\]/s
|
|
252
|
+
);
|
|
253
|
+
assert.match(
|
|
254
|
+
replacements.__JSKIT_CRUD_LIST_CONFIG_LINES__,
|
|
255
|
+
/\/\/ searchColumns: \["name"\],\s+orderBy:/s
|
|
256
|
+
);
|
|
226
257
|
assert.doesNotMatch(
|
|
227
258
|
replacements.__JSKIT_CRUD_RESOURCE_OUTPUT_NORMALIZATION_LINES__,
|
|
228
259
|
/== null \?/
|
|
@@ -231,6 +262,20 @@ test("buildReplacementsFromSnapshot builds deterministic template replacement pa
|
|
|
231
262
|
assert.equal(replacements.__JSKIT_CRUD_MIGRATION_FOREIGN_KEY_LINES__, "");
|
|
232
263
|
});
|
|
233
264
|
|
|
265
|
+
test("buildReplacementsFromSnapshot omits default list ordering when created_at is absent", () => {
|
|
266
|
+
const snapshot = createSnapshot({
|
|
267
|
+
hasCreatedAtColumn: false
|
|
268
|
+
});
|
|
269
|
+
const replacements = __testables.buildReplacementsFromSnapshot({
|
|
270
|
+
namespace: "contacts",
|
|
271
|
+
snapshot,
|
|
272
|
+
resolvedOwnershipFilter: "workspace_user"
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
assert.doesNotMatch(replacements.__JSKIT_CRUD_LIST_CONFIG_LINES__, /orderBy/);
|
|
276
|
+
assert.match(replacements.__JSKIT_CRUD_LIST_CONFIG_LINES__, /searchColumns/);
|
|
277
|
+
});
|
|
278
|
+
|
|
234
279
|
test("buildReplacementsFromSnapshot renders append-only field meta entries from foreign keys", () => {
|
|
235
280
|
const snapshot = {
|
|
236
281
|
...createSnapshot(),
|
|
@@ -444,6 +489,7 @@ test("crud repository template defines explicit one-line CRUD methods over repos
|
|
|
444
489
|
templateSource,
|
|
445
490
|
/from "@jskit-ai\/crud-core\/server\/repositoryMethods";/
|
|
446
491
|
);
|
|
492
|
+
assert.match(templateSource, /import \{ LIST_CONFIG \} from "\.\/listConfig\.js";/);
|
|
447
493
|
assert.match(templateSource, /const repositoryRuntime = createCrudRepositoryRuntime\(/);
|
|
448
494
|
assert.match(templateSource, /return crudRepositoryList\(repositoryRuntime, knex, query, options, callOptions\);/);
|
|
449
495
|
assert.match(templateSource, /return crudRepositoryFindById\(repositoryRuntime, knex, recordId, options, callOptions\);/);
|
|
@@ -451,9 +497,31 @@ test("crud repository template defines explicit one-line CRUD methods over repos
|
|
|
451
497
|
assert.match(templateSource, /return crudRepositoryCreate\(repositoryRuntime, knex, payload, options, callOptions\);/);
|
|
452
498
|
assert.match(templateSource, /return crudRepositoryUpdateById\(repositoryRuntime, knex, recordId, patch, options, callOptions\);/);
|
|
453
499
|
assert.match(templateSource, /return crudRepositoryDeleteById\(repositoryRuntime, knex, recordId, options, callOptions\);/);
|
|
500
|
+
assert.doesNotMatch(templateSource, /listByForeignIds/);
|
|
501
|
+
assert.doesNotMatch(templateSource, /crudRepository requires knex/);
|
|
454
502
|
});
|
|
455
503
|
|
|
456
|
-
test("crud
|
|
504
|
+
test("crud actions and routes templates share LIST_CONFIG for cursor validation", async () => {
|
|
505
|
+
const testDirectory = path.dirname(fileURLToPath(import.meta.url));
|
|
506
|
+
const actionsTemplatePath = path.resolve(testDirectory, "..", "templates", "src", "local-package", "server", "actions.js");
|
|
507
|
+
const registerRoutesTemplatePath = path.resolve(testDirectory, "..", "templates", "src", "local-package", "server", "registerRoutes.js");
|
|
508
|
+
const listConfigTemplatePath = path.resolve(testDirectory, "..", "templates", "src", "local-package", "server", "listConfig.js");
|
|
509
|
+
|
|
510
|
+
const actionsTemplateSource = await readFile(actionsTemplatePath, "utf8");
|
|
511
|
+
const registerRoutesTemplateSource = await readFile(registerRoutesTemplatePath, "utf8");
|
|
512
|
+
const listConfigTemplateSource = await readFile(listConfigTemplatePath, "utf8");
|
|
513
|
+
|
|
514
|
+
assert.match(actionsTemplateSource, /createCrudCursorPaginationQueryValidator/);
|
|
515
|
+
assert.match(actionsTemplateSource, /import \{ LIST_CONFIG \} from "\.\/listConfig\.js";/);
|
|
516
|
+
assert.match(actionsTemplateSource, /const listCursorPaginationQueryValidator = createCrudCursorPaginationQueryValidator\(LIST_CONFIG\);/);
|
|
517
|
+
assert.match(registerRoutesTemplateSource, /createCrudCursorPaginationQueryValidator/);
|
|
518
|
+
assert.match(registerRoutesTemplateSource, /import \{ LIST_CONFIG \} from "\.\/listConfig\.js";/);
|
|
519
|
+
assert.match(registerRoutesTemplateSource, /const listCursorPaginationQueryValidator = createCrudCursorPaginationQueryValidator\(LIST_CONFIG\);/);
|
|
520
|
+
assert.match(listConfigTemplateSource, /const LIST_CONFIG = Object\.freeze\(\{/);
|
|
521
|
+
assert.match(listConfigTemplateSource, /__JSKIT_CRUD_LIST_CONFIG_LINES__/);
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
test("crud service template defines explicit service methods over shared service primitives and preserves overridable default events", async () => {
|
|
457
525
|
const testDirectory = path.dirname(fileURLToPath(import.meta.url));
|
|
458
526
|
const templatePath = path.resolve(testDirectory, "..", "templates", "src", "local-package", "server", "service.js");
|
|
459
527
|
const templateSource = await readFile(templatePath, "utf8");
|
|
@@ -464,16 +532,81 @@ test("crud service template defines explicit service methods and semi-explicit d
|
|
|
464
532
|
);
|
|
465
533
|
assert.match(
|
|
466
534
|
templateSource,
|
|
467
|
-
/from "@jskit-ai\/crud-core\/server\/
|
|
535
|
+
/from "@jskit-ai\/crud-core\/server\/serviceMethods";/
|
|
468
536
|
);
|
|
469
|
-
assert.match(templateSource, /const
|
|
470
|
-
assert.match(templateSource, /const
|
|
537
|
+
assert.match(templateSource, /const serviceRuntime = createCrudServiceRuntime\(resource,/);
|
|
538
|
+
assert.match(templateSource, /const baseServiceEvents = createCrudServiceEvents\(resource,/);
|
|
471
539
|
assert.match(templateSource, /const serviceEvents = Object\.freeze\(\{/);
|
|
472
540
|
assert.match(templateSource, /createRecord: \[\.\.\.baseServiceEvents\.createRecord\],/);
|
|
541
|
+
assert.match(templateSource, /function createService\(\{ \$\{option:namespace\|camel\}Repository, fieldAccess = DEFAULT_FIELD_ACCESS \} = \{\}\)/);
|
|
473
542
|
assert.match(templateSource, /async function listRecords\(query = \{\}, options = \{\}\)/);
|
|
474
|
-
assert.match(templateSource, /return
|
|
475
|
-
assert.match(templateSource, /
|
|
476
|
-
assert.match(templateSource, /
|
|
543
|
+
assert.match(templateSource, /return crudServiceListRecords\(serviceRuntime, \$\{option:namespace\|camel\}Repository, fieldAccess, query, options\);/);
|
|
544
|
+
assert.match(templateSource, /async function updateRecord\(recordId, payload = \{\}, options = \{\}\)/);
|
|
545
|
+
assert.match(templateSource, /return crudServiceUpdateRecord\(serviceRuntime, \$\{option:namespace\|camel\}Repository, fieldAccess, recordId, payload, options\);/);
|
|
546
|
+
assert.match(templateSource, /return Object\.freeze\(\{/);
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
test("crud generator renders time columns with html-time-compatible schemas", async () => {
|
|
550
|
+
const testDirectory = path.dirname(fileURLToPath(import.meta.url));
|
|
551
|
+
const templatePath = path.resolve(testDirectory, "..", "src", "server", "buildTemplateContext.js");
|
|
552
|
+
const templateSource = await readFile(templatePath, "utf8");
|
|
553
|
+
|
|
554
|
+
assert.match(
|
|
555
|
+
templateSource,
|
|
556
|
+
/NULLABLE_HTML_TIME_STRING_SCHEMA/
|
|
557
|
+
);
|
|
558
|
+
assert.match(
|
|
559
|
+
templateSource,
|
|
560
|
+
/HTML_TIME_STRING_SCHEMA/
|
|
561
|
+
);
|
|
562
|
+
assert.doesNotMatch(templateSource, /format: "time"/);
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
test("buildReplacementsFromSnapshot uses shared framework time schemas in generated resources", () => {
|
|
566
|
+
const snapshot = createSnapshot({
|
|
567
|
+
tableName: "opening_hours"
|
|
568
|
+
});
|
|
569
|
+
const timeColumn = Object.freeze({
|
|
570
|
+
name: "from_time",
|
|
571
|
+
key: "fromTime",
|
|
572
|
+
dataType: "time",
|
|
573
|
+
columnType: "time",
|
|
574
|
+
typeKind: "time",
|
|
575
|
+
nullable: true,
|
|
576
|
+
hasDefault: false,
|
|
577
|
+
defaultValue: null,
|
|
578
|
+
autoIncrement: false,
|
|
579
|
+
unsigned: false,
|
|
580
|
+
extra: "",
|
|
581
|
+
maxLength: null,
|
|
582
|
+
numericPrecision: null,
|
|
583
|
+
numericScale: null,
|
|
584
|
+
enumValues: Object.freeze([])
|
|
585
|
+
});
|
|
586
|
+
const replacements = __testables.buildReplacementsFromSnapshot({
|
|
587
|
+
snapshot: {
|
|
588
|
+
...snapshot,
|
|
589
|
+
columns: Object.freeze([...snapshot.columns, timeColumn])
|
|
590
|
+
},
|
|
591
|
+
resolvedOwnershipFilter: "workspace_user"
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
assert.match(
|
|
595
|
+
replacements.__JSKIT_CRUD_RESOURCE_VALIDATORS_IMPORT__,
|
|
596
|
+
/NULLABLE_HTML_TIME_STRING_SCHEMA/
|
|
597
|
+
);
|
|
598
|
+
assert.match(
|
|
599
|
+
replacements.__JSKIT_CRUD_RESOURCE_OUTPUT_SCHEMA_PROPERTIES__,
|
|
600
|
+
/fromTime: NULLABLE_HTML_TIME_STRING_SCHEMA/
|
|
601
|
+
);
|
|
602
|
+
assert.match(
|
|
603
|
+
replacements.__JSKIT_CRUD_RESOURCE_CREATE_SCHEMA_PROPERTIES__,
|
|
604
|
+
/fromTime: NULLABLE_HTML_TIME_STRING_SCHEMA/
|
|
605
|
+
);
|
|
606
|
+
assert.doesNotMatch(
|
|
607
|
+
replacements.__JSKIT_CRUD_RESOURCE_OUTPUT_SCHEMA_PROPERTIES__,
|
|
608
|
+
/Type\.String\(\{ pattern:/
|
|
609
|
+
);
|
|
477
610
|
});
|
|
478
611
|
|
|
479
612
|
test("crud provider template uses shared lookup provider helpers instead of inline wiring", async () => {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import test, { after } from "node:test";
|
|
2
2
|
import assert from "node:assert/strict";
|
|
3
3
|
import { createTemplateServerFixture } from "../test-support/templateServerFixture.js";
|
|
4
|
+
import descriptor from "../package.descriptor.mjs";
|
|
4
5
|
|
|
5
6
|
const fixture = await createTemplateServerFixture();
|
|
6
7
|
const { createActions } = await fixture.importServerModule("actions.js");
|
|
@@ -53,3 +54,37 @@ test("template createActions requires explicit surface", () => {
|
|
|
53
54
|
/requires a non-empty surface/
|
|
54
55
|
);
|
|
55
56
|
});
|
|
57
|
+
|
|
58
|
+
test("template createActions requires namespaced CRUD permissions by default", () => {
|
|
59
|
+
const actions = createActions({ surface: "admin" });
|
|
60
|
+
|
|
61
|
+
assert.deepEqual(
|
|
62
|
+
actions.map((action) => action.permission),
|
|
63
|
+
[
|
|
64
|
+
{ require: "all", permissions: ["crud.customers.list"] },
|
|
65
|
+
{ require: "all", permissions: ["crud.customers.view"] },
|
|
66
|
+
{ require: "all", permissions: ["crud.customers.create"] },
|
|
67
|
+
{ require: "all", permissions: ["crud.customers.update"] },
|
|
68
|
+
{ require: "all", permissions: ["crud.customers.delete"] }
|
|
69
|
+
]
|
|
70
|
+
);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("crud generator appends member role grants for generated CRUD permissions", () => {
|
|
74
|
+
assert.deepEqual(
|
|
75
|
+
descriptor.mutations.text,
|
|
76
|
+
[
|
|
77
|
+
{
|
|
78
|
+
op: "append-text",
|
|
79
|
+
file: "config/roles.js",
|
|
80
|
+
position: "bottom",
|
|
81
|
+
skipIfContains: "\"crud.${option:namespace|snake}.list\"",
|
|
82
|
+
value:
|
|
83
|
+
"\nroleCatalog.roles.member.permissions.push(\n \"crud.${option:namespace|snake}.list\",\n \"crud.${option:namespace|snake}.view\",\n \"crud.${option:namespace|snake}.create\",\n \"crud.${option:namespace|snake}.update\",\n \"crud.${option:namespace|snake}.delete\"\n);\n",
|
|
84
|
+
reason: "Grant generated CRUD action permissions to the default member role in the app-owned role catalog.",
|
|
85
|
+
category: "crud",
|
|
86
|
+
id: "crud-role-catalog-permissions-${option:namespace|snake}"
|
|
87
|
+
}
|
|
88
|
+
]
|
|
89
|
+
);
|
|
90
|
+
});
|
package/test/crudService.test.js
CHANGED
|
@@ -47,6 +47,7 @@ test("crudService delegates CRUD operations to the repository", async () => {
|
|
|
47
47
|
["list", { limit: 10 }],
|
|
48
48
|
["findById", 3],
|
|
49
49
|
["create", { textField: "Example", dateField: "2026-03-11", numberField: 3 }],
|
|
50
|
+
["findById", 4],
|
|
50
51
|
["updateById", 4, { textField: "Changed" }],
|
|
51
52
|
["deleteById", 5]
|
|
52
53
|
]);
|
|
@@ -95,6 +96,48 @@ test("crudService exports default realtime events for create/update/delete", ()
|
|
|
95
96
|
assert.equal(serviceEvents.deleteRecord[0].realtime.event, "customers.record.changed");
|
|
96
97
|
});
|
|
97
98
|
|
|
99
|
+
test("crudService passes existing records into patch normalization via the shared CRUD service", async () => {
|
|
100
|
+
const calls = [];
|
|
101
|
+
const service = createService({
|
|
102
|
+
customersRepository: {
|
|
103
|
+
async list() {
|
|
104
|
+
return { items: [], nextCursor: null };
|
|
105
|
+
},
|
|
106
|
+
async findById(recordId) {
|
|
107
|
+
calls.push(["findById", recordId]);
|
|
108
|
+
return {
|
|
109
|
+
id: recordId,
|
|
110
|
+
textField: "Existing",
|
|
111
|
+
dateField: "2026-03-11T00:00:00.000Z",
|
|
112
|
+
numberField: 3
|
|
113
|
+
};
|
|
114
|
+
},
|
|
115
|
+
async create(payload) {
|
|
116
|
+
return { id: 1, ...payload };
|
|
117
|
+
},
|
|
118
|
+
async updateById(recordId, payload) {
|
|
119
|
+
calls.push(["updateById", recordId, payload]);
|
|
120
|
+
return {
|
|
121
|
+
id: recordId,
|
|
122
|
+
textField: payload.textField || "",
|
|
123
|
+
dateField: "2026-03-11T00:00:00.000Z",
|
|
124
|
+
numberField: payload.numberField ?? 0
|
|
125
|
+
};
|
|
126
|
+
},
|
|
127
|
+
async deleteById(recordId) {
|
|
128
|
+
return { id: recordId, deleted: true };
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
await service.updateRecord(4, { textField: "Changed" }, {});
|
|
134
|
+
|
|
135
|
+
assert.deepEqual(calls, [
|
|
136
|
+
["findById", 4],
|
|
137
|
+
["updateById", 4, { textField: "Changed" }]
|
|
138
|
+
]);
|
|
139
|
+
});
|
|
140
|
+
|
|
98
141
|
test("crudService supports optional fieldAccess hooks for writable filtering", async () => {
|
|
99
142
|
const calls = [];
|
|
100
143
|
const service = createService({
|
|
@@ -18,7 +18,15 @@ const TEMPLATE_REPLACEMENTS = Object.freeze([
|
|
|
18
18
|
["${option:namespace|camel}", CRUD_NAMESPACE.camel],
|
|
19
19
|
["${option:namespace|singular|camel}", CRUD_NAMESPACE.singularCamel],
|
|
20
20
|
["${option:namespace|pascal}", CRUD_NAMESPACE.pascal],
|
|
21
|
-
["__JSKIT_CRUD_ID_COLUMN__", JSON.stringify("id")]
|
|
21
|
+
["__JSKIT_CRUD_ID_COLUMN__", JSON.stringify("id")],
|
|
22
|
+
[
|
|
23
|
+
"__JSKIT_CRUD_LIST_CONFIG_LINES__",
|
|
24
|
+
[
|
|
25
|
+
" // defaultLimit: 20,",
|
|
26
|
+
" // maxLimit: 100,",
|
|
27
|
+
" // searchColumns: [\"name\"],"
|
|
28
|
+
].join("\n")
|
|
29
|
+
]
|
|
22
30
|
]);
|
|
23
31
|
|
|
24
32
|
function applyTemplateReplacements(sourceText = "") {
|
|
@@ -145,7 +153,7 @@ async function createTemplateServerFixture() {
|
|
|
145
153
|
);
|
|
146
154
|
await writeFile(path.join(sharedRoot, "customerResource.js"), buildResourceStubSource(), "utf8");
|
|
147
155
|
|
|
148
|
-
for (const fileName of ["actionIds.js", "actions.js", "registerRoutes.js", "repository.js", "service.js"]) {
|
|
156
|
+
for (const fileName of ["actionIds.js", "actions.js", "listConfig.js", "registerRoutes.js", "repository.js", "service.js"]) {
|
|
149
157
|
await renderServerTemplateFile(serverRoot, fileName);
|
|
150
158
|
}
|
|
151
159
|
|