@omnifyjp/ts 8.0.7 → 8.1.1

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/README.md CHANGED
@@ -26,7 +26,9 @@ Per model with `options.service` or `options.api` (in `<output>/base/`):
26
26
  - **`<Model>QueryKeys.ts`** — TanStack Query key factory per model
27
27
  - **`<Model>Hooks.ts`** — React Query hooks (`useList`, `useDetail`,
28
28
  `useCreate`, `useUpdate`, `useDelete`, `useRestore`) with auto-invalidation
29
- and toast notifications via `sonner`
29
+ and toast notifications via `sonner` (configurable with `hooks.toastImport`,
30
+ or skipped with `hooks.enable: false` — see
31
+ [`docs/reference/typescript-hooks.md`](../../docs/reference/typescript-hooks.md))
30
32
 
31
33
  Plus shared infrastructure at the codegen output root:
32
34
 
package/dist/cli.js CHANGED
@@ -113,6 +113,7 @@ function resolveFromConfig(configPath) {
113
113
  tsPlatform: tsConfig?.platform,
114
114
  tsAuth: tsConfig?.auth,
115
115
  tsEnumStyle: tsConfig?.enumStyle,
116
+ tsHooks: tsConfig?.hooks,
116
117
  laravelEnabled,
117
118
  laravelOverrides,
118
119
  };
@@ -125,6 +126,11 @@ program
125
126
  .option('-i, --input <spec>', 'schemas.json source: local path, http(s) URL, or npm package spec (overrides config)')
126
127
  .option('-o, --output <path>', 'Output directory for TypeScript (overrides config)')
127
128
  .option('--force', 'Force regeneration of auto-generated files', false)
129
+ .option('--platform <platform>', 'TypeScript target platform with --input/--output: web | expo')
130
+ .option('--auth <auth>', 'Auth strategy with --input/--output: cookie | secureStore')
131
+ .option('--enum-style <style>', 'Enum emission style with --input/--output: enum | const')
132
+ .option('--toast-import <module>', 'Module the generated hooks import `toast` from (default "sonner")')
133
+ .option('--no-hooks', 'Skip generating {Entity}Hooks.ts')
128
134
  .option('--frozen-lockfile', 'Fail if remote input differs from .omnify/input.lock.json (CI mode)', false)
129
135
  .option('--update', 'Force re-fetch of remote input and refresh the lockfile', false)
130
136
  .action(async (opts) => {
@@ -134,6 +140,7 @@ program
134
140
  let tsPlatform;
135
141
  let tsAuth;
136
142
  let tsEnumStyle;
143
+ let tsHooks;
137
144
  let laravelEnabled = false;
138
145
  let laravelOverrides;
139
146
  let configDir = process.cwd();
@@ -142,6 +149,19 @@ program
142
149
  // passed through unchanged so URLs/npm specs work too.
143
150
  inputSpec = opts.input;
144
151
  tsOutput = resolve(opts.output);
152
+ // The workspace dispatcher calls this mode once per TypeScript target
153
+ // and has no config file to point at, so the target's options arrive
154
+ // as flags. Before these flags existed every workspace target silently
155
+ // generated with the web/cookie/enum/"sonner" defaults.
156
+ tsPlatform = opts.platform;
157
+ tsAuth = opts.auth;
158
+ tsEnumStyle = opts.enumStyle;
159
+ if (opts.toastImport !== undefined || opts.hooks === false) {
160
+ tsHooks = {
161
+ ...(opts.hooks === false ? { enable: false } : {}),
162
+ ...(opts.toastImport !== undefined ? { toastImport: opts.toastImport } : {}),
163
+ };
164
+ }
145
165
  }
146
166
  else {
147
167
  // Read from omnify.yaml
@@ -159,6 +179,7 @@ program
159
179
  tsPlatform = resolved.tsPlatform;
160
180
  tsAuth = resolved.tsAuth;
161
181
  tsEnumStyle = resolved.tsEnumStyle;
182
+ tsHooks = resolved.tsHooks;
162
183
  laravelEnabled = resolved.laravelEnabled;
163
184
  laravelOverrides = resolved.laravelOverrides;
164
185
  }
@@ -187,6 +208,7 @@ program
187
208
  platform: tsPlatform,
188
209
  auth: tsAuth,
189
210
  enumStyle: tsEnumStyle,
211
+ hooks: tsHooks,
190
212
  });
