Skip to content

Commit

Permalink
Add initial insert & update queries
Browse files Browse the repository at this point in the history
  • Loading branch information
ChiriVulpes committed Mar 4, 2024
1 parent c57934c commit 56260f9
Show file tree
Hide file tree
Showing 8 changed files with 228 additions and 39 deletions.
20 changes: 18 additions & 2 deletions src/IStrongPG.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { ExpressionOr } from "./expressions/Expression";

export type Type = DataTypeID;

export enum DataTypeID {
Expand Down Expand Up @@ -139,7 +141,7 @@ export type DataTypeFromString<STR extends TypeString> =

export type ValidDate = Date | number | typeof Keyword.CurrentTimestamp;

export interface TypeMap {
export interface MigrationTypeMap {
// numeric
[DataTypeID.SMALLINT]: number;
[DataTypeID.INTEGER]: number;
Expand Down Expand Up @@ -171,13 +173,27 @@ export interface TypeMap {

// special
[DataTypeID.TSVECTOR]: null;
[DataTypeID.JSON]: null;
}

export interface InputTypeMap extends Omit<MigrationTypeMap, DataTypeID.JSON> {
[DataTypeID.JSON]: any;
}

export interface OutputTypeMap extends Omit<InputTypeMap, DataTypeID.DATE | DataTypeID.TIMESTAMP | DataTypeID.TIME> {
// datetime
[DataTypeID.DATE]: Date;
[DataTypeID.TIMESTAMP]: Date;
[DataTypeID.TIME]: Date;
// INTERVAL,
}

export type ValidType = string | boolean | number | symbol | Date | RegExp | undefined | null;

export const SYMBOL_COLUMNS = Symbol("COLUMNS");
export type TypeFromString<STR extends TypeString> = STR extends "*" ? typeof SYMBOL_COLUMNS : TypeMap[DataTypeFromString<STR>];
export type MigrationTypeFromString<STR extends TypeString> = STR extends "*" ? typeof SYMBOL_COLUMNS : MigrationTypeMap[DataTypeFromString<STR>];
export type InputTypeFromString<STR extends TypeString, VARS = {}> = STR extends "*" ? typeof SYMBOL_COLUMNS : ExpressionOr<VARS, InputTypeMap[DataTypeFromString<STR>]>;
export type OutputTypeFromString<STR extends TypeString> = STR extends "*" ? typeof SYMBOL_COLUMNS : OutputTypeMap[DataTypeFromString<STR>];

export namespace TypeString {
export function resolve (typeString: TypeString) {
Expand Down
4 changes: 3 additions & 1 deletion src/Schema.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { DataTypeID, EnumToTuple, TypeString, TypeStringMap } from "./IStrongPG";
import { DataTypeID, EnumToTuple, InputTypeFromString, OutputTypeFromString, TypeString, TypeStringMap } from "./IStrongPG";

interface SpecialKeys<SCHEMA> {
PRIMARY_KEY?: keyof SCHEMA | (keyof SCHEMA)[];
Expand Down Expand Up @@ -153,6 +153,8 @@ namespace Schema {
export type ColumnTyped<SCHEMA, TYPE> =
keyof { [COLUMN in keyof SCHEMA as COLUMN extends keyof SpecialKeys<any> ? never : SCHEMA[COLUMN] extends Vaguify<TYPE> ? COLUMN : never]: SCHEMA[COLUMN] };
export type Columns<SCHEMA> = { [COLUMN in keyof SCHEMA as COLUMN extends keyof SpecialKeys<any> ? never : COLUMN]: SCHEMA[COLUMN] };
export type RowOutput<SCHEMA> = { [COLUMN in keyof SCHEMA as COLUMN extends keyof SpecialKeys<any> ? never : COLUMN]: OutputTypeFromString<Extract<SCHEMA[COLUMN], TypeString>> };
export type RowInput<SCHEMA> = { [COLUMN in keyof SCHEMA as COLUMN extends keyof SpecialKeys<any> ? never : COLUMN]: InputTypeFromString<Extract<SCHEMA[COLUMN], TypeString>> };

type Vaguify<T> = T extends TypeStringMap[DataTypeID.BIGINT] ? TypeStringMap[DataTypeID.BIGINT] | TypeStringMap[DataTypeID.BIGSERIAL]
: T;
Expand Down
72 changes: 68 additions & 4 deletions src/Table.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,81 @@
import { Initialiser } from "./IStrongPG";
import Schema, { TableSchema } from "./Schema";
import InsertIntoTable, { InsertIntoTableFactory } from "./statements/Insert";
import SelectFromTable from "./statements/Select";
import UpdateTable from "./statements/Update";

export default class Table<SCHEMA extends TableSchema> {
public constructor (protected readonly name: string, protected readonly schema: SCHEMA) {
}

/**
* SELECT *
*/
public select (): SelectFromTable<SCHEMA, "*"[]>;
/**
* SELECT *
* ...then provide an initialiser for tweaking the query
*/
public select<RETURN extends SelectFromTable<SCHEMA, "*"[], any> = SelectFromTable<SCHEMA, "*"[]>> (initialiser: Initialiser<SelectFromTable<SCHEMA, "*"[]>, RETURN>): RETURN;
/**
* SELECT columns
*/
public select<COLUMNS extends Schema.Column<SCHEMA>[]> (...columns: COLUMNS): SelectFromTable<SCHEMA, COLUMNS>;
public select<COLUMNS extends Schema.Column<SCHEMA>[]> (...columnsAndInitialiser: [...COLUMNS, Initialiser<SelectFromTable<SCHEMA, COLUMNS>>]): SelectFromTable<SCHEMA, COLUMNS>;
public select (...params: (Schema.Column<SCHEMA> | Initialiser<SelectFromTable<SCHEMA>>)[]) {
/**
* SELECT columns
* ...then provide an initialiser for tweaking the query
*/
public select<COLUMNS extends Schema.Column<SCHEMA>[], RETURN extends SelectFromTable<SCHEMA, COLUMNS, any>> (...columnsAndInitialiser: [...COLUMNS, Initialiser<SelectFromTable<SCHEMA, COLUMNS>, RETURN>]): RETURN;
public select (...params: (Schema.Column<SCHEMA> | "*" | Initialiser<SelectFromTable<SCHEMA>> | Initialiser<SelectFromTable<SCHEMA, "*"[]>>)[]): SelectFromTable<SCHEMA, Schema.Column<SCHEMA>[]> | SelectFromTable<SCHEMA, "*"[]> {
const initialiser = typeof params[params.length - 1] === "function" ? params.pop() as Initialiser<SelectFromTable<SCHEMA>> : undefined;
const query = new SelectFromTable<SCHEMA>(this.name, this.schema, params as Schema.Column<SCHEMA>[]);
initialiser?.(query);
return query;
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return initialiser?.(query) ?? query;
}

public insert (data: Partial<Schema.RowInput<SCHEMA>>): InsertIntoTable<SCHEMA>;
public insert (data: Partial<Schema.RowInput<SCHEMA>>, initialiser: Initialiser<InsertIntoTable<SCHEMA>>): InsertIntoTable<SCHEMA>;
public insert<COLUMNS extends Schema.Column<SCHEMA>[]> (...columns: COLUMNS): InsertIntoTableFactory<SCHEMA, COLUMNS>;
public insert<COLUMNS extends Schema.Column<SCHEMA>[], RETURN extends InsertIntoTableFactory<SCHEMA, COLUMNS> | InsertIntoTable<SCHEMA>> (...columnsAndInitialiser: [...COLUMNS, Initialiser<InsertIntoTableFactory<SCHEMA, COLUMNS>, RETURN>]): RETURN;
public insert (...params: (boolean | Partial<Schema.RowInput<SCHEMA>> | Schema.Column<SCHEMA> | Initialiser<InsertIntoTableFactory<SCHEMA>> | Initialiser<InsertIntoTable<SCHEMA>>)[]): InsertIntoTableFactory<SCHEMA> | InsertIntoTable<SCHEMA> {
const isUpsert = params[0] === true;
if (typeof params[0] === "boolean")
params.shift();

const initialiser = typeof params[params.length - 1] === "function" ? params.pop() as Initialiser<InsertIntoTableFactory<SCHEMA> | InsertIntoTable<SCHEMA>> : undefined;

if (typeof params[0] === "object") {
const keys = Object.keys(params[0]);
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
const query = ((this.insert as any)(isUpsert as any, ...keys as Schema.Column<SCHEMA>[]) as InsertIntoTableFactory<SCHEMA>)
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-return
.values(...keys.map(key => (params[0] as any)[key]));

return initialiser?.(query as InsertIntoTable<SCHEMA>) as InsertIntoTable<SCHEMA> ?? query;
}

const query = InsertIntoTable.columns<SCHEMA>(this.name, this.schema, params as Schema.Column<SCHEMA>[], isUpsert);
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return initialiser?.(query) ?? query;
}

public upsert (data: Schema.RowInput<SCHEMA>): InsertIntoTable<SCHEMA>;
public upsert<RETURN extends InsertIntoTable<SCHEMA, any>> (data: Schema.RowInput<SCHEMA>, initialiser: Initialiser<InsertIntoTable<SCHEMA>, RETURN>): RETURN;
public upsert<COLUMNS extends Schema.Column<SCHEMA>[]> (...columns: COLUMNS): InsertIntoTableFactory<SCHEMA, COLUMNS>;
public upsert<COLUMNS extends Schema.Column<SCHEMA>[], RETURN extends InsertIntoTableFactory<SCHEMA, COLUMNS> | InsertIntoTable<SCHEMA>> (...columnsAndInitialiser: [...COLUMNS, Initialiser<InsertIntoTableFactory<SCHEMA, COLUMNS>, RETURN>]): RETURN;
public upsert (...params: (Schema.RowInput<SCHEMA> | Schema.Column<SCHEMA> | Initialiser<InsertIntoTableFactory<SCHEMA>> | Initialiser<InsertIntoTable<SCHEMA>>)[]): InsertIntoTableFactory<SCHEMA> | InsertIntoTable<SCHEMA> {
// eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-call
return (this.insert as any)(true, ...params as Schema.Column<SCHEMA>[]);
}

public update (data: Schema.RowInput<SCHEMA>): UpdateTable<SCHEMA>;
public update<RETURN extends UpdateTable<SCHEMA, any>> (data: Schema.RowInput<SCHEMA>, initialiser: Initialiser<UpdateTable<SCHEMA>, RETURN>): RETURN;
public update (data: Schema.RowInput<SCHEMA>, initialiser?: Initialiser<UpdateTable<SCHEMA>, UpdateTable<SCHEMA, any>>): UpdateTable<SCHEMA, any> {
const query = new UpdateTable<SCHEMA, any>(this.name, this.schema);
for (const key of Object.keys(data))
query.set(key as Schema.Column<SCHEMA>, data[key]);

// eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-argument
return initialiser?.(query) ?? query;
}
}
47 changes: 27 additions & 20 deletions src/expressions/Expression.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Initialiser, TypeFromString, TypeString, ValidType } from "../IStrongPG";
import { Initialiser, MigrationTypeFromString, TypeString, ValidType } from "../IStrongPG";
import Statement from "../statements/Statement";

export interface ExpressionOperations<VARS = never, CURRENT_VALUE = null> {
Expand All @@ -8,7 +8,7 @@ export interface ExpressionOperations<VARS = never, CURRENT_VALUE = null> {
equals: ExpressionValue<VARS, CURRENT_VALUE, boolean>;
or: ExpressionValue<VARS, boolean, boolean>;
matches: CURRENT_VALUE extends string ? ExpressionValue<VARS, RegExp, boolean> : never;
as<TYPE extends TypeString> (type: TYPE): ExpressionOperations<VARS, TypeFromString<TYPE>>;
as<TYPE extends TypeString> (type: TYPE): ExpressionOperations<VARS, MigrationTypeFromString<TYPE>>;
}

export interface ExpressionValue<VARS = never, EXPECTED_VALUE = null, RESULT = null> {
Expand All @@ -18,7 +18,7 @@ export interface ExpressionValue<VARS = never, EXPECTED_VALUE = null, RESULT = n

export interface ExpressionValues<VARS = never, VALUE = null, RESULT = null> {
value: ExpressionValue<VARS, VALUE, RESULT>;
var<VAR extends keyof VARS> (name: VAR): ExpressionOperations<VARS, TypeFromString<VARS[VAR] & TypeString>>;
var<VAR extends keyof VARS> (name: VAR): ExpressionOperations<VARS, MigrationTypeFromString<VARS[VAR] & TypeString>>;
lowercase: ExpressionValue<VARS, string, string>;
uppercase: ExpressionValue<VARS, string, string>;
nextValue (sequenceId: string): ExpressionOperations<VARS, number>;
Expand All @@ -27,14 +27,33 @@ export interface ExpressionValues<VARS = never, VALUE = null, RESULT = null> {

export type ExpressionInitialiser<VARS, RESULT = any> = Initialiser<ExpressionValues<VARS, null, null>, ExpressionOperations<VARS, RESULT>>;

export type ExpressionOr<VARS, T> = T | ExpressionInitialiser<VARS, T>;

export type ImplementableExpression = { [KEY in keyof ExpressionValues | keyof ExpressionOperations]: any };

export default class Expression<VARS = never> implements ImplementableExpression {

public static stringifyValue<VARS = never> (value: ExpressionOr<VARS, ValidType>, vars?: any[], enableStringConcatenation = false) {
let result: string;
if (typeof value === "function") {
const expr = new Expression(vars, enableStringConcatenation);
value(expr as any as ExpressionValues<VARS, null, null>);
result = `(${expr.compile()})`;
} else if (typeof value === "string" && !enableStringConcatenation) {
vars ??= [];
vars.push(value);
result = `$${vars.length}`;
} else {
result = Expression.stringifyValueRaw(value);
}

return result;
}

/**
* Warning: Do not use outside of migrations
*/
public static stringifyValue (value: ValidType) {
public static stringifyValueRaw (value: ValidType) {
switch (typeof value) {
case "string":
return `'${value}'`;
Expand All @@ -61,8 +80,8 @@ export default class Expression<VARS = never> implements ImplementableExpression
}
}

public static compile (initialiser: ExpressionInitialiser<any, any>, enableStringConcatenation = false) {
const expr = new Expression(undefined, enableStringConcatenation);
public static compile (initialiser: ExpressionInitialiser<any, any>, enableStringConcatenation = false, vars?: any[]) {
const expr = new Expression(vars, enableStringConcatenation);
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
initialiser(expr as any);
return new Statement.Queryable(expr.compile(), undefined, expr.vars);
Expand Down Expand Up @@ -121,20 +140,8 @@ export default class Expression<VARS = never> implements ImplementableExpression

public value (value: ValidType | Initialiser<Expression>, mapper?: (value: string) => string) {
this.parts.push(() => {
let result: string;
if (typeof value === "function") {
const expr = new Expression(this.vars, this.enableStringConcatenation);
value(expr);
result = `(${expr.compile()})`;
} else if (typeof value === "string" && !this.enableStringConcatenation) {
this.vars ??= [];
this.vars.push(value);
result = `$${this.vars.length}`;
} else {
result = Expression.stringifyValue(value);
}

return mapper ? mapper(result) : result;
const stringified = Expression.stringifyValue(value as ValidType | ExpressionInitialiser<VARS>, this.vars, this.enableStringConcatenation);
return mapper ? mapper(stringified) : stringified;
});

return this;
Expand Down
66 changes: 66 additions & 0 deletions src/statements/Insert.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { QueryResult } from "pg";
import { Initialiser, InputTypeFromString, Value } from "../IStrongPG";
import Schema, { TableSchema } from "../Schema";
import Expression from "../expressions/Expression";
import Statement from "./Statement";
import UpdateTable from "./Update";

export interface InsertIntoTableFactory<SCHEMA extends TableSchema, COLUMNS extends Schema.Column<SCHEMA>[] = Schema.Column<SCHEMA>[]> {
values (...values: { [I in keyof COLUMNS]: InputTypeFromString<SCHEMA[COLUMNS[I]]> }): InsertIntoTable<SCHEMA, COLUMNS>;
}

export default class InsertIntoTable<SCHEMA extends TableSchema, RESULT = []> extends Statement<RESULT> {

public static columns<SCHEMA extends TableSchema, COLUMNS extends Schema.Column<SCHEMA>[] = Schema.Column<SCHEMA>[]> (tableName: string, schema: SCHEMA, columns: COLUMNS, isUpsert = false): InsertIntoTableFactory<SCHEMA, COLUMNS> {
return {
values: (...values: any[]) => {
const query = new InsertIntoTable<SCHEMA, COLUMNS>(tableName, schema, columns, values as never);
if (isUpsert) {
query.onConflictDoUpdate(update => {
for (let i = 0; i < columns.length; i++) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
update.set(columns[i], values[i]);
}
});
}

return query;
},
};
}

private vars: any[] = [];
public constructor (public readonly tableName: string, public readonly schema: SCHEMA, public readonly columns: Schema.Column<SCHEMA>[], public readonly values: Value<Schema.RowInput<SCHEMA>>[]) {
super();
}

private onConflict?: null | UpdateTable<SCHEMA, any>;
public onConflictDoNothing () {
this.onConflict = null;
return this;
}

public onConflictDoUpdate (initialiser: Initialiser<UpdateTable<SCHEMA, any>>) {
this.onConflict = new UpdateTable(undefined, this.schema, this.vars);
initialiser(this.onConflict);
return this;
}

public compile () {
const values = this.values.map(value => Expression.stringifyValue(value, this.vars)).join(",");
let onConflict = this.onConflict === undefined ? " "
: this.onConflict === null ? "ON CONFLICT DO NOTHING"
: undefined;

if (this.onConflict) {
const compiled = this.onConflict.compile()[0];
onConflict = `ON CONFLICT DO ${compiled.text}`;
}

return this.queryable(`INSERT INTO ${this.tableName} (${this.columns.join(",")}) VALUES (${values}) ${onConflict!}`, undefined, this.vars);
}

protected override resolveQueryOutput (output: QueryResult<any>) {
return output.rows as RESULT;
}
}
10 changes: 5 additions & 5 deletions src/statements/Select.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { QueryResult } from "pg";
import { TypeFromString } from "../IStrongPG";
import { MigrationTypeFromString, OutputTypeFromString } from "../IStrongPG";
import Schema, { TableSchema } from "../Schema";
import Expression, { ExpressionInitialiser } from "../expressions/Expression";
import Statement from "./Statement";

type SingleStringUnion<T> = ((k: ((T extends any ? () => T : never) extends infer U ? ((U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never) extends () => (infer R) ? R : never : never)) => any) extends (k: T) => any ? T : never;

export default class SelectFromTable<SCHEMA extends TableSchema, COLUMNS extends Schema.Column<SCHEMA>[] = Schema.Column<SCHEMA>[]> extends Statement<{ [K in COLUMNS[number]]: TypeFromString<SCHEMA[K]> }[]> {
export default class SelectFromTable<SCHEMA extends TableSchema, COLUMNS extends (Schema.Column<SCHEMA> | "*")[] = Schema.Column<SCHEMA>[], RESULT = { [K in "*" extends COLUMNS[number] ? Schema.Column<SCHEMA> : COLUMNS[number]]: OutputTypeFromString<SCHEMA[K]> }[]> extends Statement<RESULT> {

private vars?: any[];
public constructor (public readonly tableName: string, public readonly schema: SCHEMA, public readonly columns: COLUMNS) {
Expand All @@ -22,12 +22,12 @@ export default class SelectFromTable<SCHEMA extends TableSchema, COLUMNS extends
}

private isPrimaryKeyed?: true;
public primaryKeyed (id: TypeFromString<SCHEMA[SingleStringUnion<Schema.PrimaryKey<SCHEMA>[number]>]>) {
public primaryKeyed (id: MigrationTypeFromString<SCHEMA[SingleStringUnion<Schema.PrimaryKey<SCHEMA>[number]>]>) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
const primaryKey = this.schema["PRIMARY_KEY"][0];
this.isPrimaryKeyed = true;
this.where(expr => expr.var(primaryKey).equals(id));
return this as Statement<{ [K in COLUMNS[number]]: TypeFromString<SCHEMA[K]> } | undefined>;
this.where(expr => expr.var(primaryKey).equals(id as never));
return this as SelectFromTable<SCHEMA, COLUMNS, { [K in "*" extends COLUMNS[number] ? Schema.Column<SCHEMA> : COLUMNS[number]]: OutputTypeFromString<SCHEMA[K]> } | undefined>;
}

public compile () {
Expand Down
34 changes: 34 additions & 0 deletions src/statements/Update.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { QueryResult } from "pg";
import { InputTypeFromString, ValidType } from "../IStrongPG";
import Schema, { TableSchema } from "../Schema";
import Expression from "../expressions/Expression";
import Statement from "./Statement";

export default class UpdateTable<SCHEMA extends TableSchema, RESULT = []> extends Statement<RESULT> {

private vars?: any[];
public constructor (public readonly tableName: string | undefined, public readonly schema: SCHEMA, vars?: any[]) {
super();
this.vars = vars ?? [];
}

private assignments: string[] = [];
public set (input: Partial<Schema.RowInput<SCHEMA>>): this;
public set<COLUMN_NAME extends Schema.Column<SCHEMA>> (column: COLUMN_NAME, value: InputTypeFromString<SCHEMA[COLUMN_NAME]>): this;
public set (input: Schema.Column<SCHEMA> | Partial<Schema.RowInput<SCHEMA>>, value?: ValidType) {
if (typeof input === "object")
for (const column of Object.keys(input))
this.assignments.push(`${column} = ${Expression.stringifyValue(input[column], this.vars)}`);
else
this.assignments.push(`${String(input)} = ${Expression.stringifyValue(value, this.vars)}`);
return this;
}

public compile () {
return this.queryable(`UPDATE ${this.tableName ?? ""} SET ${this.assignments.join(", ")}`, undefined, this.vars);
}

protected override resolveQueryOutput (output: QueryResult<any>) {
return output.rows as RESULT;
}
}
Loading

0 comments on commit 56260f9

Please sign in to comment.