@jskit-ai/crud-server-generator 0.1.30 → 0.1.32
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 +52 -11
- package/package.json +6 -6
- package/src/server/buildTemplateContext.js +204 -32
- package/src/server/subcommands/addField.js +9 -9
- package/src/server/subcommands/resourceAst.js +13 -13
- package/src/shared/crud/crudResource.js +17 -0
- package/templates/src/local-package/server/CrudProvider.js +3 -1
- package/templates/src/local-package/server/actions.js +22 -8
- 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 -10
- package/templates/src/local-package/server/service.js +15 -62
- package/templates/src/local-package/shared/crudResource.js +18 -4
- package/templates/src/local-package/shared/index.js +1 -1
- package/test/addFieldSubcommand.test.js +4 -4
- package/test/buildTemplateContext.test.js +186 -9
- package/test/crudServerGuards.test.js +35 -0
- package/test/crudService.test.js +43 -0
- package/test/templateSymbolConsistency.test.js +40 -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.32",
|
|
5
5
|
kind: "generator",
|
|
6
6
|
description: "CRUD server generator with routes, actions, and persistence scaffolding.",
|
|
7
7
|
options: {
|
|
@@ -80,10 +80,39 @@ export default Object.freeze({
|
|
|
80
80
|
}
|
|
81
81
|
},
|
|
82
82
|
metadata: {
|
|
83
|
+
generatorPrimarySubcommand: "scaffold",
|
|
83
84
|
generatorSubcommands: {
|
|
84
|
-
"
|
|
85
|
+
"scaffold": {
|
|
86
|
+
description: "Scaffold a CRUD resource package for a table (same behavior as running generate with no subcommand).",
|
|
87
|
+
optionNames: [
|
|
88
|
+
"namespace",
|
|
89
|
+
"surface",
|
|
90
|
+
"ownership-filter",
|
|
91
|
+
"table-name",
|
|
92
|
+
"id-column",
|
|
93
|
+
"directory-prefix"
|
|
94
|
+
]
|
|
95
|
+
},
|
|
96
|
+
"scaffold-field": {
|
|
85
97
|
entrypoint: "src/server/subcommands/addField.js",
|
|
86
|
-
export: "runGeneratorSubcommand"
|
|
98
|
+
export: "runGeneratorSubcommand",
|
|
99
|
+
description: "Patch one writable field into an existing generated CRUD resource module.",
|
|
100
|
+
positionalArgs: [
|
|
101
|
+
{
|
|
102
|
+
name: "<fieldKey>",
|
|
103
|
+
required: true,
|
|
104
|
+
description: "Resource field key (camelCase) resolved from the DB snapshot."
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
name: "<targetFile>",
|
|
108
|
+
required: true,
|
|
109
|
+
description: "Path to the generated CRUD resource file relative to app root."
|
|
110
|
+
}
|
|
111
|
+
],
|
|
112
|
+
optionNames: [
|
|
113
|
+
"table-name",
|
|
114
|
+
"id-column"
|
|
115
|
+
]
|
|
87
116
|
}
|
|
88
117
|
},
|
|
89
118
|
apiSummary: {
|
|
@@ -105,13 +134,13 @@ export default Object.freeze({
|
|
|
105
134
|
mutations: {
|
|
106
135
|
dependencies: {
|
|
107
136
|
runtime: {
|
|
108
|
-
"@jskit-ai/auth-core": "0.1.
|
|
109
|
-
"@jskit-ai/crud-core": "0.1.
|
|
110
|
-
"@jskit-ai/database-runtime": "0.1.
|
|
111
|
-
"@jskit-ai/http-runtime": "0.1.
|
|
112
|
-
"@jskit-ai/kernel": "0.1.
|
|
113
|
-
"@jskit-ai/realtime": "0.1.
|
|
114
|
-
"@jskit-ai/users-core": "0.1.
|
|
137
|
+
"@jskit-ai/auth-core": "0.1.23",
|
|
138
|
+
"@jskit-ai/crud-core": "0.1.32",
|
|
139
|
+
"@jskit-ai/database-runtime": "0.1.24",
|
|
140
|
+
"@jskit-ai/http-runtime": "0.1.23",
|
|
141
|
+
"@jskit-ai/kernel": "0.1.24",
|
|
142
|
+
"@jskit-ai/realtime": "0.1.23",
|
|
143
|
+
"@jskit-ai/users-core": "0.1.33",
|
|
115
144
|
"@local/${option:namespace|kebab}": "file:packages/${option:namespace|kebab}",
|
|
116
145
|
"typebox": "^1.0.81"
|
|
117
146
|
},
|
|
@@ -218,6 +247,18 @@ export default Object.freeze({
|
|
|
218
247
|
}
|
|
219
248
|
}
|
|
220
249
|
],
|
|
221
|
-
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
|
+
]
|
|
222
263
|
}
|
|
223
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.32",
|
|
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.32",
|
|
17
|
+
"@jskit-ai/database-runtime": "0.1.24",
|
|
18
|
+
"@jskit-ai/http-runtime": "0.1.23",
|
|
19
|
+
"@jskit-ai/kernel": "0.1.24",
|
|
20
|
+
"@jskit-ai/users-core": "0.1.33",
|
|
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;
|
|
@@ -751,32 +764,34 @@ function renderMigrationForeignKeyLines(snapshot) {
|
|
|
751
764
|
return lines.join("\n");
|
|
752
765
|
}
|
|
753
766
|
|
|
754
|
-
function mergeFieldMetaEntries(
|
|
767
|
+
function mergeFieldMetaEntries(...entryGroups) {
|
|
755
768
|
const mergedByKey = new Map();
|
|
756
|
-
for (const
|
|
757
|
-
const
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
if (existing.relation || sourceEntry.relation) {
|
|
768
|
-
next.relation = {
|
|
769
|
-
...(existing.relation && typeof existing.relation === "object" ? existing.relation : {}),
|
|
770
|
-
...(sourceEntry.relation && typeof sourceEntry.relation === "object" ? sourceEntry.relation : {})
|
|
771
|
-
};
|
|
772
|
-
}
|
|
773
|
-
if (existing.ui || sourceEntry.ui) {
|
|
774
|
-
next.ui = {
|
|
775
|
-
...(existing.ui && typeof existing.ui === "object" ? existing.ui : {}),
|
|
776
|
-
...(sourceEntry.ui && typeof sourceEntry.ui === "object" ? sourceEntry.ui : {})
|
|
769
|
+
for (const sourceEntries of entryGroups) {
|
|
770
|
+
for (const sourceEntry of Array.isArray(sourceEntries) ? sourceEntries : []) {
|
|
771
|
+
const key = normalizeText(sourceEntry?.key);
|
|
772
|
+
if (!key) {
|
|
773
|
+
continue;
|
|
774
|
+
}
|
|
775
|
+
const existing = mergedByKey.get(key) || {};
|
|
776
|
+
const next = {
|
|
777
|
+
...existing,
|
|
778
|
+
...sourceEntry,
|
|
779
|
+
key
|
|
777
780
|
};
|
|
781
|
+
if (existing.relation || sourceEntry.relation) {
|
|
782
|
+
next.relation = {
|
|
783
|
+
...(existing.relation && typeof existing.relation === "object" ? existing.relation : {}),
|
|
784
|
+
...(sourceEntry.relation && typeof sourceEntry.relation === "object" ? sourceEntry.relation : {})
|
|
785
|
+
};
|
|
786
|
+
}
|
|
787
|
+
if (existing.ui || sourceEntry.ui) {
|
|
788
|
+
next.ui = {
|
|
789
|
+
...(existing.ui && typeof existing.ui === "object" ? existing.ui : {}),
|
|
790
|
+
...(sourceEntry.ui && typeof sourceEntry.ui === "object" ? sourceEntry.ui : {})
|
|
791
|
+
};
|
|
792
|
+
}
|
|
793
|
+
mergedByKey.set(key, next);
|
|
778
794
|
}
|
|
779
|
-
mergedByKey.set(key, next);
|
|
780
795
|
}
|
|
781
796
|
|
|
782
797
|
return [...mergedByKey.values()].sort((left, right) => left.key.localeCompare(right.key));
|
|
@@ -791,6 +806,87 @@ function resolveLookupNamespaceFromTableName(tableName = "") {
|
|
|
791
806
|
return normalizedTableName.replace(/_/g, "-");
|
|
792
807
|
}
|
|
793
808
|
|
|
809
|
+
function toFieldLabel(key = "") {
|
|
810
|
+
const normalizedKey = normalizeText(key);
|
|
811
|
+
if (!normalizedKey) {
|
|
812
|
+
return "";
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
const words = normalizedKey
|
|
816
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1 $2")
|
|
817
|
+
.replace(/[_\-.]+/g, " ")
|
|
818
|
+
.split(/\s+/)
|
|
819
|
+
.map((entry) => normalizeText(entry))
|
|
820
|
+
.filter(Boolean);
|
|
821
|
+
if (words.length < 1) {
|
|
822
|
+
return "";
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
return words
|
|
826
|
+
.map((entry) => `${entry.slice(0, 1).toUpperCase()}${entry.slice(1)}`)
|
|
827
|
+
.join(" ");
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
function isSupportedSelectOptionValue(value) {
|
|
831
|
+
return typeof value === "string" || typeof value === "number" || typeof value === "boolean";
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
function toSelectOptionIdentity(value) {
|
|
835
|
+
return `${typeof value}:${String(value)}`;
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
function toSelectOptionLabel(value) {
|
|
839
|
+
if (typeof value === "string") {
|
|
840
|
+
return toFieldLabel(value) || value;
|
|
841
|
+
}
|
|
842
|
+
if (typeof value === "number" || typeof value === "boolean") {
|
|
843
|
+
return String(value);
|
|
844
|
+
}
|
|
845
|
+
return "";
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
function normalizeFieldMetaUiOptions(rawOptions = []) {
|
|
849
|
+
if (!Array.isArray(rawOptions)) {
|
|
850
|
+
return [];
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
const options = [];
|
|
854
|
+
const seenValues = new Set();
|
|
855
|
+
for (const rawEntry of rawOptions) {
|
|
856
|
+
if (!rawEntry || typeof rawEntry !== "object" || Array.isArray(rawEntry)) {
|
|
857
|
+
continue;
|
|
858
|
+
}
|
|
859
|
+
const value = rawEntry.value;
|
|
860
|
+
if (!isSupportedSelectOptionValue(value)) {
|
|
861
|
+
continue;
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
const identity = toSelectOptionIdentity(value);
|
|
865
|
+
if (seenValues.has(identity)) {
|
|
866
|
+
continue;
|
|
867
|
+
}
|
|
868
|
+
seenValues.add(identity);
|
|
869
|
+
|
|
870
|
+
const explicitLabel = normalizeText(rawEntry.label);
|
|
871
|
+
options.push({
|
|
872
|
+
value,
|
|
873
|
+
label: explicitLabel || toSelectOptionLabel(value) || String(value)
|
|
874
|
+
});
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
return options;
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
function resolveEnumFieldMetaUiOptions(enumValues = []) {
|
|
881
|
+
const options = Array.isArray(enumValues)
|
|
882
|
+
? enumValues.map((value) => ({
|
|
883
|
+
value,
|
|
884
|
+
label: toSelectOptionLabel(value)
|
|
885
|
+
}))
|
|
886
|
+
: [];
|
|
887
|
+
return normalizeFieldMetaUiOptions(options);
|
|
888
|
+
}
|
|
889
|
+
|
|
794
890
|
function buildFieldMetaEntries({ outputColumns = [], writableColumns = [], snapshot = {} } = {}) {
|
|
795
891
|
const fieldColumns = [...outputColumns, ...writableColumns];
|
|
796
892
|
const fieldColumnsByName = new Map();
|
|
@@ -858,7 +954,33 @@ function buildFieldMetaEntries({ outputColumns = [], writableColumns = [], snaps
|
|
|
858
954
|
});
|
|
859
955
|
}
|
|
860
956
|
|
|
861
|
-
|
|
957
|
+
const relationFieldKeys = new Set(
|
|
958
|
+
relationEntries
|
|
959
|
+
.map((entry) => normalizeText(entry?.key))
|
|
960
|
+
.filter(Boolean)
|
|
961
|
+
);
|
|
962
|
+
const enumEntries = [];
|
|
963
|
+
for (const column of fieldColumnsByKey.values()) {
|
|
964
|
+
const key = normalizeText(column?.key);
|
|
965
|
+
if (!key || relationFieldKeys.has(key)) {
|
|
966
|
+
continue;
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
const options = resolveEnumFieldMetaUiOptions(column?.enumValues);
|
|
970
|
+
if (options.length < 1) {
|
|
971
|
+
continue;
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
enumEntries.push({
|
|
975
|
+
key,
|
|
976
|
+
ui: {
|
|
977
|
+
formControl: "select",
|
|
978
|
+
options
|
|
979
|
+
}
|
|
980
|
+
});
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
return mergeFieldMetaEntries(dbColumnEntries, relationEntries, enumEntries);
|
|
862
984
|
}
|
|
863
985
|
|
|
864
986
|
function renderFieldMetaEntryLines(entry = {}) {
|
|
@@ -896,17 +1018,39 @@ function renderFieldMetaEntryLines(entry = {}) {
|
|
|
896
1018
|
topLevelProperties.push(relationLines.join("\n"));
|
|
897
1019
|
}
|
|
898
1020
|
|
|
1021
|
+
const fieldUiOptions = normalizeFieldMetaUiOptions(entry?.ui?.options);
|
|
899
1022
|
const formControl = checkCrudLookupFormControl(entry?.ui?.formControl, {
|
|
900
1023
|
context: `resource.fieldMeta["${normalizeText(entry.key)}"].ui.formControl`,
|
|
901
|
-
defaultValue: relation ? "autocomplete" : ""
|
|
1024
|
+
defaultValue: relation ? "autocomplete" : (fieldUiOptions.length > 0 ? "select" : "")
|
|
902
1025
|
});
|
|
903
|
-
if (formControl) {
|
|
1026
|
+
if (formControl || fieldUiOptions.length > 0) {
|
|
1027
|
+
const uiPropertyBlocks = [];
|
|
1028
|
+
if (formControl) {
|
|
1029
|
+
uiPropertyBlocks.push([
|
|
1030
|
+
`formControl: ${JSON.stringify(formControl)}${relation ? " // or \"select\"" : ""}`
|
|
1031
|
+
]);
|
|
1032
|
+
}
|
|
1033
|
+
if (fieldUiOptions.length > 0) {
|
|
1034
|
+
const optionsJsonLines = JSON.stringify(fieldUiOptions, null, 2).split("\n");
|
|
1035
|
+
const optionPropertyLines = [`options: ${optionsJsonLines[0]}`];
|
|
1036
|
+
for (const jsonLine of optionsJsonLines.slice(1)) {
|
|
1037
|
+
optionPropertyLines.push(jsonLine);
|
|
1038
|
+
}
|
|
1039
|
+
uiPropertyBlocks.push(optionPropertyLines);
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
const uiLines = ["ui: {"];
|
|
1043
|
+
for (const [propertyIndex, propertyLines] of uiPropertyBlocks.entries()) {
|
|
1044
|
+
const isLastProperty = propertyIndex >= uiPropertyBlocks.length - 1;
|
|
1045
|
+
const propertySuffix = isLastProperty ? "" : ",";
|
|
1046
|
+
for (const [lineIndex, line] of propertyLines.entries()) {
|
|
1047
|
+
const isLastLine = lineIndex >= propertyLines.length - 1;
|
|
1048
|
+
uiLines.push(` ${line}${isLastLine ? propertySuffix : ""}`);
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
uiLines.push("}");
|
|
904
1052
|
topLevelProperties.push(
|
|
905
|
-
|
|
906
|
-
"ui: {",
|
|
907
|
-
` formControl: ${JSON.stringify(formControl)} // or "select"`,
|
|
908
|
-
"}"
|
|
909
|
-
].join("\n")
|
|
1053
|
+
uiLines.join("\n")
|
|
910
1054
|
);
|
|
911
1055
|
}
|
|
912
1056
|
|
|
@@ -933,6 +1077,29 @@ function renderResourceFieldMetaPushLines(entries = []) {
|
|
|
933
1077
|
return sourceEntries.map((entry) => renderFieldMetaEntryLines(entry)).join("\n\n");
|
|
934
1078
|
}
|
|
935
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
|
+
|
|
936
1103
|
function buildReplacementsFromSnapshot({
|
|
937
1104
|
snapshot,
|
|
938
1105
|
resolvedOwnershipFilter
|
|
@@ -959,6 +1126,7 @@ function buildReplacementsFromSnapshot({
|
|
|
959
1126
|
const needsNullableDateInput = writableColumns.some(
|
|
960
1127
|
(column) => column.typeKind === "date" && column.nullable === true
|
|
961
1128
|
);
|
|
1129
|
+
const needsHtmlTimeSchemas = resourceColumns.some((column) => column.typeKind === "time");
|
|
962
1130
|
const needsDate = resourceColumns.some((column) => column.typeKind === "date");
|
|
963
1131
|
const needsJson = resourceColumns.some((column) => column.typeKind === "json");
|
|
964
1132
|
const needsNormalizeText = resourceColumns.some((column) =>
|
|
@@ -976,6 +1144,9 @@ function buildReplacementsFromSnapshot({
|
|
|
976
1144
|
__JSKIT_CRUD_TABLE_NAME__: JSON.stringify(snapshot.tableName),
|
|
977
1145
|
__JSKIT_CRUD_ID_COLUMN__: JSON.stringify(snapshot.idColumn || DEFAULT_ID_COLUMN),
|
|
978
1146
|
__JSKIT_CRUD_RESOLVED_OWNERSHIP_FILTER__: resolvedOwnershipFilter,
|
|
1147
|
+
__JSKIT_CRUD_RESOURCE_VALIDATORS_IMPORT__: renderResourceValidatorsImport({
|
|
1148
|
+
needsHtmlTimeSchemas
|
|
1149
|
+
}),
|
|
979
1150
|
__JSKIT_CRUD_RESOURCE_DATABASE_RUNTIME_IMPORT__: renderResourceDatabaseRuntimeImport({
|
|
980
1151
|
needsToIsoString: needsDateTimeOutput || needsDate,
|
|
981
1152
|
needsToDatabaseDateTimeUtc: needsDateTimeInput
|
|
@@ -1002,6 +1173,7 @@ function buildReplacementsFromSnapshot({
|
|
|
1002
1173
|
__JSKIT_CRUD_RESOURCE_OUTPUT_NORMALIZATION_LINES__: renderResourceOutputNormalizationLines(outputColumns),
|
|
1003
1174
|
__JSKIT_CRUD_RESOURCE_CREATE_REQUIRED_FIELDS__: JSON.stringify(createRequiredFieldKeys),
|
|
1004
1175
|
__JSKIT_CRUD_RESOURCE_FIELD_META_PUSH_LINES__: renderResourceFieldMetaPushLines(fieldMetaEntries),
|
|
1176
|
+
__JSKIT_CRUD_LIST_CONFIG_LINES__: renderRepositoryListConfigLines(snapshot),
|
|
1005
1177
|
__JSKIT_CRUD_MIGRATION_COLUMN_LINES__: renderMigrationColumnLines(snapshot),
|
|
1006
1178
|
__JSKIT_CRUD_MIGRATION_INDEX_LINES__: renderMigrationIndexLines(snapshot),
|
|
1007
1179
|
__JSKIT_CRUD_MIGRATION_FOREIGN_KEY_LINES__: renderMigrationForeignKeyLines(snapshot)
|
|
@@ -35,12 +35,12 @@ function toPosixPath(value = "") {
|
|
|
35
35
|
function resolveTargetFilePath(appRoot, targetFile) {
|
|
36
36
|
const appRootAbsolute = path.resolve(String(appRoot || ""));
|
|
37
37
|
if (!appRootAbsolute) {
|
|
38
|
-
throw new Error("crud-server-generator
|
|
38
|
+
throw new Error("crud-server-generator scaffold-field requires appRoot.");
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
const normalizedTargetFile = normalizeText(targetFile);
|
|
42
42
|
if (!normalizedTargetFile) {
|
|
43
|
-
throw new Error("crud-server-generator
|
|
43
|
+
throw new Error("crud-server-generator scaffold-field requires target file path.");
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
const absolutePath = path.isAbsolute(normalizedTargetFile)
|
|
@@ -53,7 +53,7 @@ function resolveTargetFilePath(appRoot, targetFile) {
|
|
|
53
53
|
relativePath.startsWith(`..${path.sep}`) ||
|
|
54
54
|
path.isAbsolute(relativePath)
|
|
55
55
|
) {
|
|
56
|
-
throw new Error("crud-server-generator
|
|
56
|
+
throw new Error("crud-server-generator scaffold-field target file must stay within app root.");
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
return {
|
|
@@ -68,10 +68,10 @@ function parseSubcommandArgs(args = []) {
|
|
|
68
68
|
const targetFile = normalizeText(source[1]);
|
|
69
69
|
|
|
70
70
|
if (!fieldKey) {
|
|
71
|
-
throw new Error("crud-server-generator
|
|
71
|
+
throw new Error("crud-server-generator scaffold-field requires <fieldKey>.");
|
|
72
72
|
}
|
|
73
73
|
if (!targetFile) {
|
|
74
|
-
throw new Error("crud-server-generator
|
|
74
|
+
throw new Error("crud-server-generator scaffold-field requires <targetFile>.");
|
|
75
75
|
}
|
|
76
76
|
|
|
77
77
|
return {
|
|
@@ -80,7 +80,7 @@ function parseSubcommandArgs(args = []) {
|
|
|
80
80
|
};
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
-
function resolveRequestedTableConfig(source = "", options = {}, context = "crud-server-generator
|
|
83
|
+
function resolveRequestedTableConfig(source = "", options = {}, context = "crud-server-generator scaffold-field") {
|
|
84
84
|
const defaults = resolveCrudResourceDefaults(source, context);
|
|
85
85
|
const tableName = normalizeText(options?.["table-name"] || defaults.tableName);
|
|
86
86
|
if (!tableName) {
|
|
@@ -110,7 +110,7 @@ function resolveColumnForField(snapshot = {}, fieldKey = "", { idColumn = "id" }
|
|
|
110
110
|
.filter(Boolean)
|
|
111
111
|
.join(", ");
|
|
112
112
|
throw new Error(
|
|
113
|
-
`crud-server-generator
|
|
113
|
+
`crud-server-generator scaffold-field could not find field "${key}" in DB snapshot columns. Available: ${available || "<none>"}.`
|
|
114
114
|
);
|
|
115
115
|
}
|
|
116
116
|
|
|
@@ -178,7 +178,7 @@ async function runGeneratorSubcommand({
|
|
|
178
178
|
resolveSnapshot = resolveGenerationSnapshot
|
|
179
179
|
} = {}) {
|
|
180
180
|
const normalizedSubcommand = normalizeText(subcommand).toLowerCase();
|
|
181
|
-
if (normalizedSubcommand !== "
|
|
181
|
+
if (normalizedSubcommand !== "scaffold-field") {
|
|
182
182
|
throw new Error(`Unsupported crud-server-generator subcommand: ${normalizedSubcommand || "<empty>"}.`);
|
|
183
183
|
}
|
|
184
184
|
|
|
@@ -197,7 +197,7 @@ async function runGeneratorSubcommand({
|
|
|
197
197
|
const column = resolveColumnForField(snapshot, fieldKey, { idColumn });
|
|
198
198
|
if (column?.writable !== true) {
|
|
199
199
|
throw new Error(
|
|
200
|
-
`crud-server-generator
|
|
200
|
+
`crud-server-generator scaffold-field cannot patch non-writable field "${fieldKey}" (column "${column.name}").`
|
|
201
201
|
);
|
|
202
202
|
}
|
|
203
203
|
|
|
@@ -37,7 +37,7 @@ function isIdentifierName(value = "") {
|
|
|
37
37
|
return IDENTIFIER_PATTERN.test(String(value || ""));
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
function parseModule(source = "", context = "crud-server-generator
|
|
40
|
+
function parseModule(source = "", context = "crud-server-generator scaffold-field") {
|
|
41
41
|
try {
|
|
42
42
|
return recast.parse(String(source || ""), { parser: BABEL_REC_AST_PARSER });
|
|
43
43
|
} catch (error) {
|
|
@@ -47,7 +47,7 @@ function parseModule(source = "", context = "crud-server-generator add-field") {
|
|
|
47
47
|
}
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
function parseExpression(source = "", context = "crud-server-generator
|
|
50
|
+
function parseExpression(source = "", context = "crud-server-generator scaffold-field") {
|
|
51
51
|
const expressionSource = `const __jskitFieldExpression = ${String(source || "")};`;
|
|
52
52
|
const ast = parseModule(expressionSource, context);
|
|
53
53
|
const statement = ast?.program?.body?.[0];
|
|
@@ -61,7 +61,7 @@ function parseExpression(source = "", context = "crud-server-generator add-field
|
|
|
61
61
|
return declaration.init;
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
function parseStatement(source = "", context = "crud-server-generator
|
|
64
|
+
function parseStatement(source = "", context = "crud-server-generator scaffold-field") {
|
|
65
65
|
const ast = parseModule(String(source || ""), context);
|
|
66
66
|
const statement = ast?.program?.body?.[0];
|
|
67
67
|
if (!statement) {
|
|
@@ -114,7 +114,7 @@ function findVariableDeclarator(programNode, variableName = "") {
|
|
|
114
114
|
return null;
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
-
function requireVariableDeclarator(programNode, variableName = "", context = "crud-server-generator
|
|
117
|
+
function requireVariableDeclarator(programNode, variableName = "", context = "crud-server-generator scaffold-field") {
|
|
118
118
|
const declaration = findVariableDeclarator(programNode, variableName);
|
|
119
119
|
if (declaration) {
|
|
120
120
|
return declaration;
|
|
@@ -122,7 +122,7 @@ function requireVariableDeclarator(programNode, variableName = "", context = "cr
|
|
|
122
122
|
throw new Error(`${context} could not find const ${variableName}.`);
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
-
function requireSchemaPropertiesObject(programNode, variableName = "", context = "crud-server-generator
|
|
125
|
+
function requireSchemaPropertiesObject(programNode, variableName = "", context = "crud-server-generator scaffold-field") {
|
|
126
126
|
const declaration = requireVariableDeclarator(programNode, variableName, context);
|
|
127
127
|
const initExpression = declaration.init;
|
|
128
128
|
if (!n.CallExpression.check(initExpression)) {
|
|
@@ -149,7 +149,7 @@ function requireSchemaPropertiesObject(programNode, variableName = "", context =
|
|
|
149
149
|
return firstArgument;
|
|
150
150
|
}
|
|
151
151
|
|
|
152
|
-
function requireObjectFreezePayloadObject(programNode, variableName = "", context = "crud-server-generator
|
|
152
|
+
function requireObjectFreezePayloadObject(programNode, variableName = "", context = "crud-server-generator scaffold-field") {
|
|
153
153
|
const declaration = requireVariableDeclarator(programNode, variableName, context);
|
|
154
154
|
const initExpression = declaration.init;
|
|
155
155
|
if (!n.CallExpression.check(initExpression)) {
|
|
@@ -197,7 +197,7 @@ function findObjectPropertyByName(objectNode, propertyName = "") {
|
|
|
197
197
|
return null;
|
|
198
198
|
}
|
|
199
199
|
|
|
200
|
-
function requireNormalizeFunctionBody(programNode, variableName = "", context = "crud-server-generator
|
|
200
|
+
function requireNormalizeFunctionBody(programNode, variableName = "", context = "crud-server-generator scaffold-field") {
|
|
201
201
|
const validatorObject = requireObjectFreezePayloadObject(programNode, variableName, context);
|
|
202
202
|
const normalizeProperty = findObjectPropertyByName(validatorObject, "normalize");
|
|
203
203
|
if (!normalizeProperty) {
|
|
@@ -218,7 +218,7 @@ function requireNormalizeFunctionBody(programNode, variableName = "", context =
|
|
|
218
218
|
throw new Error(`${context} expected ${variableName}.normalize to be a function with a block body.`);
|
|
219
219
|
}
|
|
220
220
|
|
|
221
|
-
function requireNormalizedObjectLiteral(functionBody, context = "crud-server-generator
|
|
221
|
+
function requireNormalizedObjectLiteral(functionBody, context = "crud-server-generator scaffold-field") {
|
|
222
222
|
for (const statement of functionBody.body || []) {
|
|
223
223
|
if (!n.VariableDeclaration.check(statement)) {
|
|
224
224
|
continue;
|
|
@@ -248,7 +248,7 @@ function insertObjectProperty(
|
|
|
248
248
|
propertyName = "",
|
|
249
249
|
valueExpressionSource = "",
|
|
250
250
|
{
|
|
251
|
-
context = "crud-server-generator
|
|
251
|
+
context = "crud-server-generator scaffold-field",
|
|
252
252
|
insertBeforeComputed = false
|
|
253
253
|
} = {}
|
|
254
254
|
) {
|
|
@@ -448,7 +448,7 @@ function resolveObjectPropertyStringValue(objectNode, propertyName = "") {
|
|
|
448
448
|
return "";
|
|
449
449
|
}
|
|
450
450
|
|
|
451
|
-
function resolveCrudResourceDefaults(source = "", context = "crud-server-generator
|
|
451
|
+
function resolveCrudResourceDefaults(source = "", context = "crud-server-generator scaffold-field") {
|
|
452
452
|
const ast = parseModule(source, context);
|
|
453
453
|
const statements = Array.isArray(ast?.program?.body) ? ast.program.body : [];
|
|
454
454
|
|
|
@@ -478,7 +478,7 @@ function resolveCrudResourceDefaults(source = "", context = "crud-server-generat
|
|
|
478
478
|
function renderResourceFieldMetaPushStatement(entry = {}) {
|
|
479
479
|
const key = normalizeText(entry?.key);
|
|
480
480
|
if (!key) {
|
|
481
|
-
throw new Error("crud-server-generator
|
|
481
|
+
throw new Error("crud-server-generator scaffold-field fieldMeta entry requires key.");
|
|
482
482
|
}
|
|
483
483
|
|
|
484
484
|
const lines = ["RESOURCE_FIELD_META.push({"];
|
|
@@ -495,7 +495,7 @@ function renderResourceFieldMetaPushStatement(entry = {}) {
|
|
|
495
495
|
normalizeCrudLookupNamespace(relation.namespace) ||
|
|
496
496
|
normalizeCrudLookupNamespace(relation.apiPath);
|
|
497
497
|
if (!relationNamespace) {
|
|
498
|
-
throw new Error("crud-server-generator
|
|
498
|
+
throw new Error("crud-server-generator scaffold-field fieldMeta relation requires namespace.");
|
|
499
499
|
}
|
|
500
500
|
lines.push(" relation: {");
|
|
501
501
|
lines.push(` kind: ${JSON.stringify(normalizeText(relation.kind) || "lookup")},`);
|
|
@@ -530,7 +530,7 @@ function applyCrudResourceFieldPatch(
|
|
|
530
530
|
normalizeImportNames = [],
|
|
531
531
|
databaseRuntimeImportNames = [],
|
|
532
532
|
databaseRuntimeRepositoryOptionsImportNames = [],
|
|
533
|
-
context = "crud-server-generator
|
|
533
|
+
context = "crud-server-generator scaffold-field"
|
|
534
534
|
} = {}
|
|
535
535
|
) {
|
|
536
536
|
const normalizedFieldKey = normalizeText(fieldKey);
|
|
@@ -183,4 +183,21 @@ const crudResource = {
|
|
|
183
183
|
|
|
184
184
|
void CRUD_RESOURCE_FIELD_META;
|
|
185
185
|
|
|
186
|
+
// Example 1:n collection hydration:
|
|
187
|
+
// CRUD_RESOURCE_FIELD_META.push({
|
|
188
|
+
// key: "pets",
|
|
189
|
+
// relation: {
|
|
190
|
+
// kind: "collection",
|
|
191
|
+
// namespace: "pets",
|
|
192
|
+
// foreignKey: "customerId",
|
|
193
|
+
// parentValueKey: "id",
|
|
194
|
+
// hydrateOnList: false, // list: opt-in with include=pets
|
|
195
|
+
// hydrateOnView: true // view: hydrated by default
|
|
196
|
+
// }
|
|
197
|
+
// });
|
|
198
|
+
//
|
|
199
|
+
// To hydrate child lookups too, request nested include paths:
|
|
200
|
+
// - include=pets
|
|
201
|
+
// - include=pets,pets.breedId
|
|
202
|
+
|
|
186
203
|
export { crudResource };
|
|
@@ -45,7 +45,9 @@ class ${option:namespace|pascal}Provider {
|
|
|
45
45
|
});
|
|
46
46
|
|
|
47
47
|
app.singleton("crud.lookup.${option:namespace|snake}", (scope) => {
|
|
48
|
-
return createCrudLookupProvider(scope.make("repository.${option:namespace|snake}")
|
|
48
|
+
return createCrudLookupProvider(scope.make("repository.${option:namespace|snake}"), {
|
|
49
|
+
ownershipFilter: crudPolicy.ownershipFilter
|
|
50
|
+
});
|
|
49
51
|
});
|
|
50
52
|
|
|
51
53
|
app.service(
|