Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

@embracesql/postgres

Package Overview
Dependencies
Maintainers
1
Versions
18
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@embracesql/postgres - npm Package Compare versions

Comparing version 0.0.4 to 0.0.6

src/generator/typescript/autocrud/create.ts

9

package.json
{
"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 @@ */

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc