temporeest
Version:
349 lines (305 loc) • 10.3 kB
text/typescript
import { nullthrows, upcaseAt } from '@strut/utils';
import { CodegenFile, CodegenStep, generatedDir } from '@aphro/codegen-api';
import TypescriptFile from './TypescriptFile.js';
import {
Field,
SchemaNode,
EdgeDeclaration,
EdgeReferenceDeclaration,
ID,
Import,
SchemaEdge,
FieldDeclaration,
} from '@aphro/schema-api';
import { nodeFn, edgeFn, tsImport } from '@aphro/schema';
import { importsToString } from './tsUtils.js';
import * as path from 'path';
export default class GenTypescriptQuery extends CodegenStep {
// This can technicall take a node _or_ an edge.
// also... should we have access to the entire schema file?
static accepts(schema: SchemaNode | SchemaEdge): boolean {
return schema.storage.type !== 'ephemeral';
}
private schema: SchemaNode | SchemaEdge;
private edges: { [key: string]: SchemaEdge };
constructor(opts: {
nodeOrEdge: SchemaNode | SchemaEdge;
edges: { [key: string]: SchemaEdge };
dest: string;
}) {
super();
this.schema = opts.nodeOrEdge;
this.edges = opts.edges;
}
// Nit:
// we can technically return an array of files.
// Since we can have edge data... so we'd need edge queries rather than a query per schema.
// b/c structure on the edges...
// TODO: de-duplicate imports by storing imports in an intermediate structure.
async gen(): Promise<CodegenFile> {
const imports = this.collectImports();
return new TypescriptFile(
path.join(generatedDir, nodeFn.queryTypeName(this.schema.name) + '.ts'),
`${importsToString(imports)}
export default class ${nodeFn.queryTypeName(this.schema.name)} extends DerivedQuery<${
this.schema.name
}> {
static create(ctx: Context) {
return new ${nodeFn.queryTypeName(this.schema.name)}(
ctx,
QueryFactory.createSourceQueryFor(ctx, ${nodeFn.specName(this.schema.name)}),
modelLoad(ctx, ${nodeFn.specName(this.schema.name)}.createFrom),
);
}
static empty(ctx: Context) {
return new ${nodeFn.queryTypeName(this.schema.name)}(
ctx,
new EmptyQuery(ctx),
);
}
protected derive(expression: Expression): ${nodeFn.queryTypeName(this.schema.name)} {
return new ${nodeFn.queryTypeName(this.schema.name)}(
this.ctx,
this,
expression,
)
}
${this.getFromIdMethodCode()}
${this.getFromInboundFieldEdgeMethodsCode()}
${this.getFilterMethodsCode()}
${this.getHopMethodsCode()}
${this.getTakeMethodCode()}
${this.getOrderByMethodsCode()}
${this.getProjectionMethodsCode()}
}
`,
);
}
private collectImports(): Import[] {
return [
tsImport('{Context}', null, '@aphro/runtime-ts'),
...[
'DerivedQuery',
'QueryFactory',
'modelLoad',
'filter',
'Predicate',
'take',
'orderBy',
'P',
'ModelFieldGetter',
'Expression',
'EmptyQuery',
].map(i => tsImport(`{${i}}`, null, '@aphro/runtime-ts')),
tsImport('{SID_of}', null, '@aphro/runtime-ts'),
tsImport(this.schema.name, null, `../${this.schema.name}.js`),
tsImport('{Data}', null, `./${this.schema.name}Base.js`),
tsImport(
nodeFn.specName(this.schema.name),
null,
`./${nodeFn.specName(this.schema.name)}.js`,
),
...this.getIdFieldImports(),
...this.getEdgeImports(),
];
}
private getFilterMethodsCode(): string {
const ret: string[] = [];
const fields = Object.values(this.schema.fields);
for (const field of fields) {
ret.push(`
where${upcaseAt(field.name, 0)}(p: Predicate<Data["${field.name}"]>) {
${this.getFilterMethodBody(field)}
}`);
}
return ret.join('\n');
}
private getFilterMethodBody(field: FieldDeclaration): string {
return `return this.derive(
// @ts-ignore #43
filter(
new ModelFieldGetter<"${field.name}", Data, ${this.schema.name}>("${field.name}"),
p,
),
)`;
}
private getOrderByMethodsCode(): string {
const ret: string[] = [];
const fields = Object.values(this.schema.fields);
for (const field of fields) {
ret.push(`
orderBy${upcaseAt(field.name, 0)}(direction: 'asc' | 'desc' = 'asc') {
${this.getOrderByMethodBody(field)}
}`);
}
return ret.join('\n');
}
private getOrderByMethodBody(field: FieldDeclaration): string {
return `return this.derive(
orderBy(
new ModelFieldGetter<"${field.name}", Data, ${this.schema.name}>("${field.name}"),
direction,
),
)`;
}
private getFromIdMethodCode(): string {
if (this.schema.type === 'standaloneEdge') {
return '';
}
return `
static fromId(ctx: Context, id: SID_of<${this.schema.name}>) {
return this.create(ctx).whereId(P.equals(id));
}
`;
}
private getFromInboundFieldEdgeMethodsCode(): string {
const schema = this.schema;
if (schema.type === 'standaloneEdge') {
return '';
}
// this would be inbound edges, right?
// inbound edges to me based on one of my fields.
const inbound: EdgeDeclaration[] = Object.values(schema.extensions.inboundEdges?.edges || {})
.filter((edge): edge is EdgeDeclaration => edge.type === 'edge')
.filter((edge: EdgeDeclaration) => edgeFn.isThroughNode(schema, edge));
return inbound.map(this.getFromInboundFieldEdgeMethodCode).join('\n');
}
private getFromInboundFieldEdgeMethodCode(edge: EdgeDeclaration): string {
const column = nullthrows(edge.throughOrTo.column);
const field = this.schema.fields[column];
const idParts = field.type.filter((f): f is ID => typeof f !== 'string' && f.type === 'id');
if (idParts.length === 0) {
throw new Error('fields edges must refer to id fields');
} else if (idParts.length > 1) {
throw new Error(
`unioning of ids for edges is not yet supported. Processing field ${field.name}`,
);
}
return `
static from${upcaseAt(column, 0)}(ctx: Context, id: SID_of<${idParts[0].of}>) {
return this.create(ctx).where${upcaseAt(field.name, 0)}(P.equals(id));
}
`;
}
private getEdgeImports(): Import[] {
const schema = this.schema;
if (schema.type === 'standaloneEdge') {
return [];
}
const inbound = Object.values(schema.extensions.inboundEdges?.edges || {}).map(e =>
edgeFn.dereference(e, this.edges),
);
const outbound = Object.values(schema.extensions.outboundEdges?.edges || {}).map(e =>
edgeFn.dereference(e, this.edges),
);
return [...inbound, ...outbound]
.filter(edge => edgeFn.queryTypeName(schema, edge) !== nodeFn.queryTypeName(this.schema.name))
.flatMap(edge => [
tsImport(
edgeFn.destModelSpecName(schema, edge),
null,
`./${edgeFn.destModelSpecName(schema, edge)}.js`,
),
tsImport(
edgeFn.queryTypeName(schema, edge),
null,
`./${edgeFn.queryTypeName(schema, edge)}.js`,
),
]);
// import edge reference queries too
}
private getIdFieldImports(): Import[] {
const idFields = Object.values(this.schema.fields)
.flatMap(f => f.type)
.filter((f): f is ID => typeof f !== 'string' && f.type === 'id' && f.of !== 'any');
return idFields.map(f => tsImport(f.of, null, '../' + f.of + '.js'));
}
private getHopMethodsCode(): string {
if (this.schema.type === 'standaloneEdge') {
return '';
}
// hop methods are edges
// e.g., Deck.querySlides().queryComponents()
const outbound = Object.values(this.schema.extensions.outboundEdges?.edges || {});
return outbound.map(e => this.getHopMethod(e)).join('\n');
}
private getHopMethod(edge: EdgeDeclaration | EdgeReferenceDeclaration): string {
if (this.schema.type === 'standaloneEdge') {
return '';
}
const e = edgeFn.dereference(edge, this.edges);
// if (edgeFn.isTo(edge)) {
// body = this.getHopMethodForJunctionLikeEdge(edge);
// }
return `query${upcaseAt(edge.name, 0)}(): ${edgeFn.queryTypeName(this.schema, e)} {
${this.getHopMethodBody(e, edge)}
}`;
}
private getHopMethodForJunctionLikeEdge(edge: EdgeDeclaration | SchemaEdge): string {
return '';
}
private getHopMethodBody(
edge: EdgeDeclaration | SchemaEdge,
ref: EdgeDeclaration | EdgeReferenceDeclaration,
): string {
if (this.schema.type === 'standaloneEdge') {
return '';
}
return `return new ${edgeFn.queryTypeName(
this.schema,
edge,
)}(this.ctx, QueryFactory.createHopQueryFor(this.ctx, this, ${nodeFn.specName(
this.schema.name,
)}.outboundEdges.${ref.name}),
modelLoad(this.ctx, ${edgeFn.destModelSpecName(this.schema, edge)}.createFrom),
);`;
}
private getTakeMethodCode(): string {
return `take(n: number) {
return new ${nodeFn.queryTypeName(this.schema.name)}(
this.ctx,
this,
take(n),
);
}`;
}
private getProjectionMethodsCode(): string {
return '';
}
/*
return new ComponentQuery(
QueryFactory.createHopQueryFor(this, spec, ComponentSpec, edgeDef??),
modelLoad(ComponentSpec.createFrom),
) // --> rm this as edge def does it: .whereSlideId(P.equals(this.id));
createHopQuery should take in:
- source spec & field for join
- dest spec & field for join
Now if it is neo... we'll figure something out.
Maybe it is just src, dest, edge name?
*/
}
/*
Codegening the query shouldn't care what the underlying storage impl is.
Query layer is storage agnostic.
Thus we should use the `schema` to call into a `factory` which will construct the
`source query` / `source expression` based on the underlying storage type.
*/
/*
Derived query example:
SlideQuery extends DerivedQuery {
static create() {
return new SlideQuery(
Factory.createSourceQueryFor(schema) // e.g., new SQLSourceQuery(schema),
// convert raw db result into model load.
// we'd want to move this expression to the end in plan optimizaiton.
new ModelLoadExpression(Slide.createFromData)
);
}
whereName(predicate: Predicate) {
return new SlideQuery(
this, // the prior query
new ModelFilterExpression(field, predicate)
);
}
}
*/