@embracesql/postgres
Advanced tools
Comparing version 0.0.4 to 0.0.6
{ | ||
"name": "@embracesql/postgres", | ||
"version": "0.0.4", | ||
"version": "0.0.6", | ||
"description": "EmbraceSQL shared library for talking to postgres. Used in Node.", | ||
@@ -13,8 +13,7 @@ "type": "module", | ||
"dependencies": { | ||
"@embracesql/shared": "^0.0.4", | ||
"@embracesql/shared": "^0.0.6", | ||
"glob": "^10.3.10", | ||
"object-hash": "^3.0.0", | ||
"pg-connection-string": "^2.6.2", | ||
"postgres": "^3.4.3", | ||
"prettier": "3.1.1" | ||
"postgres": "^3.4.3" | ||
}, | ||
@@ -24,3 +23,3 @@ "devDependencies": { | ||
}, | ||
"gitHead": "343e5761ee3ff99619a2ba34c18139d0ef6240ad" | ||
"gitHead": "924be0edd5fb5b1941d1c9160198ddfda5b93b47" | ||
} |
import { PGAttributes } from "./generator/pgtype/pgattribute"; | ||
import { PGCatalogType } from "./generator/pgtype/pgcatalogtype"; | ||
import { PGIndexes } from "./generator/pgtype/pgindex"; | ||
@@ -9,14 +8,15 @@ import { PGNamespace } from "./generator/pgtype/pgnamespace"; | ||
import { PGTypeEnumValues } from "./generator/pgtype/pgtypeenum"; | ||
import { oneBasedArgumentNamefromZeroBasedIndex } from "./util"; | ||
import { | ||
ColumnNode, | ||
PARAMETERS, | ||
ASTKind, | ||
ASTNode, | ||
AttributeNode, | ||
CompositeTypeNode, | ||
DatabaseNode, | ||
IndexColumnNode, | ||
IndexNode, | ||
IsNamed, | ||
SchemaNode, | ||
TableNode, | ||
TablesNode, | ||
TypeNode, | ||
TypesNode, | ||
GenerationContextProps, | ||
RESULTS, | ||
ScriptsNode, | ||
} from "@embracesql/shared"; | ||
import path from "path"; | ||
import pgconnectionstring from "pg-connection-string"; | ||
@@ -92,3 +92,3 @@ import postgres from "postgres"; | ||
type ConnectionProps = { | ||
type ConnectionStringProps = { | ||
[Property in keyof pgconnectionstring.ConnectionOptions]: NonNullable< | ||
@@ -99,2 +99,5 @@ pgconnectionstring.ConnectionOptions[Property] | ||
type InitializeContextProps = postgres.Options<never> & | ||
Partial<GenerationContextProps>; | ||
/** | ||
@@ -109,4 +112,11 @@ * A single context is passed through as a shared blackboard to | ||
*/ | ||
export const initializeContext = async (postgresUrl = DEFAULT_POSTGRES_URL) => { | ||
const parsed = pgconnectionstring.parse(postgresUrl) as ConnectionProps; | ||
export const initializeContext = async ( | ||
postgresUrl = DEFAULT_POSTGRES_URL, | ||
props?: InitializeContextProps, | ||
) => { | ||
// props leaking in .database will cause a connection failure that is | ||
// confusing to read -- it'll look like a proper URL that really does | ||
// exist, trouble is it just doesn't match the PARAMETERS to `postgres` | ||
delete props?.database; | ||
const parsed = pgconnectionstring.parse(postgresUrl) as ConnectionStringProps; | ||
// little tweaks of types | ||
@@ -122,2 +132,3 @@ const connection = { | ||
prepare: false, | ||
...(props ?? {}), | ||
}); | ||
@@ -176,2 +187,6 @@ /** | ||
const database = new DatabaseNode(databaseName); | ||
const generationContext = { | ||
...props, | ||
database, | ||
}; | ||
// ok, this is a bit tricky since - tables and types can cross namespaces | ||
@@ -181,62 +196,71 @@ // so the first pass will set up all the schemas from catalog namespaces | ||
namespaces.forEach((n) => { | ||
const schema = new SchemaNode(database, n.namespace); | ||
database.children.push(schema); | ||
const types = new TypesNode(schema); | ||
schema.children.push(types); | ||
// all types in the namespace | ||
n.types.forEach((t) => { | ||
const type = new TypeNode(t.typescriptName, types, t.oid, t); | ||
database.registerType(type.id, type); | ||
types.children.push(type); | ||
}); | ||
n.types.forEach((t) => t.loadAST(generationContext)); | ||
}); | ||
// ok -- we now know all types -- now we have enough information to make tables | ||
// now we have an initial generation context that can resolve types | ||
// we now know all types -- now we have enough information to load the | ||
// AST with database schema objects - tables, columns, indexes | ||
namespaces.forEach((n) => n.loadAST(generationContext)); | ||
// second pass now that all types are registered | ||
namespaces.forEach((n) => { | ||
const schema = database.children.find( | ||
(c) => (c as unknown as IsNamed)?.name === n.namespace, | ||
) as SchemaNode; | ||
if (schema) { | ||
const tables = new TablesNode(schema); | ||
schema.children.push(tables); | ||
n.tables.forEach((t) => { | ||
// the table and ... | ||
const table = new TableNode(tables, t.table.relname); | ||
tables.children.push(table); | ||
// it's columns | ||
t.tableType.attributes.forEach((a) => { | ||
const typeNode = database.resolveType(a.attribute.atttypid); | ||
if (typeNode) { | ||
table.children.push(new ColumnNode(table, a.name, typeNode)); | ||
} else { | ||
throw new Error( | ||
`${a.name} cannot find type ${a.attribute.atttypid}`, | ||
// all types in the namespace | ||
n.types.forEach((t) => t.finalizeAST(generationContext)); | ||
}); | ||
// stored scripts -- load up the AST | ||
await ScriptsNode.loadAST(generationContext); | ||
// visit all scripts and ask the database for metadata | ||
// we'll be discarding the string results | ||
await database.visit({ | ||
...generationContext, | ||
handlers: { | ||
[ASTKind.Script]: { | ||
before: async (context, node) => { | ||
// these are not 'data base catalog types' | ||
// -- do not register them with the database | ||
// there is no actual database object or oid | ||
const scriptPath = path.join(node.path.dir, node.path.base); | ||
const metadata = await sql.file(scriptPath).describe(); | ||
const resultsNode = new CompositeTypeNode(RESULTS, node, ""); | ||
metadata.columns.forEach( | ||
(a, i) => | ||
new AttributeNode( | ||
resultsNode, | ||
a.name, | ||
i, | ||
context.database.resolveType(a.type)!, | ||
true, | ||
true, | ||
), | ||
); | ||
if (metadata.types.length) { | ||
const PARAMETERSNode = new CompositeTypeNode(PARAMETERS, node, ""); | ||
metadata.types.forEach( | ||
(a, i) => | ||
new AttributeNode( | ||
PARAMETERSNode, | ||
// these don't have natural names, just positions | ||
// so manufacture names | ||
oneBasedArgumentNamefromZeroBasedIndex(i), | ||
i, | ||
context.database.resolveType(a)!, | ||
true, | ||
false, | ||
), | ||
); | ||
} | ||
}); | ||
// indexes go on the table as well | ||
t.indexes.forEach((i) => { | ||
const index = new IndexNode( | ||
table, | ||
i.name, | ||
i.index.indisunique, | ||
i.index.indisprimary, | ||
); | ||
i.attributes.forEach((a) => { | ||
const typeNode = database.resolveType(a.attribute.atttypid); | ||
if (typeNode) { | ||
index.children.push(new IndexColumnNode(index, a.name, typeNode)); | ||
} else { | ||
throw new Error( | ||
`${a.name} cannot find type ${a.attribute.atttypid}`, | ||
); | ||
} | ||
}); | ||
table.children.push(index); | ||
}); | ||
}); | ||
} else { | ||
throw new Error(`cannot find namespace ${n.namespace}`); | ||
} | ||
return ""; | ||
}, | ||
}, | ||
}, | ||
}); | ||
// map in procedure calls | ||
await procCatalog.loadAST(generationContext); | ||
// sanity check and verification of all created nodes | ||
ASTNode.verify(); | ||
// now we set up a new sql that can do type marshalling - runtime data | ||
@@ -251,4 +275,4 @@ // from the database is complete | ||
// type resolvers that can parse composite and RETURNS TABLE types at runtime | ||
const resolveType = <T extends PGCatalogType>(oid: number) => { | ||
return typeCatalog.typesByOid[oid] as T; | ||
const resolveType = (oid: number) => { | ||
return typeCatalog.typesByOid[oid] ?? procCatalog.pseudoTypesByOid[oid]; | ||
}; | ||
@@ -265,6 +289,13 @@ const context = { | ||
// expand out the type resolvers for all types | ||
typeCatalog.types.forEach( | ||
(t) => (types[t.postgresMarshallName] = t.postgresTypecast(context)), | ||
); | ||
// expand out the type resolvers for all types -- these are used by | ||
// the postgres driver to encode/decodej | ||
typeCatalog.types.forEach((t) => { | ||
// by oid -- postgres style | ||
types[t.oid] = t.postgresTypecast(context); | ||
// by name -- typescript generated style | ||
const typeNode = database.resolveType(t.oid); | ||
if (typeNode) { | ||
types[typeNode.typescriptNamespacedName] = t.postgresTypecast(context); | ||
} | ||
}); | ||
// and resolvers for procs, which have their own pseudo types as return types | ||
@@ -275,3 +306,9 @@ namespaces | ||
.forEach((p) => { | ||
procTypes[p.postgresMarshallName] = p; | ||
// by oid -- postgres style | ||
procTypes[p.proc.oid] = p; | ||
// by name -- typescript generated style | ||
const typeNode = database.resolveType(p.proc.oid); | ||
if (typeNode) { | ||
procTypes[typeNode.typescriptNamespacedName] = p; | ||
} | ||
}); | ||
@@ -278,0 +315,0 @@ |
import { Context } from "../context"; | ||
import { GenerationContext as GC } from "@embracesql/shared"; | ||
import * as prettier from "prettier"; | ||
@@ -16,15 +15,1 @@ export { generateDatabaseRoot } from "./typescript/generateDatabaseRoot"; | ||
export type GenerationContext = Context & GC; | ||
//TODO - this does not belong in postgres | ||
/** | ||
* Make that generated source 💄. | ||
*/ | ||
export const formatSource = async (source: string) => { | ||
try { | ||
return await prettier.format(source, { parser: "typescript" }); | ||
} catch { | ||
// no format -- we'll need it to debug then | ||
return source; | ||
} | ||
}; |
@@ -12,17 +12,19 @@ import { Context } from "../../.."; | ||
return ` | ||
if (from === null) return null; | ||
if(['t', 'T', 'true', 'True'].includes(from)) return true; | ||
try { | ||
if (Number.parseFloat(from) > 0) return true; | ||
} catch (e) { | ||
// eat | ||
if (typeof from === "string") { | ||
if(['t', 'T', 'true', 'True'].includes(from)) return true; | ||
try { | ||
if (Number.parseFloat(from) > 0) return true; | ||
} catch (e) { | ||
// eat | ||
} | ||
} | ||
if (typeof from === "number") { | ||
return from !== 0; | ||
} | ||
return false; | ||
`; | ||
} | ||
typescriptTypeDefinition(context: Context) { | ||
typescriptTypeDefinition(context: GenerationContext) { | ||
console.assert(context); | ||
return ` | ||
export type ${this.typescriptName} = boolean; | ||
`; | ||
return `boolean`; | ||
} | ||
@@ -29,0 +31,0 @@ serializeToPostgres(context: Context, x: unknown) { |
@@ -12,7 +12,5 @@ import { Context } from "../../../context"; | ||
export class PGTypeNumber extends PGCatalogType { | ||
typescriptTypeDefinition(context: Context) { | ||
typescriptTypeDefinition(context: GenerationContext) { | ||
console.assert(context); | ||
return ` | ||
export type ${this.typescriptName} = number; | ||
`; | ||
return `number`; | ||
} | ||
@@ -23,4 +21,9 @@ | ||
return ` | ||
if (from === null) return null; | ||
if (typeof from === "string"){ | ||
return Number.parseFloat(from); | ||
} | ||
if (typeof from === "number") { | ||
return from; | ||
} | ||
return null; | ||
`; | ||
@@ -57,13 +60,19 @@ } | ||
return ` | ||
if (from === null) return null; | ||
if (typeof from === "bigint") { | ||
return from; | ||
} | ||
if (typeof from === "number") { | ||
return BigInt(from); | ||
} | ||
if (typeof from === "string") { | ||
if (from === '') return null; | ||
return BigInt(from); | ||
} | ||
return null; | ||
`; | ||
} | ||
typescriptTypeDefinition(context: Context) { | ||
typescriptTypeDefinition(context: GenerationContext) { | ||
console.assert(context); | ||
return ` | ||
export type ${this.typescriptName} = bigint; | ||
`; | ||
return `bigint`; | ||
} | ||
@@ -84,11 +93,15 @@ | ||
return ` | ||
return from ? new Uint8Array(JSON.parse(from)) : null; | ||
if (typeof from === "string"){ | ||
return new Uint8Array(JSON.parse(from)); | ||
} | ||
if (Array.isArray(from)) { | ||
return new Uint8Array(from); | ||
} | ||
return []; | ||
`; | ||
} | ||
typescriptTypeDefinition(context: Context) { | ||
typescriptTypeDefinition(context: GenerationContext) { | ||
console.assert(context); | ||
return ` | ||
export type ${this.typescriptName} = Uint8Array; | ||
`; | ||
return `Uint8Array`; | ||
} | ||
} |
@@ -1,3 +0,3 @@ | ||
import { Context } from "../../.."; | ||
import { PGCatalogType } from "../pgcatalogtype"; | ||
import { GenerationContext } from "@embracesql/shared"; | ||
@@ -9,6 +9,16 @@ /** | ||
export class PGTypeText extends PGCatalogType { | ||
typescriptTypeDefinition(context: Context) { | ||
typescriptTypeDefinition(context: GenerationContext) { | ||
console.assert(context); | ||
return `string`; | ||
} | ||
typescriptTypeParser(context: GenerationContext) { | ||
console.assert(context); | ||
// parsing a string doesn't really make 'sense' in that | ||
// a string is expected to be a string | ||
return ` | ||
export type ${this.typescriptName} = string; | ||
if (typeof from === "string") { | ||
return from; | ||
} | ||
throw new Error(\`from is not a string\`, {cause: from}); | ||
`; | ||
@@ -22,8 +32,6 @@ } | ||
export class PGTypeTextArray extends PGCatalogType { | ||
typescriptTypeDefinition(context: Context) { | ||
typescriptTypeDefinition(context: GenerationContext) { | ||
console.assert(context); | ||
return ` | ||
export type ${this.typescriptName} = Array<string>; | ||
`; | ||
return `Array<string>`; | ||
} | ||
} |
import { GenerationContext } from "../.."; | ||
import { Context } from "../../.."; | ||
import { PGCatalogType } from "../pgcatalogtype"; | ||
@@ -9,11 +8,9 @@ | ||
return ` | ||
return from ? new URL(from) : null; | ||
return new URL(from); | ||
`; | ||
} | ||
typescriptTypeDefinition(context: Context) { | ||
typescriptTypeDefinition(context: GenerationContext) { | ||
console.assert(context); | ||
return ` | ||
export type ${this.typescriptName} = URL; | ||
`; | ||
return `URL`; | ||
} | ||
} |
@@ -13,12 +13,14 @@ /* eslint-disable @typescript-eslint/no-unsafe-return */ | ||
return ` | ||
if (from === null) return null; | ||
const source = Array.isArray(from) ? new Float32Array(from) : JSON.parse(from); | ||
return new Float32Array(source); | ||
if (typeof from === "string"){ | ||
return new Float32Array(JSON.parse(from)); | ||
} | ||
if (Array.isArray(from)) { | ||
return new Float32Array(from); | ||
} | ||
return null; | ||
`; | ||
} | ||
typescriptTypeDefinition(context: Context) { | ||
typescriptTypeDefinition(context: GenerationContext) { | ||
console.assert(context); | ||
return ` | ||
export type ${this.typescriptName} = Float32Array; | ||
`; | ||
return `Float32Array`; | ||
} | ||
@@ -25,0 +27,0 @@ |
@@ -10,13 +10,10 @@ import { Context } from "../../../context"; | ||
return ` | ||
if (from === null) return null; | ||
if ((from as unknown) instanceof global.Date) return from; | ||
return new global.Date(from); | ||
if ((from as unknown) instanceof global.Date) return from as Date; | ||
return new global.Date(from as string); | ||
`; | ||
} | ||
typescriptTypeDefinition(context: Context) { | ||
typescriptTypeDefinition(context: GenerationContext) { | ||
console.assert(context); | ||
// date ends up with a type alias | ||
return ` | ||
export type ${this.typescriptName} = JsDate; | ||
`; | ||
return `JsDate`; | ||
} | ||
@@ -23,0 +20,0 @@ |
@@ -1,13 +0,13 @@ | ||
import { Context } from "../../../../context"; | ||
import { PGCatalogType } from "../../pgcatalogtype"; | ||
import { registerOverride } from "../_overrides"; | ||
import { GenerationContext } from "@embracesql/shared"; | ||
class PGTypeBox extends PGCatalogType { | ||
typescriptTypeDefinition(context: Context) { | ||
typescriptTypeDefinition(context: GenerationContext) { | ||
console.assert(context); | ||
return ` | ||
export type ${this.typescriptName} = { | ||
{ | ||
upperRight: Point; | ||
lowerLeft: Point; | ||
}; | ||
} | ||
`; | ||
@@ -14,0 +14,0 @@ } |
@@ -1,13 +0,13 @@ | ||
import { Context } from "../../../.."; | ||
import { PGTypeBase } from "../../pgtypebase"; | ||
import { registerOverride } from "../_overrides"; | ||
import { GenerationContext } from "@embracesql/shared"; | ||
class PGTypeCircle extends PGTypeBase { | ||
typescriptTypeDefinition(context: Context) { | ||
typescriptTypeDefinition(context: GenerationContext) { | ||
console.assert(context); | ||
return ` | ||
export type ${this.typescriptName} = { | ||
{ | ||
center: Point; | ||
radius: number; | ||
}; | ||
} | ||
`; | ||
@@ -14,0 +14,0 @@ } |
@@ -1,14 +0,14 @@ | ||
import { Context } from "../../../../context"; | ||
import { PGCatalogType } from "../../pgcatalogtype"; | ||
import { registerOverride } from "../_overrides"; | ||
import { GenerationContext } from "@embracesql/shared"; | ||
class PGLine extends PGCatalogType { | ||
typescriptTypeDefinition(context: Context) { | ||
typescriptTypeDefinition(context: GenerationContext) { | ||
console.assert(context); | ||
return ` | ||
export type ${this.typescriptName} = { | ||
{ | ||
a: number; | ||
b: number; | ||
c: number; | ||
}; | ||
} | ||
`; | ||
@@ -15,0 +15,0 @@ } |
@@ -1,13 +0,13 @@ | ||
import { Context } from "../../../../context"; | ||
import { PGCatalogType } from "../../pgcatalogtype"; | ||
import { registerOverride } from "../_overrides"; | ||
import { GenerationContext } from "@embracesql/shared"; | ||
class PGLineSegment extends PGCatalogType { | ||
typescriptTypeDefinition(context: Context) { | ||
typescriptTypeDefinition(context: GenerationContext) { | ||
console.assert(context); | ||
return ` | ||
export type ${this.typescriptName} = { | ||
{ | ||
from: Point; | ||
to: Point; | ||
}; | ||
} | ||
`; | ||
@@ -14,0 +14,0 @@ } |
@@ -1,13 +0,13 @@ | ||
import { Context } from "../../../../context"; | ||
import { PGCatalogType } from "../../pgcatalogtype"; | ||
import { registerOverride } from "../_overrides"; | ||
import { GenerationContext } from "@embracesql/shared"; | ||
class PGTypePoint extends PGCatalogType { | ||
typescriptTypeDefinition(context: Context) { | ||
typescriptTypeDefinition(context: GenerationContext) { | ||
console.assert(context); | ||
return ` | ||
export type ${this.typescriptName} = { | ||
{ | ||
x: number; | ||
y: number; | ||
}; | ||
} | ||
`; | ||
@@ -18,7 +18,5 @@ } | ||
class PGTypePointArray extends PGCatalogType { | ||
typescriptTypeDefinition(context: Context) { | ||
typescriptTypeDefinition(context: GenerationContext) { | ||
console.assert(context); | ||
return ` | ||
export type ${this.typescriptName} = Array<Point>; | ||
`; | ||
return `Array<Point>`; | ||
} | ||
@@ -25,0 +23,0 @@ } |
@@ -7,9 +7,8 @@ /* eslint-disable @typescript-eslint/no-unsafe-return */ | ||
import { registerOverride } from "./_overrides"; | ||
import { GenerationContext } from "@embracesql/shared"; | ||
class PGJson extends PGCatalogType { | ||
typescriptTypeDefinition(context: Context) { | ||
typescriptTypeDefinition(context: GenerationContext) { | ||
console.assert(context); | ||
return ` | ||
export type ${this.typescriptName} = JSONObject; | ||
`; | ||
return `JSONObject`; | ||
} | ||
@@ -16,0 +15,0 @@ |
@@ -10,10 +10,8 @@ import { Context } from "../../../context"; | ||
return ` | ||
return from ? new UUID(from) : null; | ||
return new UUID(from as string); | ||
`; | ||
} | ||
typescriptTypeDefinition(context: Context) { | ||
typescriptTypeDefinition(context: GenerationContext) { | ||
console.assert(context); | ||
return ` | ||
export type ${this.typescriptName} = UUID; | ||
`; | ||
return `UUID`; | ||
} | ||
@@ -20,0 +18,0 @@ |
@@ -10,12 +10,14 @@ import { Context } from "../../../context"; | ||
return ` | ||
if (from === null) return null; | ||
const source = Array.isArray(from) ? new Uint16Array(from) : JSON.parse(from); | ||
return new Uint16Array(source); | ||
if (typeof from === "string") { | ||
return new Uint16Array(JSON.parse(from)); | ||
} | ||
if (Array.isArray(from)) { | ||
return new Uint16Array(from); | ||
} | ||
return []; | ||
`; | ||
} | ||
typescriptTypeDefinition(context: Context) { | ||
typescriptTypeDefinition(context: GenerationContext) { | ||
console.assert(context); | ||
return ` | ||
export type ${this.typescriptName} = Uint16Array; | ||
`; | ||
return `Uint16Array`; | ||
} | ||
@@ -39,12 +41,14 @@ // eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
return ` | ||
if (from === null) return null; | ||
const source = Array.isArray(from) ? new Float32Array(from) : JSON.parse(from); | ||
return new Float32Array(source); | ||
if (typeof from === "string") { | ||
return new Float32Array(JSON.parse(from)); | ||
} | ||
if (Array.isArray(from)) { | ||
return new Float32Array(from); | ||
} | ||
return []; | ||
`; | ||
} | ||
typescriptTypeDefinition(context: Context) { | ||
typescriptTypeDefinition(context: GenerationContext) { | ||
console.assert(context); | ||
return ` | ||
export type ${this.typescriptName} = Float32Array; | ||
`; | ||
return `Float32Array`; | ||
} | ||
@@ -51,0 +55,0 @@ |
import { Context } from "../../context"; | ||
import { groupBy } from "../../util"; | ||
import { PGTypeComposite } from "./pgtypecomposite"; | ||
import { cleanIdentifierForTypescript } from "@embracesql/shared"; | ||
import { camelCase } from "change-case"; | ||
@@ -79,3 +80,3 @@ import path from "path"; | ||
// camel case -- this is a 'property like' | ||
return `${camelCase(this.attribute.attname)}`; | ||
return `${camelCase(cleanIdentifierForTypescript(this.attribute.attname))}`; | ||
} | ||
@@ -87,15 +88,2 @@ | ||
typescriptTypeDefinition(context: Context) { | ||
// nullability, but otherwise delegate to the type of the attribute | ||
const underlyingType = | ||
context | ||
.resolveType(this.attribute.atttypid) | ||
?.typescriptNameWithNamespace(context) ?? "void"; | ||
if (this.attribute.attnotnull) { | ||
return underlyingType; | ||
} else { | ||
return `Nullable<${underlyingType}>`; | ||
} | ||
} | ||
/** | ||
@@ -118,3 +106,3 @@ * Render a code generation string that will create a postgres 'right hand side' | ||
: "sql`DEFAULT`"; | ||
const valueExpression = `typed.${postgresType.postgresMarshallName}(${parameterHolder}.${this.typescriptName})`; | ||
const valueExpression = `typed[${postgresType.oid}](${parameterHolder}.${this.typescriptName})`; | ||
const combinedExpression = `${parameterHolder}.${this.typescriptName} === undefined ? ${undefinedExpression} : ${valueExpression}`; | ||
@@ -121,0 +109,0 @@ return `\${ ${combinedExpression} }`; |
import { Context, PostgresTypecast } from "../../context"; | ||
import { asDocComment } from "../../util"; | ||
import { CatalogRow } from "./pgtype"; | ||
import { | ||
GeneratesTypeScriptParser, | ||
GeneratesTypeScript, | ||
GenerationContext, | ||
TypeNode, | ||
} from "@embracesql/shared"; | ||
import { pascalCase } from "change-case"; | ||
@@ -13,7 +12,5 @@ /** | ||
*/ | ||
export class PGCatalogType implements GeneratesTypeScriptParser { | ||
export class PGCatalogType implements GeneratesTypeScript { | ||
/** | ||
* Base constructions picks out the name. | ||
* | ||
* @param catalog | ||
*/ | ||
@@ -26,2 +23,26 @@ constructor( | ||
/** | ||
* First pass load this type into the AST within the passed `context`. | ||
*/ | ||
loadAST(context: GenerationContext) { | ||
const schema = context.database.resolveSchema(this.catalog.nspname); | ||
const type = new TypeNode( | ||
this.catalog.typname, | ||
schema.types, | ||
this.oid, | ||
this, | ||
); | ||
context.database.registerType(type.id, type); | ||
} | ||
/** | ||
* Types are a connected graph, not just a tree, so two passes | ||
* are required to build cross references and back links. | ||
*/ | ||
finalizeAST(context: GenerationContext) { | ||
console.assert(context); | ||
// default is no operation | ||
} | ||
/** | ||
* The all powerful oid. | ||
@@ -44,59 +65,16 @@ */ | ||
/** | ||
* Convention is pascal case for TS. | ||
*/ | ||
get typescriptName() { | ||
return pascalCase(this.catalog.typname); | ||
} | ||
/** | ||
* Convention is pascal case for TS. Excludes reserved words. | ||
*/ | ||
get typescriptNamespaceName() { | ||
const formatted = pascalCase(this.catalog.nspname); | ||
return formatted; | ||
} | ||
typescriptNameWithNamespace(context: Context) { | ||
if (this.catalog.nspname === context.currentNamespace) { | ||
return this.typescriptName; | ||
} else { | ||
return `${this.typescriptNamespaceName}.${this.typescriptName}`; | ||
} | ||
} | ||
/** | ||
* Convention is snake case in PG, separating namespace(schema) from | ||
* the object (type, table, proc...) with a `.`. | ||
*/ | ||
get postgresName() { | ||
return `${this.catalog.nspname}.${this.catalog.typname}`; | ||
} | ||
/** | ||
* Marshalling name mashes the namespace and the type into one snake string. | ||
* | ||
* This doesn't look very TypeScript-y on purpose so it stands out. | ||
*/ | ||
get postgresMarshallName() { | ||
return `${this.catalog.nspname}_${this.catalog.typname}`; | ||
} | ||
/** | ||
* TypeScript source code for a type definition for this database | ||
* type. | ||
* | ||
* Good news is -- nested types can be referenced by name, so there | ||
* is no need for a 'global' type catalog to make this work. | ||
* | ||
* You are gonna need to override this. | ||
*/ | ||
typescriptTypeDefinition(context: Context) { | ||
typescriptTypeDefinition(context: GenerationContext): string { | ||
console.assert(context); | ||
return ` | ||
${asDocComment(this.comment)} | ||
export type ${this.typescriptName} = void; | ||
`; | ||
return `unknown`; | ||
} | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
/** | ||
* Given a value, turn it into postgres protocol serialization format | ||
* for use with the postgres driver. | ||
*/ | ||
serializeToPostgres(context: Context, x: unknown) { | ||
@@ -116,3 +94,3 @@ console.assert(context); | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
parseFromPostgres(context: Context, x: any) { | ||
parseFromPostgres(context: Context, x: unknown) { | ||
// default is just to echo it -- which is almost never correct | ||
@@ -119,0 +97,0 @@ // eslint-disable-next-line @typescript-eslint/no-unsafe-return |
@@ -6,2 +6,3 @@ import { Context, TypeFactoryContext } from "../../context"; | ||
import { PGTypeComposite } from "./pgtypecomposite"; | ||
import { GenerationContext, IndexNode, TableNode } from "@embracesql/shared"; | ||
import { pascalCase } from "change-case"; | ||
@@ -69,2 +70,15 @@ import path from "path"; | ||
loadAST(context: GenerationContext, table: TableNode) { | ||
new IndexNode( | ||
table, | ||
this.name, | ||
this.index.indisunique, | ||
this.index.indisprimary, | ||
this.attributes.map((a) => { | ||
const typeNode = context.database.resolveType(a.attribute.atttypid)!; | ||
return { name: a.name, type: typeNode }; | ||
}), | ||
); | ||
} | ||
get name() { | ||
@@ -103,13 +117,2 @@ return `by_${this.attributes.map((a) => a.typescriptName).join("_")}`; | ||
typescriptTypeDefinition(context: Context) { | ||
const namedValues = this.attributes.map( | ||
(a) => `${a.typescriptName}: ${a.typescriptTypeDefinition(context)} ;`, | ||
); | ||
return ` | ||
export interface ${this.typescriptName} { | ||
${namedValues.join("\n")} | ||
}; | ||
`; | ||
} | ||
/** | ||
@@ -116,0 +119,0 @@ * Code generation builder for an exact index match. |
@@ -1,8 +0,7 @@ | ||
import { Context } from "../../context"; | ||
import { groupBy } from "../../util"; | ||
import { PGCatalogType } from "./pgcatalogtype"; | ||
import { PGProc, PGProcs } from "./pgproc/pgproc"; | ||
import { PGProc, PGProcPseudoType, PGProcs } from "./pgproc/pgproc"; | ||
import { PGTable, PGTables } from "./pgtable"; | ||
import { PGTypes } from "./pgtype"; | ||
import { pascalCase } from "change-case"; | ||
import { GenerationContext, TablesNode } from "@embracesql/shared"; | ||
@@ -38,21 +37,17 @@ /** | ||
); | ||
return Object.keys(typesByNamespace).map( | ||
(namespace) => | ||
new PGNamespace( | ||
namespace, | ||
typesByNamespace[namespace] ?? [], | ||
tablesByNamespace[namespace] ?? [], | ||
procsByNamespace[namespace] ?? [], | ||
), | ||
); | ||
return Object.keys(typesByNamespace).map((namespace): PGNamespace => { | ||
return new PGNamespace( | ||
namespace, | ||
[ | ||
...typesByNamespace[namespace], | ||
...(procsByNamespace[namespace] ?? []) | ||
.filter((p) => p.returnsPseudoTypeRecord) | ||
.map((p) => new PGProcPseudoType(p)), | ||
] ?? [], | ||
tablesByNamespace[namespace] ?? [], | ||
procsByNamespace[namespace] ?? [], | ||
); | ||
}); | ||
} | ||
/** | ||
* Name formatting for typescript, which PascalCase as a sql namespace | ||
* is like a typescript namespace or nested class. | ||
*/ | ||
static typescriptName(name: string) { | ||
return pascalCase(name); | ||
} | ||
constructor( | ||
@@ -65,4 +60,8 @@ public namespace: string, | ||
get typescriptName() { | ||
return PGNamespace.typescriptName(this.namespace); | ||
loadAST(context: GenerationContext) { | ||
const schema = context.database.resolveSchema(this.nspname); | ||
const tables = new TablesNode(schema); | ||
this.tables.forEach((t) => { | ||
t.loadAST(context, tables); | ||
}); | ||
} | ||
@@ -73,19 +72,2 @@ | ||
} | ||
/** | ||
* Generate typescript source for this namespace. | ||
*/ | ||
typescriptTypeDefinition(context: Context) { | ||
return ` | ||
export namespace ${this.typescriptName} { | ||
${this.types.map((t) => t.typescriptTypeDefinition(context)).join("\n")} | ||
${this.procs.map((t) => t.typescriptTypeDefinition(context)).join("\n")} | ||
export namespace Tables { | ||
${this.tables | ||
.map((t) => t.typescriptTypeDefinition(context)) | ||
.join("\n")} | ||
} | ||
} | ||
`; | ||
} | ||
} |
import { Context, PostgresProcTypecast } from "../../../context"; | ||
import { buildTypescriptParameterName } from "../../../util"; | ||
import { PGNamespace } from "../pgnamespace"; | ||
import { PGCatalogType } from "../pgcatalogtype"; | ||
import { PGTypes } from "../pgtype"; | ||
import { generateRequestType } from "./generateRequestType"; | ||
import { generateResponseType } from "./generateResponseType"; | ||
import { | ||
PARAMETERS, | ||
AliasTypeNode, | ||
AttributeNode, | ||
CompositeTypeNode, | ||
GenerationContext, | ||
ProcedureNode, | ||
RESULTS, | ||
compositeAttribute, | ||
parseObjectWithAttributes, | ||
} from "@embracesql/shared"; | ||
import { camelCase, pascalCase } from "change-case"; | ||
import { camelCase } from "change-case"; | ||
import hash from "object-hash"; | ||
@@ -52,5 +56,63 @@ import parsimmon from "parsimmon"; | ||
procs: PGProc[]; | ||
pseudoTypesByOid: Record<number, PGProcPseudoType>; | ||
private constructor(context: PGProcsContext, procRows: ProcRow[]) { | ||
this.procs = procRows.map((r) => new PGProc(context, r)); | ||
this.pseudoTypesByOid = Object.fromEntries( | ||
this.procs.map((t) => [t.proc.oid, new PGProcPseudoType(t)]), | ||
); | ||
} | ||
async loadAST(context: GenerationContext) { | ||
for (const proc of this.procs) { | ||
const schemaNode = context.database.resolveSchema(proc.nspname); | ||
const procsNode = schemaNode.procedures; | ||
const returnsAttributes = new PGProcPseudoType(proc).pseudoTypeAttributes( | ||
context, | ||
); | ||
// by the time we are generating procedures, we have already made | ||
// a first pass over types, so the result type should be available | ||
// hence the ! | ||
const procReturnType = (() => { | ||
if (returnsAttributes.length === 1) { | ||
// just need the single type as is | ||
// single attribute, table of one column which is | ||
// array like results | ||
return returnsAttributes[0].type; | ||
} else { | ||
// resolve the pseudo type composite for the proc to | ||
// contain multiple attributes -- table like results | ||
return context.database.resolveType( | ||
proc.returnsPseudoTypeRecord ? proc.proc.oid : proc.proc.prorettype, | ||
)!; | ||
} | ||
})(); | ||
const procNode = new ProcedureNode( | ||
proc.name, | ||
procsNode, | ||
proc.proc.oid, | ||
proc.proc.proname, | ||
proc.returnsPseudoTypeRecord || proc.returnsSet, | ||
proc.returnsPseudoTypeRecord, | ||
); | ||
// inputs | ||
const parametersNode = new CompositeTypeNode(PARAMETERS, procNode, ""); | ||
proc.proc.proargtypes | ||
.flatMap((t) => t) | ||
.forEach((oid, i) => { | ||
const type = context.database.resolveType(oid)!; | ||
new AttributeNode( | ||
parametersNode, | ||
proc.proc.proargnames[i] ?? "", | ||
i, | ||
type, | ||
i > proc.proc.proargtypes.length - proc.proc.pronargdefaults, | ||
true, | ||
); | ||
}); | ||
// outputs | ||
new AliasTypeNode(RESULTS, procReturnType, procNode); | ||
} | ||
} | ||
} | ||
@@ -78,56 +140,8 @@ | ||
get typescriptName() { | ||
const seed = pascalCase(this.proc.proname); | ||
get name() { | ||
const seed = this.proc.proname; | ||
const stem = hash(this.proc.proargtypes.flatMap((x) => x)).substring(0, 4); | ||
return this.overloaded ? `${seed}${stem}` : seed; | ||
return this.overloaded ? `${seed}_${stem}` : seed; | ||
} | ||
get typescriptNameForNamespace() { | ||
return PGNamespace.typescriptName(this.proc.nspname); | ||
} | ||
typescriptNameForResponse(withNamespace = false) { | ||
return ( | ||
(withNamespace ? `${this.typescriptNameForNamespace}.` : "") + | ||
`${this.typescriptName}Response` | ||
); | ||
} | ||
typescriptNameForPostgresArguments(withNamespace = false) { | ||
return ( | ||
(withNamespace ? `${this.typescriptNameForNamespace}.` : "") + | ||
`${this.typescriptName}Arguments` | ||
); | ||
} | ||
typescriptNameForPostgresResult(withNamespace = false) { | ||
if (this.returnsPseudoTypeRecord || this.returnsSet) { | ||
return this.typescriptNameForPostgresResultset(withNamespace); | ||
} else { | ||
return this.typescriptNameForPostgresResultsetRecord(withNamespace); | ||
} | ||
} | ||
typescriptNameForPostgresResultsetRecord(withNamespace = false) { | ||
return ( | ||
(withNamespace ? `${this.typescriptNameForNamespace}.` : "") + | ||
`${this.typescriptName}SingleResultsetRecord` | ||
); | ||
} | ||
typescriptNameForPostgresResultset(withNamespace = false) { | ||
return ( | ||
(withNamespace ? `${this.typescriptNameForNamespace}.` : "") + | ||
`${this.typescriptName}Resultset` | ||
); | ||
} | ||
get postgresName() { | ||
return `${this.proc.nspname}.${this.proc.proname}`; | ||
} | ||
get resultsetName() { | ||
return `${this.proc.proname}`; | ||
} | ||
get returnsSet() { | ||
@@ -137,11 +151,2 @@ return this.proc.proretset; | ||
/** | ||
* Marshalling name mashes the namespace and the type into one snake string. | ||
* | ||
* This doesn't look very TypeScript-y on purpose so it stands out. | ||
*/ | ||
get postgresMarshallName() { | ||
return `${this.proc.nspname}_${this.proc.proname}`; | ||
} | ||
get returnsPseudoTypeRecord() { | ||
@@ -154,16 +159,2 @@ return ( | ||
pseudoTypeAttributes(context: Context) { | ||
const skipThisMany = this.proc.proargtypes.flatMap((x) => x).length; | ||
return this.proc.proallargtypes | ||
.flatMap((x) => x) | ||
.map((oid, i) => { | ||
const type = context.resolveType(oid); | ||
return { | ||
name: this.proc.proargnames[i], | ||
type, | ||
}; | ||
}) | ||
.slice(skipThisMany); | ||
} | ||
/** | ||
@@ -177,72 +168,79 @@ * Pseudo types coming back from a proc are all 'record'. So -- they don't | ||
*/ | ||
parseFromPostgresIfRecord(context: Context, x: string) { | ||
parseFromPostgresIfRecord(context: Context, x: unknown) { | ||
const attributes = new PGProcPseudoType(this).pseudoTypeAttributes(context); | ||
// only one attribute, then there is no record type - just a setof a single type | ||
if (attributes.length === 1) { | ||
return x; | ||
} | ||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return | ||
// have parsimmon pick out an object right from our metadata | ||
const attributes = this.pseudoTypeAttributes(context).map( | ||
(a) => | ||
[ | ||
camelCase(a.name), | ||
compositeAttribute.map((parsedAttributeText) => | ||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return | ||
a.type.parseFromPostgres(context, parsedAttributeText), | ||
), | ||
] as [string, parsimmon.Parser<string | null>], | ||
); | ||
const parseAttributes = attributes.map((a) => { | ||
// need to postgres side type to get the postgres protocol parser | ||
const postgresAttribute = context.resolveType(a.type?.id as number); | ||
return [ | ||
camelCase(a.name), | ||
compositeAttribute.map((parsedAttributeText) => | ||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return | ||
postgresAttribute.parseFromPostgres(context, parsedAttributeText), | ||
), | ||
] as [string, parsimmon.Parser<string | null>]; | ||
}); | ||
return parseObjectWithAttributes(attributes, x); | ||
return parseObjectWithAttributes(parseAttributes, x as string); | ||
} | ||
} | ||
/** | ||
* TypeScript source code for: | ||
* - request messages that encapsulates a proc's parameters | ||
* - response messages from a proc's return | ||
* | ||
*/ | ||
typescriptTypeDefinition(context: Context) { | ||
return ` | ||
${generateRequestType(this, context)} | ||
${generateResponseType(this, context)} | ||
`; | ||
/** | ||
* This isn't a real catalog type, it's inferred from the parameters | ||
* by picking apart the 'input' and 'output' parameters. | ||
*/ | ||
export class PGProcPseudoType extends PGCatalogType { | ||
constructor(public proc: PGProc) { | ||
super({ | ||
oid: proc.proc.oid, | ||
nspname: proc.proc.nspname, | ||
typname: `${proc.proc.proname}_results`, | ||
typbasetype: 0, | ||
typelem: 0, | ||
rngsubtype: 0, | ||
typcategory: "", | ||
typoutput: "", | ||
typrelid: 0, | ||
typtype: "", | ||
}); | ||
} | ||
/** | ||
* Build up the string that is the call / argument pass to a database proc. | ||
*/ | ||
typescriptProcedureCallArguments(context: Context) { | ||
// won't be name value pairs in the call, but instead sql marshalling | ||
const args = this.proc.proargtypes | ||
.flatMap((t) => t) | ||
loadAST(context: GenerationContext) { | ||
const schema = context.database.resolveSchema(this.catalog.nspname); | ||
const returnsAttributes = this.pseudoTypeAttributes(context); | ||
if (returnsAttributes.length === 1) { | ||
// single attribute will already have a type available | ||
// no need to register | ||
} else { | ||
// multiple attributes creates a composite | ||
const type = new CompositeTypeNode( | ||
this.proc.name, | ||
schema.types, | ||
this.oid, | ||
); | ||
returnsAttributes.forEach( | ||
(a, i) => new AttributeNode(type, a.name, i, a.type, true, true), | ||
); | ||
context.database.registerType(type.id, type); | ||
} | ||
} | ||
pseudoTypeAttributes(context: GenerationContext) { | ||
const skipThisMany = this.proc.proc.proargtypes.flatMap((x) => x).length; | ||
return this.proc.proc.proallargtypes | ||
.flatMap((x) => x) | ||
.map((oid, i) => { | ||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion | ||
const type = context.resolveType(oid)!; | ||
const type = context.database.resolveType(oid)!; | ||
return { | ||
name: buildTypescriptParameterName(this, i), | ||
namedParameter: this.proc.proargnames[i] !== undefined, | ||
name: this.proc.proc.proargnames[i], | ||
type, | ||
}; | ||
}) | ||
.map( | ||
(a) => | ||
(a.namedParameter ? `${a.name} =>` : ``) + | ||
` \${ typed.${a.type.postgresMarshallName}(undefinedIsNull(parameters.${a.name})) }`, | ||
); | ||
return `(${args.join(",")})`; | ||
.slice(skipThisMany); | ||
} | ||
/** | ||
* Build up a string that is the return type from a database proc. | ||
* | ||
* Functions can return types or pseudo record types defined by the | ||
* pg_proc catalog. | ||
* | ||
* These can be scalars or arrays. | ||
* | ||
*/ | ||
typescriptReturnType(context: Context) { | ||
const resultType = context.resolveType(this.proc.prorettype); | ||
const typeString = this.returnsPseudoTypeRecord | ||
? `${this.typescriptNameForResponse()}Record` | ||
: `${resultType.typescriptNameWithNamespace(context)}`; | ||
return typeString; | ||
} | ||
} |
@@ -1,6 +0,11 @@ | ||
import { Context, TypeFactoryContext } from "../../context"; | ||
import { TypeFactoryContext } from "../../context"; | ||
import { PGIndex } from "./pgindex"; | ||
import { PGTypes } from "./pgtype"; | ||
import { PGTypeComposite } from "./pgtypecomposite"; | ||
import { pascalCase } from "change-case"; | ||
import { | ||
ColumnNode, | ||
GenerationContext, | ||
TableNode, | ||
TablesNode, | ||
} from "@embracesql/shared"; | ||
import path from "path"; | ||
@@ -59,41 +64,23 @@ import { Sql } from "postgres"; | ||
get typescriptName() { | ||
return pascalCase(this.table.relname); | ||
} | ||
loadAST(context: GenerationContext, tables: TablesNode) { | ||
const table = new TableNode( | ||
tables, | ||
this.table.relname, | ||
context.database.resolveType(this.table.tabletypeoid), | ||
); | ||
// hash lookup of all tables | ||
context.database.registerTable(table.type.id, table); | ||
// columns -- derived from the table type | ||
this.tableType.attributes.forEach((a) => { | ||
const typeNode = context.database.resolveType(a.attribute.atttypid); | ||
if (typeNode) { | ||
new ColumnNode(table, a.name, typeNode, a.hasDefault, a.allowsNull); | ||
} else { | ||
throw new Error(`${a.name} cannot find type ${a.attribute.atttypid}`); | ||
} | ||
}); | ||
get postgresName() { | ||
return `${this.table.nspname}.${this.table.relname}`; | ||
// and the indexes | ||
this.indexes.forEach((i) => i.loadAST(context, table)); | ||
} | ||
typescriptTypeDefinition(context: Context) { | ||
console.assert(context); | ||
return ` | ||
export namespace ${this.typescriptName} { | ||
${this.indexes.map((i) => i.typescriptTypeDefinition(context)).join("\n")} | ||
}; | ||
`; | ||
} | ||
/** | ||
* Code generation builder for all fields updating. | ||
* | ||
* This will do self assignment for undefined properties, allowing | ||
* partial updates in typescript by omitting properties corresponding to | ||
* columns. | ||
* | ||
* Nulls in the passed typescript turn into SQL NULL. | ||
*/ | ||
sqlSetExpressions(context: Context, parameterHolder = "parameters") { | ||
const tableType = context.resolveType<PGTypeComposite>( | ||
this.table.tabletypeoid, | ||
); | ||
return tableType.attributes | ||
.map( | ||
(a) => | ||
`${a.postgresName} = ${a.postgresValueExpression( | ||
context, | ||
parameterHolder, | ||
)}`, | ||
) | ||
.join(" , "); | ||
} | ||
} |
@@ -23,3 +23,2 @@ import { TypeFactoryContext } from "../../context"; | ||
oid: number; | ||
fullname: string; | ||
nspname: string; | ||
@@ -34,3 +33,2 @@ typname: string; | ||
typcategory: string; | ||
description: string; | ||
}; | ||
@@ -76,3 +74,2 @@ | ||
// TODO: update registerOverride to allow a function expression | ||
if (catalog.typname === "oidvector") | ||
@@ -79,0 +76,0 @@ return PGTypeBase.factory(context, catalog); |
@@ -5,2 +5,3 @@ import { Context, TypeFactoryContext } from "../../context"; | ||
import { | ||
ArrayTypeNode, | ||
DELIMITER, | ||
@@ -11,3 +12,2 @@ GenerationContext, | ||
} from "@embracesql/shared"; | ||
import { pascalCase } from "change-case"; | ||
@@ -30,37 +30,20 @@ type Props = { | ||
typescriptTypeParser(context: GenerationContext) { | ||
const elementType = context.database.resolveType(this.catalog.typelem); | ||
if (elementType) { | ||
return ` | ||
if (from === null) return null; | ||
const rawArray = JSON.parse(from); | ||
return rawArray.map((e:unknown) => { | ||
return ${elementType.typescriptName}.parse( | ||
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions | ||
\`\${e}\` | ||
); | ||
}); | ||
`; | ||
} else { | ||
throw new Error( | ||
`${this.catalog.typname} could not resolve type of element`, | ||
); | ||
} | ||
} | ||
loadAST(context: GenerationContext) { | ||
const schema = context.database.resolveSchema(this.catalog.nspname); | ||
get typescriptName() { | ||
return `${pascalCase(this.catalog.typname)}${ | ||
this.props.arraySuffix ? "Array" : "" | ||
}`; | ||
const type = new ArrayTypeNode( | ||
`${this.catalog.typname}${this.props.arraySuffix ? "_array" : ""}`, | ||
schema.types, | ||
this.oid, | ||
); | ||
context.database.registerType(type.id, type); | ||
} | ||
typescriptTypeDefinition(context: Context) { | ||
console.assert(context); | ||
return ` | ||
export type ${this.typescriptName} = Array<${ | ||
context | ||
.resolveType(this.catalog.typelem) | ||
?.typescriptNameWithNamespace(context) ?? "void" | ||
}>; | ||
`; | ||
finalizeAST(context: GenerationContext): void { | ||
// this needs to be done 'later' to make sure the member type | ||
// has had a chance to be created -- two passes since types form a | ||
// graph, not a strict tree | ||
const memberType = context.database.resolveType(this.catalog.typelem); | ||
context.database.resolveType<ArrayTypeNode>(this.catalog.oid).memberType = | ||
memberType; | ||
} | ||
@@ -78,2 +61,4 @@ | ||
const value = elementType.serializeToPostgres(context, e); | ||
// null out unknown values | ||
if (value === null || value === undefined) return "NULL"; | ||
// quick escape with regex | ||
@@ -80,0 +65,0 @@ return value ? escapeArrayValue.tryParse(`${value}`) : ""; |
@@ -1,3 +0,2 @@ | ||
import { Context, TypeFactoryContext } from "../../context"; | ||
import { asDocComment } from "../../util"; | ||
import { TypeFactoryContext } from "../../context"; | ||
import { PGTypeBool } from "./base/bool"; | ||
@@ -10,2 +9,3 @@ import { PGTypeBigInt, PGTypeBytea, PGTypeNumber } from "./base/number"; | ||
import { PGTypeArray } from "./pgtypearray"; | ||
import { GenerationContext } from "@embracesql/shared"; | ||
@@ -78,2 +78,3 @@ /** | ||
default: | ||
// TODO: handle types | ||
return new PGTypeText( | ||
@@ -89,9 +90,9 @@ catalog, | ||
class PGTypeTid extends PGTypeBase { | ||
typescriptTypeDefinition(context: Context) { | ||
typescriptTypeDefinition(context: GenerationContext) { | ||
console.assert(context); | ||
return ` | ||
export type ${this.typescriptName} = { | ||
{ | ||
blockNumber: number; | ||
tupleIndex: number; | ||
}; | ||
} | ||
`; | ||
@@ -102,9 +103,6 @@ } | ||
class PGTypeInet extends PGTypeBase { | ||
typescriptTypeDefinition(context: Context) { | ||
typescriptTypeDefinition(context: GenerationContext) { | ||
console.assert(context); | ||
return ` | ||
${asDocComment(this.comment)} | ||
export type ${this.typescriptName} = string; | ||
`; | ||
return `string`; | ||
} | ||
} |
@@ -7,3 +7,7 @@ import { Context, TypeFactoryContext } from "../../context"; | ||
import { | ||
AttributeNode, | ||
CompositeTypeNode, | ||
DELIMITER, | ||
GenerationContext, | ||
cleanIdentifierForTypescript, | ||
compositeAttribute, | ||
@@ -34,2 +38,29 @@ escapeCompositeValue, | ||
loadAST(context: GenerationContext) { | ||
const schema = context.database.resolveSchema(this.catalog.nspname); | ||
// there is no guarantee that the types of the attributes are loaded yet | ||
const type = new CompositeTypeNode( | ||
this.catalog.typname, | ||
schema.types, | ||
this.oid, | ||
); | ||
context.database.registerType(type.id, type); | ||
} | ||
finalizeAST(context: GenerationContext) { | ||
const typeNode = context.database.resolveType<CompositeTypeNode>(this.oid); | ||
this.attributes.forEach( | ||
(a, i) => | ||
new AttributeNode( | ||
typeNode, | ||
a.name, | ||
i, | ||
context.database.resolveType(a.attribute.atttypid), | ||
!a.isOptional, | ||
!a.notNull, | ||
), | ||
); | ||
} | ||
get hasPrimaryKey() { | ||
@@ -72,59 +103,25 @@ return this.primaryKey !== undefined; | ||
typescriptTypeDefinition(context: Context) { | ||
const generationBuffer = [``]; | ||
typescriptTypeDefinition(context: GenerationContext) { | ||
// all the fields -- and a partial type to allow filling out with | ||
// various sub selects | ||
const nameAndType = this.attributes.map( | ||
(a) => | ||
`${a.typescriptName}${ | ||
a.isOptional ? "?" : "" | ||
}: ${a.typescriptTypeDefinition(context)};`, | ||
); | ||
generationBuffer.push(` | ||
export interface ${this.typescriptName} { | ||
${nameAndType.join("\n")} | ||
}; | ||
`); | ||
if (this.hasPrimaryKey) { | ||
// without the primary key, used to create rows when there | ||
// is a default value in the database on the primary key | ||
const nameAndType = this.notPrimaryKeyAttributes.map( | ||
(a) => | ||
`${a.typescriptName}${ | ||
a.attribute.attnotnull ? "" : "?" | ||
}: ${a.typescriptTypeDefinition(context)};`, | ||
); | ||
generationBuffer.push(` | ||
export interface ${this.typescriptName}NotPrimaryKey { | ||
${nameAndType.join("\n")} | ||
}; | ||
`); | ||
const primaryKeyNames = | ||
this.primaryKey?.attributes.map( | ||
(a) => `value.${a.typescriptName} !== undefined`, | ||
) || []; | ||
generationBuffer.push(` | ||
export function includes${this.typescriptName}PrimaryKey(value: Partial<${ | ||
this.typescriptName | ||
}>): value is ${this.typescriptName}{ | ||
return ${primaryKeyNames.join(" && ")} | ||
} | ||
`); | ||
} | ||
return generationBuffer.join("\n"); | ||
const nameAndType = this.attributes.map((a) => { | ||
const attributeType = context.database.resolveType(a.attribute.atttypid)!; | ||
const attributeName = `${camelCase( | ||
cleanIdentifierForTypescript(a.attribute.attname), | ||
)}`; | ||
const attributeTypeName = a.notNull | ||
? attributeType.typescriptNamespacedName | ||
: `Nullable<${attributeType.typescriptNamespacedName}>`; | ||
return `${attributeName}${ | ||
a.isOptional ? "?" : "" | ||
}: ${attributeTypeName};`; | ||
}); | ||
return `{${nameAndType.join(" ")}}`; | ||
} | ||
sqlColumns(context: Context) { | ||
console.assert(context); | ||
get sqlColumns() { | ||
return this.attributes.map((a) => a.postgresName).join(","); | ||
} | ||
postgresResultRecordToTypescript( | ||
context: Context, | ||
resultsetName = "response", | ||
) { | ||
console.assert(context); | ||
get postgresResultRecordToTypescript() { | ||
// snippet will pick resultset fields to type map | ||
@@ -135,5 +132,3 @@ const recordPieceBuilders = this.attributes.map( | ||
// all the fields in the resultset mapped out to an inferred type array | ||
return `${resultsetName}.map(record => ({ ${recordPieceBuilders.join( | ||
",", | ||
)} }))`; | ||
return `response.map(record => ({ ${recordPieceBuilders.join(",")} }))`; | ||
} | ||
@@ -169,7 +164,9 @@ | ||
camelCase(a.attribute.attname), | ||
compositeAttribute.map((parsedAttributeText) => | ||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return | ||
context | ||
.resolveType(a.attribute.atttypid) | ||
.parseFromPostgres(context, parsedAttributeText), | ||
compositeAttribute.map( | ||
(parsedAttributeText) => | ||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return | ||
context | ||
.resolveType(a.attribute.atttypid) | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
.parseFromPostgres(context, parsedAttributeText) as any, | ||
), | ||
@@ -176,0 +173,0 @@ ]); |
import { Context, TypeFactoryContext } from "../../context"; | ||
import { PGCatalogType } from "./pgcatalogtype"; | ||
import { CatalogRow } from "./pgtype"; | ||
import { DomainTypeNode, GenerationContext } from "@embracesql/shared"; | ||
@@ -15,12 +16,26 @@ /** | ||
typescriptTypeDefinition(context: Context) { | ||
return ` | ||
export type ${this.typescriptName} = ${ | ||
context | ||
.resolveType(this.catalog.typbasetype) | ||
?.typescriptNameWithNamespace(context) ?? "void" | ||
}; | ||
`; | ||
loadAST(context: GenerationContext) { | ||
const schema = context.database.resolveSchema(this.catalog.nspname); | ||
// there is no guarantee that the types of the attributes are loaded yet | ||
const type = new DomainTypeNode( | ||
this.catalog.typname, | ||
schema.types, | ||
this.oid, | ||
); | ||
context.database.registerType(type.id, type); | ||
} | ||
finalizeAST(context: GenerationContext) { | ||
const typeNode = context.database.resolveType<DomainTypeNode>(this.oid); | ||
typeNode.baseType = context.database.resolveType(this.catalog.typbasetype); | ||
} | ||
typescriptTypeDefinition(context: GenerationContext) { | ||
return `${ | ||
context.database.resolveType(this.catalog.typbasetype) | ||
?.typescriptNamespacedName ?? "void" | ||
}`; | ||
} | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
@@ -27,0 +42,0 @@ parseFromPostgres(context: Context, x: any) { |
@@ -1,5 +0,6 @@ | ||
import { Context, TypeFactoryContext } from "../../context"; | ||
import { cleanIdentifierForTypescript, groupBy } from "../../util"; | ||
import { TypeFactoryContext } from "../../context"; | ||
import { groupBy } from "../../util"; | ||
import { PGCatalogType } from "./pgcatalogtype"; | ||
import { CatalogRow } from "./pgtype"; | ||
import { EnumTypeNode, GenerationContext } from "@embracesql/shared"; | ||
import path from "path"; | ||
@@ -44,15 +45,17 @@ import { Sql } from "postgres"; | ||
this.values = context.enumValues.enumValuesByTypeId[catalog.oid]; | ||
this.values.toSorted((l, r) => l.enumsortorder - r.enumsortorder); | ||
} | ||
typescriptTypeDefinition(context: Context) { | ||
console.assert(context); | ||
const namedValues = this.values.map( | ||
(a) => `${cleanIdentifierForTypescript(a.enumlabel)} = "${a.enumlabel}"`, | ||
loadAST(context: GenerationContext) { | ||
const schema = context.database.resolveSchema(this.catalog.nspname); | ||
const type = new EnumTypeNode( | ||
this.catalog.typname, | ||
this.values.map((v) => v.enumlabel), | ||
schema.types, | ||
this.oid, | ||
this, | ||
); | ||
return ` | ||
export enum ${this.typescriptName} { | ||
${namedValues.join(",")} | ||
}; | ||
`; | ||
context.database.registerType(type.id, type); | ||
} | ||
} |
@@ -1,4 +0,5 @@ | ||
import { Context, TypeFactoryContext } from "../../context"; | ||
import { TypeFactoryContext } from "../../context"; | ||
import { PGCatalogType } from "./pgcatalogtype"; | ||
import { CatalogRow } from "./pgtype"; | ||
import { GenerationContext } from "@embracesql/shared"; | ||
@@ -14,13 +15,9 @@ /** | ||
typescriptTypeDefinition(context: Context) { | ||
typescriptTypeDefinition(context: GenerationContext) { | ||
// look up the range'd type and use it to make a tuple. | ||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion | ||
const type = context.resolveType(this.catalog.rngsubtype)!; | ||
const type = context.database.resolveType(this.catalog.rngsubtype)!; | ||
// array with two elements is the nearst-to-a-tuple in JS | ||
return ` | ||
export type ${this.typescriptName} = [${type.typescriptNameWithNamespace( | ||
context, | ||
)}, ${type.typescriptNameWithNamespace(context)}]; | ||
`; | ||
return `[${type.typescriptNamespacedName}, ${type.typescriptNamespacedName}]`; | ||
} | ||
} |
@@ -1,6 +0,38 @@ | ||
import { GenerationContext } from ".."; | ||
import { DatabaseOperation } from "../operations/database"; | ||
import { SqlScriptOperations } from "../operations/sqlscript"; | ||
import { CreateOperation } from "./autocrud/create"; | ||
import { DeleteOperation } from "./autocrud/delete"; | ||
import { ReadOperation } from "./autocrud/read"; | ||
import { UpdateOperation } from "./autocrud/update"; | ||
import { generateTypecastMap } from "./generateTypecastMap"; | ||
import { | ||
ASTKind, | ||
CompositeTypeNode, | ||
GenerationContext, | ||
NamedASTNode, | ||
} from "@embracesql/shared"; | ||
/** | ||
* Creating nested classes intead of namespaces to allow shared access | ||
* to the `database`. | ||
*/ | ||
const NestedNamedClassVisitor = { | ||
before: async (context: GenerationContext, node: NamedASTNode) => { | ||
console.assert(context); | ||
return ` | ||
public ${node.typescriptName} = new class implements HasDatabase { | ||
constructor(private hasDatabase: HasDatabase) { | ||
} | ||
get database() { | ||
return this.hasDatabase.database; | ||
} | ||
`; | ||
}, | ||
after: async (context: GenerationContext, node: NamedASTNode) => { | ||
console.assert(context); | ||
console.assert(node); | ||
return `}(this)`; | ||
}, | ||
}; | ||
/** | ||
* Generate a root object class that serves as 'the database'. | ||
@@ -13,113 +45,165 @@ * | ||
export const generateDatabaseRoot = async (context: GenerationContext) => { | ||
// starting off with all the imports, append to this list | ||
// and it will be the final output | ||
const generationBuffer = [ | ||
` | ||
// BEGIN - Node side database connectivity layer | ||
import { Context, initializeContext } from "@embracesql/postgres"; | ||
import postgres from "postgres"; | ||
`, | ||
]; | ||
// the schema | ||
// common database interface | ||
generationBuffer.push(` | ||
interface HasDatabase { | ||
database: Database; | ||
} | ||
`); | ||
return await context.database.visit({ | ||
...context, | ||
handlers: { | ||
[ASTKind.Database]: { | ||
before: async () => { | ||
return [ | ||
// starting off with all the imports, append to this list | ||
// and it will be the final output | ||
` | ||
// BEGIN - Node side database connectivity layer | ||
import { Context, initializeContext, PostgresDatabase } from "@embracesql/postgres"; | ||
import postgres from "postgres"; | ||
`, | ||
// include all schemas -- need those built in types | ||
await generateTypecastMap({ ...context, skipSchemas: [] }), | ||
// common database interface | ||
` | ||
interface HasDatabase { | ||
database: Database; | ||
} | ||
`, | ||
// typecast map for postgres driver codec. | ||
// class start | ||
generationBuffer.push(`export class Database { `); | ||
generationBuffer.push(` | ||
// generated database class starts here | ||
`export class Database extends PostgresDatabase implements HasDatabase { `, | ||
`get database() { return this};`, | ||
` | ||
/** | ||
* Connect to your database server via URL, and return | ||
* a fully typed database you can use to access it. | ||
*/ | ||
static async connect(postgresUrl: string, props?: postgres.Options<never>) { | ||
return new Database(await initializeContext(postgresUrl, props)); | ||
} | ||
`, | ||
].join("\n"); | ||
}, | ||
after: async () => { | ||
return [ | ||
// database class end | ||
`}`, | ||
].join("\n"); | ||
}, | ||
}, | ||
[ASTKind.Schema]: NestedNamedClassVisitor, | ||
[ASTKind.Scripts]: NestedNamedClassVisitor, | ||
[ASTKind.ScriptFolder]: NestedNamedClassVisitor, | ||
[ASTKind.Script]: { | ||
before: async (context, node) => { | ||
const parameterPasses = node.parametersType?.attributes?.length | ||
? ", [" + | ||
node.parametersType.attributes | ||
.map((a) => `parameters.${a.typescriptPropertyName}`) | ||
.join(",") + | ||
"]" | ||
: ""; | ||
// just a bit of escaping of the passsed sql script | ||
const preparedSql = node.script.replace("`", "\\`"); | ||
/** | ||
* Connect to your database server via URL, and return | ||
* a fully typed database you can use to access it. | ||
*/ | ||
static async connect(postgresUrl: string) { | ||
return new Database(await initializeContext(postgresUrl)); | ||
} | ||
// pick results fields and add in null handling | ||
// this will need to account for alias types | ||
const resultsFinalType = node.resultsResolvedType; | ||
// this is a bit over defensive programming -- scripts always | ||
// come back with composite types | ||
const recordPieceBuilders = (resultsFinalType as CompositeTypeNode) | ||
.attributes | ||
? (resultsFinalType as CompositeTypeNode).attributes.map( | ||
(c) => | ||
`${c.typescriptPropertyName}: undefinedIsNull(${c.type.typescriptNamespacedName}.parse(record.${c.typescriptPropertyName}))`, | ||
) | ||
: []; | ||
// and here is the really defensive part... | ||
console.assert(recordPieceBuilders.length); | ||
const parameters = node.parametersType | ||
? `parameters: ${node.parametersType.typescriptNamespacedName}` | ||
: ""; | ||
return ` | ||
async ${node.typescriptPropertyName} (${parameters}) { | ||
const sql = this.database.context.sql; | ||
const response = await sql.unsafe(\` | ||
${preparedSql} | ||
\`${parameterPasses}); | ||
return response.map(record => ({ ${recordPieceBuilders.join( | ||
",", | ||
)} })); | ||
`; | ||
}, | ||
after: async () => { | ||
return `}`; | ||
}, | ||
}, | ||
[ASTKind.Procedures]: NestedNamedClassVisitor, | ||
[ASTKind.Procedure]: { | ||
before: async (_, node) => { | ||
const resultType = `${node.resultsResolvedType?.typescriptNamespacedName}`; | ||
// function call start, passing in parameters | ||
const generationBuffer = [ | ||
` async ${node.typescriptPropertyName}(parameters : ${node.parametersType?.typescriptNamespacedName})`, | ||
`{`, | ||
]; | ||
// turn parameters into the postgres driver escape sequence | ||
// this is making a string interpolation with string interpoplation | ||
const parameterExpressions = | ||
node.parametersType?.attributes.map( | ||
(a) => | ||
(a.name ? `${a.name} =>` : ``) + | ||
` \${ typed[${a.type.id}](undefinedIsNull(parameters.${a.typescriptPropertyName})) }`, | ||
) ?? []; | ||
// if there is a composite -- pseudo -- return type, this will | ||
// need to call back into the sql driver to parse the results | ||
if (node.isPseudoType) { | ||
generationBuffer.push(` | ||
const parseResult = (context: Context, | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
result: any) => { | ||
return context.procTypes[${node.id}].parseFromPostgresIfRecord(context, result) as unknown as ${node.resultsResolvedType?.typescriptNamespacedName}; | ||
} | ||
`); | ||
} | ||
// and the call body | ||
generationBuffer.push(` | ||
console.assert(parameters); | ||
const sql = this.database.context.sql; | ||
const typed = sql.typed as unknown as PostgresTypecasts; | ||
const response = | ||
await sql\` | ||
SELECT | ||
${node.databaseName}(${parameterExpressions.join(",")})\`; | ||
const results = response; | ||
const responseBody = ( ${(() => { | ||
// pseudo record -- which is always a table type but needs more parsing | ||
if (node.isPseudoType) { | ||
return `results.map(x => parseResult(this.database.context, x.${node.nameInDatabase})).filter<${resultType}>((r):r is ${resultType} => r !== null)`; | ||
} | ||
// table cast of a defined type | ||
if (node.returnsMany) { | ||
return `results.map(x => ${resultType}.parse(x.${node.nameInDatabase})).filter<${resultType}>((r):r is ${resultType} => r !== null)`; | ||
} | ||
// pick out the scalar case | ||
return `${resultType}.parse(results?.[0].${node.nameInDatabase})`; | ||
})()} ); | ||
return responseBody; | ||
private constructor(public context: Context) {`); | ||
// constructor body currently empty | ||
generationBuffer.push(` | ||
} | ||
/** | ||
* Clean up the connection. | ||
*/ | ||
async public disconnect() { | ||
await this.context.sql.end() | ||
} | ||
`); | ||
generationBuffer.push(`}`); | ||
// transaction support is a nested context | ||
generationBuffer.push(` | ||
/** | ||
* Use the database inside a transaction. | ||
* | ||
* A successful return is a commit. | ||
* An escaping exception is a rollback. | ||
*/ | ||
async withTransaction<T>(body: (database: Database) => Promise<T>) { | ||
if(this.context.sql.begin) { | ||
// root transaction | ||
return await this.context.sql.begin(async (sql) => await body(new Database({...this.context, sql}))); | ||
} else { | ||
// nested transaction | ||
const nested = this.context.sql as postgres.TransactionSql | ||
return await nested.savepoint(async (sql) => await body(new Database({...this.context, sql}))); | ||
} | ||
} | ||
return generationBuffer.join("\n"); | ||
}, | ||
}, | ||
// tables and indexes host AutoCRUD | ||
[ASTKind.Tables]: NestedNamedClassVisitor, | ||
[ASTKind.Table]: NestedNamedClassVisitor, | ||
[ASTKind.Index]: NestedNamedClassVisitor, | ||
/** | ||
* Returns a database scoped to a new transaction. | ||
* You must explicitly call \`rollback\` or \`commit\`. | ||
*/ | ||
async beginTransaction() { | ||
return await new Promise<{ | ||
database: Database; | ||
commit: () => void; | ||
rollback: (message?: string) => void; | ||
}>((resolveReady) => { | ||
const complete = new Promise((resolve, reject) => { | ||
this.context.sql.begin(async (sql) => { | ||
resolveReady({ | ||
database: new Database({ ...this.context, sql }), | ||
commit: () => resolve(true), | ||
rollback: (message?: string) => reject(message), | ||
}); | ||
await complete; | ||
}).catch((reason) => reason); | ||
}); | ||
}); | ||
} | ||
`); | ||
// wheel through every namespace, and every proc and generate calls | ||
// each schema / namespace turns into a .<Schema> grouping | ||
const operations = await DatabaseOperation.factory(context); | ||
generationBuffer.push(operations.typescriptDefinition(context)); | ||
// holder for all scripts provides a .Scripts grouping | ||
if (context.sqlScriptsFrom?.length) { | ||
const scripts = await SqlScriptOperations.factory( | ||
context, | ||
context.sqlScriptsFrom, | ||
); | ||
generationBuffer.push(` | ||
public Scripts = new class implements HasDatabase { | ||
constructor(public database: Database) {} | ||
`); | ||
generationBuffer.push(scripts.typescriptDefinition(context)); | ||
// close off Scripts outer scope | ||
generationBuffer.push(`}(this)`); | ||
} | ||
//class end | ||
generationBuffer.push(`}`); | ||
return generationBuffer.join("\n"); | ||
// C R U D - AutoCRUD! | ||
[ASTKind.CreateOperation]: CreateOperation, | ||
[ASTKind.ReadOperation]: ReadOperation, | ||
[ASTKind.UpdateOperation]: UpdateOperation, | ||
[ASTKind.DeleteOperation]: DeleteOperation, | ||
}, | ||
}); | ||
}; |
@@ -1,5 +0,28 @@ | ||
import { GenerationContext } from ".."; | ||
import { DatabaseOperation } from "../operations/database"; | ||
import { SqlScriptOperations } from "../operations/sqlscript"; | ||
import { | ||
ASTKind, | ||
GenerationContext, | ||
FunctionOperationNode, | ||
CreateOperationNode, | ||
ReadOperationNode, | ||
UpdateOperationNode, | ||
DeleteOperationNode, | ||
} from "@embracesql/shared"; | ||
const FunctionOperationNodeVisitor = { | ||
before: async (_: GenerationContext, node: FunctionOperationNode) => { | ||
const callee: string[] = []; | ||
// parameters go first! | ||
if (node.parametersType) { | ||
callee.push( | ||
`request.parameters as ${node.parametersType.typescriptNamespacedName}`, | ||
); | ||
} | ||
return `"${ | ||
node.typescriptNamespacedPropertyName | ||
}": async (request: EmbraceSQLRequest<object, object>) => database.${ | ||
node.typescriptNamespacedPropertyName | ||
}(${callee.join(",")}),`; | ||
}, | ||
}; | ||
/** | ||
@@ -12,54 +35,92 @@ * Wrap up the database class in a hash style dispatch map for operation processing | ||
) => { | ||
// start off a new class foor the dispatcher | ||
const generationBuffer = [ | ||
` | ||
// BEGIN - Node side operation dispatcher for HTTP/S endpoints | ||
import { EmbraceSQLRequest, OperationDispatchMethod } from "@embracesql/shared"; | ||
export class OperationDispatcher { | ||
private dispatchMap: Record<string, OperationDispatchMethod>; | ||
constructor(private database: Database){ | ||
this.dispatchMap = { | ||
return await context.database.visit({ | ||
...context, | ||
handlers: { | ||
[ASTKind.Database]: { | ||
before: async () => { | ||
return ` | ||
// begin - operation dispatch map | ||
import { EmbraceSQLRequest, OperationDispatchMethod } from "@embracesql/shared"; | ||
export class OperationDispatcher { | ||
private dispatchMap: Record<string, OperationDispatchMethod>; | ||
constructor(private database: Database){ | ||
this.dispatchMap = { | ||
`, | ||
]; | ||
// all possible operations | ||
const operations = await DatabaseOperation.factory(context); | ||
operations.dispatchable.forEach((o) => { | ||
const callee: string[] = []; | ||
// parameters go first! | ||
if (o.typescriptParametersType(context)) { | ||
callee.push( | ||
`request.parameters as ${o.typescriptParametersType(context)}`, | ||
); | ||
} | ||
if (o.typescriptValuesType(context)) { | ||
callee.push(`request.values as ${o.typescriptValuesType(context)}`); | ||
} | ||
generationBuffer.push( | ||
`"${o.dispatchName( | ||
context, | ||
)}": async (request: EmbraceSQLRequest<object, object>) => database.${o.dispatchName( | ||
context, | ||
)}(${callee.join(",")}),`, | ||
); | ||
`; | ||
}, | ||
after: async () => { | ||
return [ | ||
`}`, // dispatch map | ||
`}`, // constructor | ||
` | ||
async dispatch(request: EmbraceSQLRequest<object, object>) { | ||
if (!this.dispatchMap[request.operation]) { | ||
throw new Error(\`\${request.operation} not available\`); | ||
} | ||
return this.dispatchMap[request.operation](request); | ||
} | ||
`, | ||
`}`, // class | ||
].join("\n"); | ||
}, | ||
}, | ||
[ASTKind.Script]: FunctionOperationNodeVisitor, | ||
[ASTKind.Procedure]: FunctionOperationNodeVisitor, | ||
[ASTKind.CreateOperation]: { | ||
before: async (_: GenerationContext, node: CreateOperationNode) => { | ||
const callee: string[] = []; | ||
callee.push( | ||
`request.values as ${node.table.typescriptNamespacedName}.Values`, | ||
); | ||
return `"${ | ||
node.typescriptNamespacedPropertyName | ||
}": async (request: EmbraceSQLRequest<object, object>) => database.${ | ||
node.typescriptNamespacedPropertyName | ||
}(${callee.join(",")}),`; | ||
}, | ||
}, | ||
[ASTKind.ReadOperation]: { | ||
before: async (_: GenerationContext, node: ReadOperationNode) => { | ||
const callee: string[] = []; | ||
callee.push( | ||
`request.parameters as ${node.index.typescriptNamespacedName}`, | ||
); | ||
return `"${ | ||
node.typescriptNamespacedPropertyName | ||
}": async (request: EmbraceSQLRequest<object, object>) => database.${ | ||
node.typescriptNamespacedPropertyName | ||
}(${callee.join(",")}),`; | ||
}, | ||
}, | ||
[ASTKind.UpdateOperation]: { | ||
before: async (_: GenerationContext, node: UpdateOperationNode) => { | ||
const callee: string[] = []; | ||
callee.push( | ||
`request.parameters as ${node.index.typescriptNamespacedName}`, | ||
); | ||
callee.push( | ||
`request.values as Partial<${node.index.table.typescriptNamespacedName}.Values>`, | ||
); | ||
return `"${ | ||
node.typescriptNamespacedPropertyName | ||
}": async (request: EmbraceSQLRequest<object, object>) => database.${ | ||
node.typescriptNamespacedPropertyName | ||
}(${callee.join(",")}),`; | ||
}, | ||
}, | ||
[ASTKind.DeleteOperation]: { | ||
before: async (_: GenerationContext, node: DeleteOperationNode) => { | ||
const callee: string[] = []; | ||
callee.push( | ||
`request.parameters as ${node.index.typescriptNamespacedName}`, | ||
); | ||
return `"${ | ||
node.typescriptNamespacedPropertyName | ||
}": async (request: EmbraceSQLRequest<object, object>) => database.${ | ||
node.typescriptNamespacedPropertyName | ||
}(${callee.join(",")}),`; | ||
}, | ||
}, | ||
}, | ||
}); | ||
// all possible scripts | ||
if (context.sqlScriptsFrom?.length) { | ||
const scripts = await SqlScriptOperations.factory( | ||
context, | ||
context.sqlScriptsFrom, | ||
); | ||
console.assert(scripts); | ||
} | ||
// close constructor | ||
generationBuffer.push(`}}`); | ||
generationBuffer.push(` | ||
async dispatch(request: EmbraceSQLRequest<object, object>) { | ||
return this.dispatchMap[request.operation](request); | ||
}`); | ||
// close class | ||
generationBuffer.push(`}`); | ||
return generationBuffer.join("\n"); | ||
}; |
import { GenerationContext } from ".."; | ||
import { ASTKind, IsNamed, NamespaceVisitor } from "@embracesql/shared"; | ||
import { camelCase, pascalCase } from "change-case"; | ||
import { ASTKind, NamespaceVisitor } from "@embracesql/shared"; | ||
import { camelCase } from "change-case"; | ||
@@ -15,17 +15,20 @@ /** | ||
// primary key 'pickers' used to debounce and hash objects | ||
generationBuffer.push(`// begin primary key pickers`); | ||
context.handlers = { | ||
[ASTKind.Database]: { | ||
before: async () => { | ||
return `// begin primary key pickers`; | ||
}, | ||
}, | ||
[ASTKind.Schema]: NamespaceVisitor, | ||
[ASTKind.Tables]: NamespaceVisitor, | ||
[ASTKind.Table]: { | ||
before: async (context, node) => { | ||
const tableTypeName = `${pascalCase( | ||
(node as unknown as IsNamed)?.name, | ||
)}`; | ||
const generationBuffer = [ | ||
await NamespaceVisitor.before!(context, node), | ||
]; | ||
const primaryKey = node.primaryKey; | ||
if (primaryKey) { | ||
const generationBuffer = [ | ||
await NamespaceVisitor.before(context, node), | ||
]; | ||
// extract primary key | ||
generationBuffer.push( | ||
`export function primaryKeyFrom(value: ${tableTypeName}) : string {`, | ||
`export function primaryKeyFrom(value: ${node.typescriptNamespacedName}.Record) : string {`, | ||
); | ||
@@ -40,5 +43,24 @@ generationBuffer.push(`return JSON.stringify({`); | ||
generationBuffer.push(`}`); | ||
// boolean guard on primary key | ||
const primaryKeyNames = | ||
primaryKey.columns.map( | ||
(a) => `value.${camelCase(a.typescriptName)} !== undefined`, | ||
) || []; | ||
generationBuffer.push(` | ||
export function includesPrimaryKey(value: Partial<Record>){ | ||
return ${primaryKeyNames.join(" && ")} | ||
} | ||
`); | ||
return generationBuffer.join("\n"); | ||
} else { | ||
return [ | ||
await NamespaceVisitor.before(context, node), | ||
`export function primaryKeyFrom(value: ${node.typescriptNamespacedName}.Record) {`, | ||
` return "";`, | ||
`}`, | ||
`export function includesPrimaryKey(value: ${node.typescriptNamespacedName}.Record) {`, | ||
` return false;`, | ||
`}`, | ||
].join("\n"); | ||
} | ||
return generationBuffer.join("\n"); | ||
}, | ||
@@ -49,4 +71,3 @@ after: NamespaceVisitor.after, | ||
generationBuffer.push(await context.database.visit(context)); | ||
generationBuffer.push(`// end primary key pickers`); | ||
return generationBuffer.join("\n"); | ||
} |
import { GenerationContext } from ".."; | ||
import { | ||
SCRIPT_TYPES_NAMESPACE, | ||
SqlScriptOperations, | ||
} from "../operations/sqlscript"; | ||
import { generatePrimaryKeyPickers } from "./generatePrimaryKeyPickers"; | ||
import { generateTypeGuards } from "./generateTypeGuards"; | ||
import { generateTypeParsers } from "./generateTypeParsers"; | ||
import { | ||
ASTKind, | ||
AbstractTypeNode, | ||
NamespaceVisitor, | ||
VALUES, | ||
cleanIdentifierForTypescript, | ||
} from "@embracesql/shared"; | ||
import { GenerationContext as GC } from "@embracesql/shared"; | ||
import { pascalCase } from "change-case"; | ||
@@ -32,57 +38,124 @@ /** | ||
*/ | ||
/* eslint-disable @typescript-eslint/no-explicit-any */ | ||
/* eslint-disable @typescript-eslint/no-empty-interface */ | ||
/* eslint-disable @typescript-eslint/no-namespace */ | ||
/* eslint-disable @typescript-eslint/no-unused-vars */ | ||
import {UUID, JsDate, JSONValue, JSONObject, Empty, Nullable, undefinedIsNull} from "@embracesql/shared"; | ||
/* eslint-disable @typescript-eslint/no-unnecessary-type-assertion */ | ||
/* eslint-disable @typescript-eslint/no-redundant-type-constituents */ | ||
/* @typescript-eslint/no-redundant-type-constituents */ | ||
import {UUID, JsDate, JSONValue, JSONObject, Empty, Nullable, NullableMembers, undefinedIsNull, nullIsUndefined} from "@embracesql/shared"; | ||
import type { PartiallyOptional } from "@embracesql/shared"; | ||
`, | ||
]; | ||
// each postgres namespace gets a typescript namespace -- generates itself | ||
// this includes all namespaces in order to get all types which can | ||
// be used by user defined schemas | ||
await Promise.all( | ||
context.namespaces.map((n) => { | ||
generationBuffer.push( | ||
n.typescriptTypeDefinition({ | ||
...context, | ||
currentNamespace: n.namespace, | ||
}), | ||
); | ||
const TypeDefiner = { | ||
before: async (context: GC, node: AbstractTypeNode) => { | ||
return `export type ${ | ||
node.typescriptName | ||
} = ${node.typescriptTypeDefinition(context)};`; | ||
}, | ||
}; | ||
// no skipping schemas while generating the type definitions - schemas | ||
// can reference on another, particularly pg_catalog base types | ||
generationBuffer.push( | ||
await context.database.visit({ | ||
...context, | ||
skipSchemas: [], | ||
handlers: { | ||
[ASTKind.Database]: { | ||
before: async () => { | ||
return `// begin type definitions`; | ||
}, | ||
}, | ||
[ASTKind.Schema]: NamespaceVisitor, | ||
[ASTKind.Types]: NamespaceVisitor, | ||
[ASTKind.Type]: TypeDefiner, | ||
[ASTKind.Enum]: { | ||
before: async (_, node) => { | ||
const namedValues = node.values.map( | ||
(a) => `${cleanIdentifierForTypescript(a)} = "${a}"`, | ||
); | ||
return ` | ||
export enum ${node.typescriptName} { | ||
${namedValues.join(",")} | ||
}; | ||
`; | ||
}, | ||
}, | ||
[ASTKind.Tables]: NamespaceVisitor, | ||
// tables are defined by types -- but actual rows/records | ||
// coming back from queries need a required so we get | ||
// DBMS style value | null semantics -- no undefinded in SQL | ||
[ASTKind.Table]: { | ||
before: async (context, node) => { | ||
return [ | ||
await NamespaceVisitor.before(context, node), | ||
`export type Record = {`, | ||
node.allColumns | ||
.map( | ||
(c) => | ||
`${c.typescriptPropertyName}: ${ | ||
node.type.typescriptNamespacedName | ||
}["${c.typescriptPropertyName}"] ${ | ||
c.allowsNull ? " | null" : "" | ||
}`, | ||
) | ||
.join(";"), | ||
`};`, | ||
].join("\n"); | ||
}, | ||
after: async (context, node) => { | ||
return [ | ||
// exhaustive -- if there is no primary key, say so explicitly | ||
node.primaryKey ? "" : `export type PrimaryKey = never;`, | ||
// optional columns -- won't always need to pass these | ||
// ex: database has a default | ||
`export type Optional = Pick<Record,${ | ||
node.optionalColumns | ||
.map((c) => `"${c.typescriptPropertyName}"`) | ||
.join("|") || "never" | ||
}>`, | ||
// values type -- used in create and update | ||
`export type ${pascalCase( | ||
VALUES, | ||
)} = PartiallyOptional<Record, Optional & PrimaryKey>`, | ||
await NamespaceVisitor.after(context, node), | ||
].join("\n"); | ||
}, | ||
}, | ||
[ASTKind.Index]: { | ||
before: async (_, node) => `export type ${node.typescriptName} = {`, | ||
after: async (_, node) => | ||
[ | ||
`}`, | ||
// alias primary key to the correct index | ||
node.primaryKey | ||
? `export type PrimaryKey = ${node.typescriptName};` | ||
: "", | ||
].join("\n"), | ||
}, | ||
[ASTKind.IndexColumn]: { | ||
before: async (_, node) => | ||
`${node.typescriptPropertyName}: ${node.type.typescriptNamespacedName} ;`, | ||
}, | ||
[ASTKind.Procedures]: NamespaceVisitor, | ||
[ASTKind.Procedure]: NamespaceVisitor, | ||
[ASTKind.CompositeType]: TypeDefiner, | ||
[ASTKind.DomainType]: TypeDefiner, | ||
[ASTKind.AliasType]: TypeDefiner, | ||
[ASTKind.ArrayType]: TypeDefiner, | ||
[ASTKind.Scripts]: NamespaceVisitor, | ||
[ASTKind.ScriptFolder]: NamespaceVisitor, | ||
[ASTKind.Script]: NamespaceVisitor, | ||
}, | ||
}), | ||
); | ||
// all typecasts collected into a single interface | ||
generationBuffer.push( | ||
` | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
type ArgumentToPostgres = any; | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
type ArgumentFromPostgres = any; | ||
type Typecast = (x: ArgumentToPostgres) => ArgumentFromPostgres; | ||
export interface PostgresTypecasts { | ||
${context.namespaces | ||
.flatMap((n) => n.types) | ||
.map((t) => `${t.postgresMarshallName}: Typecast`) | ||
.join(";\n")} | ||
} | ||
`, | ||
); | ||
// script parameter and return types | ||
// holder for all scripts provides a .Scripts grouping | ||
if (context.sqlScriptsFrom?.length) { | ||
const scripts = await SqlScriptOperations.factory( | ||
context, | ||
context.sqlScriptsFrom, | ||
); | ||
generationBuffer.push(`export namespace ${SCRIPT_TYPES_NAMESPACE}{`); | ||
generationBuffer.push(scripts.typescriptTypeDefinition(context)); | ||
// close off Scripts namespace | ||
generationBuffer.push(`}`); | ||
} | ||
generationBuffer.push(await generateTypeParsers(context)); | ||
generationBuffer.push(await generatePrimaryKeyPickers(context)); | ||
generationBuffer.push(await generateTypeGuards(context)); | ||
return generationBuffer.join("\n"); | ||
}; |
@@ -1,3 +0,7 @@ | ||
import { GenerationContext } from ".."; | ||
import { ASTKind, NamespaceVisitor } from "@embracesql/shared"; | ||
import { | ||
ASTKind, | ||
AbstractTypeNode, | ||
GenerationContext, | ||
NamespaceVisitor, | ||
} from "@embracesql/shared"; | ||
@@ -10,49 +14,78 @@ /** | ||
generationBuffer.push(`// begin string parsers`); | ||
context.handlers = { | ||
[ASTKind.Schema]: NamespaceVisitor, | ||
[ASTKind.Types]: NamespaceVisitor, | ||
[ASTKind.Type]: { | ||
before: async (context, node) => { | ||
const generationBuffer = [ | ||
await NamespaceVisitor.before!(context, node), | ||
]; | ||
generationBuffer.push(`export function parse(from: string|null) {`); | ||
generationBuffer.push(`${node.parser.typescriptTypeParser(context)}`); | ||
generationBuffer.push(`}`); | ||
return generationBuffer.join("\n"); | ||
}, | ||
after: NamespaceVisitor.after, | ||
const ParseVisitor = { | ||
before: async (context: GenerationContext, node: AbstractTypeNode) => { | ||
return [ | ||
await NamespaceVisitor.before(context, node), | ||
`export function parse(from: unknown) {`, | ||
`// ${ASTKind[node.kind]}`, | ||
`${node.typescriptNullOrUndefined(context)}`, | ||
`${node.typescriptTypeParser(context)}`, | ||
`}`, | ||
"\n", | ||
].join("\n"); | ||
}, | ||
after: NamespaceVisitor.after, | ||
}; | ||
// no skipping schemas for parsing | ||
generationBuffer.push( | ||
await context.database.visit({ ...context, skipSchemas: [] }), | ||
await context.database.visit({ | ||
...context, | ||
// no skipping schemas for parsing, folks can and will reference across schemas | ||
skipSchemas: [], | ||
handlers: { | ||
[ASTKind.Database]: { | ||
before: async () => { | ||
return `// begin string parsers`; | ||
}, | ||
}, | ||
[ASTKind.Schema]: NamespaceVisitor, | ||
[ASTKind.Types]: NamespaceVisitor, | ||
[ASTKind.Procedures]: NamespaceVisitor, | ||
[ASTKind.Procedure]: NamespaceVisitor, | ||
[ASTKind.Tables]: NamespaceVisitor, | ||
[ASTKind.Table]: NamespaceVisitor, | ||
[ASTKind.Scripts]: NamespaceVisitor, | ||
[ASTKind.ScriptFolder]: NamespaceVisitor, | ||
[ASTKind.Script]: NamespaceVisitor, | ||
[ASTKind.CreateOperation]: NamespaceVisitor, | ||
[ASTKind.Type]: ParseVisitor, | ||
[ASTKind.CompositeType]: ParseVisitor, | ||
[ASTKind.AliasType]: ParseVisitor, | ||
[ASTKind.Enum]: ParseVisitor, | ||
[ASTKind.DomainType]: ParseVisitor, | ||
[ASTKind.ArrayType]: ParseVisitor, | ||
}, | ||
}), | ||
); | ||
generationBuffer.push(`// begin table column parser mapping`); | ||
context.handlers = { | ||
[ASTKind.Schema]: NamespaceVisitor, | ||
[ASTKind.Tables]: NamespaceVisitor, | ||
[ASTKind.Table]: NamespaceVisitor, | ||
[ASTKind.Column]: { | ||
before: async (context, node) => { | ||
const generationBuffer = [ | ||
await NamespaceVisitor.before!(context, node), | ||
]; | ||
generationBuffer.push( | ||
`export const parse = ${node.type.typescriptNamespacedName}.parse;`, | ||
); | ||
return generationBuffer.join("\n"); | ||
}, | ||
after: NamespaceVisitor.after, | ||
}, | ||
}; | ||
// no skipping schemas for parsing | ||
generationBuffer.push( | ||
await context.database.visit({ ...context, skipSchemas: [] }), | ||
await context.database.visit({ | ||
...context, | ||
handlers: { | ||
[ASTKind.Database]: { | ||
before: async () => { | ||
return `// begin table column parser mapping`; | ||
}, | ||
}, | ||
[ASTKind.Schema]: NamespaceVisitor, | ||
[ASTKind.Tables]: NamespaceVisitor, | ||
[ASTKind.Table]: NamespaceVisitor, | ||
[ASTKind.Column]: { | ||
before: async (context, node) => { | ||
const generationBuffer = [ | ||
await NamespaceVisitor.before(context, node), | ||
]; | ||
generationBuffer.push( | ||
`export const parse = ${node.type.typescriptNamespacedName}.parse;`, | ||
); | ||
return generationBuffer.join("\n"); | ||
}, | ||
after: NamespaceVisitor.after, | ||
}, | ||
}, | ||
skipSchemas: [], | ||
}), | ||
); | ||
generationBuffer.push(`// end string parsers`); | ||
return generationBuffer.join("\n"); | ||
} |
@@ -0,3 +1,84 @@ | ||
import { Context } from "./context"; | ||
import postgres from "postgres"; | ||
export { initializeContext } from "./context"; | ||
export type { Context } from "./context"; | ||
export * from "./generator"; | ||
interface ConstructorOf<T> { | ||
new (context: Context): T; | ||
} | ||
/** | ||
* A single postgres database. Inherit from this in generated code. | ||
*/ | ||
export abstract class PostgresDatabase { | ||
constructor(public context: Context) {} | ||
/** | ||
* Clean up the connection. | ||
*/ | ||
public async disconnect() { | ||
await this.context.sql.end(); | ||
} | ||
get cls(): ConstructorOf<this> { | ||
const current = Object.getPrototypeOf(this).constructor; | ||
return current; | ||
} | ||
get self(): this { | ||
return this; | ||
} | ||
/** | ||
* Returns a database scoped to a new transaction. | ||
* You must explicitly call `rollback` or `commit`. | ||
*/ | ||
async beginTransaction() { | ||
const myself = this.self; | ||
const CurrentSubclass = this.cls; | ||
return await new Promise<{ | ||
database: typeof myself; | ||
commit: () => void; | ||
rollback: (message?: string) => void; | ||
}>((resolveReady) => { | ||
const complete = new Promise((resolve, reject) => { | ||
this.context.sql | ||
.begin(async (sql) => { | ||
resolveReady({ | ||
database: new CurrentSubclass({ ...this.context, sql }), | ||
commit: () => resolve(true), | ||
rollback: (message?: string) => reject(message), | ||
}); | ||
await complete; | ||
}) | ||
.catch((reason) => reason); | ||
}); | ||
}); | ||
} | ||
/** | ||
* Use the database inside a transaction. | ||
* | ||
* A successful return is a commit. | ||
* An escaping exception is a rollback. | ||
*/ | ||
async withTransaction<T>(body: (database: this) => Promise<T>) { | ||
const CurrentSubclass = this.cls; | ||
if (this.context.sql.begin) { | ||
// root transaction | ||
return await this.context.sql.begin( | ||
async (sql) => | ||
await body(new CurrentSubclass({ ...this.context, sql })), | ||
); | ||
} else { | ||
// nested transaction | ||
const nested = this.context.sql as postgres.TransactionSql; | ||
return await nested.savepoint( | ||
async (sql) => | ||
await body(new CurrentSubclass({ ...this.context, sql })), | ||
); | ||
} | ||
} | ||
} |
@@ -1,4 +0,1 @@ | ||
import { PGProc } from "./generator/pgtype/pgproc/pgproc"; | ||
import { camelCase } from "change-case"; | ||
/** | ||
@@ -25,30 +22,10 @@ * Handy utility to group up a list by a predicate that picks part of a list | ||
// identifier legalizer? | ||
const notLegalInIdentifiers = /[^\w$]/g; | ||
/** | ||
* Clean identifiers to be only legal characters. | ||
* Unnamed positional parameters in postgres are one based | ||
*/ | ||
export const cleanIdentifierForTypescript = (identifier: string) => { | ||
return identifier.replace(notLegalInIdentifiers, "_"); | ||
}; | ||
export function oneBasedArgumentNamefromZeroBasedIndex(index: number) { | ||
return `argument_${index + 1}`; | ||
} | ||
/** | ||
* Procs in postgres can have parameters without names -- positional parameters. | ||
* | ||
* In postgres sql, these get 'numerical' names, so we'll follow along | ||
* for numerical style typescript parameter names. | ||
*/ | ||
export const buildTypescriptParameterName = ( | ||
proc: PGProc, | ||
parameterIndex: number, | ||
) => { | ||
if (proc.proc.proargnames[parameterIndex] !== undefined) { | ||
return camelCase(proc.proc.proargnames[parameterIndex]); | ||
} else { | ||
return `_${parameterIndex}`; | ||
} | ||
}; | ||
/** | ||
* Take a string and turn it into a doc comment. | ||
@@ -55,0 +32,0 @@ */ |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
No README
QualityPackage does not have a README. This may indicate a failed publish or a low quality package.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
5
111495
55
3268
2
0
+ Added@embracesql/shared@0.0.6(transitive)
- Removedprettier@3.1.1
- Removed@embracesql/shared@0.0.4(transitive)
- Removedprettier@3.1.1(transitive)
Updated@embracesql/shared@^0.0.6