init
This commit is contained in:
8
.changeset/README.md
Normal file
8
.changeset/README.md
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
# Changesets
|
||||||
|
|
||||||
|
Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
|
||||||
|
with multi-package repos, or single-package repos to help you version and publish your code. You can
|
||||||
|
find the full documentation for it [in our repository](https://github.com/changesets/changesets)
|
||||||
|
|
||||||
|
We have a quick list of common questions to get you started engaging with this project in
|
||||||
|
[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)
|
||||||
11
.changeset/config.json
Normal file
11
.changeset/config.json
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://unpkg.com/@changesets/config@3.0.0/schema.json",
|
||||||
|
"changelog": "@changesets/cli/changelog",
|
||||||
|
"commit": false,
|
||||||
|
"fixed": [],
|
||||||
|
"linked": [],
|
||||||
|
"access": "public",
|
||||||
|
"baseBranch": "main",
|
||||||
|
"updateInternalDependencies": "patch",
|
||||||
|
"ignore": ["playground"]
|
||||||
|
}
|
||||||
19
.github/renovate.json
vendored
Normal file
19
.github/renovate.json
vendored
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||||
|
"extends": ["config:recommended"],
|
||||||
|
"dependencyDashboard": true,
|
||||||
|
"lockFileMaintenance": {
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
"postUpdateOptions": ["pnpmDedupe"],
|
||||||
|
"packageRules": [
|
||||||
|
{
|
||||||
|
"groupName": "all dependencies",
|
||||||
|
"groupSlug": "all",
|
||||||
|
"matchPackagePatterns": ["*"],
|
||||||
|
"schedule": ["before 4am on Monday"],
|
||||||
|
"rangeStrategy": "bump"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"ignoreDeps": ["node"]
|
||||||
|
}
|
||||||
25
.github/workflows/build.yml
vendored
Normal file
25
.github/workflows/build.yml
vendored
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
name: Build
|
||||||
|
|
||||||
|
on: [pull_request]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup PNPM
|
||||||
|
run: corepack enable && pnpm -v
|
||||||
|
|
||||||
|
- name: Setup Node
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 18.19.0
|
||||||
|
cache: pnpm
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: pnpm --filter astro-pocketbase build
|
||||||
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
node_modules
|
||||||
|
TODO.md
|
||||||
22
.vscode/settings.json
vendored
Normal file
22
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"workbench.colorCustomizations": {
|
||||||
|
"activityBar.activeBackground": "#93a1fb",
|
||||||
|
"activityBar.background": "#93a1fb",
|
||||||
|
"activityBar.foreground": "#15202b",
|
||||||
|
"activityBar.inactiveForeground": "#15202b99",
|
||||||
|
"activityBarBadge.background": "#d70824",
|
||||||
|
"activityBarBadge.foreground": "#e7e7e7",
|
||||||
|
"commandCenter.border": "#e7e7e799",
|
||||||
|
"sash.hoverBorder": "#93a1fb",
|
||||||
|
"statusBar.background": "#6276f9",
|
||||||
|
"statusBar.foreground": "#e7e7e7",
|
||||||
|
"statusBarItem.hoverBackground": "#93a1fb",
|
||||||
|
"statusBarItem.remoteBackground": "#6276f9",
|
||||||
|
"statusBarItem.remoteForeground": "#e7e7e7",
|
||||||
|
"titleBar.activeBackground": "#6276f9",
|
||||||
|
"titleBar.activeForeground": "#e7e7e7",
|
||||||
|
"titleBar.inactiveBackground": "#6276f999",
|
||||||
|
"titleBar.inactiveForeground": "#e7e7e799"
|
||||||
|
},
|
||||||
|
"peacock.color": "#6276f9"
|
||||||
|
}
|
||||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2024 Gregory Bouteiller
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
9
README.md
Normal file
9
README.md
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
# zod-pocketbase
|
||||||
|
|
||||||
|
Zod tooling for your Pocketbase instance.
|
||||||
|
|
||||||
|
To see how to get started, check out the [docs](https://zod-pocketbase.vercel.app)
|
||||||
|
|
||||||
|
## Licensing
|
||||||
|
|
||||||
|
[MIT Licensed](./LICENSE). Made with ❤️ by [Gregory Bouteiller](https://github.com/gbouteiller).
|
||||||
21
doc/.gitignore
vendored
Normal file
21
doc/.gitignore
vendored
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
# build output
|
||||||
|
dist/
|
||||||
|
# generated types
|
||||||
|
.astro/
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# logs
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
|
||||||
|
|
||||||
|
# environment variables
|
||||||
|
.env
|
||||||
|
.env.production
|
||||||
|
|
||||||
|
# macOS-specific files
|
||||||
|
.DS_Store
|
||||||
4
doc/.vscode/extensions.json
vendored
Normal file
4
doc/.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"recommendations": ["astro-build.astro-vscode"],
|
||||||
|
"unwantedRecommendations": []
|
||||||
|
}
|
||||||
11
doc/.vscode/launch.json
vendored
Normal file
11
doc/.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"command": "./node_modules/.bin/astro dev",
|
||||||
|
"name": "Development server",
|
||||||
|
"request": "launch",
|
||||||
|
"type": "node-terminal"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
13
doc/CHANGELOG.md
Normal file
13
doc/CHANGELOG.md
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
# doc
|
||||||
|
|
||||||
|
## 0.1.0
|
||||||
|
|
||||||
|
### Minor Changes
|
||||||
|
|
||||||
|
- remove "cache" option for "helpersFrom" and replace it with "fetch" to allow a more generic custom fetch and remove "@11ty/eleventy-fetch" dependency
|
||||||
|
|
||||||
|
## 0.0.2
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- update packages
|
||||||
55
doc/README.md
Normal file
55
doc/README.md
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
# Starlight Starter Kit: Basics
|
||||||
|
|
||||||
|
[](https://starlight.astro.build)
|
||||||
|
|
||||||
|
```
|
||||||
|
npm create astro@latest -- --template starlight
|
||||||
|
```
|
||||||
|
|
||||||
|
[](https://stackblitz.com/github/withastro/starlight/tree/main/examples/basics)
|
||||||
|
[](https://codesandbox.io/p/sandbox/github/withastro/starlight/tree/main/examples/basics)
|
||||||
|
[](https://app.netlify.com/start/deploy?repository=https://github.com/withastro/starlight&create_from_path=examples/basics)
|
||||||
|
[](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fwithastro%2Fstarlight%2Ftree%2Fmain%2Fexamples%2Fbasics&project-name=my-starlight-docs&repository-name=my-starlight-docs)
|
||||||
|
|
||||||
|
> 🧑🚀 **Seasoned astronaut?** Delete this file. Have fun!
|
||||||
|
|
||||||
|
## 🚀 Project Structure
|
||||||
|
|
||||||
|
Inside of your Astro + Starlight project, you'll see the following folders and files:
|
||||||
|
|
||||||
|
```
|
||||||
|
.
|
||||||
|
├── public/
|
||||||
|
├── src/
|
||||||
|
│ ├── assets/
|
||||||
|
│ ├── content/
|
||||||
|
│ │ ├── docs/
|
||||||
|
│ │ └── config.ts
|
||||||
|
│ └── env.d.ts
|
||||||
|
├── astro.config.mjs
|
||||||
|
├── package.json
|
||||||
|
└── tsconfig.json
|
||||||
|
```
|
||||||
|
|
||||||
|
Starlight looks for `.md` or `.mdx` files in the `src/content/docs/` directory. Each file is exposed as a route based on its file name.
|
||||||
|
|
||||||
|
Images can be added to `src/assets/` and embedded in Markdown with a relative link.
|
||||||
|
|
||||||
|
Static assets, like favicons, can be placed in the `public/` directory.
|
||||||
|
|
||||||
|
## 🧞 Commands
|
||||||
|
|
||||||
|
All commands are run from the root of the project, from a terminal:
|
||||||
|
|
||||||
|
| Command | Action |
|
||||||
|
| :------------------------ | :----------------------------------------------- |
|
||||||
|
| `npm install` | Installs dependencies |
|
||||||
|
| `npm run dev` | Starts local dev server at `localhost:4321` |
|
||||||
|
| `npm run build` | Build your production site to `./dist/` |
|
||||||
|
| `npm run preview` | Preview your build locally, before deploying |
|
||||||
|
| `npm run astro ...` | Run CLI commands like `astro add`, `astro check` |
|
||||||
|
| `npm run astro -- --help` | Get help using the Astro CLI |
|
||||||
|
|
||||||
|
## 👀 Want to learn more?
|
||||||
|
|
||||||
|
Check out [Starlight’s docs](https://starlight.astro.build/), read [the Astro documentation](https://docs.astro.build), or jump into the [Astro Discord server](https://astro.build/chat).
|
||||||
29
doc/astro.config.mjs
Normal file
29
doc/astro.config.mjs
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
// @ts-check
|
||||||
|
import { defineConfig } from "astro/config";
|
||||||
|
import starlight from "@astrojs/starlight";
|
||||||
|
|
||||||
|
// https://astro.build/config
|
||||||
|
export default defineConfig({
|
||||||
|
integrations: [
|
||||||
|
starlight({
|
||||||
|
title: "Zod PocketBase",
|
||||||
|
social: {
|
||||||
|
github: "https://github.com/gbouteiller/zod-pocketbase",
|
||||||
|
},
|
||||||
|
sidebar: [
|
||||||
|
{
|
||||||
|
label: "Start here",
|
||||||
|
autogenerate: { directory: "start-here" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Guides",
|
||||||
|
autogenerate: { directory: "guides" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Reference",
|
||||||
|
autogenerate: { directory: "reference" },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
||||||
19
doc/package.json
Normal file
19
doc/package.json
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"name": "doc",
|
||||||
|
"type": "module",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "astro dev",
|
||||||
|
"start": "astro dev",
|
||||||
|
"build": "astro check && astro build",
|
||||||
|
"preview": "astro preview",
|
||||||
|
"astro": "astro"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@astrojs/check": "^0.9.4",
|
||||||
|
"@astrojs/starlight": "^0.30.3",
|
||||||
|
"astro": "^5.1.1",
|
||||||
|
"sharp": "^0.33.5",
|
||||||
|
"typescript": "^5.7.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
5217
doc/pnpm-lock.yaml
generated
Normal file
5217
doc/pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
1
doc/public/favicon.svg
Normal file
1
doc/public/favicon.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128"><path fill-rule="evenodd" d="M81 36 64 0 47 36l-1 2-9-10a6 6 0 0 0-9 9l10 10h-2L0 64l36 17h2L28 91a6 6 0 1 0 9 9l9-10 1 2 17 36 17-36v-2l9 10a6 6 0 1 0 9-9l-9-9 2-1 36-17-36-17-2-1 9-9a6 6 0 1 0-9-9l-9 10v-2Zm-17 2-2 5c-4 8-11 15-19 19l-5 2 5 2c8 4 15 11 19 19l2 5 2-5c4-8 11-15 19-19l5-2-5-2c-8-4-15-11-19-19l-2-5Z" clip-rule="evenodd"/><path d="M118 19a6 6 0 0 0-9-9l-3 3a6 6 0 1 0 9 9l3-3Zm-96 4c-2 2-6 2-9 0l-3-3a6 6 0 1 1 9-9l3 3c3 2 3 6 0 9Zm0 82c-2-2-6-2-9 0l-3 3a6 6 0 1 0 9 9l3-3c3-2 3-6 0-9Zm96 4a6 6 0 0 1-9 9l-3-3a6 6 0 1 1 9-9l3 3Z"/><style>path{fill:#000}@media (prefers-color-scheme:dark){path{fill:#fff}}</style></svg>
|
||||||
|
After Width: | Height: | Size: 696 B |
BIN
doc/src/assets/cli.png
Normal file
BIN
doc/src/assets/cli.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 100 KiB |
BIN
doc/src/assets/houston.webp
Normal file
BIN
doc/src/assets/houston.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 96 KiB |
6
doc/src/content/config.ts
Normal file
6
doc/src/content/config.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { defineCollection } from 'astro:content';
|
||||||
|
import { docsSchema } from '@astrojs/starlight/schema';
|
||||||
|
|
||||||
|
export const collections = {
|
||||||
|
docs: defineCollection({ schema: docsSchema() }),
|
||||||
|
};
|
||||||
71
doc/src/content/docs/guides/cli.mdx
Normal file
71
doc/src/content/docs/guides/cli.mdx
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
---
|
||||||
|
title: CLI
|
||||||
|
description: The CLI generates Zod schemas for your PocketBase instance.
|
||||||
|
---
|
||||||
|
import { Tabs, TabItem } from '@astrojs/starlight/components';
|
||||||
|
import { Image } from 'astro:assets';
|
||||||
|
import cliImage from '../../../assets/cli.png';
|
||||||
|
|
||||||
|
The CLI generates Zod schemas for your PocketBase instance.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
To generate your schemas, run the following from your project directory:
|
||||||
|
|
||||||
|
<Tabs>
|
||||||
|
<TabItem label="npm">
|
||||||
|
```shell
|
||||||
|
npx zod-pocketbase
|
||||||
|
```
|
||||||
|
</TabItem>
|
||||||
|
<TabItem label="pnpm">
|
||||||
|
```shell
|
||||||
|
pnpm zod-pocketbase
|
||||||
|
```
|
||||||
|
</TabItem>
|
||||||
|
<TabItem label="yarn">
|
||||||
|
```shell
|
||||||
|
yarn zod-pocketbase
|
||||||
|
```
|
||||||
|
</TabItem>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
|
<Image src={cliImage} alt="Zod PocketBase CLI." />
|
||||||
|
|
||||||
|
## Config File
|
||||||
|
|
||||||
|
You can provide a `zod-pocketbase.config.ts` or `zod-pocketbase.config.js` config file.
|
||||||
|
|
||||||
|
```ts title="zod-pocketbase.config.ts"
|
||||||
|
import type { Config } from "zod-pocketbase";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
// default values
|
||||||
|
adminEmail: undefined,
|
||||||
|
adminPassword: undefined,
|
||||||
|
ignore: [],
|
||||||
|
nameEnum: (name: string) => snakeCase(name).toUpperCase(),
|
||||||
|
nameEnumField: (collectionName: string, fieldName: string) => `${collectionName}${pascalCase(fieldName)}`,
|
||||||
|
nameEnumSchema: (name: string) => pascalCase(name),
|
||||||
|
nameEnumType: (name: string) => pascalCase(name),
|
||||||
|
nameEnumValues: (name: string) => `${name}Values`,
|
||||||
|
nameRecordSchema: (name: string) => `${pascalCase(name)}Record`,
|
||||||
|
nameRecordType: (name: string) => `${pascalCase(name)}Record`,
|
||||||
|
output: "./zod-pocketbase.ts",
|
||||||
|
url: undefined,
|
||||||
|
} satisfies Config;
|
||||||
|
```
|
||||||
|
:::tip[For more details]
|
||||||
|
See the [reference](/reference/config).
|
||||||
|
:::
|
||||||
|
|
||||||
|
|
||||||
|
## Environment variables
|
||||||
|
|
||||||
|
You can provide your admin credentials and your **PocketBase** instance url as environment variables:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
ZOD_POCKETBASE_ADMIN_EMAIL="admin@mydomain.com"
|
||||||
|
ZOD_POCKETBASE_ADMIN_PASSWORD="mypassword"
|
||||||
|
ZOD_POCKETBASE_URL="https://myproject.pockethost.io"
|
||||||
|
```
|
||||||
52
doc/src/content/docs/guides/helpers.md
Normal file
52
doc/src/content/docs/guides/helpers.md
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
---
|
||||||
|
title: Helpers
|
||||||
|
description: Helpers are syntactic sugar to get records from your PocketBase instance.
|
||||||
|
---
|
||||||
|
|
||||||
|
Helpers are syntactic sugar to get records from your PocketBase instance. In addition to simplifying the process of fetching records, they also provide type safety, autocompletion and caching.
|
||||||
|
|
||||||
|
To access helpers, you have to, at least, provide an instance of PocketBase SDK to the `helpersFrom` function.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import Fetch from "@11ty/eleventy-fetch";
|
||||||
|
import { helpersFrom } from "zod-pocketbase";
|
||||||
|
import Pocketbase from "pocketbase";
|
||||||
|
|
||||||
|
const { getRecord, getRecords } = helpersFrom({
|
||||||
|
pocketbase: new Pocketbase(import.meta.env.ZOD_POCKETBASE_URL),
|
||||||
|
fetch: async (url, fetchOptions) => {
|
||||||
|
const { body, ...init } = await Fetch(url, { fetchOptions, returnType: "response", type: "json" });
|
||||||
|
return new Response(JSON.stringify(body), init);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## getRecord
|
||||||
|
|
||||||
|
`getRecord` is a helper to get a single record from your PocketBase instance. It takes a [RecordRef](/reference/types#recordref) as its first argument and an object with a schema as its second argument.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const myFirstPost = await getRecord({ collection: "posts", slug: "my-first-post" }, { schema: PostRecord });
|
||||||
|
const mySecondPost = await getRecord({ collection: "posts", id: "3vwc4g9d23orc1r" }, { schema: PostRecord });
|
||||||
|
```
|
||||||
|
|
||||||
|
:::tip[For more details]
|
||||||
|
See the [reference](/reference/methods#getrecord).
|
||||||
|
:::
|
||||||
|
|
||||||
|
## getRecords
|
||||||
|
|
||||||
|
`getRecords` is a helper to get multiple records from your PocketBase instance. It takes a collection name as its first argument and an object with some options (at least a record schema) as its second argument.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const { items: myPosts } = await getRecords("posts", { schema: PostRecord });
|
||||||
|
const { items: someSpecificPosts } = await getRecords("posts", { schema: PostRecord, sort: "-updated", page: 2, perPage: 10 });
|
||||||
|
```
|
||||||
|
|
||||||
|
:::caution
|
||||||
|
The provided schema is only for a record. The method returns a [RecordsList](/reference/types/#recordslist) object.
|
||||||
|
:::
|
||||||
|
|
||||||
|
:::tip[For more details]
|
||||||
|
See the [reference](/reference/methods#getrecords).
|
||||||
|
:::
|
||||||
121
doc/src/content/docs/guides/schemas.md
Normal file
121
doc/src/content/docs/guides/schemas.md
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
---
|
||||||
|
title: Schemas
|
||||||
|
description: How to secure and simplify schemas for your PocketBase instance.
|
||||||
|
---
|
||||||
|
|
||||||
|
Here you can find how to secure and simplify schemas for your PocketBase instance:
|
||||||
|
|
||||||
|
1. with the CLI generated schemas
|
||||||
|
2. with the `expand` method
|
||||||
|
3. with the `pick` method
|
||||||
|
4. with the `select` method
|
||||||
|
|
||||||
|
## 0 - A Zod schema for a Post collection
|
||||||
|
|
||||||
|
Here is the original schema for a `Post` collection:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const Post = z
|
||||||
|
.object({
|
||||||
|
content: z.string(),
|
||||||
|
expand: z.object({
|
||||||
|
author: z
|
||||||
|
.object({
|
||||||
|
name: z.string(),
|
||||||
|
expand: z.object({
|
||||||
|
image: z.object({
|
||||||
|
alt: z.string(),
|
||||||
|
src: z.string(),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
.transform(({ expand, ...rest }) => ({ ...rest, ...expand })),
|
||||||
|
image: z.object({
|
||||||
|
alt: z.string(),
|
||||||
|
src: z.string(),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
title: z.string(),
|
||||||
|
updated: z.coerce.date(),
|
||||||
|
})
|
||||||
|
.transform(({ expand, ...rest }) => ({ ...rest, ...expand }));
|
||||||
|
```
|
||||||
|
|
||||||
|
## 1 - With CLI generated schema
|
||||||
|
|
||||||
|
All schemas are generated from your PocketBase.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import {AuthorRecord, ImageRecord, PostRecord} from "./schemas";
|
||||||
|
|
||||||
|
const Post = PostRecord.pick({ content: true, title: true, updated: true })
|
||||||
|
.extend({
|
||||||
|
expand: z.object({
|
||||||
|
author: AuthorRecord.pick({ name: true })
|
||||||
|
.extend({
|
||||||
|
expand: z.object({
|
||||||
|
image: ImageRecord.pick({ alt: true, src: true }),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
.transform(({ expand, ...rest }) => ({ ...rest, ...expand })),
|
||||||
|
image: ImageRecord.pick({ alt: true, src: true }),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
.transform(({ expand, ...rest }) => ({ ...rest, ...expand }));
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2 - With pick
|
||||||
|
|
||||||
|
`pick` is a syntactic sugar for native zod `pick`.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { pick } from "zod-pocketbase";
|
||||||
|
import {AuthorRecord, ImageRecord, PostRecord} from "./schemas";
|
||||||
|
|
||||||
|
const Post = pick(PostRecord, ["content", "title", "updated"])
|
||||||
|
.extend({
|
||||||
|
expand: z.object({
|
||||||
|
author: pick(AuthorRecord, ["name"])
|
||||||
|
.extend({
|
||||||
|
expand: z.object({
|
||||||
|
image: pick(ImageRecord, ["alt", "src"]),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
.transform(({ expand, ...rest }) => ({ ...rest, ...expand })),
|
||||||
|
image: pick(ImageRecord, ["alt", "src"]),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
.transform(({ expand, ...rest }) => ({ ...rest, ...expand }));
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3 - With expand
|
||||||
|
|
||||||
|
`expand` is a syntactic sugar for native zod `extend` on the property `expand` coupled with `transform`.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { expand, pick } from "zod-pocketbase";
|
||||||
|
import {AuthorRecord, ImageRecord, PostRecord} from "./schemas";
|
||||||
|
|
||||||
|
const Post = expand(pick(PostRecord, ["content", "title", "updated"]), {
|
||||||
|
author: expand(pick(AuthorRecord, ["name"]), {
|
||||||
|
image: pick(ImageRecord, ["alt", "src"])
|
||||||
|
}),
|
||||||
|
image: pick(ImageRecord, ["alt", "src"])
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4 - With select
|
||||||
|
|
||||||
|
`select` is the union of `pick` and `expand`.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { select } from "zod-pocketbase";
|
||||||
|
import {AuthorRecord, ImageRecord, PostRecord} from "./schemas";
|
||||||
|
|
||||||
|
const Post = select(PostRecord, ["content", "title", "updated"], {
|
||||||
|
author: select(AuthorRecord, ["name"], {
|
||||||
|
image: select(ImageRecord, ["alt", "src"])
|
||||||
|
}),
|
||||||
|
image: select(ImageRecord, ["alt", "src"])
|
||||||
|
});
|
||||||
|
```
|
||||||
30
doc/src/content/docs/index.mdx
Normal file
30
doc/src/content/docs/index.mdx
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
---
|
||||||
|
title: Welcome to Zod PocketBase
|
||||||
|
description: This library adds Zod safety with ease to your PocketBase instance.
|
||||||
|
template: splash
|
||||||
|
hero:
|
||||||
|
tagline: Add Zod to PocketBase with ease!
|
||||||
|
image:
|
||||||
|
file: ../../assets/houston.webp
|
||||||
|
actions:
|
||||||
|
- text: Why this library?
|
||||||
|
link: /start-here/why/
|
||||||
|
icon: right-arrow
|
||||||
|
- text: View on GitHub
|
||||||
|
link: https://github.com/gbouteiller/zod-pocketbase
|
||||||
|
icon: external
|
||||||
|
variant: minimal
|
||||||
|
---
|
||||||
|
|
||||||
|
import { CardGrid, Card } from '@astrojs/starlight/components';
|
||||||
|
|
||||||
|
## Also check out...
|
||||||
|
|
||||||
|
<CardGrid stagger>
|
||||||
|
<Card title="Astro PocketBase" icon="puzzle">
|
||||||
|
Add [PocketBase to Astro](https://astro-pocketbase-five.vercel.app) with ease!
|
||||||
|
</Card>
|
||||||
|
<Card title="Astro Superforms" icon="puzzle">
|
||||||
|
Add [Superforms to Astro](https://astro-superforms.vercel.app) with ease!
|
||||||
|
</Card>
|
||||||
|
</CardGrid>
|
||||||
122
doc/src/content/docs/reference/config.md
Normal file
122
doc/src/content/docs/reference/config.md
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
---
|
||||||
|
title: Config
|
||||||
|
description: A reference for the Config file.
|
||||||
|
---
|
||||||
|
|
||||||
|
## ignore
|
||||||
|
|
||||||
|
- **Type:** `string[]`
|
||||||
|
- **Default:** `[]`
|
||||||
|
|
||||||
|
The `ignore` option allows you to ignore specific collections from being processed.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
{
|
||||||
|
ignore: ["users"],
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## adminEmail
|
||||||
|
|
||||||
|
- **Type:** `string`
|
||||||
|
- **Default:** `undefined`
|
||||||
|
|
||||||
|
`adminEmail` represents the email of an admin user of your PocketBase instance.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
{
|
||||||
|
adminEmail: "admin@mydomain.com",
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## adminPassword
|
||||||
|
|
||||||
|
- **Type:** `string`
|
||||||
|
- **Default:** `undefined`
|
||||||
|
|
||||||
|
`adminPassword` represents the password of the above admin user of your PocketBase instance.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
{
|
||||||
|
adminPassword: "mypassword",
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## nameEnum
|
||||||
|
|
||||||
|
- **Type:** `(enumFieldName: string) => string`
|
||||||
|
- **Default:** `(enumFieldName) => snakeCase(enumFieldName).toUpperCase()`
|
||||||
|
|
||||||
|
`nameEnum` is a function that takes an enum field name and returns the name of the generated enum.
|
||||||
|
|
||||||
|
## nameEnumField
|
||||||
|
|
||||||
|
- **Type:** `(collectionName: string, fieldName: string) => string`
|
||||||
|
- **Default:** `(collectionName, fieldName) => collectionName + pascalName(fieldName)`
|
||||||
|
|
||||||
|
`nameEnumField` is a function that takes a field name and its collection name and returns the name of the generated enum field.
|
||||||
|
|
||||||
|
## nameEnumSchema
|
||||||
|
|
||||||
|
- **Type:** `(enumFieldName: string) => string`
|
||||||
|
- **Default:** `(enumFieldName) => pascalName(enumFieldName)`
|
||||||
|
|
||||||
|
`nameEnumSchema` is a function that takes an enum field name and returns the name of the generated enum schema.
|
||||||
|
|
||||||
|
## nameEnumType
|
||||||
|
|
||||||
|
- **Type:** `(enumFieldName: string) => string`
|
||||||
|
- **Default:** `(enumFieldName) => pascalName(enumFieldName)`
|
||||||
|
|
||||||
|
`nameEnumType` is a function that takes an enum field name and returns the name of the generated enum type.
|
||||||
|
|
||||||
|
## nameEnumValues
|
||||||
|
|
||||||
|
- **Type:** `(enumFieldName: string) => string`
|
||||||
|
- **Default:** `(enumFieldName) => enumFieldName + "Values"`
|
||||||
|
|
||||||
|
`nameEnumValues` is a function that takes an enum field name and returns the name of the generated enum values.
|
||||||
|
|
||||||
|
## nameRecordSchema
|
||||||
|
|
||||||
|
- **Type:** `(collectionName: string) => string`
|
||||||
|
- **Default:** `(collectionName) => pascalName(collectionName) + "Record"`
|
||||||
|
|
||||||
|
`nameRecordSchema` is a function that takes a collection name and returns the name of the generated record schema.
|
||||||
|
|
||||||
|
## nameRecordType
|
||||||
|
|
||||||
|
- **Type:** `(collectionName: string) => string`
|
||||||
|
- **Default:** `(collectionName) => pascalName(collectionName) + "Record"`
|
||||||
|
|
||||||
|
`nameRecordType` is a function that takes a collection name and returns the name of the generated record type.
|
||||||
|
|
||||||
|
## output
|
||||||
|
|
||||||
|
- **Type:** `string`
|
||||||
|
- **Default:** `./zod-pocketbase.ts`
|
||||||
|
|
||||||
|
`output` represents the path of the generated file.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
{
|
||||||
|
output: "./src/lib/pocketbase/schemas.ts",
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
:::caution
|
||||||
|
`output` must be a relative path.
|
||||||
|
:::
|
||||||
|
|
||||||
|
## url
|
||||||
|
|
||||||
|
- **Type:** `string`
|
||||||
|
- **Default:** `undefined`
|
||||||
|
|
||||||
|
`url` represents the url of your PocketBase instance.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
{
|
||||||
|
url: "https://myproject.pockethost.io",
|
||||||
|
}
|
||||||
|
```
|
||||||
88
doc/src/content/docs/reference/methods.md
Normal file
88
doc/src/content/docs/reference/methods.md
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
---
|
||||||
|
title: Methods
|
||||||
|
description: A reference for the methods.
|
||||||
|
---
|
||||||
|
|
||||||
|
## helpersFrom
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { helpersFrom } from "zod-pocketbase";
|
||||||
|
|
||||||
|
const helpers = helpersFrom({ fetch, pocketbase });
|
||||||
|
```
|
||||||
|
|
||||||
|
The `helpersFrom` method returns an object with two methods: `getRecord` and `getRecords` described below.
|
||||||
|
|
||||||
|
### fetch
|
||||||
|
|
||||||
|
- **Type:** `(url: RequestInfo | URL, config?: RequestInit) => Promise<Response>`
|
||||||
|
|
||||||
|
Optional custom fetch function to use for sending the request.
|
||||||
|
|
||||||
|
### pocketbase
|
||||||
|
|
||||||
|
- **Required**
|
||||||
|
- **Type:** `TypedPocketbase`
|
||||||
|
|
||||||
|
The `pocketbase` parameter is a mandatory parameter that specifies a PocketBase instance.
|
||||||
|
|
||||||
|
## getRecord
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const { getRecord } = helpersFrom({ pocketbase });
|
||||||
|
const record = await getRecord(reference, { schema });
|
||||||
|
```
|
||||||
|
|
||||||
|
The `getRecord` method returns a single record from your PocketBase instance.
|
||||||
|
|
||||||
|
### reference
|
||||||
|
|
||||||
|
- **Required**
|
||||||
|
- **Type:** [`RecordRef`](/reference/types#recordref)
|
||||||
|
|
||||||
|
### schema
|
||||||
|
|
||||||
|
- **Required**
|
||||||
|
- **Type:** [`AnyZodRecord`](/reference/types#anyzodrecord)
|
||||||
|
|
||||||
|
## getRecords
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const { getRecords } = helpersFrom({ pocketbase });
|
||||||
|
const recordsList = await getRecords(collection, { filter, page, perPage, schema, skipTotal, sort });
|
||||||
|
```
|
||||||
|
|
||||||
|
The `getRecords` method returns a records list from your PocketBase instance.
|
||||||
|
|
||||||
|
### collection
|
||||||
|
|
||||||
|
- **Required**
|
||||||
|
- **Type:** `string`
|
||||||
|
|
||||||
|
### filter
|
||||||
|
|
||||||
|
- **Type:** `string`
|
||||||
|
|
||||||
|
### page
|
||||||
|
|
||||||
|
- **Type:** `number`
|
||||||
|
- **Default:** `1`
|
||||||
|
|
||||||
|
### perPage
|
||||||
|
|
||||||
|
- **Type:** `number`
|
||||||
|
- **Default:** `200`
|
||||||
|
|
||||||
|
### schema
|
||||||
|
|
||||||
|
- **Required**
|
||||||
|
- **Type:** [`AnyZodRecord`](/reference/types#anyzodrecord)
|
||||||
|
|
||||||
|
### skipTotal
|
||||||
|
|
||||||
|
- **Type:** `boolean`
|
||||||
|
- **Default:** `true`
|
||||||
|
|
||||||
|
### sort
|
||||||
|
|
||||||
|
- **Type:** [`ZodRecordSort`](/reference/types#zodrecordsort)
|
||||||
80
doc/src/content/docs/reference/types.md
Normal file
80
doc/src/content/docs/reference/types.md
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
---
|
||||||
|
title: Types
|
||||||
|
description: A reference for the types.
|
||||||
|
---
|
||||||
|
|
||||||
|
## RecordIdRef
|
||||||
|
|
||||||
|
`RecordIdRef` represents a reference to a record in a collection by its `id` and its `collection` name.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
type RecordIdRef<C extends string> = { collection: C; id: string };
|
||||||
|
```
|
||||||
|
|
||||||
|
## RecordSlugRef
|
||||||
|
|
||||||
|
`RecordSlugRef` represents a reference to a record in a collection by its `slug` and its `collection` name.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
type RecordSlugRef<C extends string> = { collection: C; slug: string };
|
||||||
|
```
|
||||||
|
|
||||||
|
:::tip[What is slug?]
|
||||||
|
`slug` is not a default field of a collection but it can be used as another unique identifier for a record more readble than its `id`.
|
||||||
|
:::
|
||||||
|
|
||||||
|
## RecordRef
|
||||||
|
|
||||||
|
`RecordRef` represents either a `RecordIdRef` or a `RecordSlugRef`.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
type RecordRef<C extends string> = RecordIdRef<C> | RecordSlugRef<C>;
|
||||||
|
```
|
||||||
|
|
||||||
|
## RecordFullListOpts
|
||||||
|
|
||||||
|
```ts
|
||||||
|
type RecordFullListOpts<S extends AnyZodRecord> = RecordListOpts<S> & { batch?: number };
|
||||||
|
```
|
||||||
|
|
||||||
|
## RecordListOpts
|
||||||
|
|
||||||
|
```ts
|
||||||
|
type RecordListOpts<S extends AnyZodRecord> = {
|
||||||
|
filter?: string;
|
||||||
|
page?: number;
|
||||||
|
perPage?: number;
|
||||||
|
skipTotal?: boolean;
|
||||||
|
sort?: ZodRecordSort<S>;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## AnyZodRecord
|
||||||
|
|
||||||
|
`AnyZodRecord` represents the type for a record schema.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
type AnyZodRecord = AnyZodObject | ZodEffects<AnyZodObject>;
|
||||||
|
```
|
||||||
|
|
||||||
|
## ZodRecordSort
|
||||||
|
|
||||||
|
`ZodRecordSort` represents the type for a record sort options.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export type ZodRecordSort<S extends AnyZodRecord> = `${"+" | "-"}${ZodRecordMainKeys<S>}` | "@random";
|
||||||
|
```
|
||||||
|
|
||||||
|
## RecordsList
|
||||||
|
|
||||||
|
`RecordsList` represents the type for a records list.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export type RecordsList<S extends AnyZodRecord> = {
|
||||||
|
items: S["_output"][];
|
||||||
|
page: number;
|
||||||
|
perPage: number;
|
||||||
|
totalItems: number;
|
||||||
|
totalPages: number;
|
||||||
|
};
|
||||||
|
```
|
||||||
32
doc/src/content/docs/start-here/getting-started.mdx
Normal file
32
doc/src/content/docs/start-here/getting-started.mdx
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
---
|
||||||
|
title: Getting started
|
||||||
|
description: This library adds Zod safety with ease to your PocketBase instance.
|
||||||
|
---
|
||||||
|
import { Tabs, TabItem } from '@astrojs/starlight/components';
|
||||||
|
|
||||||
|
This library adds Zod safety with ease to your PocketBase instance. It provides you with:
|
||||||
|
- a CLI to generate schemas for your selected collections
|
||||||
|
- methods to make pocketbase SDK typesafe
|
||||||
|
- helpers to simplify the way you fetch your collections
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
To install `zod-pocketbase`, run the following from your project directory:
|
||||||
|
|
||||||
|
<Tabs>
|
||||||
|
<TabItem label="npm">
|
||||||
|
```shell
|
||||||
|
npm install zod-pocketbase
|
||||||
|
```
|
||||||
|
</TabItem>
|
||||||
|
<TabItem label="pnpm">
|
||||||
|
```shell
|
||||||
|
pnpm add zod-pocketbase
|
||||||
|
```
|
||||||
|
</TabItem>
|
||||||
|
<TabItem label="yarn">
|
||||||
|
```shell
|
||||||
|
yarn install zod-pocketbase
|
||||||
|
```
|
||||||
|
</TabItem>
|
||||||
|
</Tabs>
|
||||||
202
doc/src/content/docs/start-here/why.md
Normal file
202
doc/src/content/docs/start-here/why.md
Normal file
@@ -0,0 +1,202 @@
|
|||||||
|
---
|
||||||
|
title: Why this library?
|
||||||
|
description: A guide in my new Starlight docs site.
|
||||||
|
---
|
||||||
|
|
||||||
|
You want to get the last 10 updated posts from your PocketBase collection. So you start with...
|
||||||
|
|
||||||
|
## The PocketBase SDK
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import PocketBase from "pocketbase";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
const pocketbase = new PocketBase("https://my-pocketbase.com");
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
sort: "-updated"
|
||||||
|
};
|
||||||
|
|
||||||
|
const { items: firstPosts } = await pocketbase.collection("posts").getList(1, 10, options);
|
||||||
|
```
|
||||||
|
|
||||||
|
Oops, you forgot to expand the `author` field because you don't want the author id but the author name. So you...
|
||||||
|
|
||||||
|
## Expand
|
||||||
|
|
||||||
|
```diff lang="ts"
|
||||||
|
import PocketBase from "pocketbase";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
const pocketbase = new PocketBase("https://my-pocketbase.com");
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
sort: "-updated",
|
||||||
|
+ expand: ["author"],
|
||||||
|
};
|
||||||
|
|
||||||
|
const { items: firstPosts } = await pocketbase.collection("posts").getList(1, 10, options);
|
||||||
|
```
|
||||||
|
|
||||||
|
Great, but now you want to validate the data you get from PocketBase because the rule is that you should not trust anything for the outer world.
|
||||||
|
You also just include the fields that you really need for your app and remove the ugly `expand` properties. So you add...
|
||||||
|
|
||||||
|
## A Zod Schema
|
||||||
|
|
||||||
|
```diff lang="ts"
|
||||||
|
import PocketBase from "pocketbase";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
const pocketbase = new PocketBase("https://my-pocketbase.com");
|
||||||
|
|
||||||
|
+ const Post = z
|
||||||
|
+ .object({
|
||||||
|
+ content: z.string(),
|
||||||
|
+ expand: z.object({
|
||||||
|
+ author: z.object({
|
||||||
|
+ name: z.string(),
|
||||||
|
+ }),
|
||||||
|
+ }),
|
||||||
|
+ title: z.string(),
|
||||||
|
+ })
|
||||||
|
+ .transform(({ expand, ...rest }) => ({ ...rest, ...expand }));
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
sort: "-updated",
|
||||||
|
expand: ["author"],
|
||||||
|
};
|
||||||
|
|
||||||
|
const { items } = await pocketbase.collection("posts").getList(1, 10, options);
|
||||||
|
+ const firstPosts = Post.array().parse(items);
|
||||||
|
```
|
||||||
|
|
||||||
|
Better, but even if you get your validated structured data now, you could reduce the size of the response from the server by adding...
|
||||||
|
|
||||||
|
## Fields
|
||||||
|
|
||||||
|
```diff lang="ts"
|
||||||
|
import PocketBase from "pocketbase";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
const pocketbase = new PocketBase("https://my-pocketbase.com");
|
||||||
|
|
||||||
|
const Post = z
|
||||||
|
.object({
|
||||||
|
content: z.string(),
|
||||||
|
expand: z.object({
|
||||||
|
author: z.object({
|
||||||
|
name: z.string(),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
title: z.string(),
|
||||||
|
})
|
||||||
|
.transform(({ expand, ...rest }) => ({ ...rest, ...expand }));
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
sort: "-updated",
|
||||||
|
expand: ["author"],
|
||||||
|
+ fields: ["content", "expand.author.name", "title"],
|
||||||
|
};
|
||||||
|
|
||||||
|
const { items } = await pocketbase.collection("posts").getList(1, 10, options);
|
||||||
|
const firstPosts = Post.array().parse(items);
|
||||||
|
```
|
||||||
|
|
||||||
|
Cool! So what would you need Zod PocketBase for?
|
||||||
|
|
||||||
|
## Generated Schemas
|
||||||
|
|
||||||
|
```diff lang="ts"
|
||||||
|
import PocketBase from "pocketbase";
|
||||||
|
import { z } from "zod";
|
||||||
|
+ import { pick, select } from "zod-pocketbase";
|
||||||
|
+ import { AuthorRecord, PostRecord } from "./schemas";
|
||||||
|
|
||||||
|
const pocketbase = new PocketBase("https://my-pocketbase.com");
|
||||||
|
|
||||||
|
- const Post = z
|
||||||
|
- .object({
|
||||||
|
- content: z.string(),
|
||||||
|
- expand: z.object({
|
||||||
|
- author: z.object({
|
||||||
|
- name: z.string(),
|
||||||
|
- }),
|
||||||
|
- }),
|
||||||
|
- title: z.string(),
|
||||||
|
- })
|
||||||
|
- .transform(({ expand, ...rest }) => ({ ...rest, ...expand }));
|
||||||
|
|
||||||
|
+ const Post = PostRecord.pick({content: true, title: true }).extend({
|
||||||
|
+ expand: z.object({
|
||||||
|
+ author: AuthorRecord.pick({ name: true })
|
||||||
|
+ }),
|
||||||
|
+ }).transform(({ expand, ...rest }) => ({ ...rest, ...expand }));
|
||||||
|
|
||||||
|
/* Or, for the syntactic sugar addicts */
|
||||||
|
+ const Post = select(PostRecord, ["content", "title"], {
|
||||||
|
+ author: select(AuthorRecord, ["name"])
|
||||||
|
+ });
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
sort: "-updated",
|
||||||
|
expand: ["author"],
|
||||||
|
fields: ["content", "expand.author.name", "title"],
|
||||||
|
};
|
||||||
|
|
||||||
|
const { items } = await pocketbase.collection("posts").getList(1, 10, options);
|
||||||
|
const firstPosts = Post.array().parse(items);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Type-safe expand and fields options
|
||||||
|
|
||||||
|
```diff lang="ts"
|
||||||
|
import PocketBase from "pocketbase";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { select } from "zod-pocketbase";
|
||||||
|
+ import { expandFrom, fieldsFrom, listOptionsFrom } from "zod-pocketbase";
|
||||||
|
import { AuthorRecord, PostRecord } from "./schemas";
|
||||||
|
|
||||||
|
const pocketbase = new PocketBase("https://my-pocketbase.com");
|
||||||
|
|
||||||
|
const Post = select(PostRecord, ["content", "title"], {
|
||||||
|
author: select(AuthorRecord, ["name"])
|
||||||
|
});
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
sort: "-updated",
|
||||||
|
- expand: ["author"],
|
||||||
|
+ expand: expandFrom(Post),
|
||||||
|
- fields: ["content", "expand.author.name", "title"],
|
||||||
|
+ fields: fieldsFrom(Post),
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Or, for the syntactic sugar addicts */
|
||||||
|
+ const options = listOptionsFrom(Post, { sort: "-updated" });
|
||||||
|
|
||||||
|
const { items } = await pocketbase.collection("posts").getList(1, 10, options);
|
||||||
|
const firstPosts = Post.array().parse(items);
|
||||||
|
```
|
||||||
|
|
||||||
|
## More sugar with helpers
|
||||||
|
|
||||||
|
```diff lang="ts"
|
||||||
|
import PocketBase from "pocketbase";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { select } from "zod-pocketbase";
|
||||||
|
+ import { helpersFrom } from "zod-pocketbase";
|
||||||
|
import { AuthorRecord, PostRecord } from "./schemas";
|
||||||
|
|
||||||
|
const pocketbase = new PocketBase("https://my-pocketbase.com");
|
||||||
|
+ const { getRecords } = helpersFrom({ pocketbase });
|
||||||
|
|
||||||
|
const Post = select(PostRecord, ["content", "title"], {
|
||||||
|
author: select(AuthorRecord, ["name"])
|
||||||
|
});
|
||||||
|
|
||||||
|
- const options = optionsFrom(Post, { sort: "-updated" });
|
||||||
|
+ const options = { perPage: 10, schema: Post, sort: "-updated" };
|
||||||
|
|
||||||
|
- const { items } = await pocketbase.collection("posts").getList(1, 10, options);
|
||||||
|
- const firstPosts = Post.array().parse(items);
|
||||||
|
+ const { items: firstPosts } = getRecords("posts", options);
|
||||||
|
```
|
||||||
2
doc/src/env.d.ts
vendored
Normal file
2
doc/src/env.d.ts
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
/// <reference path="../.astro/types.d.ts" />
|
||||||
|
/// <reference types="astro/client" />
|
||||||
3
doc/tsconfig.json
Normal file
3
doc/tsconfig.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"extends": "astro/tsconfigs/strict"
|
||||||
|
}
|
||||||
3
eslint.config.mjs
Normal file
3
eslint.config.mjs
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import astro from "eslint-plugin-astro";
|
||||||
|
|
||||||
|
export default [...astro.configs["flat/recommended"], ...astro.configs["flat/jsx-a11y-strict"]];
|
||||||
26
package.json
Normal file
26
package.json
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"name": "root",
|
||||||
|
"private": true,
|
||||||
|
"packageManager": "pnpm@9.9.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.20.3"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"doc:dev": "pnpm --filter doc dev",
|
||||||
|
"package:dev": "pnpm --filter zod-pocketbase dev",
|
||||||
|
"playground:dev": "pnpm --filter playground dev",
|
||||||
|
"playground:gen": "pnpm --filter playground generate",
|
||||||
|
"changeset": "changeset",
|
||||||
|
"release": "node scripts/release.mjs"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@changesets/cli": "^2.27.11",
|
||||||
|
"@typescript-eslint/parser": "^8.18.1",
|
||||||
|
"eslint": "^9.17.0",
|
||||||
|
"eslint-plugin-astro": "^1.3.1",
|
||||||
|
"eslint-plugin-jsx-a11y": "^6.10.2",
|
||||||
|
"prettier": "^3.4.2",
|
||||||
|
"prettier-plugin-astro": "^0.14.1",
|
||||||
|
"prettier-plugin-tailwindcss": "^0.6.9"
|
||||||
|
}
|
||||||
|
}
|
||||||
1
package/.gitignore
vendored
Normal file
1
package/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
dist
|
||||||
106
package/CHANGELOG.md
Normal file
106
package/CHANGELOG.md
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
# zod-pocketbase
|
||||||
|
|
||||||
|
## 0.5.0
|
||||||
|
|
||||||
|
### Minor Changes
|
||||||
|
|
||||||
|
- remove "cache" option for "helpersFrom" and replace it with "fetch" to allow a more generic custom fetch and remove "@11ty/eleventy-fetch" dependency
|
||||||
|
|
||||||
|
## 0.4.1
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- update packages
|
||||||
|
- fix json import
|
||||||
|
|
||||||
|
## 0.4.0
|
||||||
|
|
||||||
|
### Minor Changes
|
||||||
|
|
||||||
|
- separate files that use node packages in server directory
|
||||||
|
|
||||||
|
## 0.3.8
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- fix cached version
|
||||||
|
|
||||||
|
## 0.3.7
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- d5481dc: fix getRecords return type
|
||||||
|
|
||||||
|
## 0.3.6
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- a8d3098: add recordsListFrom method to construct a records list schema from a record schema
|
||||||
|
- a8d3098: fix getRecords helper return type
|
||||||
|
|
||||||
|
## 0.3.5
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- optimize SKD calls and options
|
||||||
|
|
||||||
|
## 0.3.4
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- rename out to output
|
||||||
|
|
||||||
|
## 0.3.3
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- refactor code
|
||||||
|
|
||||||
|
## 0.3.2
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- fix syntax
|
||||||
|
|
||||||
|
## 0.3.1
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- fix generated syntax
|
||||||
|
|
||||||
|
## 0.3.0
|
||||||
|
|
||||||
|
### Minor Changes
|
||||||
|
|
||||||
|
- refactor CLI
|
||||||
|
|
||||||
|
## 0.2.4
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- 03555e2: fix optional expand in expand method
|
||||||
|
|
||||||
|
## 0.2.3
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- cdf2b59: fix the transformation when expand is undefined
|
||||||
|
|
||||||
|
## 0.2.2
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- 6980455: fix the case when expand only contains keys with optional schemas
|
||||||
|
|
||||||
|
## 0.2.1
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- be3d479: add cache option to helpers
|
||||||
|
|
||||||
|
## 0.2.0
|
||||||
|
|
||||||
|
### Minor Changes
|
||||||
|
|
||||||
|
- 5435793: rename getHelpers to helpersFrom
|
||||||
|
- 5435793: add collectionName literal to schema
|
||||||
42
package/assets/stubs/index.ts
Normal file
42
package/assets/stubs/index.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import type Pocketbase from "pocketbase";
|
||||||
|
import type { RecordService } from "pocketbase";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
/******* ENUMS *******/
|
||||||
|
export const collectionValues = @@_COLLECTION_NAMES_@@ as const;
|
||||||
|
export const Collection = z.enum(collectionValues);
|
||||||
|
export type Collection = z.infer<typeof Collection>;
|
||||||
|
export const COLLECTION = Collection.enum;
|
||||||
|
|
||||||
|
@@_ENUMS_@@
|
||||||
|
|
||||||
|
/******* BASE *******/
|
||||||
|
export const BaseModel = z.object({
|
||||||
|
created: z.string().pipe(z.coerce.date()),
|
||||||
|
id: z.string(),
|
||||||
|
updated: z.string().pipe(z.coerce.date()),
|
||||||
|
});
|
||||||
|
export type BaseModel = z.infer<typeof BaseModel>;
|
||||||
|
|
||||||
|
export const AdminModel = z.object({
|
||||||
|
...BaseModel.shape,
|
||||||
|
avatar: z.string(),
|
||||||
|
email: z.string().email(),
|
||||||
|
});
|
||||||
|
export type AdminModel = z.infer<typeof AdminModel>;
|
||||||
|
|
||||||
|
export const RecordModel = z.object({
|
||||||
|
...BaseModel.shape,
|
||||||
|
collectionId: z.string(),
|
||||||
|
collectionName: z.string(),
|
||||||
|
expand: z.any().optional(),
|
||||||
|
});
|
||||||
|
export type RecordModel = z.infer<typeof RecordModel>;
|
||||||
|
|
||||||
|
/******* RECORDS *******/
|
||||||
|
@@_RECORDS_@@
|
||||||
|
|
||||||
|
/******* CLIENT *******/
|
||||||
|
export type TypedPocketbase = Pocketbase & {
|
||||||
|
@@_SERVICES_@@
|
||||||
|
};
|
||||||
63
package/package.json
Normal file
63
package/package.json
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
{
|
||||||
|
"name": "zod-pocketbase",
|
||||||
|
"version": "0.5.0",
|
||||||
|
"description": "",
|
||||||
|
"author": {
|
||||||
|
"email": "gregory.bouteiller@niama.re",
|
||||||
|
"name": "Gregory Bouteiller",
|
||||||
|
"url": "https://github.com/gbouteiller"
|
||||||
|
},
|
||||||
|
"license": "MIT",
|
||||||
|
"keywords": [
|
||||||
|
"pocketbase",
|
||||||
|
"schemas",
|
||||||
|
"typescript",
|
||||||
|
"typegen",
|
||||||
|
"type generation",
|
||||||
|
"zod"
|
||||||
|
],
|
||||||
|
"homepage": "https://github.com/gbouteiller/zod-pocketbase",
|
||||||
|
"publishConfig": {
|
||||||
|
"access": "public"
|
||||||
|
},
|
||||||
|
"type": "module",
|
||||||
|
"sideEffects": false,
|
||||||
|
"main": "dist/index.js",
|
||||||
|
"bin": {
|
||||||
|
"zod-pocketbase": "dist/server/cli.js"
|
||||||
|
},
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"types": "./dist/index.d.ts",
|
||||||
|
"default": "./dist/index.js"
|
||||||
|
},
|
||||||
|
"./server": {
|
||||||
|
"types": "./dist/server/index.d.ts",
|
||||||
|
"default": "./dist/server/index.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"dist",
|
||||||
|
"assets"
|
||||||
|
],
|
||||||
|
"scripts": {
|
||||||
|
"dev": "tsup --watch",
|
||||||
|
"build": "tsup"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@clack/prompts": "^0.9.0",
|
||||||
|
"c12": "^2.0.1",
|
||||||
|
"citty": "^0.1.6",
|
||||||
|
"es-toolkit": "^1.30.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^22.10.2",
|
||||||
|
"pocketbase": "<0.22.0",
|
||||||
|
"tsup": "^8.3.5",
|
||||||
|
"zod": "^3.24.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"pocketbase": "<0.22.0",
|
||||||
|
"zod": "^3.23.8"
|
||||||
|
}
|
||||||
|
}
|
||||||
1209
package/pnpm-lock.yaml
generated
Normal file
1209
package/pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
66
package/src/config.ts
Normal file
66
package/src/config.ts
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import { pascalCase, snakeCase } from "es-toolkit";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default config values.
|
||||||
|
*/
|
||||||
|
export const defaultConfig = {
|
||||||
|
ignore: [],
|
||||||
|
nameEnum: (name: string) => snakeCase(name).toUpperCase(),
|
||||||
|
nameEnumField: (collectionName: string, fieldName: string) => `${collectionName}${pascalCase(fieldName)}`,
|
||||||
|
nameEnumSchema: (name: string) => pascalCase(name),
|
||||||
|
nameEnumType: (name: string) => pascalCase(name),
|
||||||
|
nameEnumValues: (name: string) => `${name}Values`,
|
||||||
|
nameRecordSchema: (name: string) => `${pascalCase(name)}Record`,
|
||||||
|
nameRecordType: (name: string) => `${pascalCase(name)}Record`,
|
||||||
|
output: "./zod-pocketbase.ts",
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Schema for the PocketBase credentials.
|
||||||
|
*/
|
||||||
|
export const Credentials = z.object({
|
||||||
|
adminEmail: z.string().email(),
|
||||||
|
adminPassword: z.string(),
|
||||||
|
url: z.string().url(),
|
||||||
|
});
|
||||||
|
export type Credentials = z.infer<typeof Credentials>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Schema for the config file.
|
||||||
|
*/
|
||||||
|
export const Config = z.object({
|
||||||
|
...Credentials.partial().shape,
|
||||||
|
ignore: z.string().array().default(defaultConfig.ignore),
|
||||||
|
nameEnum: z
|
||||||
|
.function(z.tuple([z.string()]), z.string())
|
||||||
|
.optional()
|
||||||
|
.transform((f) => f ?? defaultConfig.nameEnum),
|
||||||
|
nameEnumField: z
|
||||||
|
.function(z.tuple([z.string(), z.string()]), z.string())
|
||||||
|
.optional()
|
||||||
|
.transform((f) => f ?? defaultConfig.nameEnumField),
|
||||||
|
nameEnumSchema: z
|
||||||
|
.function(z.tuple([z.string()]), z.string())
|
||||||
|
.optional()
|
||||||
|
.transform((f) => f ?? defaultConfig.nameEnumSchema),
|
||||||
|
nameEnumType: z
|
||||||
|
.function(z.tuple([z.string()]), z.string())
|
||||||
|
.optional()
|
||||||
|
.transform((f) => f ?? defaultConfig.nameEnumType),
|
||||||
|
nameEnumValues: z
|
||||||
|
.function(z.tuple([z.string()]), z.string())
|
||||||
|
.optional()
|
||||||
|
.transform((f) => f ?? defaultConfig.nameEnumValues),
|
||||||
|
nameRecordSchema: z
|
||||||
|
.function(z.tuple([z.string()]), z.string())
|
||||||
|
.optional()
|
||||||
|
.transform((f) => f ?? defaultConfig.nameRecordSchema),
|
||||||
|
nameRecordType: z
|
||||||
|
.function(z.tuple([z.string()]), z.string())
|
||||||
|
.optional()
|
||||||
|
.transform((f) => f ?? defaultConfig.nameRecordType),
|
||||||
|
output: z.string().default(defaultConfig.output),
|
||||||
|
});
|
||||||
|
export type Config = z.input<typeof Config>;
|
||||||
|
export type ResolvedConfig = z.infer<typeof Config>;
|
||||||
120
package/src/content.ts
Normal file
120
package/src/content.ts
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
import { sortBy } from "es-toolkit";
|
||||||
|
import type { CollectionModel, SchemaField } from "pocketbase";
|
||||||
|
import type { GenerateOpts } from "./server/utils.ts";
|
||||||
|
|
||||||
|
export function stringifyContent(collections: CollectionModel[], opts: GenerateOpts) {
|
||||||
|
function getCollectionSelectFields() {
|
||||||
|
return collections.flatMap((collection) =>
|
||||||
|
collection.schema
|
||||||
|
.filter((field) => field.type === "select")
|
||||||
|
.map((field) => ({ name: opts.nameEnumField(collection.name, field.name), values: (field.options.values ?? []) as string[] })),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function stringifyEnum({ name, values }: SelectField) {
|
||||||
|
const valuesName = opts.nameEnumValues(name);
|
||||||
|
const schemaName = opts.nameEnumSchema(name);
|
||||||
|
const enumName = opts.nameEnum(name);
|
||||||
|
const typeName = opts.nameEnumType(name);
|
||||||
|
return `export const ${valuesName} = [\n\t${values.map((value) => `"${value}"`).join(",\n\t")},\n] as const;\nexport const ${schemaName} = z.enum(${valuesName});\nexport type ${typeName} = z.infer<typeof ${schemaName}>;\nexport const ${enumName} = ${schemaName}.enum;`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function stringifyRecord({ name, schema }: CollectionModel) {
|
||||||
|
const schemaName = opts.nameRecordSchema(name);
|
||||||
|
const typeName = opts.nameRecordType(name);
|
||||||
|
const fields = sortBy(schema, ["name"]).map((field) => stringifyField(field, name));
|
||||||
|
return `export const ${schemaName} = z.object({\n\t...RecordModel.omit({ expand: true }).shape,\n\tcollectionName: z.literal("${name}"),\n\t${fields.join(",\n\t")},\n});\nexport type ${typeName} = z.infer<typeof ${schemaName}>;`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function stringifyField(field: SchemaField, collectionName: string) {
|
||||||
|
let schema: string | undefined;
|
||||||
|
if (field.type === "bool") schema = stringifyBoolField(field);
|
||||||
|
else if (field.type === "date") schema = stringifyDateField(field);
|
||||||
|
else if (field.type === "editor") schema = stringifyEditorField(field);
|
||||||
|
else if (field.type === "email") schema = stringifyEmailField(field);
|
||||||
|
else if (field.type === "file") schema = stringifyFileField(field);
|
||||||
|
else if (field.type === "json") schema = stringifyJsonField(field);
|
||||||
|
else if (field.type === "number") schema = stringifyNumberField(field);
|
||||||
|
else if (field.type === "relation") schema = stringifyRelationField(field);
|
||||||
|
else if (field.type === "select") schema = stringifySelectField(field, collectionName);
|
||||||
|
else if (field.type === "text") schema = stringifyTextField(field);
|
||||||
|
else if (field.type === "url") schema = stringifyUrlField(field);
|
||||||
|
// TODO: manage unknown field type
|
||||||
|
return `${field.name}: ${schema}${field.required ? "" : ".optional()"}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function stringifyBoolField(_: SchemaField) {
|
||||||
|
return "z.boolean()";
|
||||||
|
}
|
||||||
|
|
||||||
|
function stringifyDateField(_field: SchemaField) {
|
||||||
|
// TODO: implement min and max
|
||||||
|
return "z.string().pipe(z.coerce.date())";
|
||||||
|
}
|
||||||
|
|
||||||
|
function stringifyEditorField(_field: SchemaField) {
|
||||||
|
// TODO: implement convertUrls
|
||||||
|
return "z.string()";
|
||||||
|
}
|
||||||
|
|
||||||
|
function stringifyEmailField(_field: SchemaField) {
|
||||||
|
// TODO: implement exceptDomains and onlyDomains
|
||||||
|
return "z.string().email()";
|
||||||
|
}
|
||||||
|
|
||||||
|
function stringifyFileField({ options: { maxSelect } }: SchemaField) {
|
||||||
|
// TODO: implement maxSize, mimeTypes, protected, thumbs
|
||||||
|
return `z.string()${maxSelect === 1 ? "" : `.array().max(${maxSelect})`}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function stringifyJsonField(_field: SchemaField) {
|
||||||
|
// TODO: implement maxSize and json schema
|
||||||
|
return "z.any()";
|
||||||
|
}
|
||||||
|
|
||||||
|
function stringifyNumberField({ options: { max, min, noDecimal } }: SchemaField) {
|
||||||
|
return `z.number()${noDecimal ? ".int()" : ""}${min ? `.min(${min})` : ""}${max ? `.max(${max})` : ""}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function stringifyRelationField({ options, required }: SchemaField) {
|
||||||
|
const { maxSelect, minSelect } = options;
|
||||||
|
// TODO: implement cascadeDelete, displayFields
|
||||||
|
const min = minSelect ? `.min(${minSelect})` : "";
|
||||||
|
const max = maxSelect ? `.max(${maxSelect})` : "";
|
||||||
|
const multiple = maxSelect === 1 ? "" : `.array()${min}${max}`;
|
||||||
|
const isOptional = required || maxSelect !== 1 ? `` : `.transform((id) => id === "" ? undefined : id)`;
|
||||||
|
return `z.string()${isOptional}${multiple}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function stringifySelectField({ name, options: { maxSelect } }: SchemaField, collectionName: string) {
|
||||||
|
// TODO: implement values
|
||||||
|
return `${opts.nameEnumSchema(opts.nameEnumField(collectionName, name))}${maxSelect === 1 ? "" : `.array().max(${maxSelect})`}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function stringifyTextField({ options: { max, min } }: SchemaField) {
|
||||||
|
// TODO: implement pattern
|
||||||
|
return `z.string()${min ? `.min(${min})` : ""}${max ? `.max(${max})` : ""}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function stringifyUrlField(_field: SchemaField) {
|
||||||
|
// TODO: implement exceptDomains and onlyDomains
|
||||||
|
return "z.string().url()";
|
||||||
|
}
|
||||||
|
|
||||||
|
function stringifySchemasEntry({ name }: CollectionModel) {
|
||||||
|
return `["${name}", ${opts.nameRecordSchema(name)}]`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function stringifyService({ name }: CollectionModel) {
|
||||||
|
return `\t\tcollection(idOrName: "${name}"): RecordService<z.input<typeof ${opts.nameRecordSchema(name)}>>;`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
collectionNames: `[\n\t${collections.map(({ name }) => `"${name}"`).join(",\n\t")},\n]`,
|
||||||
|
enums: getCollectionSelectFields().map(stringifyEnum).join("\n\n"),
|
||||||
|
records: `${collections.map(stringifyRecord).join("\n\n")}\n\nexport const records = new Map<Collection, z.AnyZodObject>([\n\t${collections.map(stringifySchemasEntry).join(",\n\t")},\n]);`,
|
||||||
|
services: collections.map(stringifyService).join("\n"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SelectField = { name: string; values: string[] };
|
||||||
29
package/src/helpers.ts
Normal file
29
package/src/helpers.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import type { default as Pocketbase, SendOptions } from "pocketbase";
|
||||||
|
import { fullListOptionsFrom, optionsFrom } from "./options.js";
|
||||||
|
import type { AnyZodRecord, RecordFullListOpts, RecordIdRef, RecordRef, RecordSlugRef } from "./types.ts";
|
||||||
|
import { AnyRecordsList, type RecordsList } from "./schemas.ts";
|
||||||
|
|
||||||
|
export function helpersFrom({ fetch, pocketbase }: HelpersFromOpts) {
|
||||||
|
async function getRecord<C extends string, S extends AnyZodRecord>(ref: RecordSlugRef<C>, opts: GetRecordOpts<S>): Promise<S["_output"]>;
|
||||||
|
async function getRecord<C extends string, S extends AnyZodRecord>(ref: RecordIdRef<C>, opts: GetRecordOpts<S>): Promise<S["_output"]>;
|
||||||
|
async function getRecord<C extends string, S extends AnyZodRecord>(ref: RecordRef<C>, opts: GetRecordOpts<S>) {
|
||||||
|
const { schema } = opts;
|
||||||
|
const sdkOpts = { ...optionsFrom(schema), ...(fetch ? { fetch } : {}) };
|
||||||
|
const unsafeRecord = await ("id" in ref
|
||||||
|
? pocketbase.collection(ref.collection).getOne(ref.id, sdkOpts)
|
||||||
|
: pocketbase.collection(ref.collection).getFirstListItem(`slug = "${ref.slug}"`, sdkOpts));
|
||||||
|
return schema.parseAsync(unsafeRecord);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getRecords<C extends string, S extends AnyZodRecord>(collection: C, opts: GetRecordsOpts<S>): Promise<RecordsList<S>> {
|
||||||
|
const { schema, ...otherOpts } = opts;
|
||||||
|
const sdkOpts = { ...fullListOptionsFrom(schema, otherOpts), ...(fetch ? { fetch } : {}) };
|
||||||
|
const recordsList = await pocketbase.collection(collection).getList(sdkOpts.page, sdkOpts.perPage, sdkOpts);
|
||||||
|
return AnyRecordsList.extend({ items: schema.array() }).parseAsync(recordsList);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { getRecord, getRecords };
|
||||||
|
}
|
||||||
|
export type GetRecordOpts<S extends AnyZodRecord> = { schema: S };
|
||||||
|
export type GetRecordsOpts<S extends AnyZodRecord> = RecordFullListOpts<S> & { schema: S };
|
||||||
|
export type HelpersFromOpts = { fetch?: SendOptions["fetch"]; pocketbase: Pocketbase };
|
||||||
6
package/src/index.ts
Normal file
6
package/src/index.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export * from "./config.js";
|
||||||
|
export * from "./helpers.js";
|
||||||
|
export * from "./options.js";
|
||||||
|
export * from "./schemas.js";
|
||||||
|
export * from "./types.ts";
|
||||||
|
export * from "./utils.js";
|
||||||
97
package/src/options.ts
Normal file
97
package/src/options.ts
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
import { z, type AnyZodObject, type ZodTypeAny } from "zod";
|
||||||
|
import type { AnyZodRecord, RecordFullListOpts, RecordListOpts } from "./types.ts";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extends the given schema with the given expansion.
|
||||||
|
* @param schema - The original schema
|
||||||
|
* @param shape - The shape of the expansion
|
||||||
|
* @returns A new schema extended with the given expansion
|
||||||
|
*/
|
||||||
|
export function expandFrom<S extends AnyZodRecord>(schema: S) {
|
||||||
|
return expandFromRec(schema)
|
||||||
|
.sort((k1, k2) => (k1 < k2 ? -1 : 1))
|
||||||
|
.join(",");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extends the given schema with the given expansion.
|
||||||
|
* @param schema - The original schema
|
||||||
|
* @param shape - The shape of the expansion
|
||||||
|
* @returns A new schema extended with the given expansion
|
||||||
|
*/
|
||||||
|
export function fieldsFrom<S extends AnyZodRecord>(schema: S) {
|
||||||
|
return fieldsFromRec(schema)
|
||||||
|
.sort((k1, k2) => (k1 < k2 ? -1 : 1))
|
||||||
|
.join(",");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extends the given schema with the given expansion.
|
||||||
|
* @param schema - The original schema
|
||||||
|
* @param shape - The shape of the expansion
|
||||||
|
* @returns A new schema extended with the given expansion
|
||||||
|
*/
|
||||||
|
export function optionsFrom<S extends AnyZodRecord>(schema: S) {
|
||||||
|
return { expand: expandFrom(schema), fields: fieldsFrom(schema) };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extends the given schema with the given expansion.
|
||||||
|
* @param schema - The original schema
|
||||||
|
* @param shape - The shape of the expansion
|
||||||
|
* @returns A new schema extended with the given expansion
|
||||||
|
*/
|
||||||
|
export function listOptionsFrom<S extends AnyZodRecord>(schema: S, opts: RecordListOpts<S>) {
|
||||||
|
const { page = 1, perPage = 30, ...rest } = opts;
|
||||||
|
return { ...optionsFrom(schema), page, perPage, ...rest };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extends the given schema with the given expansion.
|
||||||
|
* @param schema - The original schema
|
||||||
|
* @param shape - The shape of the expansion
|
||||||
|
* @returns A new schema extended with the given expansion
|
||||||
|
*/
|
||||||
|
export function fullListOptionsFrom<S extends AnyZodRecord>(schema: S, opts: RecordFullListOpts<S>) {
|
||||||
|
const { page = 1, perPage = 200, skipTotal = true, ...rest } = opts;
|
||||||
|
return listOptionsFrom(schema, { page, perPage, skipTotal, ...rest });
|
||||||
|
}
|
||||||
|
|
||||||
|
function expandFromRec<S extends ZodTypeAny>(schema: S, prefix = "") {
|
||||||
|
let expands: string[] = [];
|
||||||
|
const shape = getObjectSchemaDescendant(schema)?.shape;
|
||||||
|
if (!shape || !("expand" in shape)) return [];
|
||||||
|
for (const [key, value] of Object.entries(getObjectSchemaDescendant(shape.expand)!.shape)) {
|
||||||
|
expands = [...expands, `${prefix}${key}`];
|
||||||
|
if (hasObjectSchemaDescendant(value)) expands = [...expands, ...expandFromRec(getObjectSchemaDescendant(value)!, `${prefix}${key}.`)];
|
||||||
|
}
|
||||||
|
return expands;
|
||||||
|
}
|
||||||
|
|
||||||
|
function fieldsFromRec<S extends z.ZodTypeAny>(schema: S, prefix = "") {
|
||||||
|
let fields: string[] = [];
|
||||||
|
const shape = getObjectSchemaDescendant(schema)?.shape;
|
||||||
|
if (!shape) return [];
|
||||||
|
for (const [key, value] of Object.entries(shape)) {
|
||||||
|
fields = [
|
||||||
|
...fields,
|
||||||
|
...(hasObjectSchemaDescendant(value) ? fieldsFromRec(getObjectSchemaDescendant(value)!, `${prefix}${key}.`) : [`${prefix}${key}`]),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
return fields.sort((k1, k2) => (k1 < k2 ? -1 : 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasObjectSchemaDescendant(value: unknown): value is z.ZodTypeAny {
|
||||||
|
if (value instanceof z.ZodEffects) return hasObjectSchemaDescendant(value.innerType());
|
||||||
|
if (value instanceof z.ZodArray) return hasObjectSchemaDescendant(value.element);
|
||||||
|
if (value instanceof z.ZodOptional) return hasObjectSchemaDescendant(value.unwrap());
|
||||||
|
return value instanceof z.ZodObject;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getObjectSchemaDescendant<S extends z.ZodTypeAny>(schema: S): AnyZodObject | undefined {
|
||||||
|
if (schema instanceof z.ZodEffects) return getObjectSchemaDescendant(schema.innerType());
|
||||||
|
if (schema instanceof z.ZodArray) return getObjectSchemaDescendant(schema.element);
|
||||||
|
if (schema instanceof z.ZodOptional) return getObjectSchemaDescendant(schema.unwrap());
|
||||||
|
if (schema instanceof z.ZodObject) return schema;
|
||||||
|
return;
|
||||||
|
}
|
||||||
71
package/src/schemas.ts
Normal file
71
package/src/schemas.ts
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
import { type AnyZodObject, type objectUtil, z, type ZodEffects, type ZodObject, ZodOptional, type ZodRawShape } from "zod";
|
||||||
|
import type { AnyZodRecord, HasRequiredKeys, ZodRecordKeys } from "./types.ts";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Records list schema.
|
||||||
|
*/
|
||||||
|
export const AnyRecordsList = z.object({
|
||||||
|
items: z.any().array(),
|
||||||
|
page: z.number().int().min(1),
|
||||||
|
perPage: z.number().int().min(1),
|
||||||
|
totalItems: z.number().int().min(-1),
|
||||||
|
totalPages: z.number().int().min(-1),
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extends the given schema with the given expansion.
|
||||||
|
* @param schema - The original schema
|
||||||
|
* @param shape - The shape of the expansion
|
||||||
|
* @returns A new schema extended with the given expansion
|
||||||
|
*/
|
||||||
|
export function expand<S extends AnyZodObject, E extends ZodRawShape>(schema: S, shape: E) {
|
||||||
|
const isExpandOptional = Object.entries(shape).every(([, value]) => value instanceof z.ZodOptional);
|
||||||
|
return z
|
||||||
|
.object({ ...schema.shape, expand: isExpandOptional ? z.object(shape).optional() : z.object(shape) })
|
||||||
|
.transform(({ expand, ...rest }) => ({ ...rest, ...(expand ?? {}) })) as ZodObjectExpand<S, E>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Picks the given keys from the given schema.
|
||||||
|
* @param schema - The original schema
|
||||||
|
* @param keys - The keys to keep
|
||||||
|
* @returns A new schema with only the given keys
|
||||||
|
*/
|
||||||
|
export function pick<S extends AnyZodObject, K extends ZodRecordKeys<S>[]>(schema: S, keys: K) {
|
||||||
|
return schema.pick(Object.fromEntries(keys.map((key) => [key, true]))) as ZodObjectPick<S, K>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Picks the given keys from the given schema and extends it with the given expansion.
|
||||||
|
* @param schema - The original schema
|
||||||
|
* @param keys - The keys to keep
|
||||||
|
* @param shape - The shape of the expansion
|
||||||
|
* @returns A new schema with only the given keys
|
||||||
|
*/
|
||||||
|
export function select<S extends AnyZodObject, K extends ZodRecordKeys<S>[], E extends ZodRawShape>(
|
||||||
|
schema: S,
|
||||||
|
keys: K,
|
||||||
|
shape: E,
|
||||||
|
): ZodObjectExpand<ZodObjectPick<S, K>, E>;
|
||||||
|
export function select<S extends AnyZodObject, K extends ZodRecordKeys<S>[]>(schema: S, keys: K): ZodObjectPick<S, K>;
|
||||||
|
export function select<S extends AnyZodObject, K extends ZodRecordKeys<S>[], E extends ZodRawShape | undefined>(
|
||||||
|
schema: S,
|
||||||
|
keys: K,
|
||||||
|
shape?: E,
|
||||||
|
) {
|
||||||
|
return shape ? expand(pick(schema, keys), shape) : pick(schema, keys);
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ZodObjectExpand<S extends AnyZodObject, E extends ZodRawShape> =
|
||||||
|
S extends ZodObject<infer T, infer U, infer C>
|
||||||
|
? ZodEffects<
|
||||||
|
ZodObject<objectUtil.extendShape<T, { expand: HasRequiredKeys<E> extends true ? ZodObject<E> : ZodOptional<ZodObject<E>> }>, U, C>,
|
||||||
|
ZodObject<objectUtil.extendShape<T, E>, U, C>["_output"]
|
||||||
|
>
|
||||||
|
: never;
|
||||||
|
|
||||||
|
export type ZodObjectPick<S extends AnyZodObject, K extends ZodRecordKeys<S>[]> =
|
||||||
|
S extends ZodObject<infer T, infer U, infer C> ? ZodObject<Pick<T, K[number]>, U, C> : never;
|
||||||
|
|
||||||
|
export type AnyRecordsList = z.infer<typeof AnyRecordsList>;
|
||||||
|
export type RecordsList<S extends AnyZodRecord> = Omit<AnyRecordsList, "items"> & { items: S["_output"][] };
|
||||||
13
package/src/sdk.ts
Normal file
13
package/src/sdk.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import Pocketbase from "pocketbase";
|
||||||
|
import type { Credentials } from "./config.ts";
|
||||||
|
|
||||||
|
let adminPocketbase: Pocketbase;
|
||||||
|
|
||||||
|
export async function getPocketbase({ adminEmail, adminPassword, url }: Credentials) {
|
||||||
|
if (!adminPocketbase) {
|
||||||
|
const pocketbase = new Pocketbase(url);
|
||||||
|
await pocketbase.admins.authWithPassword(adminEmail, adminPassword);
|
||||||
|
adminPocketbase = pocketbase;
|
||||||
|
}
|
||||||
|
return adminPocketbase;
|
||||||
|
}
|
||||||
91
package/src/server/cli.ts
Normal file
91
package/src/server/cli.ts
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
import { Config, type ResolvedConfig } from "../config.ts";
|
||||||
|
import pkg from "../../package.json" assert { type: "json" };
|
||||||
|
import { loadConfig } from "c12";
|
||||||
|
import { defineCommand, runMain } from "citty";
|
||||||
|
import { cancel, group, intro, log, outro, confirm, text, spinner, multiselect, isCancel } from "@clack/prompts";
|
||||||
|
import { fetchCollections } from "../utils.ts";
|
||||||
|
import { generate } from "./utils.ts";
|
||||||
|
import { existsSync } from "node:fs";
|
||||||
|
|
||||||
|
async function getConfig() {
|
||||||
|
const { config } = await loadConfig({ name: "zod-pocketbase", rcFile: false, dotenv: true });
|
||||||
|
const { ZOD_POCKETBASE_ADMIN_EMAIL: adminEmail, ZOD_POCKETBASE_ADMIN_PASSWORD: adminPassword, ZOD_POCKETBASE_URL: url } = process.env;
|
||||||
|
const result = Config.safeParse({ ...config, adminEmail, adminPassword, url });
|
||||||
|
if (!result.success) {
|
||||||
|
log.error("Invalid fields in your config file.");
|
||||||
|
onCancel();
|
||||||
|
}
|
||||||
|
return result.data!;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onCancel() {
|
||||||
|
cancel("Operation cancelled.");
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function selectCollections(config: ResolvedConfig) {
|
||||||
|
const credentialPrompts = {
|
||||||
|
url: () => text({ message: "What is the url of your pocketbase instance?", initialValue: config.url ?? "" }),
|
||||||
|
adminEmail: () => text({ message: "What is your admin's email?", initialValue: config.adminEmail ?? "" }),
|
||||||
|
adminPassword: () => text({ message: "What is your admin's password?", initialValue: config.adminPassword ?? "" }),
|
||||||
|
};
|
||||||
|
const credentials = await group(credentialPrompts, { onCancel });
|
||||||
|
const s = spinner();
|
||||||
|
s.start("Fetching collections...");
|
||||||
|
try {
|
||||||
|
const allCollections = await fetchCollections(credentials);
|
||||||
|
s.stop("Successfully fetched collections.");
|
||||||
|
const collectionNames = await multiselect({
|
||||||
|
message: "What collections do you want to generate schemas for?",
|
||||||
|
options: allCollections.map(({ name: value }) => ({ value })),
|
||||||
|
initialValues: allCollections.filter(({ name }) => !config.ignore.includes(name)).map(({ name }) => name),
|
||||||
|
});
|
||||||
|
if (isCancel(collectionNames)) onCancel();
|
||||||
|
return allCollections.filter(({ name }) => (collectionNames as string[]).includes(name));
|
||||||
|
} catch {
|
||||||
|
s.stop("Failed to fetch collections.Please check your credentials and try again.");
|
||||||
|
return selectCollections(config);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function setGeneratedFilePath(config: ResolvedConfig) {
|
||||||
|
const output = await text({
|
||||||
|
message: "What is the generated file path?",
|
||||||
|
initialValue: config.output,
|
||||||
|
validate: (value) => {
|
||||||
|
if (!value) return "Please enter a path.";
|
||||||
|
if (value[0] !== ".") return "Please enter a relative path.";
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (isCancel(output)) onCancel();
|
||||||
|
|
||||||
|
if (existsSync(output as string)) {
|
||||||
|
const confirmed = await confirm({ message: "The file already exists, would you like to overwrite it?" });
|
||||||
|
if (isCancel(confirmed)) onCancel();
|
||||||
|
if (!confirmed) return setGeneratedFilePath(config);
|
||||||
|
}
|
||||||
|
|
||||||
|
return output as string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const main = defineCommand({
|
||||||
|
meta: { name: "zod-pocketbase", version: pkg.version, description: "Generate Zod schemas for your pocketbase instance." },
|
||||||
|
run: async () => {
|
||||||
|
intro(`ZOD POCKETBASE`);
|
||||||
|
const config = await getConfig();
|
||||||
|
const collections = await selectCollections(config);
|
||||||
|
const output = await setGeneratedFilePath(config);
|
||||||
|
|
||||||
|
const s = spinner();
|
||||||
|
s.start("Generating your schemas...");
|
||||||
|
await generate(collections, { ...config, output });
|
||||||
|
s.stop("Schemas successfully generated.");
|
||||||
|
|
||||||
|
outro("Operation completed.");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
runMain(main);
|
||||||
1
package/src/server/index.ts
Normal file
1
package/src/server/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from "./utils.js";
|
||||||
19
package/src/server/utils.ts
Normal file
19
package/src/server/utils.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
||||||
|
import { dirname, resolve } from "node:path";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
import type { CollectionModel } from "pocketbase";
|
||||||
|
import { stringifyContent } from "../content.js";
|
||||||
|
import type { ResolvedConfig } from "../config.ts";
|
||||||
|
|
||||||
|
export async function generate(collections: CollectionModel[], opts: GenerateOpts) {
|
||||||
|
const stub = readFileSync(resolve(dirname(fileURLToPath(import.meta.url)), "../../assets/stubs/index.ts"), "utf-8");
|
||||||
|
const { collectionNames, enums, records, services } = stringifyContent(collections, opts);
|
||||||
|
const content = stub
|
||||||
|
.replace("@@_COLLECTION_NAMES_@@", collectionNames)
|
||||||
|
.replace("@@_ENUMS_@@", enums)
|
||||||
|
.replace("@@_RECORDS_@@", records)
|
||||||
|
.replace("@@_SERVICES_@@", services);
|
||||||
|
mkdirSync(dirname(opts.output), { recursive: true });
|
||||||
|
writeFileSync(opts.output, content);
|
||||||
|
}
|
||||||
|
export type GenerateOpts = Omit<ResolvedConfig, "adminEmail" | "adminPassword" | "ignore" | "url">;
|
||||||
31
package/src/types.ts
Normal file
31
package/src/types.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import type { AnyZodObject, ZodEffects, ZodOptional, ZodRawShape, ZodTypeAny } from "zod";
|
||||||
|
|
||||||
|
export type AnyZodRecord = AnyZodObject | ZodEffects<AnyZodObject>;
|
||||||
|
|
||||||
|
export type RecordFullListOpts<S extends AnyZodRecord> = RecordListOpts<S> & { batch?: number };
|
||||||
|
|
||||||
|
export type RecordListOpts<S extends AnyZodRecord> = {
|
||||||
|
filter?: string;
|
||||||
|
page?: number;
|
||||||
|
perPage?: number;
|
||||||
|
skipTotal?: boolean;
|
||||||
|
sort?: ZodRecordSort<S>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type RecordIdRef<C extends string> = { collection: C; id: string };
|
||||||
|
export type RecordSlugRef<C extends string> = { collection: C; slug: string };
|
||||||
|
export type RecordRef<C extends string> = RecordIdRef<C> | RecordSlugRef<C>;
|
||||||
|
|
||||||
|
export type ZodRecordKeys<S extends AnyZodRecord> = Extract<keyof S["_input"], string>;
|
||||||
|
|
||||||
|
export type ZodRecordMainKeys<S extends AnyZodRecord> = Exclude<ZodRecordKeys<S>, "expand">;
|
||||||
|
|
||||||
|
export type ZodRecordSort<S extends AnyZodRecord> = `${"+" | "-"}${ZodRecordMainKeys<S>}` | "@random";
|
||||||
|
|
||||||
|
type RequiredKeysOf<S extends ZodRawShape> = Exclude<
|
||||||
|
{
|
||||||
|
[Key in keyof S]: S[Key] extends ZodOptional<ZodTypeAny> ? never : Key;
|
||||||
|
}[keyof S],
|
||||||
|
undefined
|
||||||
|
>;
|
||||||
|
export type HasRequiredKeys<S extends ZodRawShape> = RequiredKeysOf<S> extends never ? false : true;
|
||||||
10
package/src/utils.ts
Normal file
10
package/src/utils.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { sortBy } from "es-toolkit";
|
||||||
|
import type { CollectionModel } from "pocketbase";
|
||||||
|
import { getPocketbase } from "./sdk.js";
|
||||||
|
import type { Credentials } from "./config.ts";
|
||||||
|
|
||||||
|
export async function fetchCollections(credentials: Credentials): Promise<CollectionModel[]> {
|
||||||
|
const pocketbase = await getPocketbase(credentials);
|
||||||
|
const collections = await pocketbase.collections.getFullList();
|
||||||
|
return sortBy(collections, ["name"]);
|
||||||
|
}
|
||||||
52
package/tsconfig.json
Normal file
52
package/tsconfig.json
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/tsconfig",
|
||||||
|
"compilerOptions": {
|
||||||
|
// Enable top-level await, and other modern ESM features.
|
||||||
|
"target": "ESNext",
|
||||||
|
"module": "NodeNext",
|
||||||
|
// Enable module resolution without file extensions on relative paths, for things like npm package imports.
|
||||||
|
"moduleResolution": "Node16",
|
||||||
|
// Allow importing TypeScript files using their native extension (.ts(x)).
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
// Enable JSON imports.
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
// Enforce the usage of type-only imports when needed, which helps avoiding bundling issues.
|
||||||
|
"verbatimModuleSyntax": true,
|
||||||
|
// Ensure that each file can be transpiled without relying on other imports.
|
||||||
|
// This is redundant with the previous option, however it ensures that it's on even if someone disable `verbatimModuleSyntax`
|
||||||
|
"isolatedModules": true,
|
||||||
|
// Astro directly run TypeScript code, no transpilation needed.
|
||||||
|
"noEmit": true,
|
||||||
|
// Report an error when importing a file using a casing different from another import of the same file.
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
// Properly support importing CJS modules in ESM
|
||||||
|
"esModuleInterop": true,
|
||||||
|
// Skip typechecking libraries and .d.ts files
|
||||||
|
"skipLibCheck": true,
|
||||||
|
// Allow JavaScript files to be imported
|
||||||
|
"allowJs": true,
|
||||||
|
// Allow JSX files (or files that are internally considered JSX, like Astro files) to be imported inside `.js` and `.ts` files.
|
||||||
|
"jsx": "preserve",
|
||||||
|
// Enable strict mode. This enables a few options at a time, see https://www.typescriptlang.org/tsconfig#strict for a list.
|
||||||
|
"strict": true,
|
||||||
|
// Report errors for fallthrough cases in switch statements
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
// Force functions designed to override their parent class to be specified as `override`.
|
||||||
|
"noImplicitOverride": true,
|
||||||
|
// Force functions to specify that they can return `undefined` if a possible code path does not return a value.
|
||||||
|
"noImplicitReturns": true,
|
||||||
|
// Report an error when a variable is declared but never used.
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
// Report an error when a parameter is declared but never used.
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
// Force the usage of the indexed syntax to access fields declared using an index signature.
|
||||||
|
"noUncheckedIndexedAccess": true,
|
||||||
|
// Report an error when the value `undefined` is given to an optional property that doesn't specify `undefined` as a valid value.
|
||||||
|
"exactOptionalPropertyTypes": true,
|
||||||
|
// Report an error for unreachable code instead of just a warning.
|
||||||
|
"allowUnreachableCode": false,
|
||||||
|
// Report an error for unused labels instead of just a warning.
|
||||||
|
"allowUnusedLabels": false
|
||||||
|
},
|
||||||
|
"exclude": ["dist"]
|
||||||
|
}
|
||||||
19
package/tsup.config.ts
Normal file
19
package/tsup.config.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { defineConfig } from "tsup";
|
||||||
|
import { peerDependencies } from "./package.json";
|
||||||
|
|
||||||
|
export default defineConfig((options) => {
|
||||||
|
const dev = !!options.watch;
|
||||||
|
return {
|
||||||
|
entry: ["src/**/*.(ts|js)"],
|
||||||
|
format: ["esm"],
|
||||||
|
target: "node18",
|
||||||
|
bundle: true,
|
||||||
|
dts: true,
|
||||||
|
sourcemap: true,
|
||||||
|
clean: true,
|
||||||
|
splitting: false,
|
||||||
|
minify: !dev,
|
||||||
|
external: [...Object.keys(peerDependencies), "dotenv"],
|
||||||
|
tsconfig: "tsconfig.json",
|
||||||
|
};
|
||||||
|
});
|
||||||
26
playground/.gitignore
vendored
Normal file
26
playground/.gitignore
vendored
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
# build output
|
||||||
|
dist/
|
||||||
|
|
||||||
|
# generated types
|
||||||
|
.astro/
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# logs
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
|
||||||
|
# environment variables
|
||||||
|
.env
|
||||||
|
.env.production
|
||||||
|
|
||||||
|
# macOS-specific files
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# jetbrains setting folder
|
||||||
|
.idea/
|
||||||
|
|
||||||
|
.cache
|
||||||
4
playground/.vscode/extensions.json
vendored
Normal file
4
playground/.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"recommendations": ["astro-build.astro-vscode"],
|
||||||
|
"unwantedRecommendations": []
|
||||||
|
}
|
||||||
11
playground/.vscode/launch.json
vendored
Normal file
11
playground/.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"command": "./node_modules/.bin/astro dev",
|
||||||
|
"name": "Development server",
|
||||||
|
"request": "launch",
|
||||||
|
"type": "node-terminal"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
48
playground/README.md
Normal file
48
playground/README.md
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
# Astro Starter Kit: Basics
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm create astro@latest -- --template basics
|
||||||
|
```
|
||||||
|
|
||||||
|
[](https://stackblitz.com/github/withastro/astro/tree/latest/examples/basics)
|
||||||
|
[](https://codesandbox.io/p/sandbox/github/withastro/astro/tree/latest/examples/basics)
|
||||||
|
[](https://codespaces.new/withastro/astro?devcontainer_path=.devcontainer/basics/devcontainer.json)
|
||||||
|
|
||||||
|
> 🧑🚀 **Seasoned astronaut?** Delete this file. Have fun!
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## 🚀 Project Structure
|
||||||
|
|
||||||
|
Inside of your Astro project, you'll see the following folders and files:
|
||||||
|
|
||||||
|
```text
|
||||||
|
/
|
||||||
|
├── public/
|
||||||
|
│ └── favicon.svg
|
||||||
|
├── src/
|
||||||
|
│ ├── layouts/
|
||||||
|
│ │ └── Layout.astro
|
||||||
|
│ └── pages/
|
||||||
|
│ └── index.astro
|
||||||
|
└── package.json
|
||||||
|
```
|
||||||
|
|
||||||
|
To learn more about the folder structure of an Astro project, refer to [our guide on project structure](https://docs.astro.build/en/basics/project-structure/).
|
||||||
|
|
||||||
|
## 🧞 Commands
|
||||||
|
|
||||||
|
All commands are run from the root of the project, from a terminal:
|
||||||
|
|
||||||
|
| Command | Action |
|
||||||
|
| :------------------------ | :----------------------------------------------- |
|
||||||
|
| `npm install` | Installs dependencies |
|
||||||
|
| `npm run dev` | Starts local dev server at `localhost:4321` |
|
||||||
|
| `npm run build` | Build your production site to `./dist/` |
|
||||||
|
| `npm run preview` | Preview your build locally, before deploying |
|
||||||
|
| `npm run astro ...` | Run CLI commands like `astro add`, `astro check` |
|
||||||
|
| `npm run astro -- --help` | Get help using the Astro CLI |
|
||||||
|
|
||||||
|
## 👀 Want to learn more?
|
||||||
|
|
||||||
|
Feel free to check [our documentation](https://docs.astro.build) or jump into our [Discord server](https://astro.build/chat).
|
||||||
11
playground/astro.config.mjs
Normal file
11
playground/astro.config.mjs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
// @ts-check
|
||||||
|
import { defineConfig, envField } from "astro/config";
|
||||||
|
|
||||||
|
// https://astro.build/config
|
||||||
|
export default defineConfig({
|
||||||
|
env: {
|
||||||
|
schema: {
|
||||||
|
ZOD_POCKETBASE_URL: envField.string({ context: "server", access: "public" }),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
20
playground/package.json
Normal file
20
playground/package.json
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"name": "playground",
|
||||||
|
"type": "module",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "astro dev",
|
||||||
|
"build": "astro build",
|
||||||
|
"preview": "astro preview",
|
||||||
|
"astro": "astro",
|
||||||
|
"generate": "pnpm exec zod-pocketbase"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@11ty/eleventy-fetch": "^5.0.1",
|
||||||
|
"astro": "^5.1.1",
|
||||||
|
"es-toolkit": "^1.30.1",
|
||||||
|
"pocketbase": "<0.22.0",
|
||||||
|
"zod": "^3.24.1",
|
||||||
|
"zod-pocketbase": "workspace:*"
|
||||||
|
}
|
||||||
|
}
|
||||||
9
playground/public/favicon.svg
Normal file
9
playground/public/favicon.svg
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 128 128">
|
||||||
|
<path d="M50.4 78.5a75.1 75.1 0 0 0-28.5 6.9l24.2-65.7c.7-2 1.9-3.2 3.4-3.2h29c1.5 0 2.7 1.2 3.4 3.2l24.2 65.7s-11.6-7-28.5-7L67 45.5c-.4-1.7-1.6-2.8-2.9-2.8-1.3 0-2.5 1.1-2.9 2.7L50.4 78.5Zm-1.1 28.2Zm-4.2-20.2c-2 6.6-.6 15.8 4.2 20.2a17.5 17.5 0 0 1 .2-.7 5.5 5.5 0 0 1 5.7-4.5c2.8.1 4.3 1.5 4.7 4.7.2 1.1.2 2.3.2 3.5v.4c0 2.7.7 5.2 2.2 7.4a13 13 0 0 0 5.7 4.9v-.3l-.2-.3c-1.8-5.6-.5-9.5 4.4-12.8l1.5-1a73 73 0 0 0 3.2-2.2 16 16 0 0 0 6.8-11.4c.3-2 .1-4-.6-6l-.8.6-1.6 1a37 37 0 0 1-22.4 2.7c-5-.7-9.7-2-13.2-6.2Z" />
|
||||||
|
<style>
|
||||||
|
path { fill: #000; }
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
path { fill: #FFF; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 749 B |
1
playground/src/assets/astro.svg
Normal file
1
playground/src/assets/astro.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" width="115" height="48"><path fill="#17191E" d="M7.77 36.35C6.4 35.11 6 32.51 6.57 30.62c.99 1.2 2.35 1.57 3.75 1.78 2.18.33 4.31.2 6.33-.78.23-.12.44-.27.7-.42.18.55.23 1.1.17 1.67a4.56 4.56 0 0 1-1.94 3.23c-.43.32-.9.61-1.34.91-1.38.94-1.76 2.03-1.24 3.62l.05.17a3.63 3.63 0 0 1-1.6-1.38 3.87 3.87 0 0 1-.63-2.1c0-.37 0-.74-.05-1.1-.13-.9-.55-1.3-1.33-1.32a1.56 1.56 0 0 0-1.63 1.26c0 .06-.03.12-.05.2Z"/><path fill="url(#a)" d="M7.77 36.35C6.4 35.11 6 32.51 6.57 30.62c.99 1.2 2.35 1.57 3.75 1.78 2.18.33 4.31.2 6.33-.78.23-.12.44-.27.7-.42.18.55.23 1.1.17 1.67a4.56 4.56 0 0 1-1.94 3.23c-.43.32-.9.61-1.34.91-1.38.94-1.76 2.03-1.24 3.62l.05.17a3.63 3.63 0 0 1-1.6-1.38 3.87 3.87 0 0 1-.63-2.1c0-.37 0-.74-.05-1.1-.13-.9-.55-1.3-1.33-1.32a1.56 1.56 0 0 0-1.63 1.26c0 .06-.03.12-.05.2Z"/><path fill="#17191E" d="M.02 30.31s4.02-1.95 8.05-1.95l3.04-9.4c.11-.45.44-.76.82-.76.37 0 .7.31.82.76l3.04 9.4c4.77 0 8.05 1.95 8.05 1.95L17 11.71c-.2-.56-.53-.91-.98-.91H7.83c-.44 0-.76.35-.97.9L.02 30.31Zm42.37-5.97c0 1.64-2.05 2.62-4.88 2.62-1.85 0-2.5-.45-2.5-1.41 0-1 .8-1.49 2.65-1.49 1.67 0 3.09.03 4.73.23v.05Zm.03-2.04a21.37 21.37 0 0 0-4.37-.36c-5.32 0-7.82 1.25-7.82 4.18 0 3.04 1.71 4.2 5.68 4.2 3.35 0 5.63-.84 6.46-2.92h.14c-.03.5-.05 1-.05 1.4 0 1.07.18 1.16 1.06 1.16h4.15a16.9 16.9 0 0 1-.36-4c0-1.67.06-2.93.06-4.62 0-3.45-2.07-5.64-8.56-5.64-2.8 0-5.9.48-8.26 1.19.22.93.54 2.83.7 4.06 2.04-.96 4.95-1.37 7.2-1.37 3.11 0 3.97.71 3.97 2.15v.57Zm11.37 3c-.56.07-1.33.07-2.12.07-.83 0-1.6-.03-2.12-.1l-.02.58c0 2.85 1.87 4.52 8.45 4.52 6.2 0 8.2-1.64 8.2-4.55 0-2.74-1.33-4.09-7.2-4.39-4.58-.2-4.99-.7-4.99-1.28 0-.66.59-1 3.65-1 3.18 0 4.03.43 4.03 1.35v.2a46.13 46.13 0 0 1 4.24.03l.02-.55c0-3.36-2.8-4.46-8.2-4.46-6.08 0-8.13 1.49-8.13 4.39 0 2.6 1.64 4.23 7.48 4.48 4.3.14 4.77.62 4.77 1.28 0 .7-.7 1.03-3.71 1.03-3.47 0-4.35-.48-4.35-1.47v-.13Zm19.82-12.05a17.5 17.5 0 0 1-6.24 3.48c.03.84.03 2.4.03 3.24l1.5.02c-.02 1.63-.04 3.6-.04 4.9 0 3.04 1.6 5.32 6.58 5.32 2.1 0 3.5-.23 5.23-.6a43.77 43.77 0 0 1-.46-4.13c-1.03.34-2.34.53-3.78.53-2 0-2.82-.55-2.82-2.13 0-1.37 0-2.65.03-3.84 2.57.02 5.13.07 6.64.11-.02-1.18.03-2.9.1-4.04-2.2.04-4.65.07-6.68.07l.07-2.93h-.16Zm13.46 6.04a767.33 767.33 0 0 1 .07-3.18H82.6c.07 1.96.07 3.98.07 6.92 0 2.95-.03 4.99-.07 6.93h5.18c-.09-1.37-.11-3.68-.11-5.65 0-3.1 1.26-4 4.12-4 1.33 0 2.28.16 3.1.46.03-1.16.26-3.43.4-4.43-.86-.25-1.81-.41-2.96-.41-2.46-.03-4.26.98-5.1 3.38l-.17-.02Zm22.55 3.65c0 2.5-1.8 3.66-4.64 3.66-2.81 0-4.61-1.1-4.61-3.66s1.82-3.52 4.61-3.52c2.82 0 4.64 1.03 4.64 3.52Zm4.71-.11c0-4.96-3.87-7.18-9.35-7.18-5.5 0-9.23 2.22-9.23 7.18 0 4.94 3.49 7.59 9.21 7.59 5.77 0 9.37-2.65 9.37-7.6Z"/><defs><linearGradient id="a" x1="6.33" x2="19.43" y1="40.8" y2="34.6" gradientUnits="userSpaceOnUse"><stop stop-color="#D83333"/><stop offset="1" stop-color="#F041FF"/></linearGradient></defs></svg>
|
||||||
|
After Width: | Height: | Size: 2.8 KiB |
1
playground/src/assets/background.svg
Normal file
1
playground/src/assets/background.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="1440" height="1024" fill="none"><path fill="url(#a)" fill-rule="evenodd" d="M-217.58 475.75c91.82-72.02 225.52-29.38 341.2-44.74C240 415.56 372.33 315.14 466.77 384.9c102.9 76.02 44.74 246.76 90.31 366.31 29.83 78.24 90.48 136.14 129.48 210.23 57.92 109.99 169.67 208.23 155.9 331.77-13.52 121.26-103.42 264.33-224.23 281.37-141.96 20.03-232.72-220.96-374.06-196.99-151.7 25.73-172.68 330.24-325.85 315.72-128.6-12.2-110.9-230.73-128.15-358.76-12.16-90.14 65.87-176.25 44.1-264.57-26.42-107.2-167.12-163.46-176.72-273.45-10.15-116.29 33.01-248.75 124.87-320.79Z" clip-rule="evenodd" style="opacity:.154"/><path fill="url(#b)" fill-rule="evenodd" d="M1103.43 115.43c146.42-19.45 275.33-155.84 413.5-103.59 188.09 71.13 409 212.64 407.06 413.88-1.94 201.25-259.28 278.6-414.96 405.96-130 106.35-240.24 294.39-405.6 265.3-163.7-28.8-161.93-274.12-284.34-386.66-134.95-124.06-436-101.46-445.82-284.6-9.68-180.38 247.41-246.3 413.54-316.9 101.01-42.93 207.83 21.06 316.62 6.61Z" clip-rule="evenodd" style="opacity:.154"/><defs><linearGradient id="b" x1="373" x2="1995.44" y1="1100" y2="118.03" gradientUnits="userSpaceOnUse"><stop stop-color="#D83333"/><stop offset="1" stop-color="#F041FF"/></linearGradient><linearGradient id="a" x1="107.37" x2="1130.66" y1="1993.35" y2="1026.31" gradientUnits="userSpaceOnUse"><stop stop-color="#3245FF"/><stop offset="1" stop-color="#BC52EE"/></linearGradient></defs></svg>
|
||||||
|
After Width: | Height: | Size: 1.4 KiB |
10
playground/src/env.d.ts
vendored
Normal file
10
playground/src/env.d.ts
vendored
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import type { helpersFrom } from "zod-pocketbase";
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
declare namespace App {
|
||||||
|
interface Locals {
|
||||||
|
getRecord: ReturnType<typeof helpersFrom>["getRecord"];
|
||||||
|
getRecords: ReturnType<typeof helpersFrom>["getRecords"];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
200
playground/src/lib/pocketbase.ts
Normal file
200
playground/src/lib/pocketbase.ts
Normal file
@@ -0,0 +1,200 @@
|
|||||||
|
import type Pocketbase from "pocketbase";
|
||||||
|
import type { RecordService } from "pocketbase";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
/******* ENUMS *******/
|
||||||
|
export const collectionValues = [
|
||||||
|
"config",
|
||||||
|
"events",
|
||||||
|
"images",
|
||||||
|
"knowledges",
|
||||||
|
"pages",
|
||||||
|
"places",
|
||||||
|
"posts",
|
||||||
|
"products",
|
||||||
|
"services",
|
||||||
|
"testimonies",
|
||||||
|
] as const;
|
||||||
|
export const Collection = z.enum(collectionValues);
|
||||||
|
export type Collection = z.infer<typeof Collection>;
|
||||||
|
export const COLLECTION = Collection.enum;
|
||||||
|
|
||||||
|
export const servicesCategoryValues = [
|
||||||
|
"consult",
|
||||||
|
"training",
|
||||||
|
"workshop",
|
||||||
|
] as const;
|
||||||
|
export const ServicesCategory = z.enum(servicesCategoryValues);
|
||||||
|
export type ServicesCategory = z.infer<typeof ServicesCategory>;
|
||||||
|
export const SERVICES_CATEGORY = ServicesCategory.enum;
|
||||||
|
|
||||||
|
/******* BASE *******/
|
||||||
|
export const BaseModel = z.object({
|
||||||
|
created: z.string().pipe(z.coerce.date()),
|
||||||
|
id: z.string(),
|
||||||
|
updated: z.string().pipe(z.coerce.date()),
|
||||||
|
});
|
||||||
|
export type BaseModel = z.infer<typeof BaseModel>;
|
||||||
|
|
||||||
|
export const AdminModel = z.object({
|
||||||
|
...BaseModel.shape,
|
||||||
|
avatar: z.string(),
|
||||||
|
email: z.string().email(),
|
||||||
|
});
|
||||||
|
export type AdminModel = z.infer<typeof AdminModel>;
|
||||||
|
|
||||||
|
export const RecordModel = z.object({
|
||||||
|
...BaseModel.shape,
|
||||||
|
collectionId: z.string(),
|
||||||
|
collectionName: z.string(),
|
||||||
|
expand: z.any().optional(),
|
||||||
|
});
|
||||||
|
export type RecordModel = z.infer<typeof RecordModel>;
|
||||||
|
|
||||||
|
/******* RECORDS *******/
|
||||||
|
export const zConfigRecord = z.object({
|
||||||
|
...RecordModel.omit({ expand: true }).shape,
|
||||||
|
collectionName: z.literal("config"),
|
||||||
|
city: z.string(),
|
||||||
|
email: z.string().email(),
|
||||||
|
facebook: z.string().url(),
|
||||||
|
instagram: z.string().url(),
|
||||||
|
phone: z.string(),
|
||||||
|
street: z.string(),
|
||||||
|
title: z.string(),
|
||||||
|
website: z.string().url(),
|
||||||
|
zipcode: z.string(),
|
||||||
|
});
|
||||||
|
export type ConfigRecord = z.infer<typeof zConfigRecord>;
|
||||||
|
|
||||||
|
export const zEventsRecord = z.object({
|
||||||
|
...RecordModel.omit({ expand: true }).shape,
|
||||||
|
collectionName: z.literal("events"),
|
||||||
|
excerpt: z.string(),
|
||||||
|
from: z.string().pipe(z.coerce.date()),
|
||||||
|
image: z.string(),
|
||||||
|
name: z.string(),
|
||||||
|
places: z.string().array(),
|
||||||
|
service: z.string(),
|
||||||
|
slug: z.string(),
|
||||||
|
to: z.string().pipe(z.coerce.date()),
|
||||||
|
url: z.string().url(),
|
||||||
|
});
|
||||||
|
export type EventsRecord = z.infer<typeof zEventsRecord>;
|
||||||
|
|
||||||
|
export const zImagesRecord = z.object({
|
||||||
|
...RecordModel.omit({ expand: true }).shape,
|
||||||
|
collectionName: z.literal("images"),
|
||||||
|
alt: z.string(),
|
||||||
|
height: z.number().int(),
|
||||||
|
src: z.string(),
|
||||||
|
width: z.number().int(),
|
||||||
|
});
|
||||||
|
export type ImagesRecord = z.infer<typeof zImagesRecord>;
|
||||||
|
|
||||||
|
export const zKnowledgesRecord = z.object({
|
||||||
|
...RecordModel.omit({ expand: true }).shape,
|
||||||
|
collectionName: z.literal("knowledges"),
|
||||||
|
image: z.string(),
|
||||||
|
name: z.string(),
|
||||||
|
slug: z.string(),
|
||||||
|
text: z.string(),
|
||||||
|
});
|
||||||
|
export type KnowledgesRecord = z.infer<typeof zKnowledgesRecord>;
|
||||||
|
|
||||||
|
export const zPagesRecord = z.object({
|
||||||
|
...RecordModel.omit({ expand: true }).shape,
|
||||||
|
collectionName: z.literal("pages"),
|
||||||
|
knowledge: z.string(),
|
||||||
|
post: z.string(),
|
||||||
|
services: z.string().array().optional(),
|
||||||
|
slug: z.string(),
|
||||||
|
testimoniesImage: z.string().transform((id) => id === "" ? undefined : id).optional(),
|
||||||
|
title: z.string(),
|
||||||
|
});
|
||||||
|
export type PagesRecord = z.infer<typeof zPagesRecord>;
|
||||||
|
|
||||||
|
export const zPlacesRecord = z.object({
|
||||||
|
...RecordModel.omit({ expand: true }).shape,
|
||||||
|
collectionName: z.literal("places"),
|
||||||
|
name: z.string(),
|
||||||
|
slug: z.string(),
|
||||||
|
});
|
||||||
|
export type PlacesRecord = z.infer<typeof zPlacesRecord>;
|
||||||
|
|
||||||
|
export const zPostsRecord = z.object({
|
||||||
|
...RecordModel.omit({ expand: true }).shape,
|
||||||
|
collectionName: z.literal("posts"),
|
||||||
|
excerpt: z.string(),
|
||||||
|
image: z.string().transform((id) => id === "" ? undefined : id).optional(),
|
||||||
|
knowledge: z.string(),
|
||||||
|
slug: z.string(),
|
||||||
|
text: z.string(),
|
||||||
|
title: z.string(),
|
||||||
|
});
|
||||||
|
export type PostsRecord = z.infer<typeof zPostsRecord>;
|
||||||
|
|
||||||
|
export const zProductsRecord = z.object({
|
||||||
|
...RecordModel.omit({ expand: true }).shape,
|
||||||
|
collectionName: z.literal("products"),
|
||||||
|
excerpt: z.string(),
|
||||||
|
image: z.string(),
|
||||||
|
name: z.string(),
|
||||||
|
price: z.string(),
|
||||||
|
slug: z.string(),
|
||||||
|
text: z.string(),
|
||||||
|
url: z.string().url(),
|
||||||
|
});
|
||||||
|
export type ProductsRecord = z.infer<typeof zProductsRecord>;
|
||||||
|
|
||||||
|
export const zServicesRecord = z.object({
|
||||||
|
...RecordModel.omit({ expand: true }).shape,
|
||||||
|
collectionName: z.literal("services"),
|
||||||
|
category: ServicesCategory,
|
||||||
|
duration: z.string(),
|
||||||
|
excerpt: z.string(),
|
||||||
|
image: z.string(),
|
||||||
|
knowledge: z.string(),
|
||||||
|
name: z.string(),
|
||||||
|
places: z.string().array(),
|
||||||
|
price: z.string(),
|
||||||
|
slug: z.string(),
|
||||||
|
text: z.string(),
|
||||||
|
});
|
||||||
|
export type ServicesRecord = z.infer<typeof zServicesRecord>;
|
||||||
|
|
||||||
|
export const zTestimoniesRecord = z.object({
|
||||||
|
...RecordModel.omit({ expand: true }).shape,
|
||||||
|
collectionName: z.literal("testimonies"),
|
||||||
|
author: z.string(),
|
||||||
|
text: z.string(),
|
||||||
|
title: z.string(),
|
||||||
|
});
|
||||||
|
export type TestimoniesRecord = z.infer<typeof zTestimoniesRecord>;
|
||||||
|
|
||||||
|
export const records = new Map<Collection, z.AnyZodObject>([
|
||||||
|
["config", zConfigRecord],
|
||||||
|
["events", zEventsRecord],
|
||||||
|
["images", zImagesRecord],
|
||||||
|
["knowledges", zKnowledgesRecord],
|
||||||
|
["pages", zPagesRecord],
|
||||||
|
["places", zPlacesRecord],
|
||||||
|
["posts", zPostsRecord],
|
||||||
|
["products", zProductsRecord],
|
||||||
|
["services", zServicesRecord],
|
||||||
|
["testimonies", zTestimoniesRecord],
|
||||||
|
]);
|
||||||
|
|
||||||
|
/******* CLIENT *******/
|
||||||
|
export type TypedPocketbase = Pocketbase & {
|
||||||
|
collection(idOrName: "config"): RecordService<z.input<typeof zConfigRecord>>;
|
||||||
|
collection(idOrName: "events"): RecordService<z.input<typeof zEventsRecord>>;
|
||||||
|
collection(idOrName: "images"): RecordService<z.input<typeof zImagesRecord>>;
|
||||||
|
collection(idOrName: "knowledges"): RecordService<z.input<typeof zKnowledgesRecord>>;
|
||||||
|
collection(idOrName: "pages"): RecordService<z.input<typeof zPagesRecord>>;
|
||||||
|
collection(idOrName: "places"): RecordService<z.input<typeof zPlacesRecord>>;
|
||||||
|
collection(idOrName: "posts"): RecordService<z.input<typeof zPostsRecord>>;
|
||||||
|
collection(idOrName: "products"): RecordService<z.input<typeof zProductsRecord>>;
|
||||||
|
collection(idOrName: "services"): RecordService<z.input<typeof zServicesRecord>>;
|
||||||
|
collection(idOrName: "testimonies"): RecordService<z.input<typeof zTestimoniesRecord>>;
|
||||||
|
};
|
||||||
21
playground/src/middleware.ts
Normal file
21
playground/src/middleware.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
// @ts-ignore
|
||||||
|
import Fetch from "@11ty/eleventy-fetch";
|
||||||
|
import { defineMiddleware } from "astro:middleware";
|
||||||
|
import { ZOD_POCKETBASE_URL } from "astro:env/server";
|
||||||
|
import Pocketbase from "pocketbase";
|
||||||
|
import { helpersFrom } from "zod-pocketbase";
|
||||||
|
|
||||||
|
export const onRequest = defineMiddleware((context, next) => {
|
||||||
|
const { getRecord, getRecords } = helpersFrom({
|
||||||
|
pocketbase: new Pocketbase(ZOD_POCKETBASE_URL),
|
||||||
|
fetch: import.meta.env.DEV
|
||||||
|
? async (url, fetchOptions) => {
|
||||||
|
const { body, ...init } = await Fetch(url, { fetchOptions, returnType: "response", type: "json" });
|
||||||
|
return new Response(JSON.stringify(body), init);
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
});
|
||||||
|
context.locals.getRecord = getRecord;
|
||||||
|
context.locals.getRecords = getRecords;
|
||||||
|
return next();
|
||||||
|
});
|
||||||
19
playground/src/pages/index.astro
Normal file
19
playground/src/pages/index.astro
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
---
|
||||||
|
import { COLLECTION, zKnowledgesRecord } from "@/lib/pocketbase";
|
||||||
|
|
||||||
|
const { items } = await Astro.locals.getRecords(COLLECTION.knowledges, { schema: zKnowledgesRecord, sort: "+name" });
|
||||||
|
---
|
||||||
|
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
|
<meta name="generator" content={Astro.generator} />
|
||||||
|
<title>Astro Basics</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<ul>{items.map(({ name }) => <li>{name}</li>)}</ul>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
11
playground/tsconfig.json
Normal file
11
playground/tsconfig.json
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"extends": "astro/tsconfigs/strict",
|
||||||
|
"compilerOptions": {
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["src/*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": [".astro/types.d.ts", "**/*"],
|
||||||
|
"exclude": ["dist"]
|
||||||
|
}
|
||||||
8
playground/zod-pocketbase.config.ts
Normal file
8
playground/zod-pocketbase.config.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import { pascalCase } from "es-toolkit";
|
||||||
|
import type { Config } from "zod-pocketbase";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
ignore: ["users"],
|
||||||
|
nameRecordSchema: (name: string) => `z${pascalCase(name)}Record`,
|
||||||
|
output: "./src/lib/pocketbase.ts",
|
||||||
|
} satisfies Config;
|
||||||
8566
pnpm-lock.yaml
generated
Normal file
8566
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
4
pnpm-workspace.yaml
Normal file
4
pnpm-workspace.yaml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
packages:
|
||||||
|
- doc
|
||||||
|
- package
|
||||||
|
- playground
|
||||||
6
prettier.config.mjs
Normal file
6
prettier.config.mjs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
/** @type {import("prettier").Config} */
|
||||||
|
export default {
|
||||||
|
printWidth: 140,
|
||||||
|
plugins: ["prettier-plugin-astro", "prettier-plugin-tailwindcss"],
|
||||||
|
overrides: [{ files: "*.astro", options: { parser: "astro" } }],
|
||||||
|
};
|
||||||
51
scripts/release.mjs
Normal file
51
scripts/release.mjs
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import { spawn } from "node:child_process";
|
||||||
|
import { resolve } from "node:path";
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {string} command
|
||||||
|
* @param {...Array<string>} args
|
||||||
|
*
|
||||||
|
* @returns {Promise<string>}
|
||||||
|
*/
|
||||||
|
const run = async (command, ...args) => {
|
||||||
|
const cwd = resolve();
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const cmd = spawn(command, args, {
|
||||||
|
stdio: ["inherit", "pipe", "pipe"], // Inherit stdin, pipe stdout, pipe stderr
|
||||||
|
shell: true,
|
||||||
|
cwd,
|
||||||
|
});
|
||||||
|
|
||||||
|
let output = "";
|
||||||
|
|
||||||
|
cmd.stdout.on("data", (data) => {
|
||||||
|
process.stdout.write(data.toString());
|
||||||
|
output += data.toString();
|
||||||
|
});
|
||||||
|
|
||||||
|
cmd.stderr.on("data", (data) => {
|
||||||
|
process.stderr.write(data.toString());
|
||||||
|
});
|
||||||
|
|
||||||
|
cmd.on("close", () => {
|
||||||
|
resolve(output);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const main = async () => {
|
||||||
|
await run("pnpm changeset version");
|
||||||
|
await run("git add .");
|
||||||
|
await run('git commit -m "chore: update version"');
|
||||||
|
await run("git push");
|
||||||
|
await run("pnpm --filter zod-pocketbase build");
|
||||||
|
await run("pnpm changeset publish");
|
||||||
|
await run("git push --follow-tags");
|
||||||
|
const tag = (await run("git describe --abbrev=0")).replace("\n", "");
|
||||||
|
await run(
|
||||||
|
`gh release create ${tag} --title ${tag} --notes "Please refer to [CHANGELOG.md](https://github.com/gbouteiller/zod-pocketbase/blob/main/package/CHANGELOG.md) for details."`,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
main();
|
||||||
Reference in New Issue
Block a user