@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,372 @@
|
|
|
1
|
+
import {
|
|
2
|
+
TYPE_FIELD,
|
|
3
|
+
TYPE_ORIGIN,
|
|
4
|
+
TYPE_RELATIONSHIP,
|
|
5
|
+
TYPE_SOURCE,
|
|
6
|
+
SOURCE_SUBTYPE_DB_VIEW,
|
|
7
|
+
SOURCE_DB_VIEW_ATTR_NAME,
|
|
8
|
+
ORIGIN_SUBTYPE_PASSTHROUGH,
|
|
9
|
+
ORIGIN_SUBTYPE_AGGREGATE,
|
|
10
|
+
ORIGIN_PASSTHROUGH_ATTR_FROM,
|
|
11
|
+
ORIGIN_PASSTHROUGH_ATTR_VIA,
|
|
12
|
+
ORIGIN_AGGREGATE_ATTR_AGG,
|
|
13
|
+
ORIGIN_AGGREGATE_ATTR_OF,
|
|
14
|
+
ORIGIN_AGGREGATE_ATTR_VIA,
|
|
15
|
+
RELATIONSHIP_ATTR_OBJECT_REF,
|
|
16
|
+
RELATIONSHIP_ATTR_CARDINALITY,
|
|
17
|
+
CARDINALITY_ONE,
|
|
18
|
+
FIELD_ATTR_DB_COLUMN,
|
|
19
|
+
findReferenceBetween,
|
|
20
|
+
type AggregateFunction,
|
|
21
|
+
} from "@metaobjectsdev/metadata";
|
|
22
|
+
import { type MetaData, type MetaRoot, MetaObject } from "@metaobjectsdev/metadata";
|
|
23
|
+
import {
|
|
24
|
+
columnNameFromField,
|
|
25
|
+
viewNameFromProjection,
|
|
26
|
+
} from "../naming.js";
|
|
27
|
+
import type { ColumnNamingStrategy } from "../metaobjects-config.js";
|
|
28
|
+
import type {
|
|
29
|
+
JoinNode, JoinTree, SelectColumn, SelectSpec, ViewSpec,
|
|
30
|
+
} from "./view-spec.js";
|
|
31
|
+
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
// Public context type
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
export interface ExtractContext {
|
|
37
|
+
readonly columnNamingStrategy: ColumnNamingStrategy;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
// Private helpers
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
|
|
44
|
+
function findRelationship(obj: MetaData, name: string): MetaData | undefined {
|
|
45
|
+
return obj.ownChildren().find(
|
|
46
|
+
(c) => c.type === TYPE_RELATIONSHIP && c.name === name,
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function viewName(projection: MetaObject, ctx: ExtractContext): string {
|
|
51
|
+
const dbView = projection.ownChildren().find(
|
|
52
|
+
(c) => c.type === TYPE_SOURCE && c.subType === SOURCE_SUBTYPE_DB_VIEW,
|
|
53
|
+
);
|
|
54
|
+
const explicit = dbView?.ownAttr(SOURCE_DB_VIEW_ATTR_NAME) as string | undefined;
|
|
55
|
+
return explicit ?? viewNameFromProjection(projection.name, ctx.columnNamingStrategy);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function baseEntityFor(
|
|
59
|
+
projection: MetaObject,
|
|
60
|
+
root: MetaRoot,
|
|
61
|
+
): MetaObject {
|
|
62
|
+
// v1: base entity is the resolved super (set via `extends:` in metadata).
|
|
63
|
+
const superModel = projection.superResolved;
|
|
64
|
+
const superName = superModel?.name ?? projection.superRef;
|
|
65
|
+
if (!superName) {
|
|
66
|
+
throw new Error(
|
|
67
|
+
`Projection ${projection.name}: missing extends — projections must extend a writable entity in v1.`,
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
const base =
|
|
71
|
+
superModel instanceof MetaObject ? superModel : root.findObject(superName);
|
|
72
|
+
if (!base) {
|
|
73
|
+
throw new Error(
|
|
74
|
+
`Projection ${projection.name}: extends "${superName}" does not resolve to any entity.`,
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
return base;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function sourceColumnNameFor(
|
|
81
|
+
entityField: MetaData,
|
|
82
|
+
ctx: ExtractContext,
|
|
83
|
+
): string {
|
|
84
|
+
const explicit = entityField.ownAttr(FIELD_ATTR_DB_COLUMN) as string | undefined;
|
|
85
|
+
return explicit ?? columnNameFromField(entityField.name, ctx.columnNamingStrategy);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function shortAliasFor(entityName: string, used: Set<string>): string {
|
|
89
|
+
const base = (entityName[0] ?? "x").toLowerCase();
|
|
90
|
+
if (!used.has(base)) { used.add(base); return base; }
|
|
91
|
+
let i = 0;
|
|
92
|
+
let candidate: string;
|
|
93
|
+
do { candidate = `${base}${i}`; i++; } while (used.has(candidate));
|
|
94
|
+
used.add(candidate);
|
|
95
|
+
return candidate;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ---------------------------------------------------------------------------
|
|
99
|
+
// JoinTree builder — walks all `@via` paths from origin children, dedupes via
|
|
100
|
+
// prefix into a trie, then converts to JoinNode tree.
|
|
101
|
+
// ---------------------------------------------------------------------------
|
|
102
|
+
|
|
103
|
+
interface PathStep {
|
|
104
|
+
entity: MetaData;
|
|
105
|
+
relationship: string;
|
|
106
|
+
cardinality: "one" | "many";
|
|
107
|
+
fkField: string;
|
|
108
|
+
pkField: string;
|
|
109
|
+
referenceHolder: "source" | "target";
|
|
110
|
+
targetEntity: string;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
type Path = PathStep[];
|
|
114
|
+
|
|
115
|
+
interface TrieNode {
|
|
116
|
+
children: Map<string, TrieNode>;
|
|
117
|
+
step?: PathStep;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function buildJoinTree(
|
|
121
|
+
projection: MetaObject,
|
|
122
|
+
base: MetaObject,
|
|
123
|
+
root: MetaRoot,
|
|
124
|
+
usedAliases: Set<string>,
|
|
125
|
+
baseAlias: string,
|
|
126
|
+
): JoinTree {
|
|
127
|
+
const allPaths: Path[] = [];
|
|
128
|
+
|
|
129
|
+
for (const field of projection.ownChildren()) {
|
|
130
|
+
if (field.type !== TYPE_FIELD) continue;
|
|
131
|
+
for (const origin of field.ownChildren()) {
|
|
132
|
+
if (origin.type !== TYPE_ORIGIN) continue;
|
|
133
|
+
const viaAttr = origin.subType === ORIGIN_SUBTYPE_AGGREGATE
|
|
134
|
+
? (origin.ownAttr(ORIGIN_AGGREGATE_ATTR_VIA) as string | undefined)
|
|
135
|
+
: (origin.ownAttr(ORIGIN_PASSTHROUGH_ATTR_VIA) as string | undefined);
|
|
136
|
+
if (!viaAttr) continue;
|
|
137
|
+
|
|
138
|
+
const segments = viaAttr.split(".");
|
|
139
|
+
const entityName = segments[0];
|
|
140
|
+
const relSegments = segments.slice(1);
|
|
141
|
+
if (!entityName) continue;
|
|
142
|
+
let currentObj = root.findObject(entityName);
|
|
143
|
+
if (!currentObj) continue;
|
|
144
|
+
|
|
145
|
+
const path: Path = [];
|
|
146
|
+
for (const relName of relSegments) {
|
|
147
|
+
const rel = findRelationship(currentObj, relName);
|
|
148
|
+
if (!rel) break;
|
|
149
|
+
const targetName = rel.ownAttr(RELATIONSHIP_ATTR_OBJECT_REF) as string | undefined;
|
|
150
|
+
const target = targetName ? root.findObject(targetName) : undefined;
|
|
151
|
+
if (!target || !targetName) break;
|
|
152
|
+
|
|
153
|
+
const cardAttr = rel.ownAttr(RELATIONSHIP_ATTR_CARDINALITY) as string | undefined;
|
|
154
|
+
const cardinality: "one" | "many" = cardAttr === CARDINALITY_ONE ? "one" : "many";
|
|
155
|
+
|
|
156
|
+
const ref = findReferenceBetween(currentObj as MetaObject, target);
|
|
157
|
+
if (!ref) break;
|
|
158
|
+
|
|
159
|
+
const fkField = ref.referenceIdentity.fields[0];
|
|
160
|
+
if (!fkField) break;
|
|
161
|
+
|
|
162
|
+
const resolvedPkField = ref.referenceIdentity.resolvedTargetPkField(root) ?? "id";
|
|
163
|
+
|
|
164
|
+
const referenceHolder: "source" | "target" =
|
|
165
|
+
ref.holder.name === currentObj.name ? "source" : "target";
|
|
166
|
+
|
|
167
|
+
path.push({
|
|
168
|
+
entity: currentObj,
|
|
169
|
+
relationship: relName,
|
|
170
|
+
cardinality,
|
|
171
|
+
fkField,
|
|
172
|
+
pkField: resolvedPkField,
|
|
173
|
+
referenceHolder,
|
|
174
|
+
targetEntity: targetName,
|
|
175
|
+
});
|
|
176
|
+
currentObj = target;
|
|
177
|
+
}
|
|
178
|
+
if (path.length > 0) allPaths.push(path);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Dedupe by prefix: paths sharing a prefix collapse into one join branch.
|
|
183
|
+
const trieRoot: TrieNode = { children: new Map() };
|
|
184
|
+
for (const path of allPaths) {
|
|
185
|
+
let node = trieRoot;
|
|
186
|
+
for (const step of path) {
|
|
187
|
+
let child = node.children.get(step.relationship);
|
|
188
|
+
if (!child) {
|
|
189
|
+
child = { children: new Map(), step };
|
|
190
|
+
node.children.set(step.relationship, child);
|
|
191
|
+
}
|
|
192
|
+
node = child;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function toJoinNode(node: TrieNode): JoinNode {
|
|
197
|
+
const step = node.step!;
|
|
198
|
+
return {
|
|
199
|
+
relationship: step.relationship,
|
|
200
|
+
targetEntity: step.targetEntity,
|
|
201
|
+
alias: shortAliasFor(step.targetEntity, usedAliases),
|
|
202
|
+
cardinality: step.cardinality,
|
|
203
|
+
fkField: step.fkField,
|
|
204
|
+
pkField: step.pkField,
|
|
205
|
+
referenceHolder: step.referenceHolder,
|
|
206
|
+
children: Array.from(node.children.values()).map(toJoinNode),
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return {
|
|
211
|
+
baseEntity: base.name,
|
|
212
|
+
baseAlias,
|
|
213
|
+
joins: Array.from(trieRoot.children.values()).map(toJoinNode),
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// ---------------------------------------------------------------------------
|
|
218
|
+
// Helpers for SelectSpec building
|
|
219
|
+
// ---------------------------------------------------------------------------
|
|
220
|
+
|
|
221
|
+
function findAliasInTree(
|
|
222
|
+
joinTree: JoinTree,
|
|
223
|
+
entityName: string,
|
|
224
|
+
): string | undefined {
|
|
225
|
+
if (joinTree.baseEntity === entityName) return joinTree.baseAlias;
|
|
226
|
+
|
|
227
|
+
function recurse(nodes: readonly JoinNode[]): string | undefined {
|
|
228
|
+
for (const n of nodes) {
|
|
229
|
+
if (n.targetEntity === entityName) return n.alias;
|
|
230
|
+
const found = recurse(n.children);
|
|
231
|
+
if (found !== undefined) return found;
|
|
232
|
+
}
|
|
233
|
+
return undefined;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return recurse(joinTree.joins);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function buildSelectSpec(
|
|
240
|
+
projection: MetaObject,
|
|
241
|
+
base: MetaObject,
|
|
242
|
+
joinTree: JoinTree,
|
|
243
|
+
root: MetaRoot,
|
|
244
|
+
ctx: ExtractContext,
|
|
245
|
+
): SelectSpec {
|
|
246
|
+
const columns: SelectColumn[] = [];
|
|
247
|
+
|
|
248
|
+
// Inherited fields from extends parent — emit as passthrough on baseAlias.
|
|
249
|
+
// Skip fields that the projection has overridden with an explicit origin.
|
|
250
|
+
// fields() is effective-by-default, so multi-level inheritance (base → BaseEntity) works.
|
|
251
|
+
for (const baseField of base.fields()) {
|
|
252
|
+
const overridden = projection.ownChildren().find(
|
|
253
|
+
(c) => c.type === TYPE_FIELD && c.name === baseField.name,
|
|
254
|
+
);
|
|
255
|
+
if (overridden) continue;
|
|
256
|
+
columns.push({
|
|
257
|
+
kind: "passthrough",
|
|
258
|
+
fieldName: baseField.name,
|
|
259
|
+
dbColAlias: sourceColumnNameFor(baseField, ctx),
|
|
260
|
+
sourceAlias: joinTree.baseAlias,
|
|
261
|
+
sourceColumn: sourceColumnNameFor(baseField, ctx),
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Fields explicitly declared on the projection.
|
|
266
|
+
for (const field of projection.ownChildren()) {
|
|
267
|
+
if (field.type !== TYPE_FIELD) continue;
|
|
268
|
+
const origin = field.ownChildren().find((c) => c.type === TYPE_ORIGIN);
|
|
269
|
+
const dbCol = sourceColumnNameFor(field, ctx);
|
|
270
|
+
|
|
271
|
+
if (!origin) {
|
|
272
|
+
// Declared on projection but no origin — passthrough from base table.
|
|
273
|
+
columns.push({
|
|
274
|
+
kind: "passthrough",
|
|
275
|
+
fieldName: field.name,
|
|
276
|
+
dbColAlias: dbCol,
|
|
277
|
+
sourceAlias: joinTree.baseAlias,
|
|
278
|
+
sourceColumn: dbCol,
|
|
279
|
+
});
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (origin.subType === ORIGIN_SUBTYPE_PASSTHROUGH) {
|
|
284
|
+
const from = origin.ownAttr(ORIGIN_PASSTHROUGH_ATTR_FROM) as string;
|
|
285
|
+
const dotIdx = from.indexOf(".");
|
|
286
|
+
if (dotIdx < 1) continue;
|
|
287
|
+
const entityName = from.slice(0, dotIdx);
|
|
288
|
+
const fieldName = from.slice(dotIdx + 1);
|
|
289
|
+
const targetEntity = root.findObject(entityName);
|
|
290
|
+
const sourceAlias = findAliasInTree(joinTree, entityName);
|
|
291
|
+
if (!targetEntity || sourceAlias === undefined) continue;
|
|
292
|
+
const targetField = targetEntity.ownChildren().find(
|
|
293
|
+
(c) => c.type === TYPE_FIELD && c.name === fieldName,
|
|
294
|
+
);
|
|
295
|
+
if (!targetField) continue;
|
|
296
|
+
columns.push({
|
|
297
|
+
kind: "passthrough",
|
|
298
|
+
fieldName: field.name,
|
|
299
|
+
dbColAlias: dbCol,
|
|
300
|
+
sourceAlias,
|
|
301
|
+
sourceColumn: sourceColumnNameFor(targetField, ctx),
|
|
302
|
+
});
|
|
303
|
+
} else if (origin.subType === ORIGIN_SUBTYPE_AGGREGATE) {
|
|
304
|
+
const agg = origin.ownAttr(ORIGIN_AGGREGATE_ATTR_AGG) as AggregateFunction;
|
|
305
|
+
const of_ = origin.ownAttr(ORIGIN_AGGREGATE_ATTR_OF) as string;
|
|
306
|
+
if (!agg || !of_) continue;
|
|
307
|
+
const dotIdx = of_.indexOf(".");
|
|
308
|
+
if (dotIdx < 1) continue;
|
|
309
|
+
const entityName = of_.slice(0, dotIdx);
|
|
310
|
+
const fieldName = of_.slice(dotIdx + 1);
|
|
311
|
+
const targetEntity = root.findObject(entityName);
|
|
312
|
+
const sourceAlias = findAliasInTree(joinTree, entityName);
|
|
313
|
+
if (!targetEntity || sourceAlias === undefined) continue;
|
|
314
|
+
const targetField = targetEntity.ownChildren().find(
|
|
315
|
+
(c) => c.type === TYPE_FIELD && c.name === fieldName,
|
|
316
|
+
);
|
|
317
|
+
if (!targetField) continue;
|
|
318
|
+
columns.push({
|
|
319
|
+
kind: "aggregate",
|
|
320
|
+
fieldName: field.name,
|
|
321
|
+
dbColAlias: dbCol,
|
|
322
|
+
agg,
|
|
323
|
+
sourceAlias,
|
|
324
|
+
sourceColumn: sourceColumnNameFor(targetField, ctx),
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
return { columns };
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function buildGroupBy(spec: SelectSpec): string[] {
|
|
333
|
+
const hasAgg = spec.columns.some((c) => c.kind === "aggregate");
|
|
334
|
+
if (!hasAgg) return [];
|
|
335
|
+
return spec.columns
|
|
336
|
+
.filter((c) => c.kind === "passthrough")
|
|
337
|
+
.map((c) => `${c.sourceAlias}.${c.sourceColumn}`);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// ---------------------------------------------------------------------------
|
|
341
|
+
// Public API
|
|
342
|
+
// ---------------------------------------------------------------------------
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Walk a projection's origin children to produce a ViewSpec.
|
|
346
|
+
*
|
|
347
|
+
* @param projection The projection entity (has a source[dbView] child
|
|
348
|
+
* and extends a writable entity).
|
|
349
|
+
* @param root The loader's MetaRoot — all top-level objects are
|
|
350
|
+
* direct children of root (returned by `MetaDataLoader.load()`
|
|
351
|
+
* / `FileMetaDataLoader.loadFiles()` as `result.root`).
|
|
352
|
+
* @param ctx Column naming strategy for SQL identifiers.
|
|
353
|
+
*/
|
|
354
|
+
export function extractViewSpec(
|
|
355
|
+
projection: MetaObject,
|
|
356
|
+
root: MetaRoot,
|
|
357
|
+
ctx: ExtractContext,
|
|
358
|
+
): ViewSpec {
|
|
359
|
+
const base = baseEntityFor(projection, root);
|
|
360
|
+
const usedAliases = new Set<string>();
|
|
361
|
+
const baseAlias = shortAliasFor(base.name, usedAliases);
|
|
362
|
+
const joinTree = buildJoinTree(projection, base, root, usedAliases, baseAlias);
|
|
363
|
+
const selectSpec = buildSelectSpec(projection, base, joinTree, root, ctx);
|
|
364
|
+
const groupBy = buildGroupBy(selectSpec);
|
|
365
|
+
|
|
366
|
+
return {
|
|
367
|
+
viewName: viewName(projection, ctx),
|
|
368
|
+
joinTree,
|
|
369
|
+
selectSpec,
|
|
370
|
+
groupBy,
|
|
371
|
+
};
|
|
372
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import {
|
|
2
|
+
TYPE_SOURCE,
|
|
3
|
+
SOURCE_SUBTYPE_DB_TABLE,
|
|
4
|
+
SOURCE_SUBTYPE_DB_VIEW,
|
|
5
|
+
} from "@metaobjectsdev/metadata";
|
|
6
|
+
import type { MetaData } from "@metaobjectsdev/metadata";
|
|
7
|
+
|
|
8
|
+
function hasSource(entity: MetaData, subType: string): boolean {
|
|
9
|
+
return entity.ownChildren().some(
|
|
10
|
+
(c) => c.type === TYPE_SOURCE && c.subType === subType,
|
|
11
|
+
);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function isProjection(entity: MetaData): boolean {
|
|
15
|
+
return (
|
|
16
|
+
hasSource(entity, SOURCE_SUBTYPE_DB_VIEW) &&
|
|
17
|
+
!hasSource(entity, SOURCE_SUBTYPE_DB_TABLE)
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function isWriteThrough(entity: MetaData): boolean {
|
|
22
|
+
return (
|
|
23
|
+
hasSource(entity, SOURCE_SUBTYPE_DB_VIEW) &&
|
|
24
|
+
hasSource(entity, SOURCE_SUBTYPE_DB_TABLE)
|
|
25
|
+
);
|
|
26
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { toSnakeCase } from "../naming.js";
|
|
2
|
+
import type { JoinNode, ViewSpec } from "./view-spec.js";
|
|
3
|
+
|
|
4
|
+
export interface EmitOptions {
|
|
5
|
+
readonly dialect: "postgres" | "sqlite";
|
|
6
|
+
/** Resolved table name for the JoinTree's base entity. */
|
|
7
|
+
readonly baseTableName: string;
|
|
8
|
+
/** Map from entity name → table name for every entity referenced in joins. */
|
|
9
|
+
readonly joinTables: Readonly<Record<string, string>>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function renderColumn(c: import("./view-spec.js").SelectColumn): string {
|
|
13
|
+
if (c.kind === "passthrough") {
|
|
14
|
+
return `${c.sourceAlias}.${c.sourceColumn} AS ${c.dbColAlias}`;
|
|
15
|
+
}
|
|
16
|
+
// aggregate — use DISTINCT for count() over joined PKs to avoid join inflation.
|
|
17
|
+
if (c.agg === "count") {
|
|
18
|
+
return `COUNT(DISTINCT ${c.sourceAlias}.${c.sourceColumn}) AS ${c.dbColAlias}`;
|
|
19
|
+
}
|
|
20
|
+
return `${c.agg.toUpperCase()}(${c.sourceAlias}.${c.sourceColumn}) AS ${c.dbColAlias}`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function renderJoin(
|
|
24
|
+
node: JoinNode,
|
|
25
|
+
parentAlias: string,
|
|
26
|
+
options: EmitOptions,
|
|
27
|
+
): string {
|
|
28
|
+
const table = options.joinTables[node.targetEntity];
|
|
29
|
+
if (!table) {
|
|
30
|
+
throw new Error(
|
|
31
|
+
`view-ddl-emit: no table name registered for joined entity "${node.targetEntity}".`,
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
// JoinNode stores raw camelCase field names from metadata (e.g. "programId").
|
|
35
|
+
// Emit always applies snake_case — view DDL is SQL-side and always snake_case.
|
|
36
|
+
const fkCol = toSnakeCase(node.fkField);
|
|
37
|
+
const pkCol = toSnakeCase(node.pkField);
|
|
38
|
+
const childAlias = node.alias;
|
|
39
|
+
// referenceHolder = "source" → FK on parent (source): child.pk = parent.fk (belongs-to)
|
|
40
|
+
// referenceHolder = "target" → FK on child (target): child.fk = parent.pk (has-many)
|
|
41
|
+
const onClause = node.referenceHolder === "source"
|
|
42
|
+
? `${childAlias}.${pkCol} = ${parentAlias}.${fkCol}`
|
|
43
|
+
: `${childAlias}.${fkCol} = ${parentAlias}.${pkCol}`;
|
|
44
|
+
let sql = ` LEFT OUTER JOIN ${table} ${childAlias} ON ${onClause}`;
|
|
45
|
+
for (const childJoin of node.children) {
|
|
46
|
+
sql += "\n" + renderJoin(childJoin, childAlias, options);
|
|
47
|
+
}
|
|
48
|
+
return sql;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function emitViewDdl(spec: ViewSpec, options: EmitOptions): string {
|
|
52
|
+
const cols = spec.selectSpec.columns
|
|
53
|
+
.map((c) => " " + renderColumn(c))
|
|
54
|
+
.join(",\n");
|
|
55
|
+
const fromClause = ` FROM ${options.baseTableName} ${spec.joinTree.baseAlias}`;
|
|
56
|
+
const joinsClause = spec.joinTree.joins
|
|
57
|
+
.map((j) => renderJoin(j, spec.joinTree.baseAlias, options))
|
|
58
|
+
.join("\n");
|
|
59
|
+
const groupByClause =
|
|
60
|
+
spec.groupBy.length > 0 ? `\n GROUP BY ${spec.groupBy.join(", ")}` : "";
|
|
61
|
+
|
|
62
|
+
return `CREATE VIEW ${spec.viewName} AS
|
|
63
|
+
SELECT
|
|
64
|
+
${cols}
|
|
65
|
+
${fromClause}${joinsClause ? "\n" + joinsClause : ""}${groupByClause};`;
|
|
66
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { AggregateFunction } from "@metaobjectsdev/metadata";
|
|
2
|
+
|
|
3
|
+
/** One node in the JOIN tree. `alias` is an auto-generated unique short alias. */
|
|
4
|
+
export interface JoinNode {
|
|
5
|
+
/** Relationship name on the parent object (e.g., "weeks"). */
|
|
6
|
+
readonly relationship: string;
|
|
7
|
+
/** Entity name this join lands on (e.g., "Week"). */
|
|
8
|
+
readonly targetEntity: string;
|
|
9
|
+
/** Auto-assigned SQL alias for this join (e.g., "w", "w0"). */
|
|
10
|
+
readonly alias: string;
|
|
11
|
+
/** Cardinality of the relationship being traversed. */
|
|
12
|
+
readonly cardinality: "one" | "many";
|
|
13
|
+
/** FK field name (lives on whichever side `referenceHolder` indicates). */
|
|
14
|
+
readonly fkField: string;
|
|
15
|
+
/** PK field name on the side that does NOT hold the FK. */
|
|
16
|
+
readonly pkField: string;
|
|
17
|
+
/** Which side of this hop physically holds the FK: the parent (source) or the child (target). */
|
|
18
|
+
readonly referenceHolder: "source" | "target";
|
|
19
|
+
/** Child joins. */
|
|
20
|
+
readonly children: readonly JoinNode[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Tree of JOINs rooted at the projection's base entity. */
|
|
24
|
+
export interface JoinTree {
|
|
25
|
+
/** Base entity name (e.g., "Program"). */
|
|
26
|
+
readonly baseEntity: string;
|
|
27
|
+
/** SQL alias for the base entity (typically "p", "p0"). */
|
|
28
|
+
readonly baseAlias: string;
|
|
29
|
+
/** Joined entities (could be empty for a flat projection). */
|
|
30
|
+
readonly joins: readonly JoinNode[];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** One column of the SELECT list. */
|
|
34
|
+
export type SelectColumn =
|
|
35
|
+
| {
|
|
36
|
+
readonly kind: "passthrough";
|
|
37
|
+
readonly fieldName: string; // projection field name
|
|
38
|
+
readonly dbColAlias: string; // SQL output column name
|
|
39
|
+
readonly sourceAlias: string; // join alias of the source
|
|
40
|
+
readonly sourceColumn: string; // source table's column name (already strategy-applied)
|
|
41
|
+
}
|
|
42
|
+
| {
|
|
43
|
+
readonly kind: "aggregate";
|
|
44
|
+
readonly fieldName: string;
|
|
45
|
+
readonly dbColAlias: string;
|
|
46
|
+
readonly agg: AggregateFunction;
|
|
47
|
+
readonly sourceAlias: string;
|
|
48
|
+
readonly sourceColumn: string;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export interface SelectSpec {
|
|
52
|
+
readonly columns: readonly SelectColumn[];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Top-level view specification consumed by view-ddl-emit + Drizzle declaration. */
|
|
56
|
+
export interface ViewSpec {
|
|
57
|
+
readonly viewName: string; // already strategy-applied (e.g., "v_program_summary")
|
|
58
|
+
readonly joinTree: JoinTree;
|
|
59
|
+
readonly selectSpec: SelectSpec;
|
|
60
|
+
/** non-aggregate column SQL fragments to put in GROUP BY (empty if no aggregates). */
|
|
61
|
+
readonly groupBy: readonly string[];
|
|
62
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// Relation resolver — pre-pass that builds the inverse-side map for relations() emission.
|
|
2
|
+
// For each entity, we need to know:
|
|
3
|
+
// - Which outgoing belongs-to relationships it declares (one-side, reference on this entity)
|
|
4
|
+
// - Which incoming relationships point to it (many-side, reference on the other entity)
|
|
5
|
+
//
|
|
6
|
+
// Reads identity.reference declarations to determine the physical reference side.
|
|
7
|
+
|
|
8
|
+
import type { MetaRoot } from "@metaobjectsdev/metadata";
|
|
9
|
+
import {
|
|
10
|
+
RELATIONSHIP_ATTR_CARDINALITY,
|
|
11
|
+
RELATIONSHIP_ATTR_OBJECT_REF,
|
|
12
|
+
CARDINALITY_ONE,
|
|
13
|
+
stripPackage,
|
|
14
|
+
} from "@metaobjectsdev/metadata";
|
|
15
|
+
import { variableNameFromEntity } from "./naming.js";
|
|
16
|
+
import { isProjection } from "./projection/projection-detector.js";
|
|
17
|
+
|
|
18
|
+
export interface RelationEntry {
|
|
19
|
+
/** Name of the relationship (e.g., "author") */
|
|
20
|
+
name: string;
|
|
21
|
+
/** Cardinality: 'one' | 'many' */
|
|
22
|
+
cardinality: "one" | "many";
|
|
23
|
+
/** The other entity's name (e.g., "User") */
|
|
24
|
+
targetEntity: string;
|
|
25
|
+
/** For cardinality 'one': the field on THIS entity that holds the FK (e.g., "authorId") */
|
|
26
|
+
fkField?: string;
|
|
27
|
+
/** For cardinality 'one': the target entity's PK field (e.g., "id") */
|
|
28
|
+
targetPkField?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Map from entity name → list of relations for that entity's relations() block */
|
|
32
|
+
export type RelationMap = Map<string, RelationEntry[]>;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Walk all entities, collect relationship children, and also register inverse
|
|
36
|
+
* many() sides on the target entity.
|
|
37
|
+
*/
|
|
38
|
+
export function buildRelationMap(root: MetaRoot): RelationMap {
|
|
39
|
+
const result: RelationMap = new Map();
|
|
40
|
+
|
|
41
|
+
const ensure = (name: string): RelationEntry[] => {
|
|
42
|
+
if (!result.has(name)) result.set(name, []);
|
|
43
|
+
return result.get(name)!;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
for (const obj of root.objects()) {
|
|
47
|
+
// Projections (source.dbView) are view-backed; they never emit a relations()
|
|
48
|
+
// block, and their inherited belongs-to relationships would otherwise register
|
|
49
|
+
// a spurious inverse-many on the target entity.
|
|
50
|
+
if (isProjection(obj)) continue;
|
|
51
|
+
|
|
52
|
+
for (const child of obj.relationships()) {
|
|
53
|
+
const cardinality = child.ownAttr(RELATIONSHIP_ATTR_CARDINALITY) as string | undefined;
|
|
54
|
+
if (cardinality !== CARDINALITY_ONE) continue;
|
|
55
|
+
|
|
56
|
+
const targetEntityRaw = child.ownAttr(RELATIONSHIP_ATTR_OBJECT_REF) as string | undefined;
|
|
57
|
+
if (!targetEntityRaw) continue;
|
|
58
|
+
const targetEntity = stripPackage(targetEntityRaw);
|
|
59
|
+
|
|
60
|
+
// Find an identity.reference on `obj` whose @references targets this relationship's target.
|
|
61
|
+
// Compare against package-stripped names since both relationship @objectRef and
|
|
62
|
+
// identity.reference @references may carry package-qualified entity names.
|
|
63
|
+
const refs = obj.referenceIdentities();
|
|
64
|
+
const matching = refs.find((r) => stripPackage(r.targetEntity ?? "") === targetEntity);
|
|
65
|
+
if (!matching) continue;
|
|
66
|
+
|
|
67
|
+
const fkFields = matching.fields;
|
|
68
|
+
if (fkFields.length === 0) continue;
|
|
69
|
+
const fkField = fkFields[0]!;
|
|
70
|
+
|
|
71
|
+
ensure(obj.name).push({
|
|
72
|
+
name: child.name,
|
|
73
|
+
cardinality: "one",
|
|
74
|
+
targetEntity,
|
|
75
|
+
fkField,
|
|
76
|
+
targetPkField: "id",
|
|
77
|
+
});
|
|
78
|
+
ensure(targetEntity).push({
|
|
79
|
+
name: variableNameFromEntity(obj.name),
|
|
80
|
+
cardinality: "many",
|
|
81
|
+
targetEntity: obj.name,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return result;
|
|
87
|
+
}
|