@omnifyjp/omnify 3.13.1 → 3.14.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/package.json +6 -6
- package/ts-dist/generator.js +8 -0
- package/ts-dist/metadata-generator.d.ts +2 -0
- package/ts-dist/metadata-generator.js +6 -0
- package/ts-dist/php/index.js +4 -1
- package/ts-dist/php/schema-reader.d.ts +11 -0
- package/ts-dist/php/schema-reader.js +18 -0
- package/ts-dist/php/service-generator.d.ts +6 -2
- package/ts-dist/php/service-generator.js +573 -263
- package/ts-dist/ts-hooks-generator.d.ts +10 -0
- package/ts-dist/ts-hooks-generator.js +131 -0
- package/ts-dist/ts-query-keys-generator.d.ts +9 -0
- package/ts-dist/ts-query-keys-generator.js +52 -0
- package/ts-dist/ts-service-generator.d.ts +10 -0
- package/ts-dist/ts-service-generator.js +190 -0
- package/ts-dist/types.d.ts +12 -0
- package/ts-dist/zod-generator.js +33 -3
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @omnify/ts — Frontend Hooks Generator (issue #58)
|
|
3
|
+
*
|
|
4
|
+
* Generates a `{Entity}Hooks.ts` per schema with `options.service` or
|
|
5
|
+
* `options.api`. Each file exports a `create{Entity}BaseHooks` factory
|
|
6
|
+
* that returns TanStack Query hooks (useQuery + useMutation) with toast
|
|
7
|
+
* notifications and cache invalidation.
|
|
8
|
+
*/
|
|
9
|
+
import type { SchemaDefinition, TypeScriptFile } from './types.js';
|
|
10
|
+
export declare function generateTsHooks(schemas: Record<string, SchemaDefinition>): TypeScriptFile[];
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @omnify/ts — Frontend Hooks Generator (issue #58)
|
|
3
|
+
*
|
|
4
|
+
* Generates a `{Entity}Hooks.ts` per schema with `options.service` or
|
|
5
|
+
* `options.api`. Each file exports a `create{Entity}BaseHooks` factory
|
|
6
|
+
* that returns TanStack Query hooks (useQuery + useMutation) with toast
|
|
7
|
+
* notifications and cache invalidation.
|
|
8
|
+
*/
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
// Public API
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
export function generateTsHooks(schemas) {
|
|
13
|
+
const files = [];
|
|
14
|
+
for (const [name, schema] of Object.entries(schemas)) {
|
|
15
|
+
if (schema.kind === 'enum')
|
|
16
|
+
continue;
|
|
17
|
+
if (schema.options?.hidden)
|
|
18
|
+
continue;
|
|
19
|
+
if (!schema.options?.api && !schema.options?.service)
|
|
20
|
+
continue;
|
|
21
|
+
files.push(generateHooksFile(name, schema));
|
|
22
|
+
}
|
|
23
|
+
return files;
|
|
24
|
+
}
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
// Per-schema generator
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
function generateHooksFile(name, schema) {
|
|
29
|
+
const hasSoftDelete = schema.options?.softDelete ?? false;
|
|
30
|
+
const hasRestore = schema.options?.api?.restore ?? hasSoftDelete;
|
|
31
|
+
const restoreHook = hasRestore ? `
|
|
32
|
+
useRestore: (slug: string) => {
|
|
33
|
+
const qc = useQueryClient();
|
|
34
|
+
return useMutation({
|
|
35
|
+
mutationFn: (id: string) => service.restore(slug, id),
|
|
36
|
+
onSuccess: () => {
|
|
37
|
+
toast.success(\`\${entityLabel} restored.\`);
|
|
38
|
+
qc.invalidateQueries({ queryKey: keys.all(slug) });
|
|
39
|
+
},
|
|
40
|
+
onError: (e: Error) =>
|
|
41
|
+
toast.error(e.message || \`Failed to restore \${entityLabel}.\`),
|
|
42
|
+
});
|
|
43
|
+
},` : '';
|
|
44
|
+
const content = `/**
|
|
45
|
+
* DO NOT EDIT - Auto-generated by Omnify.
|
|
46
|
+
* @generated by omnify
|
|
47
|
+
*/
|
|
48
|
+
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
49
|
+
import { toast } from "sonner";
|
|
50
|
+
import type { ${name}Filters, ${name}BaseServiceType } from "./${name}Service";
|
|
51
|
+
import type { create${name}QueryKeys } from "./${name}QueryKeys";
|
|
52
|
+
|
|
53
|
+
export function create${name}BaseHooks(
|
|
54
|
+
service: ${name}BaseServiceType,
|
|
55
|
+
keys: ReturnType<typeof create${name}QueryKeys>,
|
|
56
|
+
entityLabel: string,
|
|
57
|
+
) {
|
|
58
|
+
return {
|
|
59
|
+
useList: (slug: string, filters: ${name}Filters = {}) =>
|
|
60
|
+
useQuery({
|
|
61
|
+
queryKey: keys.list(slug, filters),
|
|
62
|
+
queryFn: () => service.list(slug, filters),
|
|
63
|
+
enabled: !!slug,
|
|
64
|
+
}),
|
|
65
|
+
|
|
66
|
+
useDetail: (slug: string, id: string) =>
|
|
67
|
+
useQuery({
|
|
68
|
+
queryKey: keys.detail(slug, id),
|
|
69
|
+
queryFn: () => service.getById(slug, id),
|
|
70
|
+
enabled: !!slug && !!id,
|
|
71
|
+
}),
|
|
72
|
+
|
|
73
|
+
useLookup: (slug: string) =>
|
|
74
|
+
useQuery({
|
|
75
|
+
queryKey: keys.lookup(slug),
|
|
76
|
+
queryFn: () => service.lookup(slug),
|
|
77
|
+
enabled: !!slug,
|
|
78
|
+
}),
|
|
79
|
+
|
|
80
|
+
useCreate: (slug: string) => {
|
|
81
|
+
const qc = useQueryClient();
|
|
82
|
+
return useMutation({
|
|
83
|
+
mutationFn: (data: Parameters<typeof service.create>[1]) =>
|
|
84
|
+
service.create(slug, data),
|
|
85
|
+
onSuccess: () => {
|
|
86
|
+
toast.success(\`\${entityLabel} created.\`);
|
|
87
|
+
qc.invalidateQueries({ queryKey: keys.all(slug) });
|
|
88
|
+
},
|
|
89
|
+
onError: (e: Error) =>
|
|
90
|
+
toast.error(e.message || \`Failed to create \${entityLabel}.\`),
|
|
91
|
+
});
|
|
92
|
+
},
|
|
93
|
+
|
|
94
|
+
useUpdate: (slug: string) => {
|
|
95
|
+
const qc = useQueryClient();
|
|
96
|
+
return useMutation({
|
|
97
|
+
mutationFn: ({ id, data }: { id: string; data: Parameters<typeof service.update>[2] }) =>
|
|
98
|
+
service.update(slug, id, data),
|
|
99
|
+
onSuccess: () => {
|
|
100
|
+
toast.success(\`\${entityLabel} updated.\`);
|
|
101
|
+
qc.invalidateQueries({ queryKey: keys.all(slug) });
|
|
102
|
+
},
|
|
103
|
+
onError: (e: Error) =>
|
|
104
|
+
toast.error(e.message || \`Failed to update \${entityLabel}.\`),
|
|
105
|
+
});
|
|
106
|
+
},
|
|
107
|
+
|
|
108
|
+
useDelete: (slug: string) => {
|
|
109
|
+
const qc = useQueryClient();
|
|
110
|
+
return useMutation({
|
|
111
|
+
mutationFn: (id: string) => service.delete(slug, id),
|
|
112
|
+
onSuccess: () => {
|
|
113
|
+
toast.success(\`\${entityLabel} deleted.\`);
|
|
114
|
+
qc.invalidateQueries({ queryKey: keys.all(slug) });
|
|
115
|
+
},
|
|
116
|
+
onError: (e: Error) =>
|
|
117
|
+
toast.error(e.message || \`Failed to delete \${entityLabel}.\`),
|
|
118
|
+
});
|
|
119
|
+
},
|
|
120
|
+
${restoreHook}
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
`;
|
|
124
|
+
return {
|
|
125
|
+
filePath: `base/${name}Hooks.ts`,
|
|
126
|
+
content,
|
|
127
|
+
types: [`create${name}BaseHooks`],
|
|
128
|
+
overwrite: true,
|
|
129
|
+
category: 'base',
|
|
130
|
+
};
|
|
131
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @omnify/ts — Frontend QueryKeys Generator (issue #58)
|
|
3
|
+
*
|
|
4
|
+
* Generates a `{Entity}QueryKeys.ts` per schema with `options.service` or
|
|
5
|
+
* `options.api`. Each file exports a `create{Entity}QueryKeys` factory for
|
|
6
|
+
* TanStack Query cache key management.
|
|
7
|
+
*/
|
|
8
|
+
import type { SchemaDefinition, TypeScriptFile } from './types.js';
|
|
9
|
+
export declare function generateTsQueryKeys(schemas: Record<string, SchemaDefinition>): TypeScriptFile[];
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @omnify/ts — Frontend QueryKeys Generator (issue #58)
|
|
3
|
+
*
|
|
4
|
+
* Generates a `{Entity}QueryKeys.ts` per schema with `options.service` or
|
|
5
|
+
* `options.api`. Each file exports a `create{Entity}QueryKeys` factory for
|
|
6
|
+
* TanStack Query cache key management.
|
|
7
|
+
*/
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
// Public API
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
export function generateTsQueryKeys(schemas) {
|
|
12
|
+
const files = [];
|
|
13
|
+
for (const [name, schema] of Object.entries(schemas)) {
|
|
14
|
+
if (schema.kind === 'enum')
|
|
15
|
+
continue;
|
|
16
|
+
if (schema.options?.hidden)
|
|
17
|
+
continue;
|
|
18
|
+
if (!schema.options?.api && !schema.options?.service)
|
|
19
|
+
continue;
|
|
20
|
+
files.push(generateQueryKeysFile(name));
|
|
21
|
+
}
|
|
22
|
+
return files;
|
|
23
|
+
}
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
// Per-schema generator
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
function generateQueryKeysFile(name) {
|
|
28
|
+
const lowerName = name.charAt(0).toLowerCase() + name.slice(1);
|
|
29
|
+
const content = `/**
|
|
30
|
+
* DO NOT EDIT - Auto-generated by Omnify.
|
|
31
|
+
* @generated by omnify
|
|
32
|
+
*/
|
|
33
|
+
export function create${name}QueryKeys(entity: string) {
|
|
34
|
+
return {
|
|
35
|
+
all: (slug: string) => [entity, slug] as const,
|
|
36
|
+
list: (slug: string, filters?: object) =>
|
|
37
|
+
[entity, slug, "list", filters] as const,
|
|
38
|
+
detail: (slug: string, id: string) =>
|
|
39
|
+
[entity, slug, "detail", id] as const,
|
|
40
|
+
lookup: (slug: string) =>
|
|
41
|
+
[entity, slug, "lookup"] as const,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
`;
|
|
45
|
+
return {
|
|
46
|
+
filePath: `base/${name}QueryKeys.ts`,
|
|
47
|
+
content,
|
|
48
|
+
types: [`create${name}QueryKeys`],
|
|
49
|
+
overwrite: true,
|
|
50
|
+
category: 'base',
|
|
51
|
+
};
|
|
52
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @omnify/ts — Frontend BaseService Generator (issue #58)
|
|
3
|
+
*
|
|
4
|
+
* Generates a `{Entity}Service.ts` per schema with `options.service` or
|
|
5
|
+
* `options.api`. Each file exports a factory function `create{Entity}BaseService`
|
|
6
|
+
* that returns typed CRUD methods. URL construction is injected by the caller
|
|
7
|
+
* so the generated code never hardcodes routes.
|
|
8
|
+
*/
|
|
9
|
+
import type { SchemaDefinition, TypeScriptFile, GeneratorOptions } from './types.js';
|
|
10
|
+
export declare function generateTsServices(schemas: Record<string, SchemaDefinition>, options: GeneratorOptions): TypeScriptFile[];
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @omnify/ts — Frontend BaseService Generator (issue #58)
|
|
3
|
+
*
|
|
4
|
+
* Generates a `{Entity}Service.ts` per schema with `options.service` or
|
|
5
|
+
* `options.api`. Each file exports a factory function `create{Entity}BaseService`
|
|
6
|
+
* that returns typed CRUD methods. URL construction is injected by the caller
|
|
7
|
+
* so the generated code never hardcodes routes.
|
|
8
|
+
*/
|
|
9
|
+
import { toSnakeCase } from './interface-generator.js';
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// Public API
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
export function generateTsServices(schemas, options) {
|
|
14
|
+
const files = [];
|
|
15
|
+
for (const [name, schema] of Object.entries(schemas)) {
|
|
16
|
+
if (schema.kind === 'enum')
|
|
17
|
+
continue;
|
|
18
|
+
if (schema.options?.hidden)
|
|
19
|
+
continue;
|
|
20
|
+
if (!schema.options?.api && !schema.options?.service)
|
|
21
|
+
continue;
|
|
22
|
+
files.push(generateServiceFile(name, schema, options));
|
|
23
|
+
}
|
|
24
|
+
return files;
|
|
25
|
+
}
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
// Per-schema generator
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
function generateServiceFile(name, schema, options) {
|
|
30
|
+
const svc = schema.options?.service ?? {};
|
|
31
|
+
const api = schema.options?.api;
|
|
32
|
+
const hasSoftDelete = schema.options?.softDelete ?? false;
|
|
33
|
+
const hasRestore = api?.restore ?? hasSoftDelete;
|
|
34
|
+
const lowerName = name.charAt(0).toLowerCase() + name.slice(1);
|
|
35
|
+
// Resolve filterable/searchable fields
|
|
36
|
+
const properties = schema.properties ?? {};
|
|
37
|
+
const propertyOrder = schema.propertyOrder ?? Object.keys(properties);
|
|
38
|
+
const filterableFields = resolveFilterable(svc, properties, propertyOrder);
|
|
39
|
+
const lookupFields = svc.lookupFields ?? ['id', 'name'];
|
|
40
|
+
// Build filters interface
|
|
41
|
+
const filtersInterface = buildFiltersInterface(name, filterableFields, hasSoftDelete, properties);
|
|
42
|
+
// Build lookup item interface
|
|
43
|
+
const lookupInterface = buildLookupInterface(name, lookupFields, properties);
|
|
44
|
+
// Build toParams function
|
|
45
|
+
const toParamsBody = buildToParams(filterableFields, hasSoftDelete, properties);
|
|
46
|
+
// Build service factory
|
|
47
|
+
const serviceMethods = buildServiceMethods(name, hasRestore);
|
|
48
|
+
const content = `/**
|
|
49
|
+
* DO NOT EDIT - Auto-generated by Omnify.
|
|
50
|
+
* @generated by omnify
|
|
51
|
+
*/
|
|
52
|
+
import type { ${name}, ${name}CreateFormState, ${name}UpdateFormState } from "./${name}";
|
|
53
|
+
|
|
54
|
+
${filtersInterface}
|
|
55
|
+
|
|
56
|
+
${lookupInterface}
|
|
57
|
+
|
|
58
|
+
${toParamsBody}
|
|
59
|
+
|
|
60
|
+
// Factory — URL is injected by user-land, not hardcoded.
|
|
61
|
+
export function create${name}BaseService(baseUrl: (slug: string, path?: string) => string) {
|
|
62
|
+
return {
|
|
63
|
+
${serviceMethods}
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export type ${name}BaseServiceType = ReturnType<typeof create${name}BaseService>;
|
|
68
|
+
`;
|
|
69
|
+
return {
|
|
70
|
+
filePath: `base/${name}Service.ts`,
|
|
71
|
+
content,
|
|
72
|
+
types: [`${name}Filters`, `${name}LookupItem`, `create${name}BaseService`, `${name}BaseServiceType`],
|
|
73
|
+
overwrite: true,
|
|
74
|
+
category: 'base',
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
function resolveFilterable(svc, properties, propertyOrder) {
|
|
78
|
+
const fields = [];
|
|
79
|
+
const names = svc.filterable && svc.filterable.length > 0
|
|
80
|
+
? svc.filterable
|
|
81
|
+
: propertyOrder.filter(p => properties[p]?.filterable);
|
|
82
|
+
for (const propName of names) {
|
|
83
|
+
const prop = properties[propName];
|
|
84
|
+
if (!prop)
|
|
85
|
+
continue;
|
|
86
|
+
const colName = toSnakeCase(propName);
|
|
87
|
+
const isBool = prop.type === 'Boolean';
|
|
88
|
+
const tsType = isBool ? 'boolean' : 'string';
|
|
89
|
+
fields.push({ name: propName, colName, tsType, isBool });
|
|
90
|
+
}
|
|
91
|
+
return fields;
|
|
92
|
+
}
|
|
93
|
+
function buildFiltersInterface(name, filterableFields, hasSoftDelete, properties) {
|
|
94
|
+
const lines = [];
|
|
95
|
+
lines.push(`export interface ${name}Filters {`);
|
|
96
|
+
lines.push(` page?: number;`);
|
|
97
|
+
lines.push(` per_page?: number;`);
|
|
98
|
+
lines.push(` search?: string;`);
|
|
99
|
+
for (const f of filterableFields) {
|
|
100
|
+
lines.push(` ${f.colName}?: ${f.tsType};`);
|
|
101
|
+
}
|
|
102
|
+
if (hasSoftDelete) {
|
|
103
|
+
lines.push(` with_trashed?: boolean;`);
|
|
104
|
+
lines.push(` only_trashed?: boolean;`);
|
|
105
|
+
}
|
|
106
|
+
lines.push(` sort?: string;`);
|
|
107
|
+
lines.push(`}`);
|
|
108
|
+
return lines.join('\n');
|
|
109
|
+
}
|
|
110
|
+
function buildLookupInterface(name, lookupFields, properties) {
|
|
111
|
+
const lines = [];
|
|
112
|
+
lines.push(`export interface ${name}LookupItem {`);
|
|
113
|
+
for (const f of lookupFields) {
|
|
114
|
+
const col = toSnakeCase(f);
|
|
115
|
+
const prop = properties[f];
|
|
116
|
+
let tsType = 'string';
|
|
117
|
+
if (prop) {
|
|
118
|
+
if (prop.type === 'Int' || prop.type === 'BigInt' || prop.type === 'TinyInt' || prop.type === 'Float' || prop.type === 'Decimal') {
|
|
119
|
+
tsType = 'number';
|
|
120
|
+
}
|
|
121
|
+
else if (prop.type === 'Boolean') {
|
|
122
|
+
tsType = 'boolean';
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
lines.push(` ${col}: ${tsType};`);
|
|
126
|
+
}
|
|
127
|
+
lines.push(`}`);
|
|
128
|
+
return lines.join('\n');
|
|
129
|
+
}
|
|
130
|
+
function buildToParams(filterableFields, hasSoftDelete, properties) {
|
|
131
|
+
const lines = [];
|
|
132
|
+
lines.push(`function toParams(filters: ${filterableFields.length > 0 ? 'Record<string, any>' : 'Record<string, any>'}): string {`);
|
|
133
|
+
lines.push(` const p = new URLSearchParams();`);
|
|
134
|
+
lines.push(` if (filters.page) p.set("page", String(filters.page));`);
|
|
135
|
+
lines.push(` if (filters.per_page) p.set("per_page", String(filters.per_page));`);
|
|
136
|
+
lines.push(` if (filters.search) p.set("search", filters.search);`);
|
|
137
|
+
for (const f of filterableFields) {
|
|
138
|
+
if (f.isBool) {
|
|
139
|
+
lines.push(` if (filters.${f.colName} !== undefined) p.set("${f.colName}", filters.${f.colName} ? "1" : "0");`);
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
lines.push(` if (filters.${f.colName}) p.set("${f.colName}", filters.${f.colName});`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
if (hasSoftDelete) {
|
|
146
|
+
lines.push(` if (filters.with_trashed) p.set("with_trashed", "1");`);
|
|
147
|
+
lines.push(` if (filters.only_trashed) p.set("only_trashed", "1");`);
|
|
148
|
+
}
|
|
149
|
+
lines.push(` if (filters.sort) p.set("sort", filters.sort);`);
|
|
150
|
+
lines.push(` return p.toString();`);
|
|
151
|
+
lines.push(`}`);
|
|
152
|
+
return lines.join('\n');
|
|
153
|
+
}
|
|
154
|
+
function buildServiceMethods(name, hasRestore) {
|
|
155
|
+
const lines = [];
|
|
156
|
+
const indent = ' ';
|
|
157
|
+
lines.push(`${indent}list: (slug: string, filters: ${name}Filters = {}) =>`);
|
|
158
|
+
lines.push(`${indent} apiFetch<PaginatedResponse<${name}>>(`);
|
|
159
|
+
lines.push(`${indent} \`\${baseUrl(slug)}?\${toParams({ per_page: 25, ...filters })}\``);
|
|
160
|
+
lines.push(`${indent} ),`);
|
|
161
|
+
lines.push('');
|
|
162
|
+
lines.push(`${indent}getById: (slug: string, id: string) =>`);
|
|
163
|
+
lines.push(`${indent} apiFetch<{ data: ${name} }>(baseUrl(slug, \`/\${id}\`)),`);
|
|
164
|
+
lines.push('');
|
|
165
|
+
lines.push(`${indent}lookup: (slug: string) =>`);
|
|
166
|
+
lines.push(`${indent} apiFetch<{ data: ${name}LookupItem[] }>(baseUrl(slug, "/lookup")),`);
|
|
167
|
+
lines.push('');
|
|
168
|
+
lines.push(`${indent}create: (slug: string, data: ${name}CreateFormState) =>`);
|
|
169
|
+
lines.push(`${indent} apiFetch<{ data: ${name} }>(baseUrl(slug), {`);
|
|
170
|
+
lines.push(`${indent} method: "POST",`);
|
|
171
|
+
lines.push(`${indent} body: JSON.stringify(data),`);
|
|
172
|
+
lines.push(`${indent} }),`);
|
|
173
|
+
lines.push('');
|
|
174
|
+
lines.push(`${indent}update: (slug: string, id: string, data: Partial<${name}UpdateFormState>) =>`);
|
|
175
|
+
lines.push(`${indent} apiFetch<{ data: ${name} }>(baseUrl(slug, \`/\${id}\`), {`);
|
|
176
|
+
lines.push(`${indent} method: "PUT",`);
|
|
177
|
+
lines.push(`${indent} body: JSON.stringify(data),`);
|
|
178
|
+
lines.push(`${indent} }),`);
|
|
179
|
+
lines.push('');
|
|
180
|
+
lines.push(`${indent}delete: (slug: string, id: string) =>`);
|
|
181
|
+
lines.push(`${indent} apiFetch<null>(baseUrl(slug, \`/\${id}\`), { method: "DELETE" }),`);
|
|
182
|
+
if (hasRestore) {
|
|
183
|
+
lines.push('');
|
|
184
|
+
lines.push(`${indent}restore: (slug: string, id: string) =>`);
|
|
185
|
+
lines.push(`${indent} apiFetch<{ data: ${name} }>(baseUrl(slug, \`/\${id}/restore\`), {`);
|
|
186
|
+
lines.push(`${indent} method: "POST",`);
|
|
187
|
+
lines.push(`${indent} }),`);
|
|
188
|
+
}
|
|
189
|
+
return lines.join('\n');
|
|
190
|
+
}
|
package/ts-dist/types.d.ts
CHANGED
|
@@ -113,6 +113,15 @@ export interface ApiOptions {
|
|
|
113
113
|
readonly middleware?: readonly string[];
|
|
114
114
|
readonly perPage?: number;
|
|
115
115
|
}
|
|
116
|
+
/** Service layer codegen options — controls BaseService generation. Issue #57. */
|
|
117
|
+
export interface ServiceOptions {
|
|
118
|
+
readonly searchable?: readonly string[];
|
|
119
|
+
readonly filterable?: readonly string[];
|
|
120
|
+
readonly defaultSort?: string;
|
|
121
|
+
readonly eagerLoad?: readonly string[];
|
|
122
|
+
readonly eagerCount?: readonly string[];
|
|
123
|
+
readonly lookupFields?: readonly string[];
|
|
124
|
+
}
|
|
116
125
|
/** Schema options. */
|
|
117
126
|
export interface SchemaOptions {
|
|
118
127
|
readonly id?: boolean | string;
|
|
@@ -129,6 +138,8 @@ export interface SchemaOptions {
|
|
|
129
138
|
readonly updatedBy?: boolean;
|
|
130
139
|
readonly deletedBy?: boolean;
|
|
131
140
|
};
|
|
141
|
+
/** Service layer codegen options. Issue #57. */
|
|
142
|
+
readonly service?: ServiceOptions;
|
|
132
143
|
/** Schema-level default ordering — generates a global Eloquent scope. Issue #40. */
|
|
133
144
|
readonly defaultOrder?: readonly OrderByItem[];
|
|
134
145
|
}
|
|
@@ -328,6 +339,7 @@ export interface SchemaDisplayNames {
|
|
|
328
339
|
readonly displayName: LocaleMap;
|
|
329
340
|
readonly propertyDisplayNames: Record<string, LocaleMap>;
|
|
330
341
|
readonly propertyPlaceholders: Record<string, LocaleMap>;
|
|
342
|
+
readonly propertyDescriptions: Record<string, LocaleMap>;
|
|
331
343
|
}
|
|
332
344
|
/** Generation options (internal). */
|
|
333
345
|
export interface GeneratorOptions {
|
package/ts-dist/zod-generator.js
CHANGED
|
@@ -26,6 +26,27 @@ function getMultiLocaleDisplayName(value, locales, fallbackLocale, defaultValue)
|
|
|
26
26
|
}
|
|
27
27
|
return result;
|
|
28
28
|
}
|
|
29
|
+
/**
|
|
30
|
+
* Auto-derive placeholder from label per locale (issue #59).
|
|
31
|
+
* - ja: "{label}を入力"
|
|
32
|
+
* - en: "Enter {label}"
|
|
33
|
+
* - vi: "Nhập {label}"
|
|
34
|
+
* Other locales: "Enter {label}" (fallback to English pattern)
|
|
35
|
+
*/
|
|
36
|
+
function autoDerivePlaceholder(label, locales) {
|
|
37
|
+
const PREFIXES = {
|
|
38
|
+
ja: { prefix: '', suffix: 'を入力' },
|
|
39
|
+
en: { prefix: 'Enter ', suffix: '' },
|
|
40
|
+
vi: { prefix: 'Nhập ', suffix: '' },
|
|
41
|
+
};
|
|
42
|
+
const result = {};
|
|
43
|
+
for (const locale of locales) {
|
|
44
|
+
const l = label[locale] ?? '';
|
|
45
|
+
const pattern = PREFIXES[locale] ?? PREFIXES['en'];
|
|
46
|
+
result[locale] = `${pattern.prefix}${l}${pattern.suffix}`;
|
|
47
|
+
}
|
|
48
|
+
return result;
|
|
49
|
+
}
|
|
29
50
|
/** Type categories for Zod rule applicability. */
|
|
30
51
|
const ZOD_STRING_TYPES = new Set([
|
|
31
52
|
'String', 'Email', 'Password', 'Text', 'MediumText', 'LongText',
|
|
@@ -340,6 +361,7 @@ export function generateDisplayNames(schema, options) {
|
|
|
340
361
|
const displayName = getMultiLocaleDisplayName(schema.displayName, locales, fallbackLocale, schema.name);
|
|
341
362
|
const propertyDisplayNames = {};
|
|
342
363
|
const propertyPlaceholders = {};
|
|
364
|
+
const propertyDescriptions = {};
|
|
343
365
|
if (schema.properties) {
|
|
344
366
|
const propNames = schema.propertyOrder ?? Object.keys(schema.properties);
|
|
345
367
|
for (const propName of propNames) {
|
|
@@ -378,14 +400,22 @@ export function generateDisplayNames(schema, options) {
|
|
|
378
400
|
continue;
|
|
379
401
|
}
|
|
380
402
|
// Regular field display name
|
|
381
|
-
|
|
382
|
-
|
|
403
|
+
const label = getMultiLocaleDisplayName(propDef.displayName, locales, fallbackLocale, propName);
|
|
404
|
+
propertyDisplayNames[fieldName] = label;
|
|
405
|
+
// Placeholder — explicit or auto-derived from label (issue #59)
|
|
383
406
|
if (propDef.placeholder) {
|
|
384
407
|
propertyPlaceholders[fieldName] = getMultiLocaleDisplayName(propDef.placeholder, locales, fallbackLocale, '');
|
|
385
408
|
}
|
|
409
|
+
else if (propDef.displayName) {
|
|
410
|
+
propertyPlaceholders[fieldName] = autoDerivePlaceholder(label, locales);
|
|
411
|
+
}
|
|
412
|
+
// Description (issue #59)
|
|
413
|
+
if (propDef.description) {
|
|
414
|
+
propertyDescriptions[fieldName] = getMultiLocaleDisplayName(propDef.description, locales, fallbackLocale, '');
|
|
415
|
+
}
|
|
386
416
|
}
|
|
387
417
|
}
|
|
388
|
-
return { displayName, propertyDisplayNames, propertyPlaceholders };
|
|
418
|
+
return { displayName, propertyDisplayNames, propertyPlaceholders, propertyDescriptions };
|
|
389
419
|
}
|
|
390
420
|
/**
|
|
391
421
|
* Get fields to exclude from create/update schemas.
|