From 84de4113363e6c1ccea5bdfcbdf3a2e087876f2e Mon Sep 17 00:00:00 2001 From: "Shahid N. Shah" Date: Tue, 12 Dec 2023 11:48:55 -0500 Subject: [PATCH] refactor: generalize polygenix terminology #176 --- .vscode/settings.json | 2 +- render/ddl/table/foreign-key.ts | 15 +- render/ddl/table/table.ts | 2 +- render/diagram/plantuml-ie-notation.ts | 53 +++-- render/domain/domain.ts | 2 +- render/emit/js.ts | 6 + render/emit/polygenix-notebook.ts | 26 +- render/emit/polygenix.ts | 73 +++--- render/graph.ts | 70 ++++-- render/graph_test.ts | 2 + render/polygenix/diagram/mod.ts | 1 + .../polygenix/diagram/plantuml-ie-notation.ts | 222 ++++++++++++++++++ render/polygenix/engine.ts | 66 ------ render/polygenix/governance.ts | 23 ++ render/polygenix/info-model.ts | 65 ----- render/polygenix/mod.ts | 1 + .../polygenix/rust/mod_test-fixture-serde.rs | 13 + render/polygenix/rust/mod_test.ts | 23 +- render/polygenix/rust/serde.ts | 148 ++++++++---- 19 files changed, 517 insertions(+), 296 deletions(-) create mode 100644 render/polygenix/diagram/mod.ts create mode 100644 render/polygenix/diagram/plantuml-ie-notation.ts delete mode 100644 render/polygenix/engine.ts create mode 100644 render/polygenix/governance.ts diff --git a/.vscode/settings.json b/.vscode/settings.json index a6d302d6..1e2e5051 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -12,7 +12,7 @@ "deno.codeLens.implementations": false, "editor.formatOnSave": false, "editor.codeActionsOnSave": { - "source.fixAll.eslint": true + "source.fixAll.eslint": "explicit" }, "[javascript]": { "editor.formatOnSave": true, diff --git a/render/ddl/table/foreign-key.ts b/render/ddl/table/foreign-key.ts index 26c1a259..6cde5f38 100644 --- a/render/ddl/table/foreign-key.ts +++ b/render/ddl/table/foreign-key.ts @@ -286,30 +286,31 @@ export function foreignKeysFactory< for (const key of tableShapeKeys) { const zodTypeSD = zbSchema[key]; if (isForeignKeyDestination(zodTypeSD.sqlDomain)) { - const fks = zodTypeSD.sqlDomain.foreignKeySource; - const target = entityByName(fks.tableName); + const fkNature = zodTypeSD.sqlDomain.foreignKeyRelNature; + const fkSrc = zodTypeSD.sqlDomain.foreignKeySource; + const target = entityByName(fkSrc.tableName); if (target) { const attr = target.attributes.find((sd) => - sd.identity == fks.columnName + sd.identity == fkSrc.columnName ); if (attr) { yield { from: { entity, attr: zodTypeSD.sqlDomain }, to: { entity: target, attr }, - nature: isBelongsToForeignKeyNature(fks) - ? { isBelongsTo: true, collectionName: fks.collectionName } + nature: isBelongsToForeignKeyNature(fkNature) + ? { isBelongsTo: true, collectionName: fkNature.collectionName } : "reference", }; } else { reportIssue({ // deno-fmt-ignore - lintIssue: `table column '${fks.columnName}' referenced in foreignKey ${JSON.stringify(fks)} not found in graph`, + lintIssue: `table column '${fkSrc.columnName}' referenced in foreignKey ${JSON.stringify(fkSrc)} not found in graph`, }); } } else { reportIssue({ // deno-fmt-ignore - lintIssue: `table '${fks.tableName}' referenced in foreignKey ${JSON.stringify(fks)} not found in graph`, + lintIssue: `table '${fkSrc.tableName}' referenced in foreignKey ${JSON.stringify(fkSrc)} not found in graph`, }); } } diff --git a/render/ddl/table/table.ts b/render/ddl/table/table.ts index 24d846df..cb8aa7c9 100644 --- a/render/ddl/table/table.ts +++ b/render/ddl/table/table.ts @@ -303,7 +303,7 @@ export function tableDefinition< const result: g.GraphEntityDefinition< TableName, Context, - Any, + string, DomainQS, DomainsQS > = { diff --git a/render/diagram/plantuml-ie-notation.ts b/render/diagram/plantuml-ie-notation.ts index 76d90cfa..94ed6d90 100644 --- a/render/diagram/plantuml-ie-notation.ts +++ b/render/diagram/plantuml-ie-notation.ts @@ -1,8 +1,5 @@ import * as SQLa from "../mod.ts"; -// deno-lint-ignore no-explicit-any -type Any = any; - export interface PlantUmlIeOptions< Context extends SQLa.SqlEmitContext, DomainQS extends SQLa.SqlDomainQS, @@ -10,30 +7,42 @@ export interface PlantUmlIeOptions< > { readonly diagramName: string; readonly includeEntityAttr: ( - ea: SQLa.GraphEntityAttrReference, + ea: SQLa.GraphEntityAttrReference< + string, + string, + Context, + DomainQS, + DomainsQS + >, ) => boolean; readonly elaborateEntityAttr?: ( - ea: SQLa.GraphEntityAttrReference, + ea: SQLa.GraphEntityAttrReference< + string, + string, + Context, + DomainQS, + DomainsQS + >, entity: ( name: string, ) => - | SQLa.GraphEntityDefinition + | SQLa.GraphEntityDefinition | undefined, ns: SQLa.SqlObjectNames, ) => string; readonly includeEntity: ( - e: SQLa.GraphEntityDefinition, + e: SQLa.GraphEntityDefinition, ) => boolean; readonly includeRelationship: ( - edge: SQLa.GraphEdge, + edge: SQLa.GraphEdge, ) => boolean; readonly relationshipIndicator: ( - edge: SQLa.GraphEdge, + edge: SQLa.GraphEdge, ) => string | false; readonly includeChildren: ( ir: SQLa.EntityGraphInboundRelationship< - Any, - Any, + string, + string, Context, DomainQS, DomainsQS @@ -65,9 +74,9 @@ export function typicalPlantUmlIeOptions< export function plantUmlIE< Entity extends SQLa.GraphEntityDefinition< - Any, + string, Context, - Any, + string, DomainQS, DomainsQS >, @@ -79,11 +88,23 @@ export function plantUmlIE< entityDefns: (ctx: Context) => Generator, puieOptions: PlantUmlIeOptions, ) { - const graph = SQLa.entitiesGraph(ctx, entityDefns); + const graph = SQLa.entitiesGraph< + SQLa.GraphEntityDefinition, + Context, + DomainQS, + DomainsQS, + SQLa.EntitiesGraphQS + >(ctx, entityDefns); const ns = ctx.sqlNamingStrategy(ctx); const attrPuml = ( - ea: SQLa.GraphEntityAttrReference, + ea: SQLa.GraphEntityAttrReference< + string, + string, + Context, + DomainQS, + DomainsQS + >, ) => { const tcName = ns.tableColumnName({ tableName: ea.entity.identity("presentation"), @@ -103,7 +124,7 @@ export function plantUmlIE< }; const entityPuml = ( - e: SQLa.GraphEntityDefinition, + e: SQLa.GraphEntityDefinition, ) => { const columns: string[] = []; // we want to put all the primary keys at the top of the entity diff --git a/render/domain/domain.ts b/render/domain/domain.ts index 223327aa..801c51b2 100644 --- a/render/domain/domain.ts +++ b/render/domain/domain.ts @@ -44,7 +44,7 @@ export type SqlDomain< ) => tmpl.SqlTextSupplier; readonly polygenixDataType: ( purpose: "info-model", - ) => tmpl.PolygenSrcCode; + ) => tmpl.PolygenCellContent; readonly sqlDefaultValue?: ( purpose: "create table column" | "stored routine arg", ) => tmpl.SqlTextSupplier; diff --git a/render/emit/js.ts b/render/emit/js.ts index 2931fe0b..eb323849 100644 --- a/render/emit/js.ts +++ b/render/emit/js.ts @@ -8,6 +8,8 @@ export type JsTokenSupplier = ( | "js-class-member-decl" | "js-var-ref" | "ts-type-decl" + | "rust-struct-member-decl" + | "rust-type-decl" | "sql-token" | "sql-token-quoted", ) => string; @@ -33,8 +35,12 @@ export function jsSnakeCaseToken( return snakeToCamelCase(src); case "ts-type-decl": + case "rust-type-decl": return snakeToPascalCase(src); + case "rust-struct-member-decl": + return src; + case "sql-token": return src; diff --git a/render/emit/polygenix-notebook.ts b/render/emit/polygenix-notebook.ts index d8ec1028..ab3c76b9 100644 --- a/render/emit/polygenix-notebook.ts +++ b/render/emit/polygenix-notebook.ts @@ -36,16 +36,16 @@ export function polygenNotebookFactory< nbd, kernel, instance, - sourceCode: async ( + polygenContent: async ( options: { separator?: ( cell: Parameters[0], state: Parameters[1], - ) => pgen.PolygenSrcCodeBehaviorSupplier; + ) => pgen.PolygenCellContentBehaviorSupplier; onNotSrcCodeSupplier?: ( cell: Parameters[0], state: Parameters[1], - ) => pgen.PolygenSrcCodeBehaviorSupplier; + ) => pgen.PolygenCellContentBehaviorSupplier; }, ...srcCodeIdentities: CellID[] ) => { @@ -62,31 +62,31 @@ export function polygenNotebookFactory< }, }); - const sourceCode: ( - | pgen.PolygenSrcCodeSupplier - | pgen.PolygenSrcCodeBehaviorSupplier + const content: ( + | pgen.PolygenCellContentSupplier + | pgen.PolygenCellContentBehaviorSupplier )[] = []; initRunState.runState.eventEmitter.afterCell = (cell, state) => { if (state.status == "successful") { if ( - pgen.isPolygenSrcCodeSupplier(state.execResult) || - pgen.isPolygenSrcCodeBehaviorSupplier(state.execResult) + pgen.isPolygenCellContentSupplier(state.execResult) || + pgen.isPolygenCellContentBehaviorSupplier(state.execResult) ) { if (options.separator) { - sourceCode.push(options.separator(cell, state)); + content.push(options.separator(cell, state)); } - const sts = state.execResult as pgen.PolygenSrcCodeSupplier< + const sts = state.execResult as pgen.PolygenCellContentSupplier< Context >; - sourceCode.push(sts); + content.push(sts); } else { const notSTS = options.onNotSrcCodeSupplier?.(cell, state); - if (notSTS) sourceCode.push(notSTS); + if (notSTS) content.push(notSTS); } } }; await kernel.run(instance(), initRunState); - return sourceCode; + return content; }, }; } diff --git a/render/emit/polygenix.ts b/render/emit/polygenix.ts index 97158f26..d85309cf 100644 --- a/render/emit/polygenix.ts +++ b/render/emit/polygenix.ts @@ -1,82 +1,79 @@ import * as safety from "../../lib/universal/safety.ts"; import * as sql from "./sql.ts"; -export interface PolygenSrcCodeEmitOptions { - readonly tableStructName: (tableName: string) => string; - readonly tableStructFieldName: ( - tc: { tableName: string; columnName: string }, - ) => string; -} - -export type PolygenSrcCodeText = +export type PolygenCellText = | string | string[]; -export type PolygenSrcCode = - | PolygenSrcCodeText - | ((ctx: Context) => PolygenSrcCodeText | Promise) +export type PolygenCellContent = + | PolygenCellText + | ((ctx: Context) => PolygenCellText | Promise) | sql.SqlTextSupplier; -export type PolygenSrcCodeSync = - | PolygenSrcCodeText - | ((ctx: Context) => PolygenSrcCodeText) +export type PolygenCellContentSync = + | PolygenCellText + | ((ctx: Context) => PolygenCellText) | sql.SqlTextSupplier; -export async function sourceCodeText( +export async function polygenCellContent( ctx: Context, - psc: PolygenSrcCodeSupplier | PolygenSrcCode, + psc: PolygenCellContentSupplier | PolygenCellContent, ): Promise { - if (isPolygenSrcCodeSupplier(psc)) { - return sourceCodeText(ctx, psc.sourceCode); + if (isPolygenCellContentSupplier(psc)) { + return polygenCellContent(ctx, psc.polygenContent); } if (sql.isSqlTextSupplier(psc)) return psc.SQL(ctx); if (typeof psc === "string") { return psc; } else if (typeof psc === "function") { - return await sourceCodeText(ctx, await psc(ctx)); + return await polygenCellContent(ctx, await psc(ctx)); } else { if (psc.length == 0) return ""; return psc.join("\n"); } } -export function sourceCodeTextSync( +export function polygenCellContentSync( ctx: Context, - psc: PolygenSrcCodeSync, + psc: PolygenCellContentSync, ): string { if (sql.isSqlTextSupplier(psc)) return psc.SQL(ctx); if (typeof psc === "string") { return psc; } else if (typeof psc === "function") { - return sourceCodeTextSync(ctx, psc(ctx)); + return polygenCellContentSync(ctx, psc(ctx)); } else { if (psc.length == 0) return ""; return psc.join("\n"); } } -export interface PolygenSrcCodeSupplier { - readonly sourceCode: PolygenSrcCode; +export interface PolygenCellContentSupplier< + Context extends sql.SqlEmitContext, +> { + readonly polygenContent: PolygenCellContent; } -export function isPolygenSrcCodeSupplier( +export function isPolygenCellContentSupplier< + Context extends sql.SqlEmitContext, +>( o: unknown, -): o is PolygenSrcCodeSupplier { - const isPSCS = safety.typeGuard>( - "sourceCode", +): o is PolygenCellContentSupplier { + const isPCCS = safety.typeGuard>( + "polygenContent", ); - return isPSCS(o); + return isPCCS(o); } -export interface PolygenSrcCodeBehaviorEmitTransformer { +export interface PolygenCellContentEmitTransformer { before: (interpolationSoFar: string, exprIdx: number) => string; after: (nextLiteral: string, exprIdx: number) => string; } export const removeLineFromPolygenEmitStream: - PolygenSrcCodeBehaviorEmitTransformer = { + PolygenCellContentEmitTransformer = { before: (isf) => { // remove the last line in the interpolation stream return isf.replace(/\n.*?$/, ""); @@ -87,24 +84,24 @@ export const removeLineFromPolygenEmitStream: }, }; -export interface PolygenSrcCodeBehaviorSupplier< +export interface PolygenCellContentBehaviorSupplier< Context extends sql.SqlEmitContext, > { readonly executePolygenSrcCodeBehavior: ( context: Context, ) => - | PolygenSrcCodeBehaviorEmitTransformer - | PolygenSrcCodeSupplier - | PolygenSrcCodeSupplier[]; + | PolygenCellContentEmitTransformer + | PolygenCellContentSupplier + | PolygenCellContentSupplier[]; } -export function isPolygenSrcCodeBehaviorSupplier< +export function isPolygenCellContentBehaviorSupplier< Context extends sql.SqlEmitContext, >( o: unknown, -): o is PolygenSrcCodeBehaviorSupplier { +): o is PolygenCellContentBehaviorSupplier { const isPSCBS = safety.typeGuard< - PolygenSrcCodeBehaviorSupplier + PolygenCellContentBehaviorSupplier >("executePolygenSrcCodeBehavior"); return isPSCBS(o); } diff --git a/render/graph.ts b/render/graph.ts index 110483f4..0246057a 100644 --- a/render/graph.ts +++ b/render/graph.ts @@ -1,11 +1,8 @@ +import { zod } from "./deps.ts"; import * as safety from "../lib/universal/safety.ts"; import * as qs from "../lib/quality-system/mod.ts"; import * as d from "./domain/mod.ts"; import * as emit from "./emit/mod.ts"; - -// deno-lint-ignore no-explicit-any -type Any = any; - export type EntitiesGraphQS< DomainQS extends d.SqlDomainQS, DomainsQS extends d.SqlDomainsQS, @@ -24,13 +21,19 @@ export interface EntityGraphOutboundReference< DomainsQS extends d.SqlDomainsQS, > { readonly from: GraphEntityAttrReference< - Any, - Any, + string, + string, + Context, + DomainQS, + DomainsQS + >; + readonly to: GraphEntityAttrReference< + string, + string, Context, DomainQS, DomainsQS >; - readonly to: GraphEntityAttrReference; readonly nature: EntityGraphRefNature; } @@ -43,7 +46,7 @@ export type EntityGraphOutboundReferencesSupplier< readonly entity: GraphEntityDefinition< EntityName, Context, - Any, + string, DomainQS, DomainsQS >; @@ -51,7 +54,7 @@ export type EntityGraphOutboundReferencesSupplier< name: EntityName, ) => | undefined - | GraphEntityDefinition; + | GraphEntityDefinition; readonly reportIssue: (issue: emit.SqlLintIssueSupplier) => void; }) => Generator>; @@ -69,10 +72,15 @@ export interface GraphEntityDefinition< | "outbound-ref-dict-lookup" | "lint-message", ) => EntityName; - readonly attributes: d.SqlDomain[]; + readonly attributes: d.SqlDomain< + zod.ZodTypeAny, + Context, + AttrName, + DomainQS + >[]; readonly outboundReferences?: EntityGraphOutboundReferencesSupplier< Context, - Any, + EntityName, DomainQS, DomainsQS >; @@ -87,7 +95,7 @@ export interface GraphEntityDefinitionSupplier< readonly graphEntityDefn: () => GraphEntityDefinition< EntityName, Context, - Any, + string, DomainQS, DomainsQS >; @@ -124,11 +132,11 @@ export interface GraphEntityAttrReference< readonly entity: GraphEntityDefinition< EntityName, Context, - Any, + string, DomainQS, DomainsQS >; - readonly attr: d.SqlDomain; + readonly attr: d.SqlDomain; } export interface GraphEdge< @@ -137,15 +145,15 @@ export interface GraphEdge< DomainsQS extends d.SqlDomainsQS, > { readonly source: GraphEntityAttrReference< - Any, - Any, + string, + string, Context, DomainQS, DomainsQS >; readonly ref: GraphEntityAttrReference< - Any, - Any, + string, + string, Context, DomainQS, DomainsQS @@ -161,12 +169,18 @@ export interface EntityGraphInboundRelationship< > { readonly from: GraphEntityAttrReference< FromName, - Any, + string, Context, DomainQS, DomainsQS >; - readonly to: GraphEntityDefinition; + readonly to: GraphEntityDefinition< + ToName, + Context, + string, + DomainQS, + DomainsQS + >; readonly nature: EntityGraphRefNature; } @@ -189,7 +203,13 @@ export interface EntityGraphInboundRelationshipBackRef< } export function entitiesGraph< - Entity extends GraphEntityDefinition, + Entity extends GraphEntityDefinition< + string, + Context, + string, + DomainQS, + DomainsQS + >, Context extends emit.SqlEmitContext, DomainQS extends d.SqlDomainQS, DomainsQS extends d.SqlDomainsQS, @@ -205,11 +225,11 @@ export function entitiesGraph< const entityRels = new Map[]; }>(); const edges: GraphEdge[] = []; diff --git a/render/graph_test.ts b/render/graph_test.ts index 6324fff9..ce1d87a4 100644 --- a/render/graph_test.ts +++ b/render/graph_test.ts @@ -220,6 +220,8 @@ const pumlErdFixture = `@startuml IE text_nullable: TEXT * int: INTEGER int_nullable: INTEGER + -- + syntheticTableWithForeignKeyss: SyntheticTableWithForeignKeys[] } entity "synthetic_table_with_text_pk" as synthetic_table_with_text_pk { diff --git a/render/polygenix/diagram/mod.ts b/render/polygenix/diagram/mod.ts new file mode 100644 index 00000000..0a00d583 --- /dev/null +++ b/render/polygenix/diagram/mod.ts @@ -0,0 +1 @@ +export * from "./plantuml-ie-notation.ts"; diff --git a/render/polygenix/diagram/plantuml-ie-notation.ts b/render/polygenix/diagram/plantuml-ie-notation.ts new file mode 100644 index 00000000..d86607f4 --- /dev/null +++ b/render/polygenix/diagram/plantuml-ie-notation.ts @@ -0,0 +1,222 @@ +import * as d from "../../domain/mod.ts"; +import * as g from "../../graph.ts"; +import * as emit from "../../mod.ts"; +import * as im from "../info-model.ts"; +import * as e from "../governance.ts"; + +// deno-lint-ignore no-explicit-any +type Any = any; + +export interface PlantUmlIeOptions< + Context extends emit.SqlEmitContext, + DomainQS extends emit.SqlDomainQS, + DomainsQS extends emit.SqlDomainsQS, +> extends im.PolygenInfoModelOptions { + readonly diagramName: string; + readonly elaborateEntityAttr?: ( + ea: emit.GraphEntityAttrReference, + entity: ( + name: string, + ) => + | emit.GraphEntityDefinition + | undefined, + ns: emit.SqlObjectNames, + ) => string; + readonly relationshipIndicator: ( + edge: emit.GraphEdge, + ) => string | false; +} + +export function typicalPlantUmlIeOptions< + Context extends emit.SqlEmitContext, + DomainQS extends emit.SqlDomainQS, + DomainsQS extends emit.SqlDomainsQS, +>( + inherit?: Partial>, +): PlantUmlIeOptions { + // we let type inference occur so generics can follow through + return { + ...im.typicalPolygenInfoModelOptions(inherit), + diagramName: "IE", + elaborateEntityAttr: () => "", + relationshipIndicator: (_edge) => { + return "|o..o{"; + }, + ...inherit, + }; +} + +export class PlantUmlIe< + Context extends emit.SqlEmitContext, + DomainQS extends d.SqlDomainQS, + DomainsQS extends d.SqlDomainsQS, +> implements emit.PolygenCellContentSupplier { + readonly sqlNames: emit.SqlObjectNames; + readonly graph: ReturnType< + typeof g.entitiesGraph< + g.GraphEntityDefinition, + Context, + DomainQS, + DomainsQS, + g.EntitiesGraphQS + > + >; + + constructor( + readonly sqlCtx: Context, + readonly entityDefns: ( + ctx: Context, + ) => Generator< + g.GraphEntityDefinition + >, + readonly puieOptions: + & PlantUmlIeOptions + & { readonly typeStrategy?: e.PolygenTypeStrategy } + & { readonly namingStrategy?: e.PolygenNamingStrategy }, + ) { + this.sqlNames = sqlCtx.sqlNamingStrategy(sqlCtx); + this.graph = g.entitiesGraph< + g.GraphEntityDefinition, + Context, + DomainQS, + DomainsQS, + g.EntitiesGraphQS + >( + this.sqlCtx, + this.entityDefns, + ); + } + + // deno-lint-ignore require-await + async entityAttrContent( + ea: g.GraphEntityAttrReference< + string, + string, + Context, + DomainQS, + DomainsQS + >, + ) { + const tcName = this.sqlNames.tableColumnName({ + tableName: ea.entity.identity("presentation"), + columnName: ea.attr.identity, + }); + const required = ea.attr.isNullable() ? " " : "*"; + const name = emit.isTablePrimaryKeyColumnDefn(ea.attr) + ? `**${tcName}**` + : tcName; + const descr = this.puieOptions.elaborateEntityAttr?.( + ea, + (name) => this.graph.entitiesByName.get(name), + this.sqlNames, + ); + const sqlType = ea.attr.sqlDataType("diagram").SQL(this.sqlCtx); + return ` ${required} ${name}: ${sqlType}${descr ?? ""}`; + } + + async entityContent( + e: g.GraphEntityDefinition, + ) { + const columns: string[] = []; + // we want to put all the primary keys at the top of the entity + for (const column of e.attributes) { + const ea = { entity: e, attr: column }; + if (!this.puieOptions.includeEntityAttr(ea)) continue; + if (emit.isTablePrimaryKeyColumnDefn(column)) { + columns.push(await this.entityAttrContent(ea)); + columns.push(" --"); + } + } + + for (const column of e.attributes) { + if (!emit.isTablePrimaryKeyColumnDefn(column)) { + const ea = { entity: e, attr: column }; + if (!this.puieOptions.includeEntityAttr(ea)) continue; + columns.push(await this.entityAttrContent(ea)); + } + } + + const rels = this.graph.entityRels.get(e.identity("dictionary-storage")); + const children: string[] = []; + if (rels && rels.inboundRels.length > 0) { + for (const ir of rels.inboundRels) { + if (typeof ir.nature === "object" && ir.nature.isBelongsTo) { + if (!this.puieOptions.includeChildren(ir)) continue; + const collectionName = ir.nature.collectionName ?? + emit.jsSnakeCaseToken(ir.from.entity.identity("presentation")); + children.push( + ` ${ + collectionName(this.sqlCtx, "plural", "js-class-member-decl") + }: ${collectionName(this.sqlCtx, "singular", "ts-type-decl")}[]`, + ); + } + } + } + if (children.length > 0) { + children.unshift(" --"); + } + + const tableName = this.sqlNames.tableName(e.identity("presentation")); + return [ + "", + ` entity "${tableName}" as ${tableName} {`, + ...columns, + ...children, + ` }`, + ]; + } + + async polygenContent() { + const tablesPuml: string[] = []; + for (const table of this.entityDefns(this.sqlCtx)) { + if (!this.puieOptions.includeEntity(table)) { + continue; + } + + tablesPuml.push(...await this.entityContent(table)); + } + + const relationshipsPuml: string[] = []; + for (const rel of this.graph.edges) { + if (!this.puieOptions.includeRelationship(rel)) { + continue; + } + const src = rel.source; + const ref = rel.ref; + // Relationship types see: https://plantuml.com/es/ie-diagram + // Zero or One |o-- + // Exactly One ||-- + // Zero or Many }o-- + // One or Many }|-- + const relIndicator = this.puieOptions.relationshipIndicator(rel); + if (relIndicator) { + relationshipsPuml.push( + ` ${ + this.sqlNames.tableName(ref.entity.identity("presentation")) + } ${relIndicator} ${ + this.sqlNames.tableName(src.entity.identity("presentation")) + }`, + ); + } + } + if (relationshipsPuml.length > 0) relationshipsPuml.unshift(""); + + const content: emit.PolygenCellContent = [ + `@startuml ${this.puieOptions.diagramName}`, + " hide circle", + " skinparam linetype ortho", + " skinparam roundcorner 20", + " skinparam class {", + " BackgroundColor White", + " ArrowColor Silver", + " BorderColor Silver", + " FontColor Black", + " FontSize 12", + " }", + ...tablesPuml, + ...relationshipsPuml, + "@enduml", + ].join("\n"); + return content; + } +} diff --git a/render/polygenix/engine.ts b/render/polygenix/engine.ts deleted file mode 100644 index 9df3d149..00000000 --- a/render/polygenix/engine.ts +++ /dev/null @@ -1,66 +0,0 @@ -import * as d from "../domain/mod.ts"; -import * as g from "../graph.ts"; -import * as emit from "../emit/mod.ts"; - -// deno-lint-ignore no-explicit-any -type Any = any; - -export const snakeToCamelCase = (str: string) => - str.replace(/(_\w)/g, (m) => m[1].toUpperCase()); - -export const snakeToPascalCase = (str: string) => - str.replace(/(^|_)\w/g, (m) => m[m.length - 1].toUpperCase()); - -export interface PolygenTypeSupplier { - readonly type: emit.PolygenSrcCodeText; - readonly remarks?: string; -} - -export interface PolygenEngineTypeStrategy { - readonly type: (abstractType: string) => PolygenTypeSupplier; -} - -export interface PolygenEngineNamingStrategy { - readonly entityName: (sqlIdentifier: string) => string; - readonly entityAttrName: (sqlIdentifier: string) => string; -} - -export interface PolygenEngine< - Context extends emit.SqlEmitContext, - DomainQS extends d.SqlDomainQS, - DomainsQS extends d.SqlDomainsQS, -> { - readonly typeStrategy: () => PolygenEngineTypeStrategy; - readonly namingStrategy: () => PolygenEngineNamingStrategy; - readonly entityAttrSrcCode: ( - ea: g.GraphEntityAttrReference< - Any, - Any, - Context, - DomainQS, - DomainsQS - >, - entity: g.GraphEntityDefinition, - graph: ReturnType< - typeof g.entitiesGraph< - Any, - Context, - DomainQS, - DomainsQS, - g.EntitiesGraphQS - > - >, - ) => emit.PolygenSrcCodeSync | Promise>; - readonly entitySrcCode: ( - entity: g.GraphEntityDefinition, - graph: ReturnType< - typeof g.entitiesGraph< - Any, - Context, - DomainQS, - DomainsQS, - g.EntitiesGraphQS - > - >, - ) => emit.PolygenSrcCodeSync | Promise>; -} diff --git a/render/polygenix/governance.ts b/render/polygenix/governance.ts new file mode 100644 index 00000000..08822002 --- /dev/null +++ b/render/polygenix/governance.ts @@ -0,0 +1,23 @@ +import * as emit from "../emit/mod.ts"; + +export const snakeToCamelCase = (str: string) => + str.replace(/(_\w)/g, (m) => m[1].toUpperCase()); + +export const snakeToPascalCase = (str: string) => + str.replace(/(^|_)\w/g, (m) => m[m.length - 1].toUpperCase()); + +export const snakeToConstantCase = (str: string) => str.toUpperCase(); + +export interface PolygenTypeSupplier { + readonly type: emit.PolygenCellText; + readonly remarks?: string; +} + +export interface PolygenTypeStrategy { + readonly type: (abstractType: string) => PolygenTypeSupplier; +} + +export interface PolygenNamingStrategy { + readonly entityName: (sqlIdentifier: string) => string; + readonly entityAttrName: (sqlIdentifier: string) => string; +} diff --git a/render/polygenix/info-model.ts b/render/polygenix/info-model.ts index 627969b3..ddf75503 100644 --- a/render/polygenix/info-model.ts +++ b/render/polygenix/info-model.ts @@ -1,7 +1,6 @@ import * as d from "../domain/mod.ts"; import * as g from "../graph.ts"; import * as emit from "../emit/mod.ts"; -import { PolygenEngine } from "./engine.ts"; // deno-lint-ignore no-explicit-any type Any = any; @@ -65,67 +64,3 @@ export function typicalPolygenInfoModelOptions< ...inherit, }; } - -/** - * Encapsulates polyglot source code generation code for information models - * like tables, views to be represented as structs, types, etc. - */ -export class PolygenInfoModelNotebook< - Entity extends g.GraphEntityDefinition< - Any, - Context, - Any, - DomainQS, - DomainsQS - >, - Context extends emit.SqlEmitContext, - DomainQS extends d.SqlDomainQS, - DomainsQS extends d.SqlDomainsQS, -> extends emit.PolygenNotebook { - constructor( - readonly engine: PolygenEngine< - Context, - DomainQS, - DomainsQS - >, - readonly sqlCtx: Context, - readonly entityDefns: (ctx: Context) => Generator, - readonly polygenSchemaOptions: PolygenInfoModelOptions< - Context, - DomainQS, - DomainsQS - >, - ) { - super(); - } - - async entitiesSrcCode() { - const graph = g.entitiesGraph< - Entity, - Context, - DomainQS, - DomainsQS, - g.EntitiesGraphQS - >( - this.sqlCtx, - this.entityDefns, - ); - - const entitiesSrcCode: string[] = []; - for (const entity of graph.entities) { - if (!this.polygenSchemaOptions.includeEntity(entity)) { - continue; - } - - const sc = await this.engine.entitySrcCode(entity, graph); - entitiesSrcCode.push(await emit.sourceCodeText(this.sqlCtx, sc)); - } - - const pscSupplier: emit.PolygenSrcCodeSupplier = { - sourceCode: () => { - return entitiesSrcCode.join("\n"); - }, - }; - return pscSupplier; - } -} diff --git a/render/polygenix/mod.ts b/render/polygenix/mod.ts index 87b7698e..5100dc17 100644 --- a/render/polygenix/mod.ts +++ b/render/polygenix/mod.ts @@ -1,2 +1,3 @@ export * from "./info-model.ts"; export * as rust from "./rust/mod.ts"; +export * as diagram from "./diagram/mod.ts"; diff --git a/render/polygenix/rust/mod_test-fixture-serde.rs b/render/polygenix/rust/mod_test-fixture-serde.rs index abb1baf5..c84e2b2a 100644 --- a/render/polygenix/rust/mod_test-fixture-serde.rs +++ b/render/polygenix/rust/mod_test-fixture-serde.rs @@ -1,3 +1,12 @@ +/* +const SYNTHETIC_TABLE_WITHOUT_PK: &str = "synthetic_table_without_pk"; +const SYNTHETIC_TABLE_WITH_AUTO_INC_PK: &str = "synthetic_table_with_auto_inc_pk"; +const SYNTHETIC_TABLE_WITH_TEXT_PK: &str = "synthetic_table_with_text_pk"; +const SYNTHETIC_TABLE_WITH_UAOD_PK: &str = "synthetic_table_with_uaod_pk"; +const SYNTHETIC_TABLE_WITH_CONSTRAINTS: &str = "synthetic_table_with_constraints"; +*/ + +// `synthetic_table_without_pk` table #[derive(Debug, PartialEq, serde::Serialize, serde::Deserialize)] pub struct SyntheticTableWithoutPk { text: String, // 'string' maps directly to Rust type @@ -6,6 +15,7 @@ pub struct SyntheticTableWithoutPk { int_nullable: Option, // 'integer' maps directly to Rust type } +// `synthetic_table_with_auto_inc_pk` table #[derive(Debug, PartialEq, serde::Serialize, serde::Deserialize)] pub struct SyntheticTableWithAutoIncPk { auto_inc_primary_key: Option, // PRIMARY KEY ('integer' maps directly to Rust type) @@ -15,6 +25,7 @@ pub struct SyntheticTableWithAutoIncPk { int_nullable: Option, // 'integer' maps directly to Rust type } +// `synthetic_table_with_text_pk` table #[derive(Debug, PartialEq, serde::Serialize, serde::Deserialize)] pub struct SyntheticTableWithTextPk { text_primary_key: String, // PRIMARY KEY ('string' maps directly to Rust type) @@ -24,6 +35,7 @@ pub struct SyntheticTableWithTextPk { int_nullable: Option, // 'integer' maps directly to Rust type } +// `synthetic_table_with_uaod_pk` table #[derive(Debug, PartialEq, serde::Serialize, serde::Deserialize)] pub struct SyntheticTableWithUaodPk { ua_on_demand_primary_key: String, // PRIMARY KEY ('string' maps directly to Rust type) @@ -33,6 +45,7 @@ pub struct SyntheticTableWithUaodPk { int_nullable: Option, // 'integer' maps directly to Rust type } +// `synthetic_table_with_constraints` table #[derive(Debug, PartialEq, serde::Serialize, serde::Deserialize)] pub struct SyntheticTableWithConstraints { text_primary_key: String, // PRIMARY KEY ('string' maps directly to Rust type) diff --git a/render/polygenix/rust/mod_test.ts b/render/polygenix/rust/mod_test.ts index 4f7e5e3c..078d06dd 100644 --- a/render/polygenix/rust/mod_test.ts +++ b/render/polygenix/rust/mod_test.ts @@ -160,17 +160,7 @@ Deno.test("Rust information model structures", async () => { Any, Any >(); - const engine = new mod.RustSerDePolygenEngine( - ctx, - pso, - ); - const imNB = new polygen.PolygenInfoModelNotebook< - Any, - SyntheticContext, - Any, - Any - >( - engine, + const imNB = new mod.RustSerDeModels( ctx, function* () { for (const [_, value] of Object.entries(syntheticSchema())) { @@ -182,9 +172,12 @@ Deno.test("Rust information model structures", async () => { pso, ); - const fixture = Deno.readTextFileSync( - path.fromFileUrl(import.meta.resolve("./mod_test-fixture-serde.rs")), + const fixtureSrcPath = path.fromFileUrl( + import.meta.resolve("./mod_test-fixture-serde.rs"), + ); + const esc = await imNB.polygenContent(); + ta.assertEquals( + await SQLa.polygenCellContent(ctx, esc), + Deno.readTextFileSync(fixtureSrcPath), ); - const esc = await imNB.entitiesSrcCode(); - ta.assertEquals(await SQLa.sourceCodeText(ctx, esc), fixture); }); diff --git a/render/polygenix/rust/serde.ts b/render/polygenix/rust/serde.ts index 173bf31d..acaa9043 100644 --- a/render/polygenix/rust/serde.ts +++ b/render/polygenix/rust/serde.ts @@ -2,11 +2,8 @@ import * as d from "../../domain/mod.ts"; import * as g from "../../graph.ts"; import * as emit from "../../emit/mod.ts"; import * as tbl from "../../ddl/table/mod.ts"; -import * as imGen from "../info-model.ts"; -import * as e from "../engine.ts"; - -// deno-lint-ignore no-explicit-any -type Any = any; +import * as im from "../info-model.ts"; +import * as e from "../governance.ts"; export function rustSerDeTypes(pt: string, options?: { readonly notFound: () => e.PolygenTypeSupplier; @@ -60,66 +57,80 @@ export function rustSerDeTypes(pt: string, options?: { return result; } -export class RustSerDePolygenEngine< +export class RustSerDeModels< Context extends emit.SqlEmitContext, DomainQS extends d.SqlDomainQS, DomainsQS extends d.SqlDomainsQS, -> implements e.PolygenEngine { - readonly #typeStrategy: e.PolygenEngineTypeStrategy; - readonly #namingStrategy: e.PolygenEngineNamingStrategy; +> implements emit.PolygenCellContentSupplier { + readonly #typeStrategy: e.PolygenTypeStrategy; + readonly #namingStrategy: e.PolygenNamingStrategy; readonly sqlNames: emit.SqlObjectNames; + readonly graph: ReturnType< + typeof g.entitiesGraph< + g.GraphEntityDefinition, + Context, + DomainQS, + DomainsQS, + g.EntitiesGraphQS + > + >; constructor( readonly sqlCtx: Context, - readonly polygenSchemaOptions: - & imGen.PolygenInfoModelOptions< + readonly entityDefns: ( + ctx: Context, + ) => Generator< + g.GraphEntityDefinition + >, + readonly polygenOptions: + & im.PolygenInfoModelOptions< Context, DomainQS, DomainsQS > - & { readonly typeStrategy?: e.PolygenEngineTypeStrategy } - & { readonly namingStrategy?: e.PolygenEngineNamingStrategy }, + & { readonly typeStrategy?: e.PolygenTypeStrategy } + & { readonly namingStrategy?: e.PolygenNamingStrategy }, ) { this.sqlNames = sqlCtx.sqlNamingStrategy(sqlCtx); - this.#typeStrategy = polygenSchemaOptions?.typeStrategy ?? ({ + this.graph = g.entitiesGraph< + g.GraphEntityDefinition, + Context, + DomainQS, + DomainsQS, + g.EntitiesGraphQS + >( + this.sqlCtx, + this.entityDefns, + ); + this.#typeStrategy = polygenOptions?.typeStrategy ?? ({ type: (pt) => rustSerDeTypes(pt), }); // Rust likes tables/views/etc. structs to be in CamelCase and field names // in snake_case; since SQL columns, etc. are already in snake_case we just // return them as is by default. - this.#namingStrategy = polygenSchemaOptions?.namingStrategy ?? ({ + this.#namingStrategy = polygenOptions?.namingStrategy ?? ({ entityName: e.snakeToPascalCase, entityAttrName: (sqlIdentifier) => sqlIdentifier, }); } - namingStrategy(): e.PolygenEngineNamingStrategy { + namingStrategy(): e.PolygenNamingStrategy { return this.#namingStrategy; } - typeStrategy(): e.PolygenEngineTypeStrategy { + typeStrategy(): e.PolygenTypeStrategy { return this.#typeStrategy; } - async entityAttrSrcCode( + async entityAttrContent( ea: g.GraphEntityAttrReference< - Any, - Any, + string, + string, Context, DomainQS, DomainsQS >, - _e: g.GraphEntityDefinition, - _graph: ReturnType< - typeof g.entitiesGraph< - Any, - Context, - DomainQS, - DomainsQS, - g.EntitiesGraphQS - > - >, ) { const ns = this.namingStrategy(); const name = ns.entityAttrName(this.sqlNames.tableColumnName({ @@ -129,7 +140,7 @@ export class RustSerDePolygenEngine< const pgdt = ea.attr.polygenixDataType("info-model"); const types = this.typeStrategy(); const { type, remarks: pgdtRemarks } = types.type( - await emit.sourceCodeText(this.sqlCtx, pgdt), + await emit.polygenCellContent(this.sqlCtx, pgdt), ); const remarks = tbl.isTablePrimaryKeyColumnDefn(ea.attr) ? (pgdtRemarks ? `PRIMARY KEY (${pgdtRemarks})` : pgdtRemarks) @@ -138,45 +149,52 @@ export class RustSerDePolygenEngine< return ` ${name}: ${ea.attr.isNullable() ? `Option<${type}>` : type},${remarks ? ` // ${remarks}` : ''}`; } - async entitySrcCode( - entity: g.GraphEntityDefinition, - graph: ReturnType< - typeof g.entitiesGraph< - Any, - Context, - DomainQS, - DomainsQS, - g.EntitiesGraphQS - > + async entityContent( + entity: g.GraphEntityDefinition< + string, + Context, + string, + DomainQS, + DomainsQS >, ) { const columns: string[] = []; // we want to put all the primary keys at the top of the entity for (const column of entity.attributes) { const ea = { entity: entity, attr: column }; - if (!this.polygenSchemaOptions.includeEntityAttr(ea)) continue; + if (!this.polygenOptions.includeEntityAttr(ea)) continue; if (tbl.isTablePrimaryKeyColumnDefn(column)) { - columns.push(await this.entityAttrSrcCode(ea, entity, graph)); + columns.push(await this.entityAttrContent(ea)); } } for (const column of entity.attributes) { if (!tbl.isTablePrimaryKeyColumnDefn(column)) { const ea = { entity: entity, attr: column }; - if (!this.polygenSchemaOptions.includeEntityAttr(ea)) continue; - columns.push(await this.entityAttrSrcCode(ea, entity, graph)); + if (!this.polygenOptions.includeEntityAttr(ea)) continue; + columns.push(await this.entityAttrContent(ea)); } } - const entityRels = graph.entityRels.get( + const entityRels = this.graph.entityRels.get( entity.identity("dictionary-storage"), ); const children: string[] = []; if (entityRels && entityRels.inboundRels.length > 0) { for (const ir of entityRels.inboundRels) { if (typeof ir.nature === "object" && ir.nature.isBelongsTo) { - if (!this.polygenSchemaOptions.includeChildren(ir)) continue; - children.push(`// TODO: children -> ${ir.nature.collectionName}`); + if (!this.polygenOptions.includeChildren(ir)) continue; + const collectionName = ir.nature.collectionName ?? + emit.jsSnakeCaseToken(ir.from.entity.identity("presentation")); + children.push( + ` ${ + collectionName(this.sqlCtx, "plural", "rust-struct-member-decl") + }: Vec<${ + collectionName(this.sqlCtx, "singular", "rust-type-decl") + }>, // \`${ + ir.from.entity.identity("presentation") + }\` belongsTo collection`, + ); } } } @@ -186,6 +204,7 @@ export class RustSerDePolygenEngine< this.sqlNames.tableName(entity.identity("presentation")), ); return [ + `// \`${entity.identity("presentation")}\` table`, `#[derive(Debug, PartialEq, serde::Serialize, serde::Deserialize)]`, `pub struct ${structName} {`, ...columns, @@ -194,4 +213,37 @@ export class RustSerDePolygenEngine< "", ]; } + + async polygenContent() { + const entitiesSrcCode: string[] = []; + + entitiesSrcCode.push("/*"); + for (const entity of this.graph.entities) { + if (!this.polygenOptions.includeEntity(entity)) { + continue; + } + + entitiesSrcCode.push( + `const ${ + e.snakeToConstantCase(entity.identity("presentation")) + }: &str = "${entity.identity("presentation")}";`, + ); + } + entitiesSrcCode.push("*/"); + entitiesSrcCode.push(""); + + for (const entity of this.graph.entities) { + if (!this.polygenOptions.includeEntity(entity)) { + continue; + } + + const sc = await this.entityContent(entity); + entitiesSrcCode.push(await emit.polygenCellContent(this.sqlCtx, sc)); + } + + const pscSupplier: emit.PolygenCellContent = entitiesSrcCode.join( + "\n", + ); + return pscSupplier; + } }