-
-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* feat: pocketbase module * feat: add show record * feat: create record * feat: create and update record statements * refactor: abstract createRecordOptions func * fix: parameters types (not undefined allowed) and update+delete test * chore: serve pocketbase before run test --------- Co-authored-by: Puria Nafisi Azizi <[email protected]>
- Loading branch information
1 parent
42af3bf
commit 8e9b08c
Showing
11 changed files
with
770 additions
and
65 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
PB_ADDRESS = "http://127.0.0.1:8090/" | ||
EMAIL = "[email protected]" | ||
PASSWORD = "pppppppp" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
{ | ||
"name": "@slangroom/pocketbase", | ||
"version": "1.0.0", | ||
"dependencies": { | ||
"@slangroom/core": "workspace:*", | ||
"@slangroom/shared": "workspace:*", | ||
"dotenv": "^16.3.1", | ||
"pocketbase": "^0.19.0", | ||
"zod": "^3.22.4" | ||
}, | ||
"repository": "https://github.com/dyne/slangroom", | ||
"license": "AGPL-3.0-only", | ||
"type": "module", | ||
"main": "./build/esm/src/index.js", | ||
"types": "./build/esm/src/index.d.ts", | ||
"exports": { | ||
".": { | ||
"import": { | ||
"types": "./build/esm/src/index.d.ts", | ||
"default": "./build/esm/src/index.js" | ||
} | ||
}, | ||
"./*": { | ||
"import": { | ||
"types": "./build/esm/src/*.d.ts", | ||
"default": "./build/esm/src/*.js" | ||
} | ||
} | ||
}, | ||
"publishConfig": { | ||
"access": "public" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export * from '@slangroom/pocketbase/plugin'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,246 @@ | ||
import PocketBase from 'pocketbase'; | ||
import type { FullListOptions, ListResult, RecordModel, RecordOptions } from 'pocketbase'; | ||
import { Plugin } from '@slangroom/core'; | ||
import { z } from 'zod'; | ||
|
||
let pb: PocketBase; | ||
const p = new Plugin(); | ||
|
||
const serverUrlSchema = z.literal( | ||
`http${z.union([z.literal('s'), z.literal('')])}://${z.string()}/`, | ||
); | ||
export type ServerUrl = z.infer<typeof serverUrlSchema>; | ||
|
||
const credentialsSchema = z.object({ | ||
email: z.string().email(), | ||
password: z.string(), | ||
}); | ||
export type Credentials = z.infer<typeof credentialsSchema>; | ||
|
||
const baseRecordParametersSchema = z.object({ | ||
expand: z.string().nullable(), | ||
requestKey: z.string().nullable(), | ||
fields: z.string().nullable(), | ||
}); | ||
export type RecordBaseParameters = z.infer<typeof baseRecordParametersSchema>; | ||
|
||
const baseFetchRecordParametersSchema = baseRecordParametersSchema.extend({ | ||
collection: z.string(), | ||
}); | ||
|
||
const paginationSchema = z.object({ | ||
page: z.number().int(), | ||
perPage: z.number().int(), | ||
}); | ||
|
||
const listParametersBaseSchema = z | ||
.object({ | ||
sort: z.string().nullable(), | ||
}) | ||
.merge(baseFetchRecordParametersSchema); | ||
|
||
const listParametersSchema = z.discriminatedUnion('type', [ | ||
listParametersBaseSchema.extend({ type: z.literal('all'), filter: z.string().nullable() }), | ||
listParametersBaseSchema.extend({ | ||
type: z.literal('list'), | ||
filter: z.string().nullable(), | ||
pagination: paginationSchema, | ||
}), | ||
listParametersBaseSchema.extend({ type: z.literal('first'), filter: z.string() }), | ||
]); | ||
export type ListParameters = z.input<typeof listParametersSchema>; | ||
|
||
const showParametersSchema = z | ||
.object({ | ||
id: z.string(), | ||
}) | ||
.merge(baseFetchRecordParametersSchema); | ||
export type ShowRecordParameters = z.infer<typeof showParametersSchema>; | ||
|
||
const createRecordParametersSchema = z | ||
.object({ | ||
record: z.record(z.string(), z.any()), | ||
collection: z.string() | ||
}) | ||
export type CreateRecordParameters = z.infer<typeof createRecordParametersSchema>; | ||
|
||
const updateRecordParametersSchema = z | ||
.object({ | ||
id: z.string(), | ||
}) | ||
.merge(createRecordParametersSchema); | ||
export type UpdateRecordParameters = z.infer<typeof updateRecordParametersSchema>; | ||
|
||
const deleteParametersSchema = z.object({ | ||
collection: z.string(), | ||
id: z.string(), | ||
}); | ||
export type DeleteRecordParameters = z.infer<typeof deleteParametersSchema>; | ||
|
||
const isPbRunning = async () => { | ||
const res = await pb.health.check({ requestKey: null }); | ||
return res.code === 200; | ||
}; | ||
|
||
const createRecordOptions = (p: RecordBaseParameters) => { | ||
const { expand, fields, requestKey } = p; | ||
const options: RecordOptions = {}; | ||
if (expand !== null) options.expand = expand; | ||
if (fields !== null) options.fields = fields; | ||
if (requestKey !== null) options.requestKey = requestKey; | ||
return options; | ||
}; | ||
|
||
/** | ||
* @internal | ||
*/ | ||
export const setupClient = p.new(['pb_address'], 'create pb_client', async (ctx) => { | ||
const address = ctx.fetch('pb_address'); | ||
if (typeof address !== 'string') return ctx.fail('Invalid address'); | ||
try { | ||
pb = new PocketBase(address); | ||
if (!(await isPbRunning())) return ctx.fail('Client is not running'); | ||
return ctx.pass('pb client successfully created'); | ||
} catch (e) { | ||
throw new Error(e) | ||
} | ||
}); | ||
|
||
/** | ||
* @internal | ||
*/ | ||
export const authWithPassword = p.new(['my_credentials'], 'login', async (ctx) => { | ||
const credentials = ctx.fetch('my_credentials') as Credentials; | ||
|
||
const validation = credentialsSchema.safeParse(credentials); | ||
if (!validation.success) return ctx.fail(validation.error); | ||
if (!(await isPbRunning())) return ctx.fail('Client is not running'); | ||
|
||
try { | ||
const res = await pb | ||
.collection('users') | ||
.authWithPassword(credentials!.email, credentials!.password, { requestKey: null }); | ||
return ctx.pass({ token: res.token, record: res.record }); | ||
} catch (err) { | ||
throw new Error(err) | ||
} | ||
}); | ||
|
||
/** | ||
* @internal | ||
*/ | ||
export const getList = p.new(['list_parameters'], 'ask records', async (ctx) => { | ||
const params = ctx.fetch('list_parameters') as ListParameters; | ||
const validation = listParametersSchema.safeParse(params); | ||
if (!validation.success) return ctx.fail(validation.error); | ||
|
||
const { collection, sort, filter, expand, type, requestKey } = params; | ||
if (!(await isPbRunning())) return ctx.fail('client is not working'); | ||
|
||
const options: FullListOptions = { requestKey: requestKey || type }; | ||
if (sort) options.sort = sort; | ||
if (filter) options.filter = filter; | ||
if (expand) options['expand'] = expand; | ||
|
||
let res: RecordModel | RecordModel[] | ListResult<RecordModel>; | ||
if (type === 'all') { | ||
res = await pb.collection(collection).getFullList(options); | ||
} else if (type === 'list') { | ||
const { page, perPage } = params.pagination; | ||
res = await pb.collection(collection).getList(page, perPage, options); | ||
} else { | ||
res = await pb.collection(collection).getFirstListItem(filter, options); | ||
} | ||
//@ts-expect-error Jsonable should take also ListResult | ||
return ctx.pass({ records: res }); | ||
}); | ||
|
||
/** | ||
* @internal | ||
*/ | ||
export const showRecord = p.new(['show_parameters'], 'ask record', async (ctx) => { | ||
const p = ctx.fetch('show_parameters') as ShowRecordParameters; | ||
const validation = showParametersSchema.safeParse(p); | ||
if (!validation.success) return ctx.fail(validation.error); | ||
|
||
const options = createRecordOptions(p); | ||
|
||
try { | ||
const res = await pb.collection(p.collection).getOne(p.id, options); | ||
return ctx.pass(res); | ||
} catch (err) { | ||
throw new Error(err) | ||
} | ||
}); | ||
|
||
/** | ||
* @internal | ||
*/ | ||
export const createRecord = p.new( | ||
['create_parameters', 'record_parameters'], | ||
'create record', | ||
async (ctx) => { | ||
const p = ctx.fetch('create_parameters') as CreateRecordParameters; | ||
const r = ctx.fetch('record_parameters') as RecordBaseParameters; | ||
|
||
const validateCreateParams = createRecordParametersSchema.safeParse(p); | ||
const validateRecordParams = baseRecordParametersSchema.safeParse(r); | ||
if (!validateCreateParams.success) return ctx.fail(validateCreateParams.error); | ||
if (!validateRecordParams.success) return ctx.fail(validateRecordParams.error); | ||
|
||
const { collection, record } = p; | ||
const options = createRecordOptions(r); | ||
|
||
try { | ||
const res = await pb.collection(collection).create(record, options); | ||
return ctx.pass(res); | ||
} catch (err) { | ||
throw new Error(err.message); | ||
} | ||
}, | ||
); | ||
|
||
/** | ||
* @internal | ||
*/ | ||
export const updateRecord = p.new( | ||
['update_parameters', 'record_parameters'], | ||
'update record', | ||
async (ctx) => { | ||
const p = ctx.fetch('update_parameters') as UpdateRecordParameters; | ||
const r = ctx.fetch('record_parameters') as RecordBaseParameters; | ||
|
||
const validateUpdateParams = updateRecordParametersSchema.safeParse(p); | ||
const validateRecordParams = baseRecordParametersSchema.safeParse(r); | ||
if (!validateUpdateParams.success) return ctx.fail(validateUpdateParams.error); | ||
if (!validateRecordParams.success) return ctx.fail(validateRecordParams.error); | ||
|
||
const { collection, record, id } = p; | ||
const options = createRecordOptions(r); | ||
|
||
try { | ||
const res = await pb.collection(collection).update(id, record, options); | ||
return ctx.pass(res); | ||
} catch (err) { | ||
throw new Error(err.message); | ||
} | ||
}, | ||
); | ||
|
||
/** | ||
* @internal | ||
*/ | ||
export const deleteRecord = p.new(['delete_parameters'], 'delete record', async (ctx) => { | ||
const p = ctx.fetch('delete_parameters') as DeleteRecordParameters; | ||
|
||
const validation = deleteParametersSchema.safeParse(p); | ||
if (!validation.success) return ctx.fail(validation.error); | ||
|
||
const { collection, id } = p; | ||
const res = await pb.collection(collection).delete(id); | ||
if (res) return ctx.pass('deleted'); | ||
return ctx.fail('shit happened'); | ||
}); | ||
|
||
export const pocketbase = p; | ||
|
Oops, something went wrong.