@jskit-ai/crud-server-generator 0.1.62 → 0.1.64

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.
@@ -9,7 +9,7 @@ import {
9
9
  toKnexClientId
10
10
  } from "@jskit-ai/database-runtime/shared";
11
11
  import { resolveCrudSurfacePolicyFromAppConfig } from "@jskit-ai/crud-core/server/crudModuleConfig";
12
- import { checkCrudLookupFormControl } from "@jskit-ai/crud-core/shared/crudFieldMetaSupport";
12
+ import { checkCrudLookupFormControl } from "@jskit-ai/crud-core/shared/crudFieldSupport";
13
13
  import {
14
14
  importFreshModuleFromAbsolutePath,
15
15
  loadAppConfigFromModuleUrl,
@@ -663,322 +663,408 @@ function renderObjectPropertyKey(value) {
663
663
  return isIdentifier(key) ? key : JSON.stringify(key);
664
664
  }
665
665
 
666
- function renderPropertyAccess(sourceName, key) {
667
- const normalizedKey = String(key || "");
668
- if (isIdentifier(normalizedKey)) {
669
- return `${sourceName}.${normalizedKey}`;
670
- }
671
- return `${sourceName}[${JSON.stringify(normalizedKey)}]`;
672
- }
673
-
674
- function renderIntegerSchema(column) {
675
- const options = [];
666
+ function renderBoundedNumberEntries(column) {
667
+ const entries = [];
676
668
  if (Number.isFinite(column?.minimum)) {
677
- options.push(`minimum: ${column.minimum}`);
678
- } else if (Number.isFinite(column?.exclusiveMinimum)) {
679
- options.push(`exclusiveMinimum: ${column.exclusiveMinimum}`);
680
- } else if (column.unsigned === true) {
681
- options.push("minimum: 0");
669
+ entries.push(`min: ${column.minimum}`);
670
+ } else if (column?.unsigned === true) {
671
+ entries.push("min: 0");
682
672
  }
683
673
  if (Number.isFinite(column?.maximum)) {
684
- options.push(`maximum: ${column.maximum}`);
685
- }
686
- if (Number.isFinite(column?.exclusiveMaximum)) {
687
- options.push(`exclusiveMaximum: ${column.exclusiveMaximum}`);
674
+ entries.push(`max: ${column.maximum}`);
688
675
  }
689
- if (options.length > 0) {
690
- return `Type.Integer({ ${options.join(", ")} })`;
691
- }
692
- return "Type.Integer()";
676
+ return entries;
693
677
  }
694
678
 
695
- function renderStringSchema(column, { forOutput = false } = {}) {
696
- const options = [];
697
- if (!forOutput && Number.isInteger(column.maxLength) && column.maxLength > 0) {
698
- options.push(`maxLength: ${column.maxLength}`);
699
- }
700
- const enumValues = Array.isArray(column.enumValues) ? column.enumValues.filter((entry) => entry != null) : [];
701
- if (!forOutput && enumValues.length > 0) {
702
- options.push(`enum: ${JSON.stringify(enumValues)}`);
679
+ function resolveCanonicalResourceFieldRequired(column = {}) {
680
+ return column?.nullable !== true && column?.hasDefault !== true;
681
+ }
682
+
683
+ function renderCanonicalResourceOperations(column = {}) {
684
+ if (column?.isOwnerColumn === true) {
685
+ return "{}";
703
686
  }
704
- if (options.length > 0) {
705
- return `Type.String({ ${options.join(", ")} })`;
687
+
688
+ const entries = [
689
+ 'output: { required: true }'
690
+ ];
691
+
692
+ if (column?.writable === true) {
693
+ entries.push(`create: { required: ${resolveCanonicalResourceFieldRequired(column)} }`);
694
+ entries.push("patch: { required: false }");
706
695
  }
707
- return "Type.String()";
696
+
697
+ return [
698
+ "{",
699
+ ...entries.map((entry, index) => ` ${entry}${index < entries.length - 1 ? "," : ""}`),
700
+ "}"
701
+ ].join("\n");
708
702
  }
709
703
 
710
- function renderResourceFieldSchema(column, { forOutput = false } = {}) {
711
- let schemaExpression = "Type.Any()";
712
- const typeKind = String(column.typeKind || "");
704
+ function renderCanonicalResourceFieldSchema(column, { fieldContractEntry = null } = {}) {
705
+ const entries = [];
706
+ const typeKind = String(column?.typeKind || "");
707
+ const isRequired = resolveCanonicalResourceFieldRequired(column);
708
+
713
709
  if (typeKind === "string") {
714
- schemaExpression = renderStringSchema(column, { forOutput });
710
+ entries.push('type: "string"');
711
+ if (Number.isInteger(column?.maxLength) && column.maxLength > 0) {
712
+ entries.push(`maxLength: ${column.maxLength}`);
713
+ }
714
+ const enumValues = Array.isArray(column?.enumValues) ? column.enumValues.filter((entry) => entry != null) : [];
715
+ if (enumValues.length > 0) {
716
+ entries.push(`enum: ${JSON.stringify(enumValues)}`);
717
+ }
715
718
  } else if (typeKind === "integer") {
716
719
  if (column?.isRecordIdColumn === true) {
717
- return forOutput
718
- ? (column.nullable === true ? "nullableRecordIdSchema" : "recordIdSchema")
719
- : (column.nullable === true ? "nullableRecordIdInputSchema" : "recordIdInputSchema");
720
+ entries.push('type: "id"');
721
+ } else {
722
+ entries.push('type: "integer"');
723
+ entries.push(...renderBoundedNumberEntries(column));
720
724
  }
721
- schemaExpression = renderIntegerSchema(column);
722
725
  } else if (typeKind === "number") {
723
- const options = [];
724
- if (Number.isFinite(column?.minimum)) {
725
- options.push(`minimum: ${column.minimum}`);
726
- }
727
- if (Number.isFinite(column?.exclusiveMinimum)) {
728
- options.push(`exclusiveMinimum: ${column.exclusiveMinimum}`);
729
- }
730
- if (Number.isFinite(column?.maximum)) {
731
- options.push(`maximum: ${column.maximum}`);
732
- }
733
- if (Number.isFinite(column?.exclusiveMaximum)) {
734
- options.push(`exclusiveMaximum: ${column.exclusiveMaximum}`);
735
- }
736
- schemaExpression = options.length > 0
737
- ? `Type.Number({ ${options.join(", ")} })`
738
- : "Type.Number()";
726
+ entries.push('type: "number"');
727
+ entries.push(...renderBoundedNumberEntries(column));
739
728
  } else if (typeKind === "boolean") {
740
- schemaExpression = "Type.Boolean()";
729
+ entries.push('type: "boolean"');
741
730
  } else if (typeKind === "datetime") {
742
- schemaExpression = 'Type.String({ format: "date-time", minLength: 1 })';
731
+ entries.push('type: "dateTime"');
732
+ const normalizedDefault = normalizeText(column?.defaultValue).toLowerCase();
733
+ if (normalizedDefault === "current_timestamp" || normalizedDefault === "current_timestamp()") {
734
+ entries.push('default: "now()"');
735
+ }
743
736
  } else if (typeKind === "date") {
744
- schemaExpression = 'Type.String({ format: "date", minLength: 1 })';
737
+ entries.push('type: "date"');
745
738
  } else if (typeKind === "time") {
746
- return column.nullable === true
747
- ? "NULLABLE_HTML_TIME_STRING_SCHEMA"
748
- : "HTML_TIME_STRING_SCHEMA";
749
- } else if (typeKind === "json") {
750
- schemaExpression = "Type.Any()";
739
+ entries.push('type: "time"');
740
+ } else {
741
+ entries.push('type: "none"');
751
742
  }
752
743
 
753
- if (column.nullable === true) {
754
- return `Type.Union([${schemaExpression}, Type.Null()])`;
744
+ if (isRequired) {
745
+ entries.push("required: true");
746
+ }
747
+ if (column?.nullable === true) {
748
+ entries.push("nullable: true");
749
+ }
750
+ if (shouldRenderJsonRestSearch(column)) {
751
+ entries.push("search: true");
752
+ }
753
+ if (column?.isOwnerColumn === true) {
754
+ entries.push("hidden: true");
755
755
  }
756
- return schemaExpression;
757
- }
758
756
 
759
- function renderResourceValidatorsImport({ htmlTimeSchemaImports = [], recordIdValidatorImports = [] } = {}) {
760
- const imports = [
761
- "normalizeObjectInput",
762
- "createCursorListValidator"
763
- ];
764
- for (const importName of Array.isArray(recordIdValidatorImports) ? recordIdValidatorImports : []) {
765
- if (!imports.includes(importName)) {
766
- imports.push(importName);
767
- }
757
+ const actualField = normalizeText(fieldContractEntry?.actualField);
758
+ if (actualField) {
759
+ entries.push(`actualField: ${JSON.stringify(actualField)}`);
760
+ }
761
+
762
+ const parentRouteParamKey = normalizeText(fieldContractEntry?.parentRouteParamKey);
763
+ if (parentRouteParamKey) {
764
+ entries.push(`parentRouteParamKey: ${JSON.stringify(parentRouteParamKey)}`);
768
765
  }
769
- for (const importName of Array.isArray(htmlTimeSchemaImports) ? htmlTimeSchemaImports : []) {
770
- if (!imports.includes(importName)) {
771
- imports.push(importName);
766
+
767
+ const relation = fieldContractEntry?.relation && typeof fieldContractEntry.relation === "object"
768
+ ? fieldContractEntry.relation
769
+ : null;
770
+ if (relation) {
771
+ const relationNamespace =
772
+ normalizeCrudLookupNamespace(relation.namespace) ||
773
+ normalizeCrudLookupNamespace(relation.apiPath) ||
774
+ normalizeCrudLookupNamespace(relation?.source?.path) ||
775
+ normalizeCrudLookupNamespace(relation.targetResource);
776
+ if (!relationNamespace) {
777
+ throw new Error(`crud template context field "${normalizeText(column?.key)}" lookup relation requires namespace.`);
772
778
  }
779
+
780
+ const relationEntries = [
781
+ `kind: ${JSON.stringify(normalizeText(relation.kind) || "lookup")}`,
782
+ `namespace: ${JSON.stringify(relationNamespace)}`,
783
+ `valueKey: ${JSON.stringify(normalizeText(relation.valueKey) || "id")}`
784
+ ];
785
+ const labelKey = normalizeText(relation.labelKey);
786
+ if (labelKey) {
787
+ relationEntries.push(`labelKey: ${JSON.stringify(labelKey)}`);
788
+ }
789
+ entries.push(`relation: { ${relationEntries.join(", ")} }`);
773
790
  }
774
- return `import {\n ${imports.join(",\n ")}\n} from "@jskit-ai/kernel/shared/validators";`;
775
- }
776
791
 
777
- function resolveHtmlTimeSchemaImports(columns = []) {
778
- const imports = [];
779
- for (const column of Array.isArray(columns) ? columns : []) {
780
- if (column?.typeKind !== "time") {
781
- continue;
792
+ const relationshipScopeName = resolveJsonRestRelationshipScopeName(fieldContractEntry);
793
+ const relationshipAlias = resolveJsonRestRelationshipAlias(column);
794
+ if (
795
+ relationshipScopeName &&
796
+ relationshipAlias &&
797
+ column?.isOwnerColumn !== true &&
798
+ column?.isForeignIdColumn === true
799
+ ) {
800
+ entries.push(`belongsTo: ${JSON.stringify(relationshipScopeName)}`);
801
+ entries.push(`as: ${JSON.stringify(relationshipAlias)}`);
802
+ }
803
+
804
+ const fieldUiOptions = normalizeFieldMetaUiOptions(fieldContractEntry?.ui?.options);
805
+ const formControl = checkCrudLookupFormControl(fieldContractEntry?.ui?.formControl, {
806
+ context: `resource schema field "${normalizeText(column?.key)}" ui.formControl`,
807
+ defaultValue: relation ? "autocomplete" : (fieldUiOptions.length > 0 ? "select" : "")
808
+ });
809
+ if (formControl || fieldUiOptions.length > 0) {
810
+ const uiEntries = [];
811
+ if (formControl) {
812
+ uiEntries.push(`formControl: ${JSON.stringify(formControl)}`);
782
813
  }
783
- const importName = column.nullable === true
784
- ? "NULLABLE_HTML_TIME_STRING_SCHEMA"
785
- : "HTML_TIME_STRING_SCHEMA";
786
- if (!imports.includes(importName)) {
787
- imports.push(importName);
814
+ if (fieldUiOptions.length > 0) {
815
+ uiEntries.push(`options: ${JSON.stringify(fieldUiOptions)}`);
788
816
  }
817
+ entries.push(`ui: { ${uiEntries.join(", ")} }`);
818
+ }
819
+
820
+ const storageEntries = [];
821
+ if (fieldContractEntry?.storage?.mode === "virtual") {
822
+ storageEntries.push("virtual: true");
823
+ }
824
+ if (toSnakeCase(normalizeText(column?.key)) !== normalizeText(column?.name)) {
825
+ storageEntries.push(`column: ${JSON.stringify(column.name)}`);
789
826
  }
790
- return imports;
827
+ if (normalizeText(column?.typeKind).toLowerCase() === "datetime") {
828
+ storageEntries.push('writeSerializer: "datetime-utc"');
829
+ }
830
+ if (storageEntries.length > 0) {
831
+ entries.push(`storage: { ${storageEntries.join(", ")} }`);
832
+ }
833
+
834
+ const operationsLines = renderCanonicalResourceOperations(column).split("\n");
835
+ const lines = [
836
+ "{",
837
+ ...entries.map((entry) => ` ${entry},`),
838
+ ` operations: ${operationsLines[0]}`
839
+ ];
840
+
841
+ for (const line of operationsLines.slice(1)) {
842
+ lines.push(` ${line}`);
843
+ }
844
+
845
+ lines.push("}");
846
+ return lines.join("\n");
791
847
  }
792
848
 
793
- function resolveRecordIdValidatorImports(...sources) {
794
- const imports = ["recordIdSchema"];
795
- const joinedSource = sources
796
- .map((source) => String(source || ""))
849
+ function renderCanonicalResourceSchemaPropertyLines(columns = [], { fieldContractEntries = [] } = {}) {
850
+ const fieldContractByKey = Object.fromEntries(
851
+ (Array.isArray(fieldContractEntries) ? fieldContractEntries : [])
852
+ .map((entry) => [normalizeText(entry?.key), entry])
853
+ .filter(([key]) => key)
854
+ );
855
+
856
+ return (Array.isArray(columns) ? columns : [])
857
+ .filter((column) => column?.isIdColumn !== true)
858
+ .map((column) => {
859
+ const key = renderObjectPropertyKey(column.key);
860
+ const schemaLines = renderCanonicalResourceFieldSchema(column, {
861
+ fieldContractEntry: fieldContractByKey[normalizeText(column?.key)] || null
862
+ }).split("\n");
863
+ const lines = [` ${key}: ${schemaLines[0]}`];
864
+ for (const line of schemaLines.slice(1)) {
865
+ lines.push(` ${line}`);
866
+ }
867
+ lines[lines.length - 1] = `${lines[lines.length - 1]},`;
868
+ return lines.join("\n");
869
+ })
797
870
  .join("\n");
798
- for (const importName of ["recordIdInputSchema", "nullableRecordIdSchema", "nullableRecordIdInputSchema"]) {
799
- if (joinedSource.includes(importName)) {
800
- imports.push(importName);
801
- }
871
+ }
872
+
873
+ function resolveJsonRestRelationshipScopeName(fieldContractEntry = null) {
874
+ const namespace = normalizeText(fieldContractEntry?.relation?.namespace);
875
+ if (!namespace) {
876
+ return "";
802
877
  }
803
- return imports;
878
+
879
+ return toCamelCase(namespace.replace(/\//g, "-"));
804
880
  }
805
881
 
806
- function renderInputNormalizer(column) {
807
- const typeKind = String(column.typeKind || "");
808
- const nullable = column?.nullable === true;
809
- if (typeKind === "string") {
810
- return "normalizeText";
882
+ function resolveJsonRestRelationshipAlias(column = null) {
883
+ const key = normalizeText(column?.key);
884
+ if (!key) {
885
+ return "";
811
886
  }
812
- if (typeKind === "time") {
813
- if (nullable) {
814
- return "(value) => { const normalized = normalizeText(value); return normalized || null; }";
815
- }
816
- return "normalizeText";
887
+ if (key.endsWith("Id") && key.length > 2) {
888
+ return `${key.slice(0, -2).slice(0, 1).toLowerCase()}${key.slice(0, -2).slice(1)}`;
889
+ }
890
+ return "";
891
+ }
892
+
893
+ function resolveJsonRestFieldType(column = {}) {
894
+ if (column?.isRecordIdColumn === true) {
895
+ return "id";
896
+ }
897
+
898
+ const typeKind = normalizeText(column?.typeKind).toLowerCase();
899
+ if (typeKind === "string") {
900
+ return "string";
817
901
  }
818
902
  if (typeKind === "integer") {
819
- if (column?.isRecordIdColumn === true) {
820
- if (nullable) {
821
- return "(value) => normalizeRecordId(value, { fallback: null })";
822
- }
823
- return "normalizeRecordId";
824
- }
825
- return "normalizeFiniteInteger";
903
+ return "integer";
826
904
  }
827
905
  if (typeKind === "number") {
828
- return "normalizeFiniteNumber";
906
+ return "number";
829
907
  }
830
908
  if (typeKind === "boolean") {
831
- return "normalizeBoolean";
909
+ return "boolean";
832
910
  }
833
911
  if (typeKind === "datetime") {
834
- if (nullable) {
835
- return "(value) => { const normalized = normalizeText(value); return normalized ? toDatabaseDateTimeUtc(normalized) : null; }";
836
- }
837
- return "toDatabaseDateTimeUtc";
912
+ return "dateTime";
838
913
  }
839
914
  if (typeKind === "date") {
840
- if (nullable) {
841
- return "(value) => { const normalized = normalizeText(value); return normalized ? toIsoString(normalized).slice(0, 10) : null; }";
842
- }
843
- return "(value) => toIsoString(value).slice(0, 10)";
915
+ return "date";
844
916
  }
845
- if (typeKind === "json") {
846
- return "(value) => parseJsonValue(value, null, { fallback: null, allowNull: true })";
917
+ if (typeKind === "time") {
918
+ return "time";
847
919
  }
848
- return "(value) => value";
920
+ return "string";
921
+ }
922
+
923
+ function shouldRenderJsonRestSearch(column = {}) {
924
+ return column?.isCreatedAtColumn !== true && column?.isUpdatedAtColumn !== true;
849
925
  }
850
926
 
851
- function renderOutputNormalizerExpression(column) {
852
- const typeKind = String(column.typeKind || "");
853
- const nullable = column?.nullable === true;
854
- if (typeKind === "string" || typeKind === "time") {
855
- return "normalizeText";
927
+ function shouldRenderJsonRestStorage(column = {}) {
928
+ const key = normalizeText(column?.key);
929
+ const columnName = normalizeText(column?.name);
930
+ if (!key || !columnName) {
931
+ return false;
856
932
  }
857
- if (typeKind === "integer") {
858
- if (column?.isRecordIdColumn === true) {
859
- if (nullable) {
860
- return "(value) => normalizeRecordId(value, { fallback: null })";
861
- }
862
- return "normalizeRecordId";
933
+
934
+ if (toSnakeCase(key) !== columnName) {
935
+ return true;
936
+ }
937
+
938
+ return normalizeText(column?.typeKind).toLowerCase() === "datetime";
939
+ }
940
+
941
+ function renderJsonRestFieldSchema(column, { fieldContractEntry = null } = {}) {
942
+ const entries = [];
943
+ const type = resolveJsonRestFieldType(column);
944
+ entries.push(`type: ${JSON.stringify(type)}`);
945
+
946
+ if (column?.isIdColumn === true) {
947
+ entries.push("primary: true");
948
+ entries.push("required: true");
949
+ entries.push("search: true");
950
+ } else {
951
+ const required = column?.nullable !== true && column?.hasDefault !== true;
952
+ entries.push(`required: ${required}`);
953
+ if (shouldRenderJsonRestSearch(column)) {
954
+ entries.push("search: true");
863
955
  }
864
- return "normalizeFiniteInteger";
865
956
  }
866
- if (typeKind === "number") {
867
- return "normalizeFiniteNumber";
957
+
958
+ if (column?.nullable === true) {
959
+ entries.push("nullable: true");
868
960
  }
869
- if (typeKind === "boolean") {
870
- return "normalizeBoolean";
961
+
962
+ if (type === "string" && Number.isInteger(column?.maxLength) && column.maxLength > 0) {
963
+ entries.push(`max: ${column.maxLength}`);
871
964
  }
872
- if (typeKind === "datetime") {
873
- return "toIsoString";
965
+
966
+ if (column?.isOwnerColumn === true) {
967
+ entries.push("hidden: true");
874
968
  }
875
- if (typeKind === "date") {
876
- return "(value) => toIsoString(value).slice(0, 10)";
969
+
970
+ const relationshipScopeName = resolveJsonRestRelationshipScopeName(fieldContractEntry);
971
+ const relationshipAlias = resolveJsonRestRelationshipAlias(column);
972
+ if (
973
+ relationshipScopeName &&
974
+ relationshipAlias &&
975
+ column?.isOwnerColumn !== true &&
976
+ column?.isForeignIdColumn === true
977
+ ) {
978
+ entries.push(`belongsTo: ${JSON.stringify(relationshipScopeName)}`);
979
+ entries.push(`as: ${JSON.stringify(relationshipAlias)}`);
877
980
  }
878
- if (typeKind === "json") {
879
- return "(value) => parseJsonValue(value, null, { fallback: null, allowNull: true })";
981
+
982
+ if (shouldRenderJsonRestStorage(column)) {
983
+ const storageEntries = [];
984
+ if (toSnakeCase(normalizeText(column?.key)) !== normalizeText(column?.name)) {
985
+ storageEntries.push(`column: ${JSON.stringify(column.name)}`);
986
+ }
987
+ if (normalizeText(column?.typeKind).toLowerCase() === "datetime") {
988
+ storageEntries.push("serialize: serializeNullableDateTime");
989
+ }
990
+ entries.push(`storage: { ${storageEntries.join(", ")} }`);
880
991
  }
881
- return "";
882
- }
883
992
 
884
- function renderResourceSchemaPropertyLines(columns, { forOutput = false } = {}) {
885
- const sourceColumns = Array.isArray(columns) ? columns : [];
886
- return sourceColumns
887
- .map((column) => {
888
- const key = renderObjectPropertyKey(column.key);
889
- const schemaExpression = renderResourceFieldSchema(column, { forOutput });
890
- return ` ${key}: ${schemaExpression},`;
891
- })
892
- .join("\n");
993
+ return [
994
+ "{",
995
+ ...entries.map((entry, index) => ` ${entry}${index < entries.length - 1 ? "," : ""}`),
996
+ "}"
997
+ ].join("\n");
893
998
  }
894
999
 
895
- function renderResourceInputNormalizationLines(columns) {
896
- const sourceColumns = Array.isArray(columns) ? columns : [];
897
- return sourceColumns
898
- .map((column) => {
899
- const keyLiteral = JSON.stringify(String(column.key || ""));
900
- const normalizer = renderInputNormalizer(column);
901
- return ` normalizeIfInSource(source, normalized, ${keyLiteral}, ${normalizer});`;
902
- })
903
- .join("\n");
904
- }
1000
+ function renderJsonRestSchemaPropertyLines(columns = [], { fieldContractEntries = [] } = {}) {
1001
+ const fieldContractByKey = Object.fromEntries(
1002
+ (Array.isArray(fieldContractEntries) ? fieldContractEntries : [])
1003
+ .map((entry) => [normalizeText(entry?.key), entry])
1004
+ .filter(([key]) => key)
1005
+ );
905
1006
 
906
- function renderResourceOutputNormalizationLines(columns) {
907
- const sourceColumns = Array.isArray(columns) ? columns : [];
908
- return sourceColumns
1007
+ return (Array.isArray(columns) ? columns : [])
1008
+ .filter((column) => column?.isIdColumn !== true)
909
1009
  .map((column) => {
910
1010
  const key = renderObjectPropertyKey(column.key);
911
- const sourceAccess = renderPropertyAccess("source", column.key);
912
- const normalizer = renderOutputNormalizerExpression(column);
913
- if (!normalizer) {
914
- return ` ${key}: ${sourceAccess},`;
1011
+ const schemaLines = renderJsonRestFieldSchema(column, {
1012
+ fieldContractEntry: fieldContractByKey[normalizeText(column?.key)] || null
1013
+ }).split("\n");
1014
+ const lines = [` ${key}: ${schemaLines[0]}`];
1015
+ for (const line of schemaLines.slice(1)) {
1016
+ lines.push(` ${line}`);
915
1017
  }
916
- const nullishNormalizer = column.nullable === true ? "normalizeOrNull" : "normalizeIfPresent";
917
- return ` ${key}: ${nullishNormalizer}(${sourceAccess}, ${normalizer}),`;
1018
+ lines[lines.length - 1] = `${lines[lines.length - 1]},`;
1019
+ return lines.join("\n");
918
1020
  })
919
1021
  .join("\n");
920
1022
  }
921
1023
 
922
- function renderResourceDatabaseRuntimeImport({ needsToIsoString = false, needsToDatabaseDateTimeUtc = false } = {}) {
923
- const imports = [];
924
- if (needsToIsoString) {
925
- imports.push("toIsoString");
926
- }
927
- if (needsToDatabaseDateTimeUtc) {
928
- imports.push("toDatabaseDateTimeUtc");
929
- }
930
- if (imports.length < 1) {
931
- return "";
932
- }
933
- return `import {\n ${imports.join(",\n ")}\n} from "@jskit-ai/database-runtime/shared";`;
934
- }
1024
+ function renderJsonRestSearchSchemaLines(columns = []) {
1025
+ const searchableStringKeys = (Array.isArray(columns) ? columns : [])
1026
+ .filter((column) =>
1027
+ normalizeText(column?.typeKind).toLowerCase() === "string" &&
1028
+ column?.isOwnerColumn !== true &&
1029
+ column?.isIdColumn !== true &&
1030
+ column?.isCreatedAtColumn !== true &&
1031
+ column?.isUpdatedAtColumn !== true
1032
+ )
1033
+ .map((column) => normalizeText(column?.key))
1034
+ .filter(Boolean);
935
1035
 
936
- function renderResourceJsonImport({ needsJson = false } = {}) {
937
- if (!needsJson) {
938
- return "";
1036
+ const lines = [
1037
+ ' id: { type: "id", actualField: "id" },'
1038
+ ];
1039
+
1040
+ if (searchableStringKeys.length > 0) {
1041
+ lines.push(
1042
+ ` q: { type: "string", oneOf: ${JSON.stringify(searchableStringKeys)}, filterOperator: "like", splitBy: " ", matchAll: true },`
1043
+ );
939
1044
  }
940
- return 'import { parseJsonValue } from "@jskit-ai/database-runtime/shared/repositoryOptions";';
1045
+
1046
+ return lines.join("\n");
941
1047
  }
942
1048
 
943
- function renderResourceNormalizeSupportImport({
944
- needsNormalizeText = false,
945
- needsNormalizeBoolean = false,
946
- needsNormalizeFiniteNumber = false,
947
- needsNormalizeFiniteInteger = false,
948
- needsNormalizeRecordId = false,
949
- needsNormalizeIfInSource = false,
950
- needsNormalizeIfPresent = false,
951
- needsNormalizeOrNull = false
952
- } = {}) {
953
- const imports = [];
954
- if (needsNormalizeText) {
955
- imports.push("normalizeText");
956
- }
957
- if (needsNormalizeBoolean) {
958
- imports.push("normalizeBoolean");
959
- }
960
- if (needsNormalizeFiniteNumber) {
961
- imports.push("normalizeFiniteNumber");
962
- }
963
- if (needsNormalizeFiniteInteger) {
964
- imports.push("normalizeFiniteInteger");
965
- }
966
- if (needsNormalizeRecordId) {
967
- imports.push("normalizeRecordId");
968
- }
969
- if (needsNormalizeIfInSource) {
970
- imports.push("normalizeIfInSource");
971
- }
972
- if (needsNormalizeIfPresent) {
973
- imports.push("normalizeIfPresent");
974
- }
975
- if (needsNormalizeOrNull) {
976
- imports.push("normalizeOrNull");
1049
+ function renderJsonRestDefaultSortLine(columns = []) {
1050
+ const sourceColumns = Array.isArray(columns) ? columns : [];
1051
+ const createdAtColumn = sourceColumns.find((column) => column?.isCreatedAtColumn === true);
1052
+ if (createdAtColumn?.key) {
1053
+ return ` defaultSort: ${JSON.stringify([`-${createdAtColumn.key}`])},`;
977
1054
  }
978
- if (imports.length < 1) {
979
- return "";
1055
+
1056
+ const idColumn = sourceColumns.find((column) => column?.isIdColumn === true);
1057
+ if (idColumn?.key) {
1058
+ return ` defaultSort: ${JSON.stringify([`-${idColumn.key}`])},`;
980
1059
  }
981
- return `import {\n ${imports.join(",\n ")}\n} from "@jskit-ai/kernel/shared/support/normalize";`;
1060
+
1061
+ return "";
1062
+ }
1063
+
1064
+ function renderResourceDefaultSortLiteral(columns = []) {
1065
+ const sortLine = renderJsonRestDefaultSortLine(columns);
1066
+ const match = sortLine.match(/defaultSort:\s*(.+),$/);
1067
+ return match?.[1] || "[]";
982
1068
  }
983
1069
 
984
1070
  function renderMigrationDefaultClause(column) {
@@ -1411,7 +1497,7 @@ function resolveEnumFieldMetaUiOptions(enumValues = []) {
1411
1497
  return normalizeFieldMetaUiOptions(options);
1412
1498
  }
1413
1499
 
1414
- function buildFieldMetaEntries({ outputColumns = [], writableColumns = [], snapshot = {} } = {}) {
1500
+ function buildFieldContractEntries({ outputColumns = [], writableColumns = [], snapshot = {} } = {}) {
1415
1501
  const fieldColumns = [...outputColumns, ...writableColumns];
1416
1502
  const fieldColumnsByName = new Map();
1417
1503
  const fieldColumnsByKey = new Map();
@@ -1509,127 +1595,6 @@ function buildFieldMetaEntries({ outputColumns = [], writableColumns = [], snaps
1509
1595
  return mergeFieldMetaEntries(repositoryEntries, relationEntries, enumEntries);
1510
1596
  }
1511
1597
 
1512
- function renderFieldMetaEntryLines(entry = {}) {
1513
- const lines = ["RESOURCE_FIELD_META.push({"];
1514
- const topLevelProperties = [`key: ${JSON.stringify(entry.key)}`];
1515
- const repositoryColumn = normalizeText(entry?.repository?.column);
1516
- if (repositoryColumn) {
1517
- topLevelProperties.push([
1518
- "repository: {",
1519
- ` column: ${JSON.stringify(repositoryColumn)}`,
1520
- "}"
1521
- ].join("\n"));
1522
- }
1523
-
1524
- const relation = entry.relation && typeof entry.relation === "object" ? entry.relation : null;
1525
- if (relation) {
1526
- const targetResourceNamespace = normalizeCrudLookupNamespace(relation.targetResource);
1527
- const relationNamespace =
1528
- normalizeCrudLookupNamespace(relation.namespace) ||
1529
- normalizeCrudLookupNamespace(relation.apiPath) ||
1530
- normalizeCrudLookupNamespace(relation?.source?.path) ||
1531
- targetResourceNamespace;
1532
- if (!relationNamespace) {
1533
- throw new Error(`crud template context fieldMeta["${normalizeText(entry.key)}"] lookup relation requires namespace.`);
1534
- }
1535
- const relationLines = [
1536
- "relation: {",
1537
- ` kind: ${JSON.stringify(normalizeText(relation.kind) || "lookup")},`,
1538
- ` namespace: ${JSON.stringify(relationNamespace)},`,
1539
- ` valueKey: ${JSON.stringify(normalizeText(relation.valueKey) || "id")},`
1540
- ];
1541
- const labelKey = normalizeText(relation.labelKey);
1542
- if (labelKey) {
1543
- relationLines.push(` labelKey: ${JSON.stringify(labelKey)}`);
1544
- } else {
1545
- relationLines[relationLines.length - 1] = relationLines[relationLines.length - 1].replace(/,$/, "");
1546
- }
1547
- relationLines.push("}");
1548
- topLevelProperties.push(relationLines.join("\n"));
1549
- }
1550
-
1551
- const fieldUiOptions = normalizeFieldMetaUiOptions(entry?.ui?.options);
1552
- const formControl = checkCrudLookupFormControl(entry?.ui?.formControl, {
1553
- context: `resource.fieldMeta["${normalizeText(entry.key)}"].ui.formControl`,
1554
- defaultValue: relation ? "autocomplete" : (fieldUiOptions.length > 0 ? "select" : "")
1555
- });
1556
- if (formControl || fieldUiOptions.length > 0) {
1557
- const uiPropertyBlocks = [];
1558
- if (formControl) {
1559
- uiPropertyBlocks.push([
1560
- `formControl: ${JSON.stringify(formControl)}${relation ? " // or \"select\"" : ""}`
1561
- ]);
1562
- }
1563
- if (fieldUiOptions.length > 0) {
1564
- const optionsJsonLines = JSON.stringify(fieldUiOptions, null, 2).split("\n");
1565
- const optionPropertyLines = [`options: ${optionsJsonLines[0]}`];
1566
- for (const jsonLine of optionsJsonLines.slice(1)) {
1567
- optionPropertyLines.push(jsonLine);
1568
- }
1569
- uiPropertyBlocks.push(optionPropertyLines);
1570
- }
1571
-
1572
- const uiLines = ["ui: {"];
1573
- for (const [propertyIndex, propertyLines] of uiPropertyBlocks.entries()) {
1574
- const isLastProperty = propertyIndex >= uiPropertyBlocks.length - 1;
1575
- const propertySuffix = isLastProperty ? "" : ",";
1576
- for (const [lineIndex, line] of propertyLines.entries()) {
1577
- const isLastLine = lineIndex >= propertyLines.length - 1;
1578
- uiLines.push(` ${line}${isLastLine ? propertySuffix : ""}`);
1579
- }
1580
- }
1581
- uiLines.push("}");
1582
- topLevelProperties.push(
1583
- uiLines.join("\n")
1584
- );
1585
- }
1586
-
1587
- for (const [index, propertyBlock] of topLevelProperties.entries()) {
1588
- const blockLines = String(propertyBlock || "").split("\n");
1589
- const isLastProperty = index >= topLevelProperties.length - 1;
1590
- const propertySuffix = isLastProperty ? "" : ",";
1591
- for (const [lineIndex, line] of blockLines.entries()) {
1592
- const isLastLine = lineIndex >= blockLines.length - 1;
1593
- lines.push(` ${line}${isLastLine ? propertySuffix : ""}`);
1594
- }
1595
- }
1596
-
1597
- lines.push("});");
1598
- return lines.join("\n");
1599
- }
1600
-
1601
- function renderResourceFieldMetaPushLines(entries = []) {
1602
- const sourceEntries = Array.isArray(entries) ? entries : [];
1603
- if (sourceEntries.length < 1) {
1604
- return "";
1605
- }
1606
-
1607
- return sourceEntries.map((entry) => renderFieldMetaEntryLines(entry)).join("\n\n");
1608
- }
1609
-
1610
- function renderRepositoryListConfigLines(snapshot = {}) {
1611
- const commentLines = [
1612
- " // defaultLimit: 20,",
1613
- " // maxLimit: 100,",
1614
- " // searchColumns: [\"name\"],"
1615
- ];
1616
- const sourceColumns = Array.isArray(snapshot?.columns) ? snapshot.columns : [];
1617
- const hasCreatedAtColumn = sourceColumns.some((column = {}) => normalizeText(column?.name) === "created_at");
1618
- if (!hasCreatedAtColumn) {
1619
- return commentLines.join("\n");
1620
- }
1621
-
1622
- return [
1623
- ...commentLines,
1624
- " orderBy: [",
1625
- " {",
1626
- " column: \"created_at\",",
1627
- " direction: \"desc\"",
1628
- " }",
1629
- " ]"
1630
- ].join("\n");
1631
- }
1632
-
1633
1598
  function buildCrudPermissionIds(namespace = "") {
1634
1599
  const permissionNamespace = toSnakeCase(namespace);
1635
1600
  if (!permissionNamespace) {
@@ -1728,14 +1693,10 @@ function renderRouteParamsValidatorLine(operation = "", { surfaceRequiresWorkspa
1728
1693
  if (!surfaceRequiresWorkspace) {
1729
1694
  return "";
1730
1695
  }
1731
- return " paramsValidator: routeParamsValidator,";
1696
+ return " params: routeParamsValidator,";
1732
1697
  }
1733
1698
 
1734
- if (!surfaceRequiresWorkspace) {
1735
- return " paramsValidator: recordIdParamsValidator,";
1736
- }
1737
-
1738
- return " paramsValidator: [routeParamsValidator, recordIdParamsValidator],";
1699
+ return " params: recordRouteParamsValidator,";
1739
1700
  }
1740
1701
 
1741
1702
  function renderRouteInputLines(operation = "", { surfaceRequiresWorkspace = true } = {}) {
@@ -1758,13 +1719,13 @@ function renderRouteInputLines(operation = "", { surfaceRequiresWorkspace = true
1758
1719
  }
1759
1720
 
1760
1721
  if (normalizedOperation === "create") {
1761
- lines.push(" payload: request.input.body");
1722
+ lines.push(" ...(request.input.body || {})");
1762
1723
  return lines.join("\n");
1763
1724
  }
1764
1725
 
1765
1726
  if (normalizedOperation === "update") {
1766
1727
  lines.push(" recordId: request.input.params.recordId,");
1767
- lines.push(" patch: request.input.body");
1728
+ lines.push(" ...(request.input.body || {})");
1768
1729
  return lines.join("\n");
1769
1730
  }
1770
1731
 
@@ -1772,32 +1733,89 @@ function renderRouteInputLines(operation = "", { surfaceRequiresWorkspace = true
1772
1733
  return lines.join("\n");
1773
1734
  }
1774
1735
 
1775
- function renderActionInputValidatorExpression(operation = "", { surfaceRequiresWorkspace = true } = {}) {
1776
- const normalizedOperation = normalizeCrudOperation(operation, "CRUD action input validator operation");
1777
- const validators = [];
1736
+ function renderObjectSchemaDefinition(lines = [], { mode = "patch" } = {}) {
1737
+ const entries = (Array.isArray(lines) ? lines : [])
1738
+ .map((line) => String(line || "").trim())
1739
+ .filter(Boolean)
1740
+ .map((line) => line.endsWith(",") ? line.slice(0, -1) : line);
1741
+
1742
+ if (entries.length < 1) {
1743
+ throw new TypeError("renderObjectSchemaDefinition requires at least one schema definition.");
1744
+ }
1778
1745
 
1779
- if (surfaceRequiresWorkspace) {
1780
- validators.push("workspaceSlugParamsValidator");
1746
+ if (entries.length === 1) {
1747
+ return entries[0];
1781
1748
  }
1782
1749
 
1783
- if (normalizedOperation === "list") {
1784
- validators.push(
1785
- "listCursorPaginationQueryValidator",
1786
- "listSearchQueryValidator",
1787
- "listParentFilterQueryValidator",
1788
- "lookupIncludeQueryValidator"
1789
- );
1790
- } else if (normalizedOperation === "view") {
1791
- validators.push("recordIdParamsValidator", "lookupIncludeQueryValidator");
1792
- } else if (normalizedOperation === "create") {
1793
- validators.push("{ payload: resource.operations.create.bodyValidator }");
1794
- } else if (normalizedOperation === "update") {
1795
- validators.push("recordIdParamsValidator", "{ patch: resource.operations.patch.bodyValidator }");
1796
- } else {
1797
- validators.push("recordIdParamsValidator");
1750
+ if (normalizeText(mode).toLowerCase() === "patch") {
1751
+ return [
1752
+ "composeSchemaDefinitions([",
1753
+ ...entries.map((line) => ` ${line},`),
1754
+ "])"
1755
+ ].join("\n");
1798
1756
  }
1799
1757
 
1800
- return validators.length === 1 ? validators[0] : `[${validators.join(", ")}]`;
1758
+ return [
1759
+ "composeSchemaDefinitions([",
1760
+ ...entries.map((line) => ` ${line},`),
1761
+ "], {",
1762
+ ` mode: ${JSON.stringify(mode)}`,
1763
+ "})"
1764
+ ].join("\n");
1765
+ }
1766
+
1767
+ function renderActionInputExpressions({ surfaceRequiresWorkspace = true } = {}) {
1768
+ const listLines = [];
1769
+ const viewLines = [];
1770
+ const createLines = [];
1771
+ const updateLines = [];
1772
+ const deleteLines = [];
1773
+
1774
+ if (surfaceRequiresWorkspace) {
1775
+ listLines.push("workspaceSlugParamsValidator,");
1776
+ viewLines.push("workspaceSlugParamsValidator,");
1777
+ createLines.push("workspaceSlugParamsValidator,");
1778
+ updateLines.push("workspaceSlugParamsValidator,");
1779
+ deleteLines.push("workspaceSlugParamsValidator,");
1780
+ }
1781
+
1782
+ listLines.push(
1783
+ "listCursorPaginationQueryValidator,",
1784
+ "listSearchQueryValidator,",
1785
+ "listParentFilterQueryValidator,",
1786
+ "lookupIncludeQueryValidator,"
1787
+ );
1788
+ viewLines.push(
1789
+ "recordIdParamsValidator,",
1790
+ "lookupIncludeQueryValidator,"
1791
+ );
1792
+ createLines.push("resource.operations.create.body,");
1793
+ updateLines.push(
1794
+ "recordIdParamsValidator,",
1795
+ "resource.operations.patch.body,"
1796
+ );
1797
+ deleteLines.push("recordIdParamsValidator,");
1798
+
1799
+ return Object.freeze({
1800
+ list: renderObjectSchemaDefinition(listLines),
1801
+ view: renderObjectSchemaDefinition(viewLines),
1802
+ create: renderObjectSchemaDefinition(createLines, { mode: "create" }),
1803
+ update: renderObjectSchemaDefinition(updateLines),
1804
+ delete: renderObjectSchemaDefinition(deleteLines)
1805
+ });
1806
+ }
1807
+
1808
+ function renderRouteValidatorConstants({ surfaceRequiresWorkspace = true } = {}) {
1809
+ if (!surfaceRequiresWorkspace) {
1810
+ return "";
1811
+ }
1812
+
1813
+ return [
1814
+ "const recordRouteParamsValidator = composeSchemaDefinitions([",
1815
+ " routeParamsValidator,",
1816
+ " recordIdParamsValidator",
1817
+ "]);"
1818
+ ].join("\n");
1801
1819
  }
1802
1820
 
1803
1821
  function buildReplacementsFromSnapshot({
@@ -1811,39 +1829,25 @@ function buildReplacementsFromSnapshot({
1811
1829
  const scaffoldColumns = resolveScaffoldColumns(snapshot);
1812
1830
  const outputColumns = scaffoldColumns.filter((column) => !column.isOwnerColumn);
1813
1831
  const writableColumns = scaffoldColumns.filter((column) => column.writable);
1814
- const createRequiredFieldKeys = writableColumns
1815
- .filter((column) => !column.nullable && column.hasDefault !== true)
1816
- .map((column) => column.key);
1817
- const resourceColumns = [...outputColumns, ...writableColumns];
1818
- const fieldMetaEntries = buildFieldMetaEntries({
1832
+ const fieldContractEntries = buildFieldContractEntries({
1819
1833
  outputColumns,
1820
1834
  writableColumns,
1821
1835
  snapshot
1822
1836
  });
1823
- const needsFiniteInteger = resourceColumns.some((column) => column.typeKind === "integer" && column.isRecordIdColumn !== true);
1824
- const needsRecordIdSchemas = resourceColumns.some((column) => column.typeKind === "integer" && column.isRecordIdColumn === true);
1825
- const needsFiniteNumber = resourceColumns.some((column) => column.typeKind === "number");
1826
- const needsDateTimeOutput = outputColumns.some((column) => column.typeKind === "datetime");
1827
- const needsDateTimeInput = writableColumns.some((column) => column.typeKind === "datetime");
1828
- const needsNullableDateTimeInput = writableColumns.some(
1829
- (column) => column.typeKind === "datetime" && column.nullable === true
1830
- );
1831
- const needsNullableDateInput = writableColumns.some(
1832
- (column) => column.typeKind === "date" && column.nullable === true
1833
- );
1834
- const htmlTimeSchemaImports = resolveHtmlTimeSchemaImports(resourceColumns);
1835
- const needsDate = resourceColumns.some((column) => column.typeKind === "date");
1836
- const needsJson = resourceColumns.some((column) => column.typeKind === "json");
1837
- const needsNormalizeText = resourceColumns.some((column) =>
1838
- column.typeKind === "string" || column.typeKind === "time"
1839
- ) || needsNullableDateTimeInput || needsNullableDateInput;
1840
- const needsNormalizeBoolean = resourceColumns.some((column) => column.typeKind === "boolean");
1841
- const needsNormalizeIfInSource = writableColumns.length > 0;
1842
- const outputColumnsWithNormalizer = outputColumns.filter(
1843
- (column) => Boolean(renderOutputNormalizerExpression(column))
1844
- );
1845
- const needsNormalizeIfPresent = outputColumnsWithNormalizer.some((column) => column.nullable !== true);
1846
- const needsNormalizeOrNull = outputColumnsWithNormalizer.some((column) => column.nullable === true);
1837
+ const actionInputExpressions = renderActionInputExpressions({
1838
+ surfaceRequiresWorkspace
1839
+ });
1840
+ const resourceSchemaColumns = scaffoldColumns;
1841
+ const resourceSchemaPropertyLines = renderCanonicalResourceSchemaPropertyLines(resourceSchemaColumns, {
1842
+ fieldContractEntries
1843
+ });
1844
+ const resourceSearchSchemaLines = renderJsonRestSearchSchemaLines(resourceSchemaColumns);
1845
+ const resourceDefaultSortLiteral = renderResourceDefaultSortLiteral(resourceSchemaColumns);
1846
+ const jsonRestSchemaPropertyLines = renderJsonRestSchemaPropertyLines(resourceSchemaColumns, {
1847
+ fieldContractEntries
1848
+ });
1849
+ const jsonRestSearchSchemaLines = renderJsonRestSearchSchemaLines(resourceSchemaColumns);
1850
+ const jsonRestDefaultSortLine = renderJsonRestDefaultSortLine(resourceSchemaColumns);
1847
1851
 
1848
1852
  const replacements = Object.freeze({
1849
1853
  __JSKIT_CRUD_TABLE_NAME__: JSON.stringify(snapshot.tableName),
@@ -1856,36 +1860,26 @@ function buildReplacementsFromSnapshot({
1856
1860
  __JSKIT_CRUD_ACTION_WORKSPACE_VALIDATOR_IMPORT__: renderActionWorkspaceValidatorImport({
1857
1861
  surfaceRequiresWorkspace
1858
1862
  }),
1863
+ __JSKIT_CRUD_LIST_ACTION_INPUT__: actionInputExpressions.list,
1864
+ __JSKIT_CRUD_VIEW_ACTION_INPUT__: actionInputExpressions.view,
1865
+ __JSKIT_CRUD_CREATE_ACTION_INPUT__: actionInputExpressions.create,
1866
+ __JSKIT_CRUD_UPDATE_ACTION_INPUT__: actionInputExpressions.update,
1867
+ __JSKIT_CRUD_DELETE_ACTION_INPUT__: actionInputExpressions.delete,
1859
1868
  __JSKIT_CRUD_LIST_ACTION_PERMISSION__: renderActionPermissionExpression("list", {
1860
1869
  requiresNamedPermissions
1861
1870
  }),
1862
- __JSKIT_CRUD_LIST_ACTION_INPUT_VALIDATOR__: renderActionInputValidatorExpression("list", {
1863
- surfaceRequiresWorkspace
1864
- }),
1865
1871
  __JSKIT_CRUD_VIEW_ACTION_PERMISSION__: renderActionPermissionExpression("view", {
1866
1872
  requiresNamedPermissions
1867
1873
  }),
1868
- __JSKIT_CRUD_VIEW_ACTION_INPUT_VALIDATOR__: renderActionInputValidatorExpression("view", {
1869
- surfaceRequiresWorkspace
1870
- }),
1871
1874
  __JSKIT_CRUD_CREATE_ACTION_PERMISSION__: renderActionPermissionExpression("create", {
1872
1875
  requiresNamedPermissions
1873
1876
  }),
1874
- __JSKIT_CRUD_CREATE_ACTION_INPUT_VALIDATOR__: renderActionInputValidatorExpression("create", {
1875
- surfaceRequiresWorkspace
1876
- }),
1877
1877
  __JSKIT_CRUD_UPDATE_ACTION_PERMISSION__: renderActionPermissionExpression("update", {
1878
1878
  requiresNamedPermissions
1879
1879
  }),
1880
- __JSKIT_CRUD_UPDATE_ACTION_INPUT_VALIDATOR__: renderActionInputValidatorExpression("update", {
1881
- surfaceRequiresWorkspace
1882
- }),
1883
1880
  __JSKIT_CRUD_DELETE_ACTION_PERMISSION__: renderActionPermissionExpression("delete", {
1884
1881
  requiresNamedPermissions
1885
1882
  }),
1886
- __JSKIT_CRUD_DELETE_ACTION_INPUT_VALIDATOR__: renderActionInputValidatorExpression("delete", {
1887
- surfaceRequiresWorkspace
1888
- }),
1889
1883
  __JSKIT_CRUD_ROLE_CATALOG_PERMISSION_GRANTS__: renderRoleCatalogPermissionGrants(namespace, {
1890
1884
  requiresNamedPermissions
1891
1885
  }),
@@ -1894,6 +1888,10 @@ function buildReplacementsFromSnapshot({
1894
1888
  __JSKIT_CRUD_ROUTE_WORKSPACE_SUPPORT_IMPORTS__: renderRouteWorkspaceSupportImports({
1895
1889
  surfaceRequiresWorkspace
1896
1890
  }),
1891
+ __JSKIT_CRUD_ROUTE_CONTRACTS_RESOURCE_ARGS__: surfaceRequiresWorkspace ? ",\n routeParamsValidator" : "",
1892
+ __JSKIT_CRUD_ROUTE_VALIDATOR_CONSTANTS__: renderRouteValidatorConstants({
1893
+ surfaceRequiresWorkspace
1894
+ }),
1897
1895
  __JSKIT_CRUD_LIST_ROUTE_PARAMS_VALIDATOR_LINE__: renderRouteParamsValidatorLine("list", {
1898
1896
  surfaceRequiresWorkspace
1899
1897
  }),
@@ -1924,45 +1922,15 @@ function buildReplacementsFromSnapshot({
1924
1922
  __JSKIT_CRUD_DELETE_ROUTE_INPUT_LINES__: renderRouteInputLines("delete", {
1925
1923
  surfaceRequiresWorkspace
1926
1924
  }),
1927
- __JSKIT_CRUD_RESOURCE_VALIDATORS_IMPORT__: renderResourceValidatorsImport({
1928
- htmlTimeSchemaImports,
1929
- recordIdValidatorImports: resolveRecordIdValidatorImports(
1930
- renderResourceSchemaPropertyLines(outputColumns, {
1931
- forOutput: true
1932
- }),
1933
- renderResourceSchemaPropertyLines(writableColumns, {
1934
- forOutput: false
1935
- })
1936
- )
1937
- }),
1938
- __JSKIT_CRUD_RESOURCE_DATABASE_RUNTIME_IMPORT__: renderResourceDatabaseRuntimeImport({
1939
- needsToIsoString: needsDateTimeOutput || needsDate,
1940
- needsToDatabaseDateTimeUtc: needsDateTimeInput
1941
- }),
1942
- __JSKIT_CRUD_RESOURCE_NORMALIZE_SUPPORT_IMPORT__: renderResourceNormalizeSupportImport({
1943
- needsNormalizeText,
1944
- needsNormalizeBoolean,
1945
- needsNormalizeFiniteNumber: needsFiniteNumber,
1946
- needsNormalizeFiniteInteger: needsFiniteInteger,
1947
- needsNormalizeRecordId: needsRecordIdSchemas,
1948
- needsNormalizeIfInSource,
1949
- needsNormalizeIfPresent,
1950
- needsNormalizeOrNull
1951
- }),
1952
- __JSKIT_CRUD_RESOURCE_JSON_IMPORT__: renderResourceJsonImport({
1953
- needsJson
1954
- }),
1955
- __JSKIT_CRUD_RESOURCE_OUTPUT_SCHEMA_PROPERTIES__: renderResourceSchemaPropertyLines(outputColumns, {
1956
- forOutput: true
1957
- }),
1958
- __JSKIT_CRUD_RESOURCE_CREATE_SCHEMA_PROPERTIES__: renderResourceSchemaPropertyLines(writableColumns, {
1959
- forOutput: false
1960
- }),
1961
- __JSKIT_CRUD_RESOURCE_INPUT_NORMALIZATION_LINES__: renderResourceInputNormalizationLines(writableColumns),
1962
- __JSKIT_CRUD_RESOURCE_OUTPUT_NORMALIZATION_LINES__: renderResourceOutputNormalizationLines(outputColumns),
1963
- __JSKIT_CRUD_RESOURCE_CREATE_REQUIRED_FIELDS__: JSON.stringify(createRequiredFieldKeys),
1964
- __JSKIT_CRUD_RESOURCE_FIELD_META_PUSH_LINES__: renderResourceFieldMetaPushLines(fieldMetaEntries),
1965
- __JSKIT_CRUD_LIST_CONFIG_LINES__: renderRepositoryListConfigLines(snapshot),
1925
+ __JSKIT_CRUD_RESOURCE_SCHEMA_PROPERTIES__: resourceSchemaPropertyLines,
1926
+ __JSKIT_CRUD_RESOURCE_SEARCH_SCHEMA_LINES__: resourceSearchSchemaLines,
1927
+ __JSKIT_CRUD_RESOURCE_DEFAULT_SORT__: resourceDefaultSortLiteral,
1928
+ __JSKIT_CRUD_RESOURCE_AUTOFILTER__: JSON.stringify(resolvedOwnershipFilter),
1929
+ __JSKIT_CRUD_JSONREST_SCOPE_NAME__: JSON.stringify(toCamelCase(namespace)),
1930
+ __JSKIT_CRUD_JSONREST_AUTOFILTER__: JSON.stringify(resolvedOwnershipFilter),
1931
+ __JSKIT_CRUD_JSONREST_SEARCH_SCHEMA_LINES__: jsonRestSearchSchemaLines,
1932
+ __JSKIT_CRUD_JSONREST_SCHEMA_PROPERTIES__: jsonRestSchemaPropertyLines,
1933
+ __JSKIT_CRUD_JSONREST_DEFAULT_SORT_LINE__: jsonRestDefaultSortLine,
1966
1934
  __JSKIT_CRUD_MIGRATION_COLUMN_LINES__: renderMigrationColumnLines(snapshot),
1967
1935
  __JSKIT_CRUD_MIGRATION_INDEX_LINES__: renderMigrationIndexLines(snapshot),
1968
1936
  __JSKIT_CRUD_MIGRATION_FOREIGN_KEY_LINES__: renderMigrationForeignKeyLines(snapshot),
@@ -2076,13 +2044,10 @@ const __testables = Object.freeze({
2076
2044
  renderMigrationCheckConstraintLines,
2077
2045
  renderMigrationForeignKeyLine,
2078
2046
  resolveScaffoldColumns,
2079
- renderPropertyAccess,
2080
- renderResourceFieldSchema,
2081
- renderInputNormalizer,
2082
- renderOutputNormalizerExpression,
2083
2047
  resolveCrudGenerationTableName,
2084
2048
  resolveGenerationSnapshot,
2085
- buildFieldMetaEntries,
2049
+ buildFieldContractEntries,
2050
+ renderCanonicalResourceFieldSchema,
2086
2051
  resolveDefaultCrudSurfaceIdFromAppConfig,
2087
2052
  resolveCrudGenerationSurfaceId,
2088
2053
  resolveCrudSurfaceRequiresWorkspace,
@@ -2090,7 +2055,8 @@ const __testables = Object.freeze({
2090
2055
  renderRoleCatalogPermissionGrants,
2091
2056
  renderActionPermissionSupport,
2092
2057
  renderActionPermissionExpression,
2093
- renderActionInputValidatorExpression,
2058
+ renderActionInputExpressions,
2059
+ renderRouteValidatorConstants,
2094
2060
  renderRouteParamsValidatorLine,
2095
2061
  renderRouteInputLines
2096
2062
  });
@@ -2098,12 +2064,9 @@ const __testables = Object.freeze({
2098
2064
  export {
2099
2065
  buildTemplateContext,
2100
2066
  resolveScaffoldColumns,
2101
- renderPropertyAccess,
2102
2067
  resolveGenerationSnapshot,
2103
- renderResourceFieldSchema,
2104
- renderInputNormalizer,
2105
- renderOutputNormalizerExpression,
2106
- buildFieldMetaEntries,
2068
+ renderCanonicalResourceFieldSchema,
2069
+ buildFieldContractEntries,
2107
2070
  resolveCrudGenerationSurfaceId,
2108
2071
  __testables
2109
2072
  };