@jskit-ai/crud-server-generator 0.1.63 → 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)}`);
826
+ }
827
+ if (normalizeText(column?.typeKind).toLowerCase() === "datetime") {
828
+ storageEntries.push('writeSerializer: "datetime-utc"');
789
829
  }
790
- return imports;
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 ? toIsoString(normalized) : null; }";
836
- }
837
- return "toIsoString";
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";
849
921
  }
850
922
 
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";
923
+ function shouldRenderJsonRestSearch(column = {}) {
924
+ return column?.isCreatedAtColumn !== true && column?.isUpdatedAtColumn !== true;
925
+ }
926
+
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,138 +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
- const repositoryWriteSerializer = normalizeText(entry?.repository?.writeSerializer);
1517
- if (repositoryColumn || repositoryWriteSerializer) {
1518
- const repositoryLines = [
1519
- "repository: {",
1520
- ...(repositoryColumn ? [` column: ${JSON.stringify(repositoryColumn)}`] : []),
1521
- ...(repositoryWriteSerializer ? [` writeSerializer: ${JSON.stringify(repositoryWriteSerializer)}`] : [])
1522
- ];
1523
- if (repositoryLines.length > 2) {
1524
- repositoryLines[repositoryLines.length - 1] = repositoryLines[repositoryLines.length - 1].replace(/,$/, "");
1525
- }
1526
- repositoryLines.push("}");
1527
- for (let index = 1; index < repositoryLines.length - 1; index += 1) {
1528
- if (index < repositoryLines.length - 2) {
1529
- repositoryLines[index] = `${repositoryLines[index]},`;
1530
- }
1531
- }
1532
- topLevelProperties.push(repositoryLines.join("\n"));
1533
- }
1534
-
1535
- const relation = entry.relation && typeof entry.relation === "object" ? entry.relation : null;
1536
- if (relation) {
1537
- const targetResourceNamespace = normalizeCrudLookupNamespace(relation.targetResource);
1538
- const relationNamespace =
1539
- normalizeCrudLookupNamespace(relation.namespace) ||
1540
- normalizeCrudLookupNamespace(relation.apiPath) ||
1541
- normalizeCrudLookupNamespace(relation?.source?.path) ||
1542
- targetResourceNamespace;
1543
- if (!relationNamespace) {
1544
- throw new Error(`crud template context fieldMeta["${normalizeText(entry.key)}"] lookup relation requires namespace.`);
1545
- }
1546
- const relationLines = [
1547
- "relation: {",
1548
- ` kind: ${JSON.stringify(normalizeText(relation.kind) || "lookup")},`,
1549
- ` namespace: ${JSON.stringify(relationNamespace)},`,
1550
- ` valueKey: ${JSON.stringify(normalizeText(relation.valueKey) || "id")},`
1551
- ];
1552
- const labelKey = normalizeText(relation.labelKey);
1553
- if (labelKey) {
1554
- relationLines.push(` labelKey: ${JSON.stringify(labelKey)}`);
1555
- } else {
1556
- relationLines[relationLines.length - 1] = relationLines[relationLines.length - 1].replace(/,$/, "");
1557
- }
1558
- relationLines.push("}");
1559
- topLevelProperties.push(relationLines.join("\n"));
1560
- }
1561
-
1562
- const fieldUiOptions = normalizeFieldMetaUiOptions(entry?.ui?.options);
1563
- const formControl = checkCrudLookupFormControl(entry?.ui?.formControl, {
1564
- context: `resource.fieldMeta["${normalizeText(entry.key)}"].ui.formControl`,
1565
- defaultValue: relation ? "autocomplete" : (fieldUiOptions.length > 0 ? "select" : "")
1566
- });
1567
- if (formControl || fieldUiOptions.length > 0) {
1568
- const uiPropertyBlocks = [];
1569
- if (formControl) {
1570
- uiPropertyBlocks.push([
1571
- `formControl: ${JSON.stringify(formControl)}${relation ? " // or \"select\"" : ""}`
1572
- ]);
1573
- }
1574
- if (fieldUiOptions.length > 0) {
1575
- const optionsJsonLines = JSON.stringify(fieldUiOptions, null, 2).split("\n");
1576
- const optionPropertyLines = [`options: ${optionsJsonLines[0]}`];
1577
- for (const jsonLine of optionsJsonLines.slice(1)) {
1578
- optionPropertyLines.push(jsonLine);
1579
- }
1580
- uiPropertyBlocks.push(optionPropertyLines);
1581
- }
1582
-
1583
- const uiLines = ["ui: {"];
1584
- for (const [propertyIndex, propertyLines] of uiPropertyBlocks.entries()) {
1585
- const isLastProperty = propertyIndex >= uiPropertyBlocks.length - 1;
1586
- const propertySuffix = isLastProperty ? "" : ",";
1587
- for (const [lineIndex, line] of propertyLines.entries()) {
1588
- const isLastLine = lineIndex >= propertyLines.length - 1;
1589
- uiLines.push(` ${line}${isLastLine ? propertySuffix : ""}`);
1590
- }
1591
- }
1592
- uiLines.push("}");
1593
- topLevelProperties.push(
1594
- uiLines.join("\n")
1595
- );
1596
- }
1597
-
1598
- for (const [index, propertyBlock] of topLevelProperties.entries()) {
1599
- const blockLines = String(propertyBlock || "").split("\n");
1600
- const isLastProperty = index >= topLevelProperties.length - 1;
1601
- const propertySuffix = isLastProperty ? "" : ",";
1602
- for (const [lineIndex, line] of blockLines.entries()) {
1603
- const isLastLine = lineIndex >= blockLines.length - 1;
1604
- lines.push(` ${line}${isLastLine ? propertySuffix : ""}`);
1605
- }
1606
- }
1607
-
1608
- lines.push("});");
1609
- return lines.join("\n");
1610
- }
1611
-
1612
- function renderResourceFieldMetaPushLines(entries = []) {
1613
- const sourceEntries = Array.isArray(entries) ? entries : [];
1614
- if (sourceEntries.length < 1) {
1615
- return "";
1616
- }
1617
-
1618
- return sourceEntries.map((entry) => renderFieldMetaEntryLines(entry)).join("\n\n");
1619
- }
1620
-
1621
- function renderRepositoryListConfigLines(snapshot = {}) {
1622
- const commentLines = [
1623
- " // defaultLimit: 20,",
1624
- " // maxLimit: 100,",
1625
- " // searchColumns: [\"name\"],"
1626
- ];
1627
- const sourceColumns = Array.isArray(snapshot?.columns) ? snapshot.columns : [];
1628
- const hasCreatedAtColumn = sourceColumns.some((column = {}) => normalizeText(column?.name) === "created_at");
1629
- if (!hasCreatedAtColumn) {
1630
- return commentLines.join("\n");
1631
- }
1632
-
1633
- return [
1634
- ...commentLines,
1635
- " orderBy: [",
1636
- " {",
1637
- " column: \"created_at\",",
1638
- " direction: \"desc\"",
1639
- " }",
1640
- " ]"
1641
- ].join("\n");
1642
- }
1643
-
1644
1598
  function buildCrudPermissionIds(namespace = "") {
1645
1599
  const permissionNamespace = toSnakeCase(namespace);
1646
1600
  if (!permissionNamespace) {
@@ -1739,14 +1693,10 @@ function renderRouteParamsValidatorLine(operation = "", { surfaceRequiresWorkspa
1739
1693
  if (!surfaceRequiresWorkspace) {
1740
1694
  return "";
1741
1695
  }
1742
- return " paramsValidator: routeParamsValidator,";
1743
- }
1744
-
1745
- if (!surfaceRequiresWorkspace) {
1746
- return " paramsValidator: recordIdParamsValidator,";
1696
+ return " params: routeParamsValidator,";
1747
1697
  }
1748
1698
 
1749
- return " paramsValidator: [routeParamsValidator, recordIdParamsValidator],";
1699
+ return " params: recordRouteParamsValidator,";
1750
1700
  }
1751
1701
 
1752
1702
  function renderRouteInputLines(operation = "", { surfaceRequiresWorkspace = true } = {}) {
@@ -1769,13 +1719,13 @@ function renderRouteInputLines(operation = "", { surfaceRequiresWorkspace = true
1769
1719
  }
1770
1720
 
1771
1721
  if (normalizedOperation === "create") {
1772
- lines.push(" payload: request.input.body");
1722
+ lines.push(" ...(request.input.body || {})");
1773
1723
  return lines.join("\n");
1774
1724
  }
1775
1725
 
1776
1726
  if (normalizedOperation === "update") {
1777
1727
  lines.push(" recordId: request.input.params.recordId,");
1778
- lines.push(" patch: request.input.body");
1728
+ lines.push(" ...(request.input.body || {})");
1779
1729
  return lines.join("\n");
1780
1730
  }
1781
1731
 
@@ -1783,32 +1733,89 @@ function renderRouteInputLines(operation = "", { surfaceRequiresWorkspace = true
1783
1733
  return lines.join("\n");
1784
1734
  }
1785
1735
 
1786
- function renderActionInputValidatorExpression(operation = "", { surfaceRequiresWorkspace = true } = {}) {
1787
- const normalizedOperation = normalizeCrudOperation(operation, "CRUD action input validator operation");
1788
- 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);
1789
1741
 
1790
- if (surfaceRequiresWorkspace) {
1791
- validators.push("workspaceSlugParamsValidator");
1742
+ if (entries.length < 1) {
1743
+ throw new TypeError("renderObjectSchemaDefinition requires at least one schema definition.");
1792
1744
  }
1793
1745
 
1794
- if (normalizedOperation === "list") {
1795
- validators.push(
1796
- "listCursorPaginationQueryValidator",
1797
- "listSearchQueryValidator",
1798
- "listParentFilterQueryValidator",
1799
- "lookupIncludeQueryValidator"
1800
- );
1801
- } else if (normalizedOperation === "view") {
1802
- validators.push("recordIdParamsValidator", "lookupIncludeQueryValidator");
1803
- } else if (normalizedOperation === "create") {
1804
- validators.push("{ payload: resource.operations.create.bodyValidator }");
1805
- } else if (normalizedOperation === "update") {
1806
- validators.push("recordIdParamsValidator", "{ patch: resource.operations.patch.bodyValidator }");
1807
- } else {
1808
- validators.push("recordIdParamsValidator");
1746
+ if (entries.length === 1) {
1747
+ return entries[0];
1748
+ }
1749
+
1750
+ if (normalizeText(mode).toLowerCase() === "patch") {
1751
+ return [
1752
+ "composeSchemaDefinitions([",
1753
+ ...entries.map((line) => ` ${line},`),
1754
+ "])"
1755
+ ].join("\n");
1756
+ }
1757
+
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 "";
1809
1811
  }
1810
1812
 
1811
- return validators.length === 1 ? validators[0] : `[${validators.join(", ")}]`;
1813
+ return [
1814
+ "const recordRouteParamsValidator = composeSchemaDefinitions([",
1815
+ " routeParamsValidator,",
1816
+ " recordIdParamsValidator",
1817
+ "]);"
1818
+ ].join("\n");
1812
1819
  }
1813
1820
 
1814
1821
  function buildReplacementsFromSnapshot({
@@ -1822,38 +1829,25 @@ function buildReplacementsFromSnapshot({
1822
1829
  const scaffoldColumns = resolveScaffoldColumns(snapshot);
1823
1830
  const outputColumns = scaffoldColumns.filter((column) => !column.isOwnerColumn);
1824
1831
  const writableColumns = scaffoldColumns.filter((column) => column.writable);
1825
- const createRequiredFieldKeys = writableColumns
1826
- .filter((column) => !column.nullable && column.hasDefault !== true)
1827
- .map((column) => column.key);
1828
- const resourceColumns = [...outputColumns, ...writableColumns];
1829
- const fieldMetaEntries = buildFieldMetaEntries({
1832
+ const fieldContractEntries = buildFieldContractEntries({
1830
1833
  outputColumns,
1831
1834
  writableColumns,
1832
1835
  snapshot
1833
1836
  });
1834
- const needsFiniteInteger = resourceColumns.some((column) => column.typeKind === "integer" && column.isRecordIdColumn !== true);
1835
- const needsRecordIdSchemas = resourceColumns.some((column) => column.typeKind === "integer" && column.isRecordIdColumn === true);
1836
- const needsFiniteNumber = resourceColumns.some((column) => column.typeKind === "number");
1837
- const needsDateTimeOutput = outputColumns.some((column) => column.typeKind === "datetime");
1838
- const needsNullableDateTimeInput = writableColumns.some(
1839
- (column) => column.typeKind === "datetime" && column.nullable === true
1840
- );
1841
- const needsNullableDateInput = writableColumns.some(
1842
- (column) => column.typeKind === "date" && column.nullable === true
1843
- );
1844
- const htmlTimeSchemaImports = resolveHtmlTimeSchemaImports(resourceColumns);
1845
- const needsDate = resourceColumns.some((column) => column.typeKind === "date");
1846
- const needsJson = resourceColumns.some((column) => column.typeKind === "json");
1847
- const needsNormalizeText = resourceColumns.some((column) =>
1848
- column.typeKind === "string" || column.typeKind === "time"
1849
- ) || needsNullableDateTimeInput || needsNullableDateInput;
1850
- const needsNormalizeBoolean = resourceColumns.some((column) => column.typeKind === "boolean");
1851
- const needsNormalizeIfInSource = writableColumns.length > 0;
1852
- const outputColumnsWithNormalizer = outputColumns.filter(
1853
- (column) => Boolean(renderOutputNormalizerExpression(column))
1854
- );
1855
- const needsNormalizeIfPresent = outputColumnsWithNormalizer.some((column) => column.nullable !== true);
1856
- 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);
1857
1851
 
1858
1852
  const replacements = Object.freeze({
1859
1853
  __JSKIT_CRUD_TABLE_NAME__: JSON.stringify(snapshot.tableName),
@@ -1866,36 +1860,26 @@ function buildReplacementsFromSnapshot({
1866
1860
  __JSKIT_CRUD_ACTION_WORKSPACE_VALIDATOR_IMPORT__: renderActionWorkspaceValidatorImport({
1867
1861
  surfaceRequiresWorkspace
1868
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,
1869
1868
  __JSKIT_CRUD_LIST_ACTION_PERMISSION__: renderActionPermissionExpression("list", {
1870
1869
  requiresNamedPermissions
1871
1870
  }),
1872
- __JSKIT_CRUD_LIST_ACTION_INPUT_VALIDATOR__: renderActionInputValidatorExpression("list", {
1873
- surfaceRequiresWorkspace
1874
- }),
1875
1871
  __JSKIT_CRUD_VIEW_ACTION_PERMISSION__: renderActionPermissionExpression("view", {
1876
1872
  requiresNamedPermissions
1877
1873
  }),
1878
- __JSKIT_CRUD_VIEW_ACTION_INPUT_VALIDATOR__: renderActionInputValidatorExpression("view", {
1879
- surfaceRequiresWorkspace
1880
- }),
1881
1874
  __JSKIT_CRUD_CREATE_ACTION_PERMISSION__: renderActionPermissionExpression("create", {
1882
1875
  requiresNamedPermissions
1883
1876
  }),
1884
- __JSKIT_CRUD_CREATE_ACTION_INPUT_VALIDATOR__: renderActionInputValidatorExpression("create", {
1885
- surfaceRequiresWorkspace
1886
- }),
1887
1877
  __JSKIT_CRUD_UPDATE_ACTION_PERMISSION__: renderActionPermissionExpression("update", {
1888
1878
  requiresNamedPermissions
1889
1879
  }),
1890
- __JSKIT_CRUD_UPDATE_ACTION_INPUT_VALIDATOR__: renderActionInputValidatorExpression("update", {
1891
- surfaceRequiresWorkspace
1892
- }),
1893
1880
  __JSKIT_CRUD_DELETE_ACTION_PERMISSION__: renderActionPermissionExpression("delete", {
1894
1881
  requiresNamedPermissions
1895
1882
  }),
1896
- __JSKIT_CRUD_DELETE_ACTION_INPUT_VALIDATOR__: renderActionInputValidatorExpression("delete", {
1897
- surfaceRequiresWorkspace
1898
- }),
1899
1883
  __JSKIT_CRUD_ROLE_CATALOG_PERMISSION_GRANTS__: renderRoleCatalogPermissionGrants(namespace, {
1900
1884
  requiresNamedPermissions
1901
1885
  }),
@@ -1904,6 +1888,10 @@ function buildReplacementsFromSnapshot({
1904
1888
  __JSKIT_CRUD_ROUTE_WORKSPACE_SUPPORT_IMPORTS__: renderRouteWorkspaceSupportImports({
1905
1889
  surfaceRequiresWorkspace
1906
1890
  }),
1891
+ __JSKIT_CRUD_ROUTE_CONTRACTS_RESOURCE_ARGS__: surfaceRequiresWorkspace ? ",\n routeParamsValidator" : "",
1892
+ __JSKIT_CRUD_ROUTE_VALIDATOR_CONSTANTS__: renderRouteValidatorConstants({
1893
+ surfaceRequiresWorkspace
1894
+ }),
1907
1895
  __JSKIT_CRUD_LIST_ROUTE_PARAMS_VALIDATOR_LINE__: renderRouteParamsValidatorLine("list", {
1908
1896
  surfaceRequiresWorkspace
1909
1897
  }),
@@ -1934,45 +1922,15 @@ function buildReplacementsFromSnapshot({
1934
1922
  __JSKIT_CRUD_DELETE_ROUTE_INPUT_LINES__: renderRouteInputLines("delete", {
1935
1923
  surfaceRequiresWorkspace
1936
1924
  }),
1937
- __JSKIT_CRUD_RESOURCE_VALIDATORS_IMPORT__: renderResourceValidatorsImport({
1938
- htmlTimeSchemaImports,
1939
- recordIdValidatorImports: resolveRecordIdValidatorImports(
1940
- renderResourceSchemaPropertyLines(outputColumns, {
1941
- forOutput: true
1942
- }),
1943
- renderResourceSchemaPropertyLines(writableColumns, {
1944
- forOutput: false
1945
- })
1946
- )
1947
- }),
1948
- __JSKIT_CRUD_RESOURCE_DATABASE_RUNTIME_IMPORT__: renderResourceDatabaseRuntimeImport({
1949
- needsToIsoString: needsDateTimeOutput || needsDate || writableColumns.some((column) => column.typeKind === "datetime"),
1950
- needsToDatabaseDateTimeUtc: false
1951
- }),
1952
- __JSKIT_CRUD_RESOURCE_NORMALIZE_SUPPORT_IMPORT__: renderResourceNormalizeSupportImport({
1953
- needsNormalizeText,
1954
- needsNormalizeBoolean,
1955
- needsNormalizeFiniteNumber: needsFiniteNumber,
1956
- needsNormalizeFiniteInteger: needsFiniteInteger,
1957
- needsNormalizeRecordId: needsRecordIdSchemas,
1958
- needsNormalizeIfInSource,
1959
- needsNormalizeIfPresent,
1960
- needsNormalizeOrNull
1961
- }),
1962
- __JSKIT_CRUD_RESOURCE_JSON_IMPORT__: renderResourceJsonImport({
1963
- needsJson
1964
- }),
1965
- __JSKIT_CRUD_RESOURCE_OUTPUT_SCHEMA_PROPERTIES__: renderResourceSchemaPropertyLines(outputColumns, {
1966
- forOutput: true
1967
- }),
1968
- __JSKIT_CRUD_RESOURCE_CREATE_SCHEMA_PROPERTIES__: renderResourceSchemaPropertyLines(writableColumns, {
1969
- forOutput: false
1970
- }),
1971
- __JSKIT_CRUD_RESOURCE_INPUT_NORMALIZATION_LINES__: renderResourceInputNormalizationLines(writableColumns),
1972
- __JSKIT_CRUD_RESOURCE_OUTPUT_NORMALIZATION_LINES__: renderResourceOutputNormalizationLines(outputColumns),
1973
- __JSKIT_CRUD_RESOURCE_CREATE_REQUIRED_FIELDS__: JSON.stringify(createRequiredFieldKeys),
1974
- __JSKIT_CRUD_RESOURCE_FIELD_META_PUSH_LINES__: renderResourceFieldMetaPushLines(fieldMetaEntries),
1975
- __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,
1976
1934
  __JSKIT_CRUD_MIGRATION_COLUMN_LINES__: renderMigrationColumnLines(snapshot),
1977
1935
  __JSKIT_CRUD_MIGRATION_INDEX_LINES__: renderMigrationIndexLines(snapshot),
1978
1936
  __JSKIT_CRUD_MIGRATION_FOREIGN_KEY_LINES__: renderMigrationForeignKeyLines(snapshot),
@@ -2086,13 +2044,10 @@ const __testables = Object.freeze({
2086
2044
  renderMigrationCheckConstraintLines,
2087
2045
  renderMigrationForeignKeyLine,
2088
2046
  resolveScaffoldColumns,
2089
- renderPropertyAccess,
2090
- renderResourceFieldSchema,
2091
- renderInputNormalizer,
2092
- renderOutputNormalizerExpression,
2093
2047
  resolveCrudGenerationTableName,
2094
2048
  resolveGenerationSnapshot,
2095
- buildFieldMetaEntries,
2049
+ buildFieldContractEntries,
2050
+ renderCanonicalResourceFieldSchema,
2096
2051
  resolveDefaultCrudSurfaceIdFromAppConfig,
2097
2052
  resolveCrudGenerationSurfaceId,
2098
2053
  resolveCrudSurfaceRequiresWorkspace,
@@ -2100,7 +2055,8 @@ const __testables = Object.freeze({
2100
2055
  renderRoleCatalogPermissionGrants,
2101
2056
  renderActionPermissionSupport,
2102
2057
  renderActionPermissionExpression,
2103
- renderActionInputValidatorExpression,
2058
+ renderActionInputExpressions,
2059
+ renderRouteValidatorConstants,
2104
2060
  renderRouteParamsValidatorLine,
2105
2061
  renderRouteInputLines
2106
2062
  });
@@ -2108,12 +2064,9 @@ const __testables = Object.freeze({
2108
2064
  export {
2109
2065
  buildTemplateContext,
2110
2066
  resolveScaffoldColumns,
2111
- renderPropertyAccess,
2112
2067
  resolveGenerationSnapshot,
2113
- renderResourceFieldSchema,
2114
- renderInputNormalizer,
2115
- renderOutputNormalizerExpression,
2116
- buildFieldMetaEntries,
2068
+ renderCanonicalResourceFieldSchema,
2069
+ buildFieldContractEntries,
2117
2070
  resolveCrudGenerationSurfaceId,
2118
2071
  __testables
2119
2072
  };