diff --git a/src/new/index.ts b/src/client.ts similarity index 66% rename from src/new/index.ts rename to src/client.ts index a1830f7..9db7ee8 100644 --- a/src/new/index.ts +++ b/src/client.ts @@ -7,20 +7,24 @@ import PocketBase, { SendOptions } from 'pocketbase'; import { + BaseRecord, GenericCollection, GenericSchema, MaybeArray, RecordWithExpandToDotPath -} from '../types.js'; +} from './types.js'; import { ResolveSelectWithExpand, SelectWithExpand, resolveSelect -} from '../queryParams.js'; -import { Sort } from '../sort.js'; -import { Filter, FilterParam } from '../filter.js'; +} from './select.js'; +import { Sort } from './sort.js'; +import { Filter } from './filter.js'; -export interface ViewCollectionService { +export interface ViewCollectionService< + Collection extends GenericCollection, + ExpandedRecord extends BaseRecord = RecordWithExpandToDotPath +> { collectionName: Collection['collectionName']; client: PocketBase; @@ -29,8 +33,8 @@ export interface ViewCollectionService { select?: TSelect; page?: number; perPage?: number; - sort?: MaybeArray>; - filter?: FilterParam>; + sort?: MaybeArray>; + filter?: Filter; } & SendOptions ): Promise[]>; getList>( @@ -38,15 +42,15 @@ export interface ViewCollectionService { perPage?: number, options?: { select?: TSelect; - sort?: MaybeArray>; - filter?: FilterParam>; + sort?: MaybeArray>; + filter?: Filter; } & SendOptions ): Promise>>; getFirstListItem>( - filter: FilterParam>, + filter: Filter, options?: { select?: TSelect; - sort?: MaybeArray>; + sort?: MaybeArray>; } & SendOptions ): Promise>; getOne>( @@ -56,9 +60,9 @@ export interface ViewCollectionService { } & SendOptions ): Promise>; - createFilter(filter: Filter): Filter; + createFilter(filter: Filter): Filter; - createSort(...sort: Sort[]): Sort[]; + createSort(...sort: Sort[]): Sort; createSelect>(select: T): T; } @@ -90,7 +94,9 @@ export interface AuthCollectionService options?: { select?: TSelect; } & SendOptions - ): Promise>; + ): Promise< + RecordAuthResponse> + >; authWithOAuth2Code>( provider: string, code: string, @@ -102,7 +108,9 @@ export interface AuthCollectionService options?: { select?: TSelect; } & SendOptions - ): Promise>; + ): Promise< + RecordAuthResponse> + >; authWithOAuth2( options: Omit & { createData?: Collection['create']; @@ -112,7 +120,9 @@ export interface AuthCollectionService options?: { select?: TSelect; } & SendOptions - ): Promise>; + ): Promise< + RecordAuthResponse> + >; } const FORWARD_METHODS = [ @@ -127,8 +137,10 @@ const FORWARD_METHODS = [ 'unlinkExternalAuth' ] as const; -export class TypedRecordService { - constructor(readonly service: RecordService) { +export class TypedRecordService + implements BaseCollectionService +{ + constructor(readonly service: RecordService) { for (const name of FORWARD_METHODS) { // @ts-ignore this[name] = this.service[name].bind(this.service); @@ -160,35 +172,23 @@ export class TypedRecordService { return opts; } - filter(filter: string) { - return filter; + createFilter(filter: string) { + return filter ? filter : ''; } - sort(sort: string | string[]) { - return sort; + + createSort(...sorters: any[]): any { + return sorters.filter((x) => typeof x === 'string').join(','); } - select(select: any) { + + createSelect(select: any) { return select; } - getFullList(options: { - select?: any; - page?: number; - perPage?: number; - sort?: string | string[]; - filter?: string; - }) { - return this.service.getFullList(this.prepareOptions(options)); + getFullList(options?: SendOptions) { + return this.service.getFullList(this.prepareOptions(options)); } - getList( - page?: number, - perPage?: number, - options?: { - select?: any; - sort?: string | string[]; - filter?: string; - } - ) { + getList(page?: number, perPage?: number, options?: SendOptions) { return this.service.getList( page, perPage, @@ -196,35 +196,66 @@ export class TypedRecordService { ); } - getFirstListItem(id: string) { - return this.service.getFirstListItem(id); + getFirstListItem(filter: string, options?: SendOptions) { + return this.service.getFirstListItem( + filter, + this.prepareOptions(options) + ); } - getOne(id: string): Promise { - return this.service.getOne(id); + getOne( + id: string, + options?: { + select?: any; + } & SendOptions + ): Promise { + return this.service.getOne(id, this.prepareOptions(options)); } - create() { - this.service.create({}); + create( + bodyParams?: + | { + [key: string]: any; + } + | FormData, + options?: { + select?: any; + } & SendOptions + ) { + return this.service.create(bodyParams, this.prepareOptions(options)); } - update(id: string) { - this.service.update(id, {}); + update( + id: string, + bodyParams?: + | FormData + | { + [key: string]: any; + }, + options?: { + select?: any; + } & SendOptions + ) { + return this.service.update( + id, + bodyParams, + this.prepareOptions(options) + ); } - delete(id: string) { - this.service.delete(id); + delete(id: string, options?: SendOptions) { + return this.service.delete(id, this.prepareOptions(options)); } authWithPassword( usernameOrEmail: string, password: string, options?: RecordOptions | undefined - ): Promise { + ) { return this.service.authWithPassword( usernameOrEmail, password, - options + this.prepareOptions(options) ); } @@ -235,14 +266,14 @@ export class TypedRecordService { redirectUrl: string, createData?: { [key: string]: any } | undefined, options?: RecordOptions | undefined - ): Promise { + ) { return this.service.authWithOAuth2Code( provider, code, codeVerifier, redirectUrl, createData, - options + this.prepareOptions(options) ); } @@ -250,10 +281,8 @@ export class TypedRecordService { return this.service.authWithOAuth2(options); } - authRefresh( - options?: RecordOptions | undefined - ): Promise { - return this.authRefresh(options); + authRefresh(options?: RecordOptions | undefined) { + return this.service.authRefresh(this.prepareOptions(options)); } } diff --git a/src/codegen/index.ts b/src/codegen/index.ts index abdc1c4..5fe236d 100644 --- a/src/codegen/index.ts +++ b/src/codegen/index.ts @@ -162,21 +162,39 @@ ${tables // ===== ${t.name} ===== export interface ${t.typeName}Response extends ${baseRecord} { - collectionName: '${t.name}';${!t.columns.response.length ? '' : ` - ${t.columns.response.join('\n' + indent)}`} + collectionName: '${t.name}';${ + !t.columns.response.length + ? '' + : ` + ${t.columns.response.join('\n' + indent)}` + } } ${ // view collections are readonly t.type === 'view' ? '' : ` -export interface ${t.typeName}Create extends ${t.type === "base" ? "BaseCollectionRecordCreate" : "AuthCollectionRecordCreate"} ${!t.columns.create.length ? '{}' : `{ +export interface ${t.typeName}Create extends ${ + t.type === 'base' + ? 'BaseCollectionRecordCreate' + : 'AuthCollectionRecordCreate' + } ${ + !t.columns.create.length + ? '{}' + : `{ ${t.columns.create.join('\n' + indent)} -}`} +}` + } -export interface ${t.typeName}Update${t.type === "base" ? "" : " extends AuthCollectionRecordUpdate"} ${!t.columns.update.length ? '{}' : `{ +export interface ${t.typeName}Update${ + t.type === 'base' ? '' : ' extends AuthCollectionRecordUpdate' + } ${ + !t.columns.update.length + ? '{}' + : `{ ${t.columns.update.join('\n' + indent)} -}`} +}` + } ` } export interface ${t.typeName}Collection { @@ -264,12 +282,11 @@ function getFieldType(field: Field, { response, create, update }: Columns) { } case 'select': { const single = field.options.maxSelect === 1; - const values = !field.required && single - ? ["", ...field.options.values] - : field.options.values; - const singleType = values - .map((v) => `'${v}'`) - .join(' | '); + const values = + !field.required && single + ? ['', ...field.options.values] + : field.options.values; + const singleType = values.map((v) => `'${v}'`).join(' | '); const type = single ? `${singleType}` : `MaybeArray<${singleType}>`; addResponse(single ? singleType : `Array<${singleType}>`); @@ -285,7 +302,7 @@ function getFieldType(field: Field, { response, create, update }: Columns) { case 'relation': { const singleType = 'string'; const single = field.options.maxSelect === 1; - const type = single ? `${singleType}` : `MaybeArray<${singleType}>`; + const type = single ? singleType : `MaybeArray<${singleType}>`; addResponse(single ? singleType : `Array<${singleType}>`); addCreate(type); @@ -297,15 +314,13 @@ function getFieldType(field: Field, { response, create, update }: Columns) { break; } case 'file': { - const singleType = 'string'; const single = field.options.maxSelect === 1; - const type = single ? `${singleType}` : `MaybeArray<${singleType}>`; - addResponse(single ? singleType : `Array<${singleType}>`); - addCreate(type); - addUpdate(type); + addResponse(single ? 'string' : `Array`); + addCreate(single ? `File` : `MaybeArray`); + addUpdate(single ? `File` : `MaybeArray`); if (!single) { - addUpdate(type, `'${field.name}-'`); + addUpdate('string', `'${field.name}-'`); } break; } diff --git a/src/filter.ts b/src/filter.ts index 09beed5..22e6736 100644 --- a/src/filter.ts +++ b/src/filter.ts @@ -1,8 +1,4 @@ -import { - BaseRecord, - GenericCollection, - RecordWithExpandToDotPath -} from './types.js'; +import type { BaseRecord } from './types.js'; type ActualFilter = [ K, @@ -30,11 +26,7 @@ export type FilterOperand = export type FilterParam = { __record__?: T } & string; -export type Filter = FilterParam< - RecordWithExpandToDotPath ->; - -export type FilterInput = +export type Filter = | ActualFilter | FilterParam | false @@ -58,7 +50,7 @@ function serializeFilter([key, op, val]: ActualFilter) { return `${String(key)}${op}${val}`; } -export function serializeFilters(filters: FilterInput[]) { +export function serializeFilters(filters: Filter[]) { return filters .filter(Boolean) .map((filter) => @@ -67,7 +59,7 @@ export function serializeFilters(filters: FilterInput[]) { } export function and( - ...filters: FilterInput[] + ...filters: Filter[] ): FilterParam { const str = serializeFilters(filters).join(' && '); if (!str.length) return ''; @@ -75,7 +67,7 @@ export function and( } export function or( - ...filters: FilterInput[] + ...filters: Filter[] ): FilterParam { const str = serializeFilters(filters).join(' || '); if (!str.length) return ''; diff --git a/src/index.ts b/src/index.ts index 64cf288..3ee968a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,21 +1,9 @@ -import PocketBase, { RecordModel, RecordService } from 'pocketbase'; -import { GenericSchema } from './types.js'; -import { TypedRecordService } from './record-service.js'; - -export { - createOptions, +export type { ResolveSelect, ResolveSelectWithExpand, Select, - SelectWithExpand, - TypedBaseQueryParams, - TypedFullListQueryParams, - TypedListQueryParams, - TypedRecordFullListQueryParams, - TypedRecordListQueryParams, - TypedRecordQueryParams -} from './queryParams.js'; - + SelectWithExpand +} from './select.js'; export { and, or, @@ -27,15 +15,14 @@ export { lte, neq, nlike, - Filter + type Filter } from './filter.js'; -export { asc, desc, sort, Sort } from './sort.js'; -export { GenericSchema, GenericCollection, TypedRecord } from './types.js'; - -export interface TypedPocketBase - extends PocketBase { - collection( - idOrName: C - ): TypedRecordService; - collection(idOrName: string): RecordService; -} +export type { Sort } from './sort.js'; +export type { GenericSchema, GenericCollection, TypedRecord } from './types.js'; +export { + type AuthCollectionService, + type BaseCollectionService, + type ViewCollectionService, + TypedPocketBase, + TypedRecordService +} from './client.js'; diff --git a/src/new/test.ts b/src/new/test.ts new file mode 100644 index 0000000..09312f3 --- /dev/null +++ b/src/new/test.ts @@ -0,0 +1,34 @@ +import type { Schema } from '../../example/Database.js'; +import { eq, or } from '../filter.js'; +import { TypedPocketBase } from '../client.js'; + +const db = new TypedPocketBase('http://localhost:8090'); +await db.admins.authWithPassword('admin@example.com', 'secretpassword'); + +const sort = db.from('posts').createSort('+id'); + +const filter = db + .from('posts') + .createFilter(or(eq('content', 'bla'), eq('published', true))); + +const select = db.from('posts').createSelect({ + id: true, + content: true, + owner: true, + collectionName: true, + asd: true, + expand: { + owner: { + username: true, + email: true + } + } +}); + +const res = await db.from('posts').getFullList({ + filter, + select, + sort +}); + +console.log(res); diff --git a/src/queryParams.ts b/src/queryParams.ts deleted file mode 100644 index 1698c70..0000000 --- a/src/queryParams.ts +++ /dev/null @@ -1,242 +0,0 @@ -import { FilterParam } from './filter.js'; -import { Sort } from './sort.js'; -import { - ArrayInnerType, - GenericCollection, - MaybeArray, - MaybeMakeArray, - Prettify, - RecordWithExpandToDotPath -} from './types.js'; - -export type Select = { - [K in keyof Collection['response']]?: boolean; -}; - -export type SelectWithExpand = - Select & { - $expand?: { - [K in keyof Collection['relations']]?: - | SelectWithExpand> - | boolean; - }; - }; - -export type ResolveSelect< - TCollection extends GenericCollection, - TSelect extends Select -> = Extract extends never - ? TCollection['response'] - : { - [K in keyof TSelect & - keyof TCollection['response'] as TSelect[K] extends true - ? K - : never]: TCollection['response'][K]; - }; - -export type ResolveSelectWithExpand< - TCollection extends GenericCollection, - TSelect extends Select -> = Prettify< - ResolveSelect & - ('$expand' extends keyof TSelect - ? { - expand: { - [Relation in keyof TSelect['$expand'] & - keyof TCollection['relations'] as TSelect['$expand'][Relation] extends false - ? never - : Relation]: TSelect['$expand'][Relation] extends true - ? MaybeMakeArray< - TCollection['relations'][Relation], - ArrayInnerType< - TCollection['relations'][Relation] - >['response'] - > - : TSelect['$expand'][Relation] extends object - ? MaybeMakeArray< - TCollection['relations'][Relation], - ResolveSelectWithExpand< - ArrayInnerType< - TCollection['relations'][Relation] - >, - TSelect['$expand'][Relation] - > - > - : never; - }; - } - : {}) ->; - -export interface CreateOptions { - < - TCollection extends GenericCollection, - TSelect extends SelectWithExpand - >( - options: TypedRecordFullListQueryParams - ): TypedRecordFullListQueryParams; - < - TCollection extends GenericCollection, - TSelect extends SelectWithExpand - >( - options: TypedRecordListQueryParams - ): TypedRecordListQueryParams; - < - TCollection extends GenericCollection, - TSelect extends SelectWithExpand - >( - options: TypedRecordQueryParams - ): TypedRecordQueryParams; -} - -export function resolveSelect(select: any) { - const fields: string[] = []; - const expand: string[] = []; - - if (select) { - (function recurse( - { $expand, ...rest }: SelectWithExpand, - fieldsParent: string[] = [], - expandParent: string[] = [] - ) { - if (Object.keys(rest).length === 0) { - fields.push([...fieldsParent, '*'].join('.')); - } else { - for (const key in rest) { - if (rest[key]) { - fields.push([...fieldsParent, key].join('.')); - } - } - } - - if ($expand) { - for (const key in $expand) { - const sub = $expand[key]; - if (sub === true) { - expand.push([...expandParent, key].join('.')); - fields.push( - [...fieldsParent, 'expand', key, '*'].join('.') - ); - } else if (sub) { - expand.push([...expandParent, key].join('.')); - recurse( - sub, - [...fieldsParent, 'expand', key], - [...expandParent, key] - ); - } - } - } - })(select); - } else { - fields.push('*'); - } - - return { - fields: fields.join(','), - expand: expand.join(',') - }; -} - -export const createOptions: CreateOptions = (options) => { - const fields: string[] = []; - const expand: string[] = []; - - if (options.select) { - (function recurse( - { $expand, ...rest }: SelectWithExpand, - fieldsParent: string[] = [], - expandParent: string[] = [] - ) { - if (Object.keys(rest).length === 0) { - fields.push([...fieldsParent, '*'].join('.')); - } else { - for (const key in rest) { - if (rest[key]) { - fields.push([...fieldsParent, key].join('.')); - } - } - } - - if ($expand) { - for (const key in $expand) { - const sub = $expand[key]; - if (sub === true) { - expand.push([...expandParent, key].join('.')); - fields.push( - [...fieldsParent, 'expand', key, '*'].join('.') - ); - } else if (sub) { - expand.push([...expandParent, key].join('.')); - recurse( - sub, - [...fieldsParent, 'expand', key], - [...expandParent, key] - ); - } - } - } - })(options.select); - } else { - fields.push('*'); - } - - let { sort } = options as any; - if (Array.isArray(sort)) { - sort = sort.join(','); - } - - return { - ...options, - fields: fields.join(','), - expand: expand.join(','), - sort - }; -}; - -export interface TypedBaseQueryParams { - select?: TSelect; - requestKey?: string | null; - /** - * @deprecated use `requestKey:null` instead - */ - $autoCancel?: boolean; - /** - * @deprecated use `requestKey:string` instead - */ - $cancelKey?: string; -} - -export interface TypedListQueryParams< - TCollection extends GenericCollection, - TSelect -> extends TypedBaseQueryParams { - page?: number; - perPage?: number; - sort?: MaybeArray>; - filter?: FilterParam>; -} - -export interface TypedFullListQueryParams< - TCollection extends GenericCollection, - TSelect -> extends TypedListQueryParams { - batch?: number; -} - -export interface TypedRecordQueryParams - extends TypedBaseQueryParams { - select?: TSelect; -} - -export interface TypedRecordListQueryParams< - TCollection extends GenericCollection, - TSelect -> extends TypedListQueryParams, - TypedRecordQueryParams {} - -export interface TypedRecordFullListQueryParams< - TCollection extends GenericCollection, - TSelect -> extends TypedFullListQueryParams, - TypedRecordQueryParams {} diff --git a/src/record-service.ts b/src/record-service.ts deleted file mode 100644 index d636116..0000000 --- a/src/record-service.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { ListResult, RecordAuthResponse, RecordService } from 'pocketbase'; -import { GenericCollection } from './types.js'; -import { Filter } from './filter.js'; -import { - ResolveSelectWithExpand, - SelectWithExpand, - TypedRecordFullListQueryParams, - TypedRecordListQueryParams, - TypedRecordQueryParams -} from './queryParams.js'; - -// @ts-expect-error -export interface TypedRecordService - extends RecordService { - getFullList>( - options?: TypedRecordFullListQueryParams - ): Promise[]>; - getFullList>( - batch?: number, - options?: TypedRecordListQueryParams - ): Promise[]>; - - getList>( - page?: number, - perPage?: number, - options?: TypedRecordListQueryParams - ): Promise>>; - - getFirstListItem>( - filter: Filter, - options?: TypedRecordListQueryParams - ): Promise>; - - getOne>( - id: string, - options?: TypedRecordQueryParams - ): Promise>; - - create>( - bodyParams: Collection['create'], - options?: TypedRecordQueryParams - ): Promise>; - - update>( - id: string, - bodyParams: Collection['update'], - options?: TypedRecordQueryParams - ): Promise>; - - // ===== AUTH ===== - authWithPassword>( - usernameOrEmail: string, - password: string, - options?: TypedRecordQueryParams - ): Promise< - RecordAuthResponse> - >; - - authWithOAuth2Code>( - provider: string, - code: string, - codeVerifier: string, - redirectUrl: string, - createData?: { - [key: string]: any; - }, - options?: TypedRecordQueryParams - ): Promise< - RecordAuthResponse> - >; - - authWithOAuth2>( - provider: string, - code: string, - codeVerifier: string, - redirectUrl: string, - createData?: { - [key: string]: any; - }, - bodyParams?: { - [key: string]: any; - }, - options?: TypedRecordQueryParams - ): Promise< - RecordAuthResponse> - >; - - authRefresh>( - options?: TypedRecordQueryParams - ): Promise< - RecordAuthResponse> - >; -} diff --git a/src/select.ts b/src/select.ts new file mode 100644 index 0000000..431adf2 --- /dev/null +++ b/src/select.ts @@ -0,0 +1,114 @@ +import { + ArrayInnerType, + GenericCollection, + MaybeMakeArray, + Prettify +} from './types.js'; + +export type Select = { + [K in keyof Collection['response']]?: boolean; +}; + +export type SelectWithExpand = + Select & { + expand?: { + [K in keyof Collection['relations']]?: + | SelectWithExpand> + | boolean; + }; + }; + +export type ResolveSelect< + TCollection extends GenericCollection, + TSelect extends Select +> = Extract extends never + ? TCollection['response'] + : { + [K in keyof TSelect & + keyof TCollection['response'] as TSelect[K] extends true + ? K + : never]: TCollection['response'][K]; + }; + +export type ResolveSelectWithExpand< + TCollection extends GenericCollection, + TSelect extends Select +> = Prettify< + ResolveSelect & + ('expand' extends keyof TSelect + ? { + expand: { + [Relation in keyof TSelect['expand'] & + keyof TCollection['relations'] as TSelect['expand'][Relation] extends false + ? never + : Relation]: TSelect['expand'][Relation] extends true + ? MaybeMakeArray< + TCollection['relations'][Relation], + ArrayInnerType< + TCollection['relations'][Relation] + >['response'] + > + : TSelect['expand'][Relation] extends object + ? MaybeMakeArray< + TCollection['relations'][Relation], + ResolveSelectWithExpand< + ArrayInnerType< + TCollection['relations'][Relation] + >, + TSelect['expand'][Relation] + > + > + : never; + }; + } + : {}) +>; + +export function resolveSelect(select: any) { + const fieldList: string[] = []; + const expandList: string[] = []; + + if (select) { + (function recurse( + { expand, ...rest }: SelectWithExpand, + fieldsParent: string[] = [], + expandParent: string[] = [] + ) { + if (Object.keys(rest).length === 0) { + fieldList.push([...fieldsParent, '*'].join('.')); + } else { + for (const key in rest) { + if (rest[key]) { + fieldList.push([...fieldsParent, key].join('.')); + } + } + } + + if (expand) { + for (const key in expand) { + const sub = expand[key]; + if (sub === true) { + expandList.push([...expandParent, key].join('.')); + fieldList.push( + [...fieldsParent, 'expand', key, '*'].join('.') + ); + } else if (sub) { + expandList.push([...expandParent, key].join('.')); + recurse( + sub, + [...fieldsParent, 'expand', key], + [...expandParent, key] + ); + } + } + } + })(select); + } else { + fieldList.push('*'); + } + + return { + fields: fieldList.join(','), + expand: expandList.join(',') + }; +} diff --git a/src/sort.ts b/src/sort.ts index ca3c040..b495062 100644 --- a/src/sort.ts +++ b/src/sort.ts @@ -1,29 +1,7 @@ -import { - BaseRecord, - GenericCollection, - RecordWithExpandToDotPath -} from './types.js'; +import type { BaseRecord } from './types.js'; -export type SortParam = { - __record__?: T; -} & string; - -export type Sort = - | SortParam> - | PrefixedSortItem>; - -export type PrefixedSortItem = T extends string ? `${'+' | '-'}${T}` : never; - -export function sort( - ...sorters: Array | PrefixedSortItem> -): SortParam { - return sorters.filter(Boolean).join(','); -} - -export function asc(column: keyof T): SortParam { - return `+${String(column)}`; -} - -export function desc(column: keyof T): SortParam { - return `-${String(column)}`; -} +export type Sort = + | `${'+' | '-'}${keyof T & string}` + | false + | null + | undefined; diff --git a/src/types.ts b/src/types.ts index 3562e42..555bfd7 100644 --- a/src/types.ts +++ b/src/types.ts @@ -106,3 +106,5 @@ type _RecordWithExpandToDotPath< export type RecordWithExpandToDotPath = Prettify< _RecordWithExpandToDotPath >; + +export type ReservedRecordNames = 'collectionId' | 'collectionName';