@metaobjectsdev/codegen-ts 0.5.0-rc.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/LICENSE +189 -0
- package/README.md +101 -0
- package/dist/column-mapper.d.ts +38 -0
- package/dist/column-mapper.d.ts.map +1 -0
- package/dist/column-mapper.js +205 -0
- package/dist/column-mapper.js.map +1 -0
- package/dist/constants.d.ts +7 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +8 -0
- package/dist/constants.js.map +1 -0
- package/dist/errors.d.ts +7 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +11 -0
- package/dist/errors.js.map +1 -0
- package/dist/format.d.ts +2 -0
- package/dist/format.d.ts.map +1 -0
- package/dist/format.js +47 -0
- package/dist/format.js.map +1 -0
- package/dist/generator.d.ts +44 -0
- package/dist/generator.d.ts.map +1 -0
- package/dist/generator.js +17 -0
- package/dist/generator.js.map +1 -0
- package/dist/generators/barrel.d.ts +6 -0
- package/dist/generators/barrel.d.ts.map +1 -0
- package/dist/generators/barrel.js +17 -0
- package/dist/generators/barrel.js.map +1 -0
- package/dist/generators/entity-file.d.ts +8 -0
- package/dist/generators/entity-file.d.ts.map +1 -0
- package/dist/generators/entity-file.js +27 -0
- package/dist/generators/entity-file.js.map +1 -0
- package/dist/generators/index.d.ts +5 -0
- package/dist/generators/index.d.ts.map +1 -0
- package/dist/generators/index.js +5 -0
- package/dist/generators/index.js.map +1 -0
- package/dist/generators/queries-file.d.ts +8 -0
- package/dist/generators/queries-file.d.ts.map +1 -0
- package/dist/generators/queries-file.js +26 -0
- package/dist/generators/queries-file.js.map +1 -0
- package/dist/generators/routes-file.d.ts +12 -0
- package/dist/generators/routes-file.d.ts.map +1 -0
- package/dist/generators/routes-file.js +30 -0
- package/dist/generators/routes-file.js.map +1 -0
- package/dist/import-path.d.ts +41 -0
- package/dist/import-path.d.ts.map +1 -0
- package/dist/import-path.js +95 -0
- package/dist/import-path.js.map +1 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +21 -0
- package/dist/index.js.map +1 -0
- package/dist/metaobjects-config.d.ts +56 -0
- package/dist/metaobjects-config.d.ts.map +1 -0
- package/dist/metaobjects-config.js +42 -0
- package/dist/metaobjects-config.js.map +1 -0
- package/dist/naming.d.ts +29 -0
- package/dist/naming.d.ts.map +1 -0
- package/dist/naming.js +67 -0
- package/dist/naming.js.map +1 -0
- package/dist/overwrite-policy.d.ts +8 -0
- package/dist/overwrite-policy.d.ts.map +1 -0
- package/dist/overwrite-policy.js +23 -0
- package/dist/overwrite-policy.js.map +1 -0
- package/dist/pk-resolver.d.ts +18 -0
- package/dist/pk-resolver.d.ts.map +1 -0
- package/dist/pk-resolver.js +36 -0
- package/dist/pk-resolver.js.map +1 -0
- package/dist/projection/extract-view-spec.d.ts +18 -0
- package/dist/projection/extract-view-spec.d.ts.map +1 -0
- package/dist/projection/extract-view-spec.js +272 -0
- package/dist/projection/extract-view-spec.js.map +1 -0
- package/dist/projection/index.d.ts +5 -0
- package/dist/projection/index.d.ts.map +1 -0
- package/dist/projection/index.js +5 -0
- package/dist/projection/index.js.map +1 -0
- package/dist/projection/projection-detector.d.ts +4 -0
- package/dist/projection/projection-detector.d.ts.map +1 -0
- package/dist/projection/projection-detector.js +13 -0
- package/dist/projection/projection-detector.js.map +1 -0
- package/dist/projection/view-ddl-emit.d.ts +10 -0
- package/dist/projection/view-ddl-emit.d.ts.map +1 -0
- package/dist/projection/view-ddl-emit.js +47 -0
- package/dist/projection/view-ddl-emit.js.map +1 -0
- package/dist/projection/view-spec.d.ts +56 -0
- package/dist/projection/view-spec.d.ts.map +1 -0
- package/dist/projection/view-spec.js +2 -0
- package/dist/projection/view-spec.js.map +1 -0
- package/dist/relation-resolver.d.ts +21 -0
- package/dist/relation-resolver.d.ts.map +1 -0
- package/dist/relation-resolver.js +62 -0
- package/dist/relation-resolver.js.map +1 -0
- package/dist/render-context.d.ts +65 -0
- package/dist/render-context.d.ts.map +1 -0
- package/dist/render-context.js +28 -0
- package/dist/render-context.js.map +1 -0
- package/dist/runner.d.ts +17 -0
- package/dist/runner.d.ts.map +1 -0
- package/dist/runner.js +135 -0
- package/dist/runner.js.map +1 -0
- package/dist/templates/barrel.d.ts +8 -0
- package/dist/templates/barrel.d.ts.map +1 -0
- package/dist/templates/barrel.js +12 -0
- package/dist/templates/barrel.js.map +1 -0
- package/dist/templates/drizzle-schema.d.ts +13 -0
- package/dist/templates/drizzle-schema.d.ts.map +1 -0
- package/dist/templates/drizzle-schema.js +251 -0
- package/dist/templates/drizzle-schema.js.map +1 -0
- package/dist/templates/entity-constants.d.ts +4 -0
- package/dist/templates/entity-constants.d.ts.map +1 -0
- package/dist/templates/entity-constants.js +215 -0
- package/dist/templates/entity-constants.js.map +1 -0
- package/dist/templates/entity-file.d.ts +4 -0
- package/dist/templates/entity-file.d.ts.map +1 -0
- package/dist/templates/entity-file.js +45 -0
- package/dist/templates/entity-file.js.map +1 -0
- package/dist/templates/field-meta.d.ts +24 -0
- package/dist/templates/field-meta.d.ts.map +1 -0
- package/dist/templates/field-meta.js +117 -0
- package/dist/templates/field-meta.js.map +1 -0
- package/dist/templates/filter-allowlist.d.ts +5 -0
- package/dist/templates/filter-allowlist.d.ts.map +1 -0
- package/dist/templates/filter-allowlist.js +86 -0
- package/dist/templates/filter-allowlist.js.map +1 -0
- package/dist/templates/filter-shared.d.ts +15 -0
- package/dist/templates/filter-shared.d.ts.map +1 -0
- package/dist/templates/filter-shared.js +30 -0
- package/dist/templates/filter-shared.js.map +1 -0
- package/dist/templates/filter-type.d.ts +4 -0
- package/dist/templates/filter-type.d.ts.map +1 -0
- package/dist/templates/filter-type.js +78 -0
- package/dist/templates/filter-type.js.map +1 -0
- package/dist/templates/inferred-types.d.ts +4 -0
- package/dist/templates/inferred-types.d.ts.map +1 -0
- package/dist/templates/inferred-types.js +14 -0
- package/dist/templates/inferred-types.js.map +1 -0
- package/dist/templates/projection-decl.d.ts +21 -0
- package/dist/templates/projection-decl.d.ts.map +1 -0
- package/dist/templates/projection-decl.js +116 -0
- package/dist/templates/projection-decl.js.map +1 -0
- package/dist/templates/queries-file.d.ts +4 -0
- package/dist/templates/queries-file.d.ts.map +1 -0
- package/dist/templates/queries-file.js +39 -0
- package/dist/templates/queries-file.js.map +1 -0
- package/dist/templates/queries.d.ts +9 -0
- package/dist/templates/queries.d.ts.map +1 -0
- package/dist/templates/queries.js +115 -0
- package/dist/templates/queries.js.map +1 -0
- package/dist/templates/relations-block.d.ts +9 -0
- package/dist/templates/relations-block.d.ts.map +1 -0
- package/dist/templates/relations-block.js +45 -0
- package/dist/templates/relations-block.js.map +1 -0
- package/dist/templates/routes-file.d.ts +4 -0
- package/dist/templates/routes-file.d.ts.map +1 -0
- package/dist/templates/routes-file.js +158 -0
- package/dist/templates/routes-file.js.map +1 -0
- package/dist/templates/zod-validators.d.ts +4 -0
- package/dist/templates/zod-validators.d.ts.map +1 -0
- package/dist/templates/zod-validators.js +129 -0
- package/dist/templates/zod-validators.js.map +1 -0
- package/package.json +59 -0
- package/src/column-mapper.ts +266 -0
- package/src/constants.ts +10 -0
- package/src/errors.ts +10 -0
- package/src/format.ts +50 -0
- package/src/generator.ts +73 -0
- package/src/generators/barrel.ts +28 -0
- package/src/generators/entity-file.ts +33 -0
- package/src/generators/index.ts +4 -0
- package/src/generators/queries-file.ts +32 -0
- package/src/generators/routes-file.ts +36 -0
- package/src/import-path.ts +153 -0
- package/src/index.ts +45 -0
- package/src/metaobjects-config.ts +95 -0
- package/src/naming.ts +84 -0
- package/src/overwrite-policy.ts +39 -0
- package/src/pk-resolver.ts +47 -0
- package/src/projection/extract-view-spec.ts +372 -0
- package/src/projection/index.ts +4 -0
- package/src/projection/projection-detector.ts +26 -0
- package/src/projection/view-ddl-emit.ts +66 -0
- package/src/projection/view-spec.ts +62 -0
- package/src/relation-resolver.ts +87 -0
- package/src/render-context.ts +93 -0
- package/src/runner.ts +178 -0
- package/src/templates/barrel.ts +23 -0
- package/src/templates/drizzle-schema.ts +286 -0
- package/src/templates/entity-constants.ts +248 -0
- package/src/templates/entity-file.ts +51 -0
- package/src/templates/field-meta.ts +150 -0
- package/src/templates/filter-allowlist.ts +104 -0
- package/src/templates/filter-shared.ts +30 -0
- package/src/templates/filter-type.ts +93 -0
- package/src/templates/inferred-types.ts +16 -0
- package/src/templates/projection-decl.ts +146 -0
- package/src/templates/queries-file.ts +56 -0
- package/src/templates/queries.ts +132 -0
- package/src/templates/relations-block.ts +65 -0
- package/src/templates/routes-file.ts +179 -0
- package/src/templates/zod-validators.ts +140 -0
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
// Entity-constants template — emits an `<Entity>` const with all the
|
|
2
|
+
// metadata-derived strings consumers should use INSTEAD of magic strings.
|
|
3
|
+
//
|
|
4
|
+
// Shape (each non-$-prefixed key is a per-field object):
|
|
5
|
+
//
|
|
6
|
+
// export const Subscriber = {
|
|
7
|
+
// $entity: "Subscriber",
|
|
8
|
+
// $table: "subscribers",
|
|
9
|
+
// $path: "/subscribers",
|
|
10
|
+
//
|
|
11
|
+
// email: {
|
|
12
|
+
// name: "email",
|
|
13
|
+
// label: "Email Address", // from @label on the view, falls back to humanized field name
|
|
14
|
+
// view: "text", // MetaView subtype
|
|
15
|
+
// htmlType: "email", // optional; only when it maps to a real HTML input type
|
|
16
|
+
// placeholder: "you@example.com", // optional; only when @placeholder is set on the view
|
|
17
|
+
// helpText: "We never share this.", // optional; only when @helpText is set
|
|
18
|
+
// rules: { // optional; derived from validator children
|
|
19
|
+
// required: "Email is required",
|
|
20
|
+
// maxLength: { value: 255, message: "Must be 255 characters or fewer" },
|
|
21
|
+
// pattern: { value: /.../, message: "Invalid email" },
|
|
22
|
+
// },
|
|
23
|
+
// },
|
|
24
|
+
// // ...
|
|
25
|
+
// } as const;
|
|
26
|
+
//
|
|
27
|
+
// Consumers spread `form.input.email` from useEntityForm and never touch
|
|
28
|
+
// per-attribute access (placeholder, rules, etc.) by hand — the helper
|
|
29
|
+
// picks them up from this object automatically.
|
|
30
|
+
|
|
31
|
+
import { code, type Code } from "ts-poet";
|
|
32
|
+
import type { MetaData } from "@metaobjectsdev/metadata";
|
|
33
|
+
import { MetaObject, MetaField } from "@metaobjectsdev/metadata";
|
|
34
|
+
import {
|
|
35
|
+
VIEW_SUBTYPE_TEXT,
|
|
36
|
+
VIEW_SUBTYPE_TEXTAREA,
|
|
37
|
+
VIEW_SUBTYPE_NUMBER,
|
|
38
|
+
VIEW_SUBTYPE_CHECKBOX,
|
|
39
|
+
VIEW_SUBTYPE_DATE,
|
|
40
|
+
VIEW_SUBTYPE_PASSWORD,
|
|
41
|
+
VIEW_SUBTYPE_HIDDEN,
|
|
42
|
+
VIEW_SUBTYPE_DROPDOWN,
|
|
43
|
+
VIEW_SUBTYPE_RADIO,
|
|
44
|
+
VALIDATOR_SUBTYPE_REQUIRED,
|
|
45
|
+
VALIDATOR_SUBTYPE_LENGTH,
|
|
46
|
+
VALIDATOR_SUBTYPE_REGEX,
|
|
47
|
+
VALIDATOR_ATTR_MIN,
|
|
48
|
+
VALIDATOR_ATTR_MAX,
|
|
49
|
+
VALIDATOR_ATTR_PATTERN,
|
|
50
|
+
FIELD_ATTR_MAX_LENGTH,
|
|
51
|
+
FIELD_ATTR_REQUIRED,
|
|
52
|
+
} from "@metaobjectsdev/metadata";
|
|
53
|
+
import { resolveTableName, pluralize, toSnakeCase } from "@metaobjectsdev/metadata";
|
|
54
|
+
import { inferViewKind, currencyMetaFor, labelFor } from "./field-meta.js";
|
|
55
|
+
|
|
56
|
+
/** Convert a camelCase or PascalCase field name to a human-friendly label. */
|
|
57
|
+
function humanize(s: string): string {
|
|
58
|
+
return s
|
|
59
|
+
.replace(/([a-z])([A-Z])/g, "$1 $2")
|
|
60
|
+
.replace(/_/g, " ")
|
|
61
|
+
.replace(/\b\w/g, (c) => c.toUpperCase())
|
|
62
|
+
.trim();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* REST resource path for an entity. Pluralized + snake_cased + lowercased.
|
|
67
|
+
* "Subscriber" → "/subscribers"
|
|
68
|
+
* "WorkoutEvent" → "/workout_events"
|
|
69
|
+
*/
|
|
70
|
+
function resourcePath(entity: MetaData): string {
|
|
71
|
+
const overrideAttr = entity.ownAttr("routePath");
|
|
72
|
+
if (typeof overrideAttr === "string" && overrideAttr.length > 0) {
|
|
73
|
+
return overrideAttr.startsWith("/") ? overrideAttr : `/${overrideAttr}`;
|
|
74
|
+
}
|
|
75
|
+
return `/${pluralize(toSnakeCase(entity.name))}`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Resolve the view subtype: explicit `view` child (own or inherited) wins, else inferred from field subType. */
|
|
79
|
+
function resolveView(field: MetaField): { view: string; viewNode?: MetaData } {
|
|
80
|
+
const viewChild = field.views()[0];
|
|
81
|
+
if (viewChild) {
|
|
82
|
+
return { view: viewChild.subType, viewNode: viewChild };
|
|
83
|
+
}
|
|
84
|
+
return { view: inferViewKind(field) };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Map a MetaView subtype to a real HTML <input type=...> value. Returns
|
|
89
|
+
* undefined for views that don't map to <input> at all (textarea, dropdown,
|
|
90
|
+
* radio) — consumers render the right element type themselves.
|
|
91
|
+
*/
|
|
92
|
+
function htmlTypeFromView(view: string, override?: string): string | undefined {
|
|
93
|
+
if (typeof override === "string" && override.length > 0) return override;
|
|
94
|
+
switch (view) {
|
|
95
|
+
case VIEW_SUBTYPE_TEXT:
|
|
96
|
+
return "text";
|
|
97
|
+
case VIEW_SUBTYPE_NUMBER:
|
|
98
|
+
return "number";
|
|
99
|
+
case VIEW_SUBTYPE_DATE:
|
|
100
|
+
return "date";
|
|
101
|
+
case VIEW_SUBTYPE_PASSWORD:
|
|
102
|
+
return "password";
|
|
103
|
+
case VIEW_SUBTYPE_CHECKBOX:
|
|
104
|
+
return "checkbox";
|
|
105
|
+
case VIEW_SUBTYPE_HIDDEN:
|
|
106
|
+
return "hidden";
|
|
107
|
+
case VIEW_SUBTYPE_RADIO:
|
|
108
|
+
return "radio";
|
|
109
|
+
case "month":
|
|
110
|
+
return "month";
|
|
111
|
+
case "email":
|
|
112
|
+
return "email";
|
|
113
|
+
case VIEW_SUBTYPE_TEXTAREA:
|
|
114
|
+
case VIEW_SUBTYPE_DROPDOWN:
|
|
115
|
+
return undefined;
|
|
116
|
+
default:
|
|
117
|
+
return undefined;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Build RHF rules JSON-ish code from a field's validator children plus
|
|
123
|
+
* field-level attrs (`@required`, `@maxLength`). Returns the code string
|
|
124
|
+
* (e.g. `{ required: "X", maxLength: { value: 255, message: "..." } }`)
|
|
125
|
+
* or undefined when there are no rules to emit.
|
|
126
|
+
*/
|
|
127
|
+
function renderFieldRules(field: MetaField): string | undefined {
|
|
128
|
+
const ruleParts: string[] = [];
|
|
129
|
+
|
|
130
|
+
let hasRequired = false;
|
|
131
|
+
let hasMaxLength = false;
|
|
132
|
+
|
|
133
|
+
for (const child of field.validators()) {
|
|
134
|
+
if (child.subType === VALIDATOR_SUBTYPE_REQUIRED) {
|
|
135
|
+
const msg = (child.ownAttr("message") as string | undefined) ?? `${humanize(field.name)} is required`;
|
|
136
|
+
ruleParts.push(`required: ${JSON.stringify(msg)}`);
|
|
137
|
+
hasRequired = true;
|
|
138
|
+
} else if (child.subType === VALIDATOR_SUBTYPE_LENGTH) {
|
|
139
|
+
const min = child.ownAttr(VALIDATOR_ATTR_MIN);
|
|
140
|
+
const max = child.ownAttr(VALIDATOR_ATTR_MAX);
|
|
141
|
+
if (typeof min === "number") {
|
|
142
|
+
const msg = (child.ownAttr("minMessage") as string | undefined) ?? `Must be at least ${min} characters`;
|
|
143
|
+
ruleParts.push(`minLength: { value: ${min}, message: ${JSON.stringify(msg)} }`);
|
|
144
|
+
}
|
|
145
|
+
if (typeof max === "number") {
|
|
146
|
+
const msg = (child.ownAttr("maxMessage") as string | undefined) ?? `Must be ${max} characters or fewer`;
|
|
147
|
+
ruleParts.push(`maxLength: { value: ${max}, message: ${JSON.stringify(msg)} }`);
|
|
148
|
+
hasMaxLength = true;
|
|
149
|
+
}
|
|
150
|
+
} else if (child.subType === VALIDATOR_SUBTYPE_REGEX) {
|
|
151
|
+
const pattern = child.ownAttr(VALIDATOR_ATTR_PATTERN);
|
|
152
|
+
if (typeof pattern === "string") {
|
|
153
|
+
const msg = (child.ownAttr("message") as string | undefined) ?? "Invalid format";
|
|
154
|
+
// Emit as RegExp literal /.../ — `as const` preserves the value-ref.
|
|
155
|
+
// Forward-slash inside the pattern is escaped so the literal closes correctly.
|
|
156
|
+
const safe = pattern.replace(/\\/g, "\\\\").replace(/\//g, "\\/");
|
|
157
|
+
ruleParts.push(`pattern: { value: /${safe}/, message: ${JSON.stringify(msg)} }`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Field-level @required attr (if not already covered by validator).
|
|
163
|
+
if (!hasRequired && field.ownAttr(FIELD_ATTR_REQUIRED) === true) {
|
|
164
|
+
ruleParts.push(`required: ${JSON.stringify(`${humanize(field.name)} is required`)}`);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Field-level @maxLength attr (if not already covered).
|
|
168
|
+
const maxLenAttr = field.ownAttr(FIELD_ATTR_MAX_LENGTH);
|
|
169
|
+
if (!hasMaxLength && typeof maxLenAttr === "number") {
|
|
170
|
+
ruleParts.push(
|
|
171
|
+
`maxLength: { value: ${maxLenAttr}, message: ${JSON.stringify(`Must be ${maxLenAttr} characters or fewer`)} }`,
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (ruleParts.length === 0) return undefined;
|
|
176
|
+
return `{ ${ruleParts.join(", ")} }`;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** Build one nested field-object entry like `email: { name, label, ... },`. */
|
|
180
|
+
function renderFieldEntry(field: MetaField): string {
|
|
181
|
+
const { view, viewNode } = resolveView(field);
|
|
182
|
+
const label = labelFor(field);
|
|
183
|
+
const placeholder = viewNode?.ownAttr("placeholder") as string | undefined;
|
|
184
|
+
const helpText = viewNode?.ownAttr("helpText") as string | undefined;
|
|
185
|
+
const htmlType = htmlTypeFromView(view, viewNode?.ownAttr("htmlType") as string | undefined);
|
|
186
|
+
const rules = renderFieldRules(field);
|
|
187
|
+
|
|
188
|
+
const entries: string[] = [
|
|
189
|
+
`name: ${JSON.stringify(field.name)}`,
|
|
190
|
+
`label: ${JSON.stringify(label)}`,
|
|
191
|
+
`view: ${JSON.stringify(view)}`,
|
|
192
|
+
];
|
|
193
|
+
if (htmlType !== undefined) entries.push(`htmlType: ${JSON.stringify(htmlType)}`);
|
|
194
|
+
if (placeholder !== undefined) entries.push(`placeholder: ${JSON.stringify(placeholder)}`);
|
|
195
|
+
if (helpText !== undefined) entries.push(`helpText: ${JSON.stringify(helpText)}`);
|
|
196
|
+
if (rules !== undefined) entries.push(`rules: ${rules}`);
|
|
197
|
+
|
|
198
|
+
// Currency-specific keys: only emitted for currency-subtype fields.
|
|
199
|
+
const currencyMeta = currencyMetaFor(field);
|
|
200
|
+
if (currencyMeta !== null) {
|
|
201
|
+
entries.push(`currency: ${JSON.stringify(currencyMeta.currency)}`);
|
|
202
|
+
entries.push(`locale: ${JSON.stringify(currencyMeta.locale)}`);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return ` ${field.name}: {\n ${entries.join(",\n ")},\n }`;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export function renderEntityConstants(obj: MetaObject, apiPrefix = ""): Code {
|
|
209
|
+
const entityName = obj.name;
|
|
210
|
+
const tableName = resolveTableName(obj);
|
|
211
|
+
const path = resourcePath(obj);
|
|
212
|
+
|
|
213
|
+
const fieldEntries: string[] = [];
|
|
214
|
+
// Use fields() so inherited fields (from extends:/super:) appear in constants.
|
|
215
|
+
for (const child of obj.fields()) {
|
|
216
|
+
fieldEntries.push(renderFieldEntry(child));
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const body = [
|
|
220
|
+
` $entity: ${JSON.stringify(entityName)}`,
|
|
221
|
+
` $table: ${JSON.stringify(tableName)}`,
|
|
222
|
+
` $path: ${JSON.stringify(path)}`,
|
|
223
|
+
` $apiPrefix: ${JSON.stringify(apiPrefix)}`,
|
|
224
|
+
...fieldEntries,
|
|
225
|
+
].join(",\n");
|
|
226
|
+
|
|
227
|
+
return code`
|
|
228
|
+
/**
|
|
229
|
+
* Metadata constants for ${entityName}.
|
|
230
|
+
*
|
|
231
|
+
* Use these instead of magic strings so TS catches typos and refactors stay
|
|
232
|
+
* coherent. Each non-dollar-prefixed key is a per-field object carrying
|
|
233
|
+
* name, label, view, optional htmlType/placeholder/helpText, and the
|
|
234
|
+
* RHF-shaped validation rules derived from the field's validator children.
|
|
235
|
+
*
|
|
236
|
+
* Typical usage with the metaobjects React form helper:
|
|
237
|
+
*
|
|
238
|
+
* import { useEntityForm } from '@metaobjectsdev/react';
|
|
239
|
+
* const form = useEntityForm(${entityName}, ${entityName}InsertSchema);
|
|
240
|
+
* <input {...form.input.${
|
|
241
|
+
fieldEntries[0]?.match(/^\s*(\w+):/)?.[1] ?? "fieldName"
|
|
242
|
+
}} />
|
|
243
|
+
*/
|
|
244
|
+
export const ${entityName} = {
|
|
245
|
+
${body},
|
|
246
|
+
} as const;
|
|
247
|
+
`;
|
|
248
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// Entity file composer — combines drizzle-schema, inferred-types, and zod-validators
|
|
2
|
+
// into one file with the @generated header. ts-poet deduplicates imports.
|
|
3
|
+
//
|
|
4
|
+
// Dispatch:
|
|
5
|
+
// isProjection(entity) → renderProjectionDecl (read-only: view declaration + Zod + filter sections)
|
|
6
|
+
// vanilla / write-through entity → Drizzle table path
|
|
7
|
+
|
|
8
|
+
import { joinCode, type Code } from "ts-poet";
|
|
9
|
+
import type { MetaObject } from "@metaobjectsdev/metadata";
|
|
10
|
+
import type { RenderContext } from "../render-context.js";
|
|
11
|
+
import { renderDrizzleSchema } from "./drizzle-schema.js";
|
|
12
|
+
import { renderInferredTypes } from "./inferred-types.js";
|
|
13
|
+
import { renderZodValidators } from "./zod-validators.js";
|
|
14
|
+
import { renderEntityConstants } from "./entity-constants.js";
|
|
15
|
+
import { renderFilterAllowlist, renderSortAllowlist } from "./filter-allowlist.js";
|
|
16
|
+
import { renderFilterType } from "./filter-type.js";
|
|
17
|
+
import { GENERATED_HEADER } from "../constants.js";
|
|
18
|
+
import { isProjection } from "../projection/projection-detector.js";
|
|
19
|
+
import { renderProjectionDecl } from "./projection-decl.js";
|
|
20
|
+
|
|
21
|
+
export function renderEntityFile(entity: MetaObject, ctx: RenderContext): string {
|
|
22
|
+
// --- Projection path (read-only: view-backed entity with no table source) ---
|
|
23
|
+
if (isProjection(entity)) {
|
|
24
|
+
return renderProjectionDecl(entity, ctx.loadedRoot, {
|
|
25
|
+
columnNamingStrategy: ctx.columnNamingStrategy,
|
|
26
|
+
dialect: ctx.dialect,
|
|
27
|
+
apiPrefix: ctx.apiPrefix,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// --- Vanilla / write-through entity path ---
|
|
32
|
+
const sections: Code[] = [
|
|
33
|
+
renderDrizzleSchema(entity, ctx),
|
|
34
|
+
renderInferredTypes(entity),
|
|
35
|
+
renderZodValidators(entity),
|
|
36
|
+
renderEntityConstants(entity, ctx.apiPrefix),
|
|
37
|
+
renderFilterAllowlist(entity),
|
|
38
|
+
renderSortAllowlist(entity),
|
|
39
|
+
renderFilterType(entity),
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
// Render ts-poet body first (ts-poet hoists imp()-tracked imports to the top),
|
|
43
|
+
// then prepend the @generated header so it lands at line 1 — convention for
|
|
44
|
+
// generated files and what most tooling (overwrite-policy, IDEs) expects.
|
|
45
|
+
const body = joinCode(sections, { on: "\n" }).toString();
|
|
46
|
+
const header =
|
|
47
|
+
`// ${GENERATED_HEADER} — DO NOT EDIT.\n` +
|
|
48
|
+
`// Source metadata: ${entity.name} (${entity.fqn()})\n` +
|
|
49
|
+
`// Customize via ${entity.name}.extra.ts in this directory.\n`;
|
|
50
|
+
return header + body;
|
|
51
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
// Shared field-level metadata helpers — consumed by entity-constants.ts,
|
|
2
|
+
// projection-decl.ts, and any future generator that needs per-field inference.
|
|
3
|
+
//
|
|
4
|
+
// All helpers take a MetaField node.
|
|
5
|
+
|
|
6
|
+
import { MetaField } from "@metaobjectsdev/metadata";
|
|
7
|
+
import {
|
|
8
|
+
FIELD_SUBTYPE_STRING,
|
|
9
|
+
FIELD_SUBTYPE_INT,
|
|
10
|
+
FIELD_SUBTYPE_LONG,
|
|
11
|
+
FIELD_SUBTYPE_SHORT,
|
|
12
|
+
FIELD_SUBTYPE_BYTE,
|
|
13
|
+
FIELD_SUBTYPE_DOUBLE,
|
|
14
|
+
FIELD_SUBTYPE_FLOAT,
|
|
15
|
+
FIELD_SUBTYPE_DECIMAL,
|
|
16
|
+
FIELD_SUBTYPE_BOOLEAN,
|
|
17
|
+
FIELD_SUBTYPE_DATE,
|
|
18
|
+
FIELD_SUBTYPE_TIME,
|
|
19
|
+
FIELD_SUBTYPE_TIMESTAMP,
|
|
20
|
+
FIELD_SUBTYPE_CURRENCY,
|
|
21
|
+
VIEW_SUBTYPE_TEXT,
|
|
22
|
+
VIEW_SUBTYPE_DATE,
|
|
23
|
+
VIEW_SUBTYPE_NUMBER,
|
|
24
|
+
VIEW_SUBTYPE_CHECKBOX,
|
|
25
|
+
VIEW_SUBTYPE_CURRENCY,
|
|
26
|
+
FIELD_ATTR_CURRENCY,
|
|
27
|
+
FIELD_ATTR_CURRENCY_DEFAULT,
|
|
28
|
+
VIEW_CURRENCY_ATTR_LOCALE,
|
|
29
|
+
VIEW_CURRENCY_ATTR_LOCALE_DEFAULT,
|
|
30
|
+
} from "@metaobjectsdev/metadata";
|
|
31
|
+
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
// inferViewKind
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Resolve the cell-renderer key (view kind) for a field.
|
|
38
|
+
* Explicit view child wins; field subType determines default.
|
|
39
|
+
*/
|
|
40
|
+
export function inferViewKind(field: MetaField): string {
|
|
41
|
+
// Explicit view (own or inherited via extends) has highest priority.
|
|
42
|
+
const viewChild = field.views()[0];
|
|
43
|
+
if (viewChild) return viewChild.subType;
|
|
44
|
+
// Field subtype → default view.
|
|
45
|
+
return defaultViewForSubType(field.subType);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function defaultViewForSubType(subType: string): string {
|
|
49
|
+
switch (subType) {
|
|
50
|
+
case FIELD_SUBTYPE_BOOLEAN:
|
|
51
|
+
return VIEW_SUBTYPE_CHECKBOX;
|
|
52
|
+
case FIELD_SUBTYPE_INT:
|
|
53
|
+
case FIELD_SUBTYPE_LONG:
|
|
54
|
+
case FIELD_SUBTYPE_SHORT:
|
|
55
|
+
case FIELD_SUBTYPE_BYTE:
|
|
56
|
+
case FIELD_SUBTYPE_DOUBLE:
|
|
57
|
+
case FIELD_SUBTYPE_FLOAT:
|
|
58
|
+
case FIELD_SUBTYPE_DECIMAL:
|
|
59
|
+
return VIEW_SUBTYPE_NUMBER;
|
|
60
|
+
case FIELD_SUBTYPE_DATE:
|
|
61
|
+
case FIELD_SUBTYPE_TIME:
|
|
62
|
+
case FIELD_SUBTYPE_TIMESTAMP:
|
|
63
|
+
return VIEW_SUBTYPE_DATE;
|
|
64
|
+
case FIELD_SUBTYPE_CURRENCY:
|
|
65
|
+
return VIEW_SUBTYPE_CURRENCY;
|
|
66
|
+
default:
|
|
67
|
+
return VIEW_SUBTYPE_TEXT;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
// zodTypeFor
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Resolve the Zod validator expression for a field's storage type.
|
|
77
|
+
*/
|
|
78
|
+
export function zodTypeFor(field: MetaField): string {
|
|
79
|
+
switch (field.subType) {
|
|
80
|
+
case FIELD_SUBTYPE_STRING:
|
|
81
|
+
return "z.string()";
|
|
82
|
+
case FIELD_SUBTYPE_BOOLEAN:
|
|
83
|
+
return "z.boolean()";
|
|
84
|
+
case FIELD_SUBTYPE_DATE:
|
|
85
|
+
case FIELD_SUBTYPE_TIME:
|
|
86
|
+
case FIELD_SUBTYPE_TIMESTAMP:
|
|
87
|
+
// Returned as ISO strings from SQLite/Postgres drivers.
|
|
88
|
+
return "z.string()";
|
|
89
|
+
case FIELD_SUBTYPE_INT:
|
|
90
|
+
case FIELD_SUBTYPE_LONG:
|
|
91
|
+
case FIELD_SUBTYPE_SHORT:
|
|
92
|
+
case FIELD_SUBTYPE_BYTE:
|
|
93
|
+
case FIELD_SUBTYPE_CURRENCY:
|
|
94
|
+
return "z.number().int()";
|
|
95
|
+
case FIELD_SUBTYPE_DOUBLE:
|
|
96
|
+
case FIELD_SUBTYPE_FLOAT:
|
|
97
|
+
case FIELD_SUBTYPE_DECIMAL:
|
|
98
|
+
return "z.number()";
|
|
99
|
+
default:
|
|
100
|
+
return "z.unknown()";
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ---------------------------------------------------------------------------
|
|
105
|
+
// currencyMetaFor
|
|
106
|
+
// ---------------------------------------------------------------------------
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Resolve currency code + locale for a currency-subtype field.
|
|
110
|
+
* Returns null for non-currency fields.
|
|
111
|
+
*/
|
|
112
|
+
export function currencyMetaFor(field: MetaField): { currency: string; locale: string } | null {
|
|
113
|
+
if (field.subType !== FIELD_SUBTYPE_CURRENCY) return null;
|
|
114
|
+
const currency =
|
|
115
|
+
(field.ownAttr(FIELD_ATTR_CURRENCY) as string | undefined) ?? FIELD_ATTR_CURRENCY_DEFAULT;
|
|
116
|
+
const viewChild = field.views().find((c) => c.subType === VIEW_SUBTYPE_CURRENCY);
|
|
117
|
+
const locale =
|
|
118
|
+
(viewChild?.ownAttr(VIEW_CURRENCY_ATTR_LOCALE) as string | undefined) ??
|
|
119
|
+
VIEW_CURRENCY_ATTR_LOCALE_DEFAULT;
|
|
120
|
+
return { currency, locale };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// ---------------------------------------------------------------------------
|
|
124
|
+
// labelFor
|
|
125
|
+
// ---------------------------------------------------------------------------
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Resolve the human-readable label for a field.
|
|
129
|
+
* Uses @label attr on a view child if present; otherwise humanizes the field name.
|
|
130
|
+
*/
|
|
131
|
+
export function labelFor(field: MetaField): string {
|
|
132
|
+
for (const child of field.views()) {
|
|
133
|
+
const label = child.ownAttr("label");
|
|
134
|
+
if (typeof label === "string" && label.length > 0) return label;
|
|
135
|
+
}
|
|
136
|
+
return humanize(field.name);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// ---------------------------------------------------------------------------
|
|
140
|
+
// Internal
|
|
141
|
+
// ---------------------------------------------------------------------------
|
|
142
|
+
|
|
143
|
+
/** Convert a camelCase or PascalCase field name to a human-friendly label. */
|
|
144
|
+
function humanize(s: string): string {
|
|
145
|
+
return s
|
|
146
|
+
.replace(/([a-z])([A-Z])/g, "$1 $2")
|
|
147
|
+
.replace(/_/g, " ")
|
|
148
|
+
.replace(/\b\w/g, (c) => c.toUpperCase())
|
|
149
|
+
.trim();
|
|
150
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { code, type Code } from "ts-poet";
|
|
2
|
+
import { MetaField, MetaObject } from "@metaobjectsdev/metadata";
|
|
3
|
+
import {
|
|
4
|
+
FIELD_ATTR_FILTERABLE,
|
|
5
|
+
FIELD_ATTR_SORTABLE_DEFAULT_ORDER,
|
|
6
|
+
FIELD_SUBTYPE_BOOLEAN,
|
|
7
|
+
FIELD_SUBTYPE_INT,
|
|
8
|
+
FIELD_SUBTYPE_SHORT,
|
|
9
|
+
FIELD_SUBTYPE_BYTE,
|
|
10
|
+
FIELD_SUBTYPE_LONG,
|
|
11
|
+
FIELD_SUBTYPE_DOUBLE,
|
|
12
|
+
FIELD_SUBTYPE_FLOAT,
|
|
13
|
+
FIELD_SUBTYPE_DECIMAL,
|
|
14
|
+
FIELD_SUBTYPE_DATE,
|
|
15
|
+
FIELD_SUBTYPE_TIME,
|
|
16
|
+
FIELD_SUBTYPE_TIMESTAMP,
|
|
17
|
+
opsForSubType,
|
|
18
|
+
} from "@metaobjectsdev/metadata";
|
|
19
|
+
import { sortableFields } from "./filter-shared.js";
|
|
20
|
+
|
|
21
|
+
const NUMBER_SUBTYPES = new Set<string>([
|
|
22
|
+
FIELD_SUBTYPE_INT,
|
|
23
|
+
FIELD_SUBTYPE_SHORT,
|
|
24
|
+
FIELD_SUBTYPE_BYTE,
|
|
25
|
+
FIELD_SUBTYPE_LONG,
|
|
26
|
+
FIELD_SUBTYPE_DOUBLE,
|
|
27
|
+
FIELD_SUBTYPE_FLOAT,
|
|
28
|
+
FIELD_SUBTYPE_DECIMAL,
|
|
29
|
+
]);
|
|
30
|
+
|
|
31
|
+
const DATETIME_SUBTYPES = new Set<string>([
|
|
32
|
+
FIELD_SUBTYPE_DATE,
|
|
33
|
+
FIELD_SUBTYPE_TIME,
|
|
34
|
+
FIELD_SUBTYPE_TIMESTAMP,
|
|
35
|
+
]);
|
|
36
|
+
|
|
37
|
+
/** Maps a field subtype to the FilterSubType category used in generated FilterAllowlist. */
|
|
38
|
+
function filterSubTypeFor(fieldSubType: string): "string" | "number" | "boolean" | "datetime" {
|
|
39
|
+
if (fieldSubType === FIELD_SUBTYPE_BOOLEAN) return "boolean";
|
|
40
|
+
if (NUMBER_SUBTYPES.has(fieldSubType)) return "number";
|
|
41
|
+
if (DATETIME_SUBTYPES.has(fieldSubType)) return "datetime";
|
|
42
|
+
return "string";
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function filterableFields(entity: MetaObject): MetaField[] {
|
|
46
|
+
// fields() returns effective fields, so inherited fields (from extends:/super:) are included in allowlists.
|
|
47
|
+
return entity.fields().filter((c) => c.ownAttr(FIELD_ATTR_FILTERABLE) === true);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function renderFilterAllowlist(entity: MetaObject): Code {
|
|
51
|
+
const fields = filterableFields(entity);
|
|
52
|
+
if (fields.length === 0) {
|
|
53
|
+
return code`
|
|
54
|
+
import type { FilterAllowlist } from "@metaobjectsdev/runtime-ts/drizzle-fastify";
|
|
55
|
+
|
|
56
|
+
export const ${entity.name}FilterAllowlist = {} as const satisfies FilterAllowlist;
|
|
57
|
+
`;
|
|
58
|
+
}
|
|
59
|
+
const rows = fields
|
|
60
|
+
.map((f) => {
|
|
61
|
+
const ops = opsForSubType(f.subType).map((o) => JSON.stringify(o)).join(", ");
|
|
62
|
+
const sub = filterSubTypeFor(f.subType);
|
|
63
|
+
return ` ${f.name}: { ops: [${ops}] as const, subType: ${JSON.stringify(sub)} as const, leadingWildcard: false }`;
|
|
64
|
+
})
|
|
65
|
+
.join(",\n");
|
|
66
|
+
return code`
|
|
67
|
+
import type { FilterAllowlist } from "@metaobjectsdev/runtime-ts/drizzle-fastify";
|
|
68
|
+
|
|
69
|
+
export const ${entity.name}FilterAllowlist = {
|
|
70
|
+
${rows}
|
|
71
|
+
} as const satisfies FilterAllowlist;
|
|
72
|
+
`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function renderSortAllowlist(entity: MetaObject): Code {
|
|
76
|
+
// Sortable = explicit @sortable === true, OR (no @sortable AND @filterable === true).
|
|
77
|
+
// @sortable: false explicitly opts out.
|
|
78
|
+
// Uses shared isSortableField predicate — must stay in sync with renderFilterType.
|
|
79
|
+
const sortable = sortableFields(entity);
|
|
80
|
+
if (sortable.length === 0) {
|
|
81
|
+
return code`
|
|
82
|
+
import type { SortAllowlist } from "@metaobjectsdev/runtime-ts/drizzle-fastify";
|
|
83
|
+
|
|
84
|
+
export const ${entity.name}SortAllowlist = {} as const satisfies SortAllowlist;
|
|
85
|
+
`;
|
|
86
|
+
}
|
|
87
|
+
const rows = sortable
|
|
88
|
+
.map((f) => {
|
|
89
|
+
const defaultOrder = f.ownAttr(FIELD_ATTR_SORTABLE_DEFAULT_ORDER) as string | undefined;
|
|
90
|
+
const rule =
|
|
91
|
+
defaultOrder === "asc" || defaultOrder === "desc"
|
|
92
|
+
? `{ defaultOrder: ${JSON.stringify(defaultOrder)} as const }`
|
|
93
|
+
: `{}`;
|
|
94
|
+
return ` ${f.name}: ${rule}`;
|
|
95
|
+
})
|
|
96
|
+
.join(",\n");
|
|
97
|
+
return code`
|
|
98
|
+
import type { SortAllowlist } from "@metaobjectsdev/runtime-ts/drizzle-fastify";
|
|
99
|
+
|
|
100
|
+
export const ${entity.name}SortAllowlist = {
|
|
101
|
+
${rows}
|
|
102
|
+
} as const satisfies SortAllowlist;
|
|
103
|
+
`;
|
|
104
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// Shared predicate for "is this field sortable?" — used by both filter-allowlist.ts
|
|
2
|
+
// (SortAllowlist generation) and filter-type.ts (sort union generation).
|
|
3
|
+
// Both must agree on which fields are sortable; keeping them in sync via this shared
|
|
4
|
+
// helper prevents client/server mismatches.
|
|
5
|
+
|
|
6
|
+
import { MetaField, MetaObject } from "@metaobjectsdev/metadata";
|
|
7
|
+
import { FIELD_ATTR_FILTERABLE, FIELD_ATTR_SORTABLE } from "@metaobjectsdev/metadata";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Returns true if the given field should be included in sort operations.
|
|
11
|
+
*
|
|
12
|
+
* Rules (in priority order):
|
|
13
|
+
* 1. @sortable: true → always sortable (even without @filterable)
|
|
14
|
+
* 2. @sortable: false → never sortable (overrides @filterable)
|
|
15
|
+
* 3. no @sortable → sortable iff @filterable === true
|
|
16
|
+
*/
|
|
17
|
+
export function isSortableField(field: MetaField): boolean {
|
|
18
|
+
const sortableAttr = field.ownAttr(FIELD_ATTR_SORTABLE);
|
|
19
|
+
if (sortableAttr === true) return true;
|
|
20
|
+
if (sortableAttr === false) return false;
|
|
21
|
+
return field.ownAttr(FIELD_ATTR_FILTERABLE) === true;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Returns all sortable field children of the given entity.
|
|
26
|
+
*/
|
|
27
|
+
export function sortableFields(entity: MetaObject): MetaField[] {
|
|
28
|
+
// fields() returns effective fields, so inherited fields (from extends:/super:) are included in sort ops.
|
|
29
|
+
return entity.fields().filter(isSortableField);
|
|
30
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
// Filter type template — emits <Entity>Filter type with subtype-gated operators.
|
|
2
|
+
// String fields get eq/ne/in/like; numbers get eq/ne/gt/gte/lt/lte/in; booleans get eq/isNull only;
|
|
3
|
+
// datetimes get eq/ne/gt/gte/lt/lte/in.
|
|
4
|
+
|
|
5
|
+
import { code, type Code } from "ts-poet";
|
|
6
|
+
import { MetaField, MetaObject } from "@metaobjectsdev/metadata";
|
|
7
|
+
import {
|
|
8
|
+
FIELD_ATTR_FILTERABLE,
|
|
9
|
+
FIELD_SUBTYPE_BOOLEAN,
|
|
10
|
+
FIELD_SUBTYPE_INT,
|
|
11
|
+
FIELD_SUBTYPE_SHORT,
|
|
12
|
+
FIELD_SUBTYPE_BYTE,
|
|
13
|
+
FIELD_SUBTYPE_LONG,
|
|
14
|
+
FIELD_SUBTYPE_DOUBLE,
|
|
15
|
+
FIELD_SUBTYPE_FLOAT,
|
|
16
|
+
FIELD_SUBTYPE_DECIMAL,
|
|
17
|
+
opsForSubType,
|
|
18
|
+
} from "@metaobjectsdev/metadata";
|
|
19
|
+
import { isSortableField } from "./filter-shared.js";
|
|
20
|
+
|
|
21
|
+
const NUMBER_SUBTYPES = new Set<string>([
|
|
22
|
+
FIELD_SUBTYPE_INT,
|
|
23
|
+
FIELD_SUBTYPE_SHORT,
|
|
24
|
+
FIELD_SUBTYPE_BYTE,
|
|
25
|
+
FIELD_SUBTYPE_LONG,
|
|
26
|
+
FIELD_SUBTYPE_DOUBLE,
|
|
27
|
+
FIELD_SUBTYPE_FLOAT,
|
|
28
|
+
FIELD_SUBTYPE_DECIMAL,
|
|
29
|
+
]);
|
|
30
|
+
|
|
31
|
+
/** Maps a field subtype to its TS value type name for operator union codegen. */
|
|
32
|
+
function tsNameFor(fieldSubType: string): string {
|
|
33
|
+
if (fieldSubType === FIELD_SUBTYPE_BOOLEAN) return "boolean";
|
|
34
|
+
if (NUMBER_SUBTYPES.has(fieldSubType)) return "number";
|
|
35
|
+
return "string";
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function renderFieldUnion(field: MetaField): string {
|
|
39
|
+
const ops = opsForSubType(field.subType);
|
|
40
|
+
const tsName = tsNameFor(field.subType);
|
|
41
|
+
const opEntries = ops.map((op) => {
|
|
42
|
+
if (op === "in") return `in?: ${tsName}[]`;
|
|
43
|
+
if (op === "isNull") return `isNull?: boolean`;
|
|
44
|
+
if (op === "like") return `like?: string`;
|
|
45
|
+
return `${op}?: ${tsName}`;
|
|
46
|
+
});
|
|
47
|
+
return `${tsName} | { ${opEntries.join("; ")} }`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function renderFilterType(entity: MetaObject): Code {
|
|
51
|
+
// fields() returns effective fields, so inherited fields (from extends:/super:) are included in filter types.
|
|
52
|
+
const allFields = entity.fields();
|
|
53
|
+
const filterableFieldsList = allFields.filter((c) => c.ownAttr(FIELD_ATTR_FILTERABLE) === true);
|
|
54
|
+
// Sort union uses isSortableField — same predicate as renderSortAllowlist to prevent
|
|
55
|
+
// client/server mismatches (@filterable: true + @sortable: false must be excluded from both).
|
|
56
|
+
const sortFieldNames = allFields.filter(isSortableField).map((f) => `"${f.name}"`);
|
|
57
|
+
|
|
58
|
+
const fieldLines: string[] = [];
|
|
59
|
+
for (const f of filterableFieldsList) {
|
|
60
|
+
fieldLines.push(` ${f.name}?: ${renderFieldUnion(f)};`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (fieldLines.length === 0 && sortFieldNames.length === 0) {
|
|
64
|
+
return code`
|
|
65
|
+
export type ${entity.name}Filter = {
|
|
66
|
+
limit?: number;
|
|
67
|
+
offset?: number;
|
|
68
|
+
sort?: string;
|
|
69
|
+
or?: ${entity.name}Filter[];
|
|
70
|
+
and?: ${entity.name}Filter[];
|
|
71
|
+
};
|
|
72
|
+
`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const sortType =
|
|
76
|
+
sortFieldNames.length === 0
|
|
77
|
+
? `string`
|
|
78
|
+
: (() => {
|
|
79
|
+
const sortUnion = sortFieldNames.join(" | ");
|
|
80
|
+
return `\`\${${sortUnion}}:\${"asc" | "desc"}\` | ${sortUnion}`;
|
|
81
|
+
})();
|
|
82
|
+
|
|
83
|
+
return code`
|
|
84
|
+
export type ${entity.name}Filter = {
|
|
85
|
+
limit?: number;
|
|
86
|
+
offset?: number;
|
|
87
|
+
sort?: ${sortType};
|
|
88
|
+
${fieldLines.join("\n")}
|
|
89
|
+
or?: ${entity.name}Filter[];
|
|
90
|
+
and?: ${entity.name}Filter[];
|
|
91
|
+
};
|
|
92
|
+
`;
|
|
93
|
+
}
|