@famgia/omnify-laravel 0.0.12 → 0.0.14

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/plugin.cjs CHANGED
@@ -293,11 +293,18 @@ function schemaToBlueprint(schema, allSchemas) {
293
293
  }
294
294
  if (schema.options?.indexes) {
295
295
  for (const index of schema.options.indexes) {
296
- indexes.push({
297
- name: index.name,
298
- columns: index.columns.map(toColumnName),
299
- unique: index.unique ?? false
300
- });
296
+ if (typeof index === "string") {
297
+ indexes.push({
298
+ columns: [toColumnName(index)],
299
+ unique: false
300
+ });
301
+ } else {
302
+ indexes.push({
303
+ name: index.name,
304
+ columns: index.columns.map(toColumnName),
305
+ unique: index.unique ?? false
306
+ });
307
+ }
301
308
  }
302
309
  }
303
310
  if (schema.options?.unique) {
@@ -684,489 +691,611 @@ function getMigrationPath(migration, outputDir = "database/migrations") {
684
691
  return `${outputDir}/${migration.fileName}`;
685
692
  }
686
693
 
687
- // src/typescript/interface-generator.ts
688
- var TYPE_MAP = {
689
- String: "string",
690
- Int: "number",
691
- BigInt: "number",
692
- Float: "number",
693
- Boolean: "boolean",
694
- Text: "string",
695
- LongText: "string",
696
- Date: "string",
697
- Time: "string",
698
- Timestamp: "string",
699
- Json: "unknown",
700
- Email: "string",
701
- Password: "string",
702
- Enum: "string",
703
- Select: "string",
704
- Lookup: "number"
705
- };
706
- var FILE_INTERFACE_NAME = "File";
707
- var PK_TYPE_MAP = {
708
- Int: "number",
709
- BigInt: "number",
710
- Uuid: "string",
711
- String: "string"
712
- };
713
- function toPropertyName(name) {
714
- return name;
694
+ // src/utils.ts
695
+ function toSnakeCase(str) {
696
+ return str.replace(/([A-Z])/g, "_$1").replace(/^_/, "").toLowerCase();
715
697
  }
716
- function toInterfaceName(schemaName) {
717
- return schemaName;
698
+ function toPascalCase(str) {
699
+ return str.replace(/[-_](.)/g, (_, c) => c.toUpperCase()).replace(/^(.)/, (_, c) => c.toUpperCase());
718
700
  }
719
- function getPropertyType(property, _allSchemas) {
720
- if (property.type === "File") {
721
- const fileProp = property;
722
- if (fileProp.multiple) {
723
- return `${FILE_INTERFACE_NAME}[]`;
724
- }
725
- return `${FILE_INTERFACE_NAME} | null`;
726
- }
727
- if (property.type === "Association") {
728
- const assocProp = property;
729
- const targetName = assocProp.target ?? "unknown";
730
- switch (assocProp.relation) {
731
- // Standard relations
732
- case "OneToOne":
733
- case "ManyToOne":
734
- return targetName;
735
- case "OneToMany":
736
- case "ManyToMany":
737
- return `${targetName}[]`;
738
- // Polymorphic relations
739
- case "MorphTo":
740
- if (assocProp.targets && assocProp.targets.length > 0) {
741
- return assocProp.targets.join(" | ");
742
- }
743
- return "unknown";
744
- case "MorphOne":
745
- return targetName;
746
- case "MorphMany":
747
- case "MorphToMany":
748
- case "MorphedByMany":
749
- return `${targetName}[]`;
750
- default:
751
- return "unknown";
752
- }
701
+ function toCamelCase(str) {
702
+ const pascal = toPascalCase(str);
703
+ return pascal.charAt(0).toLowerCase() + pascal.slice(1);
704
+ }
705
+ function pluralize(word) {
706
+ if (word.endsWith("y") && !["ay", "ey", "iy", "oy", "uy"].some((v) => word.endsWith(v))) {
707
+ return word.slice(0, -1) + "ies";
753
708
  }
754
- if (property.type === "Enum") {
755
- const enumProp = property;
756
- if (typeof enumProp.enum === "string") {
757
- return enumProp.enum;
758
- }
759
- if (Array.isArray(enumProp.enum)) {
760
- return enumProp.enum.map((v) => `'${v}'`).join(" | ");
761
- }
709
+ if (word.endsWith("s") || word.endsWith("x") || word.endsWith("z") || word.endsWith("ch") || word.endsWith("sh")) {
710
+ return word + "es";
762
711
  }
763
- if (property.type === "Select") {
764
- const selectProp = property;
765
- if (selectProp.options && selectProp.options.length > 0) {
766
- return selectProp.options.map((v) => `'${v}'`).join(" | ");
767
- }
712
+ return word + "s";
713
+ }
714
+
715
+ // src/model/generator.ts
716
+ var DEFAULT_OPTIONS = {
717
+ baseModelNamespace: "App\\Models\\OmnifyBase",
718
+ modelNamespace: "App\\Models",
719
+ baseModelClassName: "BaseModel",
720
+ baseModelPath: "app/Models/OmnifyBase",
721
+ modelPath: "app/Models"
722
+ };
723
+ function resolveOptions(options) {
724
+ return {
725
+ baseModelNamespace: options?.baseModelNamespace ?? DEFAULT_OPTIONS.baseModelNamespace,
726
+ modelNamespace: options?.modelNamespace ?? DEFAULT_OPTIONS.modelNamespace,
727
+ baseModelClassName: options?.baseModelClassName ?? DEFAULT_OPTIONS.baseModelClassName,
728
+ baseModelPath: options?.baseModelPath ?? DEFAULT_OPTIONS.baseModelPath,
729
+ modelPath: options?.modelPath ?? DEFAULT_OPTIONS.modelPath
730
+ };
731
+ }
732
+ function getCastType(propDef) {
733
+ switch (propDef.type) {
734
+ case "Boolean":
735
+ return "boolean";
736
+ case "Int":
737
+ case "BigInt":
738
+ return "integer";
739
+ case "Float":
740
+ return "float";
741
+ case "Decimal":
742
+ return "decimal:" + (propDef.scale ?? 2);
743
+ case "Json":
744
+ return "array";
745
+ case "Date":
746
+ return "date";
747
+ case "Timestamp":
748
+ return "datetime";
749
+ case "Password":
750
+ return "hashed";
751
+ default:
752
+ return null;
768
753
  }
769
- return TYPE_MAP[property.type] ?? "unknown";
770
754
  }
771
- function propertyToTSProperties(propertyName, property, allSchemas, options = {}) {
772
- const baseProp = property;
773
- const isReadonly = options.readonly ?? true;
774
- if (property.type === "Association") {
775
- const assocProp = property;
776
- if (assocProp.relation === "MorphTo" && assocProp.targets && assocProp.targets.length > 0) {
777
- const propBaseName = toPropertyName(propertyName);
778
- const targetUnion = assocProp.targets.map((t) => `'${t}'`).join(" | ");
779
- const relationUnion = assocProp.targets.join(" | ");
780
- return [
781
- {
782
- name: `${propBaseName}Type`,
783
- type: targetUnion,
784
- optional: true,
785
- // Polymorphic columns are nullable
786
- readonly: isReadonly,
787
- comment: `Polymorphic type for ${propertyName}`
788
- },
789
- {
790
- name: `${propBaseName}Id`,
791
- type: "number",
792
- optional: true,
793
- readonly: isReadonly,
794
- comment: `Polymorphic ID for ${propertyName}`
795
- },
796
- {
797
- name: propBaseName,
798
- type: `${relationUnion} | null`,
799
- optional: true,
800
- readonly: isReadonly,
801
- comment: baseProp.displayName ?? `Polymorphic relation to ${assocProp.targets.join(", ")}`
755
+ function isNullable(propDef) {
756
+ return "nullable" in propDef && propDef.nullable === true;
757
+ }
758
+ function getPhpDocType(propDef, schemas) {
759
+ const nullable = isNullable(propDef);
760
+ switch (propDef.type) {
761
+ case "String":
762
+ case "Text":
763
+ case "LongText":
764
+ case "Email":
765
+ case "Password":
766
+ return "string" + (nullable ? "|null" : "");
767
+ case "Int":
768
+ case "BigInt":
769
+ return "int" + (nullable ? "|null" : "");
770
+ case "Float":
771
+ case "Decimal":
772
+ return "float" + (nullable ? "|null" : "");
773
+ case "Boolean":
774
+ return "bool" + (nullable ? "|null" : "");
775
+ case "Date":
776
+ case "Time":
777
+ case "Timestamp":
778
+ return "\\Carbon\\Carbon" + (nullable ? "|null" : "");
779
+ case "Json":
780
+ return "array" + (nullable ? "|null" : "");
781
+ case "Enum":
782
+ case "EnumRef":
783
+ return "string" + (nullable ? "|null" : "");
784
+ case "Association": {
785
+ const assoc = propDef;
786
+ if (assoc.target) {
787
+ const className = toPascalCase(assoc.target);
788
+ switch (assoc.relation) {
789
+ case "OneToMany":
790
+ case "ManyToMany":
791
+ case "MorphMany":
792
+ case "MorphToMany":
793
+ case "MorphedByMany":
794
+ return `\\Illuminate\\Database\\Eloquent\\Collection<${className}>`;
795
+ default:
796
+ return className + "|null";
802
797
  }
803
- ];
798
+ }
799
+ return "mixed";
804
800
  }
801
+ default:
802
+ return "mixed";
805
803
  }
806
- const type = getPropertyType(property, allSchemas);
807
- return [{
808
- name: toPropertyName(propertyName),
809
- type,
810
- optional: baseProp.nullable ?? false,
811
- readonly: isReadonly,
812
- comment: baseProp.displayName
813
- }];
814
804
  }
815
- function schemaToInterface(schema, allSchemas, options = {}) {
816
- const properties = [];
817
- if (schema.options?.id !== false) {
818
- const pkType = schema.options?.idType ?? "BigInt";
819
- properties.push({
820
- name: "id",
821
- type: PK_TYPE_MAP[pkType] ?? "number",
822
- optional: false,
823
- readonly: options.readonly ?? true,
824
- comment: "Primary key"
825
- });
826
- }
827
- if (schema.properties) {
828
- for (const [propName, property] of Object.entries(schema.properties)) {
829
- properties.push(...propertyToTSProperties(propName, property, allSchemas, options));
830
- }
831
- }
832
- if (schema.options?.timestamps !== false) {
833
- properties.push(
834
- {
835
- name: "createdAt",
836
- type: "string",
837
- optional: true,
838
- readonly: options.readonly ?? true,
839
- comment: "Creation timestamp"
840
- },
841
- {
842
- name: "updatedAt",
843
- type: "string",
844
- optional: true,
845
- readonly: options.readonly ?? true,
846
- comment: "Last update timestamp"
847
- }
848
- );
849
- }
850
- if (schema.options?.softDelete) {
851
- properties.push({
852
- name: "deletedAt",
853
- type: "string",
854
- optional: true,
855
- readonly: options.readonly ?? true,
856
- comment: "Soft delete timestamp"
857
- });
858
- }
805
+ function generateBaseModel(schemas, options, stubContent) {
806
+ const modelMap = Object.values(schemas).filter((s) => s.kind !== "enum").map((s) => {
807
+ const className = toPascalCase(s.name);
808
+ return ` '${s.name}' => \\${options.modelNamespace}\\${className}::class,`;
809
+ }).join("\n");
810
+ const content = stubContent.replace(/\{\{BASE_MODEL_NAMESPACE\}\}/g, options.baseModelNamespace).replace(/\{\{BASE_MODEL_CLASS\}\}/g, options.baseModelClassName).replace(/\{\{MODEL_MAP\}\}/g, modelMap);
859
811
  return {
860
- name: toInterfaceName(schema.name),
861
- properties,
862
- comment: schema.displayName ?? schema.name
812
+ path: `${options.baseModelPath}/${options.baseModelClassName}.php`,
813
+ content,
814
+ type: "base-model",
815
+ overwrite: true,
816
+ schemaName: "__base__"
863
817
  };
864
818
  }
865
- function formatProperty(property) {
866
- const readonly = property.readonly ? "readonly " : "";
867
- const optional = property.optional ? "?" : "";
868
- const comment = property.comment ? ` /** ${property.comment} */
869
- ` : "";
870
- return `${comment} ${readonly}${property.name}${optional}: ${property.type};`;
871
- }
872
- function formatInterface(iface) {
873
- const comment = iface.comment ? `/**
874
- * ${iface.comment}
875
- */
876
- ` : "";
877
- const extendsClause = iface.extends && iface.extends.length > 0 ? ` extends ${iface.extends.join(", ")}` : "";
878
- const properties = iface.properties.map(formatProperty).join("\n");
879
- return `${comment}export interface ${iface.name}${extendsClause} {
880
- ${properties}
881
- }`;
882
- }
883
- function generateInterfaces(schemas, options = {}) {
884
- const interfaces = [];
885
- for (const schema of Object.values(schemas)) {
886
- if (schema.kind === "enum") {
887
- continue;
819
+ function generateEntityBaseModel(schema, schemas, options, stubContent, authStubContent) {
820
+ const className = toPascalCase(schema.name);
821
+ const tableName = schema.options?.tableName ?? pluralize(toSnakeCase(schema.name));
822
+ const isAuth = schema.options?.authenticatable ?? false;
823
+ const primaryKey = "id";
824
+ const idType = schema.options?.idType ?? "BigInt";
825
+ const isUuid = idType === "Uuid";
826
+ const isStringKey = idType === "Uuid" || idType === "String";
827
+ const imports = [];
828
+ const traits = [];
829
+ const fillable = [];
830
+ const hidden = [];
831
+ const appends = [];
832
+ const casts = [];
833
+ const relations = [];
834
+ const docProperties = [];
835
+ if (schema.options?.softDelete) {
836
+ imports.push("use Illuminate\\Database\\Eloquent\\SoftDeletes;");
837
+ traits.push(" use SoftDeletes;");
838
+ }
839
+ const properties = schema.properties ?? {};
840
+ for (const [propName, propDef] of Object.entries(properties)) {
841
+ const snakeName = toSnakeCase(propName);
842
+ const phpType = getPhpDocType(propDef, schemas);
843
+ docProperties.push(` * @property ${phpType} $${snakeName}`);
844
+ if (propDef.type === "Association") {
845
+ const assoc = propDef;
846
+ if (assoc.target) {
847
+ imports.push(`use ${options.modelNamespace}\\${toPascalCase(assoc.target)};`);
848
+ }
849
+ relations.push(generateRelation(propName, assoc, options));
850
+ if (assoc.relation === "ManyToOne" || assoc.relation === "OneToOne") {
851
+ if (!assoc.mappedBy) {
852
+ const fkName = toSnakeCase(propName) + "_id";
853
+ fillable.push(` '${fkName}',`);
854
+ docProperties.push(` * @property int|null $${fkName}`);
855
+ }
856
+ }
857
+ } else if (propDef.type === "Password") {
858
+ fillable.push(` '${snakeName}',`);
859
+ hidden.push(` '${snakeName}',`);
860
+ const cast = getCastType(propDef);
861
+ if (cast) {
862
+ casts.push(` '${snakeName}' => '${cast}',`);
863
+ }
864
+ } else if (propDef.type === "File") {
865
+ const relMethod = generateFileRelation(propName, propDef);
866
+ relations.push(relMethod);
867
+ } else {
868
+ fillable.push(` '${snakeName}',`);
869
+ const cast = getCastType(propDef);
870
+ if (cast) {
871
+ casts.push(` '${snakeName}' => '${cast}',`);
872
+ }
888
873
  }
889
- interfaces.push(schemaToInterface(schema, schemas, options));
890
874
  }
891
- return interfaces;
892
- }
875
+ const docComment = `/**
876
+ * ${className}BaseModel
877
+ *
878
+ ${docProperties.join("\n")}
879
+ */`;
880
+ const stub = isAuth ? authStubContent : stubContent;
881
+ const keyType = isStringKey ? ` /**
882
+ * The "type" of the primary key ID.
883
+ */
884
+ protected $keyType = 'string';
893
885
 
894
- // src/typescript/enum-generator.ts
895
- function toEnumMemberName(value) {
896
- return value.split(/[-_\s]+/).map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join("").replace(/[^a-zA-Z0-9]/g, "");
897
- }
898
- function toEnumName(schemaName) {
899
- return schemaName;
900
- }
901
- function schemaToEnum(schema) {
902
- if (schema.kind !== "enum" || !schema.values) {
903
- return null;
886
+ ` : "";
887
+ const incrementing = isUuid ? ` /**
888
+ * Indicates if the IDs are auto-incrementing.
889
+ */
890
+ public $incrementing = false;
891
+
892
+ ` : "";
893
+ if (isUuid) {
894
+ imports.push("use Illuminate\\Database\\Eloquent\\Concerns\\HasUuids;");
895
+ traits.push(" use HasUuids;");
904
896
  }
905
- const values = schema.values.map((value) => ({
906
- name: toEnumMemberName(value),
907
- value
908
- }));
897
+ const content = stub.replace(/\{\{BASE_MODEL_NAMESPACE\}\}/g, options.baseModelNamespace).replace(/\{\{BASE_MODEL_CLASS\}\}/g, options.baseModelClassName).replace(/\{\{CLASS_NAME\}\}/g, className).replace(/\{\{TABLE_NAME\}\}/g, tableName).replace(/\{\{PRIMARY_KEY\}\}/g, primaryKey).replace(/\{\{KEY_TYPE\}\}/g, keyType).replace(/\{\{INCREMENTING\}\}/g, incrementing).replace(/\{\{TIMESTAMPS\}\}/g, schema.options?.timestamps !== false ? "true" : "false").replace(/\{\{IMPORTS\}\}/g, [...new Set(imports)].sort().join("\n")).replace(/\{\{TRAITS\}\}/g, traits.join("\n")).replace(/\{\{DOC_COMMENT\}\}/g, docComment).replace(/\{\{FILLABLE\}\}/g, fillable.join("\n")).replace(/\{\{HIDDEN\}\}/g, hidden.join("\n")).replace(/\{\{APPENDS\}\}/g, appends.join("\n")).replace(/\{\{CASTS\}\}/g, casts.join("\n")).replace(/\{\{RELATIONS\}\}/g, relations.join("\n\n"));
909
898
  return {
910
- name: toEnumName(schema.name),
911
- values,
912
- comment: schema.displayName ?? schema.name
899
+ path: `${options.baseModelPath}/${className}BaseModel.php`,
900
+ content,
901
+ type: "entity-base",
902
+ overwrite: true,
903
+ schemaName: schema.name
913
904
  };
914
905
  }
915
- function generateEnums(schemas) {
916
- const enums = [];
917
- for (const schema of Object.values(schemas)) {
918
- if (schema.kind === "enum") {
919
- const enumDef = schemaToEnum(schema);
920
- if (enumDef) {
921
- enums.push(enumDef);
906
+ function generateRelation(propName, assoc, options) {
907
+ const methodName = toCamelCase(propName);
908
+ const targetClass = assoc.target ? toPascalCase(assoc.target) : "";
909
+ const fkName = toSnakeCase(propName) + "_id";
910
+ switch (assoc.relation) {
911
+ case "ManyToOne":
912
+ return ` /**
913
+ * Get the ${propName} that owns this model.
914
+ */
915
+ public function ${methodName}(): BelongsTo
916
+ {
917
+ return $this->belongsTo(${targetClass}::class, '${fkName}');
918
+ }`;
919
+ case "OneToOne":
920
+ if (assoc.mappedBy) {
921
+ return ` /**
922
+ * Get the ${propName} for this model.
923
+ */
924
+ public function ${methodName}(): HasOne
925
+ {
926
+ return $this->hasOne(${targetClass}::class, '${toSnakeCase(assoc.mappedBy)}_id');
927
+ }`;
922
928
  }
929
+ return ` /**
930
+ * Get the ${propName} that owns this model.
931
+ */
932
+ public function ${methodName}(): BelongsTo
933
+ {
934
+ return $this->belongsTo(${targetClass}::class, '${fkName}');
935
+ }`;
936
+ case "OneToMany":
937
+ return ` /**
938
+ * Get the ${propName} for this model.
939
+ */
940
+ public function ${methodName}(): HasMany
941
+ {
942
+ return $this->hasMany(${targetClass}::class, '${toSnakeCase(assoc.inversedBy ?? propName)}_id');
943
+ }`;
944
+ case "ManyToMany": {
945
+ const pivotTable = assoc.joinTable ?? `${toSnakeCase(propName)}_pivot`;
946
+ return ` /**
947
+ * The ${propName} that belong to this model.
948
+ */
949
+ public function ${methodName}(): BelongsToMany
950
+ {
951
+ return $this->belongsToMany(${targetClass}::class, '${pivotTable}')
952
+ ->withTimestamps();
953
+ }`;
923
954
  }
955
+ case "MorphTo":
956
+ return ` /**
957
+ * Get the parent ${propName} model.
958
+ */
959
+ public function ${methodName}(): MorphTo
960
+ {
961
+ return $this->morphTo('${methodName}');
962
+ }`;
963
+ case "MorphOne":
964
+ return ` /**
965
+ * Get the ${propName} for this model.
966
+ */
967
+ public function ${methodName}(): MorphOne
968
+ {
969
+ return $this->morphOne(${targetClass}::class, '${assoc.morphName ?? propName}');
970
+ }`;
971
+ case "MorphMany":
972
+ return ` /**
973
+ * Get the ${propName} for this model.
974
+ */
975
+ public function ${methodName}(): MorphMany
976
+ {
977
+ return $this->morphMany(${targetClass}::class, '${assoc.morphName ?? propName}');
978
+ }`;
979
+ default:
980
+ return ` // TODO: Implement ${assoc.relation} relation for ${propName}`;
924
981
  }
925
- return enums;
926
982
  }
927
- function formatEnum(enumDef) {
928
- const comment = enumDef.comment ? `/**
929
- * ${enumDef.comment}
930
- */
931
- ` : "";
932
- const values = enumDef.values.map((v) => ` ${v.name} = '${v.value}',`).join("\n");
933
- return `${comment}export enum ${enumDef.name} {
934
- ${values}
935
- }`;
983
+ function generateFileRelation(propName, propDef) {
984
+ const methodName = toCamelCase(propName);
985
+ const relationType = propDef.multiple ? "MorphMany" : "MorphOne";
986
+ const relationMethod = propDef.multiple ? "morphMany" : "morphOne";
987
+ return ` /**
988
+ * Get the ${propName} file(s) for this model.
989
+ */
990
+ public function ${methodName}(): ${relationType}
991
+ {
992
+ return $this->${relationMethod}(FileUpload::class, 'uploadable')
993
+ ->where('attribute_name', '${propName}');
994
+ }`;
936
995
  }
937
- function formatTypeAlias(alias) {
938
- const comment = alias.comment ? `/**
939
- * ${alias.comment}
940
- */
941
- ` : "";
942
- return `${comment}export type ${alias.name} = ${alias.type};`;
996
+ function generateEntityModel(schema, options, stubContent) {
997
+ const className = toPascalCase(schema.name);
998
+ const content = stubContent.replace(/\{\{BASE_MODEL_NAMESPACE\}\}/g, options.baseModelNamespace).replace(/\{\{MODEL_NAMESPACE\}\}/g, options.modelNamespace).replace(/\{\{CLASS_NAME\}\}/g, className);
999
+ return {
1000
+ path: `${options.modelPath}/${className}.php`,
1001
+ content,
1002
+ type: "entity",
1003
+ overwrite: false,
1004
+ // Never overwrite user models
1005
+ schemaName: schema.name
1006
+ };
943
1007
  }
944
- function extractInlineEnums(schemas) {
945
- const typeAliases = [];
946
- for (const schema of Object.values(schemas)) {
947
- if (schema.kind === "enum" || !schema.properties) {
948
- continue;
1008
+ function getStubContent(stubName) {
1009
+ const stubs = {
1010
+ "base-model": `<?php
1011
+
1012
+ namespace {{BASE_MODEL_NAMESPACE}};
1013
+
1014
+ /**
1015
+ * Base model class for all Omnify-generated models.
1016
+ * Contains model mapping for polymorphic relations.
1017
+ *
1018
+ * DO NOT EDIT - This file is auto-generated by Omnify.
1019
+ * Any changes will be overwritten on next generation.
1020
+ *
1021
+ * @generated by @famgia/omnify-laravel
1022
+ */
1023
+
1024
+ use Illuminate\\Database\\Eloquent\\Model;
1025
+ use Illuminate\\Database\\Eloquent\\Relations\\Relation;
1026
+
1027
+ abstract class {{BASE_MODEL_CLASS}} extends Model
1028
+ {
1029
+ /**
1030
+ * Model class map for polymorphic relations.
1031
+ */
1032
+ protected static array $modelMap = [
1033
+ {{MODEL_MAP}}
1034
+ ];
1035
+
1036
+ /**
1037
+ * Boot the model and register morph map.
1038
+ */
1039
+ protected static function boot(): void
1040
+ {
1041
+ parent::boot();
1042
+
1043
+ // Register morph map for polymorphic relations
1044
+ Relation::enforceMorphMap(static::$modelMap);
949
1045
  }
950
- for (const [propName, property] of Object.entries(schema.properties)) {
951
- if (property.type === "Enum") {
952
- const enumProp = property;
953
- if (Array.isArray(enumProp.enum) && enumProp.enum.length > 0) {
954
- const typeName = `${schema.name}${propName.charAt(0).toUpperCase() + propName.slice(1)}`;
955
- typeAliases.push({
956
- name: typeName,
957
- type: enumProp.enum.map((v) => `'${v}'`).join(" | "),
958
- comment: enumProp.displayName ?? `${schema.name} ${propName} enum`
959
- });
960
- }
961
- }
962
- if (property.type === "Select") {
963
- const selectProp = property;
964
- if (selectProp.options && selectProp.options.length > 0) {
965
- const typeName = `${schema.name}${propName.charAt(0).toUpperCase() + propName.slice(1)}`;
966
- typeAliases.push({
967
- name: typeName,
968
- type: selectProp.options.map((v) => `'${v}'`).join(" | "),
969
- comment: selectProp.displayName ?? `${schema.name} ${propName} options`
970
- });
971
- }
972
- }
1046
+
1047
+ /**
1048
+ * Get the model class for a given morph type.
1049
+ */
1050
+ public static function getModelClass(string $morphType): ?string
1051
+ {
1052
+ return static::$modelMap[$morphType] ?? null;
973
1053
  }
974
- }
975
- return typeAliases;
976
1054
  }
1055
+ `,
1056
+ "entity-base": `<?php
977
1057
 
978
- // src/typescript/generator.ts
979
- var DEFAULT_OPTIONS = {
980
- singleFile: true,
981
- fileName: "types.ts",
982
- readonly: true,
983
- strictNullChecks: true
984
- };
985
- function generateHeader() {
986
- return `/**
987
- * Auto-generated TypeScript types from Omnify schemas.
988
- * DO NOT EDIT - This file is automatically generated.
1058
+ namespace {{BASE_MODEL_NAMESPACE}};
1059
+
1060
+ /**
1061
+ * DO NOT EDIT - This file is auto-generated by Omnify.
1062
+ * Any changes will be overwritten on next generation.
1063
+ *
1064
+ * @generated by @famgia/omnify-laravel
989
1065
  */
990
1066
 
991
- `;
992
- }
993
- function generateTypeScriptFile(schemas, options = {}) {
994
- const opts = { ...DEFAULT_OPTIONS, ...options };
995
- const parts = [generateHeader()];
996
- const types = [];
997
- const enums = generateEnums(schemas);
998
- if (enums.length > 0) {
999
- parts.push("// Enums\n");
1000
- for (const enumDef of enums) {
1001
- parts.push(formatEnum(enumDef));
1002
- parts.push("\n\n");
1003
- types.push(enumDef.name);
1004
- }
1005
- }
1006
- const inlineEnums = extractInlineEnums(schemas);
1007
- if (inlineEnums.length > 0) {
1008
- parts.push("// Type Aliases\n");
1009
- for (const alias of inlineEnums) {
1010
- parts.push(formatTypeAlias(alias));
1011
- parts.push("\n\n");
1012
- types.push(alias.name);
1013
- }
1014
- }
1015
- const interfaces = generateInterfaces(schemas, opts);
1016
- if (interfaces.length > 0) {
1017
- parts.push("// Interfaces\n");
1018
- for (const iface of interfaces) {
1019
- parts.push(formatInterface(iface));
1020
- parts.push("\n\n");
1021
- types.push(iface.name);
1067
+ use Illuminate\\Database\\Eloquent\\Relations\\BelongsTo;
1068
+ use Illuminate\\Database\\Eloquent\\Relations\\HasMany;
1069
+ use Illuminate\\Database\\Eloquent\\Relations\\HasOne;
1070
+ use Illuminate\\Database\\Eloquent\\Relations\\BelongsToMany;
1071
+ use Illuminate\\Database\\Eloquent\\Relations\\MorphTo;
1072
+ use Illuminate\\Database\\Eloquent\\Relations\\MorphOne;
1073
+ use Illuminate\\Database\\Eloquent\\Relations\\MorphMany;
1074
+ use Illuminate\\Database\\Eloquent\\Relations\\MorphToMany;
1075
+ use Illuminate\\Database\\Eloquent\\Collection as EloquentCollection;
1076
+ {{IMPORTS}}
1077
+
1078
+ {{DOC_COMMENT}}
1079
+ class {{CLASS_NAME}}BaseModel extends {{BASE_MODEL_CLASS}}
1080
+ {
1081
+ {{TRAITS}}
1082
+ /**
1083
+ * The table associated with the model.
1084
+ */
1085
+ protected $table = '{{TABLE_NAME}}';
1086
+
1087
+ /**
1088
+ * The primary key for the model.
1089
+ */
1090
+ protected $primaryKey = '{{PRIMARY_KEY}}';
1091
+
1092
+ {{KEY_TYPE}}
1093
+ {{INCREMENTING}}
1094
+ /**
1095
+ * Indicates if the model should be timestamped.
1096
+ */
1097
+ public $timestamps = {{TIMESTAMPS}};
1098
+
1099
+ /**
1100
+ * The attributes that are mass assignable.
1101
+ */
1102
+ protected $fillable = [
1103
+ {{FILLABLE}}
1104
+ ];
1105
+
1106
+ /**
1107
+ * The attributes that should be hidden for serialization.
1108
+ */
1109
+ protected $hidden = [
1110
+ {{HIDDEN}}
1111
+ ];
1112
+
1113
+ /**
1114
+ * The accessors to append to the model's array form.
1115
+ */
1116
+ protected $appends = [
1117
+ {{APPENDS}}
1118
+ ];
1119
+
1120
+ /**
1121
+ * Get the attributes that should be cast.
1122
+ */
1123
+ protected function casts(): array
1124
+ {
1125
+ return [
1126
+ {{CASTS}}
1127
+ ];
1022
1128
  }
1023
- }
1024
- return {
1025
- fileName: opts.fileName ?? "types.ts",
1026
- content: parts.join("").trim() + "\n",
1027
- types
1028
- };
1029
- }
1030
- function generateTypeScriptFiles(schemas, options = {}) {
1031
- const opts = { ...DEFAULT_OPTIONS, ...options };
1032
- const files = [];
1033
- const enums = generateEnums(schemas);
1034
- if (enums.length > 0) {
1035
- const content = generateHeader() + enums.map(formatEnum).join("\n\n") + "\n";
1036
- files.push({
1037
- fileName: "enums.ts",
1038
- content,
1039
- types: enums.map((e) => e.name)
1040
- });
1041
- }
1042
- const inlineEnums = extractInlineEnums(schemas);
1043
- if (inlineEnums.length > 0) {
1044
- const content = generateHeader() + inlineEnums.map(formatTypeAlias).join("\n\n") + "\n";
1045
- files.push({
1046
- fileName: "type-aliases.ts",
1047
- content,
1048
- types: inlineEnums.map((a) => a.name)
1049
- });
1050
- }
1051
- const interfaces = generateInterfaces(schemas, opts);
1052
- for (const iface of interfaces) {
1053
- const imports = collectImports(iface, enums, inlineEnums, interfaces);
1054
- const importStatement = formatImports(imports);
1055
- const content = generateHeader() + (importStatement ? importStatement + "\n\n" : "") + formatInterface(iface) + "\n";
1056
- files.push({
1057
- fileName: `${toKebabCase(iface.name)}.ts`,
1058
- content,
1059
- types: [iface.name]
1060
- });
1061
- }
1062
- const indexContent = generateIndexFile(files);
1063
- files.push({
1064
- fileName: "index.ts",
1065
- content: indexContent,
1066
- types: []
1067
- });
1068
- return files;
1069
- }
1070
- function toKebabCase(name) {
1071
- return name.replace(/([A-Z])/g, "-$1").toLowerCase().replace(/^-/, "");
1129
+
1130
+ {{RELATIONS}}
1072
1131
  }
1073
- function collectImports(iface, enums, typeAliases, allInterfaces) {
1074
- const imports = /* @__PURE__ */ new Map();
1075
- const enumNames = new Set(enums.map((e) => e.name));
1076
- const aliasNames = new Set(typeAliases.map((a) => a.name));
1077
- const interfaceNames = new Set(allInterfaces.map((i) => i.name));
1078
- for (const prop of iface.properties) {
1079
- if (enumNames.has(prop.type)) {
1080
- const existing = imports.get("./enums.js") ?? [];
1081
- if (!existing.includes(prop.type)) {
1082
- imports.set("./enums.js", [...existing, prop.type]);
1083
- }
1084
- }
1085
- if (aliasNames.has(prop.type)) {
1086
- const existing = imports.get("./type-aliases.js") ?? [];
1087
- if (!existing.includes(prop.type)) {
1088
- imports.set("./type-aliases.js", [...existing, prop.type]);
1089
- }
1132
+ `,
1133
+ "entity-base-auth": `<?php
1134
+
1135
+ namespace {{BASE_MODEL_NAMESPACE}};
1136
+
1137
+ /**
1138
+ * DO NOT EDIT - This file is auto-generated by Omnify.
1139
+ * Any changes will be overwritten on next generation.
1140
+ *
1141
+ * @generated by @famgia/omnify-laravel
1142
+ */
1143
+
1144
+ use Illuminate\\Foundation\\Auth\\User as Authenticatable;
1145
+ use Illuminate\\Database\\Eloquent\\Relations\\BelongsTo;
1146
+ use Illuminate\\Database\\Eloquent\\Relations\\HasMany;
1147
+ use Illuminate\\Database\\Eloquent\\Relations\\HasOne;
1148
+ use Illuminate\\Database\\Eloquent\\Relations\\BelongsToMany;
1149
+ use Illuminate\\Database\\Eloquent\\Relations\\MorphTo;
1150
+ use Illuminate\\Database\\Eloquent\\Relations\\MorphOne;
1151
+ use Illuminate\\Database\\Eloquent\\Relations\\MorphMany;
1152
+ use Illuminate\\Database\\Eloquent\\Relations\\MorphToMany;
1153
+ use Illuminate\\Database\\Eloquent\\Collection as EloquentCollection;
1154
+ use Illuminate\\Notifications\\Notifiable;
1155
+ {{IMPORTS}}
1156
+
1157
+ {{DOC_COMMENT}}
1158
+ class {{CLASS_NAME}}BaseModel extends Authenticatable
1159
+ {
1160
+ use Notifiable;
1161
+ {{TRAITS}}
1162
+ /**
1163
+ * The table associated with the model.
1164
+ */
1165
+ protected $table = '{{TABLE_NAME}}';
1166
+
1167
+ /**
1168
+ * The primary key for the model.
1169
+ */
1170
+ protected $primaryKey = '{{PRIMARY_KEY}}';
1171
+
1172
+ {{KEY_TYPE}}
1173
+ {{INCREMENTING}}
1174
+ /**
1175
+ * Indicates if the model should be timestamped.
1176
+ */
1177
+ public $timestamps = {{TIMESTAMPS}};
1178
+
1179
+ /**
1180
+ * The attributes that are mass assignable.
1181
+ */
1182
+ protected $fillable = [
1183
+ {{FILLABLE}}
1184
+ ];
1185
+
1186
+ /**
1187
+ * The attributes that should be hidden for serialization.
1188
+ */
1189
+ protected $hidden = [
1190
+ {{HIDDEN}}
1191
+ ];
1192
+
1193
+ /**
1194
+ * The accessors to append to the model's array form.
1195
+ */
1196
+ protected $appends = [
1197
+ {{APPENDS}}
1198
+ ];
1199
+
1200
+ /**
1201
+ * Get the attributes that should be cast.
1202
+ */
1203
+ protected function casts(): array
1204
+ {
1205
+ return [
1206
+ {{CASTS}}
1207
+ ];
1090
1208
  }
1091
- const baseType = prop.type.replace("[]", "");
1092
- if (interfaceNames.has(baseType) && baseType !== iface.name) {
1093
- const fileName = `./${toKebabCase(baseType)}.js`;
1094
- const existing = imports.get(fileName) ?? [];
1095
- if (!existing.includes(baseType)) {
1096
- imports.set(fileName, [...existing, baseType]);
1097
- }
1209
+
1210
+ {{RELATIONS}}
1211
+ }
1212
+ `,
1213
+ "entity": `<?php
1214
+
1215
+ namespace {{MODEL_NAMESPACE}};
1216
+
1217
+ use {{BASE_MODEL_NAMESPACE}}\\{{CLASS_NAME}}BaseModel;
1218
+ use Illuminate\\Database\\Eloquent\\Factories\\HasFactory;
1219
+
1220
+ /**
1221
+ * {{CLASS_NAME}} Model
1222
+ *
1223
+ * This file is generated once and can be customized.
1224
+ * Add your custom methods and logic here.
1225
+ */
1226
+ class {{CLASS_NAME}} extends {{CLASS_NAME}}BaseModel
1227
+ {
1228
+ use HasFactory;
1229
+
1230
+ /**
1231
+ * Create a new model instance.
1232
+ */
1233
+ public function __construct(array $attributes = [])
1234
+ {
1235
+ parent::__construct($attributes);
1098
1236
  }
1099
- }
1100
- return imports;
1237
+
1238
+ // Add your custom methods here
1101
1239
  }
1102
- function formatImports(imports) {
1103
- if (imports.size === 0) return "";
1104
- const lines = [];
1105
- for (const [path, names] of imports) {
1106
- lines.push(`import type { ${names.join(", ")} } from '${path}';`);
1107
- }
1108
- return lines.join("\n");
1240
+ `
1241
+ };
1242
+ return stubs[stubName] ?? "";
1109
1243
  }
1110
- function generateIndexFile(files) {
1111
- const exports2 = [generateHeader().trim(), ""];
1112
- for (const file of files) {
1113
- if (file.fileName === "index.ts") continue;
1114
- const moduleName = file.fileName.replace(".ts", ".js");
1115
- exports2.push(`export * from './${moduleName}';`);
1116
- }
1117
- return exports2.join("\n") + "\n";
1244
+ function generateModels(schemas, options) {
1245
+ const resolved = resolveOptions(options);
1246
+ const models = [];
1247
+ models.push(generateBaseModel(schemas, resolved, getStubContent("base-model")));
1248
+ for (const schema of Object.values(schemas)) {
1249
+ if (schema.kind === "enum") {
1250
+ continue;
1251
+ }
1252
+ models.push(generateEntityBaseModel(
1253
+ schema,
1254
+ schemas,
1255
+ resolved,
1256
+ getStubContent("entity-base"),
1257
+ getStubContent("entity-base-auth")
1258
+ ));
1259
+ models.push(generateEntityModel(schema, resolved, getStubContent("entity")));
1260
+ }
1261
+ return models;
1118
1262
  }
1119
- function generateTypeScript(schemas, options = {}) {
1120
- const opts = { ...DEFAULT_OPTIONS, ...options };
1121
- if (opts.singleFile) {
1122
- return [generateTypeScriptFile(schemas, opts)];
1123
- }
1124
- return generateTypeScriptFiles(schemas, opts);
1263
+ function getModelPath(model) {
1264
+ return model.path;
1125
1265
  }
1126
1266
 
1127
1267
  // src/plugin.ts
1128
1268
  var LARAVEL_CONFIG_SCHEMA = {
1129
1269
  fields: [
1130
- // Paths group
1131
1270
  {
1132
1271
  key: "migrationsPath",
1133
1272
  type: "path",
1134
1273
  label: "Migrations Path",
1135
1274
  description: "Directory for Laravel migration files",
1136
1275
  default: "database/migrations",
1137
- group: "paths"
1276
+ group: "output"
1138
1277
  },
1139
1278
  {
1140
- key: "typesPath",
1279
+ key: "modelsPath",
1141
1280
  type: "path",
1142
- label: "TypeScript Types Path",
1143
- description: "Directory for generated TypeScript type files",
1144
- default: "types",
1145
- group: "paths"
1146
- },
1147
- // Generators group
1148
- {
1149
- key: "generateMigrations",
1150
- type: "boolean",
1151
- label: "Generate Migrations",
1152
- description: "Generate Laravel migration files",
1153
- default: true,
1154
- group: "generators"
1281
+ label: "Models Path",
1282
+ description: "Directory for user-editable model files",
1283
+ default: "app/Models",
1284
+ group: "output"
1155
1285
  },
1156
1286
  {
1157
- key: "generateTypes",
1158
- type: "boolean",
1159
- label: "Generate TypeScript Types",
1160
- description: "Generate TypeScript type definitions",
1161
- default: true,
1162
- group: "generators"
1287
+ key: "baseModelsPath",
1288
+ type: "path",
1289
+ label: "Base Models Path",
1290
+ description: "Directory for auto-generated base model files",
1291
+ default: "app/Models/OmnifyBase",
1292
+ group: "output"
1163
1293
  },
1164
- // Options group
1165
1294
  {
1166
- key: "singleFile",
1295
+ key: "generateModels",
1167
1296
  type: "boolean",
1168
- label: "Single File Output",
1169
- description: "Generate all types in a single file",
1297
+ label: "Generate Models",
1298
+ description: "Generate Eloquent model classes",
1170
1299
  default: true,
1171
1300
  group: "options"
1172
1301
  },
@@ -1180,69 +1309,69 @@ var LARAVEL_CONFIG_SCHEMA = {
1180
1309
  }
1181
1310
  ]
1182
1311
  };
1183
- function resolveOptions(options) {
1312
+ function resolveOptions2(options) {
1184
1313
  return {
1185
1314
  migrationsPath: options?.migrationsPath ?? "database/migrations",
1186
- typesPath: options?.typesPath ?? "types",
1187
- singleFile: options?.singleFile ?? true,
1315
+ modelsPath: options?.modelsPath ?? "app/Models",
1316
+ baseModelsPath: options?.baseModelsPath ?? "app/Models/OmnifyBase",
1317
+ modelNamespace: options?.modelNamespace ?? "App\\Models",
1318
+ baseModelNamespace: options?.baseModelNamespace ?? "App\\Models\\OmnifyBase",
1319
+ generateModels: options?.generateModels ?? true,
1188
1320
  connection: options?.connection,
1189
- timestamp: options?.timestamp,
1190
- generateTypes: options?.generateTypes ?? true,
1191
- generateMigrations: options?.generateMigrations ?? true
1321
+ timestamp: options?.timestamp
1192
1322
  };
1193
1323
  }
1194
1324
  function laravelPlugin(options) {
1195
- const resolved = resolveOptions(options);
1325
+ const resolved = resolveOptions2(options);
1326
+ const migrationGenerator = {
1327
+ name: "laravel-migrations",
1328
+ description: "Generate Laravel migration files",
1329
+ generate: async (ctx) => {
1330
+ const migrationOptions = {
1331
+ connection: resolved.connection,
1332
+ timestamp: resolved.timestamp
1333
+ };
1334
+ const migrations = generateMigrations(ctx.schemas, migrationOptions);
1335
+ return migrations.map((migration) => ({
1336
+ path: getMigrationPath(migration, resolved.migrationsPath),
1337
+ content: migration.content,
1338
+ type: "migration",
1339
+ metadata: {
1340
+ tableName: migration.tables[0],
1341
+ migrationType: migration.type
1342
+ }
1343
+ }));
1344
+ }
1345
+ };
1346
+ const modelGenerator = {
1347
+ name: "laravel-models",
1348
+ description: "Generate Eloquent model classes",
1349
+ generate: async (ctx) => {
1350
+ const modelOptions = {
1351
+ modelNamespace: resolved.modelNamespace,
1352
+ baseModelNamespace: resolved.baseModelNamespace,
1353
+ modelPath: resolved.modelsPath,
1354
+ baseModelPath: resolved.baseModelsPath
1355
+ };
1356
+ const models = generateModels(ctx.schemas, modelOptions);
1357
+ return models.map((model) => ({
1358
+ path: getModelPath(model),
1359
+ content: model.content,
1360
+ type: "model",
1361
+ // Skip writing user models if they already exist
1362
+ skipIfExists: !model.overwrite,
1363
+ metadata: {
1364
+ modelType: model.type,
1365
+ schemaName: model.schemaName
1366
+ }
1367
+ }));
1368
+ }
1369
+ };
1196
1370
  return {
1197
1371
  name: "@famgia/omnify-laravel",
1198
- version: "0.0.12",
1372
+ version: "0.0.14",
1199
1373
  configSchema: LARAVEL_CONFIG_SCHEMA,
1200
- generators: [
1201
- // Laravel Migrations Generator
1202
- ...resolved.generateMigrations ? [
1203
- {
1204
- name: "laravel-migrations",
1205
- description: "Generate Laravel migration files",
1206
- generate: async (ctx) => {
1207
- const migrationOptions = {
1208
- connection: resolved.connection,
1209
- timestamp: resolved.timestamp
1210
- };
1211
- const migrations = generateMigrations(ctx.schemas, migrationOptions);
1212
- return migrations.map((migration) => ({
1213
- path: getMigrationPath(migration, resolved.migrationsPath),
1214
- content: migration.content,
1215
- type: "migration",
1216
- metadata: {
1217
- tableName: migration.tables[0],
1218
- migrationType: migration.type
1219
- }
1220
- }));
1221
- }
1222
- }
1223
- ] : [],
1224
- // TypeScript Types Generator
1225
- ...resolved.generateTypes ? [
1226
- {
1227
- name: "typescript-types",
1228
- description: "Generate TypeScript type definitions",
1229
- generate: async (ctx) => {
1230
- const tsOptions = {
1231
- singleFile: resolved.singleFile
1232
- };
1233
- const files = generateTypeScript(ctx.schemas, tsOptions);
1234
- return files.map((file) => ({
1235
- path: `${resolved.typesPath}/${file.fileName}`,
1236
- content: file.content,
1237
- type: "type",
1238
- metadata: {
1239
- types: file.types
1240
- }
1241
- }));
1242
- }
1243
- }
1244
- ] : []
1245
- ]
1374
+ generators: resolved.generateModels ? [migrationGenerator, modelGenerator] : [migrationGenerator]
1246
1375
  };
1247
1376
  }
1248
1377
  // Annotate the CommonJS export names for ESM import in node: