import type { DBOFullyTyped, PublishFullyTyped } from "../DBSchemaBuilder/DBSchemaBuilder"; import type { Filter, LocalParams, TableOrViewInfo } from "../DboBuilder/DboBuilder"; import type { DB, DBHandlerServer } from "../Prostgles"; export type Awaitable = T | Promise; export type DboTable = { tableName: string; clientReq: AuthClientRequest | undefined; }; export type DboTableCommand = DboTable & { command: string; }; import type pgPromise from "pg-promise"; import type { AnyObject, DBSchema, FieldFilter, FullFilter, RequiredNestedInsert, SelectParams, SQLHandler, TableSchema, } from "prostgles-types"; import type { AuthClientRequest, LoginClientInfo, SessionUser } from "../Auth/AuthTypes"; import type { TableSchemaColumn } from "../DboBuilder/DboBuilderTypes"; import type { ClientHandlers } from "../WebsocketAPI/getClientHandlers"; export type InsertRequestData = { data: object | object[]; returning: FieldFilter; }; export type SelectRequestData = { filter: object; params: SelectParams; }; export type DeleteRequestData = { filter: object; returning: FieldFilter; }; export type UpdateRequestDataOne = { filter: FullFilter; data: Partial; returning: FieldFilter; }; export type UpdateReq = { filter: FullFilter; data: Partial; }; export type UpdateRequestDataBatch = { data: UpdateReq[]; }; export type UpdateRequestData = | UpdateRequestDataOne | UpdateRequestDataBatch; export type ValidateRowArgsCommon = { row: R; dbx: DBX; tx: pgPromise.ITask<{}> | DB; } & ( | { command: "insert"; data: R; } | { command: "update"; data: Partial; } ); export type ValidateRowsArgsCommon = { rows: R[]; dbx: DBX; tx: pgPromise.ITask<{}> | DB; } & ( | { command: "insert"; data: R[]; } | { command: "update"; data: Partial[]; } ); export type ValidateRowArgs = ValidateRowArgsCommon< R, DBX > & { localParams: LocalParams; }; export type ValidateUpdateRowArgs, F = Filter, DBX = DBHandlerServer> = { update: U; filter: F; dbx: DBX; localParams: LocalParams; }; export type ValidateRow = ( args: ValidateRowArgs>, ) => R | Promise; export type PostValidateRow = ( args: ValidateRowArgs>, ) => void | Promise; export type PostValidateRowBasic = (args: ValidateRowArgs) => void | Promise; export type ValidateRowBasic = (args: ValidateRowArgs) => AnyObject | Promise; export type ValidateUpdateRow = ( args: ValidateUpdateRowArgs, FullFilter, DBOFullyTyped>, ) => Partial | Promise>; export type ValidateUpdateRowBasic = ( args: ValidateUpdateRowArgs, ) => AnyObject | Promise; export type SelectRule = { /** * Fields allowed to be selected. * Tip: Use false to exclude field */ fields: FieldFilter; /** * Fields allowed to sorted * Defaults to the "fields". Use empty array/object to disallow sorting */ orderByFields?: FieldFilter; /** * The maximum number of rows a user can get in a select query. null by default. Unless a null or higher limit is specified 100 rows will be returned by the default */ maxLimit?: number | null; /** * Filter added to every query (e.g. user_id) to restrict access */ forcedFilter?: FullFilter; /** * Fields user can filter by. If undefined will use the fields (allowed to be selected) * */ filterFields?: FieldFilter; /** * Validation logic to check/update data for each request */ validate?(args: SelectRequestData): SelectRequestData | Promise; subscribeThrottle?: number; disableMethods?: Partial>; }; export type CommonInsertUpdateRule< Cols extends AnyObject = AnyObject, S extends DBSchema | void = void, > = { /** * Filter that the new records must match or the update/insert will fail * Similar to a policy WITH CHECK clause */ checkFilter?: SelectRule["forcedFilter"]; /** * Data to include and overwrite on each update/insert * These fields cannot be updated by the user */ forcedData?: Partial; }; export type InsertRule< Cols extends AnyObject = AnyObject, S extends DBSchema | void = void, > = CommonInsertUpdateRule & { /** * Fields allowed to be inserted. Tip: Use false to exclude field */ fields: SelectRule["fields"]; /** * Fields user can view after inserting */ returningFields?: SelectRule["fields"]; /** * Validation logic to check/update data for each request. Happens before publish rule checks (for fields, forcedData/forcedFilter) */ preValidate?: S extends DBSchema ? ValidateRow : ValidateRowBasic; /** * Validation logic to check/update data for each request. Happens after publish rule checks (for fields, forcedData/forcedFilter) */ validate?: S extends DBSchema ? ValidateRow : ValidateRowBasic; /** * Validation logic to check/update data after the insert. * Happens in the same transaction so upon throwing an error the record will be deleted (not committed) */ postValidate?: S extends DBSchema ? PostValidateRow, S> : PostValidateRowBasic; /** * If defined then only nested inserts from these tables are allowed * Direct inserts will fail */ allowedNestedInserts?: { table: string; column: string; }[]; requiredNestedInserts?: RequiredNestedInsert[]; }; export type UpdateRule< Cols extends AnyObject = AnyObject, S extends DBSchema | void = void, > = CommonInsertUpdateRule & { /** * Fields allowed to be updated. Tip: Use false/0 to exclude field */ fields: SelectRule["fields"]; /** * Row level FGAC * Used when the editable fields change based on the updated row * If specified then the fields from the first matching filter table.count({ ...filter, ...updateFilter }) > 0 will be used * If none matching then the "fields" will be used * Specify in decreasing order of specificity otherwise a more general filter will match first */ dynamicFields?: { filter: FullFilter; fields: SelectRule["fields"]; }[]; /** * Filter added to every query (e.g. user_id) to restrict access * This filter cannot be updated */ forcedFilter?: SelectRule["forcedFilter"]; /** * Fields user can use to find the updates */ filterFields?: SelectRule["fields"]; /** * Fields user can view after updating */ returningFields?: SelectRule["fields"]; /** * Validation logic to check/update data for each request */ validate?: S extends DBSchema ? ValidateUpdateRow : ValidateUpdateRowBasic; /** * Validation logic to check/update data after the insert. * Happens in the same transaction so upon throwing an error the record will be deleted (not committed) */ postValidate?: S extends DBSchema ? PostValidateRow, S> : PostValidateRowBasic; disableMethods?: Partial>; }; export type DeleteRule = { /** * Filter added to every query (e.g. user_id) to restrict access */ forcedFilter?: SelectRule["forcedFilter"]; /** * Fields user can filter by */ filterFields: FieldFilter; /** * Fields user can view after deleting */ returningFields?: SelectRule["filterFields"]; /** * Validation logic to check/update data for each request */ validate?(filter: FullFilter): Awaitable; }; export type SyncConfig = { /** * Primary keys used in updating data */ id_fields: (keyof Cols)[]; /** * Numerical incrementing fieldname (last updated timestamp) used to sync items */ synced_field: keyof Cols; /** * EXPERIMENTAL. Disabled by default. If true then server will attempt to delete any records missing from client. */ // allow_delete?: boolean; /** * Throttle replication transmission in milliseconds. Defaults to 100 */ throttle?: number; /** * Number of rows to send per trip. Defaults to 50 */ batch_size?: number; }; /** * Required but possibly undefined type * */ export type Required_ish = { [K in keyof Required]: T[K]; }; export type WithRequired = T & { [P in K]-?: NonNullable }; export type TableRule = { select?: SelectRule; insert?: InsertRule; update?: UpdateRule; delete?: DeleteRule; sync?: SyncConfig; }; export type ParsedViewRule = { /** * What can be read from the table */ select?: WithRequired, "filterFields" | "orderByFields">; }; export type ParsedTableRule< RowType extends AnyObject = AnyObject, S extends DBSchema | void = void, > = ParsedViewRule & { insert?: WithRequired, "returningFields">; update?: WithRequired, "filterFields" | "returningFields">; delete?: WithRequired, "returningFields">; }; export const parsePublishTableRule = (tableRules: R | undefined) => { const selectRules: ParsedTableRule["select"] | undefined = tableRules?.select && { ...tableRules.select, /** * Unless specified. Filtering should be allowed on fields the user can select */ filterFields: tableRules.select.filterFields ?? tableRules.select.fields, orderByFields: tableRules.select.orderByFields ?? tableRules.select.fields, }; const parsedTableRules: ParsedTableRule | undefined = tableRules && { ...tableRules, select: selectRules, insert: tableRules.insert && { ...tableRules.insert, returningFields: tableRules.insert.returningFields ?? selectRules?.fields ?? tableRules.insert.fields, }, update: tableRules.update && { ...tableRules.update, filterFields: tableRules.update.filterFields ?? selectRules?.filterFields ?? [], returningFields: tableRules.update.returningFields ?? selectRules?.fields ?? tableRules.update.fields, }, delete: tableRules.delete && { ...tableRules.delete, returningFields: tableRules.delete.returningFields ?? selectRules?.fields ?? tableRules.delete.filterFields, }, }; return parsedTableRules; }; export type PublishTableRule< Col extends AnyObject = AnyObject, S extends DBSchema | void = void, > = { select?: SelectRule | PublishAllOrNothing; insert?: InsertRule | PublishAllOrNothing; update?: UpdateRule | PublishAllOrNothing; delete?: DeleteRule | PublishAllOrNothing; }; export const TABLE_RULE_NO_LIMITS = { select: { fields: "*", disableMethods: undefined, subscribeThrottle: 0, }, insert: { fields: "*", }, update: { fields: "*", filterFields: "*", }, delete: { filterFields: "*", }, } as const satisfies PublishTableRule; export type ParsedPublishTable = { select?: SelectRule; insert?: InsertRule; update?: UpdateRule; delete?: DeleteRule; }; export type DbTableInfo = { name: string; info: TableOrViewInfo; columns: TableSchemaColumn[]; }; export type PermissionScope = { allowSql?: boolean; tables?: Record< string, Partial<{ select: | true | { fields?: FieldFilter; forcedFilter?: AnyObject | undefined; }; insert: | true | { fields?: FieldFilter; }; update: | true | { fields?: FieldFilter; forcedFilter?: AnyObject | undefined; }; delete: | true | { forcedFilter?: AnyObject | undefined; }; }> >; methods?: Record; }; export type PublishParams = { sid: string | undefined; dbo: DBOFullyTyped; db: DB; sql: SQLHandler; user?: SUser["user"]; clientReq: AuthClientRequest; clientInfo: LoginClientInfo; tables: TableSchema[]; getClientDBHandlers: ( /** * Used to filter permissions */ scope: PermissionScope | undefined, ) => Promise>; }; export type RequestParams = { dbo?: DBHandlerServer; socket?: any }; export type PublishAllOrNothing = boolean | "*" | null; export type PublishObject = Record; export type ParsedPublishTables = { [table_name: string]: ParsedPublishTable; }; type PublishAllOrNothingRoot = Exclude; export type PublishedResult = PublishAllOrNothingRoot | PublishFullyTyped; export type Publish = | PublishedResult | ((params: PublishParams) => Awaitable>);