@famgia/omnify-core 0.0.10 → 0.0.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -30,6 +30,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
30
30
  // src/index.ts
31
31
  var index_exports = {};
32
32
  __export(index_exports, {
33
+ FILE_SCHEMA_NAME: () => FILE_SCHEMA_NAME,
33
34
  Omnify: () => Omnify,
34
35
  OmnifyError: () => OmnifyError,
35
36
  PluginManager: () => PluginManager,
@@ -39,10 +40,13 @@ __export(index_exports, {
39
40
  circularReferenceError: () => circularReferenceError,
40
41
  configError: () => configError,
41
42
  configNotFoundError: () => configNotFoundError,
43
+ createFileLoadedSchema: () => createFileLoadedSchema,
44
+ createFileSchemaDefinition: () => createFileSchemaDefinition,
42
45
  createOmnify: () => createOmnify,
43
46
  createPluginManager: () => createPluginManager,
44
47
  createVersionStore: () => createVersionStore,
45
48
  duplicateSchemaError: () => duplicateSchemaError,
49
+ ensureFileSchema: () => ensureFileSchema,
46
50
  err: () => import_omnify_types.err,
47
51
  expandProperty: () => expandProperty,
48
52
  expandSchema: () => expandSchema,
@@ -54,6 +58,7 @@ __export(index_exports, {
54
58
  formatError: () => formatError,
55
59
  formatErrorPlain: () => formatErrorPlain,
56
60
  formatErrorSummary: () => formatErrorSummary,
61
+ generateFileSchemaYaml: () => generateFileSchemaYaml,
57
62
  generationError: () => generationError,
58
63
  getAssociationMetadata: () => getAssociationMetadata,
59
64
  getCustomTypeNames: () => getCustomTypeNames,
@@ -92,7 +97,9 @@ __export(index_exports, {
92
97
  pluginTypeConflictError: () => pluginTypeConflictError,
93
98
  schemaNotFoundError: () => schemaNotFoundError,
94
99
  schemaParseError: () => schemaParseError,
100
+ schemasHaveFileProperties: () => schemasHaveFileProperties,
95
101
  validateAssociations: () => validateAssociations,
102
+ validateDefaultValue: () => validateDefaultValue,
96
103
  validateEnumSchema: () => validateEnumSchema,
97
104
  validateOptions: () => validateOptions,
98
105
  validateProperties: () => validateProperties,
@@ -595,8 +602,40 @@ function parseJsonSchema(content, filePath) {
595
602
  throw error;
596
603
  }
597
604
  }
605
+ var VALID_SCHEMA_FIELDS = /* @__PURE__ */ new Set([
606
+ "kind",
607
+ "displayName",
608
+ "titleIndex",
609
+ "group",
610
+ "options",
611
+ "properties",
612
+ "values"
613
+ ]);
614
+ var VALID_SCHEMA_OPTIONS_FIELDS = /* @__PURE__ */ new Set([
615
+ "id",
616
+ "idType",
617
+ "timestamps",
618
+ "softDelete",
619
+ "unique",
620
+ "indexes",
621
+ "translations",
622
+ "tableName",
623
+ "authenticatable",
624
+ "authenticatableLoginIdField",
625
+ "authenticatablePasswordField",
626
+ "authenticatableGuardName"
627
+ ]);
598
628
  function buildSchemaDefinition(data) {
599
629
  const schema = {};
630
+ const unknownFields = [];
631
+ for (const key of Object.keys(data)) {
632
+ if (!VALID_SCHEMA_FIELDS.has(key)) {
633
+ unknownFields.push(key);
634
+ }
635
+ }
636
+ if (unknownFields.length > 0) {
637
+ schema._unknownFields = unknownFields;
638
+ }
600
639
  if (data.kind !== void 0) {
601
640
  schema.kind = data.kind;
602
641
  }
@@ -610,7 +649,17 @@ function buildSchemaDefinition(data) {
610
649
  schema.group = data.group;
611
650
  }
612
651
  if (data.options !== void 0 && typeof data.options === "object") {
613
- const options = buildSchemaOptions(data.options);
652
+ const optionsData = data.options;
653
+ const unknownOptionsFields = [];
654
+ for (const key of Object.keys(optionsData)) {
655
+ if (!VALID_SCHEMA_OPTIONS_FIELDS.has(key)) {
656
+ unknownOptionsFields.push(key);
657
+ }
658
+ }
659
+ if (unknownOptionsFields.length > 0) {
660
+ schema._unknownOptionsFields = unknownOptionsFields;
661
+ }
662
+ const options = buildSchemaOptions(optionsData);
614
663
  if (Object.keys(options).length > 0) {
615
664
  schema.options = options;
616
665
  }
@@ -675,10 +724,53 @@ function buildProperties(data) {
675
724
  }
676
725
  return properties;
677
726
  }
727
+ var VALID_PROPERTY_FIELDS = /* @__PURE__ */ new Set([
728
+ "type",
729
+ "displayName",
730
+ "nullable",
731
+ "default",
732
+ "unique",
733
+ "description",
734
+ "renamedFrom",
735
+ // String properties
736
+ "length",
737
+ // Numeric properties
738
+ "unsigned",
739
+ "precision",
740
+ "scale",
741
+ // Enum properties
742
+ "enum",
743
+ // Association properties
744
+ "relation",
745
+ "target",
746
+ "targets",
747
+ "morphName",
748
+ "inversedBy",
749
+ "mappedBy",
750
+ "onDelete",
751
+ "onUpdate",
752
+ "owning",
753
+ "joinTable",
754
+ "pivotFields",
755
+ // File properties
756
+ "multiple",
757
+ "maxFiles",
758
+ "accept",
759
+ "maxSize"
760
+ ]);
678
761
  function buildPropertyDefinition(data) {
679
762
  const prop = {
680
763
  type: data.type || "String"
681
764
  };
765
+ const unknownFields = [];
766
+ for (const key of Object.keys(data)) {
767
+ if (!VALID_PROPERTY_FIELDS.has(key)) {
768
+ unknownFields.push(key);
769
+ }
770
+ }
771
+ if (unknownFields.length > 0) {
772
+ prop._unknownFields = unknownFields;
773
+ }
682
774
  if (data.displayName !== void 0 && typeof data.displayName === "string") {
683
775
  prop.displayName = data.displayName;
684
776
  }
@@ -712,12 +804,6 @@ function buildPropertyDefinition(data) {
712
804
  if (data.enum !== void 0) {
713
805
  prop.enum = data.enum;
714
806
  }
715
- if (data.options !== void 0 && Array.isArray(data.options)) {
716
- prop.options = data.options;
717
- }
718
- if (data.source !== void 0 && typeof data.source === "string") {
719
- prop.source = data.source;
720
- }
721
807
  if (data.relation !== void 0 && typeof data.relation === "string") {
722
808
  prop.relation = data.relation;
723
809
  }
@@ -742,6 +828,27 @@ function buildPropertyDefinition(data) {
742
828
  if (data.joinTable !== void 0 && typeof data.joinTable === "string") {
743
829
  prop.joinTable = data.joinTable;
744
830
  }
831
+ if (data.targets !== void 0 && Array.isArray(data.targets)) {
832
+ prop.targets = data.targets;
833
+ }
834
+ if (data.morphName !== void 0 && typeof data.morphName === "string") {
835
+ prop.morphName = data.morphName;
836
+ }
837
+ if (data.pivotFields !== void 0 && typeof data.pivotFields === "object") {
838
+ prop.pivotFields = data.pivotFields;
839
+ }
840
+ if (data.multiple !== void 0 && typeof data.multiple === "boolean") {
841
+ prop.multiple = data.multiple;
842
+ }
843
+ if (data.maxFiles !== void 0 && typeof data.maxFiles === "number") {
844
+ prop.maxFiles = data.maxFiles;
845
+ }
846
+ if (data.accept !== void 0 && Array.isArray(data.accept)) {
847
+ prop.accept = data.accept;
848
+ }
849
+ if (data.maxSize !== void 0 && typeof data.maxSize === "number") {
850
+ prop.maxSize = data.maxSize;
851
+ }
745
852
  return prop;
746
853
  }
747
854
  async function loadSchema(filePath, options = {}) {
@@ -817,6 +924,141 @@ async function loadSchemas(directoryPath, options = {}) {
817
924
  }
818
925
  return schemas;
819
926
  }
927
+ var FILE_SCHEMA_NAME = "File";
928
+ function schemasHaveFileProperties(schemas) {
929
+ for (const schema of Object.values(schemas)) {
930
+ if (!schema.properties) continue;
931
+ for (const prop of Object.values(schema.properties)) {
932
+ if (prop.type === "File") {
933
+ return true;
934
+ }
935
+ }
936
+ }
937
+ return false;
938
+ }
939
+ function createFileSchemaDefinition() {
940
+ return {
941
+ displayName: "File",
942
+ options: {
943
+ timestamps: true,
944
+ tableName: "files",
945
+ indexes: [
946
+ {
947
+ columns: ["fileableType", "fileableId", "fileableField"],
948
+ name: "files_fileable_index"
949
+ }
950
+ ]
951
+ },
952
+ properties: {
953
+ path: {
954
+ type: "String",
955
+ displayName: "Storage Path"
956
+ },
957
+ disk: {
958
+ type: "String",
959
+ displayName: "Storage Disk",
960
+ default: "local"
961
+ },
962
+ size: {
963
+ type: "BigInt",
964
+ displayName: "File Size (bytes)",
965
+ unsigned: true
966
+ },
967
+ mimeType: {
968
+ type: "String",
969
+ displayName: "MIME Type"
970
+ },
971
+ fileableType: {
972
+ type: "String",
973
+ displayName: "Fileable Type"
974
+ },
975
+ fileableId: {
976
+ type: "BigInt",
977
+ displayName: "Fileable ID",
978
+ unsigned: true
979
+ },
980
+ fileableField: {
981
+ type: "String",
982
+ displayName: "Fileable Field"
983
+ }
984
+ }
985
+ };
986
+ }
987
+ function createFileLoadedSchema(schemasDir) {
988
+ const definition = createFileSchemaDefinition();
989
+ const filePath = path.join(schemasDir, "File.yaml");
990
+ return {
991
+ ...definition,
992
+ name: FILE_SCHEMA_NAME,
993
+ filePath,
994
+ relativePath: "File.yaml"
995
+ };
996
+ }
997
+ function generateFileSchemaYaml() {
998
+ return `# File Schema - Polymorphic file storage
999
+ # Auto-generated by Omnify when File type is detected
1000
+ # You can customize this schema as needed
1001
+
1002
+ displayName: File
1003
+
1004
+ options:
1005
+ timestamps: true
1006
+ tableName: files
1007
+ indexes:
1008
+ - columns: [fileableType, fileableId, fileableField]
1009
+ name: files_fileable_index
1010
+
1011
+ properties:
1012
+ path:
1013
+ type: String
1014
+ displayName: Storage Path
1015
+
1016
+ disk:
1017
+ type: String
1018
+ displayName: Storage Disk
1019
+ default: local
1020
+
1021
+ size:
1022
+ type: BigInt
1023
+ displayName: File Size (bytes)
1024
+ unsigned: true
1025
+
1026
+ mimeType:
1027
+ type: String
1028
+ displayName: MIME Type
1029
+
1030
+ fileableType:
1031
+ type: String
1032
+ displayName: Fileable Type
1033
+
1034
+ fileableId:
1035
+ type: BigInt
1036
+ displayName: Fileable ID
1037
+ unsigned: true
1038
+
1039
+ fileableField:
1040
+ type: String
1041
+ displayName: Fileable Field
1042
+ `;
1043
+ }
1044
+ async function ensureFileSchema(schemas, schemasDir, autoCreate = false) {
1045
+ if (!schemasHaveFileProperties(schemas)) {
1046
+ return schemas;
1047
+ }
1048
+ if (schemas[FILE_SCHEMA_NAME]) {
1049
+ return schemas;
1050
+ }
1051
+ const fileSchema = createFileLoadedSchema(schemasDir);
1052
+ if (autoCreate) {
1053
+ const filePath = path.join(schemasDir, "File.yaml");
1054
+ const yamlContent = generateFileSchemaYaml();
1055
+ await fs.writeFile(filePath, yamlContent, "utf-8");
1056
+ }
1057
+ return {
1058
+ [FILE_SCHEMA_NAME]: fileSchema,
1059
+ ...schemas
1060
+ };
1061
+ }
820
1062
 
821
1063
  // src/validation/types/base.ts
822
1064
  var BASE_PROPERTY_FIELDS = [
@@ -928,6 +1170,16 @@ var TypeRegistry = class {
928
1170
  var typeRegistry = new TypeRegistry();
929
1171
 
930
1172
  // src/validation/types/primitives.ts
1173
+ function validateBooleanDefault(value) {
1174
+ if (value === void 0 || value === null || value === "") {
1175
+ return { valid: true };
1176
+ }
1177
+ const strValue = String(value).toLowerCase();
1178
+ if (!["true", "false", "1", "0"].includes(strValue)) {
1179
+ return { valid: false, error: "Must be true or false" };
1180
+ }
1181
+ return { valid: true };
1182
+ }
931
1183
  var BooleanType = {
932
1184
  name: "Boolean",
933
1185
  category: "primitive",
@@ -935,13 +1187,27 @@ var BooleanType = {
935
1187
  dbCompatibility: fullDbCompatibility(),
936
1188
  validate() {
937
1189
  return [];
938
- }
1190
+ },
1191
+ validateDefaultValue: validateBooleanDefault
939
1192
  };
940
1193
  var primitiveTypes = [
941
1194
  BooleanType
942
1195
  ];
943
1196
 
944
1197
  // src/validation/types/text.ts
1198
+ function validateTextDefault() {
1199
+ return { valid: true };
1200
+ }
1201
+ function validateEmailDefault(value) {
1202
+ if (value === void 0 || value === null || value === "") {
1203
+ return { valid: true };
1204
+ }
1205
+ const strValue = String(value);
1206
+ if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(strValue)) {
1207
+ return { valid: false, error: "Must be a valid email address" };
1208
+ }
1209
+ return { valid: true };
1210
+ }
945
1211
  function buildLocation2(file) {
946
1212
  return { file };
947
1213
  }
@@ -970,7 +1236,8 @@ var StringType = {
970
1236
  },
971
1237
  validate(propertyName, property, filePath) {
972
1238
  return validateLength(propertyName, property, filePath);
973
- }
1239
+ },
1240
+ validateDefaultValue: validateTextDefault
974
1241
  };
975
1242
  var TextType = {
976
1243
  name: "Text",
@@ -979,7 +1246,8 @@ var TextType = {
979
1246
  dbCompatibility: fullDbCompatibility(),
980
1247
  validate() {
981
1248
  return [];
982
- }
1249
+ },
1250
+ validateDefaultValue: validateTextDefault
983
1251
  };
984
1252
  var LongTextType = {
985
1253
  name: "LongText",
@@ -994,7 +1262,8 @@ var LongTextType = {
994
1262
  },
995
1263
  validate() {
996
1264
  return [];
997
- }
1265
+ },
1266
+ validateDefaultValue: validateTextDefault
998
1267
  };
999
1268
  var EmailType = {
1000
1269
  name: "Email",
@@ -1003,7 +1272,8 @@ var EmailType = {
1003
1272
  dbCompatibility: fullDbCompatibility(),
1004
1273
  validate(propertyName, property, filePath) {
1005
1274
  return validateLength(propertyName, property, filePath);
1006
- }
1275
+ },
1276
+ validateDefaultValue: validateEmailDefault
1007
1277
  };
1008
1278
  var PasswordType = {
1009
1279
  name: "Password",
@@ -1012,7 +1282,8 @@ var PasswordType = {
1012
1282
  dbCompatibility: fullDbCompatibility(),
1013
1283
  validate(propertyName, property, filePath) {
1014
1284
  return validateLength(propertyName, property, filePath);
1015
- }
1285
+ },
1286
+ validateDefaultValue: validateTextDefault
1016
1287
  };
1017
1288
  var textTypes = [
1018
1289
  StringType,
@@ -1023,6 +1294,26 @@ var textTypes = [
1023
1294
  ];
1024
1295
 
1025
1296
  // src/validation/types/numeric.ts
1297
+ function validateIntegerDefault(value) {
1298
+ if (value === void 0 || value === null || value === "") {
1299
+ return { valid: true };
1300
+ }
1301
+ const strValue = String(value);
1302
+ if (!/^-?\d+$/.test(strValue)) {
1303
+ return { valid: false, error: "Must be an integer" };
1304
+ }
1305
+ return { valid: true };
1306
+ }
1307
+ function validateNumberDefault(value) {
1308
+ if (value === void 0 || value === null || value === "") {
1309
+ return { valid: true };
1310
+ }
1311
+ const strValue = String(value);
1312
+ if (!/^-?\d+(\.\d+)?$/.test(strValue)) {
1313
+ return { valid: false, error: "Must be a number" };
1314
+ }
1315
+ return { valid: true };
1316
+ }
1026
1317
  function buildLocation3(file) {
1027
1318
  return { file };
1028
1319
  }
@@ -1033,7 +1324,8 @@ var IntType = {
1033
1324
  dbCompatibility: fullDbCompatibility(),
1034
1325
  validate() {
1035
1326
  return [];
1036
- }
1327
+ },
1328
+ validateDefaultValue: validateIntegerDefault
1037
1329
  };
1038
1330
  var BigIntType = {
1039
1331
  name: "BigInt",
@@ -1042,7 +1334,8 @@ var BigIntType = {
1042
1334
  dbCompatibility: fullDbCompatibility(),
1043
1335
  validate() {
1044
1336
  return [];
1045
- }
1337
+ },
1338
+ validateDefaultValue: validateIntegerDefault
1046
1339
  };
1047
1340
  var FloatType = {
1048
1341
  name: "Float",
@@ -1051,7 +1344,8 @@ var FloatType = {
1051
1344
  dbCompatibility: fullDbCompatibility(),
1052
1345
  validate() {
1053
1346
  return [];
1054
- }
1347
+ },
1348
+ validateDefaultValue: validateNumberDefault
1055
1349
  };
1056
1350
  var DecimalType = {
1057
1351
  name: "Decimal",
@@ -1103,7 +1397,8 @@ var DecimalType = {
1103
1397
  }
1104
1398
  }
1105
1399
  return errors;
1106
- }
1400
+ },
1401
+ validateDefaultValue: validateNumberDefault
1107
1402
  };
1108
1403
  var numericTypes = [
1109
1404
  IntType,
@@ -1113,6 +1408,36 @@ var numericTypes = [
1113
1408
  ];
1114
1409
 
1115
1410
  // src/validation/types/temporal.ts
1411
+ function validateDateDefault(value) {
1412
+ if (value === void 0 || value === null || value === "") {
1413
+ return { valid: true };
1414
+ }
1415
+ const strValue = String(value);
1416
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(strValue)) {
1417
+ return { valid: false, error: "Must be in YYYY-MM-DD format" };
1418
+ }
1419
+ return { valid: true };
1420
+ }
1421
+ function validateTimeDefault(value) {
1422
+ if (value === void 0 || value === null || value === "") {
1423
+ return { valid: true };
1424
+ }
1425
+ const strValue = String(value);
1426
+ if (!/^\d{2}:\d{2}(:\d{2})?$/.test(strValue)) {
1427
+ return { valid: false, error: "Must be in HH:MM or HH:MM:SS format" };
1428
+ }
1429
+ return { valid: true };
1430
+ }
1431
+ function validateTimestampDefault(value) {
1432
+ if (value === void 0 || value === null || value === "") {
1433
+ return { valid: true };
1434
+ }
1435
+ const strValue = String(value);
1436
+ if (!/^\d{4}-\d{2}-\d{2}(T|\s)\d{2}:\d{2}(:\d{2})?/.test(strValue)) {
1437
+ return { valid: false, error: "Must be in YYYY-MM-DD HH:MM:SS format" };
1438
+ }
1439
+ return { valid: true };
1440
+ }
1116
1441
  var DateType = {
1117
1442
  name: "Date",
1118
1443
  category: "temporal",
@@ -1120,7 +1445,8 @@ var DateType = {
1120
1445
  dbCompatibility: fullDbCompatibility(),
1121
1446
  validate() {
1122
1447
  return [];
1123
- }
1448
+ },
1449
+ validateDefaultValue: validateDateDefault
1124
1450
  };
1125
1451
  var TimeType = {
1126
1452
  name: "Time",
@@ -1135,7 +1461,8 @@ var TimeType = {
1135
1461
  },
1136
1462
  validate() {
1137
1463
  return [];
1138
- }
1464
+ },
1465
+ validateDefaultValue: validateTimeDefault
1139
1466
  };
1140
1467
  var DateTimeType = {
1141
1468
  name: "DateTime",
@@ -1144,7 +1471,8 @@ var DateTimeType = {
1144
1471
  dbCompatibility: fullDbCompatibility(),
1145
1472
  validate() {
1146
1473
  return [];
1147
- }
1474
+ },
1475
+ validateDefaultValue: validateTimestampDefault
1148
1476
  };
1149
1477
  var TimestampType = {
1150
1478
  name: "Timestamp",
@@ -1159,7 +1487,8 @@ var TimestampType = {
1159
1487
  },
1160
1488
  validate() {
1161
1489
  return [];
1162
- }
1490
+ },
1491
+ validateDefaultValue: validateTimestampDefault
1163
1492
  };
1164
1493
  var temporalTypes = [
1165
1494
  DateType,
@@ -1169,6 +1498,35 @@ var temporalTypes = [
1169
1498
  ];
1170
1499
 
1171
1500
  // src/validation/types/special.ts
1501
+ var FILE_VALID_FIELDS = ["multiple", "maxFiles", "accept", "maxSize"];
1502
+ function validateJsonDefault(value) {
1503
+ if (value === void 0 || value === null || value === "") {
1504
+ return { valid: true };
1505
+ }
1506
+ const strValue = String(value);
1507
+ try {
1508
+ JSON.parse(strValue);
1509
+ return { valid: true };
1510
+ } catch {
1511
+ return { valid: false, error: "Must be valid JSON" };
1512
+ }
1513
+ }
1514
+ function validateUuidDefault(value) {
1515
+ if (value === void 0 || value === null || value === "") {
1516
+ return { valid: true };
1517
+ }
1518
+ const strValue = String(value);
1519
+ if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(strValue)) {
1520
+ return { valid: false, error: "Must be a valid UUID" };
1521
+ }
1522
+ return { valid: true };
1523
+ }
1524
+ function validateFileDefault() {
1525
+ return { valid: true };
1526
+ }
1527
+ function buildLocation4(file) {
1528
+ return { file };
1529
+ }
1172
1530
  var JsonType = {
1173
1531
  name: "Json",
1174
1532
  category: "special",
@@ -1185,7 +1543,8 @@ var JsonType = {
1185
1543
  },
1186
1544
  validate() {
1187
1545
  return [];
1188
- }
1546
+ },
1547
+ validateDefaultValue: validateJsonDefault
1189
1548
  };
1190
1549
  var UuidType = {
1191
1550
  name: "Uuid",
@@ -1194,35 +1553,110 @@ var UuidType = {
1194
1553
  dbCompatibility: fullDbCompatibility(),
1195
1554
  validate() {
1196
1555
  return [];
1197
- }
1556
+ },
1557
+ validateDefaultValue: validateUuidDefault
1198
1558
  };
1199
1559
  var FileType = {
1200
1560
  name: "File",
1201
1561
  category: "special",
1202
- validFields: createValidFields([]),
1562
+ validFields: createValidFields(FILE_VALID_FIELDS),
1203
1563
  dbCompatibility: fullDbCompatibility(),
1204
- validate() {
1205
- return [];
1206
- }
1207
- };
1208
- var MultiFileType = {
1209
- name: "MultiFile",
1210
- category: "special",
1211
- validFields: createValidFields([]),
1212
- dbCompatibility: fullDbCompatibility(),
1213
- validate() {
1214
- return [];
1215
- }
1564
+ validate(propertyName, property, filePath) {
1565
+ const errors = [];
1566
+ const {
1567
+ multiple,
1568
+ maxFiles,
1569
+ accept,
1570
+ maxSize
1571
+ } = property;
1572
+ if (multiple !== void 0 && typeof multiple !== "boolean") {
1573
+ errors.push(
1574
+ validationError(
1575
+ `Property '${propertyName}' multiple must be a boolean`,
1576
+ buildLocation4(filePath)
1577
+ )
1578
+ );
1579
+ }
1580
+ if (maxFiles !== void 0) {
1581
+ if (!multiple) {
1582
+ errors.push(
1583
+ validationError(
1584
+ `Property '${propertyName}' maxFiles is only valid when multiple=true`,
1585
+ buildLocation4(filePath),
1586
+ "Add multiple: true or remove maxFiles"
1587
+ )
1588
+ );
1589
+ } else if (typeof maxFiles !== "number" || maxFiles < 1 || !Number.isInteger(maxFiles)) {
1590
+ errors.push(
1591
+ validationError(
1592
+ `Property '${propertyName}' maxFiles must be a positive integer`,
1593
+ buildLocation4(filePath)
1594
+ )
1595
+ );
1596
+ }
1597
+ }
1598
+ if (accept !== void 0) {
1599
+ if (!Array.isArray(accept)) {
1600
+ errors.push(
1601
+ validationError(
1602
+ `Property '${propertyName}' accept must be an array of file extensions`,
1603
+ buildLocation4(filePath),
1604
+ "Example: accept: [jpg, png, pdf]"
1605
+ )
1606
+ );
1607
+ } else {
1608
+ for (const ext of accept) {
1609
+ if (typeof ext !== "string") {
1610
+ errors.push(
1611
+ validationError(
1612
+ `Property '${propertyName}' accept array contains non-string value`,
1613
+ buildLocation4(filePath)
1614
+ )
1615
+ );
1616
+ break;
1617
+ }
1618
+ }
1619
+ }
1620
+ }
1621
+ if (maxSize !== void 0) {
1622
+ if (typeof maxSize !== "number" || maxSize < 1 || !Number.isInteger(maxSize)) {
1623
+ errors.push(
1624
+ validationError(
1625
+ `Property '${propertyName}' maxSize must be a positive integer (KB)`,
1626
+ buildLocation4(filePath)
1627
+ )
1628
+ );
1629
+ }
1630
+ }
1631
+ return errors;
1632
+ },
1633
+ validateDefaultValue: validateFileDefault
1216
1634
  };
1217
1635
  var specialTypes = [
1218
1636
  JsonType,
1219
1637
  UuidType,
1220
- FileType,
1221
- MultiFileType
1638
+ FileType
1222
1639
  ];
1223
1640
 
1224
1641
  // src/validation/types/enum.ts
1225
- function buildLocation4(file) {
1642
+ function validateEnumDefault(value, property) {
1643
+ if (value === void 0 || value === null || value === "") {
1644
+ return { valid: true };
1645
+ }
1646
+ const enumValues = property?.enum;
1647
+ if (!enumValues || !Array.isArray(enumValues)) {
1648
+ return { valid: true };
1649
+ }
1650
+ const strValue = String(value);
1651
+ if (!enumValues.includes(strValue)) {
1652
+ return {
1653
+ valid: false,
1654
+ error: `Must be one of: ${enumValues.join(", ")}`
1655
+ };
1656
+ }
1657
+ return { valid: true };
1658
+ }
1659
+ function buildLocation5(file) {
1226
1660
  return { file };
1227
1661
  }
1228
1662
  var EnumType = {
@@ -1238,7 +1672,7 @@ var EnumType = {
1238
1672
  errors.push(
1239
1673
  validationError(
1240
1674
  `Property '${propertyName}' of type 'Enum' requires 'enum' field`,
1241
- buildLocation4(filePath),
1675
+ buildLocation5(filePath),
1242
1676
  "Add enum values: enum: [value1, value2]"
1243
1677
  )
1244
1678
  );
@@ -1246,7 +1680,7 @@ var EnumType = {
1246
1680
  errors.push(
1247
1681
  validationError(
1248
1682
  `Property '${propertyName}' enum field must be an array`,
1249
- buildLocation4(filePath),
1683
+ buildLocation5(filePath),
1250
1684
  "enum: [value1, value2]"
1251
1685
  )
1252
1686
  );
@@ -1254,7 +1688,7 @@ var EnumType = {
1254
1688
  errors.push(
1255
1689
  validationError(
1256
1690
  `Property '${propertyName}' enum field cannot be empty`,
1257
- buildLocation4(filePath),
1691
+ buildLocation5(filePath),
1258
1692
  "Add at least one enum value"
1259
1693
  )
1260
1694
  );
@@ -1265,14 +1699,14 @@ var EnumType = {
1265
1699
  errors.push(
1266
1700
  validationError(
1267
1701
  `Property '${propertyName}' enum values must be strings, got ${typeof value}`,
1268
- buildLocation4(filePath)
1702
+ buildLocation5(filePath)
1269
1703
  )
1270
1704
  );
1271
1705
  } else if (seen.has(value)) {
1272
1706
  errors.push(
1273
1707
  validationError(
1274
1708
  `Property '${propertyName}' has duplicate enum value '${value}'`,
1275
- buildLocation4(filePath)
1709
+ buildLocation5(filePath)
1276
1710
  )
1277
1711
  );
1278
1712
  } else {
@@ -1281,79 +1715,56 @@ var EnumType = {
1281
1715
  }
1282
1716
  }
1283
1717
  return errors;
1284
- }
1718
+ },
1719
+ validateDefaultValue: validateEnumDefault
1285
1720
  };
1286
- var SelectType = {
1287
- name: "Select",
1721
+ var EnumRefType = {
1722
+ name: "EnumRef",
1288
1723
  category: "enum",
1289
- validFields: createValidFields(["options", "source"]),
1724
+ validFields: createValidFields(["enum"]),
1725
+ requiredFields: ["enum"],
1290
1726
  dbCompatibility: fullDbCompatibility(),
1291
1727
  validate(propertyName, property, filePath) {
1292
1728
  const errors = [];
1293
- const { options, source } = property;
1294
- if (options === void 0 && source === void 0) {
1729
+ const { enum: enumRef } = property;
1730
+ if (enumRef === void 0) {
1295
1731
  errors.push(
1296
1732
  validationError(
1297
- `Property '${propertyName}' of type 'Select' requires 'options' or 'source' field`,
1298
- buildLocation4(filePath),
1299
- "Add options: [value1, value2] or source: SchemaName"
1300
- )
1301
- );
1302
- }
1303
- if (options !== void 0 && !Array.isArray(options)) {
1304
- errors.push(
1305
- validationError(
1306
- `Property '${propertyName}' options field must be an array`,
1307
- buildLocation4(filePath)
1733
+ `Property '${propertyName}' of type 'EnumRef' requires 'enum' field`,
1734
+ buildLocation5(filePath),
1735
+ "Add enum schema reference: enum: EnumSchemaName"
1308
1736
  )
1309
1737
  );
1310
- }
1311
- if (source !== void 0 && typeof source !== "string") {
1738
+ } else if (typeof enumRef !== "string") {
1312
1739
  errors.push(
1313
1740
  validationError(
1314
- `Property '${propertyName}' source field must be a string`,
1315
- buildLocation4(filePath)
1316
- )
1317
- );
1318
- }
1319
- return errors;
1320
- }
1321
- };
1322
- var LookupType = {
1323
- name: "Lookup",
1324
- category: "enum",
1325
- validFields: createValidFields(["options", "source"]),
1326
- dbCompatibility: fullDbCompatibility(),
1327
- validate(propertyName, property, filePath) {
1328
- const errors = [];
1329
- const { source } = property;
1330
- if (source === void 0) {
1331
- errors.push(
1332
- validationError(
1333
- `Property '${propertyName}' of type 'Lookup' requires 'source' field`,
1334
- buildLocation4(filePath),
1335
- "Add source: SchemaName"
1741
+ `Property '${propertyName}' enum field must be a string (enum schema name)`,
1742
+ buildLocation5(filePath),
1743
+ "enum: EnumSchemaName"
1336
1744
  )
1337
1745
  );
1338
- } else if (typeof source !== "string") {
1746
+ } else if (enumRef.trim() === "") {
1339
1747
  errors.push(
1340
1748
  validationError(
1341
- `Property '${propertyName}' source field must be a string`,
1342
- buildLocation4(filePath)
1749
+ `Property '${propertyName}' enum field cannot be empty`,
1750
+ buildLocation5(filePath),
1751
+ "Add enum schema name"
1343
1752
  )
1344
1753
  );
1345
1754
  }
1346
1755
  return errors;
1347
- }
1756
+ },
1757
+ // EnumRef default value validation requires schema context
1758
+ // This is handled at a higher level (validator.ts) where schemas are available
1759
+ validateDefaultValue: () => ({ valid: true })
1348
1760
  };
1349
1761
  var enumTypes = [
1350
1762
  EnumType,
1351
- SelectType,
1352
- LookupType
1763
+ EnumRefType
1353
1764
  ];
1354
1765
 
1355
1766
  // src/validation/types/relations.ts
1356
- function buildLocation5(file) {
1767
+ function buildLocation6(file) {
1357
1768
  return { file };
1358
1769
  }
1359
1770
  var VALID_RELATIONS = [
@@ -1380,6 +1791,23 @@ var VALID_REFERENTIAL_ACTIONS = [
1380
1791
  "RESTRICT",
1381
1792
  "NO ACTION"
1382
1793
  ];
1794
+ var PIVOT_TABLE_RELATIONS = [
1795
+ "ManyToMany",
1796
+ "MorphToMany"
1797
+ ];
1798
+ var VALID_PIVOT_FIELD_TYPES = [
1799
+ "String",
1800
+ "Int",
1801
+ "BigInt",
1802
+ "Float",
1803
+ "Decimal",
1804
+ "Boolean",
1805
+ "Text",
1806
+ "Date",
1807
+ "Time",
1808
+ "Timestamp",
1809
+ "Json"
1810
+ ];
1383
1811
  var ASSOCIATION_FIELDS = [
1384
1812
  "relation",
1385
1813
  "target",
@@ -1392,7 +1820,9 @@ var ASSOCIATION_FIELDS = [
1392
1820
  "onDelete",
1393
1821
  "onUpdate",
1394
1822
  "owning",
1395
- "joinTable"
1823
+ "joinTable",
1824
+ "pivotFields"
1825
+ // For ManyToMany/MorphToMany: additional fields on pivot table
1396
1826
  ];
1397
1827
  var AssociationType = {
1398
1828
  name: "Association",
@@ -1414,7 +1844,7 @@ var AssociationType = {
1414
1844
  errors.push(
1415
1845
  validationError(
1416
1846
  `Property '${propertyName}' of type 'Association' requires 'relation' field`,
1417
- buildLocation5(filePath),
1847
+ buildLocation6(filePath),
1418
1848
  "Add relation: OneToOne, OneToMany, ManyToOne, ManyToMany, MorphTo, MorphOne, MorphMany, MorphToMany, or MorphedByMany"
1419
1849
  )
1420
1850
  );
@@ -1424,7 +1854,7 @@ var AssociationType = {
1424
1854
  errors.push(
1425
1855
  validationError(
1426
1856
  `Property '${propertyName}' relation field must be a string`,
1427
- buildLocation5(filePath)
1857
+ buildLocation6(filePath)
1428
1858
  )
1429
1859
  );
1430
1860
  return errors;
@@ -1433,7 +1863,7 @@ var AssociationType = {
1433
1863
  errors.push(
1434
1864
  validationError(
1435
1865
  `Property '${propertyName}' has invalid relation '${relation}'`,
1436
- buildLocation5(filePath),
1866
+ buildLocation6(filePath),
1437
1867
  `Use one of: ${VALID_RELATIONS.join(", ")}`
1438
1868
  )
1439
1869
  );
@@ -1445,7 +1875,7 @@ var AssociationType = {
1445
1875
  errors.push(
1446
1876
  validationError(
1447
1877
  `Property '${propertyName}' with relation 'MorphTo' requires 'targets' field`,
1448
- buildLocation5(filePath),
1878
+ buildLocation6(filePath),
1449
1879
  "Add targets: [Post, Video, Image]"
1450
1880
  )
1451
1881
  );
@@ -1453,7 +1883,7 @@ var AssociationType = {
1453
1883
  errors.push(
1454
1884
  validationError(
1455
1885
  `Property '${propertyName}' targets field must be an array of schema names`,
1456
- buildLocation5(filePath),
1886
+ buildLocation6(filePath),
1457
1887
  "Example: targets: [Post, Video, Image]"
1458
1888
  )
1459
1889
  );
@@ -1461,7 +1891,7 @@ var AssociationType = {
1461
1891
  errors.push(
1462
1892
  validationError(
1463
1893
  `Property '${propertyName}' targets array cannot be empty`,
1464
- buildLocation5(filePath),
1894
+ buildLocation6(filePath),
1465
1895
  "Add at least one target schema"
1466
1896
  )
1467
1897
  );
@@ -1471,7 +1901,7 @@ var AssociationType = {
1471
1901
  errors.push(
1472
1902
  validationError(
1473
1903
  `Property '${propertyName}' targets array contains non-string value`,
1474
- buildLocation5(filePath)
1904
+ buildLocation6(filePath)
1475
1905
  )
1476
1906
  );
1477
1907
  }
@@ -1481,7 +1911,7 @@ var AssociationType = {
1481
1911
  errors.push(
1482
1912
  validationError(
1483
1913
  `Property '${propertyName}' with relation 'MorphTo' should use 'targets' (plural) not 'target'`,
1484
- buildLocation5(filePath),
1914
+ buildLocation6(filePath),
1485
1915
  "Change target to targets array"
1486
1916
  )
1487
1917
  );
@@ -1491,7 +1921,7 @@ var AssociationType = {
1491
1921
  errors.push(
1492
1922
  validationError(
1493
1923
  `Property '${propertyName}' with relation '${relation}' requires 'target' field`,
1494
- buildLocation5(filePath),
1924
+ buildLocation6(filePath),
1495
1925
  "Add target schema name: target: Comment"
1496
1926
  )
1497
1927
  );
@@ -1499,7 +1929,7 @@ var AssociationType = {
1499
1929
  errors.push(
1500
1930
  validationError(
1501
1931
  `Property '${propertyName}' target field must be a string`,
1502
- buildLocation5(filePath)
1932
+ buildLocation6(filePath)
1503
1933
  )
1504
1934
  );
1505
1935
  }
@@ -1507,7 +1937,7 @@ var AssociationType = {
1507
1937
  errors.push(
1508
1938
  validationError(
1509
1939
  `Property '${propertyName}' with relation '${relation}' requires 'morphName' field`,
1510
- buildLocation5(filePath),
1940
+ buildLocation6(filePath),
1511
1941
  "Add morphName to match the MorphTo property name (e.g., morphName: commentable)"
1512
1942
  )
1513
1943
  );
@@ -1515,7 +1945,7 @@ var AssociationType = {
1515
1945
  errors.push(
1516
1946
  validationError(
1517
1947
  `Property '${propertyName}' morphName field must be a string`,
1518
- buildLocation5(filePath)
1948
+ buildLocation6(filePath)
1519
1949
  )
1520
1950
  );
1521
1951
  }
@@ -1524,7 +1954,7 @@ var AssociationType = {
1524
1954
  errors.push(
1525
1955
  validationError(
1526
1956
  `Property '${propertyName}' with relation 'MorphToMany' requires 'target' field`,
1527
- buildLocation5(filePath),
1957
+ buildLocation6(filePath),
1528
1958
  "Add target schema name: target: Tag"
1529
1959
  )
1530
1960
  );
@@ -1532,7 +1962,7 @@ var AssociationType = {
1532
1962
  errors.push(
1533
1963
  validationError(
1534
1964
  `Property '${propertyName}' target field must be a string`,
1535
- buildLocation5(filePath)
1965
+ buildLocation6(filePath)
1536
1966
  )
1537
1967
  );
1538
1968
  }
@@ -1541,7 +1971,7 @@ var AssociationType = {
1541
1971
  errors.push(
1542
1972
  validationError(
1543
1973
  `Property '${propertyName}' with relation 'MorphedByMany' requires 'target' field`,
1544
- buildLocation5(filePath),
1974
+ buildLocation6(filePath),
1545
1975
  "Add target schema name: target: Post"
1546
1976
  )
1547
1977
  );
@@ -1549,7 +1979,7 @@ var AssociationType = {
1549
1979
  errors.push(
1550
1980
  validationError(
1551
1981
  `Property '${propertyName}' target field must be a string`,
1552
- buildLocation5(filePath)
1982
+ buildLocation6(filePath)
1553
1983
  )
1554
1984
  );
1555
1985
  }
@@ -1557,7 +1987,7 @@ var AssociationType = {
1557
1987
  errors.push(
1558
1988
  validationError(
1559
1989
  `Property '${propertyName}' morphName field must be a string`,
1560
- buildLocation5(filePath)
1990
+ buildLocation6(filePath)
1561
1991
  )
1562
1992
  );
1563
1993
  }
@@ -1566,7 +1996,7 @@ var AssociationType = {
1566
1996
  errors.push(
1567
1997
  validationError(
1568
1998
  `Property '${propertyName}' of type 'Association' requires 'target' field`,
1569
- buildLocation5(filePath),
1999
+ buildLocation6(filePath),
1570
2000
  "Add target schema name: target: User"
1571
2001
  )
1572
2002
  );
@@ -1574,7 +2004,7 @@ var AssociationType = {
1574
2004
  errors.push(
1575
2005
  validationError(
1576
2006
  `Property '${propertyName}' target field must be a string`,
1577
- buildLocation5(filePath)
2007
+ buildLocation6(filePath)
1578
2008
  )
1579
2009
  );
1580
2010
  }
@@ -1584,7 +2014,7 @@ var AssociationType = {
1584
2014
  errors.push(
1585
2015
  validationError(
1586
2016
  `Property '${propertyName}' has invalid onDelete action '${onDelete}'`,
1587
- buildLocation5(filePath),
2017
+ buildLocation6(filePath),
1588
2018
  `Use one of: ${VALID_REFERENTIAL_ACTIONS.join(", ")}`
1589
2019
  )
1590
2020
  );
@@ -1595,12 +2025,77 @@ var AssociationType = {
1595
2025
  errors.push(
1596
2026
  validationError(
1597
2027
  `Property '${propertyName}' has invalid onUpdate action '${onUpdate}'`,
1598
- buildLocation5(filePath),
2028
+ buildLocation6(filePath),
1599
2029
  `Use one of: ${VALID_REFERENTIAL_ACTIONS.join(", ")}`
1600
2030
  )
1601
2031
  );
1602
2032
  }
1603
2033
  }
2034
+ const pivotFields = property.pivotFields;
2035
+ if (pivotFields !== void 0) {
2036
+ if (!PIVOT_TABLE_RELATIONS.includes(relationTyped)) {
2037
+ errors.push(
2038
+ validationError(
2039
+ `Property '${propertyName}' with relation '${relation}' cannot have pivotFields`,
2040
+ buildLocation6(filePath),
2041
+ "pivotFields is only allowed for ManyToMany and MorphToMany relations"
2042
+ )
2043
+ );
2044
+ } else if (typeof pivotFields !== "object" || pivotFields === null || Array.isArray(pivotFields)) {
2045
+ errors.push(
2046
+ validationError(
2047
+ `Property '${propertyName}' pivotFields must be an object`,
2048
+ buildLocation6(filePath),
2049
+ 'Example: pivotFields: { assigned_at: { type: "Timestamp" } }'
2050
+ )
2051
+ );
2052
+ } else {
2053
+ for (const [fieldName, fieldDef] of Object.entries(pivotFields)) {
2054
+ if (typeof fieldDef !== "object" || fieldDef === null) {
2055
+ errors.push(
2056
+ validationError(
2057
+ `Pivot field '${fieldName}' in '${propertyName}' must be an object`,
2058
+ buildLocation6(filePath)
2059
+ )
2060
+ );
2061
+ continue;
2062
+ }
2063
+ const { type: fieldType } = fieldDef;
2064
+ if (fieldType === void 0) {
2065
+ errors.push(
2066
+ validationError(
2067
+ `Pivot field '${fieldName}' in '${propertyName}' requires 'type' field`,
2068
+ buildLocation6(filePath),
2069
+ `Example: ${fieldName}: { type: "String" }`
2070
+ )
2071
+ );
2072
+ } else if (typeof fieldType !== "string") {
2073
+ errors.push(
2074
+ validationError(
2075
+ `Pivot field '${fieldName}' in '${propertyName}' type must be a string`,
2076
+ buildLocation6(filePath)
2077
+ )
2078
+ );
2079
+ } else if (fieldType === "Association") {
2080
+ errors.push(
2081
+ validationError(
2082
+ `Pivot field '${fieldName}' in '${propertyName}' cannot be of type 'Association'`,
2083
+ buildLocation6(filePath),
2084
+ "Pivot fields only support basic types like String, Int, Boolean, Timestamp, etc."
2085
+ )
2086
+ );
2087
+ } else if (!VALID_PIVOT_FIELD_TYPES.includes(fieldType)) {
2088
+ errors.push(
2089
+ validationError(
2090
+ `Pivot field '${fieldName}' in '${propertyName}' has unsupported type '${fieldType}'`,
2091
+ buildLocation6(filePath),
2092
+ `Supported types: ${VALID_PIVOT_FIELD_TYPES.join(", ")}`
2093
+ )
2094
+ );
2095
+ }
2096
+ }
2097
+ }
2098
+ }
1604
2099
  return errors;
1605
2100
  }
1606
2101
  };
@@ -1626,10 +2121,20 @@ function registerBuiltInTypes() {
1626
2121
  }
1627
2122
  }
1628
2123
  registerBuiltInTypes();
2124
+ function validateDefaultValue(typeName, value, property) {
2125
+ const typeDef = typeRegistry.get(typeName);
2126
+ if (!typeDef) {
2127
+ return { valid: true };
2128
+ }
2129
+ if (typeDef.validateDefaultValue) {
2130
+ return typeDef.validateDefaultValue(value, property);
2131
+ }
2132
+ return { valid: true };
2133
+ }
1629
2134
 
1630
2135
  // src/validation/validator.ts
1631
2136
  var VALID_ID_TYPES = ["Int", "BigInt", "Uuid", "String"];
1632
- function buildLocation6(file, line, column) {
2137
+ function buildLocation7(file, line, column) {
1633
2138
  const loc = { file };
1634
2139
  if (line !== void 0) {
1635
2140
  loc.line = line;
@@ -1639,6 +2144,68 @@ function buildLocation6(file, line, column) {
1639
2144
  }
1640
2145
  return loc;
1641
2146
  }
2147
+ function validateUnknownSchemaFields(schema, filePath) {
2148
+ const errors = [];
2149
+ const unknownFields = schema._unknownFields;
2150
+ if (!unknownFields || unknownFields.length === 0) {
2151
+ return errors;
2152
+ }
2153
+ for (const field of unknownFields) {
2154
+ let suggestion = `Valid schema fields: kind, displayName, titleIndex, group, options, properties, values`;
2155
+ if (field === "associations") {
2156
+ suggestion = `Associations should be defined inside 'properties' with type: Association.
2157
+ Example:
2158
+ properties:
2159
+ author:
2160
+ type: Association
2161
+ relation: ManyToOne
2162
+ target: User
2163
+ onDelete: CASCADE`;
2164
+ } else if (field === "indexes") {
2165
+ suggestion = `Indexes should be inside 'options.indexes'.
2166
+ Example:
2167
+ options:
2168
+ indexes:
2169
+ - columns: [email]
2170
+ unique: true`;
2171
+ } else if (field === "required") {
2172
+ suggestion = `'required' is not a valid field. Use 'nullable: false' on properties (default is not nullable).
2173
+ Example:
2174
+ properties:
2175
+ email:
2176
+ type: String
2177
+ # not nullable by default, explicitly use nullable: true to make optional`;
2178
+ } else if (field === "hidden") {
2179
+ suggestion = `'hidden' is not a schema-level field. Hidden fields should be handled by your framework.
2180
+ For Laravel, the model generator can handle this, but you need to use a custom property.`;
2181
+ }
2182
+ errors.push(
2183
+ validationError(
2184
+ `Unknown schema field '${field}'`,
2185
+ buildLocation7(filePath),
2186
+ suggestion
2187
+ )
2188
+ );
2189
+ }
2190
+ return errors;
2191
+ }
2192
+ function validateUnknownOptionsFields(schema, filePath) {
2193
+ const errors = [];
2194
+ const unknownOptionsFields = schema._unknownOptionsFields;
2195
+ if (!unknownOptionsFields || unknownOptionsFields.length === 0) {
2196
+ return errors;
2197
+ }
2198
+ for (const field of unknownOptionsFields) {
2199
+ errors.push(
2200
+ validationError(
2201
+ `Unknown option field '${field}'`,
2202
+ buildLocation7(filePath),
2203
+ `Valid option fields: id, idType, timestamps, softDelete, unique, indexes, translations, tableName, authenticatable, authenticatableLoginIdField, authenticatablePasswordField, authenticatableGuardName`
2204
+ )
2205
+ );
2206
+ }
2207
+ return errors;
2208
+ }
1642
2209
  function validateDbCompatibility(propertyName, property, filePath, databaseDriver) {
1643
2210
  const errors = [];
1644
2211
  const warnings = [];
@@ -1650,7 +2217,7 @@ function validateDbCompatibility(propertyName, property, filePath, databaseDrive
1650
2217
  errors.push(
1651
2218
  validationError(
1652
2219
  `Property '${propertyName}' uses type '${property.type}' which is not supported in ${databaseDriver}`,
1653
- buildLocation6(filePath),
2220
+ buildLocation7(filePath),
1654
2221
  `Consider using a different type or database driver`
1655
2222
  )
1656
2223
  );
@@ -1658,7 +2225,7 @@ function validateDbCompatibility(propertyName, property, filePath, databaseDrive
1658
2225
  warnings.push(
1659
2226
  validationError(
1660
2227
  `Property '${propertyName}' uses type '${property.type}' which has limited support in ${databaseDriver}`,
1661
- buildLocation6(filePath),
2228
+ buildLocation7(filePath),
1662
2229
  `The type will work but may have precision or feature limitations`
1663
2230
  )
1664
2231
  );
@@ -1666,30 +2233,51 @@ function validateDbCompatibility(propertyName, property, filePath, databaseDrive
1666
2233
  return { errors, warnings };
1667
2234
  }
1668
2235
  function validateUnknownFields(propertyName, property, filePath) {
1669
- const warnings = [];
1670
- if (!property.type) {
1671
- return warnings;
2236
+ const errors = [];
2237
+ const unknownFields = property._unknownFields;
2238
+ if (!unknownFields || unknownFields.length === 0) {
2239
+ return errors;
1672
2240
  }
1673
- const validFields = typeRegistry.getValidFields(property.type);
1674
- const propertyFields = Object.keys(property);
1675
- for (const field of propertyFields) {
1676
- if (!validFields.includes(field)) {
1677
- warnings.push(
1678
- validationError(
1679
- `Property '${propertyName}' has unknown field '${field}'`,
1680
- buildLocation6(filePath),
1681
- `Valid fields for type '${property.type}': ${validFields.join(", ")}`
1682
- )
1683
- );
2241
+ for (const field of unknownFields) {
2242
+ let suggestion = `Valid property fields: type, displayName, nullable, default, unique, description, length, unsigned, precision, scale, enum, relation, target, onDelete, onUpdate, joinTable`;
2243
+ if (field === "required") {
2244
+ suggestion = `'required' is not a valid field. Properties are required by default.
2245
+ Use 'nullable: true' to make a field optional.
2246
+ Example:
2247
+ email:
2248
+ type: String
2249
+ # required by default
2250
+
2251
+ phone:
2252
+ type: String
2253
+ nullable: true # optional field`;
2254
+ } else if (field === "hidden") {
2255
+ suggestion = `'hidden' is not a standard field. For Laravel, hidden fields are configured in the Model class.`;
2256
+ } else if (field === "foreignKey") {
2257
+ suggestion = `'foreignKey' is not needed. Omnify auto-generates foreign keys from Association properties.
2258
+ The foreign key name is derived from the property name + '_id'.
2259
+ Example:
2260
+ author:
2261
+ type: Association
2262
+ relation: ManyToOne
2263
+ target: User
2264
+ # Creates author_id column automatically`;
1684
2265
  }
2266
+ errors.push(
2267
+ validationError(
2268
+ `Property '${propertyName}' has unknown field '${field}'`,
2269
+ buildLocation7(filePath),
2270
+ suggestion
2271
+ )
2272
+ );
1685
2273
  }
1686
- return warnings;
2274
+ return errors;
1687
2275
  }
1688
2276
  function validatePropertyType(propertyName, property, filePath, customTypes = []) {
1689
2277
  const errors = [];
1690
2278
  if (!property.type) {
1691
2279
  errors.push(
1692
- missingFieldError("type", buildLocation6(filePath))
2280
+ missingFieldError("type", buildLocation7(filePath))
1693
2281
  );
1694
2282
  return errors;
1695
2283
  }
@@ -1699,7 +2287,7 @@ function validatePropertyType(propertyName, property, filePath, customTypes = []
1699
2287
  errors.push(
1700
2288
  invalidPropertyTypeError(
1701
2289
  property.type,
1702
- buildLocation6(filePath),
2290
+ buildLocation7(filePath),
1703
2291
  typeRegistry.getAllTypeNames()
1704
2292
  )
1705
2293
  );
@@ -1741,7 +2329,7 @@ function validateAssociations(schema, allSchemas) {
1741
2329
  errors.push(
1742
2330
  validationError(
1743
2331
  `Property '${name}' has invalid relation '${assocProp.relation}'`,
1744
- buildLocation6(schema.filePath),
2332
+ buildLocation7(schema.filePath),
1745
2333
  `Use one of: ${VALID_RELATIONS.join(", ")}`
1746
2334
  )
1747
2335
  );
@@ -1754,7 +2342,7 @@ function validateAssociations(schema, allSchemas) {
1754
2342
  errors.push(
1755
2343
  invalidAssociationTargetError(
1756
2344
  target,
1757
- buildLocation6(schema.filePath),
2345
+ buildLocation7(schema.filePath),
1758
2346
  availableSchemas
1759
2347
  )
1760
2348
  );
@@ -1765,7 +2353,7 @@ function validateAssociations(schema, allSchemas) {
1765
2353
  errors.push(
1766
2354
  invalidAssociationTargetError(
1767
2355
  assocProp.target,
1768
- buildLocation6(schema.filePath),
2356
+ buildLocation7(schema.filePath),
1769
2357
  availableSchemas
1770
2358
  )
1771
2359
  );
@@ -1779,7 +2367,7 @@ function validateAssociations(schema, allSchemas) {
1779
2367
  errors.push(
1780
2368
  validationError(
1781
2369
  `Property '${name}' references non-existent morphName '${assocProp.morphName}' on '${assocProp.target}'`,
1782
- buildLocation6(schema.filePath),
2370
+ buildLocation7(schema.filePath),
1783
2371
  `Add a MorphTo property '${assocProp.morphName}' to ${assocProp.target} schema`
1784
2372
  )
1785
2373
  );
@@ -1789,7 +2377,7 @@ function validateAssociations(schema, allSchemas) {
1789
2377
  errors.push(
1790
2378
  validationError(
1791
2379
  `Property '${name}' morphName '${assocProp.morphName}' on '${assocProp.target}' must be a MorphTo relation`,
1792
- buildLocation6(schema.filePath),
2380
+ buildLocation7(schema.filePath),
1793
2381
  `Change ${assocProp.target}.${assocProp.morphName} relation to MorphTo`
1794
2382
  )
1795
2383
  );
@@ -1806,7 +2394,7 @@ function validateAssociations(schema, allSchemas) {
1806
2394
  errors.push(
1807
2395
  validationError(
1808
2396
  `Property '${name}' references non-existent inverse property '${assocProp.inversedBy}' on '${assocProp.target}'`,
1809
- buildLocation6(schema.filePath),
2397
+ buildLocation7(schema.filePath),
1810
2398
  `Add property '${assocProp.inversedBy}' to ${assocProp.target} schema`
1811
2399
  )
1812
2400
  );
@@ -1817,7 +2405,7 @@ function validateAssociations(schema, allSchemas) {
1817
2405
  errors.push(
1818
2406
  validationError(
1819
2407
  `Property '${name}' has invalid onDelete action '${assocProp.onDelete}'`,
1820
- buildLocation6(schema.filePath),
2408
+ buildLocation7(schema.filePath),
1821
2409
  `Use one of: ${VALID_REFERENTIAL_ACTIONS.join(", ")}`
1822
2410
  )
1823
2411
  );
@@ -1826,7 +2414,7 @@ function validateAssociations(schema, allSchemas) {
1826
2414
  errors.push(
1827
2415
  validationError(
1828
2416
  `Property '${name}' has invalid onUpdate action '${assocProp.onUpdate}'`,
1829
- buildLocation6(schema.filePath),
2417
+ buildLocation7(schema.filePath),
1830
2418
  `Use one of: ${VALID_REFERENTIAL_ACTIONS.join(", ")}`
1831
2419
  )
1832
2420
  );
@@ -1845,7 +2433,7 @@ function validateOptions(schema, filePath) {
1845
2433
  errors.push(
1846
2434
  validationError(
1847
2435
  `Invalid idType '${options.idType}'`,
1848
- buildLocation6(filePath),
2436
+ buildLocation7(filePath),
1849
2437
  `Use one of: ${VALID_ID_TYPES.join(", ")}`
1850
2438
  )
1851
2439
  );
@@ -1860,7 +2448,7 @@ function validateOptions(schema, filePath) {
1860
2448
  errors.push(
1861
2449
  validationError(
1862
2450
  `Unique constraint references non-existent property '${column}'`,
1863
- buildLocation6(filePath),
2451
+ buildLocation7(filePath),
1864
2452
  `Available properties: ${propertyNames.join(", ")}`
1865
2453
  )
1866
2454
  );
@@ -1871,12 +2459,13 @@ function validateOptions(schema, filePath) {
1871
2459
  if (options.indexes && schema.properties) {
1872
2460
  const propertyNames = Object.keys(schema.properties);
1873
2461
  for (const index of options.indexes) {
1874
- for (const column of index.columns) {
2462
+ const columns = typeof index === "string" ? [index] : index.columns;
2463
+ for (const column of columns) {
1875
2464
  if (!propertyNames.includes(column)) {
1876
2465
  errors.push(
1877
2466
  validationError(
1878
2467
  `Index references non-existent property '${column}'`,
1879
- buildLocation6(filePath),
2468
+ buildLocation7(filePath),
1880
2469
  `Available properties: ${propertyNames.join(", ")}`
1881
2470
  )
1882
2471
  );
@@ -1890,7 +2479,7 @@ function validateOptions(schema, filePath) {
1890
2479
  errors.push(
1891
2480
  validationError(
1892
2481
  `authenticatableLoginIdField references non-existent property '${options.authenticatableLoginIdField}'`,
1893
- buildLocation6(filePath)
2482
+ buildLocation7(filePath)
1894
2483
  )
1895
2484
  );
1896
2485
  }
@@ -1900,7 +2489,7 @@ function validateOptions(schema, filePath) {
1900
2489
  errors.push(
1901
2490
  validationError(
1902
2491
  `authenticatablePasswordField references non-existent property '${options.authenticatablePasswordField}'`,
1903
- buildLocation6(filePath)
2492
+ buildLocation7(filePath)
1904
2493
  )
1905
2494
  );
1906
2495
  }
@@ -1917,7 +2506,7 @@ function validateEnumSchema(schema, filePath) {
1917
2506
  errors.push(
1918
2507
  validationError(
1919
2508
  "Enum schema requires at least one value",
1920
- buildLocation6(filePath),
2509
+ buildLocation7(filePath),
1921
2510
  "Add values: [value1, value2, ...]"
1922
2511
  )
1923
2512
  );
@@ -1929,7 +2518,7 @@ function validateEnumSchema(schema, filePath) {
1929
2518
  errors.push(
1930
2519
  validationError(
1931
2520
  `Duplicate enum value '${value}'`,
1932
- buildLocation6(filePath)
2521
+ buildLocation7(filePath)
1933
2522
  )
1934
2523
  );
1935
2524
  }
@@ -1942,6 +2531,10 @@ function validateSchema(schema, options = {}) {
1942
2531
  const errors = [];
1943
2532
  const warnings = [];
1944
2533
  const customTypes = options.customTypes ?? [];
2534
+ const unknownSchemaFieldErrors = validateUnknownSchemaFields(schema, schema.filePath);
2535
+ errors.push(...unknownSchemaFieldErrors);
2536
+ const unknownOptionsErrors = validateUnknownOptionsFields(schema, schema.filePath);
2537
+ errors.push(...unknownOptionsErrors);
1945
2538
  const propErrors = validateProperties(schema, schema.filePath, customTypes);
1946
2539
  errors.push(...propErrors);
1947
2540
  const optErrors = validateOptions(schema, schema.filePath);
@@ -1955,7 +2548,7 @@ function validateSchema(schema, options = {}) {
1955
2548
  errors.push(
1956
2549
  validationError(
1957
2550
  `titleIndex references non-existent property '${schema.titleIndex}'`,
1958
- buildLocation6(schema.filePath),
2551
+ buildLocation7(schema.filePath),
1959
2552
  `Available properties: ${Object.keys(schema.properties).join(", ")}`
1960
2553
  )
1961
2554
  );
@@ -1963,8 +2556,8 @@ function validateSchema(schema, options = {}) {
1963
2556
  }
1964
2557
  if (schema.properties) {
1965
2558
  for (const [name, property] of Object.entries(schema.properties)) {
1966
- const unknownFieldWarnings = validateUnknownFields(name, property, schema.filePath);
1967
- warnings.push(...unknownFieldWarnings);
2559
+ const unknownFieldErrors = validateUnknownFields(name, property, schema.filePath);
2560
+ errors.push(...unknownFieldErrors);
1968
2561
  const dbCompat = validateDbCompatibility(name, property, schema.filePath, options.databaseDriver);
1969
2562
  errors.push(...dbCompat.errors);
1970
2563
  warnings.push(...dbCompat.warnings);
@@ -3054,6 +3647,31 @@ var import_path = require("path");
3054
3647
  var import_js_yaml2 = __toESM(require("js-yaml"), 1);
3055
3648
  var OMNIFY_DIR = ".omnify";
3056
3649
  var VERSIONS_DIR = "versions";
3650
+ function deepEqual(a, b) {
3651
+ if (a === b) return true;
3652
+ if (a === null || b === null) return a === b;
3653
+ if (typeof a !== typeof b) return false;
3654
+ if (Array.isArray(a) && Array.isArray(b)) {
3655
+ if (a.length !== b.length) return false;
3656
+ for (let i = 0; i < a.length; i++) {
3657
+ if (!deepEqual(a[i], b[i])) return false;
3658
+ }
3659
+ return true;
3660
+ }
3661
+ if (typeof a === "object" && typeof b === "object") {
3662
+ const aObj = a;
3663
+ const bObj = b;
3664
+ const aKeys = Object.keys(aObj);
3665
+ const bKeys = Object.keys(bObj);
3666
+ if (aKeys.length !== bKeys.length) return false;
3667
+ for (const key of aKeys) {
3668
+ if (!Object.prototype.hasOwnProperty.call(bObj, key)) return false;
3669
+ if (!deepEqual(aObj[key], bObj[key])) return false;
3670
+ }
3671
+ return true;
3672
+ }
3673
+ return false;
3674
+ }
3057
3675
  var CURRENT_FILE = "current.lock";
3058
3676
  var VERSION_FILE_PATTERN = /^(\d{4})(?:_(.+))?\.lock$/;
3059
3677
  function formatVersionNumber(version) {
@@ -3312,7 +3930,7 @@ var VersionStore = class {
3312
3930
  }
3313
3931
  for (const prop of fromPropNames) {
3314
3932
  if (!toPropNames.has(prop)) continue;
3315
- if (JSON.stringify(fromProps[prop]) !== JSON.stringify(toProps[prop])) {
3933
+ if (!deepEqual(fromProps[prop], toProps[prop])) {
3316
3934
  changes.push({
3317
3935
  action: "property_modified",
3318
3936
  schema: name,
@@ -3322,13 +3940,78 @@ var VersionStore = class {
3322
3940
  });
3323
3941
  }
3324
3942
  }
3325
- if (JSON.stringify(fromSchema.options) !== JSON.stringify(toSchema.options)) {
3326
- changes.push({
3327
- action: "option_changed",
3328
- schema: name,
3329
- from: fromSchema.options,
3330
- to: toSchema.options
3331
- });
3943
+ const fromOpts = fromSchema.options ?? {};
3944
+ const toOpts = toSchema.options ?? {};
3945
+ const optionDefaults = {
3946
+ id: true,
3947
+ idType: "BigInt",
3948
+ timestamps: true,
3949
+ softDelete: false,
3950
+ tableName: void 0,
3951
+ // no default
3952
+ translations: false,
3953
+ authenticatable: false
3954
+ };
3955
+ const getNormalizedOption = (opts, key) => {
3956
+ const value = opts[key];
3957
+ if (value === void 0) {
3958
+ return optionDefaults[key];
3959
+ }
3960
+ return value;
3961
+ };
3962
+ const simpleOptions = ["timestamps", "softDelete", "id", "idType", "tableName", "translations", "authenticatable"];
3963
+ for (const opt of simpleOptions) {
3964
+ const fromVal = getNormalizedOption(fromOpts, opt);
3965
+ const toVal = getNormalizedOption(toOpts, opt);
3966
+ if (!deepEqual(fromVal, toVal)) {
3967
+ changes.push({
3968
+ action: "option_changed",
3969
+ schema: name,
3970
+ property: opt,
3971
+ from: fromOpts[opt],
3972
+ // Keep original value for display
3973
+ to: toOpts[opt]
3974
+ });
3975
+ }
3976
+ }
3977
+ const fromIndexes = fromOpts.indexes ?? [];
3978
+ const toIndexes = toOpts.indexes ?? [];
3979
+ const columnsMatch = (a, b) => {
3980
+ if (a.length !== b.length) return false;
3981
+ return a.every((col, i) => col === b[i]);
3982
+ };
3983
+ const matchedFromIndexes = /* @__PURE__ */ new Set();
3984
+ for (const toIdx of toIndexes) {
3985
+ const fromIndex = fromIndexes.findIndex(
3986
+ (fromIdx, i) => !matchedFromIndexes.has(i) && columnsMatch(fromIdx.columns, toIdx.columns)
3987
+ );
3988
+ if (fromIndex === -1) {
3989
+ changes.push({
3990
+ action: "index_added",
3991
+ schema: name,
3992
+ to: toIdx
3993
+ });
3994
+ } else {
3995
+ matchedFromIndexes.add(fromIndex);
3996
+ const fromIdx = fromIndexes[fromIndex];
3997
+ if (!deepEqual(fromIdx, toIdx)) {
3998
+ changes.push({
3999
+ action: "index_modified",
4000
+ schema: name,
4001
+ from: fromIdx,
4002
+ to: toIdx
4003
+ });
4004
+ }
4005
+ }
4006
+ }
4007
+ for (let i = 0; i < fromIndexes.length; i++) {
4008
+ if (!matchedFromIndexes.has(i)) {
4009
+ changes.push({
4010
+ action: "index_removed",
4011
+ schema: name,
4012
+ from: fromIndexes[i]
4013
+ });
4014
+ }
3332
4015
  }
3333
4016
  }
3334
4017
  return changes;
@@ -3346,6 +4029,7 @@ function createVersionStore(config) {
3346
4029
  }
3347
4030
  // Annotate the CommonJS export names for ESM import in node:
3348
4031
  0 && (module.exports = {
4032
+ FILE_SCHEMA_NAME,
3349
4033
  Omnify,
3350
4034
  OmnifyError,
3351
4035
  PluginManager,
@@ -3355,10 +4039,13 @@ function createVersionStore(config) {
3355
4039
  circularReferenceError,
3356
4040
  configError,
3357
4041
  configNotFoundError,
4042
+ createFileLoadedSchema,
4043
+ createFileSchemaDefinition,
3358
4044
  createOmnify,
3359
4045
  createPluginManager,
3360
4046
  createVersionStore,
3361
4047
  duplicateSchemaError,
4048
+ ensureFileSchema,
3362
4049
  err,
3363
4050
  expandProperty,
3364
4051
  expandSchema,
@@ -3370,6 +4057,7 @@ function createVersionStore(config) {
3370
4057
  formatError,
3371
4058
  formatErrorPlain,
3372
4059
  formatErrorSummary,
4060
+ generateFileSchemaYaml,
3373
4061
  generationError,
3374
4062
  getAssociationMetadata,
3375
4063
  getCustomTypeNames,
@@ -3408,7 +4096,9 @@ function createVersionStore(config) {
3408
4096
  pluginTypeConflictError,
3409
4097
  schemaNotFoundError,
3410
4098
  schemaParseError,
4099
+ schemasHaveFileProperties,
3411
4100
  validateAssociations,
4101
+ validateDefaultValue,
3412
4102
  validateEnumSchema,
3413
4103
  validateOptions,
3414
4104
  validateProperties,