@omnifyjp/ts 6.0.7 → 6.2.0

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/cli.js CHANGED
@@ -233,6 +233,11 @@ program
233
233
  mkdirSync(dirname(filePath), { recursive: true });
234
234
  if (!file.overwrite && existsSync(filePath)) {
235
235
  phpSkipped++;
236
+ // The file predates something the template now needs. Say so once,
237
+ // naming the file and the line, rather than rewriting user code.
238
+ if (file.expects && !readFileSync(filePath, 'utf-8').includes(file.expects.needle)) {
239
+ console.warn(`[omnify-ts] ${relative(configDir, filePath)}: ${file.expects.hint}`);
240
+ }
236
241
  continue;
237
242
  }
238
243
  // Issue #98 v5.8.18 / #99 v5.8.19: skip user-editable stub
@@ -15,15 +15,105 @@ export function generateModels(reader, config) {
15
15
  for (const [name, schema] of Object.entries(reader.getProjectObjectSchemas())) {
16
16
  files.push(...generateForSchema(name, schema, reader, config));
17
17
  }
18
- // Package schemas: user model only (extends package model)
18
+ // Package schemas: user model only (extends package model), plus a trait
19
+ // carrying anything a project-level `kind: extend` added to them (#164).
19
20
  for (const [name, schema] of Object.entries(reader.getPackageObjectSchemas())) {
20
21
  const pkgNs = reader.resolveModelNamespace(name, config.models.namespace);
21
22
  if (pkgNs !== config.models.namespace) {
22
- files.push(generatePackageUserModel(name, pkgNs, config));
23
+ const extended = extendedPropertyNames(schema);
24
+ if (extended.length > 0) {
25
+ files.push(generateExtendAttributesTrait(name, schema, extended, reader, config));
26
+ }
27
+ files.push(generatePackageUserModel(name, pkgNs, config, extended.length > 0));
23
28
  }
24
29
  }
25
30
  return files;
26
31
  }
