@famgia/omnify-laravel 0.0.13 → 0.0.15
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/chunk-REDFZUQY.js +2239 -0
- package/dist/chunk-REDFZUQY.js.map +1 -0
- package/dist/index.cjs +1203 -36
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +1 -1
- package/dist/plugin.cjs +1201 -36
- package/dist/plugin.cjs.map +1 -1
- package/dist/plugin.d.cts +44 -3
- package/dist/plugin.d.ts +44 -3
- package/dist/plugin.js +1 -1
- package/package.json +3 -3
- package/dist/chunk-G4UAJVC2.js +0 -1072
- package/dist/chunk-G4UAJVC2.js.map +0 -1
package/dist/plugin.cjs
CHANGED
|
@@ -24,6 +24,8 @@ __export(plugin_exports, {
|
|
|
24
24
|
laravelPlugin: () => laravelPlugin
|
|
25
25
|
});
|
|
26
26
|
module.exports = __toCommonJS(plugin_exports);
|
|
27
|
+
var import_node_fs = require("fs");
|
|
28
|
+
var import_node_path = require("path");
|
|
27
29
|
|
|
28
30
|
// src/migration/schema-builder.ts
|
|
29
31
|
var TYPE_METHOD_MAP = {
|
|
@@ -103,9 +105,8 @@ function propertyToColumnMethod(propertyName, property) {
|
|
|
103
105
|
if (baseProp.unique) {
|
|
104
106
|
modifiers.push({ method: "unique" });
|
|
105
107
|
}
|
|
106
|
-
if (baseProp.default !== void 0) {
|
|
107
|
-
|
|
108
|
-
modifiers.push({ method: "default", args: [defaultValue] });
|
|
108
|
+
if (baseProp.default !== void 0 && baseProp.default !== null) {
|
|
109
|
+
modifiers.push({ method: "default", args: [baseProp.default] });
|
|
109
110
|
}
|
|
110
111
|
if (baseProp.unsigned && (method === "integer" || method === "bigInteger")) {
|
|
111
112
|
modifiers.push({ method: "unsigned" });
|
|
@@ -293,11 +294,18 @@ function schemaToBlueprint(schema, allSchemas) {
|
|
|
293
294
|
}
|
|
294
295
|
if (schema.options?.indexes) {
|
|
295
296
|
for (const index of schema.options.indexes) {
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
297
|
+
if (typeof index === "string") {
|
|
298
|
+
indexes.push({
|
|
299
|
+
columns: [toColumnName(index)],
|
|
300
|
+
unique: false
|
|
301
|
+
});
|
|
302
|
+
} else {
|
|
303
|
+
indexes.push({
|
|
304
|
+
name: index.name,
|
|
305
|
+
columns: index.columns.map(toColumnName),
|
|
306
|
+
unique: index.unique ?? false
|
|
307
|
+
});
|
|
308
|
+
}
|
|
301
309
|
}
|
|
302
310
|
}
|
|
303
311
|
if (schema.options?.unique) {
|
|
@@ -334,6 +342,12 @@ function formatColumnMethod(column) {
|
|
|
334
342
|
if (typeof arg === "string") {
|
|
335
343
|
return `'${arg}'`;
|
|
336
344
|
}
|
|
345
|
+
if (typeof arg === "boolean") {
|
|
346
|
+
return arg ? "true" : "false";
|
|
347
|
+
}
|
|
348
|
+
if (typeof arg === "number") {
|
|
349
|
+
return String(arg);
|
|
350
|
+
}
|
|
337
351
|
return String(arg);
|
|
338
352
|
}).join(", ");
|
|
339
353
|
code += `->${modifier.method}(${modArgs})`;
|
|
@@ -441,6 +455,7 @@ function generatePivotTableBlueprint(pivot) {
|
|
|
441
455
|
args: [pivot.targetColumn],
|
|
442
456
|
modifiers: []
|
|
443
457
|
});
|
|
458
|
+
columns.push(...generateTimestampColumns());
|
|
444
459
|
foreignKeys.push({
|
|
445
460
|
columns: [pivot.sourceColumn],
|
|
446
461
|
references: "id",
|
|
@@ -684,6 +699,1016 @@ function getMigrationPath(migration, outputDir = "database/migrations") {
|
|
|
684
699
|
return `${outputDir}/${migration.fileName}`;
|
|
685
700
|
}
|
|
686
701
|
|
|
702
|
+
// src/utils.ts
|
|
703
|
+
function toSnakeCase(str) {
|
|
704
|
+
return str.replace(/([A-Z])/g, "_$1").replace(/^_/, "").toLowerCase();
|
|
705
|
+
}
|
|
706
|
+
function toPascalCase(str) {
|
|
707
|
+
return str.replace(/[-_](.)/g, (_, c) => c.toUpperCase()).replace(/^(.)/, (_, c) => c.toUpperCase());
|
|
708
|
+
}
|
|
709
|
+
function toCamelCase(str) {
|
|
710
|
+
const pascal = toPascalCase(str);
|
|
711
|
+
return pascal.charAt(0).toLowerCase() + pascal.slice(1);
|
|
712
|
+
}
|
|
713
|
+
function pluralize(word) {
|
|
714
|
+
if (word.endsWith("y") && !["ay", "ey", "iy", "oy", "uy"].some((v) => word.endsWith(v))) {
|
|
715
|
+
return word.slice(0, -1) + "ies";
|
|
716
|
+
}
|
|
717
|
+
if (word.endsWith("s") || word.endsWith("x") || word.endsWith("z") || word.endsWith("ch") || word.endsWith("sh")) {
|
|
718
|
+
return word + "es";
|
|
719
|
+
}
|
|
720
|
+
return word + "s";
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
// src/model/generator.ts
|
|
724
|
+
var DEFAULT_OPTIONS = {
|
|
725
|
+
baseModelNamespace: "App\\Models\\OmnifyBase",
|
|
726
|
+
modelNamespace: "App\\Models",
|
|
727
|
+
baseModelClassName: "BaseModel",
|
|
728
|
+
baseModelPath: "app/Models/OmnifyBase",
|
|
729
|
+
modelPath: "app/Models"
|
|
730
|
+
};
|
|
731
|
+
function resolveOptions(options) {
|
|
732
|
+
return {
|
|
733
|
+
baseModelNamespace: options?.baseModelNamespace ?? DEFAULT_OPTIONS.baseModelNamespace,
|
|
734
|
+
modelNamespace: options?.modelNamespace ?? DEFAULT_OPTIONS.modelNamespace,
|
|
735
|
+
baseModelClassName: options?.baseModelClassName ?? DEFAULT_OPTIONS.baseModelClassName,
|
|
736
|
+
baseModelPath: options?.baseModelPath ?? DEFAULT_OPTIONS.baseModelPath,
|
|
737
|
+
modelPath: options?.modelPath ?? DEFAULT_OPTIONS.modelPath
|
|
738
|
+
};
|
|
739
|
+
}
|
|
740
|
+
function getCastType(propDef) {
|
|
741
|
+
switch (propDef.type) {
|
|
742
|
+
case "Boolean":
|
|
743
|
+
return "boolean";
|
|
744
|
+
case "Int":
|
|
745
|
+
case "BigInt":
|
|
746
|
+
return "integer";
|
|
747
|
+
case "Float":
|
|
748
|
+
return "float";
|
|
749
|
+
case "Decimal":
|
|
750
|
+
return "decimal:" + (propDef.scale ?? 2);
|
|
751
|
+
case "Json":
|
|
752
|
+
return "array";
|
|
753
|
+
case "Date":
|
|
754
|
+
return "date";
|
|
755
|
+
case "Timestamp":
|
|
756
|
+
return "datetime";
|
|
757
|
+
case "Password":
|
|
758
|
+
return "hashed";
|
|
759
|
+
default:
|
|
760
|
+
return null;
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
function isNullable(propDef) {
|
|
764
|
+
return "nullable" in propDef && propDef.nullable === true;
|
|
765
|
+
}
|
|
766
|
+
function getPhpDocType(propDef, schemas) {
|
|
767
|
+
const nullable = isNullable(propDef);
|
|
768
|
+
switch (propDef.type) {
|
|
769
|
+
case "String":
|
|
770
|
+
case "Text":
|
|
771
|
+
case "LongText":
|
|
772
|
+
case "Email":
|
|
773
|
+
case "Password":
|
|
774
|
+
return "string" + (nullable ? "|null" : "");
|
|
775
|
+
case "Int":
|
|
776
|
+
case "BigInt":
|
|
777
|
+
return "int" + (nullable ? "|null" : "");
|
|
778
|
+
case "Float":
|
|
779
|
+
case "Decimal":
|
|
780
|
+
return "float" + (nullable ? "|null" : "");
|
|
781
|
+
case "Boolean":
|
|
782
|
+
return "bool" + (nullable ? "|null" : "");
|
|
783
|
+
case "Date":
|
|
784
|
+
case "Time":
|
|
785
|
+
case "Timestamp":
|
|
786
|
+
return "\\Carbon\\Carbon" + (nullable ? "|null" : "");
|
|
787
|
+
case "Json":
|
|
788
|
+
return "array" + (nullable ? "|null" : "");
|
|
789
|
+
case "Enum":
|
|
790
|
+
case "EnumRef":
|
|
791
|
+
return "string" + (nullable ? "|null" : "");
|
|
792
|
+
case "Association": {
|
|
793
|
+
const assoc = propDef;
|
|
794
|
+
if (assoc.target) {
|
|
795
|
+
const className = toPascalCase(assoc.target);
|
|
796
|
+
switch (assoc.relation) {
|
|
797
|
+
case "OneToMany":
|
|
798
|
+
case "ManyToMany":
|
|
799
|
+
case "MorphMany":
|
|
800
|
+
case "MorphToMany":
|
|
801
|
+
case "MorphedByMany":
|
|
802
|
+
return `\\Illuminate\\Database\\Eloquent\\Collection<${className}>`;
|
|
803
|
+
default:
|
|
804
|
+
return className + "|null";
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
return "mixed";
|
|
808
|
+
}
|
|
809
|
+
default:
|
|
810
|
+
return "mixed";
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
function generateBaseModel(schemas, options, stubContent) {
|
|
814
|
+
const modelMap = Object.values(schemas).filter((s) => s.kind !== "enum").map((s) => {
|
|
815
|
+
const className = toPascalCase(s.name);
|
|
816
|
+
return ` '${s.name}' => \\${options.modelNamespace}\\${className}::class,`;
|
|
817
|
+
}).join("\n");
|
|
818
|
+
const content = stubContent.replace(/\{\{BASE_MODEL_NAMESPACE\}\}/g, options.baseModelNamespace).replace(/\{\{BASE_MODEL_CLASS\}\}/g, options.baseModelClassName).replace(/\{\{MODEL_MAP\}\}/g, modelMap);
|
|
819
|
+
return {
|
|
820
|
+
path: `${options.baseModelPath}/${options.baseModelClassName}.php`,
|
|
821
|
+
content,
|
|
822
|
+
type: "base-model",
|
|
823
|
+
overwrite: true,
|
|
824
|
+
schemaName: "__base__"
|
|
825
|
+
};
|
|
826
|
+
}
|
|
827
|
+
function generateEntityBaseModel(schema, schemas, options, stubContent, authStubContent) {
|
|
828
|
+
const className = toPascalCase(schema.name);
|
|
829
|
+
const tableName = schema.options?.tableName ?? pluralize(toSnakeCase(schema.name));
|
|
830
|
+
const isAuth = schema.options?.authenticatable ?? false;
|
|
831
|
+
const primaryKey = "id";
|
|
832
|
+
const idType = schema.options?.idType ?? "BigInt";
|
|
833
|
+
const isUuid = idType === "Uuid";
|
|
834
|
+
const isStringKey = idType === "Uuid" || idType === "String";
|
|
835
|
+
const imports = [];
|
|
836
|
+
const traits = [];
|
|
837
|
+
const fillable = [];
|
|
838
|
+
const hidden = [];
|
|
839
|
+
const appends = [];
|
|
840
|
+
const casts = [];
|
|
841
|
+
const relations = [];
|
|
842
|
+
const docProperties = [];
|
|
843
|
+
if (schema.options?.softDelete) {
|
|
844
|
+
imports.push("use Illuminate\\Database\\Eloquent\\SoftDeletes;");
|
|
845
|
+
traits.push(" use SoftDeletes;");
|
|
846
|
+
}
|
|
847
|
+
const properties = schema.properties ?? {};
|
|
848
|
+
for (const [propName, propDef] of Object.entries(properties)) {
|
|
849
|
+
const snakeName = toSnakeCase(propName);
|
|
850
|
+
const phpType = getPhpDocType(propDef, schemas);
|
|
851
|
+
docProperties.push(` * @property ${phpType} $${snakeName}`);
|
|
852
|
+
if (propDef.type === "Association") {
|
|
853
|
+
const assoc = propDef;
|
|
854
|
+
if (assoc.target) {
|
|
855
|
+
imports.push(`use ${options.modelNamespace}\\${toPascalCase(assoc.target)};`);
|
|
856
|
+
}
|
|
857
|
+
relations.push(generateRelation(propName, assoc, schema, schemas, options));
|
|
858
|
+
if (assoc.relation === "ManyToOne" || assoc.relation === "OneToOne") {
|
|
859
|
+
if (!assoc.mappedBy) {
|
|
860
|
+
const fkName = toSnakeCase(propName) + "_id";
|
|
861
|
+
fillable.push(` '${fkName}',`);
|
|
862
|
+
docProperties.push(` * @property int|null $${fkName}`);
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
} else if (propDef.type === "Password") {
|
|
866
|
+
fillable.push(` '${snakeName}',`);
|
|
867
|
+
hidden.push(` '${snakeName}',`);
|
|
868
|
+
const cast = getCastType(propDef);
|
|
869
|
+
if (cast) {
|
|
870
|
+
casts.push(` '${snakeName}' => '${cast}',`);
|
|
871
|
+
}
|
|
872
|
+
} else if (propDef.type === "File") {
|
|
873
|
+
const relMethod = generateFileRelation(propName, propDef);
|
|
874
|
+
relations.push(relMethod);
|
|
875
|
+
} else {
|
|
876
|
+
fillable.push(` '${snakeName}',`);
|
|
877
|
+
const cast = getCastType(propDef);
|
|
878
|
+
if (cast) {
|
|
879
|
+
casts.push(` '${snakeName}' => '${cast}',`);
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
const docComment = `/**
|
|
884
|
+
* ${className}BaseModel
|
|
885
|
+
*
|
|
886
|
+
${docProperties.join("\n")}
|
|
887
|
+
*/`;
|
|
888
|
+
const stub = isAuth ? authStubContent : stubContent;
|
|
889
|
+
const keyType = isStringKey ? ` /**
|
|
890
|
+
* The "type" of the primary key ID.
|
|
891
|
+
*/
|
|
892
|
+
protected $keyType = 'string';
|
|
893
|
+
|
|
894
|
+
` : "";
|
|
895
|
+
const incrementing = isUuid ? ` /**
|
|
896
|
+
* Indicates if the IDs are auto-incrementing.
|
|
897
|
+
*/
|
|
898
|
+
public $incrementing = false;
|
|
899
|
+
|
|
900
|
+
` : "";
|
|
901
|
+
if (isUuid) {
|
|
902
|
+
imports.push("use Illuminate\\Database\\Eloquent\\Concerns\\HasUuids;");
|
|
903
|
+
traits.push(" use HasUuids;");
|
|
904
|
+
}
|
|
905
|
+
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"));
|
|
906
|
+
return {
|
|
907
|
+
path: `${options.baseModelPath}/${className}BaseModel.php`,
|
|
908
|
+
content,
|
|
909
|
+
type: "entity-base",
|
|
910
|
+
overwrite: true,
|
|
911
|
+
schemaName: schema.name
|
|
912
|
+
};
|
|
913
|
+
}
|
|
914
|
+
function findInverseRelation(currentSchemaName, targetSchemaName, schemas) {
|
|
915
|
+
const targetSchema = schemas[targetSchemaName];
|
|
916
|
+
if (!targetSchema || !targetSchema.properties) {
|
|
917
|
+
return null;
|
|
918
|
+
}
|
|
919
|
+
for (const [propName, propDef] of Object.entries(targetSchema.properties)) {
|
|
920
|
+
if (propDef.type === "Association") {
|
|
921
|
+
const assoc = propDef;
|
|
922
|
+
if (assoc.relation === "ManyToOne" && assoc.target === currentSchemaName) {
|
|
923
|
+
return propName;
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
return null;
|
|
928
|
+
}
|
|
929
|
+
function generateRelation(propName, assoc, schema, schemas, options) {
|
|
930
|
+
const methodName = toCamelCase(propName);
|
|
931
|
+
const targetClass = assoc.target ? toPascalCase(assoc.target) : "";
|
|
932
|
+
const fkName = toSnakeCase(propName) + "_id";
|
|
933
|
+
switch (assoc.relation) {
|
|
934
|
+
case "ManyToOne":
|
|
935
|
+
return ` /**
|
|
936
|
+
* Get the ${propName} that owns this model.
|
|
937
|
+
*/
|
|
938
|
+
public function ${methodName}(): BelongsTo
|
|
939
|
+
{
|
|
940
|
+
return $this->belongsTo(${targetClass}::class, '${fkName}');
|
|
941
|
+
}`;
|
|
942
|
+
case "OneToOne":
|
|
943
|
+
if (assoc.mappedBy) {
|
|
944
|
+
return ` /**
|
|
945
|
+
* Get the ${propName} for this model.
|
|
946
|
+
*/
|
|
947
|
+
public function ${methodName}(): HasOne
|
|
948
|
+
{
|
|
949
|
+
return $this->hasOne(${targetClass}::class, '${toSnakeCase(assoc.mappedBy)}_id');
|
|
950
|
+
}`;
|
|
951
|
+
}
|
|
952
|
+
return ` /**
|
|
953
|
+
* Get the ${propName} that owns this model.
|
|
954
|
+
*/
|
|
955
|
+
public function ${methodName}(): BelongsTo
|
|
956
|
+
{
|
|
957
|
+
return $this->belongsTo(${targetClass}::class, '${fkName}');
|
|
958
|
+
}`;
|
|
959
|
+
case "OneToMany": {
|
|
960
|
+
let foreignKey;
|
|
961
|
+
if (assoc.inversedBy) {
|
|
962
|
+
foreignKey = toSnakeCase(assoc.inversedBy) + "_id";
|
|
963
|
+
} else if (assoc.target) {
|
|
964
|
+
const inverseRelation = findInverseRelation(schema.name, assoc.target, schemas);
|
|
965
|
+
if (inverseRelation) {
|
|
966
|
+
foreignKey = toSnakeCase(inverseRelation) + "_id";
|
|
967
|
+
} else {
|
|
968
|
+
foreignKey = toSnakeCase(schema.name) + "_id";
|
|
969
|
+
}
|
|
970
|
+
} else {
|
|
971
|
+
foreignKey = toSnakeCase(propName) + "_id";
|
|
972
|
+
}
|
|
973
|
+
return ` /**
|
|
974
|
+
* Get the ${propName} for this model.
|
|
975
|
+
*/
|
|
976
|
+
public function ${methodName}(): HasMany
|
|
977
|
+
{
|
|
978
|
+
return $this->hasMany(${targetClass}::class, '${foreignKey}');
|
|
979
|
+
}`;
|
|
980
|
+
}
|
|
981
|
+
case "ManyToMany": {
|
|
982
|
+
const pivotTable = assoc.joinTable ?? `${toSnakeCase(propName)}_pivot`;
|
|
983
|
+
return ` /**
|
|
984
|
+
* The ${propName} that belong to this model.
|
|
985
|
+
*/
|
|
986
|
+
public function ${methodName}(): BelongsToMany
|
|
987
|
+
{
|
|
988
|
+
return $this->belongsToMany(${targetClass}::class, '${pivotTable}')
|
|
989
|
+
->withTimestamps();
|
|
990
|
+
}`;
|
|
991
|
+
}
|
|
992
|
+
case "MorphTo":
|
|
993
|
+
return ` /**
|
|
994
|
+
* Get the parent ${propName} model.
|
|
995
|
+
*/
|
|
996
|
+
public function ${methodName}(): MorphTo
|
|
997
|
+
{
|
|
998
|
+
return $this->morphTo('${methodName}');
|
|
999
|
+
}`;
|
|
1000
|
+
case "MorphOne":
|
|
1001
|
+
return ` /**
|
|
1002
|
+
* Get the ${propName} for this model.
|
|
1003
|
+
*/
|
|
1004
|
+
public function ${methodName}(): MorphOne
|
|
1005
|
+
{
|
|
1006
|
+
return $this->morphOne(${targetClass}::class, '${assoc.morphName ?? propName}');
|
|
1007
|
+
}`;
|
|
1008
|
+
case "MorphMany":
|
|
1009
|
+
return ` /**
|
|
1010
|
+
* Get the ${propName} for this model.
|
|
1011
|
+
*/
|
|
1012
|
+
public function ${methodName}(): MorphMany
|
|
1013
|
+
{
|
|
1014
|
+
return $this->morphMany(${targetClass}::class, '${assoc.morphName ?? propName}');
|
|
1015
|
+
}`;
|
|
1016
|
+
default:
|
|
1017
|
+
return ` // TODO: Implement ${assoc.relation} relation for ${propName}`;
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
function generateFileRelation(propName, propDef) {
|
|
1021
|
+
const methodName = toCamelCase(propName);
|
|
1022
|
+
const relationType = propDef.multiple ? "MorphMany" : "MorphOne";
|
|
1023
|
+
const relationMethod = propDef.multiple ? "morphMany" : "morphOne";
|
|
1024
|
+
return ` /**
|
|
1025
|
+
* Get the ${propName} file(s) for this model.
|
|
1026
|
+
*/
|
|
1027
|
+
public function ${methodName}(): ${relationType}
|
|
1028
|
+
{
|
|
1029
|
+
return $this->${relationMethod}(FileUpload::class, 'uploadable')
|
|
1030
|
+
->where('attribute_name', '${propName}');
|
|
1031
|
+
}`;
|
|
1032
|
+
}
|
|
1033
|
+
function generateEntityModel(schema, options, stubContent) {
|
|
1034
|
+
const className = toPascalCase(schema.name);
|
|
1035
|
+
const content = stubContent.replace(/\{\{BASE_MODEL_NAMESPACE\}\}/g, options.baseModelNamespace).replace(/\{\{MODEL_NAMESPACE\}\}/g, options.modelNamespace).replace(/\{\{CLASS_NAME\}\}/g, className);
|
|
1036
|
+
return {
|
|
1037
|
+
path: `${options.modelPath}/${className}.php`,
|
|
1038
|
+
content,
|
|
1039
|
+
type: "entity",
|
|
1040
|
+
overwrite: false,
|
|
1041
|
+
// Never overwrite user models
|
|
1042
|
+
schemaName: schema.name
|
|
1043
|
+
};
|
|
1044
|
+
}
|
|
1045
|
+
function getStubContent(stubName) {
|
|
1046
|
+
const stubs = {
|
|
1047
|
+
"base-model": `<?php
|
|
1048
|
+
|
|
1049
|
+
namespace {{BASE_MODEL_NAMESPACE}};
|
|
1050
|
+
|
|
1051
|
+
/**
|
|
1052
|
+
* Base model class for all Omnify-generated models.
|
|
1053
|
+
* Contains model mapping for polymorphic relations.
|
|
1054
|
+
*
|
|
1055
|
+
* DO NOT EDIT - This file is auto-generated by Omnify.
|
|
1056
|
+
* Any changes will be overwritten on next generation.
|
|
1057
|
+
*
|
|
1058
|
+
* @generated by @famgia/omnify-laravel
|
|
1059
|
+
*/
|
|
1060
|
+
|
|
1061
|
+
use Illuminate\\Database\\Eloquent\\Model;
|
|
1062
|
+
use Illuminate\\Database\\Eloquent\\Relations\\Relation;
|
|
1063
|
+
|
|
1064
|
+
abstract class {{BASE_MODEL_CLASS}} extends Model
|
|
1065
|
+
{
|
|
1066
|
+
/**
|
|
1067
|
+
* Model class map for polymorphic relations.
|
|
1068
|
+
*/
|
|
1069
|
+
protected static array $modelMap = [
|
|
1070
|
+
{{MODEL_MAP}}
|
|
1071
|
+
];
|
|
1072
|
+
|
|
1073
|
+
/**
|
|
1074
|
+
* Boot the model and register morph map.
|
|
1075
|
+
*/
|
|
1076
|
+
protected static function boot(): void
|
|
1077
|
+
{
|
|
1078
|
+
parent::boot();
|
|
1079
|
+
|
|
1080
|
+
// Register morph map for polymorphic relations
|
|
1081
|
+
Relation::enforceMorphMap(static::$modelMap);
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
/**
|
|
1085
|
+
* Get the model class for a given morph type.
|
|
1086
|
+
*/
|
|
1087
|
+
public static function getModelClass(string $morphType): ?string
|
|
1088
|
+
{
|
|
1089
|
+
return static::$modelMap[$morphType] ?? null;
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
`,
|
|
1093
|
+
"entity-base": `<?php
|
|
1094
|
+
|
|
1095
|
+
namespace {{BASE_MODEL_NAMESPACE}};
|
|
1096
|
+
|
|
1097
|
+
/**
|
|
1098
|
+
* DO NOT EDIT - This file is auto-generated by Omnify.
|
|
1099
|
+
* Any changes will be overwritten on next generation.
|
|
1100
|
+
*
|
|
1101
|
+
* @generated by @famgia/omnify-laravel
|
|
1102
|
+
*/
|
|
1103
|
+
|
|
1104
|
+
use Illuminate\\Database\\Eloquent\\Relations\\BelongsTo;
|
|
1105
|
+
use Illuminate\\Database\\Eloquent\\Relations\\HasMany;
|
|
1106
|
+
use Illuminate\\Database\\Eloquent\\Relations\\HasOne;
|
|
1107
|
+
use Illuminate\\Database\\Eloquent\\Relations\\BelongsToMany;
|
|
1108
|
+
use Illuminate\\Database\\Eloquent\\Relations\\MorphTo;
|
|
1109
|
+
use Illuminate\\Database\\Eloquent\\Relations\\MorphOne;
|
|
1110
|
+
use Illuminate\\Database\\Eloquent\\Relations\\MorphMany;
|
|
1111
|
+
use Illuminate\\Database\\Eloquent\\Relations\\MorphToMany;
|
|
1112
|
+
use Illuminate\\Database\\Eloquent\\Collection as EloquentCollection;
|
|
1113
|
+
{{IMPORTS}}
|
|
1114
|
+
|
|
1115
|
+
{{DOC_COMMENT}}
|
|
1116
|
+
class {{CLASS_NAME}}BaseModel extends {{BASE_MODEL_CLASS}}
|
|
1117
|
+
{
|
|
1118
|
+
{{TRAITS}}
|
|
1119
|
+
/**
|
|
1120
|
+
* The table associated with the model.
|
|
1121
|
+
*/
|
|
1122
|
+
protected $table = '{{TABLE_NAME}}';
|
|
1123
|
+
|
|
1124
|
+
/**
|
|
1125
|
+
* The primary key for the model.
|
|
1126
|
+
*/
|
|
1127
|
+
protected $primaryKey = '{{PRIMARY_KEY}}';
|
|
1128
|
+
|
|
1129
|
+
{{KEY_TYPE}}
|
|
1130
|
+
{{INCREMENTING}}
|
|
1131
|
+
/**
|
|
1132
|
+
* Indicates if the model should be timestamped.
|
|
1133
|
+
*/
|
|
1134
|
+
public $timestamps = {{TIMESTAMPS}};
|
|
1135
|
+
|
|
1136
|
+
/**
|
|
1137
|
+
* The attributes that are mass assignable.
|
|
1138
|
+
*/
|
|
1139
|
+
protected $fillable = [
|
|
1140
|
+
{{FILLABLE}}
|
|
1141
|
+
];
|
|
1142
|
+
|
|
1143
|
+
/**
|
|
1144
|
+
* The attributes that should be hidden for serialization.
|
|
1145
|
+
*/
|
|
1146
|
+
protected $hidden = [
|
|
1147
|
+
{{HIDDEN}}
|
|
1148
|
+
];
|
|
1149
|
+
|
|
1150
|
+
/**
|
|
1151
|
+
* The accessors to append to the model's array form.
|
|
1152
|
+
*/
|
|
1153
|
+
protected $appends = [
|
|
1154
|
+
{{APPENDS}}
|
|
1155
|
+
];
|
|
1156
|
+
|
|
1157
|
+
/**
|
|
1158
|
+
* Get the attributes that should be cast.
|
|
1159
|
+
*/
|
|
1160
|
+
protected function casts(): array
|
|
1161
|
+
{
|
|
1162
|
+
return [
|
|
1163
|
+
{{CASTS}}
|
|
1164
|
+
];
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
{{RELATIONS}}
|
|
1168
|
+
}
|
|
1169
|
+
`,
|
|
1170
|
+
"entity-base-auth": `<?php
|
|
1171
|
+
|
|
1172
|
+
namespace {{BASE_MODEL_NAMESPACE}};
|
|
1173
|
+
|
|
1174
|
+
/**
|
|
1175
|
+
* DO NOT EDIT - This file is auto-generated by Omnify.
|
|
1176
|
+
* Any changes will be overwritten on next generation.
|
|
1177
|
+
*
|
|
1178
|
+
* @generated by @famgia/omnify-laravel
|
|
1179
|
+
*/
|
|
1180
|
+
|
|
1181
|
+
use Illuminate\\Foundation\\Auth\\User as Authenticatable;
|
|
1182
|
+
use Illuminate\\Database\\Eloquent\\Relations\\BelongsTo;
|
|
1183
|
+
use Illuminate\\Database\\Eloquent\\Relations\\HasMany;
|
|
1184
|
+
use Illuminate\\Database\\Eloquent\\Relations\\HasOne;
|
|
1185
|
+
use Illuminate\\Database\\Eloquent\\Relations\\BelongsToMany;
|
|
1186
|
+
use Illuminate\\Database\\Eloquent\\Relations\\MorphTo;
|
|
1187
|
+
use Illuminate\\Database\\Eloquent\\Relations\\MorphOne;
|
|
1188
|
+
use Illuminate\\Database\\Eloquent\\Relations\\MorphMany;
|
|
1189
|
+
use Illuminate\\Database\\Eloquent\\Relations\\MorphToMany;
|
|
1190
|
+
use Illuminate\\Database\\Eloquent\\Collection as EloquentCollection;
|
|
1191
|
+
use Illuminate\\Notifications\\Notifiable;
|
|
1192
|
+
{{IMPORTS}}
|
|
1193
|
+
|
|
1194
|
+
{{DOC_COMMENT}}
|
|
1195
|
+
class {{CLASS_NAME}}BaseModel extends Authenticatable
|
|
1196
|
+
{
|
|
1197
|
+
use Notifiable;
|
|
1198
|
+
{{TRAITS}}
|
|
1199
|
+
/**
|
|
1200
|
+
* The table associated with the model.
|
|
1201
|
+
*/
|
|
1202
|
+
protected $table = '{{TABLE_NAME}}';
|
|
1203
|
+
|
|
1204
|
+
/**
|
|
1205
|
+
* The primary key for the model.
|
|
1206
|
+
*/
|
|
1207
|
+
protected $primaryKey = '{{PRIMARY_KEY}}';
|
|
1208
|
+
|
|
1209
|
+
{{KEY_TYPE}}
|
|
1210
|
+
{{INCREMENTING}}
|
|
1211
|
+
/**
|
|
1212
|
+
* Indicates if the model should be timestamped.
|
|
1213
|
+
*/
|
|
1214
|
+
public $timestamps = {{TIMESTAMPS}};
|
|
1215
|
+
|
|
1216
|
+
/**
|
|
1217
|
+
* The attributes that are mass assignable.
|
|
1218
|
+
*/
|
|
1219
|
+
protected $fillable = [
|
|
1220
|
+
{{FILLABLE}}
|
|
1221
|
+
];
|
|
1222
|
+
|
|
1223
|
+
/**
|
|
1224
|
+
* The attributes that should be hidden for serialization.
|
|
1225
|
+
*/
|
|
1226
|
+
protected $hidden = [
|
|
1227
|
+
{{HIDDEN}}
|
|
1228
|
+
];
|
|
1229
|
+
|
|
1230
|
+
/**
|
|
1231
|
+
* The accessors to append to the model's array form.
|
|
1232
|
+
*/
|
|
1233
|
+
protected $appends = [
|
|
1234
|
+
{{APPENDS}}
|
|
1235
|
+
];
|
|
1236
|
+
|
|
1237
|
+
/**
|
|
1238
|
+
* Get the attributes that should be cast.
|
|
1239
|
+
*/
|
|
1240
|
+
protected function casts(): array
|
|
1241
|
+
{
|
|
1242
|
+
return [
|
|
1243
|
+
{{CASTS}}
|
|
1244
|
+
];
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
{{RELATIONS}}
|
|
1248
|
+
}
|
|
1249
|
+
`,
|
|
1250
|
+
"entity": `<?php
|
|
1251
|
+
|
|
1252
|
+
namespace {{MODEL_NAMESPACE}};
|
|
1253
|
+
|
|
1254
|
+
use {{BASE_MODEL_NAMESPACE}}\\{{CLASS_NAME}}BaseModel;
|
|
1255
|
+
use Illuminate\\Database\\Eloquent\\Factories\\HasFactory;
|
|
1256
|
+
|
|
1257
|
+
/**
|
|
1258
|
+
* {{CLASS_NAME}} Model
|
|
1259
|
+
*
|
|
1260
|
+
* This file is generated once and can be customized.
|
|
1261
|
+
* Add your custom methods and logic here.
|
|
1262
|
+
*/
|
|
1263
|
+
class {{CLASS_NAME}} extends {{CLASS_NAME}}BaseModel
|
|
1264
|
+
{
|
|
1265
|
+
use HasFactory;
|
|
1266
|
+
|
|
1267
|
+
/**
|
|
1268
|
+
* Create a new model instance.
|
|
1269
|
+
*/
|
|
1270
|
+
public function __construct(array $attributes = [])
|
|
1271
|
+
{
|
|
1272
|
+
parent::__construct($attributes);
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
// Add your custom methods here
|
|
1276
|
+
}
|
|
1277
|
+
`,
|
|
1278
|
+
"service-provider": `<?php
|
|
1279
|
+
|
|
1280
|
+
namespace App\\Providers;
|
|
1281
|
+
|
|
1282
|
+
use Illuminate\\Database\\Eloquent\\Relations\\Relation;
|
|
1283
|
+
use Illuminate\\Support\\ServiceProvider;
|
|
1284
|
+
|
|
1285
|
+
/**
|
|
1286
|
+
* Omnify Service Provider
|
|
1287
|
+
*
|
|
1288
|
+
* DO NOT EDIT - This file is auto-generated by Omnify.
|
|
1289
|
+
* Any changes will be overwritten on next generation.
|
|
1290
|
+
*
|
|
1291
|
+
* - Loads Omnify migrations from database/migrations/omnify
|
|
1292
|
+
* - Registers morph map for polymorphic relationships
|
|
1293
|
+
*
|
|
1294
|
+
* @generated by @famgia/omnify-laravel
|
|
1295
|
+
*/
|
|
1296
|
+
class OmnifyServiceProvider extends ServiceProvider
|
|
1297
|
+
{
|
|
1298
|
+
/**
|
|
1299
|
+
* Register any application services.
|
|
1300
|
+
*/
|
|
1301
|
+
public function register(): void
|
|
1302
|
+
{
|
|
1303
|
+
//
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
/**
|
|
1307
|
+
* Bootstrap any application services.
|
|
1308
|
+
*/
|
|
1309
|
+
public function boot(): void
|
|
1310
|
+
{
|
|
1311
|
+
// Load Omnify migrations from custom directory
|
|
1312
|
+
$this->loadMigrationsFrom(database_path('migrations/omnify'));
|
|
1313
|
+
|
|
1314
|
+
// Register morph map for polymorphic relationships
|
|
1315
|
+
Relation::enforceMorphMap([
|
|
1316
|
+
{{MORPH_MAP}}
|
|
1317
|
+
]);
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
`
|
|
1321
|
+
};
|
|
1322
|
+
return stubs[stubName] ?? "";
|
|
1323
|
+
}
|
|
1324
|
+
function generateServiceProvider(schemas, options, stubContent) {
|
|
1325
|
+
const morphMap = Object.values(schemas).filter((s) => s.kind !== "enum").map((s) => {
|
|
1326
|
+
const className = toPascalCase(s.name);
|
|
1327
|
+
return ` '${s.name}' => \\${options.modelNamespace}\\${className}::class,`;
|
|
1328
|
+
}).join("\n");
|
|
1329
|
+
const content = stubContent.replace(/\{\{MORPH_MAP\}\}/g, morphMap);
|
|
1330
|
+
return {
|
|
1331
|
+
path: "app/Providers/OmnifyServiceProvider.php",
|
|
1332
|
+
content,
|
|
1333
|
+
type: "service-provider",
|
|
1334
|
+
overwrite: true,
|
|
1335
|
+
// Always overwrite to keep morph map in sync
|
|
1336
|
+
schemaName: "__service_provider__"
|
|
1337
|
+
};
|
|
1338
|
+
}
|
|
1339
|
+
function generateModels(schemas, options) {
|
|
1340
|
+
const resolved = resolveOptions(options);
|
|
1341
|
+
const models = [];
|
|
1342
|
+
models.push(generateBaseModel(schemas, resolved, getStubContent("base-model")));
|
|
1343
|
+
models.push(generateServiceProvider(schemas, resolved, getStubContent("service-provider")));
|
|
1344
|
+
for (const schema of Object.values(schemas)) {
|
|
1345
|
+
if (schema.kind === "enum") {
|
|
1346
|
+
continue;
|
|
1347
|
+
}
|
|
1348
|
+
models.push(generateEntityBaseModel(
|
|
1349
|
+
schema,
|
|
1350
|
+
schemas,
|
|
1351
|
+
resolved,
|
|
1352
|
+
getStubContent("entity-base"),
|
|
1353
|
+
getStubContent("entity-base-auth")
|
|
1354
|
+
));
|
|
1355
|
+
models.push(generateEntityModel(schema, resolved, getStubContent("entity")));
|
|
1356
|
+
}
|
|
1357
|
+
return models;
|
|
1358
|
+
}
|
|
1359
|
+
function getModelPath(model) {
|
|
1360
|
+
return model.path;
|
|
1361
|
+
}
|
|
1362
|
+
function generateProviderRegistration(existingContent, laravelVersion) {
|
|
1363
|
+
const providerClass = "App\\Providers\\OmnifyServiceProvider::class";
|
|
1364
|
+
const providerLine = ` ${providerClass},`;
|
|
1365
|
+
if (existingContent && existingContent.includes("OmnifyServiceProvider")) {
|
|
1366
|
+
return {
|
|
1367
|
+
path: laravelVersion === "laravel11+" ? "bootstrap/providers.php" : "config/app.php",
|
|
1368
|
+
content: existingContent,
|
|
1369
|
+
laravelVersion,
|
|
1370
|
+
alreadyRegistered: true
|
|
1371
|
+
};
|
|
1372
|
+
}
|
|
1373
|
+
if (laravelVersion === "laravel11+") {
|
|
1374
|
+
if (existingContent) {
|
|
1375
|
+
const lines = existingContent.split("\n");
|
|
1376
|
+
const result = [];
|
|
1377
|
+
let inserted = false;
|
|
1378
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1379
|
+
const line = lines[i];
|
|
1380
|
+
if (!inserted && line.trim() === "];") {
|
|
1381
|
+
result.push(providerLine);
|
|
1382
|
+
inserted = true;
|
|
1383
|
+
}
|
|
1384
|
+
result.push(line);
|
|
1385
|
+
}
|
|
1386
|
+
return {
|
|
1387
|
+
path: "bootstrap/providers.php",
|
|
1388
|
+
content: result.join("\n"),
|
|
1389
|
+
laravelVersion,
|
|
1390
|
+
alreadyRegistered: false
|
|
1391
|
+
};
|
|
1392
|
+
} else {
|
|
1393
|
+
return {
|
|
1394
|
+
path: "bootstrap/providers.php",
|
|
1395
|
+
content: `<?php
|
|
1396
|
+
|
|
1397
|
+
return [
|
|
1398
|
+
App\\Providers\\AppServiceProvider::class,
|
|
1399
|
+
${providerLine}
|
|
1400
|
+
];
|
|
1401
|
+
`,
|
|
1402
|
+
laravelVersion,
|
|
1403
|
+
alreadyRegistered: false
|
|
1404
|
+
};
|
|
1405
|
+
}
|
|
1406
|
+
} else {
|
|
1407
|
+
if (existingContent) {
|
|
1408
|
+
const providersSectionRegex = /'providers'\s*=>\s*\[[\s\S]*?\n(\s*)\]/m;
|
|
1409
|
+
const match = existingContent.match(providersSectionRegex);
|
|
1410
|
+
if (match) {
|
|
1411
|
+
const providersStart = existingContent.indexOf("'providers'");
|
|
1412
|
+
if (providersStart === -1) {
|
|
1413
|
+
return null;
|
|
1414
|
+
}
|
|
1415
|
+
let depth = 0;
|
|
1416
|
+
let foundStart = false;
|
|
1417
|
+
let insertPos = -1;
|
|
1418
|
+
for (let i = providersStart; i < existingContent.length; i++) {
|
|
1419
|
+
const char = existingContent[i];
|
|
1420
|
+
if (char === "[") {
|
|
1421
|
+
foundStart = true;
|
|
1422
|
+
depth++;
|
|
1423
|
+
} else if (char === "]") {
|
|
1424
|
+
depth--;
|
|
1425
|
+
if (foundStart && depth === 0) {
|
|
1426
|
+
insertPos = i;
|
|
1427
|
+
break;
|
|
1428
|
+
}
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
if (insertPos !== -1) {
|
|
1432
|
+
const beforeClose = existingContent.substring(0, insertPos);
|
|
1433
|
+
const lastNewline = beforeClose.lastIndexOf("\n");
|
|
1434
|
+
const content = existingContent.substring(0, lastNewline + 1) + providerLine + "\n" + existingContent.substring(lastNewline + 1);
|
|
1435
|
+
return {
|
|
1436
|
+
path: "config/app.php",
|
|
1437
|
+
content,
|
|
1438
|
+
laravelVersion,
|
|
1439
|
+
alreadyRegistered: false
|
|
1440
|
+
};
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
return null;
|
|
1444
|
+
} else {
|
|
1445
|
+
return null;
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
// src/factory/generator.ts
|
|
1451
|
+
function resolveOptions2(options) {
|
|
1452
|
+
return {
|
|
1453
|
+
modelNamespace: options?.modelNamespace ?? "App\\Models",
|
|
1454
|
+
factoryPath: options?.factoryPath ?? "database/factories",
|
|
1455
|
+
fakerLocale: options?.fakerLocale ?? "en_US"
|
|
1456
|
+
};
|
|
1457
|
+
}
|
|
1458
|
+
function getStubContent2() {
|
|
1459
|
+
return `<?php
|
|
1460
|
+
|
|
1461
|
+
namespace Database\\Factories;
|
|
1462
|
+
|
|
1463
|
+
use {{MODEL_NAMESPACE}}\\{{MODEL_NAME}};
|
|
1464
|
+
use Illuminate\\Database\\Eloquent\\Factories\\Factory;
|
|
1465
|
+
{{IMPORTS}}
|
|
1466
|
+
|
|
1467
|
+
/**
|
|
1468
|
+
* @extends Factory<{{MODEL_NAME}}>
|
|
1469
|
+
*/
|
|
1470
|
+
class {{MODEL_NAME}}Factory extends Factory
|
|
1471
|
+
{
|
|
1472
|
+
protected $model = {{MODEL_NAME}}::class;
|
|
1473
|
+
|
|
1474
|
+
/**
|
|
1475
|
+
* Define the model's default state.
|
|
1476
|
+
*
|
|
1477
|
+
* @return array<string, mixed>
|
|
1478
|
+
*/
|
|
1479
|
+
public function definition(): array
|
|
1480
|
+
{
|
|
1481
|
+
return [
|
|
1482
|
+
{{ATTRIBUTES}}
|
|
1483
|
+
];
|
|
1484
|
+
}
|
|
1485
|
+
}
|
|
1486
|
+
`;
|
|
1487
|
+
}
|
|
1488
|
+
function generateFakeData(propertyName, property, schema, schemas) {
|
|
1489
|
+
const type = property.type;
|
|
1490
|
+
if (["deleted_at", "created_at", "updated_at"].includes(propertyName)) {
|
|
1491
|
+
return null;
|
|
1492
|
+
}
|
|
1493
|
+
if (type === "Association") {
|
|
1494
|
+
return null;
|
|
1495
|
+
}
|
|
1496
|
+
switch (type) {
|
|
1497
|
+
case "String":
|
|
1498
|
+
return generateStringFake(propertyName, property);
|
|
1499
|
+
case "Email":
|
|
1500
|
+
return `'${propertyName}' => fake()->unique()->safeEmail(),`;
|
|
1501
|
+
case "Password":
|
|
1502
|
+
return `'${propertyName}' => bcrypt('password'),`;
|
|
1503
|
+
case "Int":
|
|
1504
|
+
case "BigInt":
|
|
1505
|
+
return generateIntFake(propertyName, property);
|
|
1506
|
+
case "Float":
|
|
1507
|
+
case "Decimal":
|
|
1508
|
+
return `'${propertyName}' => fake()->randomFloat(2, 1, 10000),`;
|
|
1509
|
+
case "Boolean":
|
|
1510
|
+
return `'${propertyName}' => fake()->boolean(),`;
|
|
1511
|
+
case "Text":
|
|
1512
|
+
return `'${propertyName}' => fake()->paragraphs(3, true),`;
|
|
1513
|
+
case "LongText":
|
|
1514
|
+
return `'${propertyName}' => fake()->paragraphs(5, true),`;
|
|
1515
|
+
case "Date":
|
|
1516
|
+
return `'${propertyName}' => fake()->date(),`;
|
|
1517
|
+
case "Time":
|
|
1518
|
+
return `'${propertyName}' => fake()->time(),`;
|
|
1519
|
+
case "Timestamp":
|
|
1520
|
+
case "DateTime":
|
|
1521
|
+
return `'${propertyName}' => fake()->dateTime(),`;
|
|
1522
|
+
case "Json":
|
|
1523
|
+
return `'${propertyName}' => [],`;
|
|
1524
|
+
case "Enum":
|
|
1525
|
+
return generateEnumFake(propertyName, property);
|
|
1526
|
+
case "EnumRef":
|
|
1527
|
+
return generateEnumRefFake(propertyName, property, schemas);
|
|
1528
|
+
default:
|
|
1529
|
+
return `'${propertyName}' => fake()->sentence(),`;
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1532
|
+
function generateStringFake(propertyName, property) {
|
|
1533
|
+
if (propertyName === "slug") {
|
|
1534
|
+
return `'${propertyName}' => fake()->unique()->slug(),`;
|
|
1535
|
+
}
|
|
1536
|
+
if (propertyName === "uuid" || propertyName === "uid") {
|
|
1537
|
+
return `'${propertyName}' => (string) \\Illuminate\\Support\\Str::uuid(),`;
|
|
1538
|
+
}
|
|
1539
|
+
if (propertyName.includes("email")) {
|
|
1540
|
+
return `'${propertyName}' => fake()->unique()->safeEmail(),`;
|
|
1541
|
+
}
|
|
1542
|
+
if (propertyName.includes("phone")) {
|
|
1543
|
+
return `'${propertyName}' => fake()->phoneNumber(),`;
|
|
1544
|
+
}
|
|
1545
|
+
if (propertyName.includes("image") || propertyName.includes("photo") || propertyName.includes("avatar")) {
|
|
1546
|
+
return `'${propertyName}' => fake()->imageUrl(),`;
|
|
1547
|
+
}
|
|
1548
|
+
if (propertyName.includes("url") || propertyName.includes("website")) {
|
|
1549
|
+
return `'${propertyName}' => fake()->url(),`;
|
|
1550
|
+
}
|
|
1551
|
+
if (propertyName.includes("path") || propertyName.includes("file")) {
|
|
1552
|
+
return `'${propertyName}' => 'uploads/' . fake()->uuid() . '.jpg',`;
|
|
1553
|
+
}
|
|
1554
|
+
if (propertyName === "name" || propertyName === "title") {
|
|
1555
|
+
return `'${propertyName}' => fake()->sentence(3),`;
|
|
1556
|
+
}
|
|
1557
|
+
if (propertyName.includes("name")) {
|
|
1558
|
+
return `'${propertyName}' => fake()->name(),`;
|
|
1559
|
+
}
|
|
1560
|
+
if (propertyName.includes("address")) {
|
|
1561
|
+
return `'${propertyName}' => fake()->address(),`;
|
|
1562
|
+
}
|
|
1563
|
+
if (propertyName.includes("city")) {
|
|
1564
|
+
return `'${propertyName}' => fake()->city(),`;
|
|
1565
|
+
}
|
|
1566
|
+
if (propertyName.includes("country")) {
|
|
1567
|
+
return `'${propertyName}' => fake()->country(),`;
|
|
1568
|
+
}
|
|
1569
|
+
if (propertyName.includes("zip") || propertyName.includes("postal")) {
|
|
1570
|
+
return `'${propertyName}' => fake()->postcode(),`;
|
|
1571
|
+
}
|
|
1572
|
+
if (propertyName.includes("color")) {
|
|
1573
|
+
return `'${propertyName}' => fake()->hexColor(),`;
|
|
1574
|
+
}
|
|
1575
|
+
if (propertyName.includes("token") || propertyName.includes("secret") || propertyName.includes("key")) {
|
|
1576
|
+
return `'${propertyName}' => \\Illuminate\\Support\\Str::random(32),`;
|
|
1577
|
+
}
|
|
1578
|
+
if (propertyName.includes("code")) {
|
|
1579
|
+
return `'${propertyName}' => fake()->unique()->regexify('[A-Z0-9]{8}'),`;
|
|
1580
|
+
}
|
|
1581
|
+
const length = property.length;
|
|
1582
|
+
if (length && length <= 50) {
|
|
1583
|
+
return `'${propertyName}' => fake()->words(3, true),`;
|
|
1584
|
+
}
|
|
1585
|
+
return `'${propertyName}' => fake()->sentence(),`;
|
|
1586
|
+
}
|
|
1587
|
+
function generateIntFake(propertyName, property) {
|
|
1588
|
+
if (propertyName.includes("count") || propertyName.includes("quantity")) {
|
|
1589
|
+
return `'${propertyName}' => fake()->numberBetween(0, 100),`;
|
|
1590
|
+
}
|
|
1591
|
+
if (propertyName.includes("price") || propertyName.includes("amount") || propertyName.includes("cost")) {
|
|
1592
|
+
return `'${propertyName}' => fake()->numberBetween(100, 10000),`;
|
|
1593
|
+
}
|
|
1594
|
+
if (propertyName.includes("order") || propertyName.includes("sort") || propertyName.includes("position")) {
|
|
1595
|
+
return `'${propertyName}' => fake()->numberBetween(1, 100),`;
|
|
1596
|
+
}
|
|
1597
|
+
if (propertyName.includes("age")) {
|
|
1598
|
+
return `'${propertyName}' => fake()->numberBetween(18, 80),`;
|
|
1599
|
+
}
|
|
1600
|
+
if (propertyName.includes("year")) {
|
|
1601
|
+
return `'${propertyName}' => fake()->year(),`;
|
|
1602
|
+
}
|
|
1603
|
+
return `'${propertyName}' => fake()->numberBetween(1, 1000),`;
|
|
1604
|
+
}
|
|
1605
|
+
function generateEnumFake(propertyName, property) {
|
|
1606
|
+
const enumValues = property.enum;
|
|
1607
|
+
if (!enumValues || enumValues.length === 0) {
|
|
1608
|
+
return `'${propertyName}' => null,`;
|
|
1609
|
+
}
|
|
1610
|
+
const values = enumValues.map((v) => typeof v === "string" ? v : v.value);
|
|
1611
|
+
const valuesStr = values.map((v) => `'${v}'`).join(", ");
|
|
1612
|
+
return `'${propertyName}' => fake()->randomElement([${valuesStr}]),`;
|
|
1613
|
+
}
|
|
1614
|
+
function generateEnumRefFake(propertyName, property, schemas) {
|
|
1615
|
+
const enumName = property.enum;
|
|
1616
|
+
if (!enumName) {
|
|
1617
|
+
return `'${propertyName}' => null,`;
|
|
1618
|
+
}
|
|
1619
|
+
const enumSchema = schemas[enumName];
|
|
1620
|
+
if (!enumSchema || enumSchema.kind !== "enum" || !enumSchema.values) {
|
|
1621
|
+
return `'${propertyName}' => null,`;
|
|
1622
|
+
}
|
|
1623
|
+
const valuesStr = enumSchema.values.map((v) => `'${v}'`).join(", ");
|
|
1624
|
+
return `'${propertyName}' => fake()->randomElement([${valuesStr}]),`;
|
|
1625
|
+
}
|
|
1626
|
+
function generateAssociationFake(propertyName, property, schema, schemas, modelNamespace) {
|
|
1627
|
+
if (property.type !== "Association") {
|
|
1628
|
+
return null;
|
|
1629
|
+
}
|
|
1630
|
+
const relation = property.relation;
|
|
1631
|
+
const target = property.target;
|
|
1632
|
+
if (relation !== "ManyToOne" || !target) {
|
|
1633
|
+
return null;
|
|
1634
|
+
}
|
|
1635
|
+
const foreignKey = `${toSnakeCase(propertyName)}_id`;
|
|
1636
|
+
const isNullable2 = property.nullable ?? false;
|
|
1637
|
+
const targetSchema = schemas[target];
|
|
1638
|
+
if (!targetSchema) {
|
|
1639
|
+
return null;
|
|
1640
|
+
}
|
|
1641
|
+
let fake;
|
|
1642
|
+
if (isNullable2) {
|
|
1643
|
+
fake = `'${foreignKey}' => ${target}::query()->inRandomOrder()->first()?->id,`;
|
|
1644
|
+
} else {
|
|
1645
|
+
fake = `'${foreignKey}' => ${target}::query()->inRandomOrder()->first()?->id ?? ${target}::factory()->create()->id,`;
|
|
1646
|
+
}
|
|
1647
|
+
let importStatement;
|
|
1648
|
+
if (target !== schema.name) {
|
|
1649
|
+
importStatement = `use ${modelNamespace}\\${target};`;
|
|
1650
|
+
}
|
|
1651
|
+
return { fake, import: importStatement };
|
|
1652
|
+
}
|
|
1653
|
+
function generateFactory(schema, schemas, options, stubContent) {
|
|
1654
|
+
if (schema.kind === "enum") {
|
|
1655
|
+
return null;
|
|
1656
|
+
}
|
|
1657
|
+
const modelName = toPascalCase(schema.name);
|
|
1658
|
+
const factoryName = `${modelName}Factory`;
|
|
1659
|
+
const attributes = [];
|
|
1660
|
+
const imports = [];
|
|
1661
|
+
if (schema.properties) {
|
|
1662
|
+
for (const [propName, prop] of Object.entries(schema.properties)) {
|
|
1663
|
+
if (prop.type === "Association") {
|
|
1664
|
+
const assocResult = generateAssociationFake(propName, prop, schema, schemas, options.modelNamespace);
|
|
1665
|
+
if (assocResult) {
|
|
1666
|
+
attributes.push(assocResult.fake);
|
|
1667
|
+
if (assocResult.import) {
|
|
1668
|
+
imports.push(assocResult.import);
|
|
1669
|
+
}
|
|
1670
|
+
}
|
|
1671
|
+
continue;
|
|
1672
|
+
}
|
|
1673
|
+
const fake = generateFakeData(propName, prop, schema, schemas);
|
|
1674
|
+
if (fake) {
|
|
1675
|
+
attributes.push(fake);
|
|
1676
|
+
}
|
|
1677
|
+
}
|
|
1678
|
+
}
|
|
1679
|
+
let content = stubContent;
|
|
1680
|
+
content = content.replace(/\{\{MODEL_NAMESPACE\}\}/g, options.modelNamespace);
|
|
1681
|
+
content = content.replace(/\{\{MODEL_NAME\}\}/g, modelName);
|
|
1682
|
+
const uniqueImports = [...new Set(imports)];
|
|
1683
|
+
const importsStr = uniqueImports.length > 0 ? "\n" + uniqueImports.join("\n") : "";
|
|
1684
|
+
content = content.replace(/\{\{IMPORTS\}\}/g, importsStr);
|
|
1685
|
+
const attributesStr = attributes.length > 0 ? attributes.map((a) => ` ${a}`).join("\n") : "";
|
|
1686
|
+
content = content.replace(/\{\{ATTRIBUTES\}\}/g, attributesStr);
|
|
1687
|
+
return {
|
|
1688
|
+
name: factoryName,
|
|
1689
|
+
schemaName: schema.name,
|
|
1690
|
+
path: `${options.factoryPath}/${factoryName}.php`,
|
|
1691
|
+
content,
|
|
1692
|
+
overwrite: false
|
|
1693
|
+
// Factories should not overwrite existing files
|
|
1694
|
+
};
|
|
1695
|
+
}
|
|
1696
|
+
function generateFactories(schemas, options) {
|
|
1697
|
+
const resolved = resolveOptions2(options);
|
|
1698
|
+
const stubContent = getStubContent2();
|
|
1699
|
+
const factories = [];
|
|
1700
|
+
for (const schema of Object.values(schemas)) {
|
|
1701
|
+
const factory = generateFactory(schema, schemas, resolved, stubContent);
|
|
1702
|
+
if (factory) {
|
|
1703
|
+
factories.push(factory);
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1706
|
+
return factories;
|
|
1707
|
+
}
|
|
1708
|
+
function getFactoryPath(factory) {
|
|
1709
|
+
return factory.path;
|
|
1710
|
+
}
|
|
1711
|
+
|
|
687
1712
|
// src/plugin.ts
|
|
688
1713
|
var LARAVEL_CONFIG_SCHEMA = {
|
|
689
1714
|
fields: [
|
|
@@ -691,10 +1716,50 @@ var LARAVEL_CONFIG_SCHEMA = {
|
|
|
691
1716
|
key: "migrationsPath",
|
|
692
1717
|
type: "path",
|
|
693
1718
|
label: "Migrations Path",
|
|
694
|
-
description: "Directory for Laravel migration files",
|
|
695
|
-
default: "database/migrations",
|
|
1719
|
+
description: "Directory for Laravel migration files (loaded via OmnifyServiceProvider)",
|
|
1720
|
+
default: "database/migrations/omnify",
|
|
1721
|
+
group: "output"
|
|
1722
|
+
},
|
|
1723
|
+
{
|
|
1724
|
+
key: "modelsPath",
|
|
1725
|
+
type: "path",
|
|
1726
|
+
label: "Models Path",
|
|
1727
|
+
description: "Directory for user-editable model files",
|
|
1728
|
+
default: "app/Models",
|
|
696
1729
|
group: "output"
|
|
697
1730
|
},
|
|
1731
|
+
{
|
|
1732
|
+
key: "baseModelsPath",
|
|
1733
|
+
type: "path",
|
|
1734
|
+
label: "Base Models Path",
|
|
1735
|
+
description: "Directory for auto-generated base model files",
|
|
1736
|
+
default: "app/Models/OmnifyBase",
|
|
1737
|
+
group: "output"
|
|
1738
|
+
},
|
|
1739
|
+
{
|
|
1740
|
+
key: "generateModels",
|
|
1741
|
+
type: "boolean",
|
|
1742
|
+
label: "Generate Models",
|
|
1743
|
+
description: "Generate Eloquent model classes",
|
|
1744
|
+
default: true,
|
|
1745
|
+
group: "options"
|
|
1746
|
+
},
|
|
1747
|
+
{
|
|
1748
|
+
key: "factoriesPath",
|
|
1749
|
+
type: "path",
|
|
1750
|
+
label: "Factories Path",
|
|
1751
|
+
description: "Directory for Laravel factory files",
|
|
1752
|
+
default: "database/factories",
|
|
1753
|
+
group: "output"
|
|
1754
|
+
},
|
|
1755
|
+
{
|
|
1756
|
+
key: "generateFactories",
|
|
1757
|
+
type: "boolean",
|
|
1758
|
+
label: "Generate Factories",
|
|
1759
|
+
description: "Generate Laravel factory classes for testing",
|
|
1760
|
+
default: true,
|
|
1761
|
+
group: "options"
|
|
1762
|
+
},
|
|
698
1763
|
{
|
|
699
1764
|
key: "connection",
|
|
700
1765
|
type: "string",
|
|
@@ -705,41 +1770,141 @@ var LARAVEL_CONFIG_SCHEMA = {
|
|
|
705
1770
|
}
|
|
706
1771
|
]
|
|
707
1772
|
};
|
|
708
|
-
function
|
|
1773
|
+
function resolveOptions3(options) {
|
|
709
1774
|
return {
|
|
710
|
-
migrationsPath: options?.migrationsPath ?? "database/migrations",
|
|
1775
|
+
migrationsPath: options?.migrationsPath ?? "database/migrations/omnify",
|
|
1776
|
+
modelsPath: options?.modelsPath ?? "app/Models",
|
|
1777
|
+
baseModelsPath: options?.baseModelsPath ?? "app/Models/OmnifyBase",
|
|
1778
|
+
modelNamespace: options?.modelNamespace ?? "App\\Models",
|
|
1779
|
+
baseModelNamespace: options?.baseModelNamespace ?? "App\\Models\\OmnifyBase",
|
|
1780
|
+
generateModels: options?.generateModels ?? true,
|
|
1781
|
+
factoriesPath: options?.factoriesPath ?? "database/factories",
|
|
1782
|
+
generateFactories: options?.generateFactories ?? true,
|
|
1783
|
+
fakerLocale: options?.fakerLocale ?? "en_US",
|
|
711
1784
|
connection: options?.connection,
|
|
712
1785
|
timestamp: options?.timestamp
|
|
713
1786
|
};
|
|
714
1787
|
}
|
|
715
1788
|
function laravelPlugin(options) {
|
|
716
|
-
const resolved =
|
|
1789
|
+
const resolved = resolveOptions3(options);
|
|
1790
|
+
const migrationGenerator = {
|
|
1791
|
+
name: "laravel-migrations",
|
|
1792
|
+
description: "Generate Laravel migration files",
|
|
1793
|
+
generate: async (ctx) => {
|
|
1794
|
+
const migrationOptions = {
|
|
1795
|
+
connection: resolved.connection,
|
|
1796
|
+
timestamp: resolved.timestamp
|
|
1797
|
+
};
|
|
1798
|
+
const migrations = generateMigrations(ctx.schemas, migrationOptions);
|
|
1799
|
+
return migrations.map((migration) => ({
|
|
1800
|
+
path: getMigrationPath(migration, resolved.migrationsPath),
|
|
1801
|
+
content: migration.content,
|
|
1802
|
+
type: "migration",
|
|
1803
|
+
metadata: {
|
|
1804
|
+
tableName: migration.tables[0],
|
|
1805
|
+
migrationType: migration.type
|
|
1806
|
+
}
|
|
1807
|
+
}));
|
|
1808
|
+
}
|
|
1809
|
+
};
|
|
1810
|
+
const modelGenerator = {
|
|
1811
|
+
name: "laravel-models",
|
|
1812
|
+
description: "Generate Eloquent model classes",
|
|
1813
|
+
generate: async (ctx) => {
|
|
1814
|
+
const modelOptions = {
|
|
1815
|
+
modelNamespace: resolved.modelNamespace,
|
|
1816
|
+
baseModelNamespace: resolved.baseModelNamespace,
|
|
1817
|
+
modelPath: resolved.modelsPath,
|
|
1818
|
+
baseModelPath: resolved.baseModelsPath
|
|
1819
|
+
};
|
|
1820
|
+
const models = generateModels(ctx.schemas, modelOptions);
|
|
1821
|
+
const outputs = models.map((model) => ({
|
|
1822
|
+
path: getModelPath(model),
|
|
1823
|
+
content: model.content,
|
|
1824
|
+
type: "model",
|
|
1825
|
+
// Skip writing user models if they already exist
|
|
1826
|
+
skipIfExists: !model.overwrite,
|
|
1827
|
+
metadata: {
|
|
1828
|
+
modelType: model.type,
|
|
1829
|
+
schemaName: model.schemaName
|
|
1830
|
+
}
|
|
1831
|
+
}));
|
|
1832
|
+
const bootstrapProvidersPath = (0, import_node_path.join)(ctx.cwd, "bootstrap/providers.php");
|
|
1833
|
+
const configAppPath = (0, import_node_path.join)(ctx.cwd, "config/app.php");
|
|
1834
|
+
let existingContent = null;
|
|
1835
|
+
let laravelVersion;
|
|
1836
|
+
if ((0, import_node_fs.existsSync)(bootstrapProvidersPath)) {
|
|
1837
|
+
laravelVersion = "laravel11+";
|
|
1838
|
+
try {
|
|
1839
|
+
existingContent = (0, import_node_fs.readFileSync)(bootstrapProvidersPath, "utf-8");
|
|
1840
|
+
} catch {
|
|
1841
|
+
existingContent = null;
|
|
1842
|
+
}
|
|
1843
|
+
} else if ((0, import_node_fs.existsSync)(configAppPath)) {
|
|
1844
|
+
laravelVersion = "laravel10-";
|
|
1845
|
+
try {
|
|
1846
|
+
existingContent = (0, import_node_fs.readFileSync)(configAppPath, "utf-8");
|
|
1847
|
+
} catch {
|
|
1848
|
+
existingContent = null;
|
|
1849
|
+
}
|
|
1850
|
+
} else {
|
|
1851
|
+
laravelVersion = "laravel11+";
|
|
1852
|
+
}
|
|
1853
|
+
const registration = generateProviderRegistration(existingContent, laravelVersion);
|
|
1854
|
+
if (registration && !registration.alreadyRegistered) {
|
|
1855
|
+
outputs.push({
|
|
1856
|
+
path: registration.path,
|
|
1857
|
+
content: registration.content,
|
|
1858
|
+
type: "other",
|
|
1859
|
+
skipIfExists: false,
|
|
1860
|
+
// We want to modify the file
|
|
1861
|
+
metadata: {
|
|
1862
|
+
registrationType: "provider-registration",
|
|
1863
|
+
laravelVersion: registration.laravelVersion
|
|
1864
|
+
}
|
|
1865
|
+
});
|
|
1866
|
+
ctx.logger.info(`OmnifyServiceProvider will be registered in ${registration.path}`);
|
|
1867
|
+
} else if (registration?.alreadyRegistered) {
|
|
1868
|
+
ctx.logger.info("OmnifyServiceProvider is already registered");
|
|
1869
|
+
}
|
|
1870
|
+
return outputs;
|
|
1871
|
+
}
|
|
1872
|
+
};
|
|
1873
|
+
const factoryGenerator = {
|
|
1874
|
+
name: "laravel-factories",
|
|
1875
|
+
description: "Generate Laravel factory classes for testing",
|
|
1876
|
+
generate: async (ctx) => {
|
|
1877
|
+
const factoryOptions = {
|
|
1878
|
+
modelNamespace: resolved.modelNamespace,
|
|
1879
|
+
factoryPath: resolved.factoriesPath,
|
|
1880
|
+
fakerLocale: resolved.fakerLocale
|
|
1881
|
+
};
|
|
1882
|
+
const factories = generateFactories(ctx.schemas, factoryOptions);
|
|
1883
|
+
return factories.map((factory) => ({
|
|
1884
|
+
path: getFactoryPath(factory),
|
|
1885
|
+
content: factory.content,
|
|
1886
|
+
type: "factory",
|
|
1887
|
+
// Skip writing factories if they already exist (allow customization)
|
|
1888
|
+
skipIfExists: !factory.overwrite,
|
|
1889
|
+
metadata: {
|
|
1890
|
+
factoryName: factory.name,
|
|
1891
|
+
schemaName: factory.schemaName
|
|
1892
|
+
}
|
|
1893
|
+
}));
|
|
1894
|
+
}
|
|
1895
|
+
};
|
|
1896
|
+
const generators = [migrationGenerator];
|
|
1897
|
+
if (resolved.generateModels) {
|
|
1898
|
+
generators.push(modelGenerator);
|
|
1899
|
+
}
|
|
1900
|
+
if (resolved.generateFactories) {
|
|
1901
|
+
generators.push(factoryGenerator);
|
|
1902
|
+
}
|
|
717
1903
|
return {
|
|
718
1904
|
name: "@famgia/omnify-laravel",
|
|
719
|
-
version: "0.0.
|
|
1905
|
+
version: "0.0.14",
|
|
720
1906
|
configSchema: LARAVEL_CONFIG_SCHEMA,
|
|
721
|
-
generators
|
|
722
|
-
{
|
|
723
|
-
name: "laravel-migrations",
|
|
724
|
-
description: "Generate Laravel migration files",
|
|
725
|
-
generate: async (ctx) => {
|
|
726
|
-
const migrationOptions = {
|
|
727
|
-
connection: resolved.connection,
|
|
728
|
-
timestamp: resolved.timestamp
|
|
729
|
-
};
|
|
730
|
-
const migrations = generateMigrations(ctx.schemas, migrationOptions);
|
|
731
|
-
return migrations.map((migration) => ({
|
|
732
|
-
path: getMigrationPath(migration, resolved.migrationsPath),
|
|
733
|
-
content: migration.content,
|
|
734
|
-
type: "migration",
|
|
735
|
-
metadata: {
|
|
736
|
-
tableName: migration.tables[0],
|
|
737
|
-
migrationType: migration.type
|
|
738
|
-
}
|
|
739
|
-
}));
|
|
740
|
-
}
|
|
741
|
-
}
|
|
742
|
-
]
|
|
1907
|
+
generators
|
|
743
1908
|
};
|
|
744
1909
|
}
|
|
745
1910
|
// Annotate the CommonJS export names for ESM import in node:
|