191
213
  mkdirSync(join(tsOutput, 'base'), { recursive: true });
192
214
  mkdirSync(join(tsOutput, 'enum'), { recursive: true });
@@ -20,6 +20,13 @@ export interface GenerateExtraConfig {
20
20
  auth?: 'cookie' | 'secureStore';
21
21
  /** Enum emission style — see GeneratorOptions.enumStyle. Issue #103. */
22
22
  enumStyle?: 'enum' | 'const';
23
+ /** Hooks layer (`{Entity}Hooks.ts`). Issue #327. */
24
+ hooks?: {
25
+ /** false skips `{Entity}Hooks.ts` entirely. Defaults to true. */
26
+ enable?: boolean;
27
+ /** Module the hooks import `toast` from. Defaults to "sonner". */
28
+ toastImport?: string;
29
+ };
23
30
  }
24
31
  /**
25
32
  * Generate all TypeScript files from schemas.json input.
package/dist/generator.js CHANGED
@@ -538,7 +538,9 @@ export function generateTypeScript(input, extra) {
538
538
  // Only for schemas with options.api or options.service.
539
539
  files.push(...generateTsServices(schemas, options));
540
540
  files.push(...generateTsQueryKeys(schemas));
541
- files.push(...generateTsHooks(schemas));
541
+ if (extra?.hooks?.enable !== false) {
542
+ files.push(...generateTsHooks(schemas, { toastImport: extra?.hooks?.toastImport }));
543
+ }
542
544
  // React Native / Expo query setup (issue #63)
543
545
  if (options.platform === 'expo') {
544
546
  files.push(generateRnQuerySetup(options.auth ?? 'secureStore'));
@@ -42,6 +42,7 @@ const TYPE_MAP = {
42
42
  Timestamp: 'DateTimeString',
43
43
  Json: 'unknown',
44
44
  Email: 'string',
45
+ Char: 'string',
45
46
  Password: 'string',
46
47
  Enum: 'string',
47
48
  Select: 'string',
@@ -21,7 +21,7 @@ import { formatTsObjectKey, tsObjectLiteral } from './ts-literal.js';
21
21
  // ---------------------------------------------------------------------------
22
22
  const TEXT_TYPES = new Set(['Text', 'MediumText', 'LongText']);
23
23
  const STRING_TYPES = new Set([
24
- 'String', 'Email', 'Password', 'Phone', 'Slug', 'Url', 'Uuid',
24
+ 'String', 'Char', 'Email', 'Password', 'Phone', 'Slug', 'Url', 'Uuid',
25
25
  ]);
26
26
  const NUMBER_TYPES = new Set([
27
27
  'TinyInt', 'Int', 'BigInt', 'Float', 'Decimal',
@@ -13,6 +13,8 @@ export function toFaker(propName, property, schemas = {}) {
13
13
  return null;
14
14
  switch (type) {
15
15
  case 'String': return stringFaker(propName, snakeName, property);
16
+ // Fixed-length: exactly `length` characters, as a CHAR column holds (#319).
17
+ case 'Char': return `'${snakeName}' => fake()->regexify('[A-Z]{${property['length'] ?? 255}}'),`;
16
18
  case 'Email': return `'${snakeName}' => fake()->unique()->safeEmail(),`;
17
19
  case 'Password': return `'${snakeName}' => bcrypt('password'),`;
18
20
  case 'Int':
@@ -339,6 +339,7 @@ function propertyToOpenApi(propName, prop, reader) {
339
339
  function toOpenApiType(type) {
340
340
  switch (type) {
341
341
  case 'String':
342
+ case 'Char':
342
343
  case 'Slug':
343
344
  case 'Phone':
344
345
  case 'Text':
@@ -4,7 +4,7 @@
4
4
  export class SchemaReader {
5
5
  data;
6
6
  constructor(data) {
7
- this.data = data;
7
+ this.data = normalizeEnumValues(data);
8
8
  }
9
9
  static fromData(data) {
10
10
  return new SchemaReader(data);
@@ -371,3 +371,51 @@ export class SchemaReader {
371
371
  return fields;
372
372
  }
373
373
  }
374
+ /**
375
+ * Bring enum values into the one shape every PHP generator reads (#322).
376
+ *
377
+ * The schema language accepts both forms in both places — `values: [draft]`
378
+ * and `values: [{value: draft, label: …}]` on a `kind: enum` schema, and the
379
+ * same two on an inline `enum:` property — and schemas.json carries them as
380
+ * written. The PHP generators were each written against one form: enum
381
+ * classes read `value.value` (so the README's plain-string example crashed),
382
+ * inline-enum rules interpolate the entry itself (so an object became
383
+ * `[object Object]`). Normalising here, once, keeps them from disagreeing.
384
+ */
385
+ function normalizeEnumValues(data) {
386
+ if (!data.schemas)
387
+ return data;
388
+ const schemas = {};
389
+ for (const [name, schema] of Object.entries(data.schemas)) {
390
+ let next = schema;
391
+ const rawValues = schema.values;
392
+ if (Array.isArray(rawValues) && rawValues.some(v => typeof v === 'string')) {
393
+ next = {
394
+ ...next,
395
+ values: rawValues.map(v => (typeof v === 'string' ? { value: v } : v)),
396
+ };
397
+ }
398
+ const props = schema.properties;
399
+ if (props) {
400
+ let changed = false;
401
+ const nextProps = {};
402
+ for (const [propName, prop] of Object.entries(props)) {
403
+ const rawEnum = prop.enum;
404
+ if (Array.isArray(rawEnum) && rawEnum.some(v => typeof v !== 'string')) {
405
+ changed = true;
406
+ nextProps[propName] = {
407
+ ...prop,
408
+ enum: rawEnum.map(v => (typeof v === 'string' ? v : String(v?.value ?? ''))),
409
+ };
410
+ }
411
+ else {
412
+ nextProps[propName] = prop;
413
+ }
414
+ }
415
+ if (changed)
416
+ next = { ...next, properties: nextProps };
417
+ }
418
+ schemas[name] = next;
419
+ }
420
+ return { ...data, schemas };
421
+ }
@@ -67,6 +67,7 @@ export function toPhpDocType(type, nullable = false) {
67
67
  let phpType;
68
68
  switch (type) {
69
69
  case 'String':
70
+ case 'Char':
70
71
  case 'Email':
71
72
  case 'Password':
72
73
  case 'Text':
@@ -108,7 +109,7 @@ export function toPhpDocType(type, nullable = false) {
108
109
  }
109
110
  /** Type categories for rule applicability. */
110
111
  const STRING_TYPES = new Set([
111
- 'String', 'Email', 'Password', 'Text', 'MediumText', 'LongText',
112
+ 'String', 'Char', 'Email', 'Password', 'Text', 'MediumText', 'LongText',
112
113
  'Slug', 'Url', 'Phone', 'Uuid',
113
114
  ]);
114
115
  const NUMERIC_TYPES = new Set(['Int', 'BigInt', 'TinyInt', 'Float', 'Decimal']);
@@ -148,6 +149,7 @@ export function toStoreRules(property, tableName, fkRuleType = 'integer') {
148
149
  }
149
150
  switch (type) {
150
151
  case 'String':
152
+ case 'Char':
151
153
  case 'Slug':
152
154
  case 'Url':
153
155
  case 'Phone': {
@@ -7,4 +7,12 @@
7
7
  * notifications and cache invalidation.
8
8
  */
9
9
  import type { SchemaDefinition, TypeScriptFile } from './types.js';
10
- export declare function generateTsHooks(schemas: Record<string, SchemaDefinition>): TypeScriptFile[];
10
+ export interface TsHooksOptions {
11
+ /**
12
+ * Module the generated hooks import `toast` from. Defaults to "sonner".
13
+ * Point it at a design-system wrapper (e.g. "@godxjp/ui") whose Toaster
14
+ * listens to its own copy of sonner. Issue #327.
15
+ */
16
+ toastImport?: string;
17
+ }
18
+ export declare function generateTsHooks(schemas: Record<string, SchemaDefinition>, hooksOptions?: TsHooksOptions): TypeScriptFile[];
@@ -6,11 +6,9 @@
6
6
  * that returns TanStack Query hooks (useQuery + useMutation) with toast
7
7
  * notifications and cache invalidation.
8
8
  */
9
- // ---------------------------------------------------------------------------
10
- // Public API
11
- // ---------------------------------------------------------------------------
12
- export function generateTsHooks(schemas) {
9
+ export function generateTsHooks(schemas, hooksOptions = {}) {
13
10
  const files = [];
11
+ const toastImport = hooksOptions.toastImport || 'sonner';
14
12
  for (const [name, schema] of Object.entries(schemas)) {
15
13
  if (schema.kind === 'enum')
16
14
  continue;
@@ -18,14 +16,14 @@ export function generateTsHooks(schemas) {
18
16
  continue;
19
17
  if (!schema.options?.api && !schema.options?.service)
20
18
  continue;
21
- files.push(generateHooksFile(name, schema));
19
+ files.push(generateHooksFile(name, schema, toastImport));
22
20
  }
23
21
  return files;
24
22
  }
25
23
  // ---------------------------------------------------------------------------
26
24
  // Per-schema generator
27
25
  // ---------------------------------------------------------------------------
28
- function generateHooksFile(name, schema) {
26
+ function generateHooksFile(name, schema, toastImport) {
29
27
  const hasSoftDelete = schema.options?.softDelete ?? false;
30
28
  const hasRestore = schema.options?.api?.restore ?? hasSoftDelete;
31
29
  const restoreHook = hasRestore ? `
@@ -46,7 +44,7 @@ function generateHooksFile(name, schema) {
46
44
  * @generated by omnify
47
45
  */
48
46
  import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
49
- import { toast } from "sonner";
47
+ import { toast } from ${JSON.stringify(toastImport)};
50
48
  import type { ${name}Filters, ${name}BaseServiceType } from "./${name}Service";
51
49
  import type { create${name}QueryKeys } from "./${name}QueryKeys";
52
50
 
@@ -50,7 +50,7 @@ function autoDerivePlaceholder(label, locales) {
50
50
  }
51
51
  /** Type categories for Zod rule applicability. */
52
52
  const ZOD_STRING_TYPES = new Set([
53
- 'String', 'Email', 'Password', 'Text', 'MediumText', 'LongText',
53
+ 'String', 'Char', 'Email', 'Password', 'Text', 'MediumText', 'LongText',
54
54
  'Slug', 'Url', 'Phone', 'Uuid',
55
55
  ]);
56
56
  const ZOD_NUMERIC_TYPES = new Set(['Int', 'BigInt', 'TinyInt', 'Float', 'Decimal']);
@@ -172,6 +172,7 @@ function getZodSchemaForType(propDef, _fieldName, options) {
172
172
  }
173
173
  switch (propDef.type) {
174
174
  case 'String':
175
+ case 'Char':
175
176
  case 'Text':
176
177
  case 'MediumText':
177
178
  case 'LongText':
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@omnifyjp/ts",
3
- "version": "8.0.7",
3
+ "version": "8.1.1",
4
4
  "description": "TypeScript model type generator from Omnify schemas.json",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",