32
+ /**
33
+ * The properties a project-level `kind: extend` added to a schema, in merge
34
+ * order, keeping only the ones the schema still declares.
35
+ */
36
+ function extendedPropertyNames(schema) {
37
+ const declared = schema.properties ?? {};
38
+ return (schema.extendedProperties ?? []).filter(name => declared[name] !== undefined);
39
+ }
40
+ /**
41
+ * A trait carrying the columns a project-level `kind: extend` added to a
42
+ * PACKAGE-owned schema (#164).
43
+ *
44
+ * The package's own generate run writes that model from the package's schemas
45
+ * alone; it cannot see a consumer's extend, so `$fillable` and `casts()` there
46
+ * are missing the added columns. The migration has them — the column exists —
47
+ * so `$model->new_column = x` is silently dropped by mass-assignment guarding
48
+ * and a Json column reads back as a raw string. The extend is only half
49
+ * applied, and nothing says so.
50
+ *
51
+ * It is written here, in the CONSUMER's own namespace, rather than back into
52
+ * the package's model. Writing into the package directory would work in a
53
+ * monorepo and silently do nothing once that package is installed under
54
+ * `vendor/`, where the next `composer install` removes it.
55
+ *
56
+ * `initialize<TraitName>()` is Laravel's own hook: `Model::__construct` calls
57
+ * `initializeTraits()`, which invokes it on every used trait. `mergeFillable` /
58
+ * `mergeCasts` add to what the parent declares instead of replacing it, which a
59
+ * redeclared `protected $fillable` in the child would do.
60
+ */
61
+ function generateExtendAttributesTrait(name, schema, extended, reader, config) {
62
+ const modelName = toPascalCase(name);
63
+ const traitName = extendAttributesTraitName(name);
64
+ const properties = (schema.properties ?? {});
65
+ const expandedProperties = reader.getExpandedProperties(name);
66
+ // The same two builders the BaseModel uses, over the extended properties
67
+ // only. Sharing them is the point: a column the BaseModel would have made
68
+ // fillable, or cast, is made fillable and cast the same way here.
69
+ const fillable = buildFillable(properties, expandedProperties, extended)
70
+ .split('\n')
71
+ .filter(line => line.trim() !== '')
72
+ .map(line => ` ${line.trim()}`)
73
+ .join('\n');
74
+ const casts = buildCasts(properties, expandedProperties, extended, reader, config)
75
+ .split('\n')
76
+ .filter(line => line.trim() !== '')
77
+ .map(line => ` ${line.trim()}`)
78
+ .join('\n');
79
+ const body = [];
80
+ if (fillable !== '') {
81
+ body.push(` $this->mergeFillable([\n${fillable}\n ]);`);
82
+ }
83
+ if (casts !== '') {
84
+ body.push(` $this->mergeCasts([\n${casts}\n ]);`);
85
+ }
86
+ const content = `<?php
87
+
88
+ namespace ${config.models.namespace}\\Concerns;
89
+
90
+ /**
91
+ * DO NOT EDIT - This file is auto-generated by Omnify.
92
+ * Any changes will be overwritten on next generation.
93
+ *
94
+ * @generated by omnify
95
+ */
96
+
97
+ /**
98
+ * Columns this project's \`kind: extend\` added to the ${modelName} schema.
99
+ *
100
+ * ${modelName} is owned by a package, so the package's own model cannot know
101
+ * about them. Used by ${config.models.namespace}\\${modelName}.
102
+ */
103
+ trait ${traitName}
104
+ {
105
+ /** Called by Laravel's Model::initializeTraits() from the constructor. */
106
+ public function initialize${traitName}(): void
107
+ {
108
+ ${body.join('\n\n')}
109
+ }
110
+ }
111
+ `;
112
+ return baseFile(`${config.models.path}/Concerns/${traitName}.php`, content);
113
+ }
114
+ function extendAttributesTraitName(name) {
115
+ return `${toPascalCase(name)}ExtendAttributes`;
116
+ }
27
117
  function generateForSchema(name, schema, reader, config) {
28
118
  const properties = (schema.properties ?? {});
29
119
  const options = schema.options ?? {};
@@ -104,9 +194,22 @@ function generateBaseModel(name, schema, reader, config) {
104
194
  const nestedSetParentColumn = null;
105
195
  const translatableFields = reader.getTranslatableFields(name);
106
196
  const hasTranslatable = translatableFields.length > 0;
107
- const primaryKey = options['primaryKey'] ?? 'id';
197
+ // A schema may name its primary key three mutually-exclusive ways: the
198
+ // `id:` shortcut (always column "id"), `options.primaryKey` (composite),
199
+ // or a property carrying `primary: true` (custom-named single-column PK
200
+ // — omnify-go#167). The schema validator rejects any schema declaring
201
+ // more than one of these, so checking them in this order is safe.
202
+ const primaryKeyOption = options['primaryKey'];
203
+ const customPrimaryEntry = primaryKeyOption
204
+ ? undefined
205
+ : propertyOrder
206
+ .map((p) => [p, properties[p]])
207
+ .find(([, prop]) => prop?.['primary'] === true);
208
+ const primaryKey = primaryKeyOption ?? (customPrimaryEntry ? toSnakeCase(customPrimaryEntry[0]) : 'id');
108
209
  const rawId = options.id;
109
- const idType = typeof rawId === 'string' ? rawId : 'BigInt';
210
+ const idType = customPrimaryEntry
211
+ ? (customPrimaryEntry[1]['type'] ?? 'String')
212
+ : (typeof rawId === 'string' ? rawId : 'BigInt');
110
213
  const isCompositeKey = primaryKey.includes(',');
111
214
  const needsUuidTrait = !isCompositeKey && idType === 'Uuid';
112
215
  const needsUlidTrait = !isCompositeKey && idType === 'Ulid';
@@ -168,6 +271,17 @@ function generateBaseModel(name, schema, reader, config) {
168
271
  */
169
272
  protected $keyType = 'string';
170
273
 
274
+ /**
275
+ * Indicates if the IDs are auto-incrementing.
276
+ */
277
+ public $incrementing = false;`;
278
+ }
279
+ else if (customPrimaryEntry && customPrimaryEntry[1]['autoIncrement'] !== true) {
280
+ // Integer-typed custom primary key not declared `autoIncrement: true`
281
+ // (omnify-go#167). Eloquent's Model default is $incrementing = true,
282
+ // which makes it omit the column on insert and read the value back via
283
+ // lastInsertId() — wrong for a key the caller assigns itself.
284
+ keyTypeSection = `
171
285
  /**
172
286
  * Indicates if the IDs are auto-incrementing.
173
287
  */
@@ -448,7 +562,11 @@ function buildDocProperties(properties, expandedProperties, propertyOrder, reade
448
562
  if (type === 'Association') {
449
563
  const relation = prop['relation'] ?? '';
450
564
  if (relation === 'ManyToOne') {
451
- const snakeName = toFkColumnName(propName);
565
+ // omnify-go#166: honor the `column:` override (ForeignColumn) the
566
+ // migration generator already applies — falling back to the
567
+ // {property}_id convention here documents a column the table
568
+ // doesn't actually have.
569
+ const snakeName = prop['column'] || toFkColumnName(propName);
452
570
  const nullable = prop['nullable'] ?? false;
453
571
  const phpType = nullable ? 'int|null' : 'int';
454
572
  lines.push(` * @property ${phpType} $${snakeName}`);
@@ -525,7 +643,8 @@ function buildFillable(properties, expandedProperties, propertyOrder) {
525
643
  if (type === 'Association') {
526
644
  const relation = prop['relation'] ?? '';
527
645
  if (relation === 'ManyToOne') {
528
- fields.push(toFkColumnName(propName));
646
+ // omnify-go#166: honor the `column:` override (ForeignColumn).
647
+ fields.push(prop['column'] || toFkColumnName(propName));
529
648
  }
530
649
  else if (relation === 'MorphTo') {
531
650
  const morphName = prop['morphName'] || toSnakeCase(propName);
@@ -633,7 +752,8 @@ function buildCasts(properties, expandedProperties, propertyOrder, reader, confi
633
752
  ? targetSchema.options.id
634
753
  : 'BigInt';
635
754
  if (targetIdType !== 'Uuid' && targetIdType !== 'Ulid') {
636
- casts.push(`'${toFkColumnName(propName)}' => 'integer',`);
755
+ // omnify-go#166: honor the `column:` override (ForeignColumn).
756
+ casts.push(`'${prop['column'] || toFkColumnName(propName)}' => 'integer',`);
637
757
  }
638
758
  }
639
759
  continue;
@@ -678,7 +798,9 @@ function buildCasts(properties, expandedProperties, propertyOrder, reader, confi
678
798
  * 4. Default — flat editable namespace (Laravel-canonical
679
799
  * `App\Models\<Name>`).
680
800
  */
681
- function resolveRelatedModelNamespace(target, modelNamespace, config, reader) {
801
+ function resolveRelatedModelNamespace(target, modelNamespace, config, reader,
802
+ /** The property's `targetNamespace`, for a target with no schema. */
803
+ externalNamespace) {
682
804
  // Package schemas use their own model namespace (we don't generate
683
805
  // editable stubs for them — relations dispatch to the package class).
684
806
  const packageNs = reader.resolveModelNamespace(target, modelNamespace);
@@ -689,8 +811,16 @@ function resolveRelatedModelNamespace(target, modelNamespace, config, reader) {
689
811
  return config.models.namespace;
690
812
  }
691
813
  const targetSchema = reader.getSchema(target);
692
- if (!targetSchema)
693
- return config.models.userEditableNamespace;
814
+ if (!targetSchema) {
815
+ // A target outside the schema set is an external model, and
816
+ // `targetNamespace` is the documented way to say where it lives
817
+ // ("Target model namespace for external packages"). Ignoring it and
818
+ // falling back to this project's editable namespace emits
819
+ // `\App\Models\User` for a model that is not there — the relation
820
+ // resolves to a class that does not exist. The fallback still applies
821
+ // when the field is absent, which is every schema that does not use it.
822
+ return externalNamespace || config.models.userEditableNamespace;
823
+ }
694
824
  const editableNs = config.models.userEditableNamespace;
695
825
  if (targetSchema.kind === 'pivot') {
696
826
  return `${editableNs}\\Pivot`;
@@ -717,7 +847,9 @@ function buildRelations(schemaName, properties, propertyOrder, modelNamespace, r
717
847
  // Issue #98 v5.8.6: also nest by target's group / Pivot/ so the
718
848
  // FQCN matches the target file's actual location.
719
849
  const target = prop['target'] ?? '';
720
- const ns = target ? resolveRelatedModelNamespace(target, modelNamespace, config, reader) : modelNamespace;
850
+ const ns = target
851
+ ? resolveRelatedModelNamespace(target, modelNamespace, config, reader, prop['targetNamespace'])
852
+ : modelNamespace;
721
853
  const targetTableName = target ? reader.getTableName(target) : '';
722
854
  // For ManyToMany, look up the explicit `kind: pivot` schema by pivotFor
723
855
  // pair (user-authored filenames may predate the sorted-name convention)
@@ -845,7 +977,11 @@ function buildInverseRelations(parentName, modelNamespace, reader, declaredMetho
845
977
  // generate a child-side accessor, not a parent-side one.
846
978
  // Here we're emitting the *parent* accessor, so the child's ManyToOne
847
979
  // (or owning OneToOne) is what we react to.
848
- const fkColumn = toFkColumnName(childPropName);
980
+ // omnify-go#166: honor the child property's `column:` override
981
+ // (ForeignColumn) — the physical FK column lives on the child's
982
+ // table, so it must match whatever the child's own migration/model
983
+ // used, not the {property}_id convention.
984
+ const fkColumn = childProp['column'] || toFkColumnName(childPropName);
849
985
  // Issue #98 v5.8.6: nest the inverse-side FQCN by the CHILD's
850
986
  // group / Pivot/ so the relation resolves to the child's actual
851
987
  // file location (which lives in `<childGroup>/<Child>.php` or
@@ -890,16 +1026,28 @@ function buildAccessors(expandedProperties) {
890
1026
  return methods.join('\n');
891
1027
  }
892
1028
  /** Generate a user model for a package schema that extends the package's model class. */
893
- function generatePackageUserModel(name, packageNamespace, config) {
1029
+ function generatePackageUserModel(name, packageNamespace, config, hasExtendAttributes = false) {
894
1030
  const modelName = toPascalCase(name);
895
1031
  const modelNamespace = config.models.namespace;
896
1032
  const packageModelClass = `${packageNamespace}\\${modelName}`;
1033
+ const traitName = extendAttributesTraitName(name);
1034
+ // This file is created once and never rewritten, so the `use` line can only
1035
+ // be put here for a model that does not exist yet. A project whose model
1036
+ // predates its extend keeps the file it has, and `generateModels` warns with
1037
+ // the one line to add — overwriting somebody's model to add an import is the
1038
+ // worse of the two.
1039
+ const traitImport = hasExtendAttributes
1040
+ ? `use ${modelNamespace}\\Concerns\\${traitName};\n`
1041
+ : '';
1042
+ const traitUse = hasExtendAttributes
1043
+ ? ` use ${traitName};\n\n`
1044
+ : '';
897
1045
  const content = `<?php
898
1046
 
899
1047
  namespace ${modelNamespace};
900
1048
 
901
1049
  use ${packageModelClass} as Package${modelName};
902
-
1050
+ ${traitImport}
903
1051
  /**
904
1052
  * ${modelName} Model (extends package model)
905
1053
  *
@@ -908,10 +1056,19 @@ use ${packageModelClass} as Package${modelName};
908
1056
  */
909
1057
  class ${modelName} extends Package${modelName}
910
1058
  {
911
- // Add your custom methods here
1059
+ ${traitUse} // Add your custom methods here
912
1060
  }
913
1061
  `;
914
- return userFile(`${config.models.path}/${modelName}.php`, content);
1062
+ return userFile(`${config.models.path}/${modelName}.php`, content, hasExtendAttributes
1063
+ ? {
1064
+ needle: `use ${traitName};`,
1065
+ hint: `this project's \`kind: extend\` adds columns to the package-owned `
1066
+ + `${modelName} schema, but this model predates the trait that carries `
1067
+ + `them, so those columns are still not fillable or cast. Add `
1068
+ + `\`use ${config.models.namespace}\\Concerns\\${traitName};\` and `
1069
+ + `\`use ${traitName};\` inside the class (#164).`,
1070
+ }
1071
+ : undefined);
915
1072
  }
916
1073
  /**
917
1074
  * Detect whether a schema uses nested set (tree structure).
@@ -10,7 +10,7 @@ export function buildRelation(propName, property, modelNamespace, context) {
10
10
  let method = null;
11
11
  switch (relation) {
12
12
  case 'ManyToOne':
13
- method = belongsTo(propName, target, modelNamespace);
13
+ method = belongsTo(propName, property, target, modelNamespace);
14
14
  break;
15
15
  case 'OneToOne':
16
16
  method = oneToOne(propName, property, target, modelNamespace);
@@ -60,8 +60,12 @@ export function getReturnType(relation) {
60
60
  default: return 'BelongsTo';
61
61
  }
62
62
  }
63
- function belongsTo(propName, target, ns) {
64
- const foreignKey = toFkColumnName(propName);
63
+ function belongsTo(propName, property, target, ns) {
64
+ // omnify-go#166: honor the `column:` override (ForeignColumn) — the
65
+ // migration generator already names the physical column from it, so
66
+ // falling back to the {property}_id convention here points the relation
67
+ // at a column the table doesn't have.
68
+ const foreignKey = property['column'] || toFkColumnName(propName);
65
69
  const methodName = toCamelCase(propName);
66
70
  const fqcn = `\\${ns}\\${target}`;
67
71
  return ` /**
@@ -86,7 +90,7 @@ function oneToOne(propName, property, target, ns) {
86
90
  return $this->hasOne(${fqcn}::class, '${foreignKey}');
87
91
  }`;
88
92
  }
89
- return belongsTo(propName, target, ns);
93
+ return belongsTo(propName, property, target, ns);
90
94
  }
91
95
  function hasMany(propName, property, target, ns) {
92
96
  const inverse = property['mappedBy'] ?? property['inversedBy'] ?? null;
@@ -18,13 +18,41 @@ import { baseFile, userFile, resolveModularBasePath, resolveModularBaseNamespace
18
18
  export function generateResources(reader, config) {
19
19
  const files = [];
20
20
  for (const [name, schema] of Object.entries(reader.getProjectVisibleObjectSchemas())) {
21
- if (schema.kind === 'extend')
21
+ if (!generatesOwnResource(schema))
22
22
  continue;
23
23
  files.push(generateBaseResource(name, schema, reader, config));
24
24
  files.push(generateUserResource(name, schema, config));
25
25
  }
26
26
  return files;
27
27
  }
28
+ /**
29
+ * Whether THIS generator writes a Resource for a project-owned schema.
30
+ *
31
+ * The loop above and the cross-resource reference below have to agree about
32
+ * this, and when they were two separate conditions they did not: the loop
33
+ * skipped `kind: extend`, and the reference did not ask at all — so a relation
34
+ * pointing at one named a class the loop had just declined to write.
35
+ */
36
+ function generatesOwnResource(schema) {
37
+ return schema.kind !== 'extend' && schema.options?.hidden !== true;
38
+ }
39
+ /**
40
+ * Whether a Resource class exists for an association target — ours or a
41
+ * package's. `false` means there is no class to name, and the caller must fall
42
+ * back to a bare `whenLoaded()`.
43
+ */
44
+ function hasResourceClass(target, reader) {
45
+ const schema = reader.getSchema(target);
46
+ // Not a schema at all: an external model reached through `targetNamespace`,
47
+ // e.g. the app's own User. Omnify generates nothing for it.
48
+ if (!schema)
49
+ return false;
50
+ // A package ships its own resources; whether they are reachable is the
51
+ // namespace question, which the caller answers separately.
52
+ if (schema.package != null)
53
+ return true;
54
+ return generatesOwnResource(schema);
55
+ }
28
56
  function generateBaseResource(name, schema, reader, config) {
29
57
  const modelName = toPascalCase(name);
30
58
  // Issue #98 v5.8.2: nest namespace by group in legacy structure so the
@@ -242,8 +270,23 @@ function addAssociationFields(propName, prop, fields, resourceNamespace, modelNa
242
270
  targetResNs = editableNs;
243
271
  }
244
272
  }
245
- // If target is a package schema without a resource namespace, use simple whenLoaded
246
- if (!targetResNs && relation !== 'MorphTo') {
273
+ // The namespace resolving is not the same question as the class existing.
274
+ // `resolveResourceNamespace()` only returns null for a PACKAGE schema whose
275
+ // package declares no resource namespace — a target that is not a schema at
276
+ // all (an external model reached through `targetNamespace`, e.g. the app's
277
+ // own User), or one this generator declines to write a Resource for, falls
278
+ // through and is treated as project-owned. The reference then names
279
+ // `\<projectResourceNs>\<Target>Resource`, a class nothing declares, and
280
+ // `whenLoaded(..., fn () => new UserResource(...))` fatals the moment that
281
+ // relation is loaded and the resource is rendered.
282
+ //
283
+ // This is the same rule `interface-generator.ts` already applies on the TS
284
+ // side (`resolveTargetName` → `unknown` for a target outside the schema set),
285
+ // arriving late on the PHP side.
286
+ const targetHasResource = target ? hasResourceClass(target, reader) : false;
287
+ // If target is a package schema without a resource namespace, or a model no
288
+ // Resource is written for, use simple whenLoaded
289
+ if ((!targetResNs || !targetHasResource) && relation !== 'MorphTo') {
247
290
  if (relation === 'ManyToOne') {
248
291
  const snakeName = toFkColumnName(propName);
249
292
  fields.push(` '${snakeName}' => $this->${snakeName},`);
@@ -7,11 +7,28 @@ export interface GeneratedFile {
7
7
  readonly content: string;
8
8
  readonly overwrite: boolean;
9
9
  readonly type: 'base' | 'user';
10
+ /**
11
+ * For a user file only: something the file has to contain that a version
12
+ * written by an EARLIER omnify would not have.
13
+ *
14
+ * A user file is created once and never rewritten, so a template that grows a
15
+ * new line reaches new projects and silently misses every existing one. The
16
+ * writer checks this against the file it skipped and prints `hint` when it is
17
+ * absent — telling the one project that needs the edit, rather than warning
18
+ * everybody on every run or overwriting somebody's code to add a line.
19
+ */
20
+ readonly expects?: {
21
+ readonly needle: string;
22
+ readonly hint: string;
23
+ };
10
24
  }
11
25
  /** Create a base file (always overwritten). */
12
26
  export declare function baseFile(path: string, content: string): GeneratedFile;
13
27
  /** Create a user file (created once, skip if exists). */
14
- export declare function userFile(path: string, content: string): GeneratedFile;
28
+ export declare function userFile(path: string, content: string, expects?: {
29
+ needle: string;
30
+ hint: string;
31
+ }): GeneratedFile;
15
32
  /** Category of base file for modular path resolution. */
16
33
  export type BaseCategory = 'Models' | 'Controllers' | 'Requests' | 'Resources' | 'Services' | 'Enums' | 'Traits' | 'Locales' | 'Policies';
17
34
  /**
package/dist/php/types.js CHANGED
@@ -6,8 +6,8 @@ export function baseFile(path, content) {
6
6
  return { path, content, overwrite: true, type: 'base' };
7
7
  }
8
8
  /** Create a user file (created once, skip if exists). */
9
- export function userFile(path, content) {
10
- return { path, content, overwrite: false, type: 'user' };
9
+ export function userFile(path, content, expects) {
10
+ return { path, content, overwrite: false, type: 'user', ...(expects ? { expects } : {}) };
11
11
  }
12
12
  /**
13
13
  * Resolve base file path for a schema based on structure.
package/dist/types.d.ts CHANGED
@@ -240,6 +240,14 @@ export interface SchemaDefinition {
240
240
  readonly properties?: Record<string, PropertyDefinition>;
241
241
  readonly propertyOrder?: readonly string[];
242
242
  readonly expandedProperties?: Record<string, ExpandedProperty>;
243
+ /**
244
+ * Properties a project-level `kind: extend` merged into this schema, in
245
+ * merge order. The Go loader folds an extend into its target and deletes
246
+ * the extend, so this is the only remaining trace of where they came from
247
+ * — which a PACKAGE-owned target needs, because the package's own generate
248
+ * run never saw them (#164).
249
+ */
250
+ readonly extendedProperties?: readonly string[];
243
251
  readonly values?: readonly EnumValueDefinition[];
244
252
  readonly pivotFor?: readonly string[];
245
253
  readonly policies?: readonly PolicyDefinition[];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@omnifyjp/ts",
3
- "version": "6.0.7",
3
+ "version": "6.2.0",
4
4
  "description": "TypeScript model type generator from Omnify schemas.json",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",