prostgles-types
Version:
Shared TypeScript object definitions for prostgles-client and prostgles-server
1,565 lines (1,369 loc) • 42 kB
text/typescript
import * as AuthTypes from "./auth";
import { FileColumnConfig } from "./files";
import { AnyObject, ComplexFilter, FullFilter, FullFilterBasic, ValueOf } from "./filters";
import type { UpsertDataToPGCast } from "./insertUpdateUtils";
import { JSONB } from "./JSONBSchemaValidation/JSONBSchema";
import { getKeys, includes, isDefined } from "./util";
export const _PG_strings = [
"bpchar",
"char",
"varchar",
"text",
"citext",
"uuid",
"bytea",
"time",
"timetz",
"interval",
"name",
"cidr",
"inet",
"macaddr",
"macaddr8",
"int4range",
"int8range",
"numrange",
"tsvector",
] as const;
export const _PG_numbers_num = ["int2", "int4", "float4", "float8", "oid"] as const;
export const _PG_numbers_str = ["int8", "numeric", "money"] as const;
export const _PG_numbers = [..._PG_numbers_num, ..._PG_numbers_str] as const;
export const _PG_json = ["json", "jsonb"] as const;
export const _PG_bool = ["bool"] as const;
export const _PG_date = ["date", "timestamp", "timestamptz"] as const;
export const _PG_interval = ["interval"] as const;
export const _PG_postgis = ["geometry", "geography"] as const;
export const _PG_geometric = ["point", "line", "lseg", "box", "path", "polygon", "circle"] as const;
export type PG_COLUMN_UDT_DATA_TYPE =
| (typeof _PG_strings)[number]
| (typeof _PG_numbers)[number]
| (typeof _PG_geometric)[number]
| (typeof _PG_json)[number]
| (typeof _PG_bool)[number]
| (typeof _PG_date)[number]
| (typeof _PG_interval)[number]
| (typeof _PG_postgis)[number];
const TS_PG_PRIMITIVES = {
string: [
..._PG_strings,
..._PG_numbers_str,
..._PG_date,
..._PG_geometric,
..._PG_postgis,
"lseg",
],
number: _PG_numbers_num,
boolean: _PG_bool,
any: [..._PG_json, ..._PG_interval], // consider as any
/** Timestamps are kept in original string format to avoid filters failing
* TODO: cast to dates if udt_name date/timestamp(0 - 3)
*/
// "Date": _PG_date,
} as const;
export const TS_PG_Types = {
...TS_PG_PRIMITIVES,
"number[]": TS_PG_PRIMITIVES.number.map((s) => `_${s}` as const),
"boolean[]": TS_PG_PRIMITIVES.boolean.map((s) => `_${s}` as const),
"string[]": TS_PG_PRIMITIVES.string.map((s) => `_${s}` as const),
"any[]": TS_PG_PRIMITIVES.any.map((s) => `_${s}` as const),
// "Date[]": _PG_date.map(s => `_${s}` as const),
// "any": [],
} as const;
export type TS_COLUMN_DATA_TYPES = keyof typeof TS_PG_Types;
export const postgresToTsType = (
udt_data_type: PG_COLUMN_UDT_DATA_TYPE
): keyof typeof TS_PG_Types => {
return (
getKeys(TS_PG_Types).find((k) => {
return includes(TS_PG_Types[k], udt_data_type);
}) ?? "any"
);
};
/**
* Generated Typescript schema for the tables and views in the database
* Example:
*
*
* type DBSchema = {
* ..view_name: {
* is_view: boolean;
* select: boolean;
* insert: boolean;
* update: boolean;
* delete: boolean;
* insertColumns: { col1?: number | null; col2: string; }
* columns: { col1: number | null; col2: string; }
* }
* }
*/
export type DBTableSchema = {
is_view?: boolean;
select?: boolean;
insert?: boolean;
update?: boolean;
delete?: boolean;
/**
* Used in update, insertm select and filters
* fields that are nullable or with a default value are be optional
*/
columns: AnyObject;
};
export type DBSchema = {
[tov_name: string]: DBTableSchema;
};
type ReferenceTable = {
ftable: string;
fcols: string[];
cols: string[];
};
export type ColumnInfo = {
name: string;
/**
* Column display name. Will be first non empty value from i18n data, comment, name
*/
label: string;
/**
* Column description (if provided)
*/
comment: string | undefined;
/**
* Ordinal position of the column within the table (count starts at 1)
*/
ordinal_position: number;
/**
* True if column is nullable
*
*/
is_nullable: boolean;
is_updatable: boolean;
/**
* If the column is a generated column (converted to boolean from ALWAYS and NEVER)
*/
is_generated: boolean;
/**
* Simplified data type
*/
data_type: string;
/**
* Postgres data type name.
* Array types start with an underscore
*/
udt_name: PG_COLUMN_UDT_DATA_TYPE;
/**
* Element data type
*/
element_type: string | undefined;
/**
* Element data type name
*/
element_udt_name: string | undefined;
/**
* PRIMARY KEY constraint on column.
* A table can have a multi column primary key
*/
is_pkey: boolean;
/**
* Foreign key constraint
* A column can reference multiple tables
*/
references?: ReferenceTable[];
/**
* true if column has a default value
* Used for excluding pkey from insert
*/
has_default: boolean;
/**
* Column default value
*/
column_default?: any;
/**
* @example: "character varying(255)"
*/
character_maximum_length?: number | null;
/**
* @example: "numeric(10,2)" precision,scale
*/
numeric_precision?: number | null;
numeric_scale?: number | null;
/**
* Extracted from tableConfig
* Used in SmartForm
*/
min?: string | number;
max?: string | number;
hint?: string;
/**
* JSONB schema (a simplified version of json schema) for the column (if defined in the tableConfig)
* A check constraint will use this schema for runtime data validation and apropriate TS types will be generated
*/
jsonbSchema?: JSONB.JSONBSchema;
/**
* If defined then this column is referencing the file table
* Extracted from FileTable config
* Used in SmartForm
*/
file?: FileColumnConfig;
};
export type ValidatedColumnInfo = ColumnInfo & {
/**
* TypeScript data type
*/
tsDataType: TS_COLUMN_DATA_TYPES;
/**
* Can be viewed/selected
* Based on access rules and postgres policies
*/
select: boolean;
/**
* Can be ordered by
* Based on access rules
*/
orderBy: boolean;
/**
* Can be filtered by
* Based on access rules
*/
filter: boolean;
/**
* Can be inserted
* Based on access rules and postgres policies
*/
insert: boolean;
/**
* Can be updated
* Based on access rules and postgres policies
*/
update: boolean;
/**
* Can be used in the delete filter
* Based on access rules
*/
delete: boolean;
};
export type DBSchemaTable = {
name: string;
info: TableInfo;
columns: ValidatedColumnInfo[];
};
/**
* List of fields to include or exclude
*/
export type FieldFilter<T extends AnyObject = AnyObject> = SelectTyped<T>;
export type AscOrDesc = 1 | -1 | boolean;
export type OrderByDetailed<T> = {
key: keyof T;
asc?: AscOrDesc | null;
nulls?: "last" | "first" | null;
nullEmpty?: boolean | null;
};
/**
* `{ product_name: -1 }` -> SORT BY product_name DESC
* [{ field_name: (1 | -1 | boolean) }]
* true | 1 -> ascending
* false | -1 -> descending
* Array order is maintained
* if nullEmpty is true then empty text will be replaced to null (so nulls sorting takes effect on it)
*/
export type OrderByTyped<T extends AnyObject> =
| { [K in keyof Partial<T>]: AscOrDesc }
| { [K in keyof Partial<T>]: AscOrDesc }[]
| OrderByDetailed<T>
| OrderByDetailed<T>[]
| Array<keyof T>
| keyof T;
export type OrderBy<T extends AnyObject | void = void> =
T extends AnyObject ? OrderByTyped<T> : OrderByTyped<AnyObject>;
export type CommonSelect = "*" | "" | { "*": 1 };
export type SelectTyped<T extends AnyObject> =
| { [K in keyof Partial<T>]: 1 | true }
| { [K in keyof Partial<T>]: 0 | false }
| (keyof T)[]
| CommonSelect;
export const JOIN_KEYS = ["$innerJoin", "$leftJoin"] as const;
export const JOIN_PARAMS = [
"select",
"filter",
"$path",
"$condition",
"offset",
"limit",
"orderBy",
] as const;
export type JoinCondition =
| {
column: string;
rootColumn: string;
}
| ComplexFilter;
export type JoinPath = {
table: string;
/**
* {
* leftColumn: "rightColumn"
* }
*/
on?: Record<string, string>[];
};
export type RawJoinPath = string | (JoinPath | string)[];
export type DetailedJoinSelect = Partial<Record<(typeof JOIN_KEYS)[number], RawJoinPath>> & {
select: Select;
filter?: FullFilter<void, void>;
having?: FullFilter<void, void>;
offset?: number;
limit?: number;
orderBy?: OrderBy;
} & (
| {
$condition?: undefined;
}
| {
/**
* If present then will overwrite $path and any inferred joins
*/
$condition?: JoinCondition[];
}
);
export type SimpleJoinSelect =
| "*"
/** Aliased Shorthand join: table_name: { ...select } */
| Record<string, 1 | "*" | true | FunctionSelect>
| Record<string, 0 | false>;
export type JoinSelect = SimpleJoinSelect | DetailedJoinSelect;
type FunctionShorthand = string;
type FunctionFull = Record<string, any[] | readonly any[] | FunctionShorthand>;
type FunctionSelect = FunctionShorthand | FunctionFull;
/**
* { computed_field: { funcName: [args] } }
*/
type FunctionAliasedSelect = Record<string, FunctionFull>;
type InclusiveSelect = true | 1 | FunctionSelect | JoinSelect;
type SelectFuncs<T extends AnyObject = AnyObject, IsTyped = false> =
| ({ [K in keyof Partial<T>]: InclusiveSelect } & Record<
string,
IsTyped extends true ? FunctionFull : InclusiveSelect
>)
| FunctionAliasedSelect
| { [K in keyof Partial<T>]: true | 1 | string }
| { [K in keyof Partial<T>]: 0 | false }
| CommonSelect
| (keyof Partial<T>)[];
/** S param is needed to ensure the non typed select works fine */
export type Select<T extends AnyObject | void = void, S extends DBSchema | void = void> =
{
t: T;
s: S;
} extends { t: AnyObject; s: DBSchema } ?
SelectFuncs<T & { $rowhash: string }, true>
: SelectFuncs<AnyObject & { $rowhash: string }, false>;
export type SelectBasic = { [key: string]: any } | {} | undefined | "" | "*";
/**
* Will return the first row as an object. Will throw an error if more than a row is returned. Use limit: 1 to avoid error.
*/
type ReturnTypeRow = "row";
/* Simpler types */
type CommonSelectParams = {
/**
* Max number of rows to return. Defaults to 1000
* - On client publish rules can affect this behaviour: cannot request more than the maxLimit (if present)
*/
limit?: number | null;
/**
* Number of rows to skip
*/
offset?: number;
/**
* Will group by all non aggregated fields specified in select (or all fields by default)
*/
groupBy?: boolean;
/**
* Result data structure/type:
* - **row**: the first row as an object
* - **value**: the first value from of first field
* - **values**: array of values from the selected field
* - **statement**: sql statement
* - **statement-no-rls**: sql statement without row level security
* - **statement-where**: sql statement where condition
*/
returnType?:
| ReturnTypeRow
/**
* Will return the first value from the selected field
*/
| "value"
/**
* Will return an array of values from the selected field. Similar to array_agg(field).
*/
| "values"
/**
* Will return the sql statement. Requires publishRawSQL privileges if called by client
*/
| "statement"
/**
* Will return the sql statement excluding the user header. Requires publishRawSQL privileges if called by client
*/
| "statement-no-rls"
/**
* Will return the sql statement where condition. Requires publishRawSQL privileges if called by client
*/
| "statement-where";
};
export type SelectParams<
T extends AnyObject | void = void,
S extends DBSchema | void = void,
> = CommonSelectParams & {
/**
* Fields/expressions/linked data to select
* - `"*"` or empty will return all fields
* - `{ field: 0 }` - all fields except the specified field will be selected
* - `{ field: 1 }` - only the specified field will be selected
* - `{ field: { $funcName: [args] } }` - the field will be selected with the specified function applied
* - `{ field: 1, referencedTable: "*" }` - field together with all fields from referencedTable will be selected
* - `{ linkedData: { referencedTable: { field: 1 } } }` - linkedData will contain the linked/joined records from referencedTable
*/
select?: Select<T, S>;
/**
* Order by options
* - Order is maintained in arrays
* - `[{ key: "field", asc: true, nulls: "last" }]`
*/
orderBy?: OrderBy<S extends DBSchema ? T : void>;
/**
* Filter applied after any aggregations (group by)
*/
having?: FullFilter<T, S>;
};
type SubscribeActions = "insert" | "delete" | "update";
export type SubscribeOptions = {
/**
* If true then the first value will not be emitted
* */
skipFirst?: boolean;
/**
* Controls which actions will trigger the subscription.
* If not provided then all actions will be triggered
*/
actions?: Partial<Record<SubscribeActions, true> | Record<SubscribeActions, false>>;
/**
* If true then the subscription will be triggered without first checking if selected column values have changed
* @default false
*/
skipChangedColumnsCheck?: boolean;
/**
* If provided then the subscription will be throttled to the provided number of milliseconds
*/
throttle?: number;
throttleOpts?: {
/**
* False by default.
* If true then the first value will be emitted at the end of the interval. Instant otherwise
* */
skipFirst?: boolean;
};
};
export type SubscribeParams<
T extends AnyObject | void = void,
S extends DBSchema | void = void,
> = SelectParams<T, S> & SubscribeOptions;
export type UpdateParams<T extends AnyObject | void = void, S extends DBSchema | void = void> = {
/**
* If defined will returns the specified fields of the updated record(s)
*/
returning?: Select<T, S>;
/**
* Used for sync.
* If true then only valid and allowed fields will be updated
*/
removeDisallowedFields?: boolean;
/* true by default. If false the update will fail if affecting more than one row */
multi?: boolean;
} & Pick<CommonSelectParams, "returnType">;
export type InsertParams<T extends AnyObject | void = void, S extends DBSchema | void = void> = {
/**
* If defined will returns the specified fields of the updated record(s)
*/
returning?: Select<T, S>;
/**
* By default the insert may fail due to a unique/exclusion constraint violation error. To control this:
* - DoNothing: will ignore the error and do nothing
* - DoUpdate: will update all non primary key columns of the conflicting row
*/
onConflict?: "DoNothing" | "DoUpdate";
/**
* Used for sync.
* If true then only valid and allowed fields will be inserted
*/
removeDisallowedFields?: boolean;
} & Pick<CommonSelectParams, "returnType">;
export type DeleteParams<T extends AnyObject | void = void, S extends DBSchema | void = void> = {
returning?: Select<T, S>;
} & Pick<CommonSelectParams, "returnType">;
export type PartialLax<T = AnyObject> = Partial<T>;
type FileTableConfig = {
/**
* Defined if direct inserts are disabled.
* Only nested inserts through the specified tables/columns are allowed
* */
allowedNestedInserts?:
| {
table: string;
column: string;
}[]
| undefined;
};
export type TableInfo = {
/**
* OID from the postgres database
* Useful in handling renamed tables
*/
oid: number;
/**
* Comment from the postgres database
*/
comment?: string;
/**
* Defined if this is the fileTable
*/
isFileTable?: FileTableConfig;
/**
* True if fileTable is enabled and this table references the fileTable
* Used in UI
*/
hasFiles?: boolean;
/**
* True if this is a view.
* Table methods (insert, update, delete) are undefined for views
*/
isView?: boolean;
/**
* Name of the fileTable (if enabled)
* Used in UI
*/
fileTableName?: string;
/**
* Used for getColumns in cases where the columns are dynamic based on the request.
* See dynamicFields from Update rules
*/
dynamicRules?: {
update?: boolean;
};
/**
* Additional table info provided through TableConfig
*/
info?: {
label?: string;
};
/**
* List of unique column indexes/constraints.
* Column groups where at least a column is not allowed to be viewed (selected) are omitted.
*/
uniqueColumnGroups?: string[][];
/**
* Controlled through the publish.table_name.insert config
* If defined then any insert on this table must also contain nested inserts for the specified tables that reference this table
*/
requiredNestedInserts?: RequiredNestedInsert[];
};
type RequiredNestedInsert = {
ftable: string;
minRows?: number;
maxRows?: number;
};
/**
* Error handler that may fire due to schema changes or other post subscribe issues
* Column or filter issues are thrown during the subscribe call
*/
export type SubscribeOnError = (err: any) => void;
type JoinedSelect = Record<string, Select>;
export type SelectFunction = Record<string, any[]>;
type ParseSelect<
Select extends SelectParams<TD>["select"],
TD extends AnyObject,
> = (Select extends { "*": 1 } ? Required<TD> : {}) & {
[Key in keyof Omit<Select, "*"> & string]: Select[Key] extends 1 ? Required<TD>[Key]
: Select[Key] extends SelectFunction ? any
: Select[Key] extends JoinedSelect ? any[]
: any;
};
type SelectDataType<
S extends DBSchema | void,
O extends SelectParams<TD, S>,
TD extends AnyObject,
> =
O extends { returnType: "value" } ? any
: O extends { returnType: "values"; select: Record<string, 1> } ?
ValueOf<Pick<Required<TD>, keyof O["select"]>>
: O extends { returnType: "values" } ? any
: O extends { select: "*" } ? Required<TD>
: O extends { select: "" } ? Record<string, never>
: O extends { select: Record<string, 0> } ? Omit<Required<TD>, keyof O["select"]>
: O extends { select: Record<string, any> } ? ParseSelect<O["select"], Required<TD>>
: Required<TD>;
export type SelectReturnType<
S extends DBSchema | void,
O extends SelectParams<TD, S>,
TD extends AnyObject,
isMulti extends boolean,
> =
O extends { returnType: "statement" } ? string
: isMulti extends true ? SelectDataType<S, O, TD>[]
: SelectDataType<S, O, TD>;
type GetReturningReturnType<
O extends UpdateParams<TD, S>,
TD extends AnyObject,
S extends DBSchema | void = void,
> =
O extends { returning: "*" } ? Required<TD>
: O extends { returning: "" } ? Record<string, never>
: O extends { returning: Record<string, 1> } ? Pick<Required<TD>, keyof O["returning"]>
: O extends { returning: Record<string, 0> } ? Omit<Required<TD>, keyof O["returning"]>
: void;
/**
*
* Nothing is returned by default.
* `returning` must be specified to return the updated records.
*/
export type UpdateReturnType<
O extends UpdateParams<TD, S>,
TD extends AnyObject,
S extends DBSchema | void = void,
> =
O extends { multi: false } ? GetReturningReturnType<O, TD, S>
: GetReturningReturnType<O, TD, S>[];
/**
* Nothing is returned by default.
* `returning` must be specified to return the updated records.
* If an array of records is inserted then an array of records will be returned
* otherwise a single record will be returned.
*/
export type InsertReturnType<
Data extends InsertData<AnyObject>,
O extends UpdateParams<TD, S>,
TD extends AnyObject,
S extends DBSchema | void = void,
> =
Data extends any[] | readonly any[] ? GetReturningReturnType<O, TD, S>[]
: GetReturningReturnType<O, TD, S>;
export type SubscriptionHandler = {
unsubscribe: () => Promise<void>;
filter: FullFilter<void, void> | {};
};
/**
* Dynamic/filter based rules (dynamicFields) allow specifying which columns can be updated based on the target record.
* Useful when the same user can update different fields based on the record state.
*/
type GetColumnsParams = {
/**
* Only "update" is supported at the moment
*/
rule: "update";
/**
* Filter specifying which records are to be updated
*/
filter: FullFilter<void, void>;
};
type GetColumns = (
/**
* Language code for i18n data. "en" by default
*/
lang?: string,
params?: GetColumnsParams
) => Promise<ValidatedColumnInfo[]>;
/**
* Callback fired once after subscribing and then every time the data matching the filter changes
*/
type SubscribeCallback<ItemsDataType> = (items: ItemsDataType) => void | Promise<void>;
/**
* Callback fired once after subscribing and then every time the data matching the filter changes
*/
type SubscribeOneCallback<ItemDataType> = (item: ItemDataType) => void | Promise<void>;
/**
* Methods for interacting with a view
* - On client-side some methods are restricted (and undefined) based on publish rules on the server
*/
export type ViewHandler<TD extends AnyObject = AnyObject, S extends DBSchema | void = void> = {
/**
* Retrieves the table/view info
*/
getInfo: (
/**
* Language code for i18n data. "en" by default
*/
lang?: string
) => Promise<TableInfo>;
/**
* Retrieves columns metadata of the table/view
*/
getColumns: GetColumns;
/**
* Retrieves a list of matching records from the view/table
*/
find: <P extends SelectParams<TD, S>>(
/**
* Filter to apply. Undefined will return all records
* - { "field": "value" }
* - { "field": { $in: ["value", "value2"] } }
* - { $or: [
* { "field1": "value" },
* { "field2": "value" }
* ]
* }
* - { $existsJoined: { linkedTable: { "linkedTableField": "value" } } }
*/
filter?: FullFilter<TD, S>,
selectParams?: P
) => Promise<SelectReturnType<S, P, TD, true>>;
/**
* Retrieves a record from the view/table
*/
findOne: <P extends SelectParams<TD, S>>(
filter?: FullFilter<TD, S>,
selectParams?: P
) => Promise<undefined | SelectReturnType<S, P, TD, false>>;
/**
* Retrieves a list of matching records from the view/table and subscribes to changes
*/
subscribe: <P extends SubscribeParams<TD, S>>(
filter: FullFilter<TD, S>,
params: P,
onData: SubscribeCallback<SelectReturnType<S, P, TD, true>>,
onError?: SubscribeOnError
) => Promise<SubscriptionHandler>;
/**
* Retrieves first matching record from the view/table and subscribes to changes
*/
subscribeOne: <P extends SubscribeParams<TD, S>>(
filter: FullFilter<TD, S>,
params: P,
onData: SubscribeOneCallback<SelectReturnType<S, P, TD, false> | undefined>,
onError?: SubscribeOnError
) => Promise<SubscriptionHandler>;
/**
* Returns the number of rows that match the filter
*/
count: <P extends SelectParams<TD, S>>(
filter?: FullFilter<TD, S>,
selectParams?: P
) => Promise<number>;
/**
* Returns result size in bits
*/
size: <P extends SelectParams<TD, S>>(
filter?: FullFilter<TD, S>,
selectParams?: P
) => Promise<string>;
};
type UpsertDataToPGCastLax<T extends AnyObject> = PartialLax<UpsertDataToPGCast<T>>;
export type InsertData<T extends AnyObject> = UpsertDataToPGCast<T> | UpsertDataToPGCast<T>[];
/**
* Methods for interacting with a table
* - On client-side some methods are restricted (and undefined) based on publish rules on the server
*/
export type TableHandler<
TD extends AnyObject = AnyObject,
S extends DBSchema | void = void,
> = ViewHandler<TD, S> & {
/**
* Updates a record in the table based on the specified filter criteria
* - Use { multi: false } to ensure no more than one row is updated
*/
update: <P extends UpdateParams<TD, S>>(
filter: FullFilter<TD, S>,
newData: UpsertDataToPGCastLax<TD>,
params?: P
) => Promise<UpdateReturnType<P, TD, S> | undefined>;
/**
* Updates multiple records in the table in a batch operation.
* - Each item in the `data` array contains a filter and the corresponding data to update.
*/
updateBatch: <P extends UpdateParams<TD, S>>(
data: [FullFilter<TD, S>, UpsertDataToPGCastLax<TD>][],
params?: P
) => Promise<UpdateReturnType<P, TD, S> | void>;
/**
* Inserts a new record into the table.
*/
insert: <P extends InsertParams<TD, S>, D extends InsertData<TD>>(
data: D,
params?: P
) => Promise<InsertReturnType<D, P, TD, S>>;
/**
* Inserts or updates a record in the table.
* - If a record matching the `filter` exists, it updates the record.
* - If no matching record exists, it inserts a new record.
*/
upsert: <P extends UpdateParams<TD, S>>(
filter: FullFilter<TD, S>,
newData: UpsertDataToPGCastLax<TD>,
params?: P
) => Promise<UpdateReturnType<P, TD, S>>;
/**
* Deletes records from the table based on the specified filter criteria.
* - If no filter is provided, all records may be deleted (use with caution).
*/
delete: <P extends DeleteParams<TD, S>>(
filter?: FullFilter<TD, S>,
params?: P
) => Promise<UpdateReturnType<P, TD, S> | undefined>;
};
export type JoinMakerOptions<TT extends AnyObject = AnyObject> = SelectParams<TT> & {
path?: RawJoinPath;
};
export type JoinMaker<TT extends AnyObject = AnyObject, S extends DBSchema | void = void> = (
filter?: FullFilter<TT, S>,
select?: Select<TT>,
options?: JoinMakerOptions<TT>
) => any;
export type JoinMakerBasic = (
filter?: FullFilterBasic,
select?: SelectBasic,
options?: SelectParams & { path?: RawJoinPath }
) => any;
export type TableJoin = {
[key: string]: JoinMaker;
};
export type TableJoinBasic = {
[key: string]: JoinMakerBasic;
};
export type DbJoinMaker = {
innerJoin: TableJoin;
leftJoin: TableJoin;
innerJoinOne: TableJoin;
leftJoinOne: TableJoin;
};
export type SQLResultInfo = {
command:
| "SELECT"
| "UPDATE"
| "DELETE"
| "CREATE"
| "ALTER"
| "LISTEN"
| "UNLISTEN"
| "INSERT"
| undefined
| string;
rowCount: number;
duration: number;
};
export type SQLResult<T extends SQLOptions["returnType"]> = SQLResultInfo & {
rows: (T extends "arrayMode" ? any : AnyObject)[];
fields: {
name: string;
dataType: string;
udt_name: PG_COLUMN_UDT_DATA_TYPE;
tsDataType: TS_COLUMN_DATA_TYPES;
tableID?: number;
tableName?: string;
tableSchema?: string;
columnID?: number;
columnName?: string;
}[];
};
export type DBEventHandles = {
socketChannel: string;
socketUnsubChannel: string;
addListener: (listener: (event: any) => void) => { removeListener: () => void };
};
export type SocketSQLStreamPacket =
| {
type: "data";
fields?: any[];
rows: any[];
ended?: boolean;
info?: SQLResultInfo;
processId: number;
}
| {
type: "error";
error: any;
};
export type SocketSQLStreamServer = {
channel: string;
unsubChannel: string;
};
export type SocketSQLStreamHandlers = {
pid: number;
run: (query: string, params?: any | any[]) => Promise<void>;
stop: (terminate?: boolean) => Promise<void>;
};
export type SocketSQLStreamClient = SocketSQLStreamServer & {
start: (listener: (packet: SocketSQLStreamPacket) => void) => Promise<SocketSQLStreamHandlers>;
};
export type CheckForListen<T, O extends SQLOptions> =
O["allowListen"] extends true ? DBEventHandles | T : T;
export type GetSQLReturnType<O extends SQLOptions> = CheckForListen<
O["returnType"] extends "row" ? AnyObject | null
: O["returnType"] extends "rows" ? AnyObject[]
: O["returnType"] extends "value" ? any | null
: O["returnType"] extends "values" ? any[]
: O["returnType"] extends "statement" ? string
: O["returnType"] extends "noticeSubscription" ? DBEventHandles
: O["returnType"] extends "stream" ? SocketSQLStreamClient
: SQLResult<O["returnType"]>,
O
>;
export type SQLHandler<ServerSideOptions = void> =
/**
*
* @param query <string> query. e.g.: SELECT * FROM users;
* @param params <any[] | object> query arguments to be escaped. e.g.: { name: 'dwadaw' }
* @param options <object> { returnType: "statement" | "rows" | "noticeSubscription" }
*/
<Opts extends SQLOptions>(
query: string,
args?: AnyObject | any[],
options?: Opts,
serverSideOptions?: ServerSideOptions
) => Promise<GetSQLReturnType<Opts>>;
type SelectMethods<T extends DBTableSchema> =
T["select"] extends true ?
keyof Pick<
TableHandler,
| "count"
| "find"
| "findOne"
| "getColumns"
| "getInfo"
| "size"
| "subscribe"
| "subscribeOne"
>
: never;
type UpdateMethods<T extends DBTableSchema> =
T["update"] extends true ? keyof Pick<TableHandler, "update" | "updateBatch"> : never;
type InsertMethods<T extends DBTableSchema> =
T["insert"] extends true ? keyof Pick<TableHandler, "insert"> : never;
type UpsertMethods<T extends DBTableSchema> =
T["insert"] extends true ?
T["update"] extends true ?
keyof Pick<TableHandler, "upsert">
: never
: never;
type DeleteMethods<T extends DBTableSchema> =
T["delete"] extends true ? keyof Pick<TableHandler, "delete"> : never;
export type ValidatedMethods<T extends DBTableSchema> =
| SelectMethods<T>
| UpdateMethods<T>
| InsertMethods<T>
| UpsertMethods<T>
| DeleteMethods<T>;
// | SyncMethods<T>
export type DBHandler<S = void> = (S extends DBSchema ?
{
[k in keyof S]: S[k]["is_view"] extends true ? ViewHandler<S[k]["columns"], S>
: Pick<TableHandler<S[k]["columns"], S>, ValidatedMethods<S[k]>>;
}
: {
[key: string]: Partial<TableHandler>;
}) &
DbJoinMaker & {
sql?: SQLHandler;
};
export type DBNoticeConfig = {
socketChannel: string;
socketUnsubChannel: string;
};
export type DBNotifConfig = DBNoticeConfig & {
notifChannel: string;
};
export type SQLOptions = {
/**
* Return type of the query
*/
returnType?:
| Required<SelectParams>["returnType"]
| "default-with-rollback"
| "statement"
| "rows"
| "noticeSubscription"
| "arrayMode"
| "stream";
/**
* If allowListen not specified and a LISTEN query is issued then expect error
*/
allowListen?: boolean;
/**
* Positive integer that works only with returnType="stream".
* If provided then the query will be cancelled when the specified number of rows have been streamed
*/
streamLimit?: number;
/**
* If true then the connection will be persisted and used for subsequent queries
*/
persistStreamConnection?: boolean;
/**
* connectionId of the stream connection to use
* Acquired from the first query with persistStreamConnection=true
*/
streamConnectionId?: string;
/**
* If false then the query will not be checked for params. Used to ignore queries with param like text (e.g.: ${someText} )
* Defaults to true
*/
hasParams?: boolean;
};
export type SQLRequest = {
query: string;
params?: any | any[];
options?: SQLOptions;
};
export type NotifSubscription = {
socketChannel: string;
socketUnsubChannel: string;
notifChannel: string;
};
export type NoticeSubscription = {
socketChannel: string;
socketUnsubChannel: string;
};
const preffix = "_psqlWS_.";
export const CHANNELS = {
SCHEMA_CHANGED: preffix + "schema-changed",
SCHEMA: preffix + "schema",
DEFAULT: preffix,
SQL: `${preffix}sql`,
SQL_STREAM: `${preffix}sql-stream`,
METHOD: `${preffix}method`,
NOTICE_EV: `${preffix}notice`,
LISTEN_EV: `${preffix}listen`,
/* Auth channels */
REGISTER: `${preffix}register`,
LOGIN: `${preffix}login`,
LOGOUT: `${preffix}logout`,
AUTHGUARD: `${preffix}authguard`,
/**
* Used for sending any connection errors from onSocketConnect
*/
CONNECTION: `${preffix}connection`,
_preffix: preffix,
};
export type SubscriptionChannels = {
/** Used by server to emit data to client */
channelName: string;
/** Used by client to confirm when ready */
channelNameReady: string;
/** Used by client to stop subscription */
channelNameUnsubscribe: string;
};
export type AuthGuardLocation = {
href: string;
origin: string;
protocol: string;
host: string;
hostname: string;
port: string;
pathname: string;
search: string;
hash: string;
};
export type AuthGuardLocationResponse = {
shouldReload: boolean;
};
export const RULE_METHODS = {
getColumns: ["getColumns"],
getInfo: ["getInfo"],
insert: ["insert", "upsert"],
update: ["update", "upsert", "updateBatch"],
select: ["findOne", "find", "count", "size"],
delete: ["delete", "remove"],
sync: ["sync", "unsync"],
subscribe: ["unsubscribe", "subscribe", "subscribeOne"],
} as const;
export type MethodKey = (typeof RULE_METHODS)[keyof typeof RULE_METHODS][number];
export type TableSchemaForClient = Record<
string,
Partial<
Record<MethodKey, MethodKey extends "insert" ? { allowedNestedInserts?: string[] } : AnyObject>
>
>;
/* Schema */
export type TableSchema = {
schema: string;
name: string;
oid: number;
comment: string;
columns: (ColumnInfo & {
privileges: Partial<Record<"INSERT" | "REFERENCES" | "SELECT" | "UPDATE", true>>;
})[];
is_view: boolean;
parent_tables: string[];
privileges: {
insert: boolean;
select: boolean;
update: boolean;
delete: boolean;
};
};
export type MethodFunction = (...args: any) => any | Promise<any>;
export type MethodFullDef = {
input: Record<string, JSONB.JSONBSchema>;
run: MethodFunction;
output?: Record<string, JSONB.JSONBSchema>;
} & (
| {
output?: undefined;
outputTable?: string;
}
| {
output?: Record<string, JSONB.JSONBSchema>;
outputTable?: undefined;
}
);
export type Method = MethodFunction | MethodFullDef;
export type MethodHandler = {
[method_name: string]: Method;
};
export type TableSchemaErrors = {
[tableName: string]: {
[method: string]: {
error: any;
};
};
};
export type ClientSchema = {
rawSQL: boolean;
joinTables: string[][];
auth: AuthTypes.AuthSocketSchema | undefined;
version: any;
err?: string;
tableSchemaErrors: TableSchemaErrors;
tableSchema: DBSchemaTable[];
schema: TableSchemaForClient;
methods: (
| string
| ({ name: string; description?: string } & Pick<MethodFullDef, "input" | "output">)
)[];
};
export type ProstglesError = {
message: string;
column?: string;
code?: string;
table?: string;
constraint?: string;
txt?: string;
code_info?: string;
detail?: string;
columns?: string[];
};
type ColumnInfoForNestedInsert = Pick<ColumnInfo, "name" | "references" | "is_pkey">;
export const getPossibleNestedInsert = (
column: ColumnInfoForNestedInsert,
schema: { name: string; columns: ColumnInfoForNestedInsert[] }[],
silent = true
) => {
const refs = column.references ?? [];
const colRefs = refs
.map((ref) => {
const { ftable, fcols } = ref;
const ftableInfo = schema.find((s) => s.name === ftable);
if (!ftableInfo) return;
const fcolsInfo = ftableInfo.columns.filter((c) => fcols.includes(c.name));
if (!fcolsInfo.length) return;
return {
ref,
fcolsInfo,
};
})
.filter(isDefined);
const [firstColRef, ...otherSingleColRefs] = colRefs ?? [];
if (!otherSingleColRefs.length) {
return firstColRef?.ref;
}
const [pkeyRef, ...otherPkeyRefs] = colRefs.filter((r) =>
r.fcolsInfo.some((fcolInfo) => fcolInfo.is_pkey)
);
if (!otherPkeyRefs.length) return pkeyRef?.ref;
if (silent) return;
throw [
"Cannot do a nested insert on column that references multiple tables.",
"Expecting only one reference to a single primary key fcol",
].join("\n");
};
/**
* Type tests
*/
() => {
type Fields = { id: number; name: number; public: number; $rowhash: string; added_day: any };
const r: Fields = 1 as any;
const sel1: Select = { id: 1, name: 1, public: 1, $rowhash: 1, added_day: { $day: [] } };
const sel2: Select<{ id: number; name: number; public: number }> = {
id: 1,
name: 1,
public: 1,
$rowhash: 1,
dsds: { d: [] },
};
const sel3: Select<{ id: number; name: number; public: number }> = "";
const sel4: Select<{ id: number; name: number; public: number }> = "*";
const sel12: Select = { id: 1, name: 1, public: 1, $rowhash: 1, dsds: { d: [] } };
const sel13: Select = "";
const sel14: Select = "*";
const fRow: FullFilter<Fields, {}> = {
$rowhash: { $in: [""] },
};
const emptyFilter: FullFilter<Fields, {}> = {};
const sel32: Select = {
dwa: 1,
};
const sel = {
a: 1,
$rowhash: 1,
dwadwA: { dwdwa: [5] },
} as const;
const sds: Select = sel;
const sds01: Select = "";
const sds02: Select = "*";
const sds03: Select = {};
const sds2: Select<{ a: number }> = sel;
const s001: Select = {
h: { $ts_headline_simple: ["name", { plainto_tsquery: "abc81" }] },
hh: { $ts_headline: ["name", "abc81"] },
added: "$date_trunc_2hour",
addedY: { $date_trunc_5minute: ["added"] },
};
//@ts-expect-error
const badSel: Select = {
a: 1,
b: 0,
};
//@ts-expect-error
const badSel1: Select<{ a: number }, {}> = {
b: 1,
a: 1,
};
const sds3: Select<{ a: number }> = {
// "*": 1,
// a: "$funcName",
a: { dwda: [] },
$rowhashD: { dwda: [] },
// dwadwa: 1, //{ dwa: []}
};
const sel1d: Select = {
dwada: 1,
$rowhash: 1,
dwawd: { funcName: [12] },
};
const sel1d2: Select<AnyObject> = ["a"];
const deletePar: DeleteParams = {
returning: { id: 1, name: 1, public: 1, $rowhash: 1, added_day: { $day: ["added"] } },
};
};
/** More Type tests */
async () => {
type GSchema = {
tbl1: {
is_view: false;
columns: {
col1: string;
col2: string;
};
delete: true;
select: true;
insert: true;
update: true;
};
};
type TableDef = { h: number; b?: number; c?: number };
const tableHandler: TableHandler<TableDef> = undefined as any;
tableHandler.insert({ h: 1, c: 2, "b.$func": { dwa: [] } });
type DBOFullyTyped<Schema = void> =
Schema extends DBSchema ?
{
[tov_name in keyof Schema]: Schema[tov_name]["is_view"] extends true ?
ViewHandler<Schema[tov_name]["columns"], Schema>
: TableHandler<Schema[tov_name]["columns"], Schema>;
}
: Record<string, ViewHandler | TableHandler>;
type TypedFFilter = FullFilter<GSchema["tbl1"]["columns"], GSchema>;
const schemaFFilter: TypedFFilter = { "col1.$eq": "dd" };
const fullFilter: FullFilter<void, void> = schemaFFilter;
const ffFunc = (f: FullFilter<void, void>) => {};
ffFunc(schemaFFilter);
const dbo: DBOFullyTyped<GSchema> = 1 as any;
const funcData = { funcName: [] };
const noRow = await dbo.tbl1.update({}, { col1: "" });
//@ts-expect-error
noRow.length;
//@ts-expect-error
noRow.col1;
const someData = await dbo.tbl1.find({}, { select: { col1: 1 }, orderBy: { col1: -1 } });
const noRowFunc = await dbo.tbl1.update({}, { col1: "" });
const oneRow = await dbo.tbl1.update({}, { col1: "" }, { returning: "*", multi: false });
//@ts-expect-error
oneRow?.length;
//@ts-expect-error
oneRow.col1;
oneRow?.col1;
const manyRows = await dbo.tbl1.update({}, { col1: "" }, { returning: "*" });
//@ts-expect-error
manyRows?.col1;
manyRows?.at(0)?.col1;
const noIRow = await dbo.tbl1.insert({ col1: "", col2: { $func: [] } });
//@ts-expect-error
noIRow.length;
//@ts-expect-error
noIRow.col1;
const irow = await dbo.tbl1.insert({ col1: "", col2: funcData }, { returning: "*" });
//@ts-expect-error
irow.length;
irow.col1;
const irowFunc = await dbo.tbl1.insert({ col1: funcData, col2: "" }, { returning: "*" });
const irows = await dbo.tbl1.insert([{ col1: "", col2: "" }], { returning: "*" });
//@ts-expect-error
irows.col1;
irows.length;
const filter: FullFilter<GSchema["tbl1"]["columns"], GSchema> = {};
const filterCheck = <F extends FullFilter<void, void> | undefined>(f: F) => {};
filterCheck(filter);
const t: UpsertDataToPGCast<GSchema["tbl1"]["columns"]> = {} as any;
const d: UpsertDataToPGCast<AnyObject> = t;
const fup = (a: UpsertDataToPGCast<AnyObject>) => {};
fup(t);
// const f = <A extends TableHandler["count"]>(a: A) => {};
const f = (s: TableHandler) => {};
const th: TableHandler<GSchema["tbl1"]["columns"], GSchema> = {} as any;
// f(th)
const sp: SelectParams<GSchema["tbl1"]["columns"]> = { select: {} };
const sf = (sp: SelectParams) => {};
sf(sp);
// const sub: TableHandler["count"] = dbo.tbl1.count
/**
* Upsert data funcs
*/
const gdw: InsertData<{ a: number; z: number }> = {
a: { dwa: [] },
z: { dwa: [] },
};
const gdwn: InsertData<{ a: number; z: number }> = {
a: 2,
z: { dwa: [] },
};
const gdw1: InsertData<{ a: number; z: number }> = { a: 1, z: 2 };
const gdw1Opt: InsertData<{ a: number; z?: number }> = { a: {}, z: 2 };
const gdw2: InsertData<{ a: number; z: number }> = { a: { dwa: [] }, z: { dwa: [] } };
//@ts-expect-error
const missingKey: InsertData<{ a: number; z: number }> = { z: 1, z: { dwa: [] } };
//@ts-expect-error
const missingKey2: InsertData<{ a: number; z: number }> = { z: 1 };
// ra(schema);
};
// import { md5 } from "./md5";
// export { get, getTextPatch, unpatchText, isEmpty, WAL, WALConfig, asName } from "./util";
// export type { WALItem, BasicOrderBy, WALItemsObj, WALConfig, TextPatch, SyncTableInfo } from "./util";
export { CONTENT_TYPE_TO_EXT } from "./files";
export type { ALLOWED_CONTENT_TYPE, ALLOWED_EXTENSION, FileColumnConfig, FileType } from "./files";
export * from "./filters";
export * from "./JSONBSchemaValidation/JSONBSchema";
export type {
ClientExpressData,
ClientSyncHandles,
ClientSyncInfo,
ClientSyncPullResponse,
SyncBatchParams,
SyncConfig,
onUpdatesParams,
} from "./replication";
export * from "./util";
export * from "./auth";
export * from "./JSONBSchemaValidation/JSONBSchemaValidation";
export * from "./JSONBSchemaValidation/getJSONBSchemaTSTypes";