diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index ab0662d4..053ae9ff 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -33,7 +33,7 @@ jobs: strategy: matrix: - node-version: [20.x] + node-version: [22.x] provider: [sqlite, postgresql] steps: diff --git a/.github/workflows/bump-version.yml b/.github/workflows/bump-version.yml index 84da4866..d18b6ce5 100644 --- a/.github/workflows/bump-version.yml +++ b/.github/workflows/bump-version.yml @@ -2,6 +2,14 @@ name: Bump Version on: workflow_dispatch: + inputs: + version_type: + description: 'Version type to bump' + required: true + type: choice + options: + - patch + - minor permissions: contents: write @@ -26,7 +34,7 @@ jobs: - name: Use Node.js uses: actions/setup-node@v4 with: - node-version: 20.x + node-version: 22.x cache: 'pnpm' - name: Install dependencies @@ -34,7 +42,7 @@ jobs: - name: Bump version id: bump - run: npx tsx scripts/bump-version.ts + run: npx tsx scripts/bump-version.ts ${{ inputs.version_type }} - name: Create PR uses: peter-evans/create-pull-request@v7 diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml index 097b1552..71283101 100644 --- a/.github/workflows/publish-release.yml +++ b/.github/workflows/publish-release.yml @@ -28,7 +28,7 @@ jobs: - name: Use Node.js uses: actions/setup-node@v4 with: - node-version: 20.x + node-version: 22.x cache: 'pnpm' registry-url: 'https://registry.npmjs.org' diff --git a/.github/workflows/update-samples.yml b/.github/workflows/update-samples.yml index f5df320c..a52fb504 100644 --- a/.github/workflows/update-samples.yml +++ b/.github/workflows/update-samples.yml @@ -65,7 +65,7 @@ jobs: if: steps.check-package.outputs.exists == 'true' uses: actions/setup-node@v4 with: - node-version: 20.x + node-version: 22.x cache: 'npm' - name: Update @zenstackhq packages to latest diff --git a/.gitignore b/.gitignore index d60aa29a..665d9c27 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ dist .pnpm-store *.vsix .DS_Store +coverage diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3a74924c..b48bc73b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -6,7 +6,7 @@ Before you start working on anything major, please make sure to open an issue or ## Prerequisites -- Node.js: v20 or above +- Node.js: v22 or above - PNPM: as specified in [package.json](./package.json) Test cases are run against both SQLite and Postgres. You should have a postgres server (16 or above) running (either natively or via Docker). The default connection is: diff --git a/README.md b/README.md index 2c45584e..92f24ca4 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@

ZenStack: Modern Data Layer for TypeScript Apps

- + diff --git a/package.json b/package.json index 0803435e..1e83969d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "zenstack-v3", - "version": "3.0.0", + "version": "3.1.0", "description": "ZenStack", "packageManager": "pnpm@10.23.0", "type": "module", @@ -12,10 +12,12 @@ "test:all": "pnpm run test:sqlite && pnpm run test:pg", "test:pg": "TEST_DB_PROVIDER=postgresql turbo run test", "test:sqlite": "TEST_DB_PROVIDER=sqlite turbo run test", + "test:coverage": "vitest run --coverage", "format": "prettier --write \"**/*.{ts,tsx,md}\"", "pr": "gh pr create --fill-first --base dev", "merge-main": "gh pr create --title \"merge dev to main\" --body \"\" --base main --head dev", - "bump-version": "gh workflow run .github/workflows/bump-version.yml --ref dev", + "bump-patch": "gh workflow run .github/workflows/bump-version.yml --ref dev -f version_type=patch", + "bump-minor": "gh workflow run .github/workflows/bump-version.yml --ref dev -f version_type=minor", "publish-all": "pnpm --filter \"./packages/**\" -r publish --access public", "publish-preview": "pnpm --filter \"./packages/**\" -r publish --force --registry https://preview.registry.zenstack.dev/", "unpublish-preview": "pnpm --filter \"./packages/**\" -r --shell-mode exec -- npm unpublish -f --registry https://preview.registry.zenstack.dev/ \"\\$PNPM_PACKAGE_NAME\"" @@ -26,8 +28,10 @@ "devDependencies": { "@eslint/js": "^9.29.0", "@types/node": "catalog:", + "@vitest/coverage-v8": "^4.0.16", "eslint": "~9.29.0", "glob": "^11.1.0", + "npm-run-all": "^4.1.5", "prettier": "^3.5.3", "prisma": "catalog:", "tsup": "^8.5.0", @@ -40,7 +44,10 @@ }, "pnpm": { "onlyBuiltDependencies": [ - "better-sqlite3" + "@parcel/watcher", + "better-sqlite3", + "esbuild", + "vue-demi" ] } } diff --git a/packages/auth-adapters/better-auth/package.json b/packages/auth-adapters/better-auth/package.json index 90beab8f..d9b687ba 100644 --- a/packages/auth-adapters/better-auth/package.json +++ b/packages/auth-adapters/better-auth/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/better-auth", - "version": "3.0.0", + "version": "3.1.0", "description": "ZenStack Better Auth Adapter. This adapter is modified from better-auth's Prisma adapter.", "type": "module", "scripts": { diff --git a/packages/cli/package.json b/packages/cli/package.json index 506a6376..39540625 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -3,7 +3,7 @@ "publisher": "zenstack", "displayName": "ZenStack CLI", "description": "FullStack database toolkit with built-in access control and automatic API generation.", - "version": "3.0.0", + "version": "3.1.0", "type": "module", "author": { "name": "ZenStack Team" @@ -28,6 +28,9 @@ "test": "vitest run", "pack": "pnpm pack" }, + "exports": { + "./package.json": "./package.json" + }, "dependencies": { "@zenstackhq/common-helpers": "workspace:*", "@zenstackhq/language": "workspace:*", diff --git a/packages/cli/src/actions/generate.ts b/packages/cli/src/actions/generate.ts index 2b7a22e9..c41a99ea 100644 --- a/packages/cli/src/actions/generate.ts +++ b/packages/cli/src/actions/generate.ts @@ -46,7 +46,7 @@ const client = new ZenStackClient(schema, { }); \`\`\` -Check documentation: https://zenstack.dev/docs/3.x`); +Check documentation: https://zenstack.dev/docs/`); } } diff --git a/packages/cli/src/actions/init.ts b/packages/cli/src/actions/init.ts index f44bf172..03635736 100644 --- a/packages/cli/src/actions/init.ts +++ b/packages/cli/src/actions/init.ts @@ -12,8 +12,8 @@ import { STARTER_ZMODEL } from './templates'; */ export async function run(projectPath: string) { const packages = [ - { name: '@zenstackhq/cli@next', dev: true }, - { name: '@zenstackhq/orm@next', dev: false }, + { name: '@zenstackhq/cli@latest', dev: true }, + { name: '@zenstackhq/orm@latest', dev: false }, ]; let pm = await detect(); if (!pm) { diff --git a/packages/clients/client-helpers/eslint.config.js b/packages/clients/client-helpers/eslint.config.js new file mode 100644 index 00000000..5698b991 --- /dev/null +++ b/packages/clients/client-helpers/eslint.config.js @@ -0,0 +1,4 @@ +import config from '@zenstackhq/eslint-config/base.js'; + +/** @type {import("eslint").Linter.Config} */ +export default config; diff --git a/packages/clients/client-helpers/package.json b/packages/clients/client-helpers/package.json new file mode 100644 index 00000000..4642852b --- /dev/null +++ b/packages/clients/client-helpers/package.json @@ -0,0 +1,40 @@ +{ + "name": "@zenstackhq/client-helpers", + "version": "3.1.0", + "description": "Helpers for implementing clients that consume ZenStack's CRUD service", + "type": "module", + "scripts": { + "build": "tsc --noEmit && tsup-node && pnpm test:typecheck", + "watch": "tsup-node --watch", + "lint": "eslint src --ext ts", + "test": "vitest run", + "test:typecheck": "tsc --noEmit --project tsconfig.test.json", + "pack": "pnpm pack" + }, + "author": "ZenStack Team", + "license": "MIT", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + }, + "./fetch": { + "types": "./dist/fetch.d.ts", + "default": "./dist/fetch.js" + } + }, + "dependencies": { + "@zenstackhq/common-helpers": "workspace:*", + "@zenstackhq/schema": "workspace:*", + "decimal.js": "catalog:", + "superjson": "^2.2.3" + }, + "devDependencies": { + "@zenstackhq/eslint-config": "workspace:*", + "@zenstackhq/language": "workspace:*", + "@zenstackhq/orm": "workspace:*", + "@zenstackhq/sdk": "workspace:*", + "@zenstackhq/typescript-config": "workspace:*", + "@zenstackhq/vitest-config": "workspace:*" + } +} diff --git a/packages/clients/client-helpers/src/constants.ts b/packages/clients/client-helpers/src/constants.ts new file mode 100644 index 00000000..ced31e94 --- /dev/null +++ b/packages/clients/client-helpers/src/constants.ts @@ -0,0 +1,4 @@ +/** + * The default query endpoint. + */ +export const DEFAULT_QUERY_ENDPOINT = '/api/model'; diff --git a/packages/clients/client-helpers/src/fetch.ts b/packages/clients/client-helpers/src/fetch.ts new file mode 100644 index 00000000..e4f3e8c7 --- /dev/null +++ b/packages/clients/client-helpers/src/fetch.ts @@ -0,0 +1,107 @@ +import { lowerCaseFirst } from '@zenstackhq/common-helpers'; +import Decimal from 'decimal.js'; +import SuperJSON from 'superjson'; +import type { QueryError } from './types'; + +/** + * Function signature for `fetch`. + */ +export type FetchFn = (url: string, options?: RequestInit) => Promise; + +/** + * A fetcher function that uses fetch API to make HTTP requests and automatically unmarshals + * the response using superjson. + */ +export async function fetcher(url: string, options?: RequestInit, customFetch?: FetchFn): Promise { + const _fetch = customFetch ?? fetch; + const res = await _fetch(url, options); + if (!res.ok) { + const errData = unmarshal(await res.text()); + if (errData.error?.rejectedByPolicy && errData.error?.rejectReason === 'cannot-read-back') { + // policy doesn't allow mutation result to be read back, just return undefined + return undefined as any; + } + const error: QueryError = new Error('An error occurred while fetching the data.'); + error.info = errData.error; + error.status = res.status; + throw error; + } + + const textResult = await res.text(); + try { + return unmarshal(textResult).data as R; + } catch (err) { + console.error(`Unable to deserialize data:`, textResult); + throw err; + } +} + +/** + * Makes a URL for the given endpoint, model, operation, and args that matches the RPC-style server API. + */ +export function makeUrl(endpoint: string, model: string, operation: string, args?: unknown) { + const baseUrl = `${endpoint}/${lowerCaseFirst(model)}/${operation}`; + if (!args) { + return baseUrl; + } + + const { data, meta } = serialize(args); + let result = `${baseUrl}?q=${encodeURIComponent(JSON.stringify(data))}`; + if (meta) { + result += `&meta=${encodeURIComponent(JSON.stringify({ serialization: meta }))}`; + } + return result; +} + +SuperJSON.registerCustom( + { + isApplicable: (v): v is Decimal => + v instanceof Decimal || + // interop with decimal.js + v?.toStringTag === '[object Decimal]', + serialize: (v) => v.toJSON(), + deserialize: (v) => new Decimal(v), + }, + 'Decimal', +); + +/** + * Serialize the given value with superjson + */ +export function serialize(value: unknown): { data: unknown; meta: unknown } { + const { json, meta } = SuperJSON.serialize(value); + return { data: json, meta }; +} + +/** + * Deserialize the given value with superjson using the given metadata + */ +export function deserialize(value: unknown, meta: any): unknown { + return SuperJSON.deserialize({ json: value as any, meta }); +} + +/** + * Marshal the given value to a string using superjson + */ +export function marshal(value: unknown) { + const { data, meta } = serialize(value); + if (meta) { + return JSON.stringify({ ...(data as any), meta: { serialization: meta } }); + } else { + return JSON.stringify(data); + } +} + +/** + * Unmarshal the given string value using superjson, assuming the value is a JSON stringified + * object containing the serialized data and serialization metadata. + */ +export function unmarshal(value: string) { + const parsed = JSON.parse(value); + if (typeof parsed === 'object' && parsed?.data && parsed?.meta?.serialization) { + const deserializedData = deserialize(parsed.data, parsed.meta.serialization); + return { ...parsed, data: deserializedData }; + } else { + return parsed; + } +} diff --git a/packages/clients/client-helpers/src/index.ts b/packages/clients/client-helpers/src/index.ts new file mode 100644 index 00000000..e1ea44b8 --- /dev/null +++ b/packages/clients/client-helpers/src/index.ts @@ -0,0 +1,9 @@ +export * from './constants'; +export * from './invalidation'; +export * from './logging'; +export * from './mutator'; +export * from './nested-read-visitor'; +export * from './nested-write-visitor'; +export * from './optimistic'; +export * from './query-analysis'; +export * from './types'; diff --git a/packages/clients/client-helpers/src/invalidation.ts b/packages/clients/client-helpers/src/invalidation.ts new file mode 100644 index 00000000..1289a881 --- /dev/null +++ b/packages/clients/client-helpers/src/invalidation.ts @@ -0,0 +1,89 @@ +import type { SchemaDef } from '@zenstackhq/schema'; +import { log, type Logger } from './logging'; +import { getMutatedModels, getReadModels } from './query-analysis'; +import type { MaybePromise, ORMWriteActionType } from './types'; + +/** + * Type for a predicate that determines whether a query should be invalidated. + */ +export type InvalidationPredicate = ({ model, args }: { model: string; args: unknown }) => boolean; + +/** + * Type for a function that invalidates queries matching the given predicate. + */ +export type InvalidateFunc = (predicate: InvalidationPredicate) => MaybePromise; + +/** + * Create a function that invalidates queries affected by the given mutation operation. + * + * @param model Model under mutation. + * @param operation Mutation operation (e.g, `update`). + * @param schema The schema. + * @param invalidator Function to invalidate queries matching a predicate. It should internally + * enumerate all query cache entries and invalidate those for which the predicate returns true. + * @param logging Logging option. + */ +export function createInvalidator( + model: string, + operation: string, + schema: SchemaDef, + invalidator: InvalidateFunc, + logging: Logger | undefined, +) { + return async (...args: unknown[]) => { + const [_, variables] = args; + const predicate = await getInvalidationPredicate( + model, + operation as ORMWriteActionType, + variables, + schema, + logging, + ); + await invalidator(predicate); + }; +} + +// gets a predicate for evaluating whether a query should be invalidated +async function getInvalidationPredicate( + model: string, + operation: ORMWriteActionType, + mutationArgs: any, + schema: SchemaDef, + logging: Logger | undefined, +): Promise { + const mutatedModels = await getMutatedModels(model, operation, mutationArgs, schema); + + return ({ model, args }) => { + if (mutatedModels.includes(model)) { + // direct match + if (logging) { + log( + logging, + `Marking "${model}" query for invalidation due to mutation "${operation}", query args: ${JSON.stringify(args)}`, + ); + } + return true; + } + + if (args) { + // traverse query args to find nested reads that match the model under mutation + if (findNestedRead(model, mutatedModels, schema, args)) { + if (logging) { + log( + logging, + `Marking "${model}" query for invalidation due to mutation "${operation}", query args: ${JSON.stringify(args)}`, + ); + } + return true; + } + } + + return false; + }; +} + +// find nested reads that match the given models +function findNestedRead(visitingModel: string, targetModels: string[], schema: SchemaDef, args: any) { + const modelsRead = getReadModels(visitingModel, schema, args); + return targetModels.some((m) => modelsRead.includes(m)); +} diff --git a/packages/clients/client-helpers/src/logging.ts b/packages/clients/client-helpers/src/logging.ts new file mode 100644 index 00000000..3ccf12c3 --- /dev/null +++ b/packages/clients/client-helpers/src/logging.ts @@ -0,0 +1,15 @@ +/** + * Logger configuration. `true` enables console logging. A function can be provided for custom logging. + */ +export type Logger = boolean | ((message: string) => void); + +/** + * Logs a message using the provided logger. + */ +export function log(logger: Logger, message: string) { + if (typeof logger === 'function') { + logger(message); + } else if (logger) { + console.log(message); + } +} diff --git a/packages/clients/tanstack-query/src/utils/mutator.ts b/packages/clients/client-helpers/src/mutator.ts similarity index 93% rename from packages/clients/tanstack-query/src/utils/mutator.ts rename to packages/clients/client-helpers/src/mutator.ts index 5d131dd9..35bdc9a6 100644 --- a/packages/clients/tanstack-query/src/utils/mutator.ts +++ b/packages/clients/client-helpers/src/mutator.ts @@ -1,5 +1,6 @@ import { clone, enumerate, invariant, zip } from '@zenstackhq/common-helpers'; import type { FieldDef, SchemaDef } from '@zenstackhq/schema'; +import { log, type Logger } from './logging'; import { NestedWriteVisitor } from './nested-write-visitor'; import type { ORMWriteActionType } from './types'; @@ -12,8 +13,8 @@ import type { ORMWriteActionType } from './types'; * @param mutationModel the model of the mutation * @param mutationOp the operation of the mutation * @param mutationArgs the arguments of the mutation - * @param schema the model metadata - * @param logging whether to log the mutation application + * @param schema the schema + * @param logging logging configuration * @returns the updated query data if the mutation is applicable, otherwise undefined */ export async function applyMutation( @@ -24,7 +25,7 @@ export async function applyMutation( mutationOp: ORMWriteActionType, mutationArgs: any, schema: SchemaDef, - logging: boolean, + logging: Logger | undefined, ) { if (!queryData || (typeof queryData !== 'object' && !Array.isArray(queryData))) { return undefined; @@ -45,7 +46,7 @@ async function doApplyMutation( mutationOp: ORMWriteActionType, mutationArgs: any, schema: SchemaDef, - logging: boolean, + logging: Logger | undefined, ) { let resultData = queryData; let updated = false; @@ -176,7 +177,13 @@ async function doApplyMutation( return updated ? resultData : undefined; } -function createMutate(queryModel: string, currentData: any, newData: any, schema: SchemaDef, logging: boolean) { +function createMutate( + queryModel: string, + currentData: any, + newData: any, + schema: SchemaDef, + logging: Logger | undefined, +) { if (!newData) { return undefined; } @@ -239,8 +246,9 @@ function createMutate(queryModel: string, currentData: any, newData: any, schema insert.$optimistic = true; if (logging) { - console.log(`Optimistic create for ${queryModel}:`, insert); + log(logging, `Applying optimistic create for ${queryModel}: ${JSON.stringify(insert)}`); } + return [insert, ...(Array.isArray(currentData) ? currentData : [])]; } @@ -250,7 +258,7 @@ function updateMutate( mutateModel: string, mutateArgs: any, schema: SchemaDef, - logging: boolean, + logging: Logger | undefined, ) { if (!currentData || typeof currentData !== 'object') { return undefined; @@ -302,7 +310,7 @@ function updateMutate( updated = true; if (logging) { - console.log(`Optimistic update for ${queryModel}:`, resultData); + log(logging, `Applying optimistic update for ${queryModel}: ${JSON.stringify(resultData)}`); } } @@ -315,7 +323,7 @@ function upsertMutate( model: string, args: { where: object; create: any; update: any }, schema: SchemaDef, - logging: boolean, + logging: Logger | undefined, ) { let updated = false; let resultData = currentData; @@ -369,7 +377,7 @@ function deleteMutate( mutateModel: string, mutateArgs: any, schema: SchemaDef, - logging: boolean, + logging: Logger | undefined, ) { // TODO: handle mutation of nested reads? @@ -390,7 +398,7 @@ function deleteMutate( result = (result as unknown[]).filter((x) => x !== item); updated = true; if (logging) { - console.log(`Optimistic delete for ${queryModel}:`, item); + log(logging, `Applying optimistic delete for ${queryModel}: ${JSON.stringify(item)}`); } } } @@ -399,7 +407,7 @@ function deleteMutate( result = null; updated = true; if (logging) { - console.log(`Optimistic delete for ${queryModel}:`, currentData); + log(logging, `Applying optimistic delete for ${queryModel}: ${JSON.stringify(currentData)}`); } } } diff --git a/packages/clients/tanstack-query/src/utils/nested-read-visitor.ts b/packages/clients/client-helpers/src/nested-read-visitor.ts similarity index 85% rename from packages/clients/tanstack-query/src/utils/nested-read-visitor.ts rename to packages/clients/client-helpers/src/nested-read-visitor.ts index 1a4323ea..74ba7070 100644 --- a/packages/clients/tanstack-query/src/utils/nested-read-visitor.ts +++ b/packages/clients/client-helpers/src/nested-read-visitor.ts @@ -1,6 +1,13 @@ import type { FieldDef, SchemaDef } from '@zenstackhq/schema'; +/** + * Callback functions for nested read visitor. + */ export type NestedReadVisitorCallback = { + /** + * Callback for each field visited. + * @returns If returns false, traversal will not continue into this field. + */ field?: ( model: string, field: FieldDef | undefined, @@ -18,7 +25,7 @@ export class NestedReadVisitor { private readonly callback: NestedReadVisitorCallback, ) {} - doVisit(model: string, field: FieldDef | undefined, kind: 'include' | 'select' | undefined, args: unknown) { + private doVisit(model: string, field: FieldDef | undefined, kind: 'include' | 'select' | undefined, args: unknown) { if (this.callback.field) { const r = this.callback.field(model, field, kind, args); if (r === false) { diff --git a/packages/clients/tanstack-query/src/utils/nested-write-visitor.ts b/packages/clients/client-helpers/src/nested-write-visitor.ts similarity index 98% rename from packages/clients/tanstack-query/src/utils/nested-write-visitor.ts rename to packages/clients/client-helpers/src/nested-write-visitor.ts index 06e89b7e..14ca1e40 100644 --- a/packages/clients/tanstack-query/src/utils/nested-write-visitor.ts +++ b/packages/clients/client-helpers/src/nested-write-visitor.ts @@ -29,7 +29,7 @@ export type NestedWriteVisitorContext = { * that the visitor should continue traversing its children, or false to stop. It can also return an object * to let the visitor traverse it instead of its original children. */ -export type NestedWriterVisitorCallback = { +export type NestedWriteVisitorCallback = { create?: (model: string, data: any, context: NestedWriteVisitorContext) => MaybePromise; createMany?: ( @@ -98,7 +98,7 @@ export type NestedWriterVisitorCallback = { export class NestedWriteVisitor { constructor( private readonly schema: SchemaDef, - private readonly callback: NestedWriterVisitorCallback, + private readonly callback: NestedWriteVisitorCallback, ) {} private isWriteAction(value: string): value is ORMWriteActionType { @@ -108,7 +108,7 @@ export class NestedWriteVisitor { /** * Start visiting * - * @see NestedWriterVisitorCallback + * @see NestedWriteVisitorCallback */ async visit(model: string, action: ORMWriteActionType, args: any): Promise { if (!args) { diff --git a/packages/clients/client-helpers/src/optimistic.ts b/packages/clients/client-helpers/src/optimistic.ts new file mode 100644 index 00000000..1e7da06a --- /dev/null +++ b/packages/clients/client-helpers/src/optimistic.ts @@ -0,0 +1,139 @@ +import type { SchemaDef } from '@zenstackhq/schema'; +import { log, type Logger } from './logging'; +import { applyMutation } from './mutator'; +import type { ORMWriteActionType, QueryInfo } from './types'; + +/** + * Custom optimistic data provider. It takes query information (usually fetched from query cache) + * and returns a verdict on how to optimistically update the query data. + * + * @param args Arguments. + * @param args.queryModel The model of the query. + * @param args.queryOperation The operation of the query, `findMany`, `count`, etc. + * @param args.queryArgs The arguments of the query. + * @param args.currentData The current cache data for the query. + * @param args.mutationArgs The arguments of the mutation. + */ +export type OptimisticDataProvider = (args: { + queryModel: string; + queryOperation: string; + queryArgs: any; + currentData: any; + mutationArgs: any; +}) => OptimisticDataProviderResult | Promise; + +/** + * Result of optimistic data provider. + */ +export type OptimisticDataProviderResult = { + /** + * Kind of the result. + * - Update: use the `data` field to update the query cache. + * - Skip: skip the optimistic update for this query. + * - ProceedDefault: proceed with the default optimistic update. + */ + kind: 'Update' | 'Skip' | 'ProceedDefault'; + + /** + * Data to update the query cache. Only applicable if `kind` is 'Update'. + * + * If the data is an object with fields updated, it should have a `$optimistic` + * field set to `true`. If it's an array and an element object is created or updated, + * the element should have a `$optimistic` field set to `true`. + */ + data?: any; +}; + +/** + * Options for optimistic update. + */ +export type OptimisticUpdateOptions = { + /** + * A custom optimistic data provider. + */ + optimisticDataProvider?: OptimisticDataProvider; +}; + +/** + * Creates a function that performs optimistic updates for queries potentially + * affected by the given mutation operation. + * + * @param model Model under mutation. + * @param operation Mutation operation (e.g, `update`). + * @param schema The schema. + * @param options Optimistic update options. + * @param getAllQueries Callback to get all cached queries. + * @param logging Logging option. + */ +export function createOptimisticUpdater( + model: string, + operation: string, + schema: SchemaDef, + options: OptimisticUpdateOptions, + getAllQueries: () => readonly QueryInfo[], + logging: Logger | undefined, +) { + return async (...args: unknown[]) => { + const [mutationArgs] = args; + + for (const queryInfo of getAllQueries()) { + const logInfo = JSON.stringify({ + model: queryInfo.model, + operation: queryInfo.operation, + args: queryInfo.args, + }); + + if (!queryInfo.optimisticUpdate) { + if (logging) { + log(logging, `Skipping optimistic update for ${logInfo} due to opt-out`); + } + continue; + } + + if (options.optimisticDataProvider) { + const providerResult = await options.optimisticDataProvider({ + queryModel: queryInfo.model, + queryOperation: queryInfo.operation, + queryArgs: queryInfo.args, + currentData: queryInfo.data, + mutationArgs, + }); + + if (providerResult?.kind === 'Skip') { + // skip + if (logging) { + log(logging, `Skipping optimistic updating due to provider result: ${logInfo}`); + } + continue; + } else if (providerResult?.kind === 'Update') { + // update cache + if (logging) { + log(logging, `Optimistically updating due to provider result: ${logInfo}`); + } + queryInfo.updateData(providerResult.data, true); + continue; + } + } + + // proceed with default optimistic update + const mutatedData = await applyMutation( + queryInfo.model, + queryInfo.operation, + queryInfo.data, + model, + operation as ORMWriteActionType, + mutationArgs, + schema, + logging, + ); + + if (mutatedData !== undefined) { + // mutation applicable to this query, update cache + if (logging) { + log(logging, `Optimistically updating due to mutation "${model}.${operation}": ${logInfo}`); + } + queryInfo.updateData(mutatedData, true); + } + } + }; +} diff --git a/packages/clients/tanstack-query/src/utils/query-analysis.ts b/packages/clients/client-helpers/src/query-analysis.ts similarity index 97% rename from packages/clients/tanstack-query/src/utils/query-analysis.ts rename to packages/clients/client-helpers/src/query-analysis.ts index ccc2af90..98db8f94 100644 --- a/packages/clients/tanstack-query/src/utils/query-analysis.ts +++ b/packages/clients/client-helpers/src/query-analysis.ts @@ -5,11 +5,6 @@ import type { ORMWriteActionType } from './types'; /** * Gets models read (including nested ones) given a query args. - * @param model - * @param targetModels - * @param schema - * @param args - * @returns */ export function getReadModels(model: string, schema: SchemaDef, args: any) { const result = new Set(); diff --git a/packages/clients/client-helpers/src/types.ts b/packages/clients/client-helpers/src/types.ts new file mode 100644 index 00000000..da66f948 --- /dev/null +++ b/packages/clients/client-helpers/src/types.ts @@ -0,0 +1,82 @@ +/** + * A type that represents either a value of type T or a Promise that resolves to type T. + */ +export type MaybePromise = T | Promise | PromiseLike; + +/** + * List of ORM write actions. + */ +export const ORMWriteActions = [ + 'create', + 'createMany', + 'createManyAndReturn', + 'connectOrCreate', + 'update', + 'updateMany', + 'updateManyAndReturn', + 'upsert', + 'connect', + 'disconnect', + 'set', + 'delete', + 'deleteMany', +] as const; + +/** + * Type representing ORM write action types. + */ +export type ORMWriteActionType = (typeof ORMWriteActions)[number]; + +/** + * Type for query and mutation errors. + */ +export type QueryError = Error & { + /** + * Additional error information. + */ + info?: unknown; + + /** + * HTTP status code. + */ + status?: number; +}; + +/** + * Information about a cached query. + */ +export type QueryInfo = { + /** + * Model of the query. + */ + model: string; + + /** + * Query operation, e.g., `findUnique` + */ + operation: string; + + /** + * Query arguments. + */ + args: unknown; + + /** + * Current data cached for this query. + */ + data: unknown; + + /** + * Whether optimistic update is enabled for this query. + */ + optimisticUpdate: boolean; + + /** + * Function to update the cached data. + * + * @param data New data to set. + * @param cancelOnTheFlyQueries Whether to cancel on-the-fly queries to avoid accidentally + * overwriting the optimistic update. + */ + updateData: (data: unknown, cancelOnTheFlyQueries: boolean) => void; +}; diff --git a/packages/clients/client-helpers/test/fetch.test.ts b/packages/clients/client-helpers/test/fetch.test.ts new file mode 100644 index 00000000..cc69d0b6 --- /dev/null +++ b/packages/clients/client-helpers/test/fetch.test.ts @@ -0,0 +1,423 @@ +import Decimal from 'decimal.js'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { deserialize, fetcher, makeUrl, marshal, serialize, unmarshal } from '../src/fetch'; +import type { QueryError } from '../src/types'; + +describe('Fetcher and serialization tests', () => { + describe('serialize and deserialize', () => { + it('serializes plain objects', () => { + const input = { name: 'John', age: 30 }; + const result = serialize(input); + expect(result.data).toEqual(input); + expect(result.meta).toBeUndefined(); + }); + + it('serializes and deserializes Decimal values', () => { + const input = { price: new Decimal('123.45') }; + const { data, meta } = serialize(input); + const result = deserialize(data, meta); + expect(result).toEqual(input); + expect((result as any).price).toBeInstanceOf(Decimal); + expect((result as any).price.toString()).toBe('123.45'); + }); + + it('serializes and deserializes Date values', () => { + const input = { createdAt: new Date('2023-01-15T12:00:00Z') }; + const { data, meta } = serialize(input); + const result = deserialize(data, meta); + expect(result).toEqual(input); + expect((result as any).createdAt).toBeInstanceOf(Date); + }); + + it('serializes complex nested objects with special types', () => { + const input = { + user: { + name: 'Alice', + balance: new Decimal('999.99'), + createdAt: new Date('2023-01-01T00:00:00Z'), + }, + items: [{ price: new Decimal('10.50') }, { price: new Decimal('20.75') }], + }; + const { data, meta } = serialize(input); + const result = deserialize(data, meta); + + expect((result as any).user.balance).toBeInstanceOf(Decimal); + expect((result as any).user.balance.toString()).toBe('999.99'); + expect((result as any).user.createdAt).toBeInstanceOf(Date); + expect((result as any).items[0].price).toBeInstanceOf(Decimal); + expect((result as any).items[1].price.toString()).toBe('20.75'); + }); + + it('handles undefined and null values', () => { + const input = { foo: undefined, bar: null }; + const { data, meta } = serialize(input); + const result = deserialize(data, meta); + expect(result).toEqual({ bar: null }); + }); + + it('handles arrays with mixed types', () => { + const input = [new Decimal('1.23'), 'string', 42, new Date('2023-01-01T00:00:00Z')]; + const { data, meta } = serialize(input); + const result = deserialize(data, meta) as any[]; + + expect(result[0]).toBeInstanceOf(Decimal); + expect(result[1]).toBe('string'); + expect(result[2]).toBe(42); + expect(result[3]).toBeInstanceOf(Date); + }); + }); + + describe('marshal and unmarshal', () => { + it('marshals and unmarshals plain objects', () => { + const input = { name: 'John', age: 30 }; + const marshaled = marshal(input); + const result = unmarshal(marshaled); + expect(result).toEqual(input); + }); + + it('marshals objects without metadata when not needed', () => { + const input = { name: 'John', age: 30 }; + const marshaled = marshal(input); + const parsed = JSON.parse(marshaled); + expect(parsed.meta).toBeUndefined(); + }); + + it('marshals and unmarshals objects with Decimal values', () => { + const input = { price: new Decimal('123.45') }; + const marshaled = marshal(input); + const parsed = JSON.parse(marshaled); + + // marshal spreads the data into the root object with meta + expect(parsed.price).toBeDefined(); + expect(parsed.meta).toBeDefined(); + expect(parsed.meta.serialization).toBeDefined(); + + // unmarshal doesn't automatically deserialize this format + // It only deserializes objects with explicit 'data' and 'meta.serialization' fields + const result = unmarshal(marshaled); + expect(result).toHaveProperty('price'); + expect(result).toHaveProperty('meta'); + }); + + it('includes metadata when serialization is needed', () => { + const input = { date: new Date('2023-01-01T00:00:00Z') }; + const marshaled = marshal(input); + const parsed = JSON.parse(marshaled); + expect(parsed.meta).toBeDefined(); + expect(parsed.meta.serialization).toBeDefined(); + }); + + it('unmarshals response format with data and meta', () => { + // Create properly serialized data using serialize/deserialize + const originalValue = { value: new Decimal('100.00') }; + const { data: serializedData, meta: serializedMeta } = serialize(originalValue); + + // Create the response format that unmarshal expects + const responseFormat = { + data: serializedData, + meta: { serialization: serializedMeta }, + }; + const marshaled = JSON.stringify(responseFormat); + + const result = unmarshal(marshaled); + expect(result.data).toBeDefined(); + expect((result.data as any).value).toBeInstanceOf(Decimal); + // Decimal normalizes '100.00' to '100' + expect((result.data as any).value.toString()).toBe('100'); + }); + + it('unmarshals plain values without data wrapper', () => { + const plainValue = { name: 'test' }; + const marshaled = JSON.stringify(plainValue); + const result = unmarshal(marshaled); + expect(result).toEqual(plainValue); + }); + }); + + describe('makeUrl', () => { + it('creates URL without args', () => { + const url = makeUrl('/api', 'User', 'findMany'); + expect(url).toBe('/api/user/findMany'); + }); + + it('creates URL with simple args', () => { + const args = { where: { id: '1' } }; + const url = makeUrl('/api', 'User', 'findUnique', args); + expect(url).toContain('/api/user/findUnique?q='); + expect(url).toContain(encodeURIComponent(JSON.stringify(args))); + }); + + it('lowercases first letter of model name', () => { + const url = makeUrl('/api', 'BlogPost', 'findMany'); + expect(url).toBe('/api/blogPost/findMany'); + }); + + it('creates URL with args containing special types', () => { + const args = { + where: { + price: new Decimal('99.99'), + createdAt: new Date('2023-01-01T00:00:00Z'), + }, + }; + const url = makeUrl('/api', 'Product', 'findFirst', args); + + expect(url).toContain('/api/product/findFirst?q='); + expect(url).toContain('&meta='); + + // Verify we can reconstruct the args from the URL + const urlObj = new URL(url, 'http://localhost'); + const qParam = urlObj.searchParams.get('q'); + const metaParam = urlObj.searchParams.get('meta'); + + expect(qParam).toBeDefined(); + expect(metaParam).toBeDefined(); + + const reconstructed = deserialize(JSON.parse(qParam!), JSON.parse(metaParam!).serialization); + expect((reconstructed as any).where.price).toBeInstanceOf(Decimal); + expect((reconstructed as any).where.createdAt).toBeInstanceOf(Date); + }); + + it('handles empty args object', () => { + const url = makeUrl('/api', 'User', 'findMany', {}); + expect(url).toContain('/api/user/findMany?q='); + }); + + it('handles complex nested args', () => { + const args = { + include: { posts: true }, + where: { AND: [{ active: true }, { verified: true }] }, + }; + const url = makeUrl('/api', 'User', 'findMany', args); + expect(url).toContain('/api/user/findMany?q='); + expect(url).toContain(encodeURIComponent(JSON.stringify(args))); + }); + }); + + describe('fetcher', () => { + let mockFetch: ReturnType; + const originalFetch = globalThis.fetch; + + beforeEach(() => { + mockFetch = vi.fn(); + global.fetch = mockFetch as typeof global.fetch; + }); + + afterEach(() => { + globalThis.fetch = originalFetch; + vi.resetAllMocks(); + }); + + it('successfully fetches and deserializes data', async () => { + const responseData = { id: '1', name: 'Alice' }; + mockFetch.mockResolvedValue({ + ok: true, + text: async () => marshal({ data: responseData }), + }); + + const result = await fetcher('/api/user/findUnique', {}); + + expect(result).toEqual(responseData); + expect(mockFetch).toHaveBeenCalledWith('/api/user/findUnique', {}); + }); + + it('deserializes response with special types', async () => { + const responseData = { + id: '1', + balance: new Decimal('500.50'), + createdAt: new Date('2023-01-01T00:00:00Z'), + }; + + // Simulate server response format: { data: {...}, meta: { serialization: {...} } } + const { data: serializedData, meta: serializedMeta } = serialize(responseData); + const serverResponse = JSON.stringify({ + data: serializedData, + meta: { serialization: serializedMeta }, + }); + + mockFetch.mockResolvedValue({ + ok: true, + text: async () => serverResponse, + }); + + const result = await fetcher('/api/user/findUnique', {}); + + expect(result.balance).toBeInstanceOf(Decimal); + expect(result.balance.toString()).toBe('500.5'); + expect(result.createdAt).toBeInstanceOf(Date); + }); + + it('throws QueryError on non-ok response', async () => { + const errorInfo = { code: 'NOT_FOUND', message: 'User not found' }; + mockFetch.mockResolvedValue({ + ok: false, + status: 404, + text: async () => JSON.stringify({ error: errorInfo }), + }); + + await expect(fetcher('/api/user/findUnique', {})).rejects.toThrow( + 'An error occurred while fetching the data.', + ); + + try { + await fetcher('/api/user/findUnique', {}); + } catch (error) { + const queryError = error as QueryError; + expect(queryError.status).toBe(404); + expect(queryError.info).toEqual(errorInfo); + } + }); + + it('returns undefined for cannot-read-back policy rejection', async () => { + const errorInfo = { + rejectedByPolicy: true, + rejectReason: 'cannot-read-back', + }; + mockFetch.mockResolvedValue({ + ok: false, + status: 403, + text: async () => JSON.stringify({ error: errorInfo }), + }); + + const result = await fetcher('/api/user/create', {}); + + expect(result).toBeUndefined(); + }); + + it('throws error for other policy rejections', async () => { + const errorInfo = { + rejectedByPolicy: true, + rejectReason: 'access-denied', + }; + mockFetch.mockResolvedValue({ + ok: false, + status: 403, + text: async () => JSON.stringify({ error: errorInfo }), + }); + + await expect(fetcher('/api/user/create', {})).rejects.toThrow(); + }); + + it('use custom fetch if provided', async () => { + const customFetch = vi.fn().mockResolvedValue({ + ok: true, + text: async () => marshal({ data: { id: '1', name: 'Custom' } }), + }); + + const result = await fetcher('/api/user/findUnique', {}, customFetch); + + // Custom fetch should be called instead of global fetch + expect(customFetch).toHaveBeenCalledWith('/api/user/findUnique', {}); + expect(customFetch).toHaveBeenCalledTimes(1); + expect(mockFetch).not.toHaveBeenCalled(); + expect(result).toEqual({ id: '1', name: 'Custom' }); + }); + + it('passes request options to fetch', async () => { + const responseData = { id: '1' }; + mockFetch.mockResolvedValue({ + ok: true, + text: async () => marshal({ data: responseData }), + }); + + const options: RequestInit = { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ name: 'test' }), + }; + + await fetcher('/api/user/create', options); + + expect(mockFetch).toHaveBeenCalledWith('/api/user/create', options); + }); + + it('handles empty response body', async () => { + mockFetch.mockResolvedValue({ + ok: true, + text: async () => marshal({ data: null }), + }); + + const result = await fetcher('/api/user/delete', {}); + expect(result).toBeNull(); + }); + + it('handles array responses', async () => { + const responseData = [ + { id: '1', name: 'Alice' }, + { id: '2', name: 'Bob' }, + ]; + mockFetch.mockResolvedValue({ + ok: true, + text: async () => marshal({ data: responseData }), + }); + + const result = await fetcher('/api/user/findMany', {}); + + expect(Array.isArray(result)).toBe(true); + expect(result).toHaveLength(2); + expect(result[0]?.name).toBe('Alice'); + expect(result[1]?.name).toBe('Bob'); + }); + + it('preserves response data structure with nested objects', async () => { + const responseData = { + id: '1', + name: 'Alice', + posts: [ + { id: 'p1', title: 'Post 1', viewCount: new Decimal('100') }, + { id: 'p2', title: 'Post 2', viewCount: new Decimal('200') }, + ], + }; + + // Simulate server response format + const { data: serializedData, meta: serializedMeta } = serialize(responseData); + const serverResponse = JSON.stringify({ + data: serializedData, + meta: { serialization: serializedMeta }, + }); + + mockFetch.mockResolvedValue({ + ok: true, + text: async () => serverResponse, + }); + + const result = await fetcher('/api/user/findUnique', {}); + + expect(result.posts).toHaveLength(2); + expect(result.posts[0]?.viewCount).toBeInstanceOf(Decimal); + expect(result.posts[0]?.viewCount.toString()).toBe('100'); + expect(result.posts[1]?.viewCount.toString()).toBe('200'); + }); + }); + + describe('Decimal custom serializer', () => { + it('handles Decimal instances', () => { + const value = new Decimal('123.456'); + const { data, meta } = serialize({ value }); + const result = deserialize(data, meta); + expect((result as any).value).toBeInstanceOf(Decimal); + expect((result as any).value.toString()).toBe('123.456'); + }); + + it('handles negative Decimal values', () => { + const value = new Decimal('-99.99'); + const { data, meta } = serialize({ value }); + const result = deserialize(data, meta); + expect((result as any).value.toString()).toBe('-99.99'); + }); + + it('handles very large Decimal values', () => { + const value = new Decimal('999999999999999999.999999999999'); + const { data, meta } = serialize({ value }); + const result = deserialize(data, meta); + expect((result as any).value).toBeInstanceOf(Decimal); + expect((result as any).value.toString()).toBe(value.toString()); + }); + + it('handles zero Decimal value', () => { + const value = new Decimal('0'); + const { data, meta } = serialize({ value }); + const result = deserialize(data, meta); + expect((result as any).value.toString()).toBe('0'); + }); + }); +}); diff --git a/packages/clients/client-helpers/test/invalidation.test.ts b/packages/clients/client-helpers/test/invalidation.test.ts new file mode 100644 index 00000000..ed301e85 --- /dev/null +++ b/packages/clients/client-helpers/test/invalidation.test.ts @@ -0,0 +1,602 @@ +import { describe, expect, it, vi } from 'vitest'; +import { createInvalidator } from '../src/invalidation'; +import type { Logger } from '../src/logging'; +import { createField, createRelationField, createSchema } from './test-helpers'; + +describe('Invalidation tests', () => { + describe('createInvalidator', () => { + it('creates an invalidator function that invalidates the mutated model', async () => { + const schema = createSchema({ + User: { + name: 'User', + fields: { + id: createField('id', 'String'), + name: createField('name', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + let capturedPredicate: any; + const invalidatorMock = vi.fn((predicate) => { + capturedPredicate = predicate; + }); + + const invalidator = createInvalidator('User', 'create', schema, invalidatorMock, undefined); + + // Call the invalidator with mutation result and variables + const result = { id: '1', name: 'John' }; + const variables = { data: { name: 'John' } }; + await invalidator(result, variables); + + // Invalidator should have been called + expect(invalidatorMock).toHaveBeenCalledTimes(1); + expect(invalidatorMock).toHaveBeenCalledWith(expect.any(Function)); + + // Test the predicate + expect(capturedPredicate({ model: 'User', args: {} })).toBe(true); + }); + + it('invalidates nested models from mutation', async () => { + const schema = createSchema({ + User: { + name: 'User', + fields: { + id: createField('id', 'String'), + posts: createRelationField('posts', 'Post'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + Post: { + name: 'Post', + fields: { + id: createField('id', 'String'), + title: createField('title', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + let capturedPredicate: any; + const invalidatorMock = vi.fn((predicate) => { + capturedPredicate = predicate; + }); + + const invalidator = createInvalidator('User', 'create', schema, invalidatorMock, undefined); + + // Create user with nested post + await invalidator( + {}, + { + data: { + name: 'John', + posts: { + create: { title: 'My Post' }, + }, + }, + }, + ); + + // Should invalidate both User and Post + expect(capturedPredicate({ model: 'User', args: {} })).toBe(true); + expect(capturedPredicate({ model: 'Post', args: {} })).toBe(true); + }); + + it('works with undefined logging', async () => { + const schema = createSchema({ + User: { + name: 'User', + fields: { + id: createField('id', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + const invalidatorMock = vi.fn(); + const invalidator = createInvalidator('User', 'create', schema, invalidatorMock, undefined); + + await invalidator({}, { data: {} }); + + expect(invalidatorMock).toHaveBeenCalled(); + }); + + it('logs when logger is provided', async () => { + const schema = createSchema({ + User: { + name: 'User', + fields: { + id: createField('id', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + const loggerMock = vi.fn() as Logger; + let capturedPredicate: any; + const invalidatorMock = vi.fn((predicate) => { + capturedPredicate = predicate; + }); + + const invalidator = createInvalidator('User', 'create', schema, invalidatorMock, loggerMock); + + await invalidator({}, { data: { name: 'John' } }); + + // Execute the predicate to trigger logging + capturedPredicate({ model: 'User', args: {} }); + + // Logger should have been called + expect(loggerMock).toHaveBeenCalledWith(expect.stringContaining('Marking "User" query for invalidation')); + }); + + it('handles multiple mutations with different operations', async () => { + const schema = createSchema({ + User: { + name: 'User', + fields: { + id: createField('id', 'String'), + name: createField('name', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + const capturedPredicates: any[] = []; + const invalidatorMock = vi.fn((predicate) => { + capturedPredicates.push(predicate); + }); + + // Create invalidators for different operations + const createInvalidatorFn = createInvalidator('User', 'create', schema, invalidatorMock, undefined); + const updateInvalidatorFn = createInvalidator('User', 'update', schema, invalidatorMock, undefined); + const deleteInvalidatorFn = createInvalidator('User', 'delete', schema, invalidatorMock, undefined); + + // Execute each invalidator + await createInvalidatorFn({}, { data: { name: 'John' } }); + await updateInvalidatorFn({}, { where: { id: '1' }, data: { name: 'Jane' } }); + await deleteInvalidatorFn({}, { where: { id: '1' } }); + + // All should invalidate User queries + expect(capturedPredicates).toHaveLength(3); + capturedPredicates.forEach((predicate) => { + expect(predicate({ model: 'User', args: {} })).toBe(true); + }); + }); + + it('handles cascade deletes correctly', async () => { + const schema = createSchema({ + User: { + name: 'User', + fields: { + id: createField('id', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + Post: { + name: 'Post', + fields: { + id: createField('id', 'String'), + user: { + name: 'user', + type: 'User', + optional: false, + relation: { + opposite: 'posts', + onDelete: 'Cascade', + }, + }, + }, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + let capturedPredicate: any; + const invalidatorMock = vi.fn((predicate) => { + capturedPredicate = predicate; + }); + + const invalidator = createInvalidator('User', 'delete', schema, invalidatorMock, undefined); + + await invalidator({}, { where: { id: '1' } }); + + // Should invalidate both User and Post (cascade) + expect(capturedPredicate({ model: 'User', args: {} })).toBe(true); + expect(capturedPredicate({ model: 'Post', args: {} })).toBe(true); + }); + + it('handles base model inheritance', async () => { + const schema = createSchema({ + Animal: { + name: 'Animal', + fields: { + id: createField('id', 'String'), + name: createField('name', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + Dog: { + name: 'Dog', + baseModel: 'Animal', + fields: { + id: createField('id', 'String'), + breed: createField('breed', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + let capturedPredicate: any; + const invalidatorMock = vi.fn((predicate) => { + capturedPredicate = predicate; + }); + + const invalidator = createInvalidator('Dog', 'create', schema, invalidatorMock, undefined); + + await invalidator({}, { data: { breed: 'Labrador' } }); + + // Should invalidate both Dog and Animal (base) + expect(capturedPredicate({ model: 'Dog', args: {} })).toBe(true); + expect(capturedPredicate({ model: 'Animal', args: {} })).toBe(true); + }); + + it('handles async invalidator function', async () => { + const schema = createSchema({ + User: { + name: 'User', + fields: { + id: createField('id', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + const invalidatorMock = vi.fn(async () => { + await new Promise((resolve) => setTimeout(resolve, 10)); + }); + + const invalidator = createInvalidator('User', 'create', schema, invalidatorMock, undefined); + + await invalidator({}, { data: {} }); + + expect(invalidatorMock).toHaveBeenCalled(); + }); + + it('passes correct predicate for nested reads', async () => { + const schema = createSchema({ + User: { + name: 'User', + fields: { + id: createField('id', 'String'), + posts: createRelationField('posts', 'Post'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + Post: { + name: 'Post', + fields: { + id: createField('id', 'String'), + title: createField('title', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + Profile: { + name: 'Profile', + fields: { + id: createField('id', 'String'), + bio: createField('bio', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + let capturedPredicate: any; + const invalidatorMock = vi.fn((predicate) => { + capturedPredicate = predicate; + }); + + const invalidator = createInvalidator('Post', 'create', schema, invalidatorMock, undefined); + + await invalidator({}, { data: { title: 'New Post' } }); + + // Should invalidate User queries that include posts + expect( + capturedPredicate({ + model: 'User', + args: { + include: { posts: true }, + }, + }), + ).toBe(true); + + // Should not invalidate User queries without posts + expect( + capturedPredicate({ + model: 'User', + args: { + select: { id: true }, + }, + }), + ).toBe(false); + + // Should not invalidate unrelated Profile queries + expect(capturedPredicate({ model: 'Profile', args: {} })).toBe(false); + }); + + it('handles undefined mutation variables', async () => { + const schema = createSchema({ + User: { + name: 'User', + fields: { + id: createField('id', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + let capturedPredicate: any; + const invalidatorMock = vi.fn((predicate) => { + capturedPredicate = predicate; + }); + + const invalidator = createInvalidator('User', 'create', schema, invalidatorMock, undefined); + + await invalidator({}, undefined); + + // Should still invalidate User queries + expect(capturedPredicate({ model: 'User', args: {} })).toBe(true); + }); + + it('uses the second argument as variables', async () => { + const schema = createSchema({ + User: { + name: 'User', + fields: { + id: createField('id', 'String'), + posts: createRelationField('posts', 'Post'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + Post: { + name: 'Post', + fields: { + id: createField('id', 'String'), + title: createField('title', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + let capturedPredicate: any; + const invalidatorMock = vi.fn((predicate) => { + capturedPredicate = predicate; + }); + + const invalidator = createInvalidator('User', 'create', schema, invalidatorMock, undefined); + + // First argument is typically the mutation result, second is variables + const result = { id: '1', name: 'John' }; + const variables = { + data: { + name: 'John', + posts: { + create: { title: 'Post' }, + }, + }, + }; + + await invalidator(result, variables); + + // Should pick up the nested Post from variables + expect(capturedPredicate({ model: 'Post', args: {} })).toBe(true); + }); + }); + + describe('real-world scenarios', () => { + it('handles blog post creation with multiple relations', async () => { + const schema = createSchema({ + User: { + name: 'User', + fields: { + id: createField('id', 'String'), + posts: createRelationField('posts', 'Post'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + Post: { + name: 'Post', + fields: { + id: createField('id', 'String'), + author: createRelationField('author', 'User'), + tags: createRelationField('tags', 'Tag'), + comments: createRelationField('comments', 'Comment'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + Tag: { + name: 'Tag', + fields: { + id: createField('id', 'String'), + name: createField('name', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + Comment: { + name: 'Comment', + fields: { + id: createField('id', 'String'), + text: createField('text', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + let capturedPredicate: any; + const invalidatorMock = vi.fn((predicate) => { + capturedPredicate = predicate; + }); + + const invalidator = createInvalidator('Post', 'create', schema, invalidatorMock, undefined); + + await invalidator( + {}, + { + data: { + title: 'My Post', + author: { connect: { id: '1' } }, + tags: { + create: [{ name: 'tech' }], + }, + comments: { + create: { text: 'First!' }, + }, + }, + }, + ); + + // Should invalidate all involved models + expect(capturedPredicate({ model: 'Post', args: {} })).toBe(true); + expect(capturedPredicate({ model: 'User', args: { include: { posts: true } } })).toBe(true); + expect(capturedPredicate({ model: 'Tag', args: {} })).toBe(true); + expect(capturedPredicate({ model: 'Comment', args: {} })).toBe(true); + }); + + it('handles complex update with disconnect and delete', async () => { + const schema = createSchema({ + User: { + name: 'User', + fields: { + id: createField('id', 'String'), + posts: createRelationField('posts', 'Post'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + Post: { + name: 'Post', + fields: { + id: createField('id', 'String'), + user: { + name: 'user', + type: 'User', + optional: false, + relation: { + opposite: 'posts', + onDelete: 'Cascade', + }, + }, + comments: createRelationField('comments', 'Comment'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + Comment: { + name: 'Comment', + fields: { + id: createField('id', 'String'), + post: { + name: 'post', + type: 'Post', + optional: false, + relation: { + opposite: 'comments', + onDelete: 'Cascade', + }, + }, + }, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + let capturedPredicate: any; + const invalidatorMock = vi.fn((predicate) => { + capturedPredicate = predicate; + }); + + const invalidator = createInvalidator('User', 'update', schema, invalidatorMock, undefined); + + await invalidator( + {}, + { + where: { id: '1' }, + data: { + posts: { + disconnect: { id: '1' }, + delete: { id: '2' }, // Will cascade to comments + }, + }, + }, + ); + + // Should invalidate all three models + expect(capturedPredicate({ model: 'User', args: {} })).toBe(true); + expect(capturedPredicate({ model: 'Post', args: {} })).toBe(true); + expect(capturedPredicate({ model: 'Comment', args: {} })).toBe(true); // cascade delete + }); + + it('integrates with query library invalidation flow', async () => { + const schema = createSchema({ + User: { + name: 'User', + fields: { + id: createField('id', 'String'), + name: createField('name', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + // Simulate a query library's invalidation mechanism + const queries = [ + { queryKey: ['User', 'findMany', {}], model: 'User', args: {} }, + { + queryKey: ['User', 'findUnique', { where: { id: '1' } }], + model: 'User', + args: { where: { id: '1' } }, + }, + { queryKey: ['Post', 'findMany', {}], model: 'Post', args: {} }, + ]; + + const invalidatedQueries: any[] = []; + const queryLibraryInvalidate = vi.fn((predicate) => { + queries.forEach((query) => { + if (predicate({ model: query.model, args: query.args })) { + invalidatedQueries.push(query.queryKey); + } + }); + }); + + const invalidator = createInvalidator('User', 'create', schema, queryLibraryInvalidate, undefined); + + await invalidator({}, { data: { name: 'John' } }); + + // Should only invalidate User queries + expect(invalidatedQueries).toHaveLength(2); + expect(invalidatedQueries).toContainEqual(['User', 'findMany', {}]); + expect(invalidatedQueries).toContainEqual(['User', 'findUnique', { where: { id: '1' } }]); + expect(invalidatedQueries).not.toContainEqual(['Post', 'findMany', {}]); + }); + }); +}); diff --git a/packages/clients/client-helpers/test/mutator.test.ts b/packages/clients/client-helpers/test/mutator.test.ts new file mode 100644 index 00000000..f5006e46 --- /dev/null +++ b/packages/clients/client-helpers/test/mutator.test.ts @@ -0,0 +1,1533 @@ +import { describe, expect, it, vi } from 'vitest'; +import type { Logger } from '../src/logging'; +import { applyMutation } from '../src/mutator'; +import { createField, createSchema } from './test-helpers'; + +describe('applyMutation', () => { + describe('basic validation', () => { + it('returns undefined for non-object query data', async () => { + const schema = createSchema({ + User: { + name: 'User', + fields: { + id: createField('id', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + const result = await applyMutation('User', 'findMany', null, 'User', 'update', {}, schema, undefined); + expect(result).toBeUndefined(); + }); + + it('returns undefined for primitive query data', async () => { + const schema = createSchema({ + User: { + name: 'User', + fields: { + id: createField('id', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + const result = await applyMutation('User', 'findMany', 42, 'User', 'update', {}, schema, undefined); + expect(result).toBeUndefined(); + }); + + it('returns undefined for non-find query operations', async () => { + const schema = createSchema({ + User: { + name: 'User', + fields: { + id: createField('id', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + const queryData = [{ id: '1', name: 'John' }]; + const result = await applyMutation('User', 'create', queryData, 'User', 'update', {}, schema, undefined); + expect(result).toBeUndefined(); + }); + }); + + describe('create mutations', () => { + it('adds new item to array with create', async () => { + const schema = createSchema({ + User: { + name: 'User', + fields: { + id: createField('id', 'String'), + name: createField('name', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + const queryData = [ + { id: '1', name: 'John' }, + { id: '2', name: 'Jane' }, + ]; + + const result = await applyMutation( + 'User', + 'findMany', + queryData, + 'User', + 'create', + { data: { name: 'Bob' } }, + schema, + undefined, + ); + + expect(result).toBeDefined(); + expect(Array.isArray(result)).toBe(true); + expect(result).toHaveLength(3); + expect(result?.[0]).toHaveProperty('name', 'Bob'); + expect(result?.[0]).toHaveProperty('$optimistic', true); + }); + + it('generates auto-increment ID for Int type', async () => { + const schema = createSchema({ + User: { + name: 'User', + fields: { + id: createField('id', 'Int'), + name: createField('name', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + const queryData = [ + { id: 1, name: 'John' }, + { id: 2, name: 'Jane' }, + ]; + + const result = await applyMutation( + 'User', + 'findMany', + queryData, + 'User', + 'create', + { data: { name: 'Bob' } }, + schema, + undefined, + ); + + expect(result?.[0]).toHaveProperty('id', 3); + }); + + it('generates UUID for String ID type', async () => { + const schema = createSchema({ + User: { + name: 'User', + fields: { + id: createField('id', 'String'), + name: createField('name', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + const queryData = [{ id: 'uuid-1', name: 'John' }]; + + const result = await applyMutation( + 'User', + 'findMany', + queryData, + 'User', + 'create', + { data: { name: 'Bob' } }, + schema, + undefined, + ); + + expect(result?.[0]).toHaveProperty('id'); + expect(typeof result?.[0]?.id).toBe('string'); + expect(result?.[0]?.id).toMatch(/^[0-9a-f-]+$/); + }); + + it('applies default values for fields', async () => { + const schema = createSchema({ + User: { + name: 'User', + fields: { + id: createField('id', 'String'), + name: createField('name', 'String'), + role: { + name: 'role', + type: 'String', + optional: false, + attributes: [ + { + name: '@default', + args: [{ value: { kind: 'literal', value: 'user' } }], + }, + ], + }, + }, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + const queryData: any[] = []; + + const result = await applyMutation( + 'User', + 'findMany', + queryData, + 'User', + 'create', + { data: { name: 'Bob' } }, + schema, + undefined, + ); + + expect(result?.[0]).toHaveProperty('role', 'user'); + }); + + it('handles DateTime fields with @default', async () => { + const schema = createSchema({ + User: { + name: 'User', + fields: { + id: createField('id', 'String'), + createdAt: { + name: 'createdAt', + type: 'DateTime', + optional: false, + attributes: [{ name: '@default' }], + }, + }, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + const queryData: any[] = []; + + const result = await applyMutation( + 'User', + 'findMany', + queryData, + 'User', + 'create', + { data: {} }, + schema, + undefined, + ); + + expect(result?.[0]?.createdAt).toBeInstanceOf(Date); + }); + + it('handles DateTime fields with @updatedAt', async () => { + const schema = createSchema({ + User: { + name: 'User', + fields: { + id: createField('id', 'String'), + updatedAt: { + name: 'updatedAt', + type: 'DateTime', + optional: false, + attributes: [{ name: '@updatedAt' }], + }, + }, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + const queryData: any[] = []; + + const result = await applyMutation( + 'User', + 'findMany', + queryData, + 'User', + 'create', + { data: {} }, + schema, + undefined, + ); + + expect(result?.[0]?.updatedAt).toBeInstanceOf(Date); + }); + + it('does not apply create to non-array query data', async () => { + const schema = createSchema({ + User: { + name: 'User', + fields: { + id: createField('id', 'String'), + name: createField('name', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + const queryData = { id: '1', name: 'John' }; + + const result = await applyMutation( + 'User', + 'findUnique', + queryData, + 'User', + 'create', + { data: { name: 'Bob' } }, + schema, + undefined, + ); + + expect(result).toBeUndefined(); + }); + + it('handles relation fields with connect', async () => { + const schema = createSchema({ + Post: { + name: 'Post', + fields: { + id: createField('id', 'String'), + title: createField('title', 'String'), + userId: createField('userId', 'String'), + user: { + name: 'user', + type: 'User', + optional: false, + relation: { + fields: ['userId'], + references: ['id'], + opposite: 'posts', + }, + }, + }, + uniqueFields: {}, + idFields: ['id'], + }, + User: { + name: 'User', + fields: { + id: createField('id', 'String'), + posts: { + name: 'posts', + type: 'Post', + optional: false, + relation: { opposite: 'user' }, + }, + }, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + const queryData: any[] = []; + + const result = await applyMutation( + 'Post', + 'findMany', + queryData, + 'Post', + 'create', + { + data: { + title: 'New Post', + user: { connect: { id: 'user-123' } }, + }, + }, + schema, + undefined, + ); + + expect(result?.[0]).toHaveProperty('userId', 'user-123'); + }); + }); + + describe('createMany mutations', () => { + it('adds multiple items to array with createMany', async () => { + const schema = createSchema({ + User: { + name: 'User', + fields: { + id: createField('id', 'String'), + name: createField('name', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + const queryData = [{ id: '1', name: 'John' }]; + + const result = await applyMutation( + 'User', + 'findMany', + queryData, + 'User', + 'createMany', + { + data: [{ name: 'Bob' }, { name: 'Alice' }], + }, + schema, + undefined, + ); + + expect(result).toBeDefined(); + expect(Array.isArray(result)).toBe(true); + expect(result).toHaveLength(3); + expect(result?.[0]).toHaveProperty('name', 'Alice'); + expect(result?.[1]).toHaveProperty('name', 'Bob'); + }); + }); + + describe('update mutations', () => { + it('updates matching single object', async () => { + const schema = createSchema({ + User: { + name: 'User', + fields: { + id: createField('id', 'String'), + name: createField('name', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + const queryData = { id: '1', name: 'John' }; + + const result = await applyMutation( + 'User', + 'findUnique', + queryData, + 'User', + 'update', + { + where: { id: '1' }, + data: { name: 'Johnny' }, + }, + schema, + undefined, + ); + + expect(result).toBeDefined(); + expect(result).toHaveProperty('name', 'Johnny'); + expect(result).toHaveProperty('$optimistic', true); + }); + + it('does not update non-matching object', async () => { + const schema = createSchema({ + User: { + name: 'User', + fields: { + id: createField('id', 'String'), + name: createField('name', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + const queryData = { id: '1', name: 'John' }; + + const result = await applyMutation( + 'User', + 'findUnique', + queryData, + 'User', + 'update', + { + where: { id: '2' }, + data: { name: 'Johnny' }, + }, + schema, + undefined, + ); + + expect(result).toBeUndefined(); + }); + + it('updates items in array', async () => { + const schema = createSchema({ + User: { + name: 'User', + fields: { + id: createField('id', 'String'), + name: createField('name', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + const queryData = [ + { id: '1', name: 'John' }, + { id: '2', name: 'Jane' }, + ]; + + const result = await applyMutation( + 'User', + 'findMany', + queryData, + 'User', + 'update', + { + where: { id: '1' }, + data: { name: 'Johnny' }, + }, + schema, + undefined, + ); + + expect(result).toBeDefined(); + expect(Array.isArray(result)).toBe(true); + expect(result?.[0]).toHaveProperty('name', 'Johnny'); + expect(result?.[0]).toHaveProperty('$optimistic', true); + expect(result?.[1]).toHaveProperty('name', 'Jane'); + }); + + it('handles relation fields with connect in update', async () => { + const schema = createSchema({ + Post: { + name: 'Post', + fields: { + id: createField('id', 'String'), + title: createField('title', 'String'), + userId: createField('userId', 'String'), + user: { + name: 'user', + type: 'User', + optional: false, + relation: { + fields: ['userId'], + references: ['id'], + opposite: 'posts', + }, + }, + }, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + const queryData = { id: '1', title: 'Post 1', userId: 'user-1' }; + + const result = await applyMutation( + 'Post', + 'findUnique', + queryData, + 'Post', + 'update', + { + where: { id: '1' }, + data: { + user: { connect: { id: 'user-2' } }, + }, + }, + schema, + undefined, + ); + + expect(result).toHaveProperty('userId', 'user-2'); + }); + + it('skips optimistically updated items', async () => { + const schema = createSchema({ + User: { + name: 'User', + fields: { + id: createField('id', 'String'), + name: createField('name', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + const queryData = [ + { id: '1', name: 'John', $optimistic: true }, + { id: '2', name: 'Jane' }, + ]; + + const result = await applyMutation( + 'User', + 'findMany', + queryData, + 'User', + 'update', + { + where: { id: '1' }, + data: { name: 'Johnny' }, + }, + schema, + undefined, + ); + + expect(result).toBeUndefined(); + }); + + it('handles compound ID fields', async () => { + const schema = createSchema({ + UserRole: { + name: 'UserRole', + fields: { + userId: createField('userId', 'String'), + roleId: createField('roleId', 'String'), + active: createField('active', 'Boolean'), + }, + uniqueFields: {}, + idFields: ['userId', 'roleId'], + }, + }); + + const queryData = { userId: 'u1', roleId: 'r1', active: false }; + + const result = await applyMutation( + 'UserRole', + 'findUnique', + queryData, + 'UserRole', + 'update', + { + where: { userId: 'u1', roleId: 'r1' }, + data: { active: true }, + }, + schema, + undefined, + ); + + expect(result).toHaveProperty('active', true); + expect(result).toHaveProperty('$optimistic', true); + }); + }); + + describe('upsert mutations', () => { + it('updates existing item in array', async () => { + const schema = createSchema({ + User: { + name: 'User', + fields: { + id: createField('id', 'String'), + name: createField('name', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + const queryData = [ + { id: '1', name: 'John' }, + { id: '2', name: 'Jane' }, + ]; + + const result = await applyMutation( + 'User', + 'findMany', + queryData, + 'User', + 'upsert', + { + where: { id: '1' }, + create: { name: 'Bob' }, + update: { name: 'Johnny' }, + }, + schema, + undefined, + ); + + expect(result).toBeDefined(); + expect(Array.isArray(result)).toBe(true); + expect(result?.[0]).toHaveProperty('name', 'Johnny'); + expect(result?.[0]).toHaveProperty('$optimistic', true); + }); + + it('creates new item when not found in array', async () => { + const schema = createSchema({ + User: { + name: 'User', + fields: { + id: createField('id', 'String'), + name: createField('name', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + const queryData = [{ id: '1', name: 'John' }]; + + const result = await applyMutation( + 'User', + 'findMany', + queryData, + 'User', + 'upsert', + { + where: { id: '2' }, + create: { name: 'Bob' }, + update: { name: 'Johnny' }, + }, + schema, + undefined, + ); + + expect(result).toBeDefined(); + expect(Array.isArray(result)).toBe(true); + expect(result).toHaveLength(2); + expect(result?.[0]).toHaveProperty('name', 'Bob'); + expect(result?.[0]).toHaveProperty('$optimistic', true); + }); + + it('updates single object when found', async () => { + const schema = createSchema({ + User: { + name: 'User', + fields: { + id: createField('id', 'String'), + name: createField('name', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + const queryData = { id: '1', name: 'John' }; + + const result = await applyMutation( + 'User', + 'findUnique', + queryData, + 'User', + 'upsert', + { + where: { id: '1' }, + create: { name: 'Bob' }, + update: { name: 'Johnny' }, + }, + schema, + undefined, + ); + + expect(result).toBeDefined(); + expect(result).toHaveProperty('name', 'Johnny'); + expect(result).toHaveProperty('$optimistic', true); + }); + + it('does not create when single object does not match', async () => { + const schema = createSchema({ + User: { + name: 'User', + fields: { + id: createField('id', 'String'), + name: createField('name', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + const queryData = { id: '1', name: 'John' }; + + const result = await applyMutation( + 'User', + 'findUnique', + queryData, + 'User', + 'upsert', + { + where: { id: '2' }, + create: { name: 'Bob' }, + update: { name: 'Johnny' }, + }, + schema, + undefined, + ); + + expect(result).toBeUndefined(); + }); + }); + + describe('delete mutations', () => { + it('deletes matching single object', async () => { + const schema = createSchema({ + User: { + name: 'User', + fields: { + id: createField('id', 'String'), + name: createField('name', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + const queryData = { id: '1', name: 'John' }; + + const result = await applyMutation( + 'User', + 'findUnique', + queryData, + 'User', + 'delete', + { where: { id: '1' } }, + schema, + undefined, + ); + + // Note: Currently returns undefined because null is falsy in the callback check + // This might be a bug in the implementation, but we test the current behavior + expect(result).toBeUndefined(); + }); + + it('does not delete non-matching single object', async () => { + const schema = createSchema({ + User: { + name: 'User', + fields: { + id: createField('id', 'String'), + name: createField('name', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + const queryData = { id: '1', name: 'John' }; + + const result = await applyMutation( + 'User', + 'findUnique', + queryData, + 'User', + 'delete', + { where: { id: '2' } }, + schema, + undefined, + ); + + expect(result).toBeUndefined(); + }); + + it('removes item from array', async () => { + const schema = createSchema({ + User: { + name: 'User', + fields: { + id: createField('id', 'String'), + name: createField('name', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + const queryData = [ + { id: '1', name: 'John' }, + { id: '2', name: 'Jane' }, + ]; + + const result = await applyMutation( + 'User', + 'findMany', + queryData, + 'User', + 'delete', + { where: { id: '1' } }, + schema, + undefined, + ); + + expect(result).toBeDefined(); + expect(Array.isArray(result)).toBe(true); + expect(result).toHaveLength(1); + expect(result?.[0]).toHaveProperty('id', '2'); + }); + + it('deletes multiple matching items from array', async () => { + const schema = createSchema({ + User: { + name: 'User', + fields: { + id: createField('id', 'String'), + name: createField('name', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + const queryData = [ + { id: '1', name: 'John' }, + { id: '1', name: 'John Duplicate' }, // duplicate ID + { id: '2', name: 'Jane' }, + ]; + + const result = await applyMutation( + 'User', + 'findMany', + queryData, + 'User', + 'delete', + { where: { id: '1' } }, + schema, + undefined, + ); + + expect(result).toBeDefined(); + expect(Array.isArray(result)).toBe(true); + expect(result).toHaveLength(1); + expect(result?.[0]).toHaveProperty('id', '2'); + }); + + it('does not delete from different model', async () => { + const schema = createSchema({ + User: { + name: 'User', + fields: { + id: createField('id', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + Post: { + name: 'Post', + fields: { + id: createField('id', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + const queryData = [{ id: '1' }]; + + const result = await applyMutation( + 'User', + 'findMany', + queryData, + 'Post', + 'delete', + { where: { id: '1' } }, + schema, + undefined, + ); + + expect(result).toBeUndefined(); + }); + }); + + describe('nested mutations', () => { + it('applies mutations to nested relation fields', async () => { + const schema = createSchema({ + User: { + name: 'User', + fields: { + id: createField('id', 'String'), + name: createField('name', 'String'), + posts: { + name: 'posts', + type: 'Post', + optional: false, + relation: { opposite: 'user' }, + }, + }, + uniqueFields: {}, + idFields: ['id'], + }, + Post: { + name: 'Post', + fields: { + id: createField('id', 'String'), + title: createField('title', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + const queryData = { + id: '1', + name: 'John', + posts: [ + { id: 'p1', title: 'Post 1' }, + { id: 'p2', title: 'Post 2' }, + ], + }; + + const result = await applyMutation( + 'User', + 'findUnique', + queryData, + 'Post', + 'update', + { + where: { id: 'p1' }, + data: { title: 'Updated Post 1' }, + }, + schema, + undefined, + ); + + expect(result).toBeDefined(); + expect(result?.posts[0]).toHaveProperty('title', 'Updated Post 1'); + expect(result?.posts[0]).toHaveProperty('$optimistic', true); + }); + + it('applies create to nested array', async () => { + const schema = createSchema({ + User: { + name: 'User', + fields: { + id: createField('id', 'String'), + name: createField('name', 'String'), + posts: { + name: 'posts', + type: 'Post', + optional: false, + relation: { opposite: 'user' }, + }, + }, + uniqueFields: {}, + idFields: ['id'], + }, + Post: { + name: 'Post', + fields: { + id: createField('id', 'String'), + title: createField('title', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + const queryData = { + id: '1', + name: 'John', + posts: [{ id: 'p1', title: 'Post 1' }], + }; + + const result = await applyMutation( + 'User', + 'findUnique', + queryData, + 'Post', + 'create', + { + data: { title: 'New Post' }, + }, + schema, + undefined, + ); + + expect(result).toBeDefined(); + expect(result?.posts).toHaveLength(2); + expect(result?.posts[0]).toHaveProperty('title', 'New Post'); + expect(result?.posts[0]).toHaveProperty('$optimistic', true); + }); + + it('applies delete to nested array', async () => { + const schema = createSchema({ + User: { + name: 'User', + fields: { + id: createField('id', 'String'), + name: createField('name', 'String'), + posts: { + name: 'posts', + type: 'Post', + optional: false, + relation: { opposite: 'user' }, + }, + }, + uniqueFields: {}, + idFields: ['id'], + }, + Post: { + name: 'Post', + fields: { + id: createField('id', 'String'), + title: createField('title', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + const queryData = { + id: '1', + name: 'John', + posts: [ + { id: 'p1', title: 'Post 1' }, + { id: 'p2', title: 'Post 2' }, + ], + }; + + const result = await applyMutation( + 'User', + 'findUnique', + queryData, + 'Post', + 'delete', + { where: { id: 'p1' } }, + schema, + undefined, + ); + + expect(result).toBeDefined(); + expect(result?.posts).toHaveLength(1); + expect(result?.posts[0]).toHaveProperty('id', 'p2'); + }); + + it('handles deeply nested relations', async () => { + const schema = createSchema({ + User: { + name: 'User', + fields: { + id: createField('id', 'String'), + profile: { + name: 'profile', + type: 'Profile', + optional: true, + relation: { opposite: 'user' }, + }, + }, + uniqueFields: {}, + idFields: ['id'], + }, + Profile: { + name: 'Profile', + fields: { + id: createField('id', 'String'), + bio: createField('bio', 'String'), + settings: { + name: 'settings', + type: 'Settings', + optional: true, + relation: { opposite: 'profile' }, + }, + }, + uniqueFields: {}, + idFields: ['id'], + }, + Settings: { + name: 'Settings', + fields: { + id: createField('id', 'String'), + theme: createField('theme', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + const queryData = { + id: 'u1', + profile: { + id: 'pr1', + bio: 'Test bio', + settings: { + id: 's1', + theme: 'light', + }, + }, + }; + + const result = await applyMutation( + 'User', + 'findUnique', + queryData, + 'Settings', + 'update', + { + where: { id: 's1' }, + data: { theme: 'dark' }, + }, + schema, + undefined, + ); + + expect(result).toBeDefined(); + expect(result?.profile?.settings).toHaveProperty('theme', 'dark'); + expect(result?.profile?.settings).toHaveProperty('$optimistic', true); + }); + }); + + describe('logging', () => { + it('logs create mutation when logger is provided', async () => { + const schema = createSchema({ + User: { + name: 'User', + fields: { + id: createField('id', 'String'), + name: createField('name', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + const logger = vi.fn() as Logger; + const queryData: any[] = []; + + await applyMutation( + 'User', + 'findMany', + queryData, + 'User', + 'create', + { data: { name: 'Bob' } }, + schema, + logger, + ); + + expect(logger).toHaveBeenCalledWith(expect.stringContaining('Applying optimistic create')); + }); + + it('logs update mutation when logger is provided', async () => { + const schema = createSchema({ + User: { + name: 'User', + fields: { + id: createField('id', 'String'), + name: createField('name', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + const logger = vi.fn() as Logger; + const queryData = { id: '1', name: 'John' }; + + await applyMutation( + 'User', + 'findUnique', + queryData, + 'User', + 'update', + { + where: { id: '1' }, + data: { name: 'Johnny' }, + }, + schema, + logger, + ); + + expect(logger).toHaveBeenCalledWith(expect.stringContaining('Applying optimistic update')); + }); + + it('logs delete mutation when logger is provided', async () => { + const schema = createSchema({ + User: { + name: 'User', + fields: { + id: createField('id', 'String'), + name: createField('name', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + const logger = vi.fn() as Logger; + const queryData = { id: '1', name: 'John' }; + + await applyMutation('User', 'findUnique', queryData, 'User', 'delete', { where: { id: '1' } }, schema, logger); + + expect(logger).toHaveBeenCalledWith(expect.stringContaining('Applying optimistic delete')); + }); + }); + + describe('edge cases', () => { + it('handles empty array', async () => { + const schema = createSchema({ + User: { + name: 'User', + fields: { + id: createField('id', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + const queryData: any[] = []; + + const result = await applyMutation( + 'User', + 'findMany', + queryData, + 'User', + 'update', + { where: { id: '1' }, data: {} }, + schema, + undefined, + ); + + expect(result).toBeUndefined(); + }); + + it('handles null nested relation', async () => { + const schema = createSchema({ + User: { + name: 'User', + fields: { + id: createField('id', 'String'), + profile: { + name: 'profile', + type: 'Profile', + optional: true, + relation: { opposite: 'user' }, + }, + }, + uniqueFields: {}, + idFields: ['id'], + }, + Profile: { + name: 'Profile', + fields: { + id: createField('id', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + const queryData = { + id: 'u1', + profile: null, + }; + + const result = await applyMutation( + 'User', + 'findUnique', + queryData, + 'Profile', + 'update', + { where: { id: 'p1' }, data: {} }, + schema, + undefined, + ); + + expect(result).toBeUndefined(); + }); + + it('does not mutate original data', async () => { + const schema = createSchema({ + User: { + name: 'User', + fields: { + id: createField('id', 'String'), + name: createField('name', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + const original = { id: '1', name: 'John' }; + const queryData = { ...original }; + + await applyMutation( + 'User', + 'findUnique', + queryData, + 'User', + 'update', + { + where: { id: '1' }, + data: { name: 'Johnny' }, + }, + schema, + undefined, + ); + + expect(queryData).toEqual(original); + }); + + it('handles BigInt ID fields', async () => { + const schema = createSchema({ + User: { + name: 'User', + fields: { + id: createField('id', 'BigInt'), + name: createField('name', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + const queryData = [ + { id: 1, name: 'John' }, + { id: 2, name: 'Jane' }, + ]; + + const result = await applyMutation( + 'User', + 'findMany', + queryData, + 'User', + 'create', + { data: { name: 'Bob' } }, + schema, + undefined, + ); + + expect(result?.[0]).toHaveProperty('id', 3); + }); + + it('handles model without id fields', async () => { + const schema = createSchema({ + User: { + name: 'User', + fields: { + name: createField('name', 'String'), + }, + uniqueFields: {}, + idFields: [], + }, + }); + + const queryData = { name: 'John' }; + + const result = await applyMutation( + 'User', + 'findFirst', + queryData, + 'User', + 'update', + { + where: {}, + data: { name: 'Johnny' }, + }, + schema, + undefined, + ); + + expect(result).toBeUndefined(); + }); + + it('handles invalid mutation args', async () => { + const schema = createSchema({ + User: { + name: 'User', + fields: { + id: createField('id', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + const queryData = { id: '1' }; + + // Missing where + const result1 = await applyMutation( + 'User', + 'findUnique', + queryData, + 'User', + 'update', + { data: {} }, + schema, + undefined, + ); + expect(result1).toBeUndefined(); + + // Missing data + const result2 = await applyMutation( + 'User', + 'findUnique', + queryData, + 'User', + 'update', + { where: { id: '1' } }, + schema, + undefined, + ); + expect(result2).toBeUndefined(); + }); + + it('handles unknown fields in mutation data', async () => { + const schema = createSchema({ + User: { + name: 'User', + fields: { + id: createField('id', 'String'), + name: createField('name', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + const queryData = { id: '1', name: 'John' }; + + const result = await applyMutation( + 'User', + 'findUnique', + queryData, + 'User', + 'update', + { + where: { id: '1' }, + data: { + name: 'Johnny', + unknownField: 'value', + }, + }, + schema, + undefined, + ); + + expect(result).toBeDefined(); + expect(result).toHaveProperty('name', 'Johnny'); + expect(result).not.toHaveProperty('unknownField'); + }); + + it('handles arrays with mixed types', async () => { + const schema = createSchema({ + User: { + name: 'User', + fields: { + id: createField('id', 'String'), + name: createField('name', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + const queryData = [{ id: '1', name: 'John' }, null, 'invalid', { id: '2', name: 'Jane' }]; + + const result = await applyMutation( + 'User', + 'findMany', + queryData, + 'User', + 'update', + { + where: { id: '1' }, + data: { name: 'Johnny' }, + }, + schema, + undefined, + ); + + // Should handle only valid objects + expect(result).toBeDefined(); + expect(result?.[0]).toHaveProperty('name', 'Johnny'); + }); + }); +}); diff --git a/packages/clients/client-helpers/test/nested-read-visitor.test.ts b/packages/clients/client-helpers/test/nested-read-visitor.test.ts new file mode 100644 index 00000000..71ef5845 --- /dev/null +++ b/packages/clients/client-helpers/test/nested-read-visitor.test.ts @@ -0,0 +1,949 @@ +import { describe, expect, it, vi } from 'vitest'; +import { NestedReadVisitor, type NestedReadVisitorCallback } from '../src/nested-read-visitor'; +import { createField, createRelationField, createSchema } from './test-helpers'; + +describe('NestedReadVisitor tests', () => { + describe('basic visiting', () => { + it('visits simple model without select or include', () => { + const schema = createSchema({ + User: { + name: 'User', + fields: { + id: createField('id', 'String'), + name: createField('name', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + const callback = vi.fn(); + const visitor = new NestedReadVisitor(schema, { field: callback }); + + visitor.visit('User', { where: { id: '1' } }); + + // Should be called once for the root with undefined field + expect(callback).toHaveBeenCalledTimes(1); + expect(callback).toHaveBeenCalledWith('User', undefined, undefined, { where: { id: '1' } }); + }); + + it('handles null or undefined args', () => { + const schema = createSchema({ + User: { + name: 'User', + fields: {}, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + const callback = vi.fn(); + const visitor = new NestedReadVisitor(schema, { field: callback }); + + visitor.visit('User', null); + expect(callback).toHaveBeenCalledWith('User', undefined, undefined, null); + + visitor.visit('User', undefined); + expect(callback).toHaveBeenCalledWith('User', undefined, undefined, undefined); + }); + }); + + describe('include visits', () => { + it('visits fields with include', () => { + const schema = createSchema({ + User: { + name: 'User', + fields: { + id: createField('id', 'String'), + posts: createRelationField('posts', 'Post'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + Post: { + name: 'Post', + fields: { + id: createField('id', 'String'), + title: createField('title', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + const callback = vi.fn(); + const visitor = new NestedReadVisitor(schema, { field: callback }); + + visitor.visit('User', { + include: { + posts: true, + }, + }); + + expect(callback).toHaveBeenCalledTimes(2); + expect(callback).toHaveBeenNthCalledWith(1, 'User', undefined, undefined, { + include: { posts: true }, + }); + expect(callback).toHaveBeenNthCalledWith( + 2, + 'Post', + expect.objectContaining({ name: 'posts' }), + 'include', + true, + ); + }); + + it('visits nested includes', () => { + const schema = createSchema({ + User: { + name: 'User', + fields: { + id: createField('id', 'String'), + posts: createRelationField('posts', 'Post'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + Post: { + name: 'Post', + fields: { + id: createField('id', 'String'), + title: createField('title', 'String'), + comments: createRelationField('comments', 'Comment'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + Comment: { + name: 'Comment', + fields: { + id: createField('id', 'String'), + text: createField('text', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + const callback = vi.fn(); + const visitor = new NestedReadVisitor(schema, { field: callback }); + + visitor.visit('User', { + include: { + posts: { + include: { + comments: true, + }, + }, + }, + }); + + expect(callback).toHaveBeenCalledTimes(3); + expect(callback).toHaveBeenNthCalledWith(1, 'User', undefined, undefined, expect.any(Object)); + expect(callback).toHaveBeenNthCalledWith( + 2, + 'Post', + expect.objectContaining({ name: 'posts' }), + 'include', + expect.any(Object), + ); + expect(callback).toHaveBeenNthCalledWith( + 3, + 'Comment', + expect.objectContaining({ name: 'comments' }), + 'include', + true, + ); + }); + + it('visits multiple includes at same level', () => { + const schema = createSchema({ + User: { + name: 'User', + fields: { + id: createField('id', 'String'), + posts: createRelationField('posts', 'Post'), + profile: createRelationField('profile', 'Profile'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + Post: { + name: 'Post', + fields: { + id: createField('id', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + Profile: { + name: 'Profile', + fields: { + id: createField('id', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + const callback = vi.fn(); + const visitor = new NestedReadVisitor(schema, { field: callback }); + + visitor.visit('User', { + include: { + posts: true, + profile: true, + }, + }); + + expect(callback).toHaveBeenCalledTimes(3); + expect(callback).toHaveBeenNthCalledWith(1, 'User', undefined, undefined, expect.any(Object)); + expect(callback).toHaveBeenNthCalledWith( + 2, + 'Post', + expect.objectContaining({ name: 'posts' }), + 'include', + true, + ); + expect(callback).toHaveBeenNthCalledWith( + 3, + 'Profile', + expect.objectContaining({ name: 'profile' }), + 'include', + true, + ); + }); + }); + + describe('select visits', () => { + it('visits fields with select', () => { + const schema = createSchema({ + User: { + name: 'User', + fields: { + id: createField('id', 'String'), + posts: createRelationField('posts', 'Post'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + Post: { + name: 'Post', + fields: { + id: createField('id', 'String'), + title: createField('title', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + const callback = vi.fn(); + const visitor = new NestedReadVisitor(schema, { field: callback }); + + visitor.visit('User', { + select: { + posts: true, + }, + }); + + expect(callback).toHaveBeenCalledTimes(2); + expect(callback).toHaveBeenNthCalledWith(1, 'User', undefined, undefined, { + select: { posts: true }, + }); + expect(callback).toHaveBeenNthCalledWith( + 2, + 'Post', + expect.objectContaining({ name: 'posts' }), + 'select', + true, + ); + }); + + it('visits nested selects', () => { + const schema = createSchema({ + User: { + name: 'User', + fields: { + id: createField('id', 'String'), + posts: createRelationField('posts', 'Post'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + Post: { + name: 'Post', + fields: { + id: createField('id', 'String'), + title: createField('title', 'String'), + comments: createRelationField('comments', 'Comment'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + Comment: { + name: 'Comment', + fields: { + id: createField('id', 'String'), + text: createField('text', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + const callback = vi.fn(); + const visitor = new NestedReadVisitor(schema, { field: callback }); + + visitor.visit('User', { + select: { + posts: { + select: { + comments: true, + }, + }, + }, + }); + + expect(callback).toHaveBeenCalledTimes(3); + expect(callback).toHaveBeenNthCalledWith( + 3, + 'Comment', + expect.objectContaining({ name: 'comments' }), + 'select', + true, + ); + }); + + it('visits scalar fields in select', () => { + const schema = createSchema({ + User: { + name: 'User', + fields: { + id: createField('id', 'String'), + name: createField('name', 'String'), + email: createField('email', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + const callback = vi.fn(); + const visitor = new NestedReadVisitor(schema, { field: callback }); + + visitor.visit('User', { + select: { + id: true, + name: true, + }, + }); + + expect(callback).toHaveBeenCalledTimes(3); + expect(callback).toHaveBeenNthCalledWith( + 2, + 'String', + expect.objectContaining({ name: 'id' }), + 'select', + true, + ); + expect(callback).toHaveBeenNthCalledWith( + 3, + 'String', + expect.objectContaining({ name: 'name' }), + 'select', + true, + ); + }); + }); + + describe('_count handling', () => { + it('visits _count field', () => { + const schema = createSchema({ + User: { + name: 'User', + fields: { + id: createField('id', 'String'), + posts: createRelationField('posts', 'Post'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + Post: { + name: 'Post', + fields: { + id: createField('id', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + const callback = vi.fn(); + const visitor = new NestedReadVisitor(schema, { field: callback }); + + visitor.visit('User', { + include: { + _count: { + select: { + posts: true, + }, + }, + }, + }); + + // Should visit root, _count recursion (same model, undefined kind), and posts within _count select + expect(callback).toHaveBeenCalledTimes(3); + expect(callback).toHaveBeenNthCalledWith(1, 'User', undefined, undefined, expect.any(Object)); + // _count causes recursion on same model with undefined kind + expect(callback).toHaveBeenNthCalledWith( + 2, + 'User', + undefined, + undefined, + expect.objectContaining({ select: { posts: true } }), + ); + // Then visits posts field + expect(callback).toHaveBeenNthCalledWith( + 3, + 'Post', + expect.objectContaining({ name: 'posts' }), + 'select', + true, + ); + }); + + it('handles _count with nested structure', () => { + const schema = createSchema({ + User: { + name: 'User', + fields: { + id: createField('id', 'String'), + posts: createRelationField('posts', 'Post'), + comments: createRelationField('comments', 'Comment'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + Post: { + name: 'Post', + fields: { + id: createField('id', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + Comment: { + name: 'Comment', + fields: { + id: createField('id', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + const callback = vi.fn(); + const visitor = new NestedReadVisitor(schema, { field: callback }); + + visitor.visit('User', { + select: { + _count: { + select: { + posts: true, + comments: true, + }, + }, + }, + }); + + expect(callback).toHaveBeenCalled(); + }); + }); + + describe('callback return value handling', () => { + it('stops visiting when callback returns false', () => { + const schema = createSchema({ + User: { + name: 'User', + fields: { + id: createField('id', 'String'), + posts: createRelationField('posts', 'Post'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + Post: { + name: 'Post', + fields: { + id: createField('id', 'String'), + comments: createRelationField('comments', 'Comment'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + Comment: { + name: 'Comment', + fields: { + id: createField('id', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + const callback = vi.fn((_model, field) => { + // Return false when visiting posts to stop recursion + if (field?.name === 'posts') { + return false; + } + return true; + }); + + const visitor = new NestedReadVisitor(schema, { field: callback }); + + visitor.visit('User', { + include: { + posts: { + include: { + comments: true, + }, + }, + }, + }); + + // Should visit User and posts, but not comments (stopped by returning false) + expect(callback).toHaveBeenCalledTimes(2); + expect(callback).toHaveBeenNthCalledWith(1, 'User', undefined, undefined, expect.any(Object)); + expect(callback).toHaveBeenNthCalledWith( + 2, + 'Post', + expect.objectContaining({ name: 'posts' }), + 'include', + expect.any(Object), + ); + }); + + it('continues visiting when callback returns undefined or true', () => { + const schema = createSchema({ + User: { + name: 'User', + fields: { + id: createField('id', 'String'), + posts: createRelationField('posts', 'Post'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + Post: { + name: 'Post', + fields: { + id: createField('id', 'String'), + comments: createRelationField('comments', 'Comment'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + Comment: { + name: 'Comment', + fields: { + id: createField('id', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + const callback = vi.fn((_model, field) => { + if (field?.name === 'posts') { + return true; // Explicitly continue + } + return undefined; + }); + + const visitor = new NestedReadVisitor(schema, { field: callback }); + + visitor.visit('User', { + include: { + posts: { + include: { + comments: true, + }, + }, + }, + }); + + // Should visit all three levels + expect(callback).toHaveBeenCalledTimes(3); + }); + }); + + describe('mixed include and select', () => { + it('handles select inside include', () => { + const schema = createSchema({ + User: { + name: 'User', + fields: { + id: createField('id', 'String'), + posts: createRelationField('posts', 'Post'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + Post: { + name: 'Post', + fields: { + id: createField('id', 'String'), + title: createField('title', 'String'), + content: createField('content', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + const callback = vi.fn(); + const visitor = new NestedReadVisitor(schema, { field: callback }); + + visitor.visit('User', { + include: { + posts: { + select: { + title: true, + }, + }, + }, + }); + + expect(callback).toHaveBeenCalledWith('User', undefined, undefined, expect.any(Object)); + expect(callback).toHaveBeenCalledWith( + 'Post', + expect.objectContaining({ name: 'posts' }), + 'include', + expect.any(Object), + ); + expect(callback).toHaveBeenCalledWith('String', expect.objectContaining({ name: 'title' }), 'select', true); + }); + + it('handles include inside select', () => { + const schema = createSchema({ + User: { + name: 'User', + fields: { + id: createField('id', 'String'), + posts: createRelationField('posts', 'Post'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + Post: { + name: 'Post', + fields: { + id: createField('id', 'String'), + comments: createRelationField('comments', 'Comment'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + Comment: { + name: 'Comment', + fields: { + id: createField('id', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + const callback = vi.fn(); + const visitor = new NestedReadVisitor(schema, { field: callback }); + + visitor.visit('User', { + select: { + posts: { + include: { + comments: true, + }, + }, + }, + }); + + expect(callback).toHaveBeenCalledWith('User', undefined, undefined, expect.any(Object)); + expect(callback).toHaveBeenCalledWith( + 'Post', + expect.objectContaining({ name: 'posts' }), + 'select', + expect.any(Object), + ); + expect(callback).toHaveBeenCalledWith( + 'Comment', + expect.objectContaining({ name: 'comments' }), + 'include', + true, + ); + }); + }); + + describe('edge cases', () => { + it('handles fields not in schema gracefully', () => { + const schema = createSchema({ + User: { + name: 'User', + fields: { + id: createField('id', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + const callback = vi.fn(); + const visitor = new NestedReadVisitor(schema, { field: callback }); + + // Try to include a field that doesn't exist + visitor.visit('User', { + include: { + nonExistentField: true, + }, + }); + + // Should only visit the root, not the non-existent field + expect(callback).toHaveBeenCalledTimes(1); + }); + + it('handles empty include object', () => { + const schema = createSchema({ + User: { + name: 'User', + fields: { + id: createField('id', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + const callback = vi.fn(); + const visitor = new NestedReadVisitor(schema, { field: callback }); + + visitor.visit('User', { + include: {}, + }); + + expect(callback).toHaveBeenCalledTimes(1); + }); + + it('handles empty select object', () => { + const schema = createSchema({ + User: { + name: 'User', + fields: { + id: createField('id', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + const callback = vi.fn(); + const visitor = new NestedReadVisitor(schema, { field: callback }); + + visitor.visit('User', { + select: {}, + }); + + expect(callback).toHaveBeenCalledTimes(1); + }); + + it('handles visitor with no callback', () => { + const schema = createSchema({ + User: { + name: 'User', + fields: { + id: createField('id', 'String'), + posts: createRelationField('posts', 'Post'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + Post: { + name: 'Post', + fields: { + id: createField('id', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + const visitor = new NestedReadVisitor(schema, {}); + + // Should not throw + expect(() => { + visitor.visit('User', { + include: { + posts: true, + }, + }); + }).not.toThrow(); + }); + + it('handles non-object select/include values', () => { + const schema = createSchema({ + User: { + name: 'User', + fields: { + id: createField('id', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + const callback = vi.fn(); + const visitor = new NestedReadVisitor(schema, { field: callback }); + + visitor.visit('User', { + include: 'not an object', + }); + + visitor.visit('User', { + select: null, + }); + + // Should handle gracefully + expect(callback).toHaveBeenCalledTimes(2); + }); + }); + + describe('complex real-world scenarios', () => { + it('handles deeply nested blog post structure', () => { + const schema = createSchema({ + User: { + name: 'User', + fields: { + id: createField('id', 'String'), + name: createField('name', 'String'), + posts: createRelationField('posts', 'Post'), + profile: createRelationField('profile', 'Profile'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + Post: { + name: 'Post', + fields: { + id: createField('id', 'String'), + title: createField('title', 'String'), + comments: createRelationField('comments', 'Comment'), + author: createRelationField('author', 'User'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + Comment: { + name: 'Comment', + fields: { + id: createField('id', 'String'), + text: createField('text', 'String'), + author: createRelationField('author', 'User'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + Profile: { + name: 'Profile', + fields: { + id: createField('id', 'String'), + bio: createField('bio', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + const visitedModels: string[] = []; + const callback: NestedReadVisitorCallback['field'] = (model) => { + visitedModels.push(model); + }; + + const visitor = new NestedReadVisitor(schema, { field: callback }); + + visitor.visit('User', { + include: { + posts: { + include: { + comments: { + include: { + author: { + select: { + name: true, + }, + }, + }, + }, + }, + }, + profile: true, + }, + }); + + expect(visitedModels).toContain('User'); + expect(visitedModels).toContain('Post'); + expect(visitedModels).toContain('Comment'); + expect(visitedModels).toContain('Profile'); + expect(visitedModels.filter((m) => m === 'User').length).toBeGreaterThan(1); // User visited multiple times + }); + + it('collects all visited field names', () => { + const schema = createSchema({ + User: { + name: 'User', + fields: { + id: createField('id', 'String'), + email: createField('email', 'String'), + posts: createRelationField('posts', 'Post'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + Post: { + name: 'Post', + fields: { + id: createField('id', 'String'), + title: createField('title', 'String'), + published: createField('published', 'Boolean'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + const fieldNames: string[] = []; + const callback: NestedReadVisitorCallback['field'] = (_model, field) => { + if (field) { + fieldNames.push(field.name); + } + }; + + const visitor = new NestedReadVisitor(schema, { field: callback }); + + visitor.visit('User', { + select: { + email: true, + posts: { + select: { + title: true, + published: true, + }, + }, + }, + }); + + expect(fieldNames).toContain('email'); + expect(fieldNames).toContain('posts'); + expect(fieldNames).toContain('title'); + expect(fieldNames).toContain('published'); + }); + }); +}); diff --git a/packages/clients/client-helpers/test/nested-write-visitor.test.ts b/packages/clients/client-helpers/test/nested-write-visitor.test.ts new file mode 100644 index 00000000..2e09e441 --- /dev/null +++ b/packages/clients/client-helpers/test/nested-write-visitor.test.ts @@ -0,0 +1,1244 @@ +import { describe, expect, it, vi } from 'vitest'; +import { NestedWriteVisitor, type NestedWriteVisitorContext } from '../src/nested-write-visitor'; +import { createField, createRelationField, createSchema } from './test-helpers'; + +describe('NestedWriteVisitor tests', () => { + describe('create action', () => { + it('visits create with simple data', async () => { + const schema = createSchema({ + User: { + name: 'User', + fields: { + id: createField('id', 'String'), + name: createField('name', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + const createCallback = vi.fn(); + const visitor = new NestedWriteVisitor(schema, { create: createCallback }); + + await visitor.visit('User', 'create', { + data: { name: 'Alice' }, + }); + + expect(createCallback).toHaveBeenCalledTimes(1); + expect(createCallback).toHaveBeenCalledWith( + 'User', + { name: 'Alice' }, + expect.objectContaining({ + parent: undefined, + field: undefined, + }), + ); + }); + + it('visits nested create in relation', async () => { + const schema = createSchema({ + User: { + name: 'User', + fields: { + id: createField('id', 'String'), + posts: createRelationField('posts', 'Post'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + Post: { + name: 'Post', + fields: { + id: createField('id', 'String'), + title: createField('title', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + const createCallback = vi.fn(); + const visitor = new NestedWriteVisitor(schema, { create: createCallback }); + + await visitor.visit('User', 'create', { + data: { + name: 'Alice', + posts: { + create: { title: 'First Post' }, + }, + }, + }); + + expect(createCallback).toHaveBeenCalledTimes(2); + expect(createCallback).toHaveBeenNthCalledWith(1, 'User', expect.any(Object), expect.any(Object)); + expect(createCallback).toHaveBeenNthCalledWith(2, 'Post', { title: 'First Post' }, expect.any(Object)); + }); + + it('visits create with array data', async () => { + const schema = createSchema({ + Post: { + name: 'Post', + fields: { + id: createField('id', 'String'), + comments: createRelationField('comments', 'Comment'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + Comment: { + name: 'Comment', + fields: { + id: createField('id', 'String'), + text: createField('text', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + const createCallback = vi.fn(); + const visitor = new NestedWriteVisitor(schema, { create: createCallback }); + + await visitor.visit('Post', 'create', { + data: { + title: 'My Post', + comments: { + create: [{ text: 'Comment 1' }, { text: 'Comment 2' }], + }, + }, + }); + + expect(createCallback).toHaveBeenCalledTimes(3); // 1 post + 2 comments + expect(createCallback).toHaveBeenNthCalledWith(3, 'Comment', { text: 'Comment 1' }, expect.any(Object)); + expect(createCallback).toHaveBeenNthCalledWith(2, 'Comment', { text: 'Comment 2' }, expect.any(Object)); + }); + + it('stops visiting when callback returns false', async () => { + const schema = createSchema({ + User: { + name: 'User', + fields: { + id: createField('id', 'String'), + posts: createRelationField('posts', 'Post'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + Post: { + name: 'Post', + fields: { + id: createField('id', 'String'), + title: createField('title', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + const createCallback = vi.fn(() => false); + const visitor = new NestedWriteVisitor(schema, { create: createCallback }); + + await visitor.visit('User', 'create', { + data: { + name: 'Alice', + posts: { + create: { title: 'First Post' }, + }, + }, + }); + + // Should only visit User, not the nested post + expect(createCallback).toHaveBeenCalledTimes(1); + }); + + it('allows callback to replace payload', async () => { + const schema = createSchema({ + User: { + name: 'User', + fields: { + id: createField('id', 'String'), + name: createField('name', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + const fieldCallback = vi.fn(); + const createCallback = vi.fn(() => ({ name: 'Bob' })); + const visitor = new NestedWriteVisitor(schema, { create: createCallback, field: fieldCallback }); + + await visitor.visit('User', 'create', { + data: { name: 'Alice' }, + }); + + // Field callback should see the replaced payload + expect(fieldCallback).toHaveBeenCalledWith( + expect.objectContaining({ name: 'name' }), + 'create', + 'Bob', + expect.any(Object), + ); + }); + }); + + describe('createMany action', () => { + it('visits createMany with data array', async () => { + const schema = createSchema({ + User: { + name: 'User', + fields: { + id: createField('id', 'String'), + name: createField('name', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + const createManyCallback = vi.fn(); + const visitor = new NestedWriteVisitor(schema, { createMany: createManyCallback }); + + await visitor.visit('User', 'createMany', { + data: [{ name: 'Alice' }, { name: 'Bob' }], + skipDuplicates: true, + }); + + expect(createManyCallback).toHaveBeenCalledTimes(1); + expect(createManyCallback).toHaveBeenCalledWith( + 'User', + { data: [{ name: 'Alice' }, { name: 'Bob' }], skipDuplicates: true }, + expect.any(Object), + ); + }); + }); + + describe('update action', () => { + it('visits update with simple data', async () => { + const schema = createSchema({ + User: { + name: 'User', + fields: { + id: createField('id', 'String'), + name: createField('name', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + const updateCallback = vi.fn(); + const visitor = new NestedWriteVisitor(schema, { update: updateCallback }); + + await visitor.visit('User', 'update', { + where: { id: '1' }, + data: { name: 'Updated' }, + }); + + expect(updateCallback).toHaveBeenCalledTimes(1); + expect(updateCallback).toHaveBeenCalledWith( + 'User', + { where: { id: '1' }, data: { name: 'Updated' } }, + expect.any(Object), + ); + }); + + it('visits nested update in relation', async () => { + const schema = createSchema({ + User: { + name: 'User', + fields: { + id: createField('id', 'String'), + posts: createRelationField('posts', 'Post'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + Post: { + name: 'Post', + fields: { + id: createField('id', 'String'), + title: createField('title', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + const updateCallback = vi.fn(); + const visitor = new NestedWriteVisitor(schema, { update: updateCallback }); + + await visitor.visit('User', 'update', { + where: { id: '1' }, + data: { + posts: { + update: { + where: { id: 'p1' }, + data: { title: 'Updated Title' }, + }, + }, + }, + }); + + expect(updateCallback).toHaveBeenCalledTimes(2); + expect(updateCallback).toHaveBeenNthCalledWith(2, 'Post', expect.any(Object), expect.any(Object)); + }); + + it('visits update with array', async () => { + const schema = createSchema({ + User: { + name: 'User', + fields: { + id: createField('id', 'String'), + posts: createRelationField('posts', 'Post'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + Post: { + name: 'Post', + fields: { + id: createField('id', 'String'), + title: createField('title', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + const updateCallback = vi.fn(); + const visitor = new NestedWriteVisitor(schema, { update: updateCallback }); + + await visitor.visit('User', 'update', { + where: { id: '1' }, + data: { + posts: { + update: [ + { where: { id: 'p1' }, data: { title: 'Title 1' } }, + { where: { id: 'p2' }, data: { title: 'Title 2' } }, + ], + }, + }, + }); + + expect(updateCallback).toHaveBeenCalledTimes(3); // 1 user + 2 posts + }); + }); + + describe('updateMany action', () => { + it('visits updateMany', async () => { + const schema = createSchema({ + User: { + name: 'User', + fields: { + id: createField('id', 'String'), + active: createField('active', 'Boolean'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + const updateManyCallback = vi.fn(); + const visitor = new NestedWriteVisitor(schema, { updateMany: updateManyCallback }); + + await visitor.visit('User', 'updateMany', { + where: { active: false }, + data: { active: true }, + }); + + expect(updateManyCallback).toHaveBeenCalledTimes(1); + expect(updateManyCallback).toHaveBeenCalledWith( + 'User', + { where: { active: false }, data: { active: true } }, + expect.any(Object), + ); + }); + }); + + describe('upsert action', () => { + it('visits upsert with create and update', async () => { + const schema = createSchema({ + User: { + name: 'User', + fields: { + id: createField('id', 'String'), + name: createField('name', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + const upsertCallback = vi.fn(); + const visitor = new NestedWriteVisitor(schema, { upsert: upsertCallback }); + + await visitor.visit('User', 'upsert', { + where: { id: '1' }, + create: { name: 'Alice' }, + update: { name: 'Updated Alice' }, + }); + + expect(upsertCallback).toHaveBeenCalledTimes(1); + expect(upsertCallback).toHaveBeenCalledWith( + 'User', + { + where: { id: '1' }, + create: { name: 'Alice' }, + update: { name: 'Updated Alice' }, + }, + expect.any(Object), + ); + }); + + it('visits nested upsert in relation', async () => { + const schema = createSchema({ + User: { + name: 'User', + fields: { + id: createField('id', 'String'), + profile: createRelationField('profile', 'Profile'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + Profile: { + name: 'Profile', + fields: { + id: createField('id', 'String'), + bio: createField('bio', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + const upsertCallback = vi.fn(); + const visitor = new NestedWriteVisitor(schema, { upsert: upsertCallback }); + + await visitor.visit('User', 'update', { + where: { id: '1' }, + data: { + profile: { + upsert: { + where: { id: 'p1' }, + create: { bio: 'New bio' }, + update: { bio: 'Updated bio' }, + }, + }, + }, + }); + + expect(upsertCallback).toHaveBeenCalledWith('Profile', expect.any(Object), expect.any(Object)); + }); + }); + + describe('connect action', () => { + it('visits connect with unique filter', async () => { + const schema = createSchema({ + User: { + name: 'User', + fields: { + id: createField('id', 'String'), + posts: createRelationField('posts', 'Post'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + Post: { + name: 'Post', + fields: { + id: createField('id', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + const connectCallback = vi.fn(); + const visitor = new NestedWriteVisitor(schema, { connect: connectCallback }); + + await visitor.visit('User', 'update', { + where: { id: '1' }, + data: { + posts: { + connect: { id: 'p1' }, + }, + }, + }); + + expect(connectCallback).toHaveBeenCalledTimes(1); + expect(connectCallback).toHaveBeenCalledWith('Post', { id: 'p1' }, expect.any(Object)); + }); + + it('visits connect with array', async () => { + const schema = createSchema({ + User: { + name: 'User', + fields: { + id: createField('id', 'String'), + posts: createRelationField('posts', 'Post'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + Post: { + name: 'Post', + fields: { + id: createField('id', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + const connectCallback = vi.fn(); + const visitor = new NestedWriteVisitor(schema, { connect: connectCallback }); + + await visitor.visit('User', 'update', { + where: { id: '1' }, + data: { + posts: { + connect: [{ id: 'p1' }, { id: 'p2' }], + }, + }, + }); + + expect(connectCallback).toHaveBeenCalledTimes(2); + }); + }); + + describe('disconnect action', () => { + it('visits disconnect with unique filter (to-many)', async () => { + const schema = createSchema({ + User: { + name: 'User', + fields: { + id: createField('id', 'String'), + posts: createRelationField('posts', 'Post'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + Post: { + name: 'Post', + fields: { + id: createField('id', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + const disconnectCallback = vi.fn(); + const visitor = new NestedWriteVisitor(schema, { disconnect: disconnectCallback }); + + await visitor.visit('User', 'update', { + where: { id: '1' }, + data: { + posts: { + disconnect: { id: 'p1' }, + }, + }, + }); + + expect(disconnectCallback).toHaveBeenCalledTimes(1); + expect(disconnectCallback).toHaveBeenCalledWith('Post', { id: 'p1' }, expect.any(Object)); + }); + + it('visits disconnect with boolean (to-one)', async () => { + const schema = createSchema({ + User: { + name: 'User', + fields: { + id: createField('id', 'String'), + profile: createRelationField('profile', 'Profile', true), + }, + uniqueFields: {}, + idFields: ['id'], + }, + Profile: { + name: 'Profile', + fields: { + id: createField('id', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + const disconnectCallback = vi.fn(); + const visitor = new NestedWriteVisitor(schema, { disconnect: disconnectCallback }); + + await visitor.visit('User', 'update', { + where: { id: '1' }, + data: { + profile: { + disconnect: true, + }, + }, + }); + + expect(disconnectCallback).toHaveBeenCalledTimes(1); + expect(disconnectCallback).toHaveBeenCalledWith('Profile', true, expect.any(Object)); + }); + }); + + describe('set action', () => { + it('visits set with unique filters', async () => { + const schema = createSchema({ + User: { + name: 'User', + fields: { + id: createField('id', 'String'), + posts: createRelationField('posts', 'Post'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + Post: { + name: 'Post', + fields: { + id: createField('id', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + const setCallback = vi.fn(); + const visitor = new NestedWriteVisitor(schema, { set: setCallback }); + + await visitor.visit('User', 'update', { + where: { id: '1' }, + data: { + posts: { + set: [{ id: 'p1' }, { id: 'p2' }], + }, + }, + }); + + expect(setCallback).toHaveBeenCalledTimes(2); + }); + }); + + describe('delete action', () => { + it('visits delete with where clause', async () => { + const schema = createSchema({ + User: { + name: 'User', + fields: { + id: createField('id', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + const deleteCallback = vi.fn(); + const visitor = new NestedWriteVisitor(schema, { delete: deleteCallback }); + + await visitor.visit('User', 'delete', { + where: { id: '1' }, + }); + + expect(deleteCallback).toHaveBeenCalledTimes(1); + // For top-level delete, the callback receives the full args object including where + expect(deleteCallback).toHaveBeenCalledWith('User', { id: '1' }, expect.any(Object)); + }); + + it('visits nested delete in relation', async () => { + const schema = createSchema({ + User: { + name: 'User', + fields: { + id: createField('id', 'String'), + posts: createRelationField('posts', 'Post'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + Post: { + name: 'Post', + fields: { + id: createField('id', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + const deleteCallback = vi.fn(); + const visitor = new NestedWriteVisitor(schema, { delete: deleteCallback }); + + await visitor.visit('User', 'update', { + where: { id: '1' }, + data: { + posts: { + delete: { id: 'p1' }, + }, + }, + }); + + expect(deleteCallback).toHaveBeenCalledTimes(1); + expect(deleteCallback).toHaveBeenCalledWith('Post', { id: 'p1' }, expect.any(Object)); + }); + }); + + describe('deleteMany action', () => { + it('visits deleteMany with where clause', async () => { + const schema = createSchema({ + User: { + name: 'User', + fields: { + id: createField('id', 'String'), + active: createField('active', 'Boolean'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + const deleteManyCallback = vi.fn(); + const visitor = new NestedWriteVisitor(schema, { deleteMany: deleteManyCallback }); + + await visitor.visit('User', 'deleteMany', { + where: { active: false }, + }); + + expect(deleteManyCallback).toHaveBeenCalledTimes(1); + // For top-level deleteMany, the callback receives the where clause directly + expect(deleteManyCallback).toHaveBeenCalledWith('User', { active: false }, expect.any(Object)); + }); + }); + + describe('connectOrCreate action', () => { + it('visits connectOrCreate with where and create', async () => { + const schema = createSchema({ + User: { + name: 'User', + fields: { + id: createField('id', 'String'), + posts: createRelationField('posts', 'Post'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + Post: { + name: 'Post', + fields: { + id: createField('id', 'String'), + title: createField('title', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + const connectOrCreateCallback = vi.fn(); + const visitor = new NestedWriteVisitor(schema, { connectOrCreate: connectOrCreateCallback }); + + await visitor.visit('User', 'update', { + where: { id: '1' }, + data: { + posts: { + connectOrCreate: { + where: { id: 'p1' }, + create: { title: 'New Post' }, + }, + }, + }, + }); + + expect(connectOrCreateCallback).toHaveBeenCalledTimes(1); + expect(connectOrCreateCallback).toHaveBeenCalledWith( + 'Post', + { where: { id: 'p1' }, create: { title: 'New Post' } }, + expect.any(Object), + ); + }); + }); + + describe('field callback', () => { + it('visits scalar fields during create', async () => { + const schema = createSchema({ + User: { + name: 'User', + fields: { + id: createField('id', 'String'), + name: createField('name', 'String'), + email: createField('email', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + const fieldCallback = vi.fn(); + const visitor = new NestedWriteVisitor(schema, { field: fieldCallback }); + + await visitor.visit('User', 'create', { + data: { + name: 'Alice', + email: 'alice@example.com', + }, + }); + + expect(fieldCallback).toHaveBeenCalledTimes(2); + expect(fieldCallback).toHaveBeenCalledWith( + expect.objectContaining({ name: 'name' }), + 'create', + 'Alice', + expect.any(Object), + ); + expect(fieldCallback).toHaveBeenCalledWith( + expect.objectContaining({ name: 'email' }), + 'create', + 'alice@example.com', + expect.any(Object), + ); + }); + + it('visits scalar fields during update', async () => { + const schema = createSchema({ + User: { + name: 'User', + fields: { + id: createField('id', 'String'), + name: createField('name', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + const fieldCallback = vi.fn(); + const visitor = new NestedWriteVisitor(schema, { field: fieldCallback }); + + await visitor.visit('User', 'update', { + where: { id: '1' }, + data: { name: 'Updated Name' }, + }); + + expect(fieldCallback).toHaveBeenCalledWith( + expect.objectContaining({ name: 'name' }), + 'update', + 'Updated Name', + expect.any(Object), + ); + }); + + it('provides correct parent in context', async () => { + const schema = createSchema({ + User: { + name: 'User', + fields: { + id: createField('id', 'String'), + name: createField('name', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + const fieldCallback = vi.fn(); + const visitor = new NestedWriteVisitor(schema, { field: fieldCallback }); + + const data = { name: 'Alice' }; + await visitor.visit('User', 'create', { data }); + + expect(fieldCallback).toHaveBeenCalledWith( + expect.anything(), + 'create', + 'Alice', + expect.objectContaining({ parent: data }), + ); + }); + }); + + describe('context and nesting path', () => { + it('builds nesting path correctly', async () => { + const schema = createSchema({ + User: { + name: 'User', + fields: { + id: createField('id', 'String'), + posts: createRelationField('posts', 'Post'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + Post: { + name: 'Post', + fields: { + id: createField('id', 'String'), + comments: createRelationField('comments', 'Comment'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + Comment: { + name: 'Comment', + fields: { + id: createField('id', 'String'), + text: createField('text', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + let commentContext: NestedWriteVisitorContext | undefined; + const createCallback = vi.fn((model, _data, context) => { + if (model === 'Comment') { + commentContext = context; + } + }); + + const visitor = new NestedWriteVisitor(schema, { create: createCallback }); + + await visitor.visit('User', 'create', { + data: { + name: 'Alice', + posts: { + create: { + title: 'Post', + comments: { + create: { text: 'Comment' }, + }, + }, + }, + }, + }); + + expect(commentContext).toBeDefined(); + expect(commentContext!.nestingPath).toHaveLength(3); + expect(commentContext!.nestingPath[0]?.model).toBe('User'); + expect(commentContext!.nestingPath[1]?.model).toBe('Post'); + expect(commentContext!.nestingPath[2]?.model).toBe('Comment'); + }); + + it('includes field in nesting path', async () => { + const schema = createSchema({ + User: { + name: 'User', + fields: { + id: createField('id', 'String'), + posts: createRelationField('posts', 'Post'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + Post: { + name: 'Post', + fields: { + id: createField('id', 'String'), + title: createField('title', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + let postContext: NestedWriteVisitorContext | undefined; + const createCallback = vi.fn((model, _data, context) => { + if (model === 'Post') { + postContext = context; + } + }); + + const visitor = new NestedWriteVisitor(schema, { create: createCallback }); + + await visitor.visit('User', 'create', { + data: { + posts: { + create: { title: 'Post' }, + }, + }, + }); + + expect(postContext).toBeDefined(); + expect(postContext!.field).toBeDefined(); + expect(postContext!.field?.name).toBe('posts'); + }); + + it('includes where clause in nesting path for update', async () => { + const schema = createSchema({ + User: { + name: 'User', + fields: { + id: createField('id', 'String'), + posts: createRelationField('posts', 'Post'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + Post: { + name: 'Post', + fields: { + id: createField('id', 'String'), + title: createField('title', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + let postContext: NestedWriteVisitorContext | undefined; + const updateCallback = vi.fn((model, _args, context) => { + if (model === 'Post') { + postContext = context; + } + }); + + const visitor = new NestedWriteVisitor(schema, { update: updateCallback }); + + await visitor.visit('User', 'update', { + where: { id: '1' }, + data: { + posts: { + update: { + where: { id: 'p1' }, + data: { title: 'Updated' }, + }, + }, + }, + }); + + expect(postContext).toBeDefined(); + expect(postContext!.nestingPath).toHaveLength(2); + expect(postContext!.nestingPath[1]?.where).toEqual({ id: 'p1' }); + }); + }); + + describe('edge cases', () => { + it('handles null args gracefully', async () => { + const schema = createSchema({ + User: { + name: 'User', + fields: { + id: createField('id', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + const createCallback = vi.fn(); + const visitor = new NestedWriteVisitor(schema, { create: createCallback }); + + await visitor.visit('User', 'create', null); + + expect(createCallback).not.toHaveBeenCalled(); + }); + + it('handles undefined args gracefully', async () => { + const schema = createSchema({ + User: { + name: 'User', + fields: { + id: createField('id', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + const createCallback = vi.fn(); + const visitor = new NestedWriteVisitor(schema, { create: createCallback }); + + await visitor.visit('User', 'create', undefined); + + expect(createCallback).not.toHaveBeenCalled(); + }); + + it('handles fields not in schema gracefully', async () => { + const schema = createSchema({ + User: { + name: 'User', + fields: { + id: createField('id', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + const fieldCallback = vi.fn(); + const visitor = new NestedWriteVisitor(schema, { field: fieldCallback }); + + await visitor.visit('User', 'create', { + data: { + nonExistentField: 'value', + }, + }); + + // Should not visit non-existent field + expect(fieldCallback).not.toHaveBeenCalled(); + }); + + it('handles visitor with no callbacks', async () => { + const schema = createSchema({ + User: { + name: 'User', + fields: { + id: createField('id', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + const visitor = new NestedWriteVisitor(schema, {}); + + await expect( + visitor.visit('User', 'create', { + data: { name: 'Alice' }, + }), + ).resolves.not.toThrow(); + }); + + it('throws error for unhandled action type', async () => { + const schema = createSchema({ + User: { + name: 'User', + fields: { + id: createField('id', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + const visitor = new NestedWriteVisitor(schema, {}); + + await expect(visitor.visit('User', 'invalidAction' as any, { data: {} })).rejects.toThrow( + 'unhandled action type', + ); + }); + }); + + describe('complex real-world scenarios', () => { + it('handles deeply nested create operations', async () => { + const schema = createSchema({ + User: { + name: 'User', + fields: { + id: createField('id', 'String'), + name: createField('name', 'String'), + posts: createRelationField('posts', 'Post'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + Post: { + name: 'Post', + fields: { + id: createField('id', 'String'), + title: createField('title', 'String'), + comments: createRelationField('comments', 'Comment'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + Comment: { + name: 'Comment', + fields: { + id: createField('id', 'String'), + text: createField('text', 'String'), + author: createRelationField('author', 'User'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + const visitedModels: string[] = []; + const createCallback = vi.fn((model) => { + visitedModels.push(model); + }); + + const visitor = new NestedWriteVisitor(schema, { create: createCallback }); + + await visitor.visit('User', 'create', { + data: { + name: 'Alice', + posts: { + create: { + title: 'Post', + comments: { + create: { + text: 'Comment', + author: { + create: { name: 'Bob' }, + }, + }, + }, + }, + }, + }, + }); + + expect(visitedModels).toContain('User'); + expect(visitedModels).toContain('Post'); + expect(visitedModels).toContain('Comment'); + expect(visitedModels.filter((m) => m === 'User').length).toBe(2); // Alice and Bob + }); + + it('handles mixed operations in update', async () => { + const schema = createSchema({ + User: { + name: 'User', + fields: { + id: createField('id', 'String'), + posts: createRelationField('posts', 'Post'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + Post: { + name: 'Post', + fields: { + id: createField('id', 'String'), + title: createField('title', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + const createCallback = vi.fn(); + const updateCallback = vi.fn(); + const deleteCallback = vi.fn(); + const connectCallback = vi.fn(); + + const visitor = new NestedWriteVisitor(schema, { + create: createCallback, + update: updateCallback, + delete: deleteCallback, + connect: connectCallback, + }); + + await visitor.visit('User', 'update', { + where: { id: '1' }, + data: { + posts: { + create: { title: 'New Post' }, + update: { where: { id: 'p1' }, data: { title: 'Updated' } }, + delete: { id: 'p2' }, + connect: { id: 'p3' }, + }, + }, + }); + + expect(createCallback).toHaveBeenCalledWith('Post', { title: 'New Post' }, expect.any(Object)); + expect(updateCallback).toHaveBeenCalledWith( + 'Post', + { where: { id: 'p1' }, data: { title: 'Updated' } }, + expect.any(Object), + ); + expect(deleteCallback).toHaveBeenCalledWith('Post', { id: 'p2' }, expect.any(Object)); + expect(connectCallback).toHaveBeenCalledWith('Post', { id: 'p3' }, expect.any(Object)); + }); + }); +}); diff --git a/packages/clients/client-helpers/test/optimistic.test.ts b/packages/clients/client-helpers/test/optimistic.test.ts new file mode 100644 index 00000000..40bea4e0 --- /dev/null +++ b/packages/clients/client-helpers/test/optimistic.test.ts @@ -0,0 +1,743 @@ +import { describe, expect, it, vi } from 'vitest'; +import type { Logger } from '../src/logging'; +import { createOptimisticUpdater } from '../src/optimistic'; +import type { QueryInfo } from '../src/types'; +import { createField, createRelationField, createSchema } from './test-helpers'; + +describe('Optimistic update tests', () => { + describe('createOptimisticUpdater', () => { + it('applies default optimistic update to matching queries', async () => { + const schema = createSchema({ + User: { + name: 'User', + fields: { + id: createField('id', 'String'), + name: createField('name', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + const updateDataMock = vi.fn(); + const queries: QueryInfo[] = [ + { + model: 'User', + operation: 'findMany', + args: {}, + data: [ + { id: '1', name: 'John' }, + { id: '2', name: 'Jane' }, + ], + optimisticUpdate: true, + updateData: updateDataMock, + }, + ]; + + const updater = createOptimisticUpdater('User', 'update', schema, {}, () => queries, undefined); + + await updater({ where: { id: '1' }, data: { name: 'Johnny' } }); + + // Should update the cache with the optimistic data + expect(updateDataMock).toHaveBeenCalledTimes(1); + const updatedData = updateDataMock.mock.calls[0]?.[0]; + expect(updatedData).toBeDefined(); + expect(Array.isArray(updatedData)).toBe(true); + }); + + it('skips queries with optimisticUpdate set to false', async () => { + const schema = createSchema({ + User: { + name: 'User', + fields: { + id: createField('id', 'String'), + name: createField('name', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + const updateDataMock = vi.fn(); + const queries: QueryInfo[] = [ + { + model: 'User', + operation: 'findMany', + args: {}, + data: [{ id: '1', name: 'John' }], + optimisticUpdate: false, // opted out + updateData: updateDataMock, + }, + ]; + + const updater = createOptimisticUpdater('User', 'update', schema, {}, () => queries, undefined); + + await updater({ where: { id: '1' }, data: { name: 'Johnny' } }); + + // Should not update the cache + expect(updateDataMock).not.toHaveBeenCalled(); + }); + + it('uses custom optimisticDataProvider when provided', async () => { + const schema = createSchema({ + User: { + name: 'User', + fields: { + id: createField('id', 'String'), + name: createField('name', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + const customData = [{ id: '1', name: 'Custom', $optimistic: true }]; + const optimisticDataProvider = vi.fn(() => ({ + kind: 'Update' as const, + data: customData, + })); + + const updateDataMock = vi.fn(); + const queries: QueryInfo[] = [ + { + model: 'User', + operation: 'findMany', + args: {}, + data: [{ id: '1', name: 'John' }], + optimisticUpdate: true, + updateData: updateDataMock, + }, + ]; + + const updater = createOptimisticUpdater( + 'User', + 'update', + schema, + { optimisticDataProvider }, + () => queries, + undefined, + ); + + await updater({ where: { id: '1' }, data: { name: 'Johnny' } }); + + // Provider should be called + expect(optimisticDataProvider).toHaveBeenCalledWith({ + queryModel: 'User', + queryOperation: 'findMany', + queryArgs: {}, + currentData: [{ id: '1', name: 'John' }], + mutationArgs: { where: { id: '1' }, data: { name: 'Johnny' } }, + }); + + // Should update with custom data + expect(updateDataMock).toHaveBeenCalledWith(customData, true); + }); + + it('skips update when provider returns Skip', async () => { + const schema = createSchema({ + User: { + name: 'User', + fields: { + id: createField('id', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + const optimisticDataProvider = vi.fn(() => ({ + kind: 'Skip' as const, + })); + + const updateDataMock = vi.fn(); + const queries: QueryInfo[] = [ + { + model: 'User', + operation: 'findMany', + args: {}, + data: [{ id: '1' }], + optimisticUpdate: true, + updateData: updateDataMock, + }, + ]; + + const updater = createOptimisticUpdater( + 'User', + 'update', + schema, + { optimisticDataProvider }, + () => queries, + undefined, + ); + + await updater({ where: { id: '1' }, data: {} }); + + // Provider should be called + expect(optimisticDataProvider).toHaveBeenCalled(); + + // Should not update + expect(updateDataMock).not.toHaveBeenCalled(); + }); + + it('proceeds with default update when provider returns ProceedDefault', async () => { + const schema = createSchema({ + User: { + name: 'User', + fields: { + id: createField('id', 'String'), + name: createField('name', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + const optimisticDataProvider = vi.fn(() => ({ + kind: 'ProceedDefault' as const, + })); + + const updateDataMock = vi.fn(); + const queries: QueryInfo[] = [ + { + model: 'User', + operation: 'findMany', + args: {}, + data: [{ id: '1', name: 'John' }], + optimisticUpdate: true, + updateData: updateDataMock, + }, + ]; + + const updater = createOptimisticUpdater( + 'User', + 'update', + schema, + { optimisticDataProvider }, + () => queries, + undefined, + ); + + await updater({ where: { id: '1' }, data: { name: 'Johnny' } }); + + // Provider should be called + expect(optimisticDataProvider).toHaveBeenCalled(); + + // Should proceed with default update + expect(updateDataMock).toHaveBeenCalled(); + }); + + it('handles async optimisticDataProvider', async () => { + const schema = createSchema({ + User: { + name: 'User', + fields: { + id: createField('id', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + const optimisticDataProvider = vi.fn(async () => { + await new Promise((resolve) => setTimeout(resolve, 10)); + return { + kind: 'Update' as const, + data: [{ id: '1', $optimistic: true }], + }; + }); + + const updateDataMock = vi.fn(); + const queries: QueryInfo[] = [ + { + model: 'User', + operation: 'findMany', + args: {}, + data: [], + optimisticUpdate: true, + updateData: updateDataMock, + }, + ]; + + const updater = createOptimisticUpdater( + 'User', + 'update', + schema, + { optimisticDataProvider }, + () => queries, + undefined, + ); + + await updater({ where: { id: '1' }, data: {} }); + + expect(optimisticDataProvider).toHaveBeenCalled(); + expect(updateDataMock).toHaveBeenCalled(); + }); + + it('processes multiple queries', async () => { + const schema = createSchema({ + User: { + name: 'User', + fields: { + id: createField('id', 'String'), + name: createField('name', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + const updateData1 = vi.fn(); + const updateData2 = vi.fn(); + const queries: QueryInfo[] = [ + { + model: 'User', + operation: 'findMany', + args: {}, + data: [{ id: '1', name: 'John' }], + optimisticUpdate: true, + updateData: updateData1, + }, + { + model: 'User', + operation: 'findUnique', + args: { where: { id: '1' } }, + data: { id: '1', name: 'John' }, + optimisticUpdate: true, + updateData: updateData2, + }, + ]; + + const updater = createOptimisticUpdater('User', 'update', schema, {}, () => queries, undefined); + + await updater({ where: { id: '1' }, data: { name: 'Johnny' } }); + + // Both queries should be updated + expect(updateData1).toHaveBeenCalled(); + expect(updateData2).toHaveBeenCalled(); + }); + + it('logs when logging is enabled', async () => { + const schema = createSchema({ + User: { + name: 'User', + fields: { + id: createField('id', 'String'), + name: createField('name', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + const logger = vi.fn() as Logger; + const updateDataMock = vi.fn(); + const queries: QueryInfo[] = [ + { + model: 'User', + operation: 'findMany', + args: {}, + data: [{ id: '1', name: 'John' }], + optimisticUpdate: true, + updateData: updateDataMock, + }, + ]; + + const updater = createOptimisticUpdater('User', 'update', schema, {}, () => queries, logger); + + await updater({ where: { id: '1' }, data: { name: 'Johnny' } }); + + // Logger should be called + expect(logger).toHaveBeenCalled(); + expect(logger).toHaveBeenCalledWith(expect.stringContaining('Optimistically updating')); + }); + + it('logs when skipping due to opt-out', async () => { + const schema = createSchema({ + User: { + name: 'User', + fields: { + id: createField('id', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + const logger = vi.fn() as Logger; + const updateDataMock = vi.fn(); + const queries: QueryInfo[] = [ + { + model: 'User', + operation: 'findMany', + args: {}, + data: [], + optimisticUpdate: false, + updateData: updateDataMock, + }, + ]; + + const updater = createOptimisticUpdater('User', 'update', schema, {}, () => queries, logger); + + await updater({ where: { id: '1' }, data: {} }); + + // Logger should be called with skip message + expect(logger).toHaveBeenCalledWith(expect.stringContaining('Skipping optimistic update')); + expect(logger).toHaveBeenCalledWith(expect.stringContaining('opt-out')); + }); + + it('logs when skipping due to provider', async () => { + const schema = createSchema({ + User: { + name: 'User', + fields: { + id: createField('id', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + const logger = vi.fn() as Logger; + const optimisticDataProvider = vi.fn(() => ({ + kind: 'Skip' as const, + })); + + const updateDataMock = vi.fn(); + const queries: QueryInfo[] = [ + { + model: 'User', + operation: 'findMany', + args: {}, + data: [], + optimisticUpdate: true, + updateData: updateDataMock, + }, + ]; + + const updater = createOptimisticUpdater( + 'User', + 'update', + schema, + { optimisticDataProvider }, + () => queries, + logger, + ); + + await updater({ where: { id: '1' }, data: {} }); + + // Logger should be called with skip message + expect(logger).toHaveBeenCalledWith(expect.stringContaining('Skipping optimistic updating')); + expect(logger).toHaveBeenCalledWith(expect.stringContaining('provider')); + }); + + it('logs when updating due to provider', async () => { + const schema = createSchema({ + User: { + name: 'User', + fields: { + id: createField('id', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + const logger = vi.fn() as Logger; + const optimisticDataProvider = vi.fn(() => ({ + kind: 'Update' as const, + data: [], + })); + + const updateDataMock = vi.fn(); + const queries: QueryInfo[] = [ + { + model: 'User', + operation: 'findMany', + args: {}, + data: [], + optimisticUpdate: true, + updateData: updateDataMock, + }, + ]; + + const updater = createOptimisticUpdater( + 'User', + 'update', + schema, + { optimisticDataProvider }, + () => queries, + logger, + ); + + await updater({ where: { id: '1' }, data: {} }); + + // Logger should be called with update message + expect(logger).toHaveBeenCalledWith(expect.stringContaining('Optimistically updating')); + expect(logger).toHaveBeenCalledWith(expect.stringContaining('provider')); + }); + + it('handles empty query list', async () => { + const schema = createSchema({ + User: { + name: 'User', + fields: { + id: createField('id', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + const queries: QueryInfo[] = []; + + const updater = createOptimisticUpdater('User', 'update', schema, {}, () => queries, undefined); + + // Should not throw + await expect(updater({ where: { id: '1' }, data: {} })).resolves.toBeUndefined(); + }); + + it('handles mutations on related models', async () => { + const schema = createSchema({ + User: { + name: 'User', + fields: { + id: createField('id', 'String'), + posts: createRelationField('posts', 'Post'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + Post: { + name: 'Post', + fields: { + id: createField('id', 'String'), + title: createField('title', 'String'), + userId: createField('userId', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + const updateDataMock = vi.fn(); + const queries: QueryInfo[] = [ + { + model: 'Post', + operation: 'findMany', + args: {}, + data: [ + { id: '1', title: 'Post 1', userId: '1' }, + { id: '2', title: 'Post 2', userId: '2' }, + ], + optimisticUpdate: true, + updateData: updateDataMock, + }, + ]; + + const updater = createOptimisticUpdater('Post', 'update', schema, {}, () => queries, undefined); + + await updater({ where: { id: '1' }, data: { title: 'Updated Post 1' } }); + + // Should update the cache + expect(updateDataMock).toHaveBeenCalled(); + }); + + it('extracts mutation args from first argument', async () => { + const schema = createSchema({ + User: { + name: 'User', + fields: { + id: createField('id', 'String'), + name: createField('name', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + let capturedMutationArgs: any; + const optimisticDataProvider = vi.fn((args) => { + capturedMutationArgs = args.mutationArgs; + return { kind: 'Skip' as const }; + }); + + const queries: QueryInfo[] = [ + { + model: 'User', + operation: 'findMany', + args: {}, + data: [], + optimisticUpdate: true, + updateData: vi.fn(), + }, + ]; + + const updater = createOptimisticUpdater( + 'User', + 'update', + schema, + { optimisticDataProvider }, + () => queries, + undefined, + ); + + const mutationArgs = { where: { id: '1' }, data: { name: 'Test' } }; + await updater(mutationArgs); + + // Should extract mutation args from first argument + expect(capturedMutationArgs).toEqual(mutationArgs); + }); + }); + + describe('real-world scenarios', () => { + it('handles user list update optimistically', async () => { + const schema = createSchema({ + User: { + name: 'User', + fields: { + id: createField('id', 'String'), + name: createField('name', 'String'), + email: createField('email', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + const updateDataMock = vi.fn(); + const queries: QueryInfo[] = [ + { + model: 'User', + operation: 'findMany', + args: {}, + data: [ + { id: '1', name: 'John', email: 'john@example.com' }, + { id: '2', name: 'Jane', email: 'jane@example.com' }, + ], + optimisticUpdate: true, + updateData: updateDataMock, + }, + ]; + + const updater = createOptimisticUpdater('User', 'update', schema, {}, () => queries, undefined); + + await updater({ where: { id: '1' }, data: { name: 'Johnny' } }); + + expect(updateDataMock).toHaveBeenCalled(); + const updatedData = updateDataMock.mock.calls[0]?.[0]; + expect(Array.isArray(updatedData)).toBe(true); + }); + + it('handles custom provider for complex business logic', async () => { + const schema = createSchema({ + Post: { + name: 'Post', + fields: { + id: createField('id', 'String'), + title: createField('title', 'String'), + published: createField('published', 'Boolean'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + // Custom provider that only updates published posts + const optimisticDataProvider = vi.fn(({ currentData, mutationArgs }) => { + const posts = currentData as any[]; + const updatedPosts = posts.map((post: any) => { + if (post.id === mutationArgs.where.id && post.published) { + return { ...post, ...mutationArgs.data, $optimistic: true }; + } + return post; + }); + return { kind: 'Update' as const, data: updatedPosts }; + }); + + const updateDataMock = vi.fn(); + const queries: QueryInfo[] = [ + { + model: 'Post', + operation: 'findMany', + args: { where: { published: true } }, + data: [ + { id: '1', title: 'Post 1', published: true }, + { id: '2', title: 'Post 2', published: true }, + ], + optimisticUpdate: true, + updateData: updateDataMock, + }, + ]; + + const updater = createOptimisticUpdater( + 'Post', + 'update', + schema, + { optimisticDataProvider }, + () => queries, + undefined, + ); + + await updater({ where: { id: '1' }, data: { title: 'Updated Post 1' } }); + + expect(optimisticDataProvider).toHaveBeenCalled(); + expect(updateDataMock).toHaveBeenCalled(); + const updatedData = updateDataMock.mock.calls[0]?.[0]; + expect(updatedData[0]).toHaveProperty('$optimistic', true); + }); + + it('handles mixed queries with different opt-in settings', async () => { + const schema = createSchema({ + User: { + name: 'User', + fields: { + id: createField('id', 'String'), + name: createField('name', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + const updateData1 = vi.fn(); + const updateData2 = vi.fn(); + const updateData3 = vi.fn(); + + const queries: QueryInfo[] = [ + { + model: 'User', + operation: 'findMany', + args: {}, + data: [{ id: '1', name: 'John' }], + optimisticUpdate: true, // opted in + updateData: updateData1, + }, + { + model: 'User', + operation: 'findUnique', + args: { where: { id: '1' } }, + data: { id: '1', name: 'John' }, + optimisticUpdate: false, // opted out + updateData: updateData2, + }, + { + model: 'User', + operation: 'findUnique', + args: { where: { id: '2' } }, + data: { id: '2', name: 'Jane' }, + optimisticUpdate: true, // opted in but different ID so won't be updated + updateData: updateData3, + }, + ]; + + const updater = createOptimisticUpdater('User', 'update', schema, {}, () => queries, undefined); + + await updater({ where: { id: '1' }, data: { name: 'Johnny' } }); + + // Only opted-in queries matching the mutation should be updated + expect(updateData1).toHaveBeenCalled(); // opted in and matches + expect(updateData2).not.toHaveBeenCalled(); // opted out + expect(updateData3).not.toHaveBeenCalled(); // opted in but different ID + }); + }); +}); diff --git a/packages/clients/client-helpers/test/query-analysis.test.ts b/packages/clients/client-helpers/test/query-analysis.test.ts new file mode 100644 index 00000000..4dc24c2d --- /dev/null +++ b/packages/clients/client-helpers/test/query-analysis.test.ts @@ -0,0 +1,1399 @@ +import { describe, expect, it } from 'vitest'; +import { getMutatedModels, getReadModels } from '../src/query-analysis'; +import { createField, createRelationField, createSchema } from './test-helpers'; + +describe('Query Analysis tests', () => { + describe('getReadModels', () => { + it('returns only the root model when no includes/selects', () => { + const schema = createSchema({ + User: { + name: 'User', + fields: { + id: createField('id', 'String'), + name: createField('name', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + const result = getReadModels('User', schema, {}); + + expect(result).toEqual(['User']); + }); + + it('returns models from include relations', () => { + const schema = createSchema({ + User: { + name: 'User', + fields: { + id: createField('id', 'String'), + posts: createRelationField('posts', 'Post'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + Post: { + name: 'Post', + fields: { + id: createField('id', 'String'), + title: createField('title', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + const result = getReadModels('User', schema, { + include: { + posts: true, + }, + }); + + expect(result).toContain('User'); + expect(result).toContain('Post'); + expect(result.length).toBe(2); + }); + + it('returns models from nested includes', () => { + const schema = createSchema({ + User: { + name: 'User', + fields: { + id: createField('id', 'String'), + posts: createRelationField('posts', 'Post'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + Post: { + name: 'Post', + fields: { + id: createField('id', 'String'), + comments: createRelationField('comments', 'Comment'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + Comment: { + name: 'Comment', + fields: { + id: createField('id', 'String'), + text: createField('text', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + const result = getReadModels('User', schema, { + include: { + posts: { + include: { + comments: true, + }, + }, + }, + }); + + expect(result).toContain('User'); + expect(result).toContain('Post'); + expect(result).toContain('Comment'); + expect(result.length).toBe(3); + }); + + it('returns models from select with relations', () => { + const schema = createSchema({ + User: { + name: 'User', + fields: { + id: createField('id', 'String'), + posts: createRelationField('posts', 'Post'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + Post: { + name: 'Post', + fields: { + id: createField('id', 'String'), + title: createField('title', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + const result = getReadModels('User', schema, { + select: { + id: true, + posts: true, + }, + }); + + // When using select with a relation field, the visitor visits: + // 1. User (root model) + // 2. String (for id field) + // 3. Post (for posts field) + expect(result).toContain('User'); + expect(result).toContain('Post'); + expect(result.length).toBe(3); // User, String, Post + }); + + it('handles select taking precedence over include', () => { + const schema = createSchema({ + User: { + name: 'User', + fields: { + id: createField('id', 'String'), + posts: createRelationField('posts', 'Post'), + profile: createRelationField('profile', 'Profile'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + Post: { + name: 'Post', + fields: { + id: createField('id', 'String'), + title: createField('title', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + Profile: { + name: 'Profile', + fields: { + id: createField('id', 'String'), + bio: createField('bio', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + // When both select and include are present, select takes precedence + const result = getReadModels('User', schema, { + include: { + posts: true, + }, + select: { + profile: true, + }, + }); + + expect(result).toContain('User'); + expect(result).toContain('Profile'); + // Posts is not included because select takes precedence + expect(result.length).toBe(2); + }); + + it('deduplicates model names', () => { + const schema = createSchema({ + User: { + name: 'User', + fields: { + id: createField('id', 'String'), + posts: createRelationField('posts', 'Post'), + comments: createRelationField('comments', 'Comment'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + Post: { + name: 'Post', + fields: { + id: createField('id', 'String'), + comments: createRelationField('comments', 'Comment'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + Comment: { + name: 'Comment', + fields: { + id: createField('id', 'String'), + text: createField('text', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + const result = getReadModels('User', schema, { + include: { + posts: { + include: { + comments: true, + }, + }, + comments: true, + }, + }); + + expect(result).toContain('User'); + expect(result).toContain('Post'); + expect(result).toContain('Comment'); + expect(result.length).toBe(3); // Comment should not be duplicated + }); + + it('handles undefined args', () => { + const schema = createSchema({ + User: { + name: 'User', + fields: { + id: createField('id', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + const result = getReadModels('User', schema, undefined); + + expect(result).toEqual(['User']); + }); + + it('handles null args', () => { + const schema = createSchema({ + User: { + name: 'User', + fields: { + id: createField('id', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + const result = getReadModels('User', schema, null); + + expect(result).toEqual(['User']); + }); + }); + + describe('getMutatedModels', () => { + describe('basic mutations', () => { + it('returns only the root model for simple create', async () => { + const schema = createSchema({ + User: { + name: 'User', + fields: { + id: createField('id', 'String'), + name: createField('name', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + const result = await getMutatedModels('User', 'create', { data: { name: 'John' } }, schema); + + expect(result).toEqual(['User']); + }); + + it('returns only the root model for simple update', async () => { + const schema = createSchema({ + User: { + name: 'User', + fields: { + id: createField('id', 'String'), + name: createField('name', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + const result = await getMutatedModels( + 'User', + 'update', + { where: { id: '1' }, data: { name: 'Jane' } }, + schema, + ); + + expect(result).toEqual(['User']); + }); + + it('returns only the root model for delete', async () => { + const schema = createSchema({ + User: { + name: 'User', + fields: { + id: createField('id', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + const result = await getMutatedModels('User', 'delete', { where: { id: '1' } }, schema); + + expect(result).toEqual(['User']); + }); + }); + + describe('nested mutations', () => { + it('includes models from nested create', async () => { + const schema = createSchema({ + User: { + name: 'User', + fields: { + id: createField('id', 'String'), + posts: createRelationField('posts', 'Post'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + Post: { + name: 'Post', + fields: { + id: createField('id', 'String'), + title: createField('title', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + const result = await getMutatedModels( + 'User', + 'create', + { + data: { + name: 'John', + posts: { + create: { title: 'My Post' }, + }, + }, + }, + schema, + ); + + expect(result).toContain('User'); + expect(result).toContain('Post'); + expect(result.length).toBe(2); + }); + + it('includes models from nested update', async () => { + const schema = createSchema({ + User: { + name: 'User', + fields: { + id: createField('id', 'String'), + posts: createRelationField('posts', 'Post'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + Post: { + name: 'Post', + fields: { + id: createField('id', 'String'), + title: createField('title', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + const result = await getMutatedModels( + 'User', + 'update', + { + where: { id: '1' }, + data: { + posts: { + update: { + where: { id: '1' }, + data: { title: 'Updated' }, + }, + }, + }, + }, + schema, + ); + + expect(result).toContain('User'); + expect(result).toContain('Post'); + expect(result.length).toBe(2); + }); + + it('includes models from nested connect', async () => { + const schema = createSchema({ + User: { + name: 'User', + fields: { + id: createField('id', 'String'), + posts: createRelationField('posts', 'Post'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + Post: { + name: 'Post', + fields: { + id: createField('id', 'String'), + title: createField('title', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + const result = await getMutatedModels( + 'User', + 'update', + { + where: { id: '1' }, + data: { + posts: { + connect: { id: '1' }, + }, + }, + }, + schema, + ); + + expect(result).toContain('User'); + expect(result).toContain('Post'); + expect(result.length).toBe(2); + }); + + it('includes models from nested disconnect', async () => { + const schema = createSchema({ + User: { + name: 'User', + fields: { + id: createField('id', 'String'), + posts: createRelationField('posts', 'Post'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + Post: { + name: 'Post', + fields: { + id: createField('id', 'String'), + title: createField('title', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + const result = await getMutatedModels( + 'User', + 'update', + { + where: { id: '1' }, + data: { + posts: { + disconnect: { id: '1' }, + }, + }, + }, + schema, + ); + + expect(result).toContain('User'); + expect(result).toContain('Post'); + expect(result.length).toBe(2); + }); + + it('includes models from nested set', async () => { + const schema = createSchema({ + User: { + name: 'User', + fields: { + id: createField('id', 'String'), + posts: createRelationField('posts', 'Post'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + Post: { + name: 'Post', + fields: { + id: createField('id', 'String'), + title: createField('title', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + const result = await getMutatedModels( + 'User', + 'update', + { + where: { id: '1' }, + data: { + posts: { + set: [{ id: '1' }], + }, + }, + }, + schema, + ); + + expect(result).toContain('User'); + expect(result).toContain('Post'); + expect(result.length).toBe(2); + }); + + it('includes models from nested upsert', async () => { + const schema = createSchema({ + User: { + name: 'User', + fields: { + id: createField('id', 'String'), + posts: createRelationField('posts', 'Post'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + Post: { + name: 'Post', + fields: { + id: createField('id', 'String'), + title: createField('title', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + const result = await getMutatedModels( + 'User', + 'update', + { + where: { id: '1' }, + data: { + posts: { + upsert: { + where: { id: '1' }, + create: { title: 'New' }, + update: { title: 'Updated' }, + }, + }, + }, + }, + schema, + ); + + expect(result).toContain('User'); + expect(result).toContain('Post'); + expect(result.length).toBe(2); + }); + + it('includes models from nested createMany', async () => { + const schema = createSchema({ + User: { + name: 'User', + fields: { + id: createField('id', 'String'), + posts: createRelationField('posts', 'Post'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + Post: { + name: 'Post', + fields: { + id: createField('id', 'String'), + title: createField('title', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + const result = await getMutatedModels( + 'User', + 'create', + { + data: { + name: 'John', + posts: { + createMany: { + data: [{ title: 'Post 1' }, { title: 'Post 2' }], + }, + }, + }, + }, + schema, + ); + + expect(result).toContain('User'); + expect(result).toContain('Post'); + expect(result.length).toBe(2); + }); + + it('includes models from nested updateMany', async () => { + const schema = createSchema({ + User: { + name: 'User', + fields: { + id: createField('id', 'String'), + posts: createRelationField('posts', 'Post'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + Post: { + name: 'Post', + fields: { + id: createField('id', 'String'), + title: createField('title', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + const result = await getMutatedModels( + 'User', + 'update', + { + where: { id: '1' }, + data: { + posts: { + updateMany: { + where: { published: false }, + data: { published: true }, + }, + }, + }, + }, + schema, + ); + + expect(result).toContain('User'); + expect(result).toContain('Post'); + expect(result.length).toBe(2); + }); + + it('includes models from nested connectOrCreate', async () => { + const schema = createSchema({ + User: { + name: 'User', + fields: { + id: createField('id', 'String'), + posts: createRelationField('posts', 'Post'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + Post: { + name: 'Post', + fields: { + id: createField('id', 'String'), + title: createField('title', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + const result = await getMutatedModels( + 'User', + 'update', + { + where: { id: '1' }, + data: { + posts: { + connectOrCreate: { + where: { id: '1' }, + create: { title: 'New Post' }, + }, + }, + }, + }, + schema, + ); + + expect(result).toContain('User'); + expect(result).toContain('Post'); + expect(result.length).toBe(2); + }); + + it('includes models from deeply nested mutations', async () => { + const schema = createSchema({ + User: { + name: 'User', + fields: { + id: createField('id', 'String'), + posts: createRelationField('posts', 'Post'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + Post: { + name: 'Post', + fields: { + id: createField('id', 'String'), + comments: createRelationField('comments', 'Comment'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + Comment: { + name: 'Comment', + fields: { + id: createField('id', 'String'), + text: createField('text', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + const result = await getMutatedModels( + 'User', + 'create', + { + data: { + name: 'John', + posts: { + create: { + title: 'My Post', + comments: { + create: { text: 'Great!' }, + }, + }, + }, + }, + }, + schema, + ); + + expect(result).toContain('User'); + expect(result).toContain('Post'); + expect(result).toContain('Comment'); + expect(result.length).toBe(3); + }); + }); + + describe('cascade deletes', () => { + it('includes cascaded models when deleting', async () => { + const schema = createSchema({ + User: { + name: 'User', + fields: { + id: createField('id', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + Post: { + name: 'Post', + fields: { + id: createField('id', 'String'), + user: { + name: 'user', + type: 'User', + optional: false, + relation: { + opposite: 'posts', + onDelete: 'Cascade', + }, + }, + }, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + const result = await getMutatedModels('User', 'delete', { where: { id: '1' } }, schema); + + expect(result).toContain('User'); + expect(result).toContain('Post'); + expect(result.length).toBe(2); + }); + + it('includes cascaded models when using deleteMany', async () => { + const schema = createSchema({ + User: { + name: 'User', + fields: { + id: createField('id', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + Post: { + name: 'Post', + fields: { + id: createField('id', 'String'), + user: { + name: 'user', + type: 'User', + optional: false, + relation: { + opposite: 'posts', + onDelete: 'Cascade', + }, + }, + }, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + const result = await getMutatedModels('User', 'deleteMany', { where: { active: false } }, schema); + + expect(result).toContain('User'); + expect(result).toContain('Post'); + expect(result.length).toBe(2); + }); + + it('includes multi-level cascade deletes', async () => { + const schema = createSchema({ + User: { + name: 'User', + fields: { + id: createField('id', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + Post: { + name: 'Post', + fields: { + id: createField('id', 'String'), + user: { + name: 'user', + type: 'User', + optional: false, + relation: { + opposite: 'posts', + onDelete: 'Cascade', + }, + }, + }, + uniqueFields: {}, + idFields: ['id'], + }, + Comment: { + name: 'Comment', + fields: { + id: createField('id', 'String'), + post: { + name: 'post', + type: 'Post', + optional: false, + relation: { + opposite: 'comments', + onDelete: 'Cascade', + }, + }, + }, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + const result = await getMutatedModels('User', 'delete', { where: { id: '1' } }, schema); + + expect(result).toContain('User'); + expect(result).toContain('Post'); + expect(result).toContain('Comment'); + expect(result.length).toBe(3); + }); + + it('does not include models without cascade delete', async () => { + const schema = createSchema({ + User: { + name: 'User', + fields: { + id: createField('id', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + Post: { + name: 'Post', + fields: { + id: createField('id', 'String'), + user: { + name: 'user', + type: 'User', + optional: false, + relation: { + opposite: 'posts', + onDelete: 'SetNull', + }, + }, + }, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + const result = await getMutatedModels('User', 'delete', { where: { id: '1' } }, schema); + + expect(result).toEqual(['User']); + }); + + it('handles circular cascade relationships', async () => { + const schema = createSchema({ + User: { + name: 'User', + fields: { + id: createField('id', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + Post: { + name: 'Post', + fields: { + id: createField('id', 'String'), + user: { + name: 'user', + type: 'User', + optional: false, + relation: { + opposite: 'posts', + onDelete: 'Cascade', + }, + }, + }, + uniqueFields: {}, + idFields: ['id'], + }, + Comment: { + name: 'Comment', + fields: { + id: createField('id', 'String'), + post: { + name: 'post', + type: 'Post', + optional: false, + relation: { + opposite: 'comments', + onDelete: 'Cascade', + }, + }, + // This creates a potential circle: User -> Post -> Comment -> Post + relatedPost: { + name: 'relatedPost', + type: 'Post', + optional: true, + relation: { + opposite: 'relatedComments', + onDelete: 'Cascade', + }, + }, + }, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + const result = await getMutatedModels('User', 'delete', { where: { id: '1' } }, schema); + + expect(result).toContain('User'); + expect(result).toContain('Post'); + expect(result).toContain('Comment'); + expect(result.length).toBe(3); + }); + }); + + describe('delegate base models', () => { + it('includes base model when mutating child', async () => { + const schema = createSchema({ + Animal: { + name: 'Animal', + fields: { + id: createField('id', 'String'), + name: createField('name', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + Dog: { + name: 'Dog', + baseModel: 'Animal', + fields: { + id: createField('id', 'String'), + breed: createField('breed', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + const result = await getMutatedModels('Dog', 'create', { data: { breed: 'Labrador' } }, schema); + + expect(result).toContain('Dog'); + expect(result).toContain('Animal'); + expect(result.length).toBe(2); + }); + + it('includes multi-level base models', async () => { + const schema = createSchema({ + Entity: { + name: 'Entity', + fields: { + id: createField('id', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + Animal: { + name: 'Animal', + baseModel: 'Entity', + fields: { + id: createField('id', 'String'), + name: createField('name', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + Dog: { + name: 'Dog', + baseModel: 'Animal', + fields: { + id: createField('id', 'String'), + breed: createField('breed', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + const result = await getMutatedModels('Dog', 'create', { data: { breed: 'Labrador' } }, schema); + + expect(result).toContain('Dog'); + expect(result).toContain('Animal'); + expect(result).toContain('Entity'); + expect(result.length).toBe(3); + }); + + it('includes base models for nested mutations', async () => { + const schema = createSchema({ + User: { + name: 'User', + fields: { + id: createField('id', 'String'), + pets: createRelationField('pets', 'Dog'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + Animal: { + name: 'Animal', + fields: { + id: createField('id', 'String'), + name: createField('name', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + Dog: { + name: 'Dog', + baseModel: 'Animal', + fields: { + id: createField('id', 'String'), + breed: createField('breed', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + const result = await getMutatedModels( + 'User', + 'create', + { + data: { + name: 'John', + pets: { + create: { breed: 'Labrador' }, + }, + }, + }, + schema, + ); + + expect(result).toContain('User'); + expect(result).toContain('Dog'); + expect(result).toContain('Animal'); + expect(result.length).toBe(3); + }); + }); + + describe('edge cases', () => { + it('handles undefined args', async () => { + const schema = createSchema({ + User: { + name: 'User', + fields: { + id: createField('id', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + const result = await getMutatedModels('User', 'create', undefined, schema); + + expect(result).toEqual(['User']); + }); + + it('handles null args', async () => { + const schema = createSchema({ + User: { + name: 'User', + fields: { + id: createField('id', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + const result = await getMutatedModels('User', 'create', null, schema); + + expect(result).toEqual(['User']); + }); + + it('deduplicates models from multiple sources', async () => { + const schema = createSchema({ + User: { + name: 'User', + fields: { + id: createField('id', 'String'), + posts: createRelationField('posts', 'Post'), + comments: createRelationField('comments', 'Comment'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + Post: { + name: 'Post', + fields: { + id: createField('id', 'String'), + comments: createRelationField('comments', 'Comment'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + Comment: { + name: 'Comment', + fields: { + id: createField('id', 'String'), + text: createField('text', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + const result = await getMutatedModels( + 'User', + 'create', + { + data: { + name: 'John', + posts: { + create: { + title: 'Post', + comments: { + create: { text: 'Comment' }, + }, + }, + }, + comments: { + create: { text: 'Comment' }, + }, + }, + }, + schema, + ); + + expect(result).toContain('User'); + expect(result).toContain('Post'); + expect(result).toContain('Comment'); + expect(result.length).toBe(3); // Comment should not be duplicated + }); + + it('handles model not in schema', async () => { + const schema = createSchema({ + User: { + name: 'User', + fields: { + id: createField('id', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + const result = await getMutatedModels('NonExistent', 'create', { data: {} }, schema); + + expect(result).toEqual(['NonExistent']); + }); + }); + + describe('real-world scenarios', () => { + it('handles complex nested mutation with cascades and base models', async () => { + const schema = createSchema({ + Entity: { + name: 'Entity', + fields: { + id: createField('id', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + User: { + name: 'User', + baseModel: 'Entity', + fields: { + id: createField('id', 'String'), + posts: createRelationField('posts', 'Post'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + Post: { + name: 'Post', + fields: { + id: createField('id', 'String'), + user: { + name: 'user', + type: 'User', + optional: false, + relation: { + opposite: 'posts', + onDelete: 'Cascade', + }, + }, + comments: createRelationField('comments', 'Comment'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + Comment: { + name: 'Comment', + fields: { + id: createField('id', 'String'), + post: { + name: 'post', + type: 'Post', + optional: false, + relation: { + opposite: 'comments', + onDelete: 'Cascade', + }, + }, + }, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + const result = await getMutatedModels( + 'User', + 'update', + { + where: { id: '1' }, + data: { + posts: { + create: { + title: 'New Post', + comments: { + create: { text: 'Comment' }, + }, + }, + delete: { id: '2' }, + }, + }, + }, + schema, + ); + + expect(result).toContain('User'); + expect(result).toContain('Entity'); // base model + expect(result).toContain('Post'); + expect(result).toContain('Comment'); // both from create and cascade delete + expect(result.length).toBe(4); + }); + + it('handles blog post creation with author, tags, and categories', async () => { + const schema = createSchema({ + User: { + name: 'User', + fields: { + id: createField('id', 'String'), + posts: createRelationField('posts', 'Post'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + Post: { + name: 'Post', + fields: { + id: createField('id', 'String'), + author: createRelationField('author', 'User'), + tags: createRelationField('tags', 'Tag'), + categories: createRelationField('categories', 'Category'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + Tag: { + name: 'Tag', + fields: { + id: createField('id', 'String'), + name: createField('name', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + Category: { + name: 'Category', + fields: { + id: createField('id', 'String'), + name: createField('name', 'String'), + }, + uniqueFields: {}, + idFields: ['id'], + }, + }); + + const result = await getMutatedModels( + 'Post', + 'create', + { + data: { + title: 'My Post', + author: { + connect: { id: '1' }, + }, + tags: { + create: [{ name: 'tech' }, { name: 'tutorial' }], + }, + categories: { + connectOrCreate: { + where: { id: '1' }, + create: { name: 'Programming' }, + }, + }, + }, + }, + schema, + ); + + expect(result).toContain('Post'); + expect(result).toContain('User'); + expect(result).toContain('Tag'); + expect(result).toContain('Category'); + expect(result.length).toBe(4); + }); + }); + }); +}); diff --git a/packages/clients/client-helpers/test/test-helpers.ts b/packages/clients/client-helpers/test/test-helpers.ts new file mode 100644 index 00000000..9d6515a5 --- /dev/null +++ b/packages/clients/client-helpers/test/test-helpers.ts @@ -0,0 +1,37 @@ +import type { FieldDef, SchemaDef } from '@zenstackhq/schema'; + +/** + * Helper to create a mock schema for testing + */ +export function createSchema(models: SchemaDef['models']): SchemaDef { + return { + provider: { type: 'postgresql' }, + models, + plugins: {}, + }; +} + +/** + * Helper to create a field definition + */ +export function createField(name: string, type: string, optional = false): FieldDef { + return { + name, + type, + optional, + }; +} + +/** + * Helper to create a relation field + */ +export function createRelationField(name: string, type: string, optional = false): FieldDef { + return { + name, + type, + optional, + relation: { + opposite: 'user', + }, + }; +} diff --git a/packages/clients/client-helpers/tsconfig.json b/packages/clients/client-helpers/tsconfig.json new file mode 100644 index 00000000..8ef64682 --- /dev/null +++ b/packages/clients/client-helpers/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "@zenstackhq/typescript-config/base.json", + "include": ["src/**/*.ts"] +} diff --git a/packages/clients/client-helpers/tsconfig.test.json b/packages/clients/client-helpers/tsconfig.test.json new file mode 100644 index 00000000..53cf6cf5 --- /dev/null +++ b/packages/clients/client-helpers/tsconfig.test.json @@ -0,0 +1,7 @@ +{ + "extends": "@zenstackhq/typescript-config/base.json", + "include": ["src/**/*.ts", "test/**/*.ts"], + "compilerOptions": { + "lib": ["ESNext"] + } +} diff --git a/packages/clients/tanstack-query/tsup.config.ts b/packages/clients/client-helpers/tsup.config.ts similarity index 61% rename from packages/clients/tanstack-query/tsup.config.ts rename to packages/clients/client-helpers/tsup.config.ts index 1bfafb93..211b4d5b 100644 --- a/packages/clients/tanstack-query/tsup.config.ts +++ b/packages/clients/client-helpers/tsup.config.ts @@ -2,14 +2,13 @@ import { defineConfig } from 'tsup'; export default defineConfig({ entry: { - react: 'src/react.ts', - vue: 'src/vue.ts', - svelte: 'src/svelte.ts', + index: 'src/index.ts', + fetch: 'src/fetch.ts', }, outDir: 'dist', splitting: false, sourcemap: true, clean: true, dts: true, - format: ['cjs', 'esm'], + format: ['esm'], }); diff --git a/packages/clients/client-helpers/vitest.config.ts b/packages/clients/client-helpers/vitest.config.ts new file mode 100644 index 00000000..75a9f709 --- /dev/null +++ b/packages/clients/client-helpers/vitest.config.ts @@ -0,0 +1,4 @@ +import base from '@zenstackhq/vitest-config/base'; +import { defineConfig, mergeConfig } from 'vitest/config'; + +export default mergeConfig(base, defineConfig({})); diff --git a/packages/clients/tanstack-query/.gitignore b/packages/clients/tanstack-query/.gitignore new file mode 100644 index 00000000..8ee84755 --- /dev/null +++ b/packages/clients/tanstack-query/.gitignore @@ -0,0 +1 @@ +.svelte-kit diff --git a/packages/clients/tanstack-query/package.json b/packages/clients/tanstack-query/package.json index 15451c0c..41659f5f 100644 --- a/packages/clients/tanstack-query/package.json +++ b/packages/clients/tanstack-query/package.json @@ -1,12 +1,15 @@ { "name": "@zenstackhq/tanstack-query", - "version": "3.0.0", + "version": "3.1.0", "description": "TanStack Query Client for consuming ZenStack v3's CRUD service", - "main": "index.js", "type": "module", "scripts": { - "build": "tsc --noEmit && tsup-node && pnpm test:generate && pnpm test:typecheck", - "watch": "tsup-node --watch", + "build": "pnpm build:general && pnpm build:svelte && pnpm test:generate && pnpm test:typecheck", + "build:general": "tsc -p tsconfig.general.json", + "build:svelte": "svelte-package -i src/svelte -o dist/svelte -t --tsconfig ./tsconfig.svelte.json", + "watch:general": "tsc -p tsconfig.general.json -w", + "watch:svelte": "svelte-package -i src/svelte -o dist/svelte -t --watch -p", + "watch": "run-p watch:*", "lint": "eslint src --ext ts", "test": "vitest run", "pack": "pnpm pack", @@ -22,72 +25,57 @@ ], "author": "ZenStack Team", "license": "MIT", + "files": [ + "dist" + ], "exports": { "./react": { - "import": { - "types": "./dist/react.d.ts", - "default": "./dist/react.js" - }, - "require": { - "types": "./dist/react.d.cts", - "default": "./dist/react.cjs" - } + "types": "./dist/react.d.ts", + "default": "./dist/react.js" }, "./vue": { - "import": { - "types": "./dist/vue.d.ts", - "default": "./dist/vue.js" - }, - "require": { - "types": "./dist/vue.d.cts", - "default": "./dist/vue.cjs" - } + "types": "./dist/vue.d.ts", + "default": "./dist/vue.js" }, "./svelte": { - "import": { - "types": "./dist/svelte.d.ts", - "default": "./dist/svelte.js" - }, - "require": { - "types": "./dist/svelte.d.cts", - "default": "./dist/svelte.cjs" - } + "types": "./dist/svelte/index.svelte.d.ts", + "svelte": "./dist/svelte/index.svelte.js", + "default": "./dist/svelte/index.svelte.js" }, - "./package.json": { - "import": "./package.json", - "require": "./package.json" - } + "./package.json": "./package.json" }, "dependencies": { + "@zenstackhq/client-helpers": "workspace:*", "@zenstackhq/common-helpers": "workspace:*", - "@zenstackhq/orm": "workspace:*", "@zenstackhq/schema": "workspace:*", - "decimal.js": "catalog:", - "superjson": "^2.2.3" + "decimal.js": "catalog:" }, "devDependencies": { + "@tanstack/query-core": "catalog:", "@tanstack/react-query": "catalog:", - "@tanstack/vue-query": "5.90.6", - "@tanstack/svelte-query": "5.90.2", + "@tanstack/svelte-query": "catalog:", + "@tanstack/vue-query": "catalog:", "@testing-library/dom": "^10.4.1", "@testing-library/react": "^16.3.0", "@types/react": "catalog:", + "@zenstackhq/cli": "workspace:*", "@zenstackhq/eslint-config": "workspace:*", "@zenstackhq/language": "workspace:*", + "@zenstackhq/orm": "workspace:*", "@zenstackhq/sdk": "workspace:*", - "@zenstackhq/cli": "workspace:*", "@zenstackhq/typescript-config": "workspace:*", "@zenstackhq/vitest-config": "workspace:*", "happy-dom": "^20.0.10", "nock": "^14.0.10", "react": "catalog:", + "svelte": "catalog:", "vue": "catalog:", - "svelte": "catalog:" + "@sveltejs/package": "^2.5.7" }, "peerDependencies": { "@tanstack/react-query": "^5.0.0", - "@tanstack/vue-query": "^5.0.0", - "@tanstack/svelte-query": "^5.0.0" + "@tanstack/svelte-query": "^6.0.0", + "@tanstack/vue-query": "^5.0.0" }, "peerDependenciesMeta": { "@tanstack/react-query": { diff --git a/packages/clients/tanstack-query/src/common/.gitignore b/packages/clients/tanstack-query/src/common/.gitignore new file mode 100644 index 00000000..cd4efd8e --- /dev/null +++ b/packages/clients/tanstack-query/src/common/.gitignore @@ -0,0 +1 @@ +*.d.ts diff --git a/packages/clients/tanstack-query/src/common/client.ts b/packages/clients/tanstack-query/src/common/client.ts new file mode 100644 index 00000000..c9609d51 --- /dev/null +++ b/packages/clients/tanstack-query/src/common/client.ts @@ -0,0 +1,41 @@ +import type { QueryClient } from '@tanstack/query-core'; +import type { InvalidationPredicate, QueryInfo } from '@zenstackhq/client-helpers'; +import { parseQueryKey } from './query-key'; + +export function invalidateQueriesMatchingPredicate(queryClient: QueryClient, predicate: InvalidationPredicate) { + return queryClient.invalidateQueries({ + predicate: ({ queryKey }) => { + const parsed = parseQueryKey(queryKey); + if (!parsed) { + return false; + } + return predicate({ model: parsed.model as string, args: parsed.args }); + }, + }); +} + +export function getAllQueries(queryClient: QueryClient): readonly QueryInfo[] { + return queryClient + .getQueryCache() + .getAll() + .map(({ queryKey, state }) => { + const parsed = parseQueryKey(queryKey); + if (!parsed) { + return undefined; + } + return { + model: parsed?.model, + operation: parsed?.operation, + args: parsed?.args, + data: state.data, + optimisticUpdate: !!parsed.flags.optimisticUpdate, + updateData: (data: unknown, cancelOnTheFlyQueries: boolean) => { + queryClient.setQueryData(queryKey, data); + if (cancelOnTheFlyQueries) { + queryClient.cancelQueries({ queryKey }, { revert: false, silent: true }); + } + }, + }; + }) + .filter((entry) => !!entry); +} diff --git a/packages/clients/tanstack-query/src/common/query-key.ts b/packages/clients/tanstack-query/src/common/query-key.ts new file mode 100644 index 00000000..9c62451b --- /dev/null +++ b/packages/clients/tanstack-query/src/common/query-key.ts @@ -0,0 +1,58 @@ +/** + * Prefix for react-query keys. + */ +export const QUERY_KEY_PREFIX = 'zenstack'; + +export type QueryKey = [ + string /* prefix */, + string /* model */, + string /* operation */, + unknown /* args */, + { + infinite: boolean; + optimisticUpdate: boolean; + } /* flags */, +]; + +/** + * Computes query key for the given model, operation and query args. + * @param model Model name. + * @param operation Query operation (e.g, `findMany`) or request URL. If it's a URL, the last path segment will be used as the operation name. + * @param args Query arguments. + * @param options Query options, including `infinite` indicating if it's an infinite query (defaults to false), and `optimisticUpdate` indicating if optimistic updates are enabled (defaults to true). + * @returns Query key + */ +export function getQueryKey( + model: string, + operation: string, + args: unknown, + options: { infinite: boolean; optimisticUpdate: boolean } = { infinite: false, optimisticUpdate: true }, +): QueryKey { + const infinite = options.infinite; + // infinite query doesn't support optimistic updates + const optimisticUpdate = options.infinite ? false : options.optimisticUpdate; + return [QUERY_KEY_PREFIX, model, operation!, args, { infinite, optimisticUpdate }]; +} + +/** + * Parses the given query key into its components. + */ +export function parseQueryKey(queryKey: readonly unknown[]) { + const [prefix, model, operation, args, flags] = queryKey as QueryKey; + if (prefix !== QUERY_KEY_PREFIX) { + return undefined; + } + return { model, operation, args, flags }; +} + +export function isZenStackQueryKey(queryKey: readonly unknown[]): queryKey is QueryKey { + if (queryKey.length < 5) { + return false; + } + + if (queryKey[0] !== QUERY_KEY_PREFIX) { + return false; + } + + return true; +} diff --git a/packages/clients/tanstack-query/src/common/types.ts b/packages/clients/tanstack-query/src/common/types.ts new file mode 100644 index 00000000..8993445e --- /dev/null +++ b/packages/clients/tanstack-query/src/common/types.ts @@ -0,0 +1,78 @@ +import type { Logger, OptimisticDataProvider } from '@zenstackhq/client-helpers'; +import type { FetchFn } from '@zenstackhq/client-helpers/fetch'; +import type { OperationsIneligibleForDelegateModels } from '@zenstackhq/orm'; +import type { GetModels, IsDelegateModel, SchemaDef } from '@zenstackhq/schema'; + +/** + * Context type for configuring the hooks. + */ +export type QueryContext = { + /** + * The endpoint to use for the queries. + */ + endpoint?: string; + + /** + * A custom fetch function for sending the HTTP requests. + */ + fetch?: FetchFn; + + /** + * If logging is enabled. + */ + logging?: Logger; +}; + +/** + * Extra query options. + */ +export type ExtraQueryOptions = { + /** + * Whether to opt-in to optimistic updates for this query. Defaults to `true`. + */ + optimisticUpdate?: boolean; +} & QueryContext; + +/** + * Extra mutation options. + */ +export type ExtraMutationOptions = { + /** + * Whether to automatically invalidate queries potentially affected by the mutation. Defaults to `true`. + */ + invalidateQueries?: boolean; + + /** + * Whether to optimistically update queries potentially affected by the mutation. Defaults to `false`. + */ + optimisticUpdate?: boolean; + + /** + * A callback for computing optimistic update data for each query cache entry. + */ + optimisticDataProvider?: OptimisticDataProvider; +} & QueryContext; + +type HooksOperationsIneligibleForDelegateModels = OperationsIneligibleForDelegateModels extends any + ? `use${Capitalize}` + : never; + +/** + * Trim operations that are ineligible for delegate models from the given model operations type. + */ +export type TrimDelegateModelOperations< + Schema extends SchemaDef, + Model extends GetModels, + T extends Record, +> = IsDelegateModel extends true ? Omit : T; + +type WithOptimisticFlag = T extends object + ? T & { + /** + * Indicates if the item is in an optimistic update state + */ + $optimistic?: boolean; + } + : T; + +export type WithOptimistic = T extends Array ? Array> : WithOptimisticFlag; diff --git a/packages/clients/tanstack-query/src/react.ts b/packages/clients/tanstack-query/src/react.ts index 9cd61727..4449e29f 100644 --- a/packages/clients/tanstack-query/src/react.ts +++ b/packages/clients/tanstack-query/src/react.ts @@ -19,6 +19,8 @@ import { type UseSuspenseQueryOptions, type UseSuspenseQueryResult, } from '@tanstack/react-query'; +import { createInvalidator, createOptimisticUpdater, DEFAULT_QUERY_ENDPOINT } from '@zenstackhq/client-helpers'; +import { fetcher, makeUrl, marshal } from '@zenstackhq/client-helpers/fetch'; import { lowerCaseFirst } from '@zenstackhq/common-helpers'; import type { AggregateArgs, @@ -48,30 +50,21 @@ import type { } from '@zenstackhq/orm'; import type { GetModels, SchemaDef } from '@zenstackhq/schema'; import { createContext, useContext } from 'react'; -import { - fetcher, - getQueryKey, - makeUrl, - marshal, - setupInvalidation, - setupOptimisticUpdate, - type APIContext, - type ExtraMutationOptions, - type ExtraQueryOptions, -} from './utils/common'; -import type { TrimDelegateModelOperations, WithOptimistic } from './utils/types'; - -export type { FetchFn } from './utils/common'; - -/** - * The default query endpoint. - */ -export const DEFAULT_QUERY_ENDPOINT = '/api/model'; +import { getAllQueries, invalidateQueriesMatchingPredicate } from './common/client'; +import { getQueryKey } from './common/query-key'; +import type { + ExtraMutationOptions, + ExtraQueryOptions, + QueryContext, + TrimDelegateModelOperations, + WithOptimistic, +} from './common/types'; +export type { FetchFn } from '@zenstackhq/client-helpers/fetch'; /** * React context for query settings. */ -export const QuerySettingsContext = createContext({ +export const QuerySettingsContext = createContext({ endpoint: DEFAULT_QUERY_ENDPOINT, fetch: undefined, }); @@ -262,15 +255,20 @@ export type ModelQueryHooks< /** * Gets data query hooks for all models in the schema. + * + * @param schema The schema. + * @param options Options for all queries originated from this hook. */ export function useClientQueries = QueryOptions>( schema: Schema, + options?: QueryContext, ): ClientHooks { return Object.keys(schema.models).reduce( (acc, model) => { (acc as any)[lowerCaseFirst(model)] = useModelQueries, Options>( schema, model as GetModels, + options, ); return acc; }, @@ -285,7 +283,7 @@ export function useModelQueries< Schema extends SchemaDef, Model extends GetModels, Options extends QueryOptions, ->(schema: Schema, model: Model): ModelQueryHooks { +>(schema: Schema, model: Model, rootOptions?: QueryContext): ModelQueryHooks { const modelDef = Object.values(schema.models).find((m) => m.name.toLowerCase() === model.toLowerCase()); if (!modelDef) { throw new Error(`Model "${model}" not found in schema`); @@ -295,95 +293,101 @@ export function useModelQueries< return { useFindUnique: (args: any, options?: any) => { - return useInternalQuery(schema, modelName, 'findUnique', args, options); + return useInternalQuery(schema, modelName, 'findUnique', args, { ...rootOptions, ...options }); }, useSuspenseFindUnique: (args: any, options?: any) => { - return useInternalSuspenseQuery(schema, modelName, 'findUnique', args, options); + return useInternalSuspenseQuery(schema, modelName, 'findUnique', args, { ...rootOptions, ...options }); }, useFindFirst: (args: any, options?: any) => { - return useInternalQuery(schema, modelName, 'findFirst', args, options); + return useInternalQuery(schema, modelName, 'findFirst', args, { ...rootOptions, ...options }); }, useSuspenseFindFirst: (args: any, options?: any) => { - return useInternalSuspenseQuery(schema, modelName, 'findFirst', args, options); + return useInternalSuspenseQuery(schema, modelName, 'findFirst', args, { ...rootOptions, ...options }); }, useFindMany: (args: any, options?: any) => { - return useInternalQuery(schema, modelName, 'findMany', args, options); + return useInternalQuery(schema, modelName, 'findMany', args, { ...rootOptions, ...options }); }, useSuspenseFindMany: (args: any, options?: any) => { - return useInternalSuspenseQuery(schema, modelName, 'findMany', args, options); + return useInternalSuspenseQuery(schema, modelName, 'findMany', args, { ...rootOptions, ...options }); }, useInfiniteFindMany: (args: any, options?: any) => { - return useInternalInfiniteQuery(schema, modelName, 'findMany', args, options); + return useInternalInfiniteQuery(schema, modelName, 'findMany', args, { ...rootOptions, ...options }); }, useSuspenseInfiniteFindMany: (args: any, options?: any) => { - return useInternalSuspenseInfiniteQuery(schema, modelName, 'findMany', args, options); + return useInternalSuspenseInfiniteQuery(schema, modelName, 'findMany', args, { + ...rootOptions, + ...options, + }); }, useCreate: (options?: any) => { - return useInternalMutation(schema, modelName, 'POST', 'create', options); + return useInternalMutation(schema, modelName, 'POST', 'create', { ...rootOptions, ...options }); }, useCreateMany: (options?: any) => { - return useInternalMutation(schema, modelName, 'POST', 'createMany', options); + return useInternalMutation(schema, modelName, 'POST', 'createMany', { ...rootOptions, ...options }); }, useCreateManyAndReturn: (options?: any) => { - return useInternalMutation(schema, modelName, 'POST', 'createManyAndReturn', options); + return useInternalMutation(schema, modelName, 'POST', 'createManyAndReturn', { + ...rootOptions, + ...options, + }); }, useUpdate: (options?: any) => { - return useInternalMutation(schema, modelName, 'PUT', 'update', options); + return useInternalMutation(schema, modelName, 'PUT', 'update', { ...rootOptions, ...options }); }, useUpdateMany: (options?: any) => { - return useInternalMutation(schema, modelName, 'PUT', 'updateMany', options); + return useInternalMutation(schema, modelName, 'PUT', 'updateMany', { ...rootOptions, ...options }); }, useUpdateManyAndReturn: (options?: any) => { - return useInternalMutation(schema, modelName, 'PUT', 'updateManyAndReturn', options); + return useInternalMutation(schema, modelName, 'PUT', 'updateManyAndReturn', { ...rootOptions, ...options }); }, useUpsert: (options?: any) => { - return useInternalMutation(schema, modelName, 'POST', 'upsert', options); + return useInternalMutation(schema, modelName, 'POST', 'upsert', { ...rootOptions, ...options }); }, useDelete: (options?: any) => { - return useInternalMutation(schema, modelName, 'DELETE', 'delete', options); + return useInternalMutation(schema, modelName, 'DELETE', 'delete', { ...rootOptions, ...options }); }, useDeleteMany: (options?: any) => { - return useInternalMutation(schema, modelName, 'DELETE', 'deleteMany', options); + return useInternalMutation(schema, modelName, 'DELETE', 'deleteMany', { ...rootOptions, ...options }); }, useCount: (args: any, options?: any) => { - return useInternalQuery(schema, modelName, 'count', args, options); + return useInternalQuery(schema, modelName, 'count', args, { ...rootOptions, ...options }); }, useSuspenseCount: (args: any, options?: any) => { - return useInternalSuspenseQuery(schema, modelName, 'count', args, options); + return useInternalSuspenseQuery(schema, modelName, 'count', args, { ...rootOptions, ...options }); }, useAggregate: (args: any, options?: any) => { - return useInternalQuery(schema, modelName, 'aggregate', args, options); + return useInternalQuery(schema, modelName, 'aggregate', args, { ...rootOptions, ...options }); }, useSuspenseAggregate: (args: any, options?: any) => { - return useInternalSuspenseQuery(schema, modelName, 'aggregate', args, options); + return useInternalSuspenseQuery(schema, modelName, 'aggregate', args, { ...rootOptions, ...options }); }, useGroupBy: (args: any, options?: any) => { - return useInternalQuery(schema, modelName, 'groupBy', args, options); + return useInternalQuery(schema, modelName, 'groupBy', args, { ...rootOptions, ...options }); }, useSuspenseGroupBy: (args: any, options?: any) => { - return useInternalSuspenseQuery(schema, modelName, 'groupBy', args, options); + return useInternalSuspenseQuery(schema, modelName, 'groupBy', args, { ...rootOptions, ...options }); }, } as ModelQueryHooks; } @@ -395,7 +399,7 @@ export function useInternalQuery( args?: unknown, options?: Omit, 'queryKey'> & ExtraQueryOptions, ) { - const { endpoint, fetch } = useHooksContext(); + const { endpoint, fetch } = useFetchOptions(options); const reqUrl = makeUrl(endpoint, model, operation, args); const queryKey = getQueryKey(model, operation, args, { infinite: false, @@ -418,7 +422,7 @@ export function useInternalSuspenseQuery( args?: unknown, options?: Omit, 'queryKey'> & ExtraQueryOptions, ) { - const { endpoint, fetch } = useHooksContext(); + const { endpoint, fetch } = useFetchOptions(options); const reqUrl = makeUrl(endpoint, model, operation, args); const queryKey = getQueryKey(model, operation, args, { infinite: false, @@ -440,14 +444,15 @@ export function useInternalInfiniteQuery( operation: string, args: unknown, options: - | Omit< + | (Omit< UseInfiniteQueryOptions>, 'queryKey' | 'initialPageParam' - > + > & + QueryContext) | undefined, ) { options = options ?? { getNextPageParam: () => undefined }; - const { endpoint, fetch } = useHooksContext(); + const { endpoint, fetch } = useFetchOptions(options); const queryKey = getQueryKey(model, operation, args, { infinite: true, optimisticUpdate: false }); return { queryKey, @@ -468,11 +473,11 @@ export function useInternalSuspenseInfiniteQuery( operation: string, args: unknown, options: Omit< - UseSuspenseInfiniteQueryOptions>, + UseSuspenseInfiniteQueryOptions> & QueryContext, 'queryKey' | 'initialPageParam' >, ) { - const { endpoint, fetch } = useHooksContext(); + const { endpoint, fetch } = useFetchOptions(options); const queryKey = getQueryKey(model, operation, args, { infinite: true, optimisticUpdate: false }); return { queryKey, @@ -505,7 +510,7 @@ export function useInternalMutation( operation: string, options?: Omit, 'mutationFn'> & ExtraMutationOptions, ) { - const { endpoint, fetch, logging } = useHooksContext(); + const { endpoint, fetch, logging } = useFetchOptions(options); const queryClient = useQueryClient(); const mutationFn = (data: any) => { const reqUrl = @@ -526,37 +531,73 @@ export function useInternalMutation( const invalidateQueries = options?.invalidateQueries !== false; const optimisticUpdate = !!options?.optimisticUpdate; - if (operation) { + if (!optimisticUpdate) { + // if optimistic update is not enabled, invalidate related queries on success if (invalidateQueries) { - setupInvalidation( + const invalidator = createInvalidator( model, operation, schema, - finalOptions, - (predicate) => queryClient.invalidateQueries({ predicate }), + (predicate) => invalidateQueriesMatchingPredicate(queryClient, predicate), logging, ); + const origOnSuccess = finalOptions.onSuccess; + finalOptions.onSuccess = async (...args) => { + // execute invalidator prior to user-provided onSuccess + await invalidator(...args); + + // call user-provided onSuccess + await origOnSuccess?.(...args); + }; } + } else { + // schedule optimistic update on mutate + const optimisticUpdater = createOptimisticUpdater( + model, + operation, + schema, + { optimisticDataProvider: finalOptions.optimisticDataProvider }, + () => getAllQueries(queryClient), + logging, + ); + const origOnMutate = finalOptions.onMutate; + finalOptions.onMutate = async (...args) => { + // execute optimistic update + await optimisticUpdater(...args); + + // call user-provided onMutate + return origOnMutate?.(...args); + }; - if (optimisticUpdate) { - setupOptimisticUpdate( + if (invalidateQueries) { + // invalidate related queries on settled (success or error) + const invalidator = createInvalidator( model, operation, schema, - finalOptions, - queryClient.getQueryCache().getAll(), - (queryKey, data) => { - // update query cache - queryClient.setQueryData(queryKey, data); - // cancel on-flight queries to avoid redundant cache updates, - // the settlement of the current mutation will trigger a new revalidation - queryClient.cancelQueries({ queryKey }, { revert: false, silent: true }); - }, - invalidateQueries ? (predicate) => queryClient.invalidateQueries({ predicate }) : undefined, + (predicate) => invalidateQueriesMatchingPredicate(queryClient, predicate), logging, ); + const origOnSettled = finalOptions.onSettled; + finalOptions.onSettled = async (...args) => { + // execute invalidator prior to user-provided onSettled + await invalidator(...args); + + // call user-provided onSettled + return origOnSettled?.(...args); + }; } } return useMutation(finalOptions); } + +function useFetchOptions(options: QueryContext | undefined) { + const { endpoint, fetch, logging } = useHooksContext(); + // options take precedence over context + return { + endpoint: options?.endpoint ?? endpoint, + fetch: options?.fetch ?? fetch, + logging: options?.logging ?? logging, + }; +} diff --git a/packages/clients/tanstack-query/src/svelte.ts b/packages/clients/tanstack-query/src/svelte/index.svelte.ts similarity index 50% rename from packages/clients/tanstack-query/src/svelte.ts rename to packages/clients/tanstack-query/src/svelte/index.svelte.ts index 36a2ab1c..a94941c4 100644 --- a/packages/clients/tanstack-query/src/svelte.ts +++ b/packages/clients/tanstack-query/src/svelte/index.svelte.ts @@ -3,6 +3,7 @@ import { createMutation, createQuery, useQueryClient, + type Accessor, type CreateInfiniteQueryOptions, type CreateInfiniteQueryResult, type CreateMutationOptions, @@ -13,8 +14,14 @@ import { type InfiniteData, type QueryFunction, type QueryKey, - type StoreOrVal, } from '@tanstack/svelte-query'; +import { + createInvalidator, + createOptimisticUpdater, + DEFAULT_QUERY_ENDPOINT, + type InvalidationPredicate, +} from '@zenstackhq/client-helpers'; +import { fetcher, makeUrl, marshal } from '@zenstackhq/client-helpers/fetch'; import { lowerCaseFirst } from '@zenstackhq/common-helpers'; import type { AggregateArgs, @@ -44,26 +51,16 @@ import type { } from '@zenstackhq/orm'; import type { GetModels, SchemaDef } from '@zenstackhq/schema'; import { getContext, setContext } from 'svelte'; -import { derived, get, type Readable } from 'svelte/store'; -import { - fetcher, - getQueryKey, - makeUrl, - marshal, - setupInvalidation, - setupOptimisticUpdate, - type APIContext, - type ExtraMutationOptions, - type ExtraQueryOptions, -} from './utils/common'; -import type { TrimDelegateModelOperations, WithOptimistic } from './utils/types'; - -export type { FetchFn } from './utils/common'; - -/** - * The default query endpoint. - */ -export const DEFAULT_QUERY_ENDPOINT = '/api/model'; +import { getAllQueries, invalidateQueriesMatchingPredicate } from '../common/client'; +import { getQueryKey } from '../common/query-key'; +import type { + ExtraMutationOptions, + ExtraQueryOptions, + QueryContext, + TrimDelegateModelOperations, + WithOptimistic, +} from '../common/types'; +export type { FetchFn } from '@zenstackhq/client-helpers/fetch'; /** * Key for setting and getting the global query context. @@ -75,38 +72,35 @@ export const SvelteQueryContextKey = 'zenstack-svelte-query-context'; * * @deprecated use {@link setQuerySettingsContext} instead. */ -export function setHooksContext(context: APIContext) { +export function setHooksContext(context: QueryContext) { setContext(SvelteQueryContextKey, context); } /** * Set context for query settings. */ -export function setQuerySettingsContext(context: APIContext) { +export function setQuerySettingsContext(context: QueryContext) { setContext(SvelteQueryContextKey, context); } -function getQuerySettings() { - const { endpoint, ...rest } = getContext(SvelteQueryContextKey) ?? {}; +function useQuerySettings() { + const { endpoint, ...rest } = getContext(SvelteQueryContextKey) ?? {}; return { endpoint: endpoint ?? DEFAULT_QUERY_ENDPOINT, ...rest }; } export type ModelQueryOptions = Omit, 'queryKey'> & ExtraQueryOptions; -export type ModelQueryResult = Readable< - UnwrapStore, DefaultError>> & { queryKey: QueryKey } ->; +export type ModelQueryResult = CreateQueryResult, DefaultError> & { queryKey: QueryKey }; export type ModelInfiniteQueryOptions = Omit< CreateInfiniteQueryOptions>, 'queryKey' | 'initialPageParam' ->; +> & + QueryContext; -export type ModelInfiniteQueryResult = Readable< - UnwrapStore> & { - queryKey: QueryKey; - } ->; +export type ModelInfiniteQueryResult = CreateInfiniteQueryResult & { + queryKey: QueryKey; +}; export type ModelMutationOptions = Omit, 'mutationFn'> & ExtraMutationOptions; @@ -119,17 +113,12 @@ export type ModelMutationModelResult< TArgs, Array extends boolean = false, Options extends QueryOptions = QueryOptions, -> = Readable< - Omit< - UnwrapStore, TArgs>>, - 'mutateAsync' - > & { - mutateAsync( - args: T, - options?: ModelMutationOptions, T>, - ): Promise>; - } ->; +> = Omit, TArgs>, 'mutateAsync'> & { + mutateAsync( + args: T, + options?: ModelMutationOptions, T>, + ): Promise>; +}; export type ClientHooks = QueryOptions> = { [Model in GetModels as `${Uncapitalize}`]: ModelQueryHooks; @@ -146,72 +135,72 @@ export type ModelQueryHooks< Model, { useFindUnique>( - args: SelectSubset>, - options?: ModelQueryOptions | null>, + args: Accessor>>, + options?: Accessor | null>>, ): ModelQueryResult | null>; useFindFirst>( - args?: SelectSubset>, - options?: ModelQueryOptions | null>, + args?: Accessor>>, + options?: Accessor | null>>, ): ModelQueryResult | null>; useFindMany>( - args?: SelectSubset>, - options?: ModelQueryOptions[]>, + args?: Accessor>>, + options?: Accessor[]>>, ): ModelQueryResult[]>; useInfiniteFindMany>( - args?: SelectSubset>, - options?: ModelInfiniteQueryOptions[]>, + args?: Accessor>>, + options?: Accessor[]>>, ): ModelInfiniteQueryResult[]>>; useCreate>( - options?: ModelMutationOptions, T>, + options?: Accessor, T>>, ): ModelMutationModelResult; useCreateMany>( - options?: ModelMutationOptions, + options?: Accessor>, ): ModelMutationResult; useCreateManyAndReturn>( - options?: ModelMutationOptions[], T>, + options?: Accessor[], T>>, ): ModelMutationModelResult; useUpdate>( - options?: ModelMutationOptions, T>, + options?: Accessor, T>>, ): ModelMutationModelResult; useUpdateMany>( - options?: ModelMutationOptions, + options?: Accessor>, ): ModelMutationResult; useUpdateManyAndReturn>( - options?: ModelMutationOptions[], T>, + options?: Accessor[], T>>, ): ModelMutationModelResult; useUpsert>( - options?: ModelMutationOptions, T>, + options?: Accessor, T>>, ): ModelMutationModelResult; useDelete>( - options?: ModelMutationOptions, T>, + options?: Accessor, T>>, ): ModelMutationModelResult; useDeleteMany>( - options?: ModelMutationOptions, + options?: Accessor>, ): ModelMutationResult; useCount>( - args?: Subset>, - options?: ModelQueryOptions>, + args?: Accessor>>, + options?: Accessor>>, ): ModelQueryResult>; useAggregate>( - args: Subset>, - options?: ModelQueryOptions>, + args: Accessor>>, + options?: Accessor>>, ): ModelQueryResult>; useGroupBy>( - args: Subset>, - options?: ModelQueryOptions>, + args: Accessor>>, + options?: Accessor>>, ): ModelQueryResult>; } >; @@ -221,12 +210,14 @@ export type ModelQueryHooks< */ export function useClientQueries = QueryOptions>( schema: Schema, + options?: Accessor, ): ClientHooks { return Object.keys(schema.models).reduce( (acc, model) => { (acc as any)[lowerCaseFirst(model)] = useModelQueries, Options>( schema, model as GetModels, + options, ); return acc; }, @@ -241,7 +232,7 @@ export function useModelQueries< Schema extends SchemaDef, Model extends GetModels, Options extends QueryOptions, ->(schema: Schema, model: Model): ModelQueryHooks { +>(schema: Schema, model: Model, rootOptions?: Accessor): ModelQueryHooks { const modelDef = Object.values(schema.models).find((m) => m.name.toLowerCase() === model.toLowerCase()); if (!modelDef) { throw new Error(`Model "${model}" not found in schema`); @@ -249,17 +240,25 @@ export function useModelQueries< const modelName = modelDef.name; + const merge = (rootOpt: unknown, opt: unknown): Accessor => { + return () => { + const rootOptVal = typeof rootOpt === 'function' ? rootOpt() : rootOpt; + const optVal = typeof opt === 'function' ? opt() : opt; + return { ...rootOptVal, ...optVal }; + }; + }; + return { useFindUnique: (args: any, options?: any) => { - return useInternalQuery(schema, modelName, 'findUnique', args, options); + return useInternalQuery(schema, modelName, 'findUnique', args, merge(rootOptions, options)); }, useFindFirst: (args: any, options?: any) => { - return useInternalQuery(schema, modelName, 'findFirst', args, options); + return useInternalQuery(schema, modelName, 'findFirst', args, merge(rootOptions, options)); }, useFindMany: (args: any, options?: any) => { - return useInternalQuery(schema, modelName, 'findMany', args, options); + return useInternalQuery(schema, modelName, 'findMany', args, merge(rootOptions, options)); }, useInfiniteFindMany: (args: any, options?: any) => { @@ -313,100 +312,87 @@ export function useModelQueries< useGroupBy: (args: any, options?: any) => { return useInternalQuery(schema, modelName, 'groupBy', args, options); }, - } as ModelQueryHooks; + } as unknown as ModelQueryHooks; } export function useInternalQuery( _schema: SchemaDef, model: string, operation: string, - args?: StoreOrVal, - options?: StoreOrVal, 'queryKey'> & ExtraQueryOptions>, + args?: Accessor, + options?: Accessor, 'queryKey'> & ExtraQueryOptions>, ) { - const { endpoint, fetch } = getQuerySettings(); - const argsValue = unwrapStore(args); - const reqUrl = makeUrl(endpoint, model, operation, argsValue); - const optionsValue = unwrapStore(options); - const queryKey = getQueryKey(model, operation, argsValue, { - infinite: false, - optimisticUpdate: optionsValue?.optimisticUpdate !== false, - }); - const queryFn: QueryFunction = ({ signal }) => - fetcher(reqUrl, { signal }, fetch); - - let mergedOpt: any; - if (isStore(options)) { - // options is store - mergedOpt = derived([options], ([$opt]) => { - return { - queryKey, - queryFn, - ...($opt as object), - }; - }); - } else { - // options is value - mergedOpt = { + const { endpoint, fetch } = useFetchOptions(options); + + const queryKey = $derived( + getQueryKey(model, operation, args?.(), { + infinite: false, + optimisticUpdate: options?.().optimisticUpdate !== false, + }), + ); + + const finalOptions = () => { + const reqUrl = makeUrl(endpoint, model, operation, args?.()); + const queryFn: QueryFunction = ({ signal }) => + fetcher(reqUrl, { signal }, fetch); + return { queryKey, queryFn, - ...options, + ...options?.(), }; - } + }; - const result = createQuery(mergedOpt); - return derived(result, (r) => ({ - queryKey, - ...r, - })); + const query = createQuery(finalOptions); + // svelte-ignore state_referenced_locally + return createQueryResult(query, queryKey); } export function useInternalInfiniteQuery( _schema: SchemaDef, model: string, operation: string, - args: StoreOrVal, - options: - | StoreOrVal< - Omit< - CreateInfiniteQueryOptions>, - 'queryKey' | 'initialPageParam' - > - > - | undefined, + args: Accessor, + options?: Accessor< + Omit< + CreateInfiniteQueryOptions>, + 'queryKey' | 'initialPageParam' + > & + QueryContext + >, ) { - options = options ?? { getNextPageParam: () => undefined }; - const { endpoint, fetch } = getQuerySettings(); - const argsValue = unwrapStore(args); - const queryKey = getQueryKey(model, operation, argsValue, { infinite: true, optimisticUpdate: false }); - const queryFn: QueryFunction = ({ pageParam, signal }) => - fetcher(makeUrl(endpoint, model, operation, pageParam ?? argsValue), { signal }, fetch); - - let mergedOpt: StoreOrVal>>; - if (isStore(options)) { - // options is store - mergedOpt = derived([options], ([$opt]) => { - return { - queryKey, - queryFn, - initialPageParam: argsValue, - ...$opt, - }; - }); - } else { - // options is value - mergedOpt = { + const { endpoint, fetch } = useFetchOptions(options); + + const queryKey = $derived(getQueryKey(model, operation, args(), { infinite: true, optimisticUpdate: false })); + + const finalOptions = () => { + const queryFn: QueryFunction = ({ pageParam, signal }) => + fetcher(makeUrl(endpoint, model, operation, pageParam ?? args()), { signal }, fetch); + const optionsValue = options?.() ?? { getNextPageParam: () => undefined }; + return { queryKey, queryFn, - initialPageParam: argsValue, - ...options, + initialPageParam: args(), + ...optionsValue, }; - } + }; - const result = createInfiniteQuery>(mergedOpt); - return derived(result, (r) => ({ - queryKey, - ...r, - })); + const query = createInfiniteQuery>(finalOptions); + // svelte-ignore state_referenced_locally + return createQueryResult(query, queryKey); +} + +function createQueryResult(query: T, queryKey: QueryKey): T & { queryKey: QueryKey } { + // CHECKME: is there a better way to do this? + // create a proxy object that properly forwards all properties while adding queryKey, + // this preserves svelte-query reactivity by using getters + return new Proxy(query as any, { + get(target, prop) { + if (prop === 'queryKey') { + return queryKey; + } + return target[prop]; + }, + }); } /** @@ -425,11 +411,10 @@ export function useInternalMutation( model: string, method: 'POST' | 'PUT' | 'DELETE', operation: string, - options?: StoreOrVal, 'mutationFn'> & ExtraMutationOptions>, + options?: Accessor, 'mutationFn'> & ExtraMutationOptions>, ) { - const { endpoint, fetch, logging } = getQuerySettings(); + const { endpoint, fetch, logging } = useQuerySettings(); const queryClient = useQueryClient(); - const optionsValue = unwrapStore(options); const mutationFn = (data: any) => { const reqUrl = method === 'DELETE' ? makeUrl(endpoint, model, operation, data) : makeUrl(endpoint, model, operation); @@ -445,58 +430,94 @@ export function useInternalMutation( return fetcher(reqUrl, fetchInit, fetch) as Promise; }; - let mergedOpt: StoreOrVal>; - - if (isStore(options)) { - mergedOpt = derived([options], ([$opt]) => ({ - ...$opt, - mutationFn, - })); - } else { - mergedOpt = { - ...options, + const finalOptions = () => { + const optionsValue = options?.(); + const invalidateQueries = optionsValue?.invalidateQueries !== false; + const optimisticUpdate = !!optionsValue?.optimisticUpdate; + const result = { + ...optionsValue, mutationFn, }; - } - - const invalidateQueries = optionsValue?.invalidateQueries !== false; - const optimisticUpdate = !!optionsValue?.optimisticUpdate; - if (operation) { - if (invalidateQueries) { - setupInvalidation( + if (!optimisticUpdate) { + // if optimistic update is not enabled, invalidate related queries on success + if (invalidateQueries) { + const invalidator = createInvalidator( + model, + operation, + schema, + (predicate: InvalidationPredicate) => + // @ts-ignore + invalidateQueriesMatchingPredicate(queryClient, predicate), + logging, + ); + + // execute invalidator prior to user-provided onSuccess + const origOnSuccess = optionsValue?.onSuccess; + const wrappedOnSuccess: typeof origOnSuccess = async (...args) => { + await invalidator(...args); + await origOnSuccess?.(...args); + }; + result.onSuccess = wrappedOnSuccess; + } + } else { + const optimisticUpdater = createOptimisticUpdater( model, operation, schema, - unwrapStore(mergedOpt), - (predicate) => queryClient.invalidateQueries({ predicate }), + { optimisticDataProvider: optionsValue?.optimisticDataProvider }, + // @ts-ignore + () => getAllQueries(queryClient), logging, ); - } - if (optimisticUpdate) { - setupOptimisticUpdate( - model, - operation, - schema, - unwrapStore(mergedOpt), - queryClient.getQueryCache().getAll(), - (queryKey, data) => queryClient.setQueryData(queryKey, data), - invalidateQueries ? (predicate) => queryClient.invalidateQueries({ predicate }) : undefined, - logging, - ); - } - } + const origOnMutate = optionsValue.onMutate; + const wrappedOnMutate: typeof origOnMutate = async (...args) => { + // execute optimistic updater prior to user-provided onMutate + await optimisticUpdater(...args); - return createMutation(mergedOpt); -} + // call user-provided onMutate + return origOnMutate?.(...args); + }; -function isStore(opt: unknown): opt is Readable { - return typeof (opt as any)?.subscribe === 'function'; -} + result.onMutate = wrappedOnMutate; + + if (invalidateQueries) { + const invalidator = createInvalidator( + model, + operation, + schema, + (predicate: InvalidationPredicate) => + // @ts-ignore + invalidateQueriesMatchingPredicate(queryClient, predicate), + logging, + ); + const origOnSettled = optionsValue.onSettled; + const wrappedOnSettled: typeof origOnSettled = async (...args) => { + // execute invalidator prior to user-provided onSettled + await invalidator(...args); + + // call user-provided onSettled + await origOnSettled?.(...args); + }; + + // replace onSettled in mergedOpt + result.onSettled = wrappedOnSettled; + } + } -function unwrapStore(storeOrValue: StoreOrVal): T { - return isStore(storeOrValue) ? get(storeOrValue) : storeOrValue; + return result; + }; + return createMutation(finalOptions); } -type UnwrapStore = T extends Readable ? U : T; +function useFetchOptions(options: Accessor | undefined) { + const { endpoint, fetch, logging } = useQuerySettings(); + const optionsValue = options?.(); + // options take precedence over context + return { + endpoint: optionsValue?.endpoint ?? endpoint, + fetch: optionsValue?.fetch ?? fetch, + logging: optionsValue?.logging ?? logging, + }; +} diff --git a/packages/clients/tanstack-query/src/utils/common.ts b/packages/clients/tanstack-query/src/utils/common.ts deleted file mode 100644 index 28710d25..00000000 --- a/packages/clients/tanstack-query/src/utils/common.ts +++ /dev/null @@ -1,448 +0,0 @@ -import { lowerCaseFirst } from '@zenstackhq/common-helpers'; -import type { SchemaDef } from '@zenstackhq/schema'; -import { applyMutation } from './mutator'; -import { getMutatedModels, getReadModels } from './query-analysis'; -import { deserialize, serialize } from './serialization'; -import type { ORMWriteActionType } from './types'; - -/** - * The default query endpoint. - */ -export const DEFAULT_QUERY_ENDPOINT = '/api/model'; - -/** - * Prefix for react-query keys. - */ -export const QUERY_KEY_PREFIX = 'zenstack'; - -/** - * Function signature for `fetch`. - */ -export type FetchFn = (url: string, options?: RequestInit) => Promise; - -/** - * Type for query and mutation errors. - */ -export type QueryError = Error & { - /** - * Additional error information. - */ - info?: unknown; - - /** - * HTTP status code. - */ - status?: number; -}; - -/** - * Result of optimistic data provider. - */ -export type OptimisticDataProviderResult = { - /** - * Kind of the result. - * - Update: use the `data` field to update the query cache. - * - Skip: skip the optimistic update for this query. - * - ProceedDefault: proceed with the default optimistic update. - */ - kind: 'Update' | 'Skip' | 'ProceedDefault'; - - /** - * Data to update the query cache. Only applicable if `kind` is 'Update'. - * - * If the data is an object with fields updated, it should have a `$optimistic` - * field set to `true`. If it's an array and an element object is created or updated, - * the element should have a `$optimistic` field set to `true`. - */ - data?: any; -}; - -/** - * Optimistic data provider. - * - * @param args Arguments. - * @param args.queryModel The model of the query. - * @param args.queryOperation The operation of the query, `findMany`, `count`, etc. - * @param args.queryArgs The arguments of the query. - * @param args.currentData The current cache data for the query. - * @param args.mutationArgs The arguments of the mutation. - */ -export type OptimisticDataProvider = (args: { - queryModel: string; - queryOperation: string; - queryArgs: any; - currentData: any; - mutationArgs: any; -}) => OptimisticDataProviderResult | Promise; - -/** - * Extra mutation options. - */ -export type ExtraMutationOptions = { - /** - * Whether to automatically invalidate queries potentially affected by the mutation. Defaults to `true`. - */ - invalidateQueries?: boolean; - - /** - * Whether to optimistically update queries potentially affected by the mutation. Defaults to `false`. - */ - optimisticUpdate?: boolean; - - /** - * A callback for computing optimistic update data for each query cache entry. - */ - optimisticDataProvider?: OptimisticDataProvider; -}; - -/** - * Extra query options. - */ -export type ExtraQueryOptions = { - /** - * Whether to opt-in to optimistic updates for this query. Defaults to `true`. - */ - optimisticUpdate?: boolean; -}; - -/** - * Context type for configuring the hooks. - */ -export type APIContext = { - /** - * The endpoint to use for the queries. - */ - endpoint?: string; - - /** - * A custom fetch function for sending the HTTP requests. - */ - fetch?: FetchFn; - - /** - * If logging is enabled. - */ - logging?: boolean; -}; - -export async function fetcher(url: string, options?: RequestInit, customFetch?: FetchFn): Promise { - const _fetch = customFetch ?? fetch; - const res = await _fetch(url, options); - if (!res.ok) { - const errData = unmarshal(await res.text()); - if (errData.error?.rejectedByPolicy && errData.error?.rejectReason === 'cannot-read-back') { - // policy doesn't allow mutation result to be read back, just return undefined - return undefined as any; - } - const error: QueryError = new Error('An error occurred while fetching the data.'); - error.info = errData.error; - error.status = res.status; - throw error; - } - - const textResult = await res.text(); - try { - return unmarshal(textResult).data as R; - } catch (err) { - console.error(`Unable to deserialize data:`, textResult); - throw err; - } -} - -type QueryKey = [ - string /* prefix */, - string /* model */, - string /* operation */, - unknown /* args */, - { - infinite: boolean; - optimisticUpdate: boolean; - } /* flags */, -]; - -/** - * Computes query key for the given model, operation and query args. - * @param model Model name. - * @param operation Query operation (e.g, `findMany`) or request URL. If it's a URL, the last path segment will be used as the operation name. - * @param args Query arguments. - * @param options Query options, including `infinite` indicating if it's an infinite query (defaults to false), and `optimisticUpdate` indicating if optimistic updates are enabled (defaults to true). - * @returns Query key - */ -export function getQueryKey( - model: string, - operation: string, - args: unknown, - options: { infinite: boolean; optimisticUpdate: boolean } = { infinite: false, optimisticUpdate: true }, -): QueryKey { - const infinite = options.infinite; - // infinite query doesn't support optimistic updates - const optimisticUpdate = options.infinite ? false : options.optimisticUpdate; - return [QUERY_KEY_PREFIX, model, operation!, args, { infinite, optimisticUpdate }]; -} - -export function marshal(value: unknown) { - const { data, meta } = serialize(value); - if (meta) { - return JSON.stringify({ ...(data as any), meta: { serialization: meta } }); - } else { - return JSON.stringify(data); - } -} - -export function unmarshal(value: string) { - const parsed = JSON.parse(value); - if (typeof parsed === 'object' && parsed?.data && parsed?.meta?.serialization) { - const deserializedData = deserialize(parsed.data, parsed.meta.serialization); - return { ...parsed, data: deserializedData }; - } else { - return parsed; - } -} - -export function makeUrl(endpoint: string, model: string, operation: string, args?: unknown) { - const baseUrl = `${endpoint}/${lowerCaseFirst(model)}/${operation}`; - if (!args) { - return baseUrl; - } - - const { data, meta } = serialize(args); - let result = `${baseUrl}?q=${encodeURIComponent(JSON.stringify(data))}`; - if (meta) { - result += `&meta=${encodeURIComponent(JSON.stringify({ serialization: meta }))}`; - } - return result; -} - -type InvalidationPredicate = ({ queryKey }: { queryKey: readonly unknown[] }) => boolean; -type InvalidateFunc = (predicate: InvalidationPredicate) => Promise; -type MutationOptions = { - onMutate?: (...args: any[]) => any; - onSuccess?: (...args: any[]) => any; - onSettled?: (...args: any[]) => any; -}; - -// sets up invalidation hook for a mutation -export function setupInvalidation( - model: string, - operation: string, - schema: SchemaDef, - options: MutationOptions, - invalidate: InvalidateFunc, - logging = false, -) { - const origOnSuccess = options?.onSuccess; - options.onSuccess = async (...args: unknown[]) => { - const [_, variables] = args; - const predicate = await getInvalidationPredicate( - model, - operation as ORMWriteActionType, - variables, - schema, - logging, - ); - await invalidate(predicate); - return origOnSuccess?.(...args); - }; -} - -// gets a predicate for evaluating whether a query should be invalidated -async function getInvalidationPredicate( - model: string, - operation: ORMWriteActionType, - mutationArgs: any, - schema: SchemaDef, - logging = false, -) { - const mutatedModels = await getMutatedModels(model, operation, mutationArgs, schema); - - return ({ queryKey }: { queryKey: readonly unknown[] }) => { - const [_, queryModel, , args] = queryKey as QueryKey; - - if (mutatedModels.includes(queryModel)) { - // direct match - if (logging) { - console.log(`Invalidating query ${JSON.stringify(queryKey)} due to mutation "${model}.${operation}"`); - } - return true; - } - - if (args) { - // traverse query args to find nested reads that match the model under mutation - if (findNestedRead(queryModel, mutatedModels, schema, args)) { - if (logging) { - console.log( - `Invalidating query ${JSON.stringify(queryKey)} due to mutation "${model}.${operation}"`, - ); - } - return true; - } - } - - return false; - }; -} - -// find nested reads that match the given models -function findNestedRead(visitingModel: string, targetModels: string[], schema: SchemaDef, args: any) { - const modelsRead = getReadModels(visitingModel, schema, args); - return targetModels.some((m) => modelsRead.includes(m)); -} - -type QueryCache = { - queryKey: readonly unknown[]; - state: { - data: unknown; - error: unknown; - }; -}[]; - -type SetCacheFunc = (queryKey: readonly unknown[], data: unknown) => void; - -/** - * Sets up optimistic update and invalidation (after settled) for a mutation. - */ -export function setupOptimisticUpdate( - model: string, - operation: string, - schema: SchemaDef, - options: MutationOptions & ExtraMutationOptions, - queryCache: QueryCache, - setCache: SetCacheFunc, - invalidate?: InvalidateFunc, - logging = false, -) { - const origOnMutate = options?.onMutate; - const origOnSettled = options?.onSettled; - - // optimistic update on mutate - options.onMutate = async (...args: unknown[]) => { - const [variables] = args; - await optimisticUpdate( - model, - operation as ORMWriteActionType, - variables, - options, - schema, - queryCache, - setCache, - logging, - ); - return origOnMutate?.(...args); - }; - - // invalidate on settled - options.onSettled = async (...args: unknown[]) => { - if (invalidate) { - const [, , variables] = args; - const predicate = await getInvalidationPredicate( - model, - operation as ORMWriteActionType, - variables, - schema, - logging, - ); - await invalidate(predicate); - } - return origOnSettled?.(...args); - }; -} - -// optimistically updates query cache -async function optimisticUpdate( - mutationModel: string, - mutationOp: string, - mutationArgs: any, - options: MutationOptions & ExtraMutationOptions, - schema: SchemaDef, - queryCache: QueryCache, - setCache: SetCacheFunc, - logging = false, -) { - for (const cacheItem of queryCache) { - const { - queryKey, - state: { data, error }, - } = cacheItem; - - if (!isZenStackQueryKey(queryKey)) { - // skip non-zenstack queries - continue; - } - - if (error) { - if (logging) { - console.warn(`Skipping optimistic update for ${JSON.stringify(queryKey)} due to error:`, error); - } - continue; - } - - const [_, queryModel, queryOperation, queryArgs, queryOptions] = queryKey; - if (!queryOptions?.optimisticUpdate) { - if (logging) { - console.log(`Skipping optimistic update for ${JSON.stringify(queryKey)} due to opt-out`); - } - continue; - } - - if (options.optimisticDataProvider) { - const providerResult = await options.optimisticDataProvider({ - queryModel, - queryOperation, - queryArgs, - currentData: data, - mutationArgs, - }); - - if (providerResult?.kind === 'Skip') { - // skip - if (logging) { - console.log(`Skipping optimistic update for ${JSON.stringify(queryKey)} due to provider`); - } - continue; - } else if (providerResult?.kind === 'Update') { - // update cache - if (logging) { - console.log(`Optimistically updating query ${JSON.stringify(queryKey)} due to provider`); - } - setCache(queryKey, providerResult.data); - continue; - } - } - - // proceed with default optimistic update - const mutatedData = await applyMutation( - queryModel, - queryOperation, - data, - mutationModel, - mutationOp as ORMWriteActionType, - mutationArgs, - schema, - logging, - ); - - if (mutatedData !== undefined) { - // mutation applicable to this query, update cache - if (logging) { - console.log( - `Optimistically updating query ${JSON.stringify( - queryKey, - )} due to mutation "${mutationModel}.${mutationOp}"`, - ); - } - setCache(queryKey, mutatedData); - } - } -} - -function isZenStackQueryKey(queryKey: readonly unknown[]): queryKey is QueryKey { - if (queryKey.length < 5) { - return false; - } - - if (queryKey[0] !== QUERY_KEY_PREFIX) { - return false; - } - - return true; -} diff --git a/packages/clients/tanstack-query/src/utils/serialization.ts b/packages/clients/tanstack-query/src/utils/serialization.ts deleted file mode 100644 index 4df15555..00000000 --- a/packages/clients/tanstack-query/src/utils/serialization.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { Buffer } from 'buffer'; -import Decimal from 'decimal.js'; -import SuperJSON from 'superjson'; - -SuperJSON.registerCustom( - { - isApplicable: (v): v is Decimal => - v instanceof Decimal || - // interop with decimal.js - v?.toStringTag === '[object Decimal]', - serialize: (v) => v.toJSON(), - deserialize: (v) => new Decimal(v), - }, - 'Decimal' -); - -SuperJSON.registerCustom( - { - isApplicable: (v): v is Buffer => Buffer.isBuffer(v), - serialize: (v) => v.toString('base64'), - deserialize: (v) => Buffer.from(v, 'base64'), - }, - 'Bytes' -); - -/** - * Serialize the given value with superjson - */ -export function serialize(value: unknown): { data: unknown; meta: unknown } { - const { json, meta } = SuperJSON.serialize(value); - return { data: json, meta }; -} - -/** - * Deserialize the given value with superjson using the given metadata - */ -export function deserialize(value: unknown, meta: any): unknown { - return SuperJSON.deserialize({ json: value as any, meta }); -} diff --git a/packages/clients/tanstack-query/src/utils/types.ts b/packages/clients/tanstack-query/src/utils/types.ts deleted file mode 100644 index b6616588..00000000 --- a/packages/clients/tanstack-query/src/utils/types.ts +++ /dev/null @@ -1,43 +0,0 @@ -import type { OperationsIneligibleForDelegateModels } from '@zenstackhq/orm'; -import type { GetModels, IsDelegateModel, SchemaDef } from '@zenstackhq/schema'; - -export type MaybePromise = T | Promise | PromiseLike; - -export const ORMWriteActions = [ - 'create', - 'createMany', - 'createManyAndReturn', - 'connectOrCreate', - 'update', - 'updateMany', - 'updateManyAndReturn', - 'upsert', - 'connect', - 'disconnect', - 'set', - 'delete', - 'deleteMany', -] as const; - -export type ORMWriteActionType = (typeof ORMWriteActions)[number]; - -type HooksOperationsIneligibleForDelegateModels = OperationsIneligibleForDelegateModels extends any - ? `use${Capitalize}` - : never; - -export type TrimDelegateModelOperations< - Schema extends SchemaDef, - Model extends GetModels, - T extends Record, -> = IsDelegateModel extends true ? Omit : T; - -type WithOptimisticFlag = T extends object - ? T & { - /** - * Indicates if the item is in an optimistic update state - */ - $optimistic?: boolean; - } - : T; - -export type WithOptimistic = T extends Array ? Array> : WithOptimisticFlag; diff --git a/packages/clients/tanstack-query/src/vue.ts b/packages/clients/tanstack-query/src/vue.ts index bcac42d9..bd4dcf74 100644 --- a/packages/clients/tanstack-query/src/vue.ts +++ b/packages/clients/tanstack-query/src/vue.ts @@ -13,6 +13,13 @@ import { type UseQueryOptions, type UseQueryReturnType, } from '@tanstack/vue-query'; +import { + createInvalidator, + createOptimisticUpdater, + DEFAULT_QUERY_ENDPOINT, + type InvalidationPredicate, +} from '@zenstackhq/client-helpers'; +import { fetcher, makeUrl, marshal } from '@zenstackhq/client-helpers/fetch'; import { lowerCaseFirst } from '@zenstackhq/common-helpers'; import type { AggregateArgs, @@ -41,22 +48,17 @@ import type { UpsertArgs, } from '@zenstackhq/orm'; import type { GetModels, SchemaDef } from '@zenstackhq/schema'; -import { inject, provide, toValue, type MaybeRefOrGetter, type UnwrapRef } from 'vue'; -import { - DEFAULT_QUERY_ENDPOINT, - fetcher, - getQueryKey, - makeUrl, - marshal, - setupInvalidation, - setupOptimisticUpdate, - type APIContext, - type ExtraMutationOptions, - type ExtraQueryOptions, -} from './utils/common'; -import type { TrimDelegateModelOperations, WithOptimistic } from './utils/types'; - -export type { FetchFn } from './utils/common'; +import { computed, inject, provide, toValue, unref, type MaybeRefOrGetter, type Ref, type UnwrapRef } from 'vue'; +import { getAllQueries, invalidateQueriesMatchingPredicate } from './common/client'; +import { getQueryKey } from './common/query-key'; +import type { + ExtraMutationOptions, + ExtraQueryOptions, + QueryContext, + TrimDelegateModelOperations, + WithOptimistic, +} from './common/types'; +export type { FetchFn } from '@zenstackhq/client-helpers/fetch'; export const VueQueryContextKey = 'zenstack-vue-query-context'; /** @@ -64,19 +66,19 @@ export const VueQueryContextKey = 'zenstack-vue-query-context'; * * @deprecated Use {@link provideQuerySettingsContext} instead. */ -export function provideHooksContext(context: APIContext) { - provide(VueQueryContextKey, context); +export function provideHooksContext(context: QueryContext) { + provide(VueQueryContextKey, context); } /** * Provide context for query settings. */ -export function provideQuerySettingsContext(context: APIContext) { - provide(VueQueryContextKey, context); +export function provideQuerySettingsContext(context: QueryContext) { + provide(VueQueryContextKey, context); } -function getQuerySettings() { - const { endpoint, ...rest } = inject(VueQueryContextKey, { +function useQuerySettings() { + const { endpoint, ...rest } = inject(VueQueryContextKey, { endpoint: DEFAULT_QUERY_ENDPOINT, fetch: undefined, logging: false, @@ -88,13 +90,14 @@ export type ModelQueryOptions = MaybeRefOrGetter< Omit>, 'queryKey'> & ExtraQueryOptions >; -export type ModelQueryResult = UseQueryReturnType, DefaultError> & { queryKey: QueryKey }; +export type ModelQueryResult = UseQueryReturnType, DefaultError> & { queryKey: Ref }; export type ModelInfiniteQueryOptions = MaybeRefOrGetter< - Omit>>, 'queryKey' | 'initialPageParam'> + Omit>>, 'queryKey' | 'initialPageParam'> & + QueryContext >; -export type ModelInfiniteQueryResult = UseInfiniteQueryReturnType & { queryKey: QueryKey }; +export type ModelInfiniteQueryResult = UseInfiniteQueryReturnType & { queryKey: Ref }; export type ModelMutationOptions = MaybeRefOrGetter< Omit>, 'mutationFn'> & ExtraMutationOptions @@ -133,74 +136,74 @@ export type ModelQueryHooks< Model, { useFindUnique>( - args: SelectSubset>, - options?: ModelQueryOptions | null>, + args: MaybeRefOrGetter>>, + options?: MaybeRefOrGetter | null>>, ): ModelQueryResult | null>; useFindFirst>( - args?: SelectSubset>, - options?: ModelQueryOptions | null>, + args?: MaybeRefOrGetter>>, + options?: MaybeRefOrGetter | null>>, ): ModelQueryResult | null>; useFindMany>( - args?: SelectSubset>, - options?: ModelQueryOptions[]>, + args?: MaybeRefOrGetter>>, + options?: MaybeRefOrGetter[]>>, ): ModelQueryResult[]>; useInfiniteFindMany>( - args?: SelectSubset>, - options?: ModelInfiniteQueryOptions[]>, + args?: MaybeRefOrGetter>>, + options?: MaybeRefOrGetter[]>>, ): ModelInfiniteQueryResult[]>>; useCreate>( - options?: ModelMutationOptions, T>, + options?: MaybeRefOrGetter, T>>, ): ModelMutationModelResult; useCreateMany>( - options?: ModelMutationOptions, + options?: MaybeRefOrGetter>, ): ModelMutationResult; useCreateManyAndReturn>( - options?: ModelMutationOptions[], T>, + options?: MaybeRefOrGetter[], T>>, ): ModelMutationModelResult; useUpdate>( - options?: ModelMutationOptions, T>, + options?: MaybeRefOrGetter, T>>, ): ModelMutationModelResult; useUpdateMany>( - options?: ModelMutationOptions, + options?: MaybeRefOrGetter>, ): ModelMutationResult; useUpdateManyAndReturn>( - options?: ModelMutationOptions[], T>, + options?: MaybeRefOrGetter[], T>>, ): ModelMutationModelResult; useUpsert>( - options?: ModelMutationOptions, T>, + options?: MaybeRefOrGetter, T>>, ): ModelMutationModelResult; useDelete>( - options?: ModelMutationOptions, T>, + options?: MaybeRefOrGetter, T>>, ): ModelMutationModelResult; useDeleteMany>( - options?: ModelMutationOptions, + options?: MaybeRefOrGetter>, ): ModelMutationResult; useCount>( - args?: Subset>, - options?: ModelQueryOptions>, + args?: MaybeRefOrGetter>>, + options?: MaybeRefOrGetter>>, ): ModelQueryResult>; useAggregate>( - args: Subset>, - options?: ModelQueryOptions>, + args: MaybeRefOrGetter>>, + options?: MaybeRefOrGetter>>, ): ModelQueryResult>; useGroupBy>( - args: Subset>, - options?: ModelQueryOptions>, + args: MaybeRefOrGetter>>, + options?: MaybeRefOrGetter>>, ): ModelQueryResult>; } >; @@ -210,12 +213,14 @@ export type ModelQueryHooks< */ export function useClientQueries = QueryOptions>( schema: Schema, + options?: MaybeRefOrGetter, ): ClientHooks { return Object.keys(schema.models).reduce( (acc, model) => { (acc as any)[lowerCaseFirst(model)] = useModelQueries, Options>( schema, model as GetModels, + options, ); return acc; }, @@ -230,7 +235,7 @@ export function useModelQueries< Schema extends SchemaDef, Model extends GetModels, Options extends QueryOptions, ->(schema: Schema, model: Model): ModelQueryHooks { +>(schema: Schema, model: Model, rootOptions?: MaybeRefOrGetter): ModelQueryHooks { const modelDef = Object.values(schema.models).find((m) => m.name.toLowerCase() === model.toLowerCase()); if (!modelDef) { throw new Error(`Model "${model}" not found in schema`); @@ -238,69 +243,75 @@ export function useModelQueries< const modelName = modelDef.name; + const merge = (rootOpt: MaybeRefOrGetter | undefined, opt: MaybeRefOrGetter | undefined): any => { + return computed(() => { + return { ...(toValue(rootOpt) as object), ...(toValue(opt) as object) }; + }); + }; + return { useFindUnique: (args: any, options?: any) => { - return useInternalQuery(schema, modelName, 'findUnique', args, options); + return useInternalQuery(schema, modelName, 'findUnique', args, merge(rootOptions, options)); }, useFindFirst: (args: any, options?: any) => { - return useInternalQuery(schema, modelName, 'findFirst', args, options); + return useInternalQuery(schema, modelName, 'findFirst', args, merge(rootOptions, options)); }, useFindMany: (args: any, options?: any) => { - return useInternalQuery(schema, modelName, 'findMany', args, options); + return useInternalQuery(schema, modelName, 'findMany', args, merge(rootOptions, options)); }, useInfiniteFindMany: (args: any, options?: any) => { - return useInternalInfiniteQuery(schema, modelName, 'findMany', args, options); + return useInternalInfiniteQuery(schema, modelName, 'findMany', args, merge(rootOptions, options)); }, useCreate: (options?: any) => { - return useInternalMutation(schema, modelName, 'POST', 'create', options); + return useInternalMutation(schema, modelName, 'POST', 'create', merge(rootOptions, options)); }, useCreateMany: (options?: any) => { - return useInternalMutation(schema, modelName, 'POST', 'createMany', options); + return useInternalMutation(schema, modelName, 'POST', 'createMany', merge(rootOptions, options)); }, useCreateManyAndReturn: (options?: any) => { - return useInternalMutation(schema, modelName, 'POST', 'createManyAndReturn', options); + return useInternalMutation(schema, modelName, 'POST', 'createManyAndReturn', merge(rootOptions, options)); }, useUpdate: (options?: any) => { - return useInternalMutation(schema, modelName, 'PUT', 'update', options); + return useInternalMutation(schema, modelName, 'PUT', 'update', merge(rootOptions, options)); }, useUpdateMany: (options?: any) => { - return useInternalMutation(schema, modelName, 'PUT', 'updateMany', options); + return useInternalMutation(schema, modelName, 'PUT', 'updateMany', merge(rootOptions, options)); }, useUpdateManyAndReturn: (options?: any) => { - return useInternalMutation(schema, modelName, 'PUT', 'updateManyAndReturn', options); + return useInternalMutation(schema, modelName, 'PUT', 'updateManyAndReturn', merge(rootOptions, options)); }, useUpsert: (options?: any) => { - return useInternalMutation(schema, modelName, 'POST', 'upsert', options); + return useInternalMutation(schema, modelName, 'POST', 'upsert', merge(rootOptions, options)); }, useDelete: (options?: any) => { - return useInternalMutation(schema, modelName, 'DELETE', 'delete', options); + return useInternalMutation(schema, modelName, 'DELETE', 'delete', merge(rootOptions, options)); }, useDeleteMany: (options?: any) => { - return useInternalMutation(schema, modelName, 'DELETE', 'deleteMany', options); + return useInternalMutation(schema, modelName, 'DELETE', 'deleteMany', merge(rootOptions, options)); }, useCount: (args: any, options?: any) => { - return useInternalQuery(schema, modelName, 'count', args, options); + return useInternalQuery(schema, modelName, 'count', args, merge(rootOptions, options)); }, useAggregate: (args: any, options?: any) => { - return useInternalQuery(schema, modelName, 'aggregate', args, options); + return useInternalQuery(schema, modelName, 'aggregate', args, merge(rootOptions, options)); }, useGroupBy: (args: any, options?: any) => { - return useInternalQuery(schema, modelName, 'groupBy', args, options); + return useInternalQuery(schema, modelName, 'groupBy', args, merge(rootOptions, options)); }, } as ModelQueryHooks; } @@ -314,23 +325,30 @@ export function useInternalQuery( Omit>, 'queryKey'> & ExtraQueryOptions >, ) { - const argsValue = toValue(args); - const { optimisticUpdate, ...restOptions } = toValue(options) ?? {}; - const queryKey = getQueryKey(model, operation, argsValue, { - infinite: false, - optimisticUpdate: optimisticUpdate !== false, + // reactive query key + const queryKey = computed(() => { + const argsValue = toValue(args); + const { optimisticUpdate } = toValue(options) ?? {}; + return getQueryKey(model, operation, argsValue, { + infinite: false, + optimisticUpdate: optimisticUpdate !== false, + }); }); - const { endpoint, fetch } = getQuerySettings(); - const finalOptions: any = { - queryKey, - queryFn: ({ queryKey, signal }: any) => { - const [_prefix, _model, _op, args] = queryKey; - const reqUrl = makeUrl(endpoint, model, operation, args); - return fetcher(reqUrl, { signal }, fetch); - }, - ...restOptions, - }; + const { endpoint, fetch } = useFetchOptions(options); + + // reactive query options + const finalOptions: any = computed(() => { + const { optimisticUpdate: _, ...restOptions } = toValue(options) ?? {}; + return { + queryKey: queryKey.value, + queryFn: ({ signal }: any) => { + const reqUrl = makeUrl(endpoint, model, operation, toValue(args)); + return fetcher(reqUrl, { signal }, fetch); + }, + ...restOptions, + }; + }); return { queryKey, ...useQuery(finalOptions) }; } @@ -339,31 +357,38 @@ export function useInternalInfiniteQuery( model: string, operation: string, args: MaybeRefOrGetter, - options: - | MaybeRefOrGetter< - Omit< - UnwrapRef>>, - 'queryKey' | 'initialPageParam' - > - > - | undefined, + options: MaybeRefOrGetter< + | (Omit< + UnwrapRef>>, + 'queryKey' | 'initialPageParam' + > & + QueryContext) + | undefined + >, ) { options = options ?? { getNextPageParam: () => undefined }; - const { endpoint, fetch } = getQuerySettings(); - const argsValue = toValue(args); - const optionsValue = toValue(options); - const queryKey = getQueryKey(model, operation, argsValue, { infinite: true, optimisticUpdate: false }); - const finalOptions: any = { - queryKey, - queryFn: ({ queryKey, signal }: any) => { - const [_prefix, _model, _op, args] = queryKey; - const reqUrl = makeUrl(endpoint, model, operation, args); - return fetcher(reqUrl, { signal }, fetch); - }, - initialPageParam: argsValue, - ...optionsValue, - }; + // reactive query key + const queryKey = computed(() => { + const argsValue = toValue(args); + return getQueryKey(model, operation, argsValue, { infinite: true, optimisticUpdate: false }); + }); + + const { endpoint, fetch } = useFetchOptions(options); + + // reactive query options + const finalOptions: any = computed(() => { + const argsValue = toValue(args); + return { + queryKey: queryKey.value, + queryFn: ({ signal }: any) => { + const reqUrl = makeUrl(endpoint, model, operation, argsValue); + return fetcher(reqUrl, { signal }, fetch); + }, + initialPageParam: toValue(argsValue), + ...toValue(options), + }; + }); return { queryKey, ...useInfiniteQuery(finalOptions), @@ -390,8 +415,9 @@ export function useInternalMutation( Omit>, 'mutationFn'> & ExtraMutationOptions >, ) { - const { endpoint, fetch, logging } = getQuerySettings(); const queryClient = useQueryClient(); + + const { endpoint, fetch, logging } = useFetchOptions(options); const mutationFn = (data: any) => { const reqUrl = method === 'DELETE' ? makeUrl(endpoint, model, operation, data) : makeUrl(endpoint, model, operation); @@ -407,42 +433,84 @@ export function useInternalMutation( return fetcher(reqUrl, fetchInit, fetch) as Promise; }; - const optionsValue = toValue(options); - const finalOptions: any = { ...optionsValue, mutationFn }; - const invalidateQueries = optionsValue?.invalidateQueries !== false; - const optimisticUpdate = !!optionsValue?.optimisticUpdate; - - if (operation) { - if (invalidateQueries) { - setupInvalidation( + // reactive mutation options + const finalOptions = computed(() => { + const optionsValue = toValue(options); + const result = { + ...optionsValue, + mutationFn, + } as UnwrapRef> & ExtraMutationOptions; + + const invalidateQueries = optionsValue?.invalidateQueries !== false; + const optimisticUpdate = !!optionsValue?.optimisticUpdate; + + if (!optimisticUpdate) { + if (invalidateQueries) { + const invalidator = createInvalidator( + model, + operation, + schema, + (predicate: InvalidationPredicate) => invalidateQueriesMatchingPredicate(queryClient, predicate), + logging, + ); + // execute invalidator prior to user-provided onSuccess + result.onSuccess = async (...args) => { + await invalidator(...args); + const origOnSuccess: any = toValue(optionsValue?.onSuccess); + await origOnSuccess?.(...args); + }; + } + } else { + const optimisticUpdater = createOptimisticUpdater( model, operation, schema, - finalOptions, - (predicate) => queryClient.invalidateQueries({ predicate }), + { optimisticDataProvider: result.optimisticDataProvider }, + () => getAllQueries(queryClient), logging, ); - } - if (optimisticUpdate) { - setupOptimisticUpdate( - model, - operation, - schema, - finalOptions, - queryClient.getQueryCache().getAll(), - (queryKey, data) => { - // update query cache - queryClient.setQueryData(queryKey, data); - // cancel on-flight queries to avoid redundant cache updates, - // the settlement of the current mutation will trigger a new revalidation - queryClient.cancelQueries({ queryKey }, { revert: false, silent: true }); - }, - invalidateQueries ? (predicate) => queryClient.invalidateQueries({ predicate }) : undefined, - logging, - ); + // optimistic update on mutate + const origOnMutate = result.onMutate; + result.onMutate = async (...args) => { + // execute optimistic updater prior to user-provided onMutate + await optimisticUpdater(...args); + + // call user-provided onMutate + return unref(origOnMutate)?.(...args); + }; + + if (invalidateQueries) { + const invalidator = createInvalidator( + model, + operation, + schema, + (predicate: InvalidationPredicate) => invalidateQueriesMatchingPredicate(queryClient, predicate), + logging, + ); + const origOnSettled = result.onSettled; + result.onSettled = async (...args) => { + // execute invalidator prior to user-provided onSettled + await invalidator(...args); + + // call user-provided onSettled + return unref(origOnSettled)?.(...args); + }; + } } - } + return result; + }); return useMutation(finalOptions); } + +function useFetchOptions(options: MaybeRefOrGetter) { + const { endpoint, fetch, logging } = useQuerySettings(); + const optionsValue = toValue(options); + // options take precedence over context + return { + endpoint: optionsValue?.endpoint ?? endpoint, + fetch: optionsValue?.fetch ?? fetch, + logging: optionsValue?.logging ?? logging, + }; +} diff --git a/packages/clients/tanstack-query/test/react-query.test.tsx b/packages/clients/tanstack-query/test/react-query.test.tsx index 9a178f48..4e2db769 100644 --- a/packages/clients/tanstack-query/test/react-query.test.tsx +++ b/packages/clients/tanstack-query/test/react-query.test.tsx @@ -7,8 +7,8 @@ import { act, cleanup, renderHook, waitFor } from '@testing-library/react'; import nock from 'nock'; import React from 'react'; import { afterEach, describe, expect, it } from 'vitest'; +import { getQueryKey } from '../src/common/query-key'; import { QuerySettingsProvider, useClientQueries } from '../src/react'; -import { getQueryKey } from '../src/utils/common'; import { schema } from './schemas/basic/schema-lite'; const BASE_URL = 'http://localhost'; diff --git a/packages/clients/tanstack-query/test/svelte-typing-test.ts b/packages/clients/tanstack-query/test/svelte-typing-test.ts index dff02583..9c8788eb 100644 --- a/packages/clients/tanstack-query/test/svelte-typing-test.ts +++ b/packages/clients/tanstack-query/test/svelte-typing-test.ts @@ -1,5 +1,4 @@ -import { get } from 'svelte/store'; -import { useClientQueries } from '../src/svelte'; +import { useClientQueries } from '../src/svelte/index.svelte'; import { schema } from './schemas/basic/schema-lite'; const client = useClientQueries(schema); @@ -7,75 +6,73 @@ const client = useClientQueries(schema); // @ts-expect-error missing args client.user.useFindUnique(); -check(get(client.user.useFindUnique({ where: { id: '1' } })).data?.email); -check(get(client.user.useFindUnique({ where: { id: '1' } })).queryKey); -check(get(client.user.useFindUnique({ where: { id: '1' } }, { optimisticUpdate: true, enabled: false }))); +check(client.user.useFindUnique(() => ({ where: { id: '1' } })).data?.email); +check(client.user.useFindUnique(() => ({ where: { id: '1' } })).queryKey); +check(client.user.useFindUnique(() => ({ where: { id: '1' } }), () => ({ optimisticUpdate: true, enabled: false }))); // @ts-expect-error unselected field -check(get(client.user.useFindUnique({ select: { email: true } })).data.name); +check(client.user.useFindUnique(() => ({ select: { email: true }, where: { id: '1' } })).data?.name); -check(get(client.user.useFindUnique({ where: { id: '1' }, include: { posts: true } })).data?.posts[0]?.title); +check(client.user.useFindUnique(() => ({ where: { id: '1' }, include: { posts: true } })).data?.posts[0]?.title); -check(get(client.user.useFindFirst()).data?.email); -check(get(client.user.useFindFirst()).data?.$optimistic); +check(client.user.useFindFirst().data?.email); +check(client.user.useFindFirst().data?.$optimistic); -check(get(client.user.useFindMany()).data?.[0]?.email); -check(get(client.user.useFindMany()).data?.[0]?.$optimistic); +check(client.user.useFindMany().data?.[0]?.email); +check(client.user.useFindMany().data?.[0]?.$optimistic); -check(get(client.user.useInfiniteFindMany()).data?.pages[0]?.[0]?.email); +check(client.user.useInfiniteFindMany().data?.pages[0]?.[0]?.email); check( - get( - client.user.useInfiniteFindMany( - {}, - { - getNextPageParam: () => ({ id: '2' }), - }, - ), + client.user.useInfiniteFindMany( + () => ({}), + () => ({ + getNextPageParam: () => ({ id: '2' }), + }), ).data?.pages[1]?.[0]?.email, ); // @ts-expect-error -check(get(client.user.useInfiniteFindMany()).data?.pages[0]?.[0]?.$optimistic); +check(client.user.useInfiniteFindMany().data?.pages[0]?.[0]?.$optimistic); -check(get(client.user.useCount()).data?.toFixed(2)); -check(get(client.user.useCount({ select: { email: true } })).data?.email.toFixed(2)); +check(client.user.useCount().data?.toFixed(2)); +check(client.user.useCount(() => ({ select: { email: true } })).data?.email.toFixed(2)); -check(get(client.user.useAggregate({ _max: { email: true } })).data?._max.email); +check(client.user.useAggregate(() => ({ _max: { email: true } })).data?._max.email); -check(get(client.user.useGroupBy({ by: ['email'], _max: { name: true } })).data?.[0]?._max.name); +check(client.user.useGroupBy(() => ({ by: ['email'], _max: { name: true } })).data?.[0]?._max.name); // @ts-expect-error missing args client.user.useCreate().mutate(); -get(client.user.useCreate()).mutate({ data: { email: 'test@example.com' } }); -get(client.user.useCreate({ optimisticUpdate: true, invalidateQueries: false, retry: 3 })).mutate({ +client.user.useCreate().mutate({ data: { email: 'test@example.com' } }); +client.user.useCreate(() => ({ optimisticUpdate: true, invalidateQueries: false, retry: 3 })).mutate({ data: { email: 'test@example.com' }, }); -get(client.user.useCreate()) +client.user.useCreate() .mutateAsync({ data: { email: 'test@example.com' }, include: { posts: true } }) .then((d) => check(d.posts[0]?.title)); -get(client.user.useCreateMany()) +client.user.useCreateMany() .mutateAsync({ data: [{ email: 'test@example.com' }, { email: 'test2@example.com' }], skipDuplicates: true, }) .then((d) => d.count); -get(client.user.useCreateManyAndReturn()) +client.user.useCreateManyAndReturn() .mutateAsync({ data: [{ email: 'test@example.com' }], }) .then((d) => check(d[0]?.name)); -get(client.user.useCreateManyAndReturn()) +client.user.useCreateManyAndReturn() .mutateAsync({ data: [{ email: 'test@example.com' }], select: { email: true }, }) // @ts-expect-error unselected field - .then((d) => check(d[0].name)); + .then((d) => check(d[0]?.name)); -get(client.user.useUpdate()).mutate( +client.user.useUpdate().mutate( { data: { email: 'updated@example.com' }, where: { id: '1' } }, { onSuccess: (d) => { @@ -84,21 +81,21 @@ get(client.user.useUpdate()).mutate( }, ); -get(client.user.useUpdateMany()).mutate({ data: { email: 'updated@example.com' } }); +client.user.useUpdateMany().mutate({ data: { email: 'updated@example.com' } }); -get(client.user.useUpdateManyAndReturn()) +client.user.useUpdateManyAndReturn() .mutateAsync({ data: { email: 'updated@example.com' } }) .then((d) => check(d[0]?.email)); -get(client.user.useUpsert()).mutate({ +client.user.useUpsert().mutate({ where: { id: '1' }, create: { email: 'new@example.com' }, update: { email: 'updated@example.com' }, }); -get(client.user.useDelete()).mutate({ where: { id: '1' }, include: { posts: true } }); +client.user.useDelete().mutate({ where: { id: '1' }, include: { posts: true } }); -get(client.user.useDeleteMany()).mutate({ where: { email: 'test@example.com' } }); +client.user.useDeleteMany().mutate({ where: { email: 'test@example.com' } }); function check(_value: unknown) { // noop diff --git a/packages/clients/tanstack-query/tsconfig.general.json b/packages/clients/tanstack-query/tsconfig.general.json new file mode 100644 index 00000000..ee76029c --- /dev/null +++ b/packages/clients/tanstack-query/tsconfig.general.json @@ -0,0 +1,10 @@ +{ + "extends": "@zenstackhq/typescript-config/base.json", + "include": ["src/**/*.ts"], + "exclude": ["src/svelte"], + "compilerOptions": { + "lib": ["ESNext"], + "outDir": "dist", + "noEmit": false + } +} diff --git a/packages/clients/tanstack-query/tsconfig.json b/packages/clients/tanstack-query/tsconfig.json index 9dd323f1..1fc0ded4 100644 --- a/packages/clients/tanstack-query/tsconfig.json +++ b/packages/clients/tanstack-query/tsconfig.json @@ -2,6 +2,7 @@ "extends": "@zenstackhq/typescript-config/base.json", "include": ["src/**/*.ts"], "compilerOptions": { - "lib": ["ESNext"] + "lib": ["ESNext"], + "noEmit": true } } diff --git a/packages/clients/tanstack-query/tsconfig.svelte.json b/packages/clients/tanstack-query/tsconfig.svelte.json new file mode 100644 index 00000000..7694ca96 --- /dev/null +++ b/packages/clients/tanstack-query/tsconfig.svelte.json @@ -0,0 +1,9 @@ +{ + "extends": "@zenstackhq/typescript-config/base.json", + "include": ["src/svelte/**/*.ts"], + "compilerOptions": { + "lib": ["ESNext"], + "outDir": "dist", + "noEmit": false + } +} diff --git a/packages/common-helpers/package.json b/packages/common-helpers/package.json index 8d60e7df..f1f4dd02 100644 --- a/packages/common-helpers/package.json +++ b/packages/common-helpers/package.json @@ -1,12 +1,13 @@ { "name": "@zenstackhq/common-helpers", - "version": "3.0.0", + "version": "3.1.0", "description": "ZenStack Common Helpers", "type": "module", "scripts": { "build": "tsc --noEmit && tsup-node", "watch": "tsup-node --watch", "lint": "eslint src --ext ts", + "test": "vitest run", "pack": "pnpm pack" }, "keywords": [], @@ -29,6 +30,7 @@ }, "devDependencies": { "@zenstackhq/typescript-config": "workspace:*", - "@zenstackhq/eslint-config": "workspace:*" + "@zenstackhq/eslint-config": "workspace:*", + "@zenstackhq/vitest-config": "workspace:*" } } diff --git a/packages/common-helpers/src/tiny-invariant.ts b/packages/common-helpers/src/tiny-invariant.ts index b4b2c216..680b6589 100644 --- a/packages/common-helpers/src/tiny-invariant.ts +++ b/packages/common-helpers/src/tiny-invariant.ts @@ -1,4 +1,4 @@ -const isProduction = process.env['NODE_ENV'] === 'production'; +const isProduction = typeof process !== 'undefined' && process.env['NODE_ENV'] === 'production'; const prefix = 'Invariant failed'; export function invariant(condition: unknown, message?: string): asserts condition { diff --git a/packages/common-helpers/test/case-conversion.test.ts b/packages/common-helpers/test/case-conversion.test.ts new file mode 100644 index 00000000..f61e7f52 --- /dev/null +++ b/packages/common-helpers/test/case-conversion.test.ts @@ -0,0 +1,55 @@ +import { describe, expect, it } from 'vitest'; +import { lowerCaseFirst } from '../src/lower-case-first'; +import { upperCaseFirst } from '../src/upper-case-first'; + +describe('lowerCaseFirst tests', () => { + it('should lowercase the first character', () => { + expect(lowerCaseFirst('Hello')).toBe('hello'); + expect(lowerCaseFirst('WORLD')).toBe('wORLD'); + expect(lowerCaseFirst('A')).toBe('a'); + }); + + it('should handle already lowercase strings', () => { + expect(lowerCaseFirst('hello')).toBe('hello'); + expect(lowerCaseFirst('world')).toBe('world'); + }); + + it('should handle empty string', () => { + expect(lowerCaseFirst('')).toBe(''); + }); + + it('should handle strings with numbers', () => { + expect(lowerCaseFirst('123abc')).toBe('123abc'); + }); + + it('should handle strings with special characters', () => { + expect(lowerCaseFirst('!Hello')).toBe('!Hello'); + expect(lowerCaseFirst('@World')).toBe('@World'); + }); +}); + +describe('upperCaseFirst tests', () => { + it('should uppercase the first character', () => { + expect(upperCaseFirst('hello')).toBe('Hello'); + expect(upperCaseFirst('world')).toBe('World'); + expect(upperCaseFirst('a')).toBe('A'); + }); + + it('should handle already uppercase strings', () => { + expect(upperCaseFirst('Hello')).toBe('Hello'); + expect(upperCaseFirst('WORLD')).toBe('WORLD'); + }); + + it('should handle empty string', () => { + expect(upperCaseFirst('')).toBe(''); + }); + + it('should handle strings with numbers', () => { + expect(upperCaseFirst('123abc')).toBe('123abc'); + }); + + it('should handle strings with special characters', () => { + expect(upperCaseFirst('!hello')).toBe('!hello'); + expect(upperCaseFirst('@world')).toBe('@world'); + }); +}); diff --git a/packages/common-helpers/test/clone.test.ts b/packages/common-helpers/test/clone.test.ts new file mode 100644 index 00000000..eecccfa6 --- /dev/null +++ b/packages/common-helpers/test/clone.test.ts @@ -0,0 +1,149 @@ +import { describe, expect, it } from 'vitest'; +import { clone } from '../src/clone'; + +describe('clone tests', () => { + describe('primitives', () => { + it('should return primitives as-is', () => { + expect(clone(42)).toBe(42); + expect(clone('hello')).toBe('hello'); + expect(clone(true)).toBe(true); + expect(clone(false)).toBe(false); + expect(clone(null)).toBe(null); + expect(clone(undefined)).toBe(undefined); + }); + }); + + describe('arrays', () => { + it('should clone simple arrays', () => { + const arr = [1, 2, 3]; + const cloned = clone(arr); + expect(cloned).toEqual(arr); + expect(cloned).not.toBe(arr); + }); + + it('should deep clone nested arrays', () => { + const arr = [1, [2, 3], [4, [5, 6]]]; + const cloned = clone(arr); + expect(cloned).toEqual(arr); + expect(cloned).not.toBe(arr); + expect(cloned[1]).not.toBe(arr[1]); + expect(cloned[2]).not.toBe(arr[2]); + }); + + it('should clone arrays with objects', () => { + const arr = [{ a: 1 }, { b: 2 }]; + const cloned = clone(arr); + expect(cloned).toEqual(arr); + expect(cloned).not.toBe(arr); + expect(cloned[0]).not.toBe(arr[0]); + expect(cloned[1]).not.toBe(arr[1]); + }); + + it('should handle empty arrays', () => { + const arr: number[] = []; + const cloned = clone(arr); + expect(cloned).toEqual([]); + expect(cloned).not.toBe(arr); + }); + }); + + describe('plain objects', () => { + it('should clone simple objects', () => { + const obj = { a: 1, b: 2 }; + const cloned = clone(obj); + expect(cloned).toEqual(obj); + expect(cloned).not.toBe(obj); + }); + + it('should deep clone nested objects', () => { + const obj = { a: 1, b: { c: 2, d: { e: 3 } } }; + const cloned = clone(obj); + expect(cloned).toEqual(obj); + expect(cloned).not.toBe(obj); + expect(cloned.b).not.toBe(obj.b); + expect(cloned.b.d).not.toBe(obj.b.d); + }); + + it('should clone objects with arrays', () => { + const obj = { a: [1, 2], b: { c: [3, 4] } }; + const cloned = clone(obj); + expect(cloned).toEqual(obj); + expect(cloned).not.toBe(obj); + expect(cloned.a).not.toBe(obj.a); + expect(cloned.b.c).not.toBe(obj.b.c); + }); + + it('should handle empty objects', () => { + const obj = {}; + const cloned = clone(obj); + expect(cloned).toEqual({}); + expect(cloned).not.toBe(obj); + }); + + it('should handle objects with null prototype', () => { + const obj = Object.create(null); + obj.foo = 'bar'; + const cloned = clone(obj); + expect(cloned).toEqual(obj); + expect(cloned).not.toBe(obj); + }); + }); + + describe('non-plain objects', () => { + it('should return Date objects as-is', () => { + const date = new Date(); + const cloned = clone(date); + expect(cloned).toBe(date); + }); + + it('should return RegExp objects as-is', () => { + const regex = /test/gi; + const cloned = clone(regex); + expect(cloned).toBe(regex); + }); + + it('should return class instances as-is', () => { + class MyClass { + value = 42; + } + const instance = new MyClass(); + const cloned = clone(instance); + expect(cloned).toBe(instance); + }); + + it('should return functions as-is', () => { + const fn = () => 42; + const cloned = clone(fn); + expect(cloned).toBe(fn); + }); + }); + + describe('mixed structures', () => { + it('should handle complex mixed structures', () => { + const complex = { + number: 42, + string: 'hello', + bool: true, + null: null, + array: [1, 2, { nested: 'value' }], + object: { + a: [1, 2, 3], + b: { c: 4 }, + }, + }; + const cloned = clone(complex); + expect(cloned).toEqual(complex); + expect(cloned).not.toBe(complex); + expect(cloned.array).not.toBe(complex.array); + expect(cloned.array[2]).not.toBe(complex.array[2]); + expect(cloned.object).not.toBe(complex.object); + expect(cloned.object.a).not.toBe(complex.object.a); + }); + + it('should preserve primitive values in nested structures', () => { + const obj = { a: { b: { c: 42 } } }; + const cloned = clone(obj); + expect(cloned.a.b.c).toBe(42); + }); + }); +}); diff --git a/packages/common-helpers/test/enumerable.test.ts b/packages/common-helpers/test/enumerable.test.ts new file mode 100644 index 00000000..fbe61f8b --- /dev/null +++ b/packages/common-helpers/test/enumerable.test.ts @@ -0,0 +1,55 @@ +import { describe, expect, it } from 'vitest'; +import { enumerate } from '../src/enumerable'; + +describe('enumerate tests', () => { + it('should return empty array for null', () => { + expect(enumerate(null)).toEqual([]); + }); + + it('should return empty array for undefined', () => { + expect(enumerate(undefined)).toEqual([]); + }); + + it('should return array as-is', () => { + const arr = [1, 2, 3]; + expect(enumerate(arr)).toBe(arr); + expect(enumerate(arr)).toEqual([1, 2, 3]); + }); + + it('should wrap scalar values in an array', () => { + expect(enumerate(42)).toEqual([42]); + expect(enumerate('hello')).toEqual(['hello']); + expect(enumerate(true)).toEqual([true]); + expect(enumerate(false)).toEqual([false]); + }); + + it('should handle empty arrays', () => { + const arr: number[] = []; + expect(enumerate(arr)).toBe(arr); + expect(enumerate(arr)).toEqual([]); + }); + + it('should handle objects', () => { + const obj = { a: 1 }; + expect(enumerate(obj)).toEqual([obj]); + }); + + it('should handle nested arrays', () => { + const arr = [[1, 2], [3, 4]]; + expect(enumerate(arr)).toBe(arr); + expect(enumerate(arr)).toEqual([[1, 2], [3, 4]]); + }); + + it('should handle functions', () => { + const fn = () => 42; + expect(enumerate(fn)).toEqual([fn]); + }); + + it('should handle zero', () => { + expect(enumerate(0)).toEqual([0]); + }); + + it('should handle empty string', () => { + expect(enumerate('')).toEqual(['']); + }); +}); diff --git a/packages/common-helpers/test/is-plain-object.test.ts b/packages/common-helpers/test/is-plain-object.test.ts new file mode 100644 index 00000000..0698d63f --- /dev/null +++ b/packages/common-helpers/test/is-plain-object.test.ts @@ -0,0 +1,50 @@ +import { describe, expect, it } from 'vitest'; +import { isPlainObject } from '../src/is-plain-object'; + +describe('isPlainObject tests', () => { + it('should return true for plain objects', () => { + expect(isPlainObject({})).toBe(true); + expect(isPlainObject({ a: 1 })).toBe(true); + expect(isPlainObject({ a: 1, b: { c: 2 } })).toBe(true); + expect(isPlainObject(Object.create(null))).toBe(true); + }); + + it('should return false for non-plain objects', () => { + expect(isPlainObject(null)).toBe(false); + expect(isPlainObject(undefined)).toBe(false); + expect(isPlainObject(42)).toBe(false); + expect(isPlainObject('string')).toBe(false); + expect(isPlainObject(true)).toBe(false); + expect(isPlainObject(false)).toBe(false); + }); + + it('should return false for arrays', () => { + expect(isPlainObject([])).toBe(false); + expect(isPlainObject([1, 2, 3])).toBe(false); + }); + + it('should return false for class instances', () => { + class MyClass {} + expect(isPlainObject(new MyClass())).toBe(false); + expect(isPlainObject(new Date())).toBe(false); + expect(isPlainObject(new Error())).toBe(false); + expect(isPlainObject(new RegExp(''))).toBe(false); + }); + + it('should return false for functions', () => { + expect(isPlainObject(() => {})).toBe(false); + expect(isPlainObject(function () {})).toBe(false); + }); + + it('should return true for objects with custom prototype', () => { + // Objects created with Object.create still have isPrototypeOf on their prototype chain + const obj = Object.create({ custom: 'prototype' }); + expect(isPlainObject(obj)).toBe(true); + }); + + it('should handle objects without hasOwnProperty', () => { + const obj = Object.create(null); + obj.foo = 'bar'; + expect(isPlainObject(obj)).toBe(true); + }); +}); diff --git a/packages/common-helpers/test/param-case.test.ts b/packages/common-helpers/test/param-case.test.ts new file mode 100644 index 00000000..623a9204 --- /dev/null +++ b/packages/common-helpers/test/param-case.test.ts @@ -0,0 +1,71 @@ +import { describe, expect, it } from 'vitest'; +import { paramCase } from '../src/param-case'; + +describe('paramCase tests', () => { + it('should convert camelCase to param-case', () => { + expect(paramCase('camelCase')).toBe('camel-case'); + expect(paramCase('userName')).toBe('user-name'); + expect(paramCase('firstName')).toBe('first-name'); + }); + + it('should convert PascalCase to param-case', () => { + expect(paramCase('PascalCase')).toBe('pascal-case'); + expect(paramCase('UserName')).toBe('user-name'); + expect(paramCase('FirstName')).toBe('first-name'); + }); + + it('should handle all uppercase strings', () => { + expect(paramCase('SCREAMING_SNAKE_CASE')).toBe('screaming-snake-case'); + expect(paramCase('HTTP')).toBe('http'); + expect(paramCase('HTTPServer')).toBe('http-server'); + }); + + it('should handle snake_case', () => { + expect(paramCase('snake_case')).toBe('snake-case'); + expect(paramCase('user_name')).toBe('user-name'); + }); + + it('should handle strings with numbers', () => { + // Numbers adjacent to letters don't create splits + expect(paramCase('version2')).toBe('version2'); + expect(paramCase('user123Name')).toBe('user123-name'); + expect(paramCase('v2API')).toBe('v2-api'); + }); + + it('should handle strings with spaces', () => { + expect(paramCase('hello world')).toBe('hello-world'); + expect(paramCase('user name')).toBe('user-name'); + }); + + it('should handle strings with special characters', () => { + expect(paramCase('hello@world')).toBe('hello-world'); + expect(paramCase('user!name')).toBe('user-name'); + expect(paramCase('first$last')).toBe('first-last'); + }); + + it('should handle mixed formats', () => { + expect(paramCase('XMLHttpRequest')).toBe('xml-http-request'); + expect(paramCase('newCustomerId')).toBe('new-customer-id'); + expect(paramCase('innerHtml')).toBe('inner-html'); + }); + + it('should handle single character strings', () => { + expect(paramCase('a')).toBe('a'); + expect(paramCase('A')).toBe('a'); + }); + + it('should handle empty string', () => { + expect(paramCase('')).toBe(''); + }); + + it('should strip leading and trailing special characters', () => { + expect(paramCase('-hello-world-')).toBe('hello-world'); + expect(paramCase('_hello_world_')).toBe('hello-world'); + expect(paramCase('!!hello!world!!')).toBe('hello-world'); + }); + + it('should handle consecutive uppercase letters', () => { + expect(paramCase('HTTPSConnection')).toBe('https-connection'); + expect(paramCase('IOError')).toBe('io-error'); + }); +}); diff --git a/packages/common-helpers/test/safe-json-stringify.test.ts b/packages/common-helpers/test/safe-json-stringify.test.ts new file mode 100644 index 00000000..10740cb6 --- /dev/null +++ b/packages/common-helpers/test/safe-json-stringify.test.ts @@ -0,0 +1,81 @@ +import { describe, expect, it } from 'vitest'; +import { safeJSONStringify } from '../src/safe-json-stringify'; + +describe('safeJSONStringify tests', () => { + it('should stringify simple objects', () => { + expect(safeJSONStringify({ a: 1, b: 2 })).toBe('{"a":1,"b":2}'); + }); + + it('should stringify arrays', () => { + expect(safeJSONStringify([1, 2, 3])).toBe('[1,2,3]'); + }); + + it('should stringify strings', () => { + expect(safeJSONStringify('hello')).toBe('"hello"'); + }); + + it('should stringify numbers', () => { + expect(safeJSONStringify(42)).toBe('42'); + }); + + it('should stringify booleans', () => { + expect(safeJSONStringify(true)).toBe('true'); + expect(safeJSONStringify(false)).toBe('false'); + }); + + it('should stringify null', () => { + expect(safeJSONStringify(null)).toBe('null'); + }); + + it('should stringify bigint values as strings', () => { + expect(safeJSONStringify(BigInt(123))).toBe('"123"'); + expect(safeJSONStringify(BigInt('9007199254740991'))).toBe('"9007199254740991"'); + }); + + it('should stringify objects containing bigint values', () => { + const obj = { id: BigInt(123), name: 'test' }; + expect(safeJSONStringify(obj)).toBe('{"id":"123","name":"test"}'); + }); + + it('should stringify nested objects with bigint', () => { + const obj = { user: { id: BigInt(456), data: { count: BigInt(789) } } }; + expect(safeJSONStringify(obj)).toBe('{"user":{"id":"456","data":{"count":"789"}}}'); + }); + + it('should stringify arrays with bigint values', () => { + const arr = [BigInt(1), BigInt(2), BigInt(3)]; + expect(safeJSONStringify(arr)).toBe('["1","2","3"]'); + }); + + it('should handle mixed types including bigint', () => { + const mixed = { + str: 'hello', + num: 42, + bool: true, + bigInt: BigInt(999), + arr: [1, BigInt(2), 'three'], + obj: { nested: BigInt(100) }, + }; + expect(safeJSONStringify(mixed)).toBe( + '{"str":"hello","num":42,"bool":true,"bigInt":"999","arr":[1,"2","three"],"obj":{"nested":"100"}}', + ); + }); + + it('should handle very large bigint values', () => { + const largeBigInt = BigInt('123456789012345678901234567890'); + expect(safeJSONStringify(largeBigInt)).toBe('"123456789012345678901234567890"'); + }); + + it('should handle zero bigint', () => { + expect(safeJSONStringify(BigInt(0))).toBe('"0"'); + }); + + it('should handle negative bigint', () => { + expect(safeJSONStringify(BigInt(-123))).toBe('"-123"'); + }); + + it('should handle empty objects and arrays', () => { + expect(safeJSONStringify({})).toBe('{}'); + expect(safeJSONStringify([])).toBe('[]'); + }); +}); diff --git a/packages/common-helpers/test/sleep.test.ts b/packages/common-helpers/test/sleep.test.ts new file mode 100644 index 00000000..2686cfac --- /dev/null +++ b/packages/common-helpers/test/sleep.test.ts @@ -0,0 +1,70 @@ +import { describe, expect, it, vi } from 'vitest'; +import { sleep } from '../src/sleep'; + +describe('sleep tests', () => { + it('should resolve after the specified timeout', async () => { + const start = Date.now(); + await sleep(100); + const elapsed = Date.now() - start; + expect(elapsed).toBeGreaterThanOrEqual(95); // Allow for small timing variations + }); + + it('should resolve immediately for zero timeout', async () => { + const start = Date.now(); + await sleep(0); + const elapsed = Date.now() - start; + expect(elapsed).toBeLessThan(50); // Should be nearly instant + }); + + it('should return a Promise', () => { + const result = sleep(10); + expect(result).toBeInstanceOf(Promise); + }); + + it('should resolve to undefined', async () => { + const result = await sleep(10); + expect(result).toBeUndefined(); + }); + + it('should work with multiple concurrent sleeps', async () => { + const start = Date.now(); + await Promise.all([sleep(50), sleep(50), sleep(50)]); + const elapsed = Date.now() - start; + // All should run concurrently, so total time should be ~50ms, not 150ms + expect(elapsed).toBeLessThan(100); + }); + + it('should work in a chain', async () => { + const start = Date.now(); + await sleep(50).then(() => sleep(50)); + const elapsed = Date.now() - start; + expect(elapsed).toBeGreaterThanOrEqual(95); // Should be ~100ms total + }); + + it('should allow using with async/await', async () => { + const order: number[] = []; + order.push(1); + await sleep(50); + order.push(2); + expect(order).toEqual([1, 2]); + }); + + it('should work with setTimeout mock', async () => { + vi.useFakeTimers(); + const promise = sleep(1000); + vi.advanceTimersByTime(1000); + await promise; + vi.useRealTimers(); + }); + + it('should handle very short timeouts', async () => { + await expect(sleep(1)).resolves.toBeUndefined(); + }); + + it('should handle longer timeouts', async () => { + const start = Date.now(); + await sleep(200); + const elapsed = Date.now() - start; + expect(elapsed).toBeGreaterThanOrEqual(190); + }); +}); diff --git a/packages/common-helpers/test/tiny-invariant.test.ts b/packages/common-helpers/test/tiny-invariant.test.ts new file mode 100644 index 00000000..5b3772b4 --- /dev/null +++ b/packages/common-helpers/test/tiny-invariant.test.ts @@ -0,0 +1,70 @@ +import { describe, expect, it } from 'vitest'; +import { invariant } from '../src/tiny-invariant'; + +describe('invariant tests', () => { + it('should not throw when condition is true', () => { + expect(() => invariant(true)).not.toThrow(); + expect(() => invariant(1)).not.toThrow(); + expect(() => invariant('non-empty')).not.toThrow(); + expect(() => invariant({})).not.toThrow(); + expect(() => invariant([])).not.toThrow(); + }); + + it('should throw when condition is false', () => { + expect(() => invariant(false)).toThrow('Invariant failed'); + }); + + it('should throw when condition is null', () => { + expect(() => invariant(null)).toThrow('Invariant failed'); + }); + + it('should throw when condition is undefined', () => { + expect(() => invariant(undefined)).toThrow('Invariant failed'); + }); + + it('should throw when condition is 0', () => { + expect(() => invariant(0)).toThrow('Invariant failed'); + }); + + it('should throw when condition is empty string', () => { + expect(() => invariant('')).toThrow('Invariant failed'); + }); + + it('should throw with custom message when provided', () => { + // Note: The production check is evaluated at module load time, + // so we test the actual behavior based on the current environment + expect(() => invariant(false, 'Custom error message')).toThrow(/Invariant failed/); + }); + + it('should work as a type guard', () => { + const value: string | null = 'hello'; + invariant(value); + // After this point, TypeScript should know value is string, not string | null + const length: number = value.length; // Should not cause type error + expect(length).toBe(5); + }); + + it('should handle complex conditions', () => { + const obj = { value: 42 }; + expect(() => invariant(obj.value > 0)).not.toThrow(); + expect(() => invariant(obj.value < 0, 'Value must be positive')).toThrow(); + }); + + it('should handle array checks', () => { + const arr = [1, 2, 3]; + expect(() => invariant(arr.length > 0)).not.toThrow(); + expect(() => invariant(arr.length === 0, 'Array should be empty')).toThrow(); + }); + + it('should handle NaN', () => { + expect(() => invariant(NaN)).toThrow('Invariant failed'); + }); + + it('should handle object existence checks', () => { + const obj: { prop?: string } = { prop: 'value' }; + expect(() => invariant(obj.prop)).not.toThrow(); + + const objWithoutProp: { prop?: string } = {}; + expect(() => invariant(objWithoutProp.prop)).toThrow(); + }); +}); diff --git a/packages/common-helpers/test/zip.test.ts b/packages/common-helpers/test/zip.test.ts new file mode 100644 index 00000000..4fe8e9b3 --- /dev/null +++ b/packages/common-helpers/test/zip.test.ts @@ -0,0 +1,96 @@ +import { describe, expect, it } from 'vitest'; +import { zip } from '../src/zip'; + +describe('zip tests', () => { + it('should zip two arrays of equal length', () => { + const arr1 = [1, 2, 3]; + const arr2 = ['a', 'b', 'c']; + expect(zip(arr1, arr2)).toEqual([ + [1, 'a'], + [2, 'b'], + [3, 'c'], + ]); + }); + + it('should zip arrays when first is shorter', () => { + const arr1 = [1, 2]; + const arr2 = ['a', 'b', 'c']; + expect(zip(arr1, arr2)).toEqual([ + [1, 'a'], + [2, 'b'], + ]); + }); + + it('should zip arrays when second is shorter', () => { + const arr1 = [1, 2, 3]; + const arr2 = ['a', 'b']; + expect(zip(arr1, arr2)).toEqual([ + [1, 'a'], + [2, 'b'], + ]); + }); + + it('should handle empty arrays', () => { + expect(zip([], [])).toEqual([]); + expect(zip([1, 2, 3], [])).toEqual([]); + expect(zip([], ['a', 'b', 'c'])).toEqual([]); + }); + + it('should handle single element arrays', () => { + expect(zip([1], ['a'])).toEqual([[1, 'a']]); + }); + + it('should handle different types', () => { + const arr1 = [1, 2, 3]; + const arr2 = [true, false, true]; + expect(zip(arr1, arr2)).toEqual([ + [1, true], + [2, false], + [3, true], + ]); + }); + + it('should handle objects in arrays', () => { + const arr1 = [{ id: 1 }, { id: 2 }]; + const arr2 = [{ name: 'a' }, { name: 'b' }]; + expect(zip(arr1, arr2)).toEqual([ + [{ id: 1 }, { name: 'a' }], + [{ id: 2 }, { name: 'b' }], + ]); + }); + + it('should handle nested arrays', () => { + const arr1 = [[1, 2], [3, 4]]; + const arr2 = [['a', 'b'], ['c', 'd']]; + expect(zip(arr1, arr2)).toEqual([ + [[1, 2], ['a', 'b']], + [[3, 4], ['c', 'd']], + ]); + }); + + it('should preserve null and undefined values', () => { + const arr1 = [1, null, undefined]; + const arr2 = ['a', 'b', 'c']; + expect(zip(arr1, arr2)).toEqual([ + [1, 'a'], + [null, 'b'], + [undefined, 'c'], + ]); + }); + + it('should work with readonly arrays', () => { + const arr1: readonly number[] = [1, 2, 3]; + const arr2: readonly string[] = ['a', 'b', 'c']; + expect(zip(arr1, arr2)).toEqual([ + [1, 'a'], + [2, 'b'], + [3, 'c'], + ]); + }); + + it('should handle very different length arrays', () => { + const arr1 = [1]; + const arr2 = ['a', 'b', 'c', 'd', 'e']; + expect(zip(arr1, arr2)).toEqual([[1, 'a']]); + }); +}); diff --git a/packages/common-helpers/vitest.config.ts b/packages/common-helpers/vitest.config.ts new file mode 100644 index 00000000..75a9f709 --- /dev/null +++ b/packages/common-helpers/vitest.config.ts @@ -0,0 +1,4 @@ +import base from '@zenstackhq/vitest-config/base'; +import { defineConfig, mergeConfig } from 'vitest/config'; + +export default mergeConfig(base, defineConfig({})); diff --git a/packages/config/eslint-config/package.json b/packages/config/eslint-config/package.json index c06180d7..1f27a4d2 100644 --- a/packages/config/eslint-config/package.json +++ b/packages/config/eslint-config/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/eslint-config", - "version": "3.0.0", + "version": "3.1.0", "type": "module", "private": true, "license": "MIT" diff --git a/packages/config/typescript-config/package.json b/packages/config/typescript-config/package.json index a6e97e28..75c83960 100644 --- a/packages/config/typescript-config/package.json +++ b/packages/config/typescript-config/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/typescript-config", - "version": "3.0.0", + "version": "3.1.0", "private": true, "license": "MIT" } diff --git a/packages/config/vitest-config/base.config.js b/packages/config/vitest-config/base.config.js index f7b36f73..ed9e1c5c 100644 --- a/packages/config/vitest-config/base.config.js +++ b/packages/config/vitest-config/base.config.js @@ -8,5 +8,9 @@ export default defineConfig({ include: ['**/*.test.ts'], testTimeout: 100000, hookTimeout: 100000, + coverage: { + provider: 'v8', + exclude: ['tests/**', 'samples/**', 'packages/**/tests/**'], + }, }, }); diff --git a/packages/config/vitest-config/package.json b/packages/config/vitest-config/package.json index 26441c95..9ffd2b30 100644 --- a/packages/config/vitest-config/package.json +++ b/packages/config/vitest-config/package.json @@ -1,7 +1,7 @@ { "name": "@zenstackhq/vitest-config", "type": "module", - "version": "3.0.0", + "version": "3.1.0", "private": true, "license": "MIT", "exports": { diff --git a/packages/create-zenstack/package.json b/packages/create-zenstack/package.json index e532900e..1b751ac6 100644 --- a/packages/create-zenstack/package.json +++ b/packages/create-zenstack/package.json @@ -1,6 +1,6 @@ { "name": "create-zenstack", - "version": "3.0.0", + "version": "3.1.0", "description": "Create a new ZenStack project", "type": "module", "scripts": { diff --git a/packages/create-zenstack/src/index.ts b/packages/create-zenstack/src/index.ts index 2099c42c..6dc8073d 100644 --- a/packages/create-zenstack/src/index.ts +++ b/packages/create-zenstack/src/index.ts @@ -66,8 +66,8 @@ function initProject(name: string) { // install packages const packages = [ - { name: '@zenstackhq/cli@next', dev: true }, - { name: '@zenstackhq/orm@next', dev: false }, + { name: '@zenstackhq/cli@latest', dev: true }, + { name: '@zenstackhq/orm@latest', dev: false }, { name: 'better-sqlite3', dev: false }, { name: '@types/better-sqlite3', dev: true }, { name: 'typescript', dev: true }, diff --git a/packages/language/package.json b/packages/language/package.json index c4b598dd..dc9cfc69 100644 --- a/packages/language/package.json +++ b/packages/language/package.json @@ -1,7 +1,7 @@ { "name": "@zenstackhq/language", "description": "ZenStack ZModel language specification", - "version": "3.0.0", + "version": "3.1.0", "license": "MIT", "author": "ZenStack Team", "files": [ diff --git a/packages/language/res/stdlib.zmodel b/packages/language/res/stdlib.zmodel index bbeafb07..81d52dc9 100644 --- a/packages/language/res/stdlib.zmodel +++ b/packages/language/res/stdlib.zmodel @@ -83,25 +83,25 @@ function now(): DateTime { /** * Generates a globally unique identifier based on the UUID specs. */ -function uuid(version: Int?): String { +function uuid(version: Int?, format: String?): String { } @@@expressionContext([DefaultValue]) /** * Generates a globally unique identifier based on the CUID spec. */ -function cuid(version: Int?): String { +function cuid(version: Int?, format: String?): String { } @@@expressionContext([DefaultValue]) /** * Generates an identifier based on the nanoid spec. */ -function nanoid(length: Int?): String { +function nanoid(length: Int?, format: String?): String { } @@@expressionContext([DefaultValue]) /** * Generates an identifier based on the ulid spec. */ -function ulid(): String { +function ulid(format: String?): String { } @@@expressionContext([DefaultValue]) /** diff --git a/packages/language/src/validators/function-invocation-validator.ts b/packages/language/src/validators/function-invocation-validator.ts index 19af7ef9..a8666565 100644 --- a/packages/language/src/validators/function-invocation-validator.ts +++ b/packages/language/src/validators/function-invocation-validator.ts @@ -87,6 +87,19 @@ export default class FunctionInvocationValidator implements AstValidator param.name === 'format'); + const formatArg = getLiteral(expr.args[formatParamIdx]?.value); + if ( + formatArg !== undefined && + !/(?(versionArg); + if (version !== undefined && version !== 4 && version !== 7) { + accept('error', 'first argument must be 4 or 7', { + node: expr.args[0]!, + }); + } + } + } + + @func('cuid') + private _checkCuid(expr: InvocationExpr, accept: ValidationAcceptor) { + // first argument must be 1 or 2 if provided + const versionArg = expr.args[0]?.value; + if (versionArg) { + const version = getLiteral(versionArg); + if (version !== undefined && version !== 1 && version !== 2) { + accept('error', 'first argument must be 1 or 2', { + node: expr.args[0]!, + }); + } + } + } + + @func('nanoid') + private _checkNanoid(expr: InvocationExpr, accept: ValidationAcceptor) { + // first argument must be positive if provided + const lengthArg = expr.args[0]?.value; + if (lengthArg) { + const length = getLiteral(lengthArg); + if (length !== undefined && length <= 0) { + accept('error', 'first argument must be a positive number', { + node: expr.args[0]!, + }); + } + } + } + @func('auth') private _checkAuth(expr: InvocationExpr, accept: ValidationAcceptor) { if (!expr.$resolvedType) { diff --git a/packages/language/test/function-invocation.test.ts b/packages/language/test/function-invocation.test.ts new file mode 100644 index 00000000..ff6bb45e --- /dev/null +++ b/packages/language/test/function-invocation.test.ts @@ -0,0 +1,417 @@ +import { describe, it } from 'vitest'; +import { loadSchema, loadSchemaWithError } from './utils'; + +describe('Function Invocation Tests', () => { + it('id functions should not require format strings', async () => { + await loadSchema( + ` + datasource db { + provider = 'sqlite' + url = 'file:./dev.db' + } + + model User { + id String @id @default(uuid()) + } + `, + ); + + await loadSchema( + ` + datasource db { + provider = 'sqlite' + url = 'file:./dev.db' + } + + model User { + id String @id @default(uuid(7)) + } + `, + ); + + await loadSchema( + ` + datasource db { + provider = 'sqlite' + url = 'file:./dev.db' + } + + model User { + id String @id @default(nanoid()) + } + `, + ); + + await loadSchema( + ` + datasource db { + provider = 'sqlite' + url = 'file:./dev.db' + } + + model User { + id String @id @default(nanoid(8)) + } + `, + ); + + await loadSchema( + ` + datasource db { + provider = 'sqlite' + url = 'file:./dev.db' + } + + model User { + id String @id @default(ulid()) + } + `, + ); + + await loadSchema( + ` + datasource db { + provider = 'sqlite' + url = 'file:./dev.db' + } + + model User { + id String @id @default(cuid()) + } + `, + ); + + await loadSchema( + ` + datasource db { + provider = 'sqlite' + url = 'file:./dev.db' + } + + model User { + id String @id @default(cuid(2)) + } + `, + ); + }); + + it('id functions should allow valid format strings', async () => { + await loadSchema( + ` + datasource db { + provider = 'sqlite' + url = 'file:./dev.db' + } + + model User { + id String @id @default(uuid(7, '%s_user')) + } + `, + ); + + await loadSchema( + ` + datasource db { + provider = 'sqlite' + url = 'file:./dev.db' + } + + model User { + id String @id @default(cuid(2, '%s')) + } + `, + ); + + await loadSchema( + ` + datasource db { + provider = 'sqlite' + url = 'file:./dev.db' + } + + model User { + id String @id @default(ulid('user_%s')) + } + `, + ); + + await loadSchema( + ` + datasource db { + provider = 'sqlite' + url = 'file:./dev.db' + } + + model User { + id String @id @default(nanoid(8, 'user_%s')) + } + `, + ); + + await loadSchema( + ` + datasource db { + provider = 'sqlite' + url = 'file:./dev.db' + } + + model User { + id String @id @default(uuid(7, '\\\\%s_%s')) + } + `, + ); + + await loadSchema( + ` + datasource db { + provider = 'sqlite' + url = 'file:./dev.db' + } + + model User { + id String @id @default(uuid(7, '%s_\\\\%s')) + } + `, + ); + }); + + it('id functions should reject invalid format strings', async () => { + await loadSchemaWithError( + ` + datasource db { + provider = 'sqlite' + url = 'file:./dev.db' + } + + model User { + id String @id @default(cuid(2, '')) + } + `, + 'argument must include', + ); + + await loadSchemaWithError( + ` + datasource db { + provider = 'sqlite' + url = 'file:./dev.db' + } + + model User { + id String @id @default(uuid(4, '\\\\%s')) + } + `, + 'argument must include', + ); + + await loadSchemaWithError( + ` + datasource db { + provider = 'sqlite' + url = 'file:./dev.db' + } + + model User { + id String @id @default(uuid(4, '\\\\%s\\\\%s')) + } + `, + 'argument must include', + ); + + await loadSchemaWithError( + ` + datasource db { + provider = 'sqlite' + url = 'file:./dev.db' + } + + model User { + id String @id @default(uuid(7, 'user_%')) + } + `, + 'argument must include', + ); + + await loadSchemaWithError( + ` + datasource db { + provider = 'sqlite' + url = 'file:./dev.db' + } + + model User { + id String @id @default(nanoid(8, 'user')) + } + `, + 'argument must include', + ); + + await loadSchemaWithError( + ` + datasource db { + provider = 'sqlite' + url = 'file:./dev.db' + } + + model User { + id String @id @default(ulid('user_%')) + } + `, + 'argument must include', + ); + + await loadSchemaWithError( + ` + datasource db { + provider = 'sqlite' + url = 'file:./dev.db' + } + + model User { + id String @id @default(cuid(2, 'user_%')) + } + `, + 'argument must include', + ); + }); + + describe('uuid() version validation', () => { + it('should accept valid uuid versions', async () => { + await loadSchema(` + datasource db { + provider = 'sqlite' + url = 'file:./dev.db' + } + + model User { + id String @id @default(uuid(4)) + } + `); + + await loadSchema(` + datasource db { + provider = 'sqlite' + url = 'file:./dev.db' + } + + model User { + id String @id @default(uuid(7)) + } + `); + }); + + it('should reject invalid uuid versions', async () => { + await loadSchemaWithError( + ` + datasource db { + provider = 'sqlite' + url = 'file:./dev.db' + } + + model User { + id String @id @default(uuid(1)) + } + `, + 'first argument must be 4 or 7', + ); + }); + }); + + describe('cuid() version validation', () => { + it('should accept valid cuid versions', async () => { + await loadSchema(` + datasource db { + provider = 'sqlite' + url = 'file:./dev.db' + } + + model User { + id String @id @default(cuid(1)) + } + `); + + await loadSchema(` + datasource db { + provider = 'sqlite' + url = 'file:./dev.db' + } + + model User { + id String @id @default(cuid(2)) + } + `); + }); + + it('should reject invalid cuid versions', async () => { + await loadSchemaWithError( + ` + datasource db { + provider = 'sqlite' + url = 'file:./dev.db' + } + + model User { + id String @id @default(cuid(0)) + } + `, + 'first argument must be 1 or 2', + ); + }); + }); + + describe('nanoid() length validation', () => { + it('should accept positive nanoid lengths', async () => { + await loadSchema(` + datasource db { + provider = 'sqlite' + url = 'file:./dev.db' + } + + model User { + id String @id @default(nanoid(1)) + } + `); + + await loadSchema(` + datasource db { + provider = 'sqlite' + url = 'file:./dev.db' + } + + model User { + id String @id @default(nanoid(21)) + } + `); + }); + + it('should reject non-positive nanoid lengths', async () => { + await loadSchemaWithError( + ` + datasource db { + provider = 'sqlite' + url = 'file:./dev.db' + } + + model User { + id String @id @default(nanoid(0)) + } + `, + 'first argument must be a positive number', + ); + + await loadSchemaWithError( + ` + datasource db { + provider = 'sqlite' + url = 'file:./dev.db' + } + + model User { + id String @id @default(nanoid(-1)) + } + `, + 'first argument must be a positive number', + ); + }); + }); +}); diff --git a/packages/orm/package.json b/packages/orm/package.json index d48d0beb..d7c16a10 100644 --- a/packages/orm/package.json +++ b/packages/orm/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/orm", - "version": "3.0.0", + "version": "3.1.0", "description": "ZenStack ORM", "type": "module", "scripts": { @@ -83,8 +83,9 @@ }, "dependencies": { "@paralleldrive/cuid2": "^2.2.2", - "@zenstackhq/schema": "workspace:*", "@zenstackhq/common-helpers": "workspace:*", + "@zenstackhq/schema": "workspace:*", + "cuid": "^3.0.0", "decimal.js": "catalog:", "json-stable-stringify": "^1.3.0", "kysely": "catalog:", @@ -98,8 +99,8 @@ "peerDependencies": { "better-sqlite3": "catalog:", "pg": "catalog:", - "zod": "catalog:", - "sql.js": "catalog:" + "sql.js": "catalog:", + "zod": "catalog:" }, "peerDependenciesMeta": { "better-sqlite3": { @@ -115,12 +116,12 @@ "devDependencies": { "@types/better-sqlite3": "catalog:", "@types/pg": "^8.0.0", + "@types/sql.js": "^1.4.9", "@types/toposort": "^2.0.7", "@zenstackhq/eslint-config": "workspace:*", "@zenstackhq/typescript-config": "workspace:*", "@zenstackhq/vitest-config": "workspace:*", "tsx": "^4.19.2", - "zod": "^4.1.0", - "@types/sql.js": "^1.4.9" + "zod": "^4.1.0" } } diff --git a/packages/orm/src/client/crud/operations/base.ts b/packages/orm/src/client/crud/operations/base.ts index 0ac2fe8c..54168951 100644 --- a/packages/orm/src/client/crud/operations/base.ts +++ b/packages/orm/src/client/crud/operations/base.ts @@ -1,5 +1,6 @@ -import { createId } from '@paralleldrive/cuid2'; +import { createId as cuid2 } from '@paralleldrive/cuid2'; import { clone, enumerate, invariant, isPlainObject } from '@zenstackhq/common-helpers'; +import { default as cuid1 } from 'cuid'; import { createQueryId, DeleteResult, @@ -859,23 +860,27 @@ export abstract class BaseOperationHandler { private evalGenerator(defaultValue: Expression) { if (ExpressionUtils.isCall(defaultValue)) { + const firstArgVal = + defaultValue.args?.[0] && ExpressionUtils.isLiteral(defaultValue.args[0]) + ? defaultValue.args[0].value + : undefined; return match(defaultValue.function) - .with('cuid', () => createId()) - .with('uuid', () => - defaultValue.args?.[0] && - ExpressionUtils.isLiteral(defaultValue.args?.[0]) && - defaultValue.args[0].value === 7 - ? uuid.v7() - : uuid.v4(), - ) - .with('nanoid', () => - defaultValue.args?.[0] && - ExpressionUtils.isLiteral(defaultValue.args[0]) && - typeof defaultValue.args[0].value === 'number' - ? nanoid(defaultValue.args[0].value) - : nanoid(), - ) - .with('ulid', () => ulid()) + .with('cuid', () => { + const version = firstArgVal; + const generated = version === 2 ? cuid2() : cuid1(); + return this.formatGeneratedValue(generated, defaultValue.args?.[1]); + }) + .with('uuid', () => { + const version = firstArgVal; + const generated = version === 7 ? uuid.v7() : uuid.v4(); + return this.formatGeneratedValue(generated, defaultValue.args?.[1]); + }) + .with('nanoid', () => { + const length = firstArgVal; + const generated = typeof length === 'number' ? nanoid(length) : nanoid(); + return this.formatGeneratedValue(generated, defaultValue.args?.[1]); + }) + .with('ulid', () => this.formatGeneratedValue(ulid(), defaultValue.args?.[0])) .otherwise(() => undefined); } else if ( ExpressionUtils.isMember(defaultValue) && @@ -893,6 +898,15 @@ export abstract class BaseOperationHandler { } } + private formatGeneratedValue(generated: string, formatExpr?: Expression) { + if (!formatExpr || !ExpressionUtils.isLiteral(formatExpr) || typeof formatExpr.value !== 'string') { + return generated; + } + + // Replace non-escaped %s with the generated value, then unescape \%s to %s + return formatExpr.value.replace(/(? columnNode), + valueNode.values, + ), + ); + } + + return super.transformBinaryOperation({ + ...node, + rightOperand: resultValue, + }); + } + } + + return super.transformBinaryOperation(node); + } + protected override transformUpdateQuery(node: UpdateQueryNode) { if (!node.table) { return super.transformUpdateQuery(node); diff --git a/packages/plugins/policy/package.json b/packages/plugins/policy/package.json index dc95282b..6f099d3e 100644 --- a/packages/plugins/policy/package.json +++ b/packages/plugins/policy/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/plugin-policy", - "version": "3.0.0", + "version": "3.1.0", "description": "ZenStack Policy Plugin", "type": "module", "scripts": { diff --git a/packages/schema/package.json b/packages/schema/package.json index 70dcf353..bb6e43f3 100644 --- a/packages/schema/package.json +++ b/packages/schema/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/schema", - "version": "3.0.0", + "version": "3.1.0", "description": "ZenStack Runtime Schema", "type": "module", "scripts": { diff --git a/packages/sdk/package.json b/packages/sdk/package.json index afe9f7e3..7b713c9d 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/sdk", - "version": "3.0.0", + "version": "3.1.0", "description": "ZenStack SDK", "type": "module", "scripts": { diff --git a/packages/server/package.json b/packages/server/package.json index 4abe793a..2add334b 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/server", - "version": "3.0.0", + "version": "3.1.0", "description": "ZenStack automatic CRUD API handlers and server adapters", "type": "module", "scripts": { @@ -147,7 +147,7 @@ "h3": "^1.15.4", "hono": "^4.6.3", "next": "catalog:", - "nuxt": "^4.2.0", + "nuxt": "catalog:", "supertest": "^7.1.4", "zod": "^4.1.0" }, diff --git a/packages/server/src/adapter/sveltekit/handler.ts b/packages/server/src/adapter/sveltekit/handler.ts index 84eb14bb..79038ea6 100644 --- a/packages/server/src/adapter/sveltekit/handler.ts +++ b/packages/server/src/adapter/sveltekit/handler.ts @@ -19,9 +19,10 @@ export interface SvelteKitHandlerOptions extends Commo } /** - * SvelteKit server hooks handler for handling CRUD requests. + * SvelteKit server hooks handler for handling CRUD requests. This handler is to be used in `hooks.server.ts`. + * @deprecated use `SvelteKitRouteHandler` instead. */ -export default function createHandler(options: SvelteKitHandlerOptions): Handle { +function createHandler(options: SvelteKitHandlerOptions): Handle { return async ({ event, resolve }) => { if (event.url.pathname.startsWith(options.prefix)) { const client = await options.getClient(event); diff --git a/packages/server/src/adapter/sveltekit/index.ts b/packages/server/src/adapter/sveltekit/index.ts index d50661d7..1dd60765 100644 --- a/packages/server/src/adapter/sveltekit/index.ts +++ b/packages/server/src/adapter/sveltekit/index.ts @@ -1 +1,2 @@ export { SvelteKitHandler, type SvelteKitHandlerOptions } from './handler'; +export { SvelteKitRouteHandler, type SvelteKitRouteHandlerOptions } from './route-handler'; diff --git a/packages/server/src/adapter/sveltekit/route-handler.ts b/packages/server/src/adapter/sveltekit/route-handler.ts new file mode 100644 index 00000000..4c90b995 --- /dev/null +++ b/packages/server/src/adapter/sveltekit/route-handler.ts @@ -0,0 +1,59 @@ +import { json, type RequestEvent, type RequestHandler } from '@sveltejs/kit'; +import type { ClientContract } from '@zenstackhq/orm'; +import type { SchemaDef } from '@zenstackhq/orm/schema'; +import { logInternalError, type CommonAdapterOptions } from '../common'; + +/** + * SvelteKit route handler options + */ +export interface SvelteKitRouteHandlerOptions extends CommonAdapterOptions { + /** + * Callback for getting a ZenStackClient for the given request event + */ + getClient: (event: RequestEvent) => ClientContract | Promise>; +} + +/** + * SvelteKit server route handler for handling CRUD requests. This handler is to be used in a `+server.ts` + * API route file. + */ +function createHandler(options: SvelteKitRouteHandlerOptions): RequestHandler { + return async (event) => { + const client = await options.getClient(event); + if (!client) { + return json({ message: 'unable to get ZenStackClient from request context' }, { status: 400 }); + } + + const query = Object.fromEntries(event.url.searchParams); + let requestBody: unknown; + if (event.request.body) { + try { + requestBody = await event.request.json(); + } catch { + return json({ message: 'invalid JSON payload' }, { status: 400 }); + } + } + + const path = event.params['path']; + if (!path) { + return json({ message: 'route is missing path parameter' }, { status: 400 }); + } + + try { + const r = await options.apiHandler.handleRequest({ + method: event.request.method, + path, + query, + requestBody, + client, + }); + + return json(r.body, { status: r.status }); + } catch (err) { + logInternalError(options.apiHandler.log, err); + return json({ message: 'An internal server error occurred' }, { status: 500 }); + } + }; +} + +export { createHandler as SvelteKitRouteHandler }; diff --git a/packages/server/src/api/rpc/index.ts b/packages/server/src/api/rpc/index.ts index e4e6ea64..e821366f 100644 --- a/packages/server/src/api/rpc/index.ts +++ b/packages/server/src/api/rpc/index.ts @@ -221,7 +221,7 @@ export class RPCApiHandler implements ApiH } private async processRequestPayload(args: any) { - const { meta, ...rest } = args; + const { meta, ...rest } = args ?? {}; if (meta?.serialization) { try { // superjson deserialization @@ -229,6 +229,9 @@ export class RPCApiHandler implements ApiH } catch (err) { return { result: undefined, error: `failed to deserialize request payload: ${(err as Error).message}` }; } + } else { + // drop meta when no serialization info is present + args = rest; } return { result: args, error: undefined }; } diff --git a/packages/server/test/api/rpc.test.ts b/packages/server/test/api/rpc.test.ts index 19e44ca0..4329e857 100644 --- a/packages/server/test/api/rpc.test.ts +++ b/packages/server/test/api/rpc.test.ts @@ -365,6 +365,7 @@ describe('RPC API Handler Tests', () => { float Float decimal Decimal boolean Boolean + stringList String[] bytes Bytes bars Bar[] } @@ -386,6 +387,7 @@ describe('RPC API Handler Tests', () => { const dateValue = new Date(); const bytesValue = new Uint8Array([1, 2, 3, 4]); const barBytesValue = new Uint8Array([7, 8, 9]); + const stringListValue = ['a', 'b', 'c']; const createData = { string: 'string', @@ -396,6 +398,7 @@ describe('RPC API Handler Tests', () => { decimal: decimalValue, boolean: true, bytes: bytesValue, + stringList: stringListValue, bars: { create: { bytes: barBytesValue }, }, @@ -424,6 +427,8 @@ describe('RPC API Handler Tests', () => { expect(data.date instanceof Date).toBeTruthy(); expect(Decimal.isDecimal(data.decimal)).toBeTruthy(); expect(data.bars[0].bytes).toBeInstanceOf(Uint8Array); + expect(Array.isArray(data.stringList)).toBeTruthy(); + expect(data.stringList).toEqual(stringListValue); // find with filter not found const serializedQ = SuperJSON.serialize({ @@ -506,6 +511,27 @@ describe('RPC API Handler Tests', () => { }); expect(r.status).toBe(200); expect(r.data).toBeNull(); + + // validate update on stringList + const serializedUpdate = SuperJSON.serialize({ + where: { id: 1 }, + data: { + stringList: ['d', 'e', 'f'], + }, + }); + r = await handleRequest({ + method: 'patch', + path: '/foo/update', + query: {}, + client, + requestBody: { + ...(serializedUpdate.json as any), + meta: { serialization: serializedUpdate.meta }, + }, + }); + expect(r.status).toBe(200); + expect(r.data).toBeTruthy(); + expect(r.data.stringList).toEqual(['d', 'e', 'f']); }); function makeHandler() { diff --git a/packages/testtools/package.json b/packages/testtools/package.json index a79a527c..cd836a07 100644 --- a/packages/testtools/package.json +++ b/packages/testtools/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/testtools", - "version": "3.0.0", + "version": "3.1.0", "description": "ZenStack Test Tools", "type": "module", "scripts": { diff --git a/packages/zod/package.json b/packages/zod/package.json index 47c1a71e..4907ed3a 100644 --- a/packages/zod/package.json +++ b/packages/zod/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/zod", - "version": "3.0.0", + "version": "3.1.0", "description": "", "type": "module", "main": "index.js", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4ee7c5e8..ba5f04d5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -6,9 +6,21 @@ settings: catalogs: default: + '@sveltejs/kit': + specifier: 2.49.1 + version: 2.49.1 + '@tanstack/query-core': + specifier: 5.90.2 + version: 5.90.2 '@tanstack/react-query': - specifier: 5.90.6 - version: 5.90.6 + specifier: 5.90.2 + version: 5.90.2 + '@tanstack/svelte-query': + specifier: 6.0.10 + version: 6.0.10 + '@tanstack/vue-query': + specifier: 5.90.2 + version: 5.90.2 '@types/better-sqlite3': specifier: ^7.6.13 version: 7.6.13 @@ -42,6 +54,9 @@ catalogs: next: specifier: 16.0.10 version: 16.0.10 + nuxt: + specifier: 4.2.2 + version: 4.2.2 pg: specifier: ^8.13.1 version: 8.16.3 @@ -58,8 +73,8 @@ catalogs: specifier: ^1.13.0 version: 1.13.0 svelte: - specifier: 5.43.3 - version: 5.43.3 + specifier: 5.45.6 + version: 5.45.6 tmp: specifier: ^0.2.3 version: 0.2.3 @@ -86,12 +101,18 @@ importers: '@types/node': specifier: 'catalog:' version: 20.19.24 + '@vitest/coverage-v8': + specifier: ^4.0.16 + version: 4.0.16(vitest@4.0.14(@edge-runtime/vm@5.0.0)(@types/node@20.19.24)(happy-dom@20.0.10)(jiti@2.6.1)(jsdom@27.1.0)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0)) eslint: specifier: ~9.29.0 version: 9.29.0(jiti@2.6.1) glob: specifier: ^11.1.0 version: 11.1.0 + npm-run-all: + specifier: ^4.1.5 + version: 4.1.5 prettier: specifier: ^3.5.3 version: 3.5.3 @@ -227,14 +248,11 @@ importers: specifier: 'catalog:' version: 0.2.3 - packages/clients/tanstack-query: + packages/clients/client-helpers: dependencies: '@zenstackhq/common-helpers': specifier: workspace:* version: link:../../common-helpers - '@zenstackhq/orm': - specifier: workspace:* - version: link:../../orm '@zenstackhq/schema': specifier: workspace:* version: link:../../schema @@ -245,15 +263,55 @@ importers: specifier: ^2.2.3 version: 2.2.3 devDependencies: + '@zenstackhq/eslint-config': + specifier: workspace:* + version: link:../../config/eslint-config + '@zenstackhq/language': + specifier: workspace:* + version: link:../../language + '@zenstackhq/orm': + specifier: workspace:* + version: link:../../orm + '@zenstackhq/sdk': + specifier: workspace:* + version: link:../../sdk + '@zenstackhq/typescript-config': + specifier: workspace:* + version: link:../../config/typescript-config + '@zenstackhq/vitest-config': + specifier: workspace:* + version: link:../../config/vitest-config + + packages/clients/tanstack-query: + dependencies: + '@zenstackhq/client-helpers': + specifier: workspace:* + version: link:../client-helpers + '@zenstackhq/common-helpers': + specifier: workspace:* + version: link:../../common-helpers + '@zenstackhq/schema': + specifier: workspace:* + version: link:../../schema + decimal.js: + specifier: 'catalog:' + version: 10.6.0 + devDependencies: + '@sveltejs/package': + specifier: ^2.5.7 + version: 2.5.7(svelte@5.45.6)(typescript@5.9.3) + '@tanstack/query-core': + specifier: 'catalog:' + version: 5.90.2 '@tanstack/react-query': specifier: 'catalog:' - version: 5.90.6(react@19.2.0) + version: 5.90.2(react@19.2.0) '@tanstack/svelte-query': - specifier: 5.90.2 - version: 5.90.2(svelte@5.43.3) + specifier: 'catalog:' + version: 6.0.10(svelte@5.45.6) '@tanstack/vue-query': - specifier: 5.90.6 - version: 5.90.6(vue@3.5.22(typescript@5.9.3)) + specifier: 'catalog:' + version: 5.90.2(vue@3.5.22(typescript@5.9.3)) '@testing-library/dom': specifier: ^10.4.1 version: 10.4.1 @@ -272,6 +330,9 @@ importers: '@zenstackhq/language': specifier: workspace:* version: link:../../language + '@zenstackhq/orm': + specifier: workspace:* + version: link:../../orm '@zenstackhq/sdk': specifier: workspace:* version: link:../../sdk @@ -292,7 +353,7 @@ importers: version: 19.2.0 svelte: specifier: 'catalog:' - version: 5.43.3 + version: 5.45.6 vue: specifier: 'catalog:' version: 3.5.22(typescript@5.9.3) @@ -305,6 +366,9 @@ importers: '@zenstackhq/typescript-config': specifier: workspace:* version: link:../config/typescript-config + '@zenstackhq/vitest-config': + specifier: workspace:* + version: link:../config/vitest-config packages/config/eslint-config: {} @@ -413,6 +477,9 @@ importers: better-sqlite3: specifier: 'catalog:' version: 12.5.0 + cuid: + specifier: ^3.0.0 + version: 3.0.0 decimal.js: specifier: 'catalog:' version: 10.6.0 @@ -579,7 +646,7 @@ importers: devDependencies: '@sveltejs/kit': specifier: ^2.48.3 - version: 2.48.3(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.43.3)(vite@7.1.12(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.1)))(svelte@5.43.3)(vite@7.1.12(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.1)) + version: 2.48.3(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.46.1)(vite@7.3.0(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.2)))(svelte@5.46.1)(vite@7.3.0(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.2)) '@types/body-parser': specifier: ^1.19.6 version: 1.19.6 @@ -626,8 +693,8 @@ importers: specifier: 'catalog:' version: 16.0.10(@babel/core@7.28.5)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) nuxt: - specifier: ^4.2.0 - version: 4.2.0(@parcel/watcher@2.5.1)(@types/node@20.19.24)(@vue/compiler-sfc@3.5.22)(better-sqlite3@12.5.0)(db0@0.3.4(better-sqlite3@12.5.0))(eslint@9.29.0(jiti@2.6.1))(ioredis@5.8.2)(lightningcss@1.30.2)(magicast@0.5.0)(optionator@0.9.4)(rollup@4.52.5)(terser@5.44.0)(tsx@4.20.3)(typescript@5.9.3)(vite@7.1.12(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.1))(yaml@2.8.1) + specifier: 'catalog:' + version: 4.2.2(@parcel/watcher@2.5.1)(@types/node@20.19.24)(@vue/compiler-sfc@3.5.26)(better-sqlite3@12.5.0)(cac@6.7.14)(db0@0.3.4(better-sqlite3@12.5.0))(eslint@9.29.0(jiti@2.6.1))(ioredis@5.8.2)(lightningcss@1.30.2)(magicast@0.5.1)(optionator@0.9.4)(rollup@4.52.5)(terser@5.44.0)(tsx@4.20.3)(typescript@5.9.3)(vite@7.3.0(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.2))(yaml@2.8.2) supertest: specifier: ^7.1.4 version: 7.1.4 @@ -722,7 +789,7 @@ importers: dependencies: '@tanstack/react-query': specifier: 'catalog:' - version: 5.90.6(react@19.2.0) + version: 5.90.2(react@19.2.0) '@zenstackhq/orm': specifier: workspace:* version: link:../../packages/orm @@ -782,6 +849,49 @@ importers: specifier: 'catalog:' version: 5.9.3 + samples/nuxt: + dependencies: + '@tailwindcss/vite': + specifier: ^4.1.18 + version: 4.1.18(vite@7.3.0(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.2)) + '@tanstack/vue-query': + specifier: 'catalog:' + version: 5.90.2(vue@3.5.22(typescript@5.9.3)) + '@zenstackhq/orm': + specifier: workspace:* + version: link:../../packages/orm + '@zenstackhq/server': + specifier: workspace:* + version: link:../../packages/server + '@zenstackhq/tanstack-query': + specifier: workspace:* + version: link:../../packages/clients/tanstack-query + better-sqlite3: + specifier: 'catalog:' + version: 12.5.0 + lorem-ipsum: + specifier: ^2.0.8 + version: 2.0.8 + nuxt: + specifier: 'catalog:' + version: 4.2.2(@parcel/watcher@2.5.1)(@types/node@20.19.24)(@vue/compiler-sfc@3.5.26)(better-sqlite3@12.5.0)(cac@6.7.14)(db0@0.3.4(better-sqlite3@12.5.0))(eslint@9.29.0(jiti@2.6.1))(ioredis@5.8.2)(lightningcss@1.30.2)(magicast@0.5.1)(optionator@0.9.4)(rollup@4.52.5)(terser@5.44.0)(tsx@4.20.3)(typescript@5.9.3)(vite@7.3.0(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.2))(yaml@2.8.2) + tailwindcss: + specifier: ^4.1.18 + version: 4.1.18 + vue: + specifier: 'catalog:' + version: 3.5.22(typescript@5.9.3) + vue-router: + specifier: ^4.6.4 + version: 4.6.4(vue@3.5.22(typescript@5.9.3)) + devDependencies: + '@types/better-sqlite3': + specifier: 'catalog:' + version: 7.6.13 + '@zenstackhq/cli': + specifier: workspace:* + version: link:../../packages/cli + samples/orm: dependencies: '@zenstackhq/orm': @@ -810,6 +920,73 @@ importers: specifier: 'catalog:' version: 6.19.0(magicast@0.3.5)(typescript@5.9.3) + samples/sveltekit: + dependencies: + '@tanstack/svelte-query': + specifier: 'catalog:' + version: 6.0.10(svelte@5.45.6) + '@zenstackhq/orm': + specifier: workspace:* + version: link:../../packages/orm + '@zenstackhq/server': + specifier: workspace:* + version: link:../../packages/server + '@zenstackhq/tanstack-query': + specifier: workspace:* + version: link:../../packages/clients/tanstack-query + better-sqlite3: + specifier: 'catalog:' + version: 12.5.0 + kysely: + specifier: 'catalog:' + version: 0.28.8 + lorem-ipsum: + specifier: ^2.0.8 + version: 2.0.8 + devDependencies: + '@sveltejs/adapter-auto': + specifier: ^7.0.0 + version: 7.0.0(@sveltejs/kit@2.49.1(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.45.6)(vite@7.3.0(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.2)))(svelte@5.45.6)(vite@7.3.0(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.2))) + '@sveltejs/kit': + specifier: 'catalog:' + version: 2.49.1(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.45.6)(vite@7.3.0(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.2)))(svelte@5.45.6)(vite@7.3.0(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.2)) + '@sveltejs/vite-plugin-svelte': + specifier: ^6.2.1 + version: 6.2.1(svelte@5.45.6)(vite@7.3.0(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.2)) + '@tailwindcss/vite': + specifier: ^4.1.17 + version: 4.1.18(vite@7.3.0(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.2)) + '@types/better-sqlite3': + specifier: 'catalog:' + version: 7.6.13 + '@types/node': + specifier: 'catalog:' + version: 20.19.24 + '@zenstackhq/cli': + specifier: workspace:* + version: link:../../packages/cli + prettier-plugin-tailwindcss: + specifier: ^0.7.2 + version: 0.7.2(prettier@3.5.3) + svelte: + specifier: 'catalog:' + version: 5.45.6 + svelte-check: + specifier: ^4.3.4 + version: 4.3.5(picomatch@4.0.3)(svelte@5.45.6)(typescript@5.9.3) + tailwindcss: + specifier: ^4.1.17 + version: 4.1.18 + tsx: + specifier: ^4.19.2 + version: 4.20.3 + typescript: + specifier: ^5.9.3 + version: 5.9.3 + vite: + specifier: ^7.2.6 + version: 7.3.0(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.2) + tests/e2e: dependencies: '@paralleldrive/cuid2': @@ -839,6 +1016,9 @@ importers: better-sqlite3: specifier: 'catalog:' version: 12.5.0 + cuid: + specifier: ^3.0.0 + version: 3.0.0 decimal.js: specifier: 'catalog:' version: 10.6.0 @@ -1100,6 +1280,10 @@ packages: resolution: {integrity: sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==} engines: {node: '>=6.9.0'} + '@bcoe/v8-coverage@1.0.2': + resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==} + engines: {node: '>=18'} + '@better-auth/core@1.3.34': resolution: {integrity: sha512-rt/Bgl0Xa8OQ2DUMKCZEJ8vL9kUw4NCJsBP9Sj9uRhbsK8NEMPiznUOFMkUY2FvrslvfKN7H/fivwyHz9c7HzQ==} peerDependencies: @@ -1119,6 +1303,21 @@ packages: '@better-fetch/fetch@1.1.18': resolution: {integrity: sha512-rEFOE1MYIsBmoMJtQbl32PGHHXuG2hDxvEd7rUHE0vCBoFQVSDqaVs9hkZEtHCxRoY+CljXKFCOuJ8uxqw1LcA==} + '@bomb.sh/tab@0.0.10': + resolution: {integrity: sha512-6ALS2rh/4LKn0Yxwm35V6LcgQuSiECHbqQo7+9g4rkgGyXZ0siOc8K+IuWIq/4u0Zkv2mevP9QSqgKhGIvLJMw==} + hasBin: true + peerDependencies: + cac: ^6.7.14 + citty: ^0.1.6 + commander: ^13.1.0 + peerDependenciesMeta: + cac: + optional: true + citty: + optional: true + commander: + optional: true + '@borewit/text-codec@0.1.1': resolution: {integrity: sha512-5L/uBxmjaCIX5h8Z+uu+kA9BQLkc/Wl06UGR5ajNRxu+/XjonB5i8JpgFMrPj3LXTCPA0pv8yxUvbUi+QthGGA==} @@ -1137,6 +1336,12 @@ packages: '@chevrotain/utils@11.0.3': resolution: {integrity: sha512-YslZMgtJUyuMbZ+aKvfF3x1f5liK4mWNxghFRv7jqRR9C3R3fAOGTTKvxXDa2Y1s9zSbcpuO0cAxDYsc9SrXoQ==} + '@clack/core@1.0.0-alpha.7': + resolution: {integrity: sha512-3vdh6Ar09D14rVxJZIm3VQJkU+ZOKKT5I5cC0cOVazy70CNyYYjiwRj9unwalhESndgxx6bGc/m6Hhs4EKF5XQ==} + + '@clack/prompts@1.0.0-alpha.8': + resolution: {integrity: sha512-YZGC4BmTKSF5OturNKEz/y4xNjYGmGk6NI785CQucJ7OEdX0qbMmL/zok+9bL6c7qE3WSYffyK5grh2RnkGNtQ==} + '@cloudflare/kv-asset-handler@0.4.0': resolution: {integrity: sha512-+tv3z+SPp+gqTIcImN9o0hqE9xyfQjI1XD9pL6NuKjua9B1y7mNYv0S9cP+QEbA4ppVgGZEmKOvHX5G5Ei1CVA==} engines: {node: '>=18.0.0'} @@ -1173,11 +1378,11 @@ packages: resolution: {integrity: sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==} engines: {node: '>=18'} - '@dxup/nuxt@0.2.0': - resolution: {integrity: sha512-tUS2040HEiGwjwZ8hTczfuRoiXSOuA+ATPXO9Bllf03nHHj1lSlmaAyVJHFsSXL5Os5NZqimNAZ1iDed7VElzA==} + '@dxup/nuxt@0.2.2': + resolution: {integrity: sha512-RNpJjDZs9+JcT9N87AnOuHsNM75DEd58itADNd/s1LIF6BZbTLZV0xxilJZb55lntn4TYvscTaXLCBX2fq9CXg==} - '@dxup/unimport@0.1.0': - resolution: {integrity: sha512-6Q/Po8qGmlrShdG/R9+rpIhme9N/PGJumpvmwr1UAxGpt9DfOCt9kF8+yJkxhtPdJFL37KgUILZBRAkSU8cJZg==} + '@dxup/unimport@0.1.2': + resolution: {integrity: sha512-/B8YJGPzaYq1NbsQmwgP8EZqg40NpTw4ZB3suuI0TplbxKHeK94jeaawLmVhCv+YwUnOpiWEz9U6SeThku/8JQ==} '@edge-runtime/primitives@6.0.0': resolution: {integrity: sha512-FqoxaBT+prPBHBwE1WXS1ocnu/VLTQyZ6NMUBAdbP7N2hsFTTxMC/jMu2D/8GAlMQfxeuppcPuCUk/HO3fpIvA==} @@ -1190,8 +1395,11 @@ packages: '@emnapi/core@1.6.0': resolution: {integrity: sha512-zq/ay+9fNIJJtJiZxdTnXS20PllcYMX3OE23ESc4HK/bdYu3cOWYVhsOhVnXALfU/uqJIxn5NBPd9z4v+SfoSg==} - '@emnapi/runtime@1.6.0': - resolution: {integrity: sha512-obtUmAHTMjll499P+D9A3axeJFlhdjOWdKUNs/U6QIGT7V5RjcUW1xToAzjvmgTSQhDbYn/NwfTRoJcQ2rNBxA==} + '@emnapi/core@1.7.1': + resolution: {integrity: sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg==} + + '@emnapi/runtime@1.7.1': + resolution: {integrity: sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==} '@emnapi/wasi-threads@1.1.0': resolution: {integrity: sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==} @@ -1208,6 +1416,12 @@ packages: cpu: [ppc64] os: [aix] + '@esbuild/aix-ppc64@0.27.2': + resolution: {integrity: sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + '@esbuild/android-arm64@0.25.11': resolution: {integrity: sha512-9slpyFBc4FPPz48+f6jyiXOx/Y4v34TUeDDXJpZqAWQn/08lKGeD8aDp9TMn9jDz2CiEuHwfhRmGBvpnd/PWIQ==} engines: {node: '>=18'} @@ -1220,6 +1434,12 @@ packages: cpu: [arm64] os: [android] + '@esbuild/android-arm64@0.27.2': + resolution: {integrity: sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + '@esbuild/android-arm@0.25.11': resolution: {integrity: sha512-uoa7dU+Dt3HYsethkJ1k6Z9YdcHjTrSb5NUy66ZfZaSV8hEYGD5ZHbEMXnqLFlbBflLsl89Zke7CAdDJ4JI+Gg==} engines: {node: '>=18'} @@ -1232,6 +1452,12 @@ packages: cpu: [arm] os: [android] + '@esbuild/android-arm@0.27.2': + resolution: {integrity: sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + '@esbuild/android-x64@0.25.11': resolution: {integrity: sha512-Sgiab4xBjPU1QoPEIqS3Xx+R2lezu0LKIEcYe6pftr56PqPygbB7+szVnzoShbx64MUupqoE0KyRlN7gezbl8g==} engines: {node: '>=18'} @@ -1244,6 +1470,12 @@ packages: cpu: [x64] os: [android] + '@esbuild/android-x64@0.27.2': + resolution: {integrity: sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + '@esbuild/darwin-arm64@0.25.11': resolution: {integrity: sha512-VekY0PBCukppoQrycFxUqkCojnTQhdec0vevUL/EDOCnXd9LKWqD/bHwMPzigIJXPhC59Vd1WFIL57SKs2mg4w==} engines: {node: '>=18'} @@ -1256,6 +1488,12 @@ packages: cpu: [arm64] os: [darwin] + '@esbuild/darwin-arm64@0.27.2': + resolution: {integrity: sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + '@esbuild/darwin-x64@0.25.11': resolution: {integrity: sha512-+hfp3yfBalNEpTGp9loYgbknjR695HkqtY3d3/JjSRUyPg/xd6q+mQqIb5qdywnDxRZykIHs3axEqU6l1+oWEQ==} engines: {node: '>=18'} @@ -1268,6 +1506,12 @@ packages: cpu: [x64] os: [darwin] + '@esbuild/darwin-x64@0.27.2': + resolution: {integrity: sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + '@esbuild/freebsd-arm64@0.25.11': resolution: {integrity: sha512-CmKjrnayyTJF2eVuO//uSjl/K3KsMIeYeyN7FyDBjsR3lnSJHaXlVoAK8DZa7lXWChbuOk7NjAc7ygAwrnPBhA==} engines: {node: '>=18'} @@ -1280,6 +1524,12 @@ packages: cpu: [arm64] os: [freebsd] + '@esbuild/freebsd-arm64@0.27.2': + resolution: {integrity: sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + '@esbuild/freebsd-x64@0.25.11': resolution: {integrity: sha512-Dyq+5oscTJvMaYPvW3x3FLpi2+gSZTCE/1ffdwuM6G1ARang/mb3jvjxs0mw6n3Lsw84ocfo9CrNMqc5lTfGOw==} engines: {node: '>=18'} @@ -1292,6 +1542,12 @@ packages: cpu: [x64] os: [freebsd] + '@esbuild/freebsd-x64@0.27.2': + resolution: {integrity: sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + '@esbuild/linux-arm64@0.25.11': resolution: {integrity: sha512-Qr8AzcplUhGvdyUF08A1kHU3Vr2O88xxP0Tm8GcdVOUm25XYcMPp2YqSVHbLuXzYQMf9Bh/iKx7YPqECs6ffLA==} engines: {node: '>=18'} @@ -1304,6 +1560,12 @@ packages: cpu: [arm64] os: [linux] + '@esbuild/linux-arm64@0.27.2': + resolution: {integrity: sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + '@esbuild/linux-arm@0.25.11': resolution: {integrity: sha512-TBMv6B4kCfrGJ8cUPo7vd6NECZH/8hPpBHHlYI3qzoYFvWu2AdTvZNuU/7hsbKWqu/COU7NIK12dHAAqBLLXgw==} engines: {node: '>=18'} @@ -1316,6 +1578,12 @@ packages: cpu: [arm] os: [linux] + '@esbuild/linux-arm@0.27.2': + resolution: {integrity: sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + '@esbuild/linux-ia32@0.25.11': resolution: {integrity: sha512-TmnJg8BMGPehs5JKrCLqyWTVAvielc615jbkOirATQvWWB1NMXY77oLMzsUjRLa0+ngecEmDGqt5jiDC6bfvOw==} engines: {node: '>=18'} @@ -1328,6 +1596,12 @@ packages: cpu: [ia32] os: [linux] + '@esbuild/linux-ia32@0.27.2': + resolution: {integrity: sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + '@esbuild/linux-loong64@0.25.11': resolution: {integrity: sha512-DIGXL2+gvDaXlaq8xruNXUJdT5tF+SBbJQKbWy/0J7OhU8gOHOzKmGIlfTTl6nHaCOoipxQbuJi7O++ldrxgMw==} engines: {node: '>=18'} @@ -1340,6 +1614,12 @@ packages: cpu: [loong64] os: [linux] + '@esbuild/linux-loong64@0.27.2': + resolution: {integrity: sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + '@esbuild/linux-mips64el@0.25.11': resolution: {integrity: sha512-Osx1nALUJu4pU43o9OyjSCXokFkFbyzjXb6VhGIJZQ5JZi8ylCQ9/LFagolPsHtgw6himDSyb5ETSfmp4rpiKQ==} engines: {node: '>=18'} @@ -1352,6 +1632,12 @@ packages: cpu: [mips64el] os: [linux] + '@esbuild/linux-mips64el@0.27.2': + resolution: {integrity: sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + '@esbuild/linux-ppc64@0.25.11': resolution: {integrity: sha512-nbLFgsQQEsBa8XSgSTSlrnBSrpoWh7ioFDUmwo158gIm5NNP+17IYmNWzaIzWmgCxq56vfr34xGkOcZ7jX6CPw==} engines: {node: '>=18'} @@ -1364,6 +1650,12 @@ packages: cpu: [ppc64] os: [linux] + '@esbuild/linux-ppc64@0.27.2': + resolution: {integrity: sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + '@esbuild/linux-riscv64@0.25.11': resolution: {integrity: sha512-HfyAmqZi9uBAbgKYP1yGuI7tSREXwIb438q0nqvlpxAOs3XnZ8RsisRfmVsgV486NdjD7Mw2UrFSw51lzUk1ww==} engines: {node: '>=18'} @@ -1376,6 +1668,12 @@ packages: cpu: [riscv64] os: [linux] + '@esbuild/linux-riscv64@0.27.2': + resolution: {integrity: sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + '@esbuild/linux-s390x@0.25.11': resolution: {integrity: sha512-HjLqVgSSYnVXRisyfmzsH6mXqyvj0SA7pG5g+9W7ESgwA70AXYNpfKBqh1KbTxmQVaYxpzA/SvlB9oclGPbApw==} engines: {node: '>=18'} @@ -1388,6 +1686,12 @@ packages: cpu: [s390x] os: [linux] + '@esbuild/linux-s390x@0.27.2': + resolution: {integrity: sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + '@esbuild/linux-x64@0.25.11': resolution: {integrity: sha512-HSFAT4+WYjIhrHxKBwGmOOSpphjYkcswF449j6EjsjbinTZbp8PJtjsVK1XFJStdzXdy/jaddAep2FGY+wyFAQ==} engines: {node: '>=18'} @@ -1400,6 +1704,12 @@ packages: cpu: [x64] os: [linux] + '@esbuild/linux-x64@0.27.2': + resolution: {integrity: sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + '@esbuild/netbsd-arm64@0.25.11': resolution: {integrity: sha512-hr9Oxj1Fa4r04dNpWr3P8QKVVsjQhqrMSUzZzf+LZcYjZNqhA3IAfPQdEh1FLVUJSiu6sgAwp3OmwBfbFgG2Xg==} engines: {node: '>=18'} @@ -1412,6 +1722,12 @@ packages: cpu: [arm64] os: [netbsd] + '@esbuild/netbsd-arm64@0.27.2': + resolution: {integrity: sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + '@esbuild/netbsd-x64@0.25.11': resolution: {integrity: sha512-u7tKA+qbzBydyj0vgpu+5h5AeudxOAGncb8N6C9Kh1N4n7wU1Xw1JDApsRjpShRpXRQlJLb9wY28ELpwdPcZ7A==} engines: {node: '>=18'} @@ -1424,6 +1740,12 @@ packages: cpu: [x64] os: [netbsd] + '@esbuild/netbsd-x64@0.27.2': + resolution: {integrity: sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + '@esbuild/openbsd-arm64@0.25.11': resolution: {integrity: sha512-Qq6YHhayieor3DxFOoYM1q0q1uMFYb7cSpLD2qzDSvK1NAvqFi8Xgivv0cFC6J+hWVw2teCYltyy9/m/14ryHg==} engines: {node: '>=18'} @@ -1436,6 +1758,12 @@ packages: cpu: [arm64] os: [openbsd] + '@esbuild/openbsd-arm64@0.27.2': + resolution: {integrity: sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + '@esbuild/openbsd-x64@0.25.11': resolution: {integrity: sha512-CN+7c++kkbrckTOz5hrehxWN7uIhFFlmS/hqziSFVWpAzpWrQoAG4chH+nN3Be+Kzv/uuo7zhX716x3Sn2Jduw==} engines: {node: '>=18'} @@ -1448,12 +1776,24 @@ packages: cpu: [x64] os: [openbsd] + '@esbuild/openbsd-x64@0.27.2': + resolution: {integrity: sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + '@esbuild/openharmony-arm64@0.25.11': resolution: {integrity: sha512-rOREuNIQgaiR+9QuNkbkxubbp8MSO9rONmwP5nKncnWJ9v5jQ4JxFnLu4zDSRPf3x4u+2VN4pM4RdyIzDty/wQ==} engines: {node: '>=18'} cpu: [arm64] os: [openharmony] + '@esbuild/openharmony-arm64@0.27.2': + resolution: {integrity: sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + '@esbuild/sunos-x64@0.25.11': resolution: {integrity: sha512-nq2xdYaWxyg9DcIyXkZhcYulC6pQ2FuCgem3LI92IwMgIZ69KHeY8T4Y88pcwoLIjbed8n36CyKoYRDygNSGhA==} engines: {node: '>=18'} @@ -1466,6 +1806,12 @@ packages: cpu: [x64] os: [sunos] + '@esbuild/sunos-x64@0.27.2': + resolution: {integrity: sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + '@esbuild/win32-arm64@0.25.11': resolution: {integrity: sha512-3XxECOWJq1qMZ3MN8srCJ/QfoLpL+VaxD/WfNRm1O3B4+AZ/BnLVgFbUV3eiRYDMXetciH16dwPbbHqwe1uU0Q==} engines: {node: '>=18'} @@ -1478,6 +1824,12 @@ packages: cpu: [arm64] os: [win32] + '@esbuild/win32-arm64@0.27.2': + resolution: {integrity: sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + '@esbuild/win32-ia32@0.25.11': resolution: {integrity: sha512-3ukss6gb9XZ8TlRyJlgLn17ecsK4NSQTmdIXRASVsiS2sQ6zPPZklNJT5GR5tE/MUarymmy8kCEf5xPCNCqVOA==} engines: {node: '>=18'} @@ -1490,6 +1842,12 @@ packages: cpu: [ia32] os: [win32] + '@esbuild/win32-ia32@0.27.2': + resolution: {integrity: sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + '@esbuild/win32-x64@0.25.11': resolution: {integrity: sha512-D7Hpz6A2L4hzsRpPaCYkQnGOotdUpDzSGRIv9I+1ITdHROSFUWW95ZPZWQmGka1Fg7W3zFJowyn9WGwMJ0+KPA==} engines: {node: '>=18'} @@ -1502,6 +1860,12 @@ packages: cpu: [x64] os: [win32] + '@esbuild/win32-x64@0.27.2': + resolution: {integrity: sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + '@eslint-community/eslint-utils@4.7.0': resolution: {integrity: sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -1784,8 +2148,8 @@ packages: '@napi-rs/wasm-runtime@0.2.12': resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==} - '@napi-rs/wasm-runtime@1.0.7': - resolution: {integrity: sha512-SeDnOO0Tk7Okiq6DbXmmBODgOAb9dp9gjlphokTUxmt8U3liIP1ZsozBahH69j/RJv+Rfs6IwUKHTgQYJ/HBAw==} + '@napi-rs/wasm-runtime@1.1.0': + resolution: {integrity: sha512-Fq6DJW+Bb5jaWE69/qOE0D1TUN9+6uWhCeZpdnSBk14pjLcCWR7Q8n49PTSPHazM37JqrsdpEthXy2xn6jWWiA==} '@next/env@16.0.10': resolution: {integrity: sha512-8tuaQkyDVgeONQ1MeT9Mkk8pQmZapMKFh5B+OrFUlG3rVmYTXcXlBetBgTurKXGaIZvkoqRT9JL5K3phXcgang==} @@ -1869,45 +2233,49 @@ packages: resolution: {integrity: sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==} engines: {node: '>=12.4.0'} - '@nuxt/cli@3.29.3': - resolution: {integrity: sha512-48GYmH4SyzR5pqd02UXVzBfrvEGaurPKMjSWxlHgqnpI5buwOYCvH+OqvHOmvnLrDP2bxR9hbDod/UIphOjMhg==} + '@nuxt/cli@3.31.3': + resolution: {integrity: sha512-K0T1ZpBXnlb41NU/RWf1F0U0C14KzlEXCoaSgD2y8BiLoCBWcgQ1UAlRtx4cThqWbJmIxaNZZTDL0NZ9d1U7ag==} engines: {node: ^16.10.0 || >=18.0.0} hasBin: true '@nuxt/devalue@2.0.2': resolution: {integrity: sha512-GBzP8zOc7CGWyFQS6dv1lQz8VVpz5C2yRszbXufwG/9zhStTIH50EtD87NmWbTMwXDvZLNg8GIpb1UFdH93JCA==} - '@nuxt/devtools-kit@2.7.0': - resolution: {integrity: sha512-MIJdah6CF6YOW2GhfKnb8Sivu6HpcQheqdjOlZqShBr+1DyjtKQbAKSCAyKPaoIzZP4QOo2SmTFV6aN8jBeEIQ==} + '@nuxt/devtools-kit@3.1.1': + resolution: {integrity: sha512-sjiKFeDCOy1SyqezSgyV4rYNfQewC64k/GhOsuJgRF+wR2qr6KTVhO6u2B+csKs74KrMrnJprQBgud7ejvOXAQ==} peerDependencies: vite: '>=6.0' - '@nuxt/devtools-wizard@2.7.0': - resolution: {integrity: sha512-iWuWR0U6BRpF7D6xrgq9ZkQ6ajsw2EA/gVmbU9V5JPKRUtV6DVpCPi+h34VFNeQ104Sf531XgvT0sl3h93AjXA==} + '@nuxt/devtools-wizard@3.1.1': + resolution: {integrity: sha512-6UORjapNKko2buv+3o57DQp69n5Z91TeJ75qdtNKcTvOfCTJrO78Ew0nZSgMMGrjbIJ4pFsHQEqXfgYLw3pNxg==} hasBin: true - '@nuxt/devtools@2.7.0': - resolution: {integrity: sha512-BtIklVYny14Ykek4SHeexAHoa28MEV9kz223ZzvoNYqE0f+YVV+cJP69ovZHf+HUVpxaAMJfWKLHXinWXiCZ4Q==} + '@nuxt/devtools@3.1.1': + resolution: {integrity: sha512-UG8oKQqcSyzwBe1l0z24zypmwn6FLW/HQMHK/F/gscUU5LeMHzgBhLPD+cuLlDvwlGAbifexWNMsS/I7n95KlA==} hasBin: true peerDependencies: + '@vitejs/devtools': '*' vite: '>=6.0' + peerDependenciesMeta: + '@vitejs/devtools': + optional: true '@nuxt/kit@3.20.0': resolution: {integrity: sha512-EoF1Gf0SPj9vxgAIcGEH+a4PRLC7Dwsy21K6f5+POzylT8DgssN8zL5pwXC+X7OcfzBrwYFh7mM7phvh7ubgeg==} engines: {node: '>=18.12.0'} - '@nuxt/kit@4.2.0': - resolution: {integrity: sha512-1yN3LL6RDN5GjkNLPUYCbNRkaYnat6hqejPyfIBBVzrWOrpiQeNMGxQM/IcVdaSuBJXAnu0sUvTKXpXkmPhljg==} + '@nuxt/kit@4.2.2': + resolution: {integrity: sha512-ZAgYBrPz/yhVgDznBNdQj2vhmOp31haJbO0I0iah/P9atw+OHH7NJLUZ3PK+LOz/0fblKTN1XJVSi8YQ1TQ0KA==} engines: {node: '>=18.12.0'} - '@nuxt/nitro-server@4.2.0': - resolution: {integrity: sha512-1fZwAV+VTQwmPVUYKH+eoeB+3jPE+c/mreK3PpuY6vvrIDuMh9L4QIeLFB0fIcY2MJ4XkvjU/5w3B9uu3GR9yQ==} + '@nuxt/nitro-server@4.2.2': + resolution: {integrity: sha512-lDITf4n5bHQ6a5MO7pvkpdQbPdWAUgSvztSHCfui/3ioLZsM2XntlN02ue6GSoh3oV9H4xSB3qGa+qlSjgxN0A==} engines: {node: ^20.19.0 || >=22.12.0} peerDependencies: - nuxt: ^4.2.0 + nuxt: ^4.2.2 - '@nuxt/schema@4.2.0': - resolution: {integrity: sha512-YMbgpEyPokgOYME6BvY8Okk7GAIwhEFYzrkkkoU9IVgu0tKWetYRrjUwbd0eICqPm9EWDBQl5tTTNJ8xCndVbw==} + '@nuxt/schema@4.2.2': + resolution: {integrity: sha512-lW/1MNpO01r5eR/VoeanQio8Lg4QpDklMOHa4mBHhhPNlBO1qiRtVYzjcnNdun3hujGauRaO9khGjv93Z5TZZA==} engines: {node: ^14.18.0 || >=16.10.0} '@nuxt/telemetry@2.6.6': @@ -1915,11 +2283,11 @@ packages: engines: {node: '>=18.12.0'} hasBin: true - '@nuxt/vite-builder@4.2.0': - resolution: {integrity: sha512-pNHIoO8kiSsOnoMo2zmxy0mk71ZBP4KJCiXr7Ahq8ewOm4W4vFQ1NV1O46wJGZyxlPC6nqFuYBvcUwVp1LgTNg==} + '@nuxt/vite-builder@4.2.2': + resolution: {integrity: sha512-Bot8fpJNtHZrM4cS1iSR7bEAZ1mFLAtJvD/JOSQ6kT62F4hSFWfMubMXOwDkLK2tnn3bnAdSqGy1nLNDBCahpQ==} engines: {node: ^20.19.0 || >=22.12.0} peerDependencies: - nuxt: 4.2.0 + nuxt: 4.2.2 rolldown: ^1.0.0-beta.38 vue: ^3.3.4 peerDependenciesMeta: @@ -1935,272 +2303,272 @@ packages: '@open-draft/until@2.1.0': resolution: {integrity: sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==} - '@oxc-minify/binding-android-arm64@0.95.0': - resolution: {integrity: sha512-ck0NakTt3oBWTMQjxKf5ZW1GzCs0y1kETzJdh8h8NAWTutlMfeWiuUxCgG4FMF4XiTnCdLq/dFAKFcdbiwcoqg==} + '@oxc-minify/binding-android-arm64@0.102.0': + resolution: {integrity: sha512-pknM+ttJTwRr7ezn1v5K+o2P4RRjLAzKI10bjVDPybwWQ544AZW6jxm7/YDgF2yUbWEV9o7cAQPkIUOmCiW8vg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - '@oxc-minify/binding-darwin-arm64@0.95.0': - resolution: {integrity: sha512-uvRkBVsh88DgMqddCIHcL1tKycKThfzLHNuBOm7csfpOD85TJimpl/1qAfrTCNrdaiteFK4U9QRKBdDvZay4RQ==} + '@oxc-minify/binding-darwin-arm64@0.102.0': + resolution: {integrity: sha512-BDLiH41ZctNND38+GCEL3ZxFn9j7qMZJLrr6SLWMt8xlG4Sl64xTkZ0zeUy4RdVEatKKZdrRIhFZ2e5wPDQT6Q==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@oxc-minify/binding-darwin-x64@0.95.0': - resolution: {integrity: sha512-SpDArHPKy/K9rduOCdlqz4BxFZte5Ad4/CPNaP0EaVTNbDW1OjBMrVOzRxr/bveWUbUJW3gbWby//YzXCese/w==} + '@oxc-minify/binding-darwin-x64@0.102.0': + resolution: {integrity: sha512-AcB8ZZ711w4hTDhMfMHNjT2d+hekTQ2XmNSUBqJdXB+a2bJbE50UCRq/nxXl44zkjaQTit3lcQbFvhk2wwKcpw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@oxc-minify/binding-freebsd-x64@0.95.0': - resolution: {integrity: sha512-U/ER7VsDCOv9HTE3rIZmNdN2ijZTT1vjDPPRsl9Z5Zyip2OsbHJxh4iNC00bO7qSw5keADuP4ooXsu2pjnfXNA==} + '@oxc-minify/binding-freebsd-x64@0.102.0': + resolution: {integrity: sha512-UlLEN9mR5QaviYVMWZQsN9DgAH3qyV67XUXDEzSrbVMLsqHsVHhFU8ZIeO0fxWTQW/cgpvldvKp9/+RdrggqWw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - '@oxc-minify/binding-linux-arm-gnueabihf@0.95.0': - resolution: {integrity: sha512-g+u5Zg72J7G9DbjnCIO6BhHE4lSaODLFjArFq9sZWu4xi4QOYapGdNZVbQWrWjzGlKTvYOhH621ySMOc07O64g==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm] - os: [linux] - - '@oxc-minify/binding-linux-arm-musleabihf@0.95.0': - resolution: {integrity: sha512-RqQctWyvgSVkJ+UMhDPLDjSO+YjAWFGoSfvikgEIvGrTVjFzXz20EDFSH+CR9J+mXsuJOku63VKmcAZr8Vd/Qg==} + '@oxc-minify/binding-linux-arm-gnueabihf@0.102.0': + resolution: {integrity: sha512-CWyCwedZrUt47n56/RwHSwKXxVI3p98hB0ntLaBNeH5qjjBujs9uOh4bQ0aAlzUWunT77b3/Y+xcQnmV42HN4A==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@oxc-minify/binding-linux-arm64-gnu@0.95.0': - resolution: {integrity: sha512-psrzacTaa5zmRXm2Skooj5YOZvueFZLOjNDAkwQcjIgrVAzl7uXtDCPq8soM46O12wGXMpDNUkrbD2BVcF+S9g==} + '@oxc-minify/binding-linux-arm64-gnu@0.102.0': + resolution: {integrity: sha512-W/DCw+Ys8rXj4j38ylJ2l6Kvp6SV+eO5SUWA11imz7yCWntNL001KJyGQ9PJNUFHg0jbxe3yqm4M50v6miWzeA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] - '@oxc-minify/binding-linux-arm64-musl@0.95.0': - resolution: {integrity: sha512-W5VWcOTIxH8bvIviiFreNHK5RkaNE7Y7hm0fxYa9pAdDe8U2OnD77JPPHmNSKYROaDa1ZsmXK1dAOnwGcxvv1w==} + '@oxc-minify/binding-linux-arm64-musl@0.102.0': + resolution: {integrity: sha512-DyH/t/zSZHuX4Nn239oBteeMC4OP7B13EyXWX18Qg8aJoZ+lZo90WPGOvhP04zII33jJ7di+vrtAUhsX64lp+A==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] - '@oxc-minify/binding-linux-riscv64-gnu@0.95.0': - resolution: {integrity: sha512-FBAaIvTcRqdXDPZAsfEBc5nK3noZtEAO82090ne5EDsDNKu8u8sjLhXYJWM3AZFD6p7OPRqBby6N4pVicrk0dA==} + '@oxc-minify/binding-linux-riscv64-gnu@0.102.0': + resolution: {integrity: sha512-CMvzrmOg+Gs44E7TRK/IgrHYp+wwVJxVV8niUrDR2b3SsrCO3NQz5LI+7bM1qDbWnuu5Cl1aiitoMfjRY61dSg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] - '@oxc-minify/binding-linux-s390x-gnu@0.95.0': - resolution: {integrity: sha512-7/OWwUC3r0/nPsHOCsTkgitdjpvDOwm8f4lE/Xeigt+9EcRcVuaSHRVOHI47mQ/cSL6V3AObVcmiAGysR36vEw==} + '@oxc-minify/binding-linux-s390x-gnu@0.102.0': + resolution: {integrity: sha512-tZWr6j2s0ddm9MTfWTI3myaAArg9GDy4UgvpF00kMQAjLcGUNhEEQbB9Bd9KtCvDQzaan8HQs0GVWUp+DWrymw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] - '@oxc-minify/binding-linux-x64-gnu@0.95.0': - resolution: {integrity: sha512-3K2lxzk679ml1vXJtO8Nt3xMD2trnDQWBb4Q676Un5g3dbaYf1WgTmEI13ZnCrwE5uBI02DFtFQplkLFqb9dGA==} + '@oxc-minify/binding-linux-x64-gnu@0.102.0': + resolution: {integrity: sha512-0YEKmAIun1bS+Iy5Shx6WOTSj3GuilVuctJjc5/vP8/EMTZ/RI8j0eq0Mu3UFPoT/bMULL3MBXuHuEIXmq7Ddg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] - '@oxc-minify/binding-linux-x64-musl@0.95.0': - resolution: {integrity: sha512-DrxQAALZs/He11OlCWZrJGsdwGSAK61nkZxcl3MnO33mL54Qs/vI9AbI2lMtggU+xB2sNKbjKTTpTbCPHOmhTA==} + '@oxc-minify/binding-linux-x64-musl@0.102.0': + resolution: {integrity: sha512-Ew4QDpEsXoV+pG5+bJpheEy3GH436GBe6ASPB0X27Hh9cQ2gb1NVZ7cY7xJj68+fizwS/PtT8GHoG3uxyH17Pg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] - '@oxc-minify/binding-wasm32-wasi@0.95.0': - resolution: {integrity: sha512-PASXKqJyLHesNjTweXqkA3kG/hdjpauGb+REP5yZ4dr8gxu5DbMqk4QjsBmW3LjDF4tXXjRs8nHR6Qt2dhxTzA==} + '@oxc-minify/binding-openharmony-arm64@0.102.0': + resolution: {integrity: sha512-wYPXS8IOu/sXiP3CGHJNPzZo4hfPAwJKevcFH2syvU2zyqUxym7hx6smfcK/mgJBiX7VchwArdGRwrEQKcBSaQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@oxc-minify/binding-wasm32-wasi@0.102.0': + resolution: {integrity: sha512-52SepCb9e+8cVisGa9S/F14K8PxW0AnbV1j4KEYi8uwfkUIxeDNKRHVHzPoBXNrr0yxW0EHLn/3i8J7a2YCpWw==} engines: {node: '>=14.0.0'} cpu: [wasm32] - '@oxc-minify/binding-win32-arm64-msvc@0.95.0': - resolution: {integrity: sha512-fPVQZWObqqBRYedFy/bOI0UzUZCqq6ra/PBZFqi31c5Zn73ETTseLYL7ebQqKgjv8l9gQPBIAFIoXYsaoxT72A==} + '@oxc-minify/binding-win32-arm64-msvc@0.102.0': + resolution: {integrity: sha512-kLs6H1y6sDBKcIimkNwu5th28SLkyvFpHNxdLtCChda0KIGeIXNSiupy5BqEutY+VlWJivKT1OV3Ev3KC5Euzg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@oxc-minify/binding-win32-x64-msvc@0.95.0': - resolution: {integrity: sha512-mtCkksnBcO4dIxuj1n9THbMihV+zjO7ZIVCPOq54pylA+hTb/OHau3OV+XyU0pnmREGTuF9xV3BUKag1SYS/lQ==} + '@oxc-minify/binding-win32-x64-msvc@0.102.0': + resolution: {integrity: sha512-XdyJZdSMN8rbBXH10CrFuU+Q9jIP2+MnxHmNzjK4+bldbTI1UxqwjUMS9bKVC5VCaIEZhh8IE8x4Vf8gmCgrKQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] - '@oxc-parser/binding-android-arm64@0.95.0': - resolution: {integrity: sha512-dZyxhhvJigwWL1wgnLlqyEiSeuqO0WdDH9H+if0dPcBM4fKa5fjVkaUcJT1jBMcBTnkjxMwTXYZy5TK60N0fjg==} + '@oxc-parser/binding-android-arm64@0.102.0': + resolution: {integrity: sha512-pD2if3w3cxPvYbsBSTbhxAYGDaG6WVwnqYG0mYRQ142D6SJ6BpNs7YVQrqpRA2AJQCmzaPP5TRp/koFLebagfQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - '@oxc-parser/binding-darwin-arm64@0.95.0': - resolution: {integrity: sha512-zun9+V33kyCtNec9oUSWwb0qi3fB8pXwum1yGFECPEr55g/CrWju6/Jv4hwwNBeW2tK9Ch/PRstEtYmOLMhHpg==} + '@oxc-parser/binding-darwin-arm64@0.102.0': + resolution: {integrity: sha512-RzMN6f6MrjjpQC2Dandyod3iOscofYBpHaTecmoRRbC5sJMwsurkqUMHzoJX9F6IM87kn8m/JcClnoOfx5Sesw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@oxc-parser/binding-darwin-x64@0.95.0': - resolution: {integrity: sha512-9djMQ/t6Ns/UXtziwUe562uVJMbhtuLtCj+Xav+HMVT/rhV9gWO8PQOG7AwDLUBjJanItsrfqrGtqhNxtZ701w==} + '@oxc-parser/binding-darwin-x64@0.102.0': + resolution: {integrity: sha512-Sr2/3K6GEcejY+HgWp5HaxRPzW5XHe9IfGKVn9OhLt8fzVLnXbK5/GjXj7JjMCNKI3G3ZPZDG2Dgm6CX3MaHCA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@oxc-parser/binding-freebsd-x64@0.95.0': - resolution: {integrity: sha512-GK6k0PgCVkkeRZtHgcosCYbXIRySpJpuPw/OInfLGFh8f3x9gp2l8Fbcfx+YO+ZOHFBCd2NNedGqw8wMgouxfA==} + '@oxc-parser/binding-freebsd-x64@0.102.0': + resolution: {integrity: sha512-s9F2N0KJCGEpuBW6ChpFfR06m2Id9ReaHSl8DCca4HvFNt8SJFPp8fq42n2PZy68rtkremQasM0JDrK2BoBeBQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - '@oxc-parser/binding-linux-arm-gnueabihf@0.95.0': - resolution: {integrity: sha512-+g/lFITtyHHEk69cunOHuiT5cX+mpUTcbGYNe8suguZ7FqgNwai+PnGv0ctCvtgxBPVfckfUK8c3RvFKo+vi/w==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm] - os: [linux] - - '@oxc-parser/binding-linux-arm-musleabihf@0.95.0': - resolution: {integrity: sha512-SXNasDtPw8ycNV7VEvFxb4LETmykvWKUhHR7K3us818coXYpDj54P8WEx8hJobP/9skuuiFuKHmtYLdjX8wntA==} + '@oxc-parser/binding-linux-arm-gnueabihf@0.102.0': + resolution: {integrity: sha512-zRCIOWzLbqhfY4g8KIZDyYfO2Fl5ltxdQI1v2GlePj66vFWRl8cf4qcBGzxKfsH3wCZHAhmWd1Ht59mnrfH/UQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@oxc-parser/binding-linux-arm64-gnu@0.95.0': - resolution: {integrity: sha512-0LzebARTU0ROfD6pDK4h1pFn+09meErCZ0MA2TaW08G72+GNneEsksPufOuI+9AxVSRa+jKE3fu0wavvhZgSkg==} + '@oxc-parser/binding-linux-arm64-gnu@0.102.0': + resolution: {integrity: sha512-5n5RbHgfjulRhKB0pW5p0X/NkQeOpI4uI9WHgIZbORUDATGFC8yeyPA6xYGEs+S3MyEAFxl4v544UEIWwqAgsA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] - '@oxc-parser/binding-linux-arm64-musl@0.95.0': - resolution: {integrity: sha512-Pvi1lGe/G+mJZ3hUojMP/aAHAzHA25AEtVr8/iuz7UV72t/15NOgJYr9kELMUMNjPqpr3vKUgXTFmTtAxp11Qw==} + '@oxc-parser/binding-linux-arm64-musl@0.102.0': + resolution: {integrity: sha512-/XWcmglH/VJ4yKAGTLRgPKSSikh3xciNxkwGiURt8dS30b+3pwc4ZZmudMu0tQ3mjSu0o7V9APZLMpbHK8Bp5w==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] - '@oxc-parser/binding-linux-riscv64-gnu@0.95.0': - resolution: {integrity: sha512-pUEVHIOVNDfhk4sTlLhn6mrNENhE4/dAwemxIfqpcSyBlYG0xYZND1F3jjR2yWY6DakXZ6VSuDbtiv1LPNlOLw==} + '@oxc-parser/binding-linux-riscv64-gnu@0.102.0': + resolution: {integrity: sha512-2jtIq4nswvy6xdqv1ndWyvVlaRpS0yqomLCvvHdCFx3pFXo5Aoq4RZ39kgvFWrbAtpeYSYeAGFnwgnqjx9ftdw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] - '@oxc-parser/binding-linux-s390x-gnu@0.95.0': - resolution: {integrity: sha512-5+olaepHTE3J/+w7g0tr3nocvv5BKilAJnzj4L8tWBCLEZbL6olJcGVoldUO+3cgg1SO1xJywP5BuLhT0mDUDw==} + '@oxc-parser/binding-linux-s390x-gnu@0.102.0': + resolution: {integrity: sha512-Yp6HX/574mvYryiqj0jNvNTJqo4pdAsNP2LPBTxlDQ1cU3lPd7DUA4MQZadaeLI8+AGB2Pn50mPuPyEwFIxeFg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] - '@oxc-parser/binding-linux-x64-gnu@0.95.0': - resolution: {integrity: sha512-8huzHlK/N98wrnYKxIcYsK8ZGBWomQchu/Mzi6m+CtbhjWOv9DmK0jQ2fUWImtluQVpTwS0uZT06d3g7XIkJrA==} + '@oxc-parser/binding-linux-x64-gnu@0.102.0': + resolution: {integrity: sha512-R4b0xZpDRhoNB2XZy0kLTSYm0ZmWeKjTii9fcv1Mk3/SIGPrrglwt4U6zEtwK54Dfi4Bve5JnQYduigR/gyDzw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] - '@oxc-parser/binding-linux-x64-musl@0.95.0': - resolution: {integrity: sha512-bWnrLfGDcx/fab0+UQnFbVFbiykof/btImbYf+cI2pU/1Egb2x+OKSmM5Qt0nEUiIpM5fgJmYXxTopybSZOKYA==} + '@oxc-parser/binding-linux-x64-musl@0.102.0': + resolution: {integrity: sha512-xM5A+03Ti3jvWYZoqaBRS3lusvnvIQjA46Fc9aBE/MHgvKgHSkrGEluLWg/33QEwBwxupkH25Pxc1yu97oZCtg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] - '@oxc-parser/binding-wasm32-wasi@0.95.0': - resolution: {integrity: sha512-0JLyqkZu1HnQIZ4e5LBGOtzqua1QwFEUOoMSycdoerXqayd4LK2b7WMfAx8eCIf+jGm1Uj6f3R00nlsx8g1faQ==} + '@oxc-parser/binding-openharmony-arm64@0.102.0': + resolution: {integrity: sha512-AieLlsliblyaTFq7Iw9Nc618tgwV02JT4fQ6VIUd/3ZzbluHIHfPjIXa6Sds+04krw5TvCS8lsegtDYAyzcyhg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@oxc-parser/binding-wasm32-wasi@0.102.0': + resolution: {integrity: sha512-w6HRyArs1PBb9rDsQSHlooe31buUlUI2iY8sBzp62jZ1tmvaJo9EIVTQlRNDkwJmk9DF9uEyIJ82EkZcCZTs9A==} engines: {node: '>=14.0.0'} cpu: [wasm32] - '@oxc-parser/binding-win32-arm64-msvc@0.95.0': - resolution: {integrity: sha512-RWvaA6s1SYlBj9CxwHfNn0CRlkPdv9fEUAXfZkGQPdP5e1ppIaO2KYE0sUov/zzp9hPTMMsTMHl4dcIbb+pHCQ==} + '@oxc-parser/binding-win32-arm64-msvc@0.102.0': + resolution: {integrity: sha512-pqP5UuLiiFONQxqGiUFMdsfybaK1EOK4AXiPlvOvacLaatSEPObZGpyCkAcj9aZcvvNwYdeY9cxGM9IT3togaA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@oxc-parser/binding-win32-x64-msvc@0.95.0': - resolution: {integrity: sha512-BQpgl7rDjFvCIHudmUR0dCwc4ylBYZl4CPVinlD3NhkMif4WD5dADckoo5ES/KOpFyvwcbKZX+grP63cjHi26g==} + '@oxc-parser/binding-win32-x64-msvc@0.102.0': + resolution: {integrity: sha512-ntMcL35wuLR1A145rLSmm7m7j8JBZGkROoB9Du0KFIFcfi/w1qk75BdCeiTl3HAKrreAnuhW3QOGs6mJhntowA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] - '@oxc-project/types@0.95.0': - resolution: {integrity: sha512-vACy7vhpMPhjEJhULNxrdR0D943TkA/MigMpJCHmBHvMXxRStRi/dPtTlfQ3uDwWSzRpT8z+7ImjZVf8JWBocQ==} + '@oxc-project/types@0.102.0': + resolution: {integrity: sha512-8Skrw405g+/UJPKWJ1twIk3BIH2nXdiVlVNtYT23AXVwpsd79es4K+KYt06Fbnkc5BaTvk/COT2JuCLYdwnCdA==} - '@oxc-transform/binding-android-arm64@0.95.0': - resolution: {integrity: sha512-eW+BCgRWOsMrDiz7FEV7BjAmaF9lGIc2ueGdRUYjRUMq4f5FSGS7gMBTYDxajdoIB3L5Gnksh1CWkIlgg95UVA==} + '@oxc-transform/binding-android-arm64@0.102.0': + resolution: {integrity: sha512-JLBT7EiExsGmB6LuBBnm6qTfg0rLSxBU+F7xjqy6UXYpL7zhqelGJL7IAq6Pu5UYFT55zVlXXmgzLOXQfpQjXA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - '@oxc-transform/binding-darwin-arm64@0.95.0': - resolution: {integrity: sha512-OUUaYZVss8tyDZZ7TGr2vnH3+i3Ouwsx0frQRGkiePNatXxaJJ3NS5+Kwgi9hh3WryXaQz2hWji4AM2RHYE7Cg==} + '@oxc-transform/binding-darwin-arm64@0.102.0': + resolution: {integrity: sha512-xmsBCk/NwE0khy8h6wLEexiS5abCp1ZqJUNHsAovJdGgIW21oGwhiC3VYg1vNLbq+zEXwOHuphVuNEYfBwyNTw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@oxc-transform/binding-darwin-x64@0.95.0': - resolution: {integrity: sha512-49UPEgIlgWUndwcP3LH6dvmOewZ92DxCMpFMo11JhUlmNJxA3sjVImEBRB56/tJ+XF+xnya9kB1oCW4yRY+mRw==} + '@oxc-transform/binding-darwin-x64@0.102.0': + resolution: {integrity: sha512-EhBsiq8hSd5BRjlWACB9MxTUiZT2He1s1b3tRP8k3lB8ZTt6sXnDXIWhxRmmM0h//xe6IJ2HuMlbvjXPo/tATg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@oxc-transform/binding-freebsd-x64@0.95.0': - resolution: {integrity: sha512-lNKrHKaDEm8pbKlVbn0rv2L97O0lbA0Tsrxx4GF/HhmdW+NgwGU1pMzZ4tB2QcylbqgKxOB+v9luebHyh1jfgA==} + '@oxc-transform/binding-freebsd-x64@0.102.0': + resolution: {integrity: sha512-eujvuYf0x7BFgKyFecbXUa2JBEXT4Ss6vmyrrhVdN07jaeJRiobaKAmeNXBkanoWL2KQLELJbSBgs1ykWYTkzg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - '@oxc-transform/binding-linux-arm-gnueabihf@0.95.0': - resolution: {integrity: sha512-+VWcLeeizI8IjU+V+o8AmzPuIMiTrGr0vrmXU3CEsV05MrywCuJU+f6ilPs3JBKno9VIwqvRpHB/z39sQabHWg==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm] - os: [linux] - - '@oxc-transform/binding-linux-arm-musleabihf@0.95.0': - resolution: {integrity: sha512-a59xPw84t6VwlvNEGcmuw3feGcKcWOC7uB8oePJ/BVSAV1yayLoB3k6JASwLTZ7N/PNPNUhcw1jDxowgAfBJfg==} + '@oxc-transform/binding-linux-arm-gnueabihf@0.102.0': + resolution: {integrity: sha512-2x7Ro356PHBVp1SS/dOsHBSnrfs5MlPYwhdKg35t6qixt2bv1kzEH0tDmn4TNEbdjOirmvOXoCTEWUvh8A4f4Q==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@oxc-transform/binding-linux-arm64-gnu@0.95.0': - resolution: {integrity: sha512-NLdrFuEHlmbiC1M1WESFV4luUcB/84GXi+cbnRXhgMjIW/CThRVJ989eTJy59QivkVlLcJSKTiKiKCt0O6TTlQ==} + '@oxc-transform/binding-linux-arm64-gnu@0.102.0': + resolution: {integrity: sha512-Rz/RbPvT4QwcHKIQ/cOt6Lwl4c7AhK2b6whZfyL6oJ7Uz8UiVl1BCwk8thedrB5h+FEykmaPHoriW1hmBev60g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] - '@oxc-transform/binding-linux-arm64-musl@0.95.0': - resolution: {integrity: sha512-GL0ffCPW8JlFI0/jeSgCY665yDdojHxA0pbYG+k8oEHOWCYZUZK9AXL+r0oerNEWYJ8CRB+L5Yq87ZtU/YUitw==} + '@oxc-transform/binding-linux-arm64-musl@0.102.0': + resolution: {integrity: sha512-I08iWABrN7zakn3wuNIBWY3hALQGsDLPQbZT1mXws7tyiQqJNGe49uS0/O50QhX3KXj+mbRGsmjVXLXGJE1CVQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] - '@oxc-transform/binding-linux-riscv64-gnu@0.95.0': - resolution: {integrity: sha512-tbH7LaClSmN3YFVo1UjMSe7D6gkb5f+CMIbj9i873UUZomVRmAjC4ygioObfzM+sj/tX0WoTXx5L1YOfQkHL6Q==} + '@oxc-transform/binding-linux-riscv64-gnu@0.102.0': + resolution: {integrity: sha512-9+SYW1ARAF6Oj/82ayoqKRe8SI7O1qvzs3Y0kijvhIqAaaZWcFRjI5DToyWRAbnzTtHlMcSllZLXNYdmxBjFxA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] - '@oxc-transform/binding-linux-s390x-gnu@0.95.0': - resolution: {integrity: sha512-8jMqiURWa0iTiPMg7BWaln89VdhhWzNlPyKM90NaFVVhBIKCr2UEhrQWdpBw/E9C8uWf/4VabBEhfPMK+0yS4w==} + '@oxc-transform/binding-linux-s390x-gnu@0.102.0': + resolution: {integrity: sha512-HV9nTyQw0TTKYPu+gBhaJBioomiM9O4LcGXi+s5IylCGG6imP0/U13q/9xJnP267QFmiWWqnnSFcv0QAWCyh8A==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] - '@oxc-transform/binding-linux-x64-gnu@0.95.0': - resolution: {integrity: sha512-D5ULJ2uWipsTgfvHIvqmnGkCtB3Fyt2ZN7APRjVO+wLr+HtmnaWddKsLdrRWX/m/6nQ2xQdoQekdJrokYK9LtQ==} + '@oxc-transform/binding-linux-x64-gnu@0.102.0': + resolution: {integrity: sha512-4wcZ08mmdFk8OjsnglyeYGu5PW3TDh87AmcMOi7tZJ3cpJjfzwDfY27KTEUx6G880OpjAiF36OFSPwdKTKgp2g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] - '@oxc-transform/binding-linux-x64-musl@0.95.0': - resolution: {integrity: sha512-DmCGU+FzRezES5wVAGVimZGzYIjMOapXbWpxuz8M8p3nMrfdBEQ5/tpwBp2vRlIohhABy4vhHJByl4c64ENCGQ==} + '@oxc-transform/binding-linux-x64-musl@0.102.0': + resolution: {integrity: sha512-rUHZSZBw0FUnUgOhL/Rs7xJz9KjH2eFur/0df6Lwq/isgJc/ggtBtFoZ+y4Fb8ON87a3Y2gS2LT7SEctX0XdPQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] - '@oxc-transform/binding-wasm32-wasi@0.95.0': - resolution: {integrity: sha512-tSo1EU4Whd1gXyae7cwSDouhppkuz6Jkd5LY8Uch9VKsHVSRhDLDW19Mq6VSwtyPxDPTJnJ2jYJWm+n8SYXiXQ==} + '@oxc-transform/binding-openharmony-arm64@0.102.0': + resolution: {integrity: sha512-98y4tccTQ/pA+r2KA/MEJIZ7J8TNTJ4aCT4rX8kWK4pGOko2YsfY3Ru9DVHlLDwmVj7wP8Z4JNxdBrAXRvK+0g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@oxc-transform/binding-wasm32-wasi@0.102.0': + resolution: {integrity: sha512-M6myOXxHty3L2TJEB1NlJPtQm0c0LmivAxcGv/+DSDadOoB/UnOUbjM8W2Utlh5IYS9ARSOjqHtBiPYLWJ15XA==} engines: {node: '>=14.0.0'} cpu: [wasm32] - '@oxc-transform/binding-win32-arm64-msvc@0.95.0': - resolution: {integrity: sha512-6eaxlgj+J5n8zgJTSugqdPLBtKGRqvxYLcvHN8b+U9hVhF/2HG/JCOrcSYV/XgWGNPQiaRVzpR3hGhmFro9QTw==} + '@oxc-transform/binding-win32-arm64-msvc@0.102.0': + resolution: {integrity: sha512-jzaA1lLiMXiJs4r7E0BHRxTPiwAkpoCfSNRr8npK/SqL4UQE4cSz3WDTX5wJWRrN2U+xqsDGefeYzH4reI8sgw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@oxc-transform/binding-win32-x64-msvc@0.95.0': - resolution: {integrity: sha512-Y8JY79A7fTuBjEXZFu+mHbHzgsV3uJDUuUKeGffpOwI1ayOGCKeBJTiMhksYkiir1xS+DkGLEz73+xse9Is9rw==} + '@oxc-transform/binding-win32-x64-msvc@0.102.0': + resolution: {integrity: sha512-eYOm6mch+1cP9qlNkMdorfBFY8aEOxY/isqrreLmEWqF/hyXA0SbLKDigTbvh3JFKny/gXlHoCKckqfua4cwtg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] @@ -2346,8 +2714,8 @@ packages: '@poppinss/colors@4.1.5': resolution: {integrity: sha512-FvdDqtcRCtz6hThExcFOgW0cWX+xwSMWcRuQe5ZEb2m7cVQOAVZOIMt+/v9RxGiD9/OY16qJBXK4CVKWAPalBw==} - '@poppinss/dumper@0.6.4': - resolution: {integrity: sha512-iG0TIdqv8xJ3Lt9O8DrPRxw1MRLjNpoqiSGU03P/wNLP/s0ra0udPJ1J2Tx5M0J3H/cVyEgpbn8xUKRY9j59kQ==} + '@poppinss/dumper@0.6.5': + resolution: {integrity: sha512-NBdYIb90J7LfOI32dOewKI1r7wnkiH6m920puQ3qHUeZkxNkQiFnXVWoE6YtFSv6QOiPPf7ys6i+HWWecDz7sw==} '@poppinss/exception@1.2.2': resolution: {integrity: sha512-m7bpKCD4QMlFCjA/nKTs23fuvoVFoA83brRKmObCUNmi/9tVu8Ve3w4YQAnJu4q3Tjf5fr685HYIC/IA2zHRSg==} @@ -2370,11 +2738,11 @@ packages: '@prisma/get-platform@6.19.0': resolution: {integrity: sha512-ym85WDO2yDhC3fIXHWYpG3kVMBA49cL1XD2GCsCF8xbwoy2OkDQY44gEbAt2X46IQ4Apq9H6g0Ex1iFfPqEkHA==} - '@rolldown/pluginutils@1.0.0-beta.29': - resolution: {integrity: sha512-NIJgOsMjbxAXvoGq/X0gD7VPMQ8j9g0BiDaNjVNVjvl+iKXxL3Jre0v31RmBYeLEmkbj2s02v8vFTbUXi5XS2Q==} + '@rolldown/pluginutils@1.0.0-beta.53': + resolution: {integrity: sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ==} - '@rolldown/pluginutils@1.0.0-beta.45': - resolution: {integrity: sha512-Le9ulGCrD8ggInzWw/k2J8QcbPz7eGIOWqfJ2L+1R0Opm7n6J37s2hiDWlh6LJN0Lk9L5sUzMvRHKW7UxBZsQA==} + '@rolldown/pluginutils@1.0.0-beta.57': + resolution: {integrity: sha512-aQNelgx14tGA+n2tNSa9x6/jeoCL9fkDeCei7nOKnHx0fEFRRMu5ReiITo+zZD5TzWDGGRjbSYCs93IfRIyTuQ==} '@rollup/plugin-alias@5.1.1': resolution: {integrity: sha512-PR9zDb+rOzkRb2VD+EuKB7UC41vU5DIwZ5qqCpk0KJudcWAyi8rvYOhS7+L5aZCspw1stTViLgN5v6FF1p5cgQ==} @@ -2682,8 +3050,8 @@ packages: resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==} engines: {node: '>=18'} - '@speed-highlight/core@1.2.8': - resolution: {integrity: sha512-IGytNtnUnPIobIbOq5Y6LIlqiHNX+vnToQIS7lj6L5819C+rA8TXRDkkG8vePsiBOGcoW9R6i+dp2YBUKdB09Q==} + '@speed-highlight/core@1.2.12': + resolution: {integrity: sha512-uilwrK0Ygyri5dToHYdZSjcvpS2ZwX0w5aSt3GCEN9hrjxWCoeV4Z2DTXuxjwbntaLQIEEAlCeNQss5SoHvAEA==} '@standard-schema/spec@1.0.0': resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==} @@ -2693,6 +3061,11 @@ packages: peerDependencies: acorn: ^8.9.0 + '@sveltejs/adapter-auto@7.0.0': + resolution: {integrity: sha512-ImDWaErTOCkRS4Gt+5gZuymKFBobnhChXUZ9lhUZLahUgvA4OOvRzi3sahzYgbxGj5nkA6OV0GAW378+dl/gyw==} + peerDependencies: + '@sveltejs/kit': ^2.0.0 + '@sveltejs/kit@2.48.3': resolution: {integrity: sha512-jf8mx3yctRXE9hvixgcqqK94YI2hDnbxI/12Upkz99XFMvxnJKCMzvz0j7lmbXSyBSNEycWO5xHvi7b73y9qkQ==} engines: {node: '>=18.13'} @@ -2706,6 +3079,26 @@ packages: '@opentelemetry/api': optional: true + '@sveltejs/kit@2.49.1': + resolution: {integrity: sha512-vByReCTTdlNM80vva8alAQC80HcOiHLkd8XAxIiKghKSHcqeNfyhp3VsYAV8VSiPKu4Jc8wWCfsZNAIvd1uCqA==} + engines: {node: '>=18.13'} + hasBin: true + peerDependencies: + '@opentelemetry/api': ^1.0.0 + '@sveltejs/vite-plugin-svelte': ^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0 + svelte: ^4.0.0 || ^5.0.0-next.0 + vite: ^5.0.3 || ^6.0.0 || ^7.0.0-beta.0 + peerDependenciesMeta: + '@opentelemetry/api': + optional: true + + '@sveltejs/package@2.5.7': + resolution: {integrity: sha512-qqD9xa9H7TDiGFrF6rz7AirOR8k15qDK/9i4MIE8te4vWsv5GEogPks61rrZcLy+yWph+aI6pIj2MdoK3YI8AQ==} + engines: {node: ^16.14 || >=18} + hasBin: true + peerDependencies: + svelte: ^3.44.0 || ^4.0.0 || ^5.0.0-next.1 + '@sveltejs/vite-plugin-svelte-inspector@5.0.1': resolution: {integrity: sha512-ubWshlMk4bc8mkwWbg6vNvCeT7lGQojE3ijDh3QTR6Zr/R+GXxsGbyH4PExEPpiFmqPhYiVSVmHBjUcVc1JIrA==} engines: {node: ^20.19 || ^22.12 || >=24} @@ -2802,60 +3195,117 @@ packages: '@tailwindcss/node@4.1.16': resolution: {integrity: sha512-BX5iaSsloNuvKNHRN3k2RcCuTEgASTo77mofW0vmeHkfrDWaoFAFvNHpEgtu0eqyypcyiBkDWzSMxJhp3AUVcw==} + '@tailwindcss/node@4.1.18': + resolution: {integrity: sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ==} + '@tailwindcss/oxide-android-arm64@4.1.16': resolution: {integrity: sha512-8+ctzkjHgwDJ5caq9IqRSgsP70xhdhJvm+oueS/yhD5ixLhqTw9fSL1OurzMUhBwE5zK26FXLCz2f/RtkISqHA==} engines: {node: '>= 10'} cpu: [arm64] os: [android] + '@tailwindcss/oxide-android-arm64@4.1.18': + resolution: {integrity: sha512-dJHz7+Ugr9U/diKJA0W6N/6/cjI+ZTAoxPf9Iz9BFRF2GzEX8IvXxFIi/dZBloVJX/MZGvRuFA9rqwdiIEZQ0Q==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [android] + '@tailwindcss/oxide-darwin-arm64@4.1.16': resolution: {integrity: sha512-C3oZy5042v2FOALBZtY0JTDnGNdS6w7DxL/odvSny17ORUnaRKhyTse8xYi3yKGyfnTUOdavRCdmc8QqJYwFKA==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] + '@tailwindcss/oxide-darwin-arm64@4.1.18': + resolution: {integrity: sha512-Gc2q4Qhs660bhjyBSKgq6BYvwDz4G+BuyJ5H1xfhmDR3D8HnHCmT/BSkvSL0vQLy/nkMLY20PQ2OoYMO15Jd0A==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + '@tailwindcss/oxide-darwin-x64@4.1.16': resolution: {integrity: sha512-vjrl/1Ub9+JwU6BP0emgipGjowzYZMjbWCDqwA2Z4vCa+HBSpP4v6U2ddejcHsolsYxwL5r4bPNoamlV0xDdLg==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] + '@tailwindcss/oxide-darwin-x64@4.1.18': + resolution: {integrity: sha512-FL5oxr2xQsFrc3X9o1fjHKBYBMD1QZNyc1Xzw/h5Qu4XnEBi3dZn96HcHm41c/euGV+GRiXFfh2hUCyKi/e+yw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + '@tailwindcss/oxide-freebsd-x64@4.1.16': resolution: {integrity: sha512-TSMpPYpQLm+aR1wW5rKuUuEruc/oOX3C7H0BTnPDn7W/eMw8W+MRMpiypKMkXZfwH8wqPIRKppuZoedTtNj2tg==} engines: {node: '>= 10'} cpu: [x64] os: [freebsd] + '@tailwindcss/oxide-freebsd-x64@4.1.18': + resolution: {integrity: sha512-Fj+RHgu5bDodmV1dM9yAxlfJwkkWvLiRjbhuO2LEtwtlYlBgiAT4x/j5wQr1tC3SANAgD+0YcmWVrj8R9trVMA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [freebsd] + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.16': resolution: {integrity: sha512-p0GGfRg/w0sdsFKBjMYvvKIiKy/LNWLWgV/plR4lUgrsxFAoQBFrXkZ4C0w8IOXfslB9vHK/JGASWD2IefIpvw==} engines: {node: '>= 10'} cpu: [arm] os: [linux] + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.18': + resolution: {integrity: sha512-Fp+Wzk/Ws4dZn+LV2Nqx3IilnhH51YZoRaYHQsVq3RQvEl+71VGKFpkfHrLM/Li+kt5c0DJe/bHXK1eHgDmdiA==} + engines: {node: '>= 10'} + cpu: [arm] + os: [linux] + '@tailwindcss/oxide-linux-arm64-gnu@4.1.16': resolution: {integrity: sha512-DoixyMmTNO19rwRPdqviTrG1rYzpxgyYJl8RgQvdAQUzxC1ToLRqtNJpU/ATURSKgIg6uerPw2feW0aS8SNr/w==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] + '@tailwindcss/oxide-linux-arm64-gnu@4.1.18': + resolution: {integrity: sha512-S0n3jboLysNbh55Vrt7pk9wgpyTTPD0fdQeh7wQfMqLPM/Hrxi+dVsLsPrycQjGKEQk85Kgbx+6+QnYNiHalnw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + '@tailwindcss/oxide-linux-arm64-musl@4.1.16': resolution: {integrity: sha512-H81UXMa9hJhWhaAUca6bU2wm5RRFpuHImrwXBUvPbYb+3jo32I9VIwpOX6hms0fPmA6f2pGVlybO6qU8pF4fzQ==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] + '@tailwindcss/oxide-linux-arm64-musl@4.1.18': + resolution: {integrity: sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + '@tailwindcss/oxide-linux-x64-gnu@4.1.16': resolution: {integrity: sha512-ZGHQxDtFC2/ruo7t99Qo2TTIvOERULPl5l0K1g0oK6b5PGqjYMga+FcY1wIUnrUxY56h28FxybtDEla+ICOyew==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + '@tailwindcss/oxide-linux-x64-gnu@4.1.18': + resolution: {integrity: sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + '@tailwindcss/oxide-linux-x64-musl@4.1.16': resolution: {integrity: sha512-Oi1tAaa0rcKf1Og9MzKeINZzMLPbhxvm7rno5/zuP1WYmpiG0bEHq4AcRUiG2165/WUzvxkW4XDYCscZWbTLZw==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + '@tailwindcss/oxide-linux-x64-musl@4.1.18': + resolution: {integrity: sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + '@tailwindcss/oxide-wasm32-wasi@4.1.16': resolution: {integrity: sha512-B01u/b8LteGRwucIBmCQ07FVXLzImWESAIMcUU6nvFt/tYsQ6IHz8DmZ5KtvmwxD+iTYBtM1xwoGXswnlu9v0Q==} engines: {node: '>=14.0.0'} @@ -2868,47 +3318,80 @@ packages: - '@emnapi/wasi-threads' - tslib + '@tailwindcss/oxide-wasm32-wasi@4.1.18': + resolution: {integrity: sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + bundledDependencies: + - '@napi-rs/wasm-runtime' + - '@emnapi/core' + - '@emnapi/runtime' + - '@tybys/wasm-util' + - '@emnapi/wasi-threads' + - tslib + '@tailwindcss/oxide-win32-arm64-msvc@4.1.16': resolution: {integrity: sha512-zX+Q8sSkGj6HKRTMJXuPvOcP8XfYON24zJBRPlszcH1Np7xuHXhWn8qfFjIujVzvH3BHU+16jBXwgpl20i+v9A==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] + '@tailwindcss/oxide-win32-arm64-msvc@4.1.18': + resolution: {integrity: sha512-HjSA7mr9HmC8fu6bdsZvZ+dhjyGCLdotjVOgLA2vEqxEBZaQo9YTX4kwgEvPCpRh8o4uWc4J/wEoFzhEmjvPbA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + '@tailwindcss/oxide-win32-x64-msvc@4.1.16': resolution: {integrity: sha512-m5dDFJUEejbFqP+UXVstd4W/wnxA4F61q8SoL+mqTypId2T2ZpuxosNSgowiCnLp2+Z+rivdU0AqpfgiD7yCBg==} engines: {node: '>= 10'} cpu: [x64] os: [win32] + '@tailwindcss/oxide-win32-x64-msvc@4.1.18': + resolution: {integrity: sha512-bJWbyYpUlqamC8dpR7pfjA0I7vdF6t5VpUGMWRkXVE3AXgIZjYUYAK7II1GNaxR8J1SSrSrppRar8G++JekE3Q==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + '@tailwindcss/oxide@4.1.16': resolution: {integrity: sha512-2OSv52FRuhdlgyOQqgtQHuCgXnS8nFSYRp2tJ+4WZXKgTxqPy7SMSls8c3mPT5pkZ17SBToGM5LHEJBO7miEdg==} engines: {node: '>= 10'} + '@tailwindcss/oxide@4.1.18': + resolution: {integrity: sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A==} + engines: {node: '>= 10'} + '@tailwindcss/postcss@4.1.16': resolution: {integrity: sha512-Qn3SFGPXYQMKR/UtqS+dqvPrzEeBZHrFA92maT4zijCVggdsXnDBMsPFJo1eArX3J+O+Gi+8pV4PkqjLCNBk3A==} + '@tailwindcss/vite@4.1.18': + resolution: {integrity: sha512-jVA+/UpKL1vRLg6Hkao5jldawNmRo7mQYrZtNHMIVpLfLhDml5nMRUo/8MwoX2vNXvnaXNNMedrMfMugAVX1nA==} + peerDependencies: + vite: ^5.2.0 || ^6 || ^7 + '@tanstack/match-sorter-utils@8.19.4': resolution: {integrity: sha512-Wo1iKt2b9OT7d+YGhvEPD3DXvPv2etTusIMhMUoG7fbhmxcXCtIjJDEygy91Y2JFlwGyjqiBPRozme7UD8hoqg==} engines: {node: '>=12'} + '@tanstack/query-core@5.90.12': + resolution: {integrity: sha512-T1/8t5DhV/SisWjDnaiU2drl6ySvsHj1bHBCWNXd+/T+Hh1cf6JodyEYMd5sgwm+b/mETT4EV3H+zCVczCU5hg==} + '@tanstack/query-core@5.90.2': resolution: {integrity: sha512-k/TcR3YalnzibscALLwxeiLUub6jN5EDLwKDiO7q5f4ICEoptJ+n9+7vcEFy5/x/i6Q+Lb/tXrsKCggf5uQJXQ==} - '@tanstack/query-core@5.90.6': - resolution: {integrity: sha512-AnZSLF26R8uX+tqb/ivdrwbVdGemdEDm1Q19qM6pry6eOZ6bEYiY7mWhzXT1YDIPTNEVcZ5kYP9nWjoxDLiIVw==} - - '@tanstack/react-query@5.90.6': - resolution: {integrity: sha512-gB1sljYjcobZKxjPbKSa31FUTyr+ROaBdoH+wSSs9Dk+yDCmMs+TkTV3PybRRVLC7ax7q0erJ9LvRWnMktnRAw==} + '@tanstack/react-query@5.90.2': + resolution: {integrity: sha512-CLABiR+h5PYfOWr/z+vWFt5VsOA2ekQeRQBFSKlcoW6Ndx/f8rfyVmq4LbgOM4GG2qtxAxjLYLOpCNTYm4uKzw==} peerDependencies: react: ^18 || ^19 - '@tanstack/svelte-query@5.90.2': - resolution: {integrity: sha512-owjnp0w8sOXlMhLZhucHrsYvCjgjHrVyII/wlqMGefxKFyroZS3xCwTee+IUx7UHbL+QmKr/HQTeTqhgxmxPQw==} + '@tanstack/svelte-query@6.0.10': + resolution: {integrity: sha512-J0kM3JNvRcRCM6cbHLeICs73aLp98N/nsihdVEtiNo3MEN4pAnO45qZ2yxX70MrEZ9vffXaCXMCChwgXs1lZ/Q==} peerDependencies: - svelte: ^3.54.0 || ^4.0.0 || ^5.0.0 + svelte: ^5.25.0 - '@tanstack/vue-query@5.90.6': - resolution: {integrity: sha512-7lKXKuTkX8XPjNd3g71B39JDE5B83Gtrr+yDPs5DiHm5wsM80OcwolLYkXumOM8+7VSm6ZYCrI9HSWszTIx5Gw==} + '@tanstack/vue-query@5.90.2': + resolution: {integrity: sha512-DLLY/B5QCbpi6AM2aaCowukQx2rXsQ4mH8RuDd8wQz0/L2bZ9Z/GgXlV310ouo47pJBmeibMVTmuoWsleT8llg==} peerDependencies: '@vue/composition-api': ^1.1.2 vue: ^2.6.0 || ^3.3.0 @@ -3288,20 +3771,29 @@ packages: engines: {node: '>=18'} hasBin: true - '@vitejs/plugin-vue-jsx@5.1.1': - resolution: {integrity: sha512-uQkfxzlF8SGHJJVH966lFTdjM/lGcwJGzwAHpVqAPDD/QcsqoUGa+q31ox1BrUfi+FLP2ChVp7uLXE3DkHyDdQ==} + '@vitejs/plugin-vue-jsx@5.1.3': + resolution: {integrity: sha512-I6Zr8cYVr5WHMW5gNOP09DNqW9rgO8RX73Wa6Czgq/0ndpTfJM4vfDChfOT1+3KtdrNqilNBtNlFwVeB02ZzGw==} engines: {node: ^20.19.0 || >=22.12.0} peerDependencies: - vite: ^5.0.0 || ^6.0.0 || ^7.0.0 + vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 vue: ^3.0.0 - '@vitejs/plugin-vue@6.0.1': - resolution: {integrity: sha512-+MaE752hU0wfPFJEUAIxqw18+20euHHdxVtMvbFcOEpjEyfqXH/5DCoTHiVJ0J29EhTJdoTkjEv5YBKU9dnoTw==} + '@vitejs/plugin-vue@6.0.3': + resolution: {integrity: sha512-TlGPkLFLVOY3T7fZrwdvKpjprR3s4fxRln0ORDo1VQ7HHyxJwTlrjKU3kpVWTlaAjIEuCTokmjkZnr8Tpc925w==} engines: {node: ^20.19.0 || >=22.12.0} peerDependencies: - vite: ^5.0.0 || ^6.0.0 || ^7.0.0 + vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 vue: ^3.2.25 + '@vitest/coverage-v8@4.0.16': + resolution: {integrity: sha512-2rNdjEIsPRzsdu6/9Eq0AYAzYdpP6Bx9cje9tL3FE5XzXRQF1fNU9pe/1yE8fCrS0HD+fBtt6gLPh6LI57tX7A==} + peerDependencies: + '@vitest/browser': 4.0.16 + vitest: 4.0.16 + peerDependenciesMeta: + '@vitest/browser': + optional: true + '@vitest/expect@4.0.14': resolution: {integrity: sha512-RHk63V3zvRiYOWAV0rGEBRO820ce17hz7cI2kDmEdfQsBjT2luEKB5tCOc91u1oSQoUOZkSv3ZyzkdkSLD7lKw==} @@ -3319,6 +3811,9 @@ packages: '@vitest/pretty-format@4.0.14': resolution: {integrity: sha512-SOYPgujB6TITcJxgd3wmsLl+wZv+fy3av2PpiPpsWPZ6J1ySUYfScfpIt2Yv56ShJXR2MOA6q2KjKHN4EpdyRQ==} + '@vitest/pretty-format@4.0.16': + resolution: {integrity: sha512-eNCYNsSty9xJKi/UdVD8Ou16alu7AYiS2fCPRs0b1OdhJiV89buAXQLpTbe+X8V9L6qrs9CqyvU7OaAopJYPsA==} + '@vitest/runner@4.0.14': resolution: {integrity: sha512-BsAIk3FAqxICqREbX8SetIteT8PiaUL/tgJjmhxJhCsigmzzH8xeadtp7LRnTpCVzvf0ib9BgAfKJHuhNllKLw==} @@ -3331,11 +3826,14 @@ packages: '@vitest/utils@4.0.14': resolution: {integrity: sha512-hLqXZKAWNg8pI+SQXyXxWCTOpA3MvsqcbVeNgSi8x/CSN2wi26dSzn1wrOhmCmFjEvN9p8/kLFRHa6PI8jHazw==} - '@volar/language-core@2.4.23': - resolution: {integrity: sha512-hEEd5ET/oSmBC6pi1j6NaNYRWoAiDhINbT8rmwtINugR39loROSlufGdYMF9TaKGfz+ViGs1Idi3mAhnuPcoGQ==} + '@vitest/utils@4.0.16': + resolution: {integrity: sha512-h8z9yYhV3e1LEfaQ3zdypIrnAg/9hguReGZoS7Gl0aBG5xgA410zBqECqmaF/+RkTggRsfnzc1XaAHA6bmUufA==} + + '@volar/language-core@2.4.27': + resolution: {integrity: sha512-DjmjBWZ4tJKxfNC1F6HyYERNHPYS7L7OPFyCrestykNdUZMFYzI9WTyvwPcaNaHlrEUwESHYsfEw3isInncZxQ==} - '@volar/source-map@2.4.23': - resolution: {integrity: sha512-Z1Uc8IB57Lm6k7q6KIDu/p+JWtf3xsXJqAX/5r18hYOTpJyBn0KXUR8oTJ4WFYOcDzWC9n3IflGgHowx6U6z9Q==} + '@volar/source-map@2.4.27': + resolution: {integrity: sha512-ynlcBReMgOZj2i6po+qVswtDUeeBRCTgDurjMGShbm8WYZgJ0PA4RmtebBJ0BCYol1qPv3GQF6jK7C9qoVc7lg==} '@vue-macros/common@3.1.1': resolution: {integrity: sha512-afW2DMjgCBVs33mWRlz7YsGHzoEEupnl0DK5ZTKsgziAlLh5syc5m+GM7eqeYrgiQpwMaVxa1fk73caCvPxyAw==} @@ -3346,73 +3844,97 @@ packages: vue: optional: true - '@vue/babel-helper-vue-transform-on@1.5.0': - resolution: {integrity: sha512-0dAYkerNhhHutHZ34JtTl2czVQHUNWv6xEbkdF5W+Yrv5pCWsqjeORdOgbtW2I9gWlt+wBmVn+ttqN9ZxR5tzA==} + '@vue/babel-helper-vue-transform-on@2.0.1': + resolution: {integrity: sha512-uZ66EaFbnnZSYqYEyplWvn46GhZ1KuYSThdT68p+am7MgBNbQ3hphTL9L+xSIsWkdktwhPYLwPgVWqo96jDdRA==} - '@vue/babel-plugin-jsx@1.5.0': - resolution: {integrity: sha512-mneBhw1oOqCd2247O0Yw/mRwC9jIGACAJUlawkmMBiNmL4dGA2eMzuNZVNqOUfYTa6vqmND4CtOPzmEEEqLKFw==} + '@vue/babel-plugin-jsx@2.0.1': + resolution: {integrity: sha512-a8CaLQjD/s4PVdhrLD/zT574ZNPnZBOY+IhdtKWRB4HRZ0I2tXBi5ne7d9eCfaYwp5gU5+4KIyFTV1W1YL9xZA==} peerDependencies: '@babel/core': ^7.0.0-0 peerDependenciesMeta: '@babel/core': optional: true - '@vue/babel-plugin-resolve-type@1.5.0': - resolution: {integrity: sha512-Wm/60o+53JwJODm4Knz47dxJnLDJ9FnKnGZJbUUf8nQRAtt6P+undLUAVU3Ha33LxOJe6IPoifRQ6F/0RrU31w==} + '@vue/babel-plugin-resolve-type@2.0.1': + resolution: {integrity: sha512-ybwgIuRGRRBhOU37GImDoWQoz+TlSqap65qVI6iwg/J7FfLTLmMf97TS7xQH9I7Qtr/gp161kYVdhr1ZMraSYQ==} peerDependencies: '@babel/core': ^7.0.0-0 '@vue/compiler-core@3.5.22': resolution: {integrity: sha512-jQ0pFPmZwTEiRNSb+i9Ow/I/cHv2tXYqsnHKKyCQ08irI2kdF5qmYedmF8si8mA7zepUFmJ2hqzS8CQmNOWOkQ==} + '@vue/compiler-core@3.5.26': + resolution: {integrity: sha512-vXyI5GMfuoBCnv5ucIT7jhHKl55Y477yxP6fc4eUswjP8FG3FFVFd41eNDArR+Uk3QKn2Z85NavjaxLxOC19/w==} + '@vue/compiler-dom@3.5.22': resolution: {integrity: sha512-W8RknzUM1BLkypvdz10OVsGxnMAuSIZs9Wdx1vzA3mL5fNMN15rhrSCLiTm6blWeACwUwizzPVqGJgOGBEN/hA==} + '@vue/compiler-dom@3.5.26': + resolution: {integrity: sha512-y1Tcd3eXs834QjswshSilCBnKGeQjQXB6PqFn/1nxcQw4pmG42G8lwz+FZPAZAby6gZeHSt/8LMPfZ4Rb+Bd/A==} + '@vue/compiler-sfc@3.5.22': resolution: {integrity: sha512-tbTR1zKGce4Lj+JLzFXDq36K4vcSZbJ1RBu8FxcDv1IGRz//Dh2EBqksyGVypz3kXpshIfWKGOCcqpSbyGWRJQ==} + '@vue/compiler-sfc@3.5.26': + resolution: {integrity: sha512-egp69qDTSEZcf4bGOSsprUr4xI73wfrY5oRs6GSgXFTiHrWj4Y3X5Ydtip9QMqiCMCPVwLglB9GBxXtTadJ3mA==} + '@vue/compiler-ssr@3.5.22': resolution: {integrity: sha512-GdgyLvg4R+7T8Nk2Mlighx7XGxq/fJf9jaVofc3IL0EPesTE86cP/8DD1lT3h1JeZr2ySBvyqKQJgbS54IX1Ww==} + '@vue/compiler-ssr@3.5.26': + resolution: {integrity: sha512-lZT9/Y0nSIRUPVvapFJEVDbEXruZh2IYHMk2zTtEgJSlP5gVOqeWXH54xDKAaFS4rTnDeDBQUYDtxKyoW9FwDw==} + '@vue/devtools-api@6.6.4': resolution: {integrity: sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==} - '@vue/devtools-core@7.7.7': - resolution: {integrity: sha512-9z9TLbfC+AjAi1PQyWX+OErjIaJmdFlbDHcD+cAMYKY6Bh5VlsAtCeGyRMrXwIlMEQPukvnWt3gZBLwTAIMKzQ==} + '@vue/devtools-core@8.0.5': + resolution: {integrity: sha512-dpCw8nl0GDBuiL9SaY0mtDxoGIEmU38w+TQiYEPOLhW03VDC0lfNMYXS/qhl4I0YlysGp04NLY4UNn6xgD0VIQ==} peerDependencies: vue: ^3.0.0 - '@vue/devtools-kit@7.7.7': - resolution: {integrity: sha512-wgoZtxcTta65cnZ1Q6MbAfePVFxfM+gq0saaeytoph7nEa7yMXoi6sCPy4ufO111B9msnw0VOWjPEFCXuAKRHA==} + '@vue/devtools-kit@8.0.5': + resolution: {integrity: sha512-q2VV6x1U3KJMTQPUlRMyWEKVbcHuxhqJdSr6Jtjz5uAThAIrfJ6WVZdGZm5cuO63ZnSUz0RCsVwiUUb0mDV0Yg==} - '@vue/devtools-shared@7.7.7': - resolution: {integrity: sha512-+udSj47aRl5aKb0memBvcUG9koarqnxNM5yjuREvqwK6T3ap4mn3Zqqc17QrBFTqSMjr3HK1cvStEZpMDpfdyw==} + '@vue/devtools-shared@8.0.5': + resolution: {integrity: sha512-bRLn6/spxpmgLk+iwOrR29KrYnJjG9DGpHGkDFG82UM21ZpJ39ztUT9OXX3g+usW7/b2z+h46I9ZiYyB07XMXg==} - '@vue/language-core@3.1.2': - resolution: {integrity: sha512-PyFDCqpdfYUT+oMLqcc61oHfJlC6yjhybaefwQjRdkchIihToOEpJ2Wu/Ebq2yrnJdd1EsaAvZaXVAqcxtnDxQ==} - peerDependencies: - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true + '@vue/language-core@3.2.1': + resolution: {integrity: sha512-g6oSenpnGMtpxHGAwKuu7HJJkNZpemK/zg3vZzZbJ6cnnXq1ssxuNrXSsAHYM3NvH8p4IkTw+NLmuxyeYz4r8A==} '@vue/reactivity@3.5.22': resolution: {integrity: sha512-f2Wux4v/Z2pqc9+4SmgZC1p73Z53fyD90NFWXiX9AKVnVBEvLFOWCEgJD3GdGnlxPZt01PSlfmLqbLYzY/Fw4A==} + '@vue/reactivity@3.5.26': + resolution: {integrity: sha512-9EnYB1/DIiUYYnzlnUBgwU32NNvLp/nhxLXeWRhHUEeWNTn1ECxX8aGO7RTXeX6PPcxe3LLuNBFoJbV4QZ+CFQ==} + '@vue/runtime-core@3.5.22': resolution: {integrity: sha512-EHo4W/eiYeAzRTN5PCextDUZ0dMs9I8mQ2Fy+OkzvRPUYQEyK9yAjbasrMCXbLNhF7P0OUyivLjIy0yc6VrLJQ==} + '@vue/runtime-core@3.5.26': + resolution: {integrity: sha512-xJWM9KH1kd201w5DvMDOwDHYhrdPTrAatn56oB/LRG4plEQeZRQLw0Bpwih9KYoqmzaxF0OKSn6swzYi84e1/Q==} + '@vue/runtime-dom@3.5.22': resolution: {integrity: sha512-Av60jsryAkI023PlN7LsqrfPvwfxOd2yAwtReCjeuugTJTkgrksYJJstg1e12qle0NarkfhfFu1ox2D+cQotww==} + '@vue/runtime-dom@3.5.26': + resolution: {integrity: sha512-XLLd/+4sPC2ZkN/6+V4O4gjJu6kSDbHAChvsyWgm1oGbdSO3efvGYnm25yCjtFm/K7rrSDvSfPDgN1pHgS4VNQ==} + '@vue/server-renderer@3.5.22': resolution: {integrity: sha512-gXjo+ao0oHYTSswF+a3KRHZ1WszxIqO7u6XwNHqcqb9JfyIL/pbWrrh/xLv7jeDqla9u+LK7yfZKHih1e1RKAQ==} peerDependencies: vue: 3.5.22 + '@vue/server-renderer@3.5.26': + resolution: {integrity: sha512-TYKLXmrwWKSodyVuO1WAubucd+1XlLg4set0YoV+Hu8Lo79mp/YMwWV5mC5FgtsDxX3qo1ONrxFaTP1OQgy1uA==} + peerDependencies: + vue: 3.5.26 + '@vue/shared@3.5.22': resolution: {integrity: sha512-F4yc6palwq3TT0u+FYf0Ns4Tfl9GRFURDN2gWG7L1ecIaS/4fCIuFOjMTnCyjsu/OK6vaDKLCrGAa+KvvH+h4w==} + '@vue/shared@3.5.26': + resolution: {integrity: sha512-7Z6/y3uFI5PRoKeorTOSXKcDj0MSasfNNltcslbFrPpcw6aXRUALq4IfJlaTRspiWIUOEZbrpM+iQGmCOiWe4A==} + abbrev@3.0.1: resolution: {integrity: sha512-AO2ac6pjRB3SJmGJo+v5/aK6Omggp6fsLrs6wN9bd35ulu4cCwaAU9+7ZhXjeqHVkaHThLuzH0nZr0YpCDhygg==} engines: {node: ^18.17.0 || >=20.5.0} @@ -3476,6 +3998,10 @@ packages: resolution: {integrity: sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==} engines: {node: '>=12'} + ansi-styles@3.2.1: + resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} + engines: {node: '>=4'} + ansi-styles@4.3.0: resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} engines: {node: '>=8'} @@ -3563,6 +4089,9 @@ packages: ast-types-flow@0.0.8: resolution: {integrity: sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==} + ast-v8-to-istanbul@0.3.9: + resolution: {integrity: sha512-dSC6tJeOJxbZrPzPbv5mMd6CMiQ1ugaVXXPRad2fXUSsy1kstFn9XQWemV9VW7Y7kpxgQ/4WMoZfwdH8XSU48w==} + ast-walker-scope@0.8.3: resolution: {integrity: sha512-cbdCP0PGOBq0ASG+sjnKIoYkWMKhhz+F/h9pRexUdX2Hd38+WOlBkRKlqkGOSm0YQpcFMQBJeK4WspUAkwsEdg==} engines: {node: '>=20.19.0'} @@ -3584,8 +4113,8 @@ packages: resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==} engines: {node: '>=8.0.0'} - autoprefixer@10.4.21: - resolution: {integrity: sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==} + autoprefixer@10.4.23: + resolution: {integrity: sha512-YYTXSFulfwytnjAPlw8QHncHJmlvFKtczb8InXaAx9Q0LbfDnfEYDE55omerIJKihhmU61Ft+cAOSzQVaBUmeA==} engines: {node: ^10 || ^12 || >=14} hasBin: true peerDependencies: @@ -3632,6 +4161,10 @@ packages: resolution: {integrity: sha512-JU0h5APyQNsHOlAM7HnQnPToSDQoEBZqzu/YBlqDnEeymPnZDREeXJA3KBMQee+dKteAxZ2AtvQEvVYdZf241Q==} hasBin: true + baseline-browser-mapping@2.9.11: + resolution: {integrity: sha512-Sg0xJUNDU1sJNGdfGWhVHX0kkZ+HWcvmVymJbj6NSgZZmW/8S9Y2HQ5euytnIgakgxN6papOAWiwDo1ctFDcoQ==} + hasBin: true + better-auth@1.3.34: resolution: {integrity: sha512-LWA52SlvnUBJRbN8VLSTLILPomZY3zZAiLxVJCeSQ5uVmaIKkMBhERitkfJcXB9RJcfl4uP+3EqKkb6hX1/uiw==} peerDependencies: @@ -3674,8 +4207,8 @@ packages: bindings@1.5.0: resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==} - birpc@2.6.1: - resolution: {integrity: sha512-LPnFhlDpdSH6FJhJyn4M0kFO7vtQ5iPw24FnG0y21q09xC7e8+1LeR31S1MAIrDAHp4m7aas4bEkTDTvMAtebQ==} + birpc@2.9.0: + resolution: {integrity: sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==} bl@4.1.0: resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} @@ -3705,6 +4238,11 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true + browserslist@4.28.1: + resolution: {integrity: sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + buffer-crc32@1.0.0: resolution: {integrity: sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==} engines: {node: '>=8.0.0'} @@ -3743,10 +4281,10 @@ packages: magicast: optional: true - c12@3.3.1: - resolution: {integrity: sha512-LcWQ01LT9tkoUINHgpIOv3mMs+Abv7oVCrtpMRi1PaapVEpWoMga5WuT7/DqFTu7URP9ftbOmimNw1KNIGh9DQ==} + c12@3.3.3: + resolution: {integrity: sha512-750hTRvgBy5kcMNPdh95Qo+XUBeGo8C7nsKSmedDmaQI+E0r82DwHeM6vBewDe4rGFbnxoa4V9pw+sPh5+Iz8Q==} peerDependencies: - magicast: ^0.3.5 + magicast: '*' peerDependenciesMeta: magicast: optional: true @@ -3774,13 +4312,17 @@ packages: caniuse-api@3.0.0: resolution: {integrity: sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==} - caniuse-lite@1.0.30001751: - resolution: {integrity: sha512-A0QJhug0Ly64Ii3eIqHu5X51ebln3k4yTUkY1j8drqpWHVreg/VLijN48cZ1bYPiqOQuqpkIKnzr/Ul8V+p6Cw==} + caniuse-lite@1.0.30001761: + resolution: {integrity: sha512-JF9ptu1vP2coz98+5051jZ4PwQgd2ni8A+gYSN7EA7dPKIMf0pDlSUxhdmVOaV3/fYK5uWBkgSXJaRLr4+3A6g==} chai@6.2.1: resolution: {integrity: sha512-p4Z49OGG5W/WBCPSS/dH3jQ73kD6tiMmUM+bckNK6Jr5JHMG3k9bg/BvKR8lKmtVBKmOiuVaV2ws8s9oSbwysg==} engines: {node: '>=18'} + chalk@2.4.2: + resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} + engines: {node: '>=4'} + chalk@4.1.2: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} @@ -3801,6 +4343,10 @@ packages: resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} engines: {node: '>= 14.16.0'} + chokidar@5.0.0: + resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==} + engines: {node: '>= 20.19.0'} + chownr@1.1.4: resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} @@ -3826,10 +4372,6 @@ packages: resolution: {integrity: sha512-5mOlNS0mhX0707P2I0aZ2V/cmHUEO/fL7VFLqszkhUsxt7RwnmrInf/eEQKlf5GzvYeHIjT+Ov1HRfNmymlG0w==} engines: {node: '>=18'} - clipboardy@5.0.0: - resolution: {integrity: sha512-MQfKHaD09eP80Pev4qBxZLbxJK/ONnqfSYAPlCmPh+7BDboYtO/3BmB6HGzxDIT0SlTRc2tzS8lQqfcdLtZ0Kg==} - engines: {node: '>=20'} - cliui@8.0.1: resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} engines: {node: '>=12'} @@ -3846,10 +4388,16 @@ packages: resolution: {integrity: sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==} engines: {node: '>=0.10.0'} + color-convert@1.9.3: + resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} + color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} engines: {node: '>=7.0.0'} + color-name@1.1.3: + resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} + color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} @@ -3953,6 +4501,9 @@ packages: resolution: {integrity: sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==} engines: {node: '>=18'} + copy-paste@2.2.0: + resolution: {integrity: sha512-jqSL4r9DSeiIvJZStLzY/sMLt9ToTM7RsK237lYOTG+KcbQJHGala3R1TUpa8h1p9adswVgIdV4qGbseVhL4lg==} + core-util-is@1.0.3: resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} @@ -3969,6 +4520,10 @@ packages: resolution: {integrity: sha512-p9nwwR4qyT5W996vBZhdvBCnMhicY5ytZkR4D1Xj0wuTDEiMnjwR57Q3RXYY/s0EpX6Ay3vgIcfaR+ewGHsi+g==} engines: {node: '>=18.0'} + cross-spawn@6.0.6: + resolution: {integrity: sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw==} + engines: {node: '>=4.8'} + cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -4028,8 +4583,12 @@ packages: resolution: {integrity: sha512-OytmFH+13/QXONJcC75QNdMtKpceNk3u8ThBjyyYjkEcy/ekBwR1mMAuNvi3gdBPW3N5TlCzQ0WZw8H0lN/bDw==} engines: {node: '>=20'} - csstype@3.1.3: - resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + cuid@3.0.0: + resolution: {integrity: sha512-WZYYkHdIDnaxdeP8Misq3Lah5vFjJwGuItJuV+tvMafosMzw0nF297T7mrm8IOWiPJkV6gc7sa8pzx27+w25Zg==} + deprecated: Cuid and other k-sortable and non-cryptographic ids (Ulid, ObjectId, KSUID, all UUIDs) are all insecure. Use @paralleldrive/cuid2 instead. damerau-levenshtein@1.0.8: resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==} @@ -4106,6 +4665,9 @@ packages: resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} engines: {node: '>=10'} + dedent-js@1.0.1: + resolution: {integrity: sha512-OUepMozQULMLUmhxS95Vudo0jb0UchLimi3+pQ2plj61Fcy8axbP9hbiD4Sz6DPqn6XG3kfmziVfQ1rSys5AJQ==} + deep-extend@0.6.0: resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} engines: {node: '>=4.0.0'} @@ -4182,6 +4744,9 @@ packages: devalue@5.4.2: resolution: {integrity: sha512-MwPZTKEPK2k8Qgfmqrd48ZKVvzSQjgW0lXLxiIBA8dQjtf/6mw6pggHNLcyDKyf+fI6eXxlQwPsfaCMTU5U+Bw==} + devalue@5.6.1: + resolution: {integrity: sha512-jDwizj+IlEZBunHcOuuFVBnIMPAEHvTsJj0BcIp94xYguLRVBcXO853px/MyIJvbVzWdsGvrRweIUWJw8hBP7A==} + dezalgo@1.0.4: resolution: {integrity: sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==} @@ -4240,6 +4805,9 @@ packages: electron-to-chromium@1.5.243: resolution: {integrity: sha512-ZCphxFW3Q1TVhcgS9blfut1PX8lusVi2SvXQgmEEnK4TCmE1JhH2JkjJN+DNt0pJJwfBri5AROBnz2b/C+YU9g==} + electron-to-chromium@1.5.267: + resolution: {integrity: sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==} + elysia@1.4.13: resolution: {integrity: sha512-6QaWQEm7QN1UCo1TPpEjaRJPHUmnM7R29y6LY224frDGk5PrpAnWmdHkoZxkcv+JRWp1j2ROr2IHbxHbG/jRjw==} peerDependencies: @@ -4284,6 +4852,13 @@ packages: resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} engines: {node: '>=0.12'} + entities@7.0.0: + resolution: {integrity: sha512-FDWG5cmEYf2Z00IkYRhbFrwIwvdFKH07uV8dvNy0omp/Qb1xcyCWp2UDtcwJF4QZZvk0sLudP6/hAu42TaqVhQ==} + engines: {node: '>=0.12'} + + error-ex@1.3.4: + resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==} + error-stack-parser-es@1.0.5: resolution: {integrity: sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==} @@ -4335,6 +4910,11 @@ packages: engines: {node: '>=18'} hasBin: true + esbuild@0.27.2: + resolution: {integrity: sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==} + engines: {node: '>=18'} + hasBin: true + escalade@3.2.0: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} @@ -4342,6 +4922,10 @@ packages: escape-html@1.0.3: resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + escape-string-regexp@1.0.5: + resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} + engines: {node: '>=0.8.0'} + escape-string-regexp@4.0.0: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} @@ -4457,8 +5041,8 @@ packages: resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} engines: {node: '>=0.10'} - esrap@2.1.1: - resolution: {integrity: sha512-ebTT9B6lOtZGMgJ3o5r12wBacHctG7oEWazIda8UlPfA3HD/Wrv8FdXoVo73vzdpwCxNyXjPauyN2bbJzMkB9A==} + esrap@2.2.1: + resolution: {integrity: sha512-GiYWG34AN/4CUyaWAgunGt0Rxvr1PTMlGC0vvEov/uOQYWne2bpN03Um+k8jT+q3op33mKouP2zeJ6OlM+qeUg==} esrecurse@4.3.0: resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} @@ -4524,6 +5108,9 @@ packages: exsolve@1.0.7: resolution: {integrity: sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw==} + exsolve@1.0.8: + resolution: {integrity: sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==} + fast-check@3.23.2: resolution: {integrity: sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==} engines: {node: '>=8.0.0'} @@ -4656,8 +5243,8 @@ packages: resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} engines: {node: '>= 0.6'} - fraction.js@4.3.7: - resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==} + fraction.js@5.3.4: + resolution: {integrity: sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==} fresh@2.0.0: resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} @@ -4802,6 +5389,10 @@ packages: resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==} engines: {node: '>= 0.4'} + has-flag@3.0.0: + resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} + engines: {node: '>=4'} + has-flag@4.0.0: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} @@ -4838,10 +5429,16 @@ packages: hookable@5.5.3: resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==} + hosted-git-info@2.8.9: + resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==} + html-encoding-sniffer@4.0.0: resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==} engines: {node: '>=18'} + html-escaper@2.0.2: + resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + http-errors@2.0.0: resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} engines: {node: '>= 0.8'} @@ -4873,6 +5470,10 @@ packages: resolution: {integrity: sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==} engines: {node: '>=18.18.0'} + iconv-lite@0.4.24: + resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} + engines: {node: '>=0.10.0'} + iconv-lite@0.6.3: resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} engines: {node: '>=0.10.0'} @@ -4939,6 +5540,9 @@ packages: resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} engines: {node: '>= 0.4'} + is-arrayish@0.2.1: + resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + is-async-function@2.1.1: resolution: {integrity: sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==} engines: {node: '>= 0.4'} @@ -5102,10 +5706,6 @@ packages: resolution: {integrity: sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==} engines: {node: '>=18'} - is-wayland@0.1.0: - resolution: {integrity: sha512-QkbMsWkIfkrzOPxenwye0h56iAXirZYHG9eHVPb22fO9y+wPbaX/CHacOWBa/I++4ohTcByimhM1/nyCsH8KNA==} - engines: {node: '>=20'} - is-weakmap@2.0.2: resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==} engines: {node: '>= 0.4'} @@ -5147,6 +5747,22 @@ packages: resolution: {integrity: sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==} engines: {node: '>=16'} + istanbul-lib-coverage@3.2.2: + resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} + engines: {node: '>=8'} + + istanbul-lib-report@3.0.1: + resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} + engines: {node: '>=10'} + + istanbul-lib-source-maps@5.0.6: + resolution: {integrity: sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==} + engines: {node: '>=10'} + + istanbul-reports@3.2.0: + resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==} + engines: {node: '>=8'} + iterator.prototype@1.1.5: resolution: {integrity: sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==} engines: {node: '>= 0.4'} @@ -5196,6 +5812,9 @@ packages: json-buffer@3.0.1: resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + json-parse-better-errors@1.0.2: + resolution: {integrity: sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==} + json-schema-ref-resolver@3.0.0: resolution: {integrity: sha512-hOrZIVL5jyYFjzk7+y7n5JDzGlU8rfWDuYyHwGa2WA8/pcmMHezp2xsVwxrebD/Q9t8Nc5DboieySDpCp4WG4A==} @@ -5252,8 +5871,8 @@ packages: resolution: {integrity: sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==} engines: {node: '>= 8'} - knitwork@1.2.0: - resolution: {integrity: sha512-xYSH7AvuQ6nXkq42x0v5S8/Iry+cfulBz/DJQzhIyESdLD7425jXsPy4vn5cCXU+HhRN2kVw51Vd1K6/By4BQg==} + knitwork@1.3.0: + resolution: {integrity: sha512-4LqMNoONzR43B1W0ek0fhXMsDNW/zxa1NdFAVMY+k28pgZLovR4G3PB5MrpTxCy1QaZCqNoiaKPr5w5qZHfSNw==} kysely-bun-sqlite@0.4.0: resolution: {integrity: sha512-2EkQE5sT4ewiw7IWfJsAkpxJ/QPVKXKO5sRYI/xjjJIJlECuOdtG+ssYM0twZJySrdrmuildNPFYVreyu1EdZg==} @@ -5379,6 +5998,10 @@ packages: resolution: {integrity: sha512-I8oW2+QL5KJo8zXNWX046M134WchxsXC7SawLPvRQpogCbkyQIaFxPE89A2HiwR7vAK2Dm2ERBAmyjTYGYEpBg==} hasBin: true + load-json-file@4.0.0: + resolution: {integrity: sha512-Kx8hMakjX03tiGTLAIdJ+lL0htKnXjEZN6hk/tozf/WOuYGdZBJrZ+rCJRbVCugsjB3jMLn9746NsQIf5VjBMw==} + engines: {node: '>=4'} + load-tsconfig@0.2.5: resolution: {integrity: sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -5461,8 +6084,12 @@ packages: magicast@0.3.5: resolution: {integrity: sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==} - magicast@0.5.0: - resolution: {integrity: sha512-D0cxqnb8DpO66P4LkD9ME6a4AhRK6A+xprXksD5vtsJN6G4zbzdI10vDaWCIyj3eLwjNZrQxUYB20FDhKrMEKQ==} + magicast@0.5.1: + resolution: {integrity: sha512-xrHS24IxaLrvuo613F719wvOIv9xPHFWQHuvGUBmPnCA/3MQxKI3b+r7n1jAoDHmsbC5bRhTZYR77invLAxVnw==} + + make-dir@4.0.0: + resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} + engines: {node: '>=10'} math-intrinsics@1.1.0: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} @@ -5481,6 +6108,10 @@ packages: memoirist@0.4.0: resolution: {integrity: sha512-zxTgA0mSYELa66DimuNQDvyLq36AwDlTuVRbnQtB+VuTcKWm5Qc4z3WkSpgsFWHNhexqkIooqpv4hdcqrX5Nmg==} + memorystream@0.3.1: + resolution: {integrity: sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw==} + engines: {node: '>= 0.10.0'} + merge-descriptors@2.0.0: resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} engines: {node: '>=18'} @@ -5663,6 +6294,9 @@ packages: sass: optional: true + nice-try@1.0.5: + resolution: {integrity: sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==} + nitropack@2.12.9: resolution: {integrity: sha512-t6qqNBn2UDGMWogQuORjbL2UPevB8PvIPsPHmqvWpeGOlPr4P8Oc5oA8t3wFwGmaolM2M/s2SwT23nx9yARmOg==} engines: {node: ^20.19.0 || >=22.12.0} @@ -5715,13 +6349,17 @@ packages: engines: {node: ^18.17.0 || >=20.5.0} hasBin: true + normalize-package-data@2.5.0: + resolution: {integrity: sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==} + normalize-path@3.0.0: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} engines: {node: '>=0.10.0'} - normalize-range@0.1.2: - resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==} - engines: {node: '>=0.10.0'} + npm-run-all@4.1.5: + resolution: {integrity: sha512-Oo82gJDAVcaMdi3nuoKFavkIHBRVqQ1qvMb+9LHk/cF4P6B2m8aP04hGf7oL6wZ9BuGwX1onlLhpuoofSyoQDQ==} + engines: {node: '>= 4'} + hasBin: true npm-run-path@5.3.0: resolution: {integrity: sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==} @@ -5734,8 +6372,8 @@ packages: nth-check@2.1.1: resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} - nuxt@4.2.0: - resolution: {integrity: sha512-4qzf2Ymf07dMMj50TZdNZgMqCdzDch8NY3NO2ClucUaIvvsr6wd9+JrDpI1CckSTHwqU37/dIPFpvIQZoeHoYA==} + nuxt@4.2.2: + resolution: {integrity: sha512-n6oYFikgLEb70J4+K19jAzfx4exZcRSRX7yZn09P5qlf2Z59VNOBqNmaZO5ObzvyGUZ308SZfL629/Q2v2FVjw==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: @@ -5790,8 +6428,8 @@ packages: obug@2.1.1: resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} - ofetch@1.5.0: - resolution: {integrity: sha512-A7llJ7eZyziA5xq9//3ZurA8OhFqtS99K5/V1sLBJ5j137CM/OAjlbA/TEJXBuOWwOfLqih+oH5U3ran4za1FQ==} + ofetch@1.5.1: + resolution: {integrity: sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA==} ohash@2.0.11: resolution: {integrity: sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==} @@ -5845,22 +6483,22 @@ packages: resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==} engines: {node: '>= 0.4'} - oxc-minify@0.95.0: - resolution: {integrity: sha512-3k//447vscNk5JZXVnr2qv0QONjUU7F8Y6ewAPFVQNgdvYh3gCLYCRjQ/DR5kVkqxFgVa8R/FFBV3X5jlztSzw==} + oxc-minify@0.102.0: + resolution: {integrity: sha512-FphAHDyTCNepQbiQTSyWFMbNc9zdUmj1WBsoLwvZhWm7rEe/IeIKYKRhy75lWOjwFsi5/i4Qucq43hgs3n2Exw==} engines: {node: ^20.19.0 || >=22.12.0} - oxc-parser@0.95.0: - resolution: {integrity: sha512-Te8fE/SmiiKWIrwBwxz5Dod87uYvsbcZ9JAL5ylPg1DevyKgTkxCXnPEaewk1Su2qpfNmry5RHoN+NywWFCG+A==} + oxc-parser@0.102.0: + resolution: {integrity: sha512-xMiyHgr2FZsphQ12ZCsXRvSYzmKXCm1ejmyG4GDZIiKOmhyt5iKtWq0klOfFsEQ6jcgbwrUdwcCVYzr1F+h5og==} engines: {node: ^20.19.0 || >=22.12.0} - oxc-transform@0.95.0: - resolution: {integrity: sha512-SmS5aThb5K0SoUZgzGbikNBjrGHfOY4X5TEqBlaZb1uy5YgXbUSbpakpZJ13yW36LNqy8Im5+y+sIk5dlzpZ/w==} + oxc-transform@0.102.0: + resolution: {integrity: sha512-MR5ohiBS6/kvxRpmUZ3LIDTTJBEC4xLAEZXfYr7vrA0eP7WHewQaNQPFDgT4Bee89TdmVQ5ZKrifGwxLjSyHHw==} engines: {node: ^20.19.0 || >=22.12.0} - oxc-walker@0.5.2: - resolution: {integrity: sha512-XYoZqWwApSKUmSDEFeOKdy3Cdh95cOcSU8f7yskFWE4Rl3cfL5uwyY+EV7Brk9mdNLy+t5SseJajd6g7KncvlA==} + oxc-walker@0.6.0: + resolution: {integrity: sha512-BA3hlxq5+Sgzp7TCQF52XDXCK5mwoIZuIuxv/+JuuTzOs2RXkLqWZgZ69d8pJDDjnL7wiREZTWHBzFp/UWH88Q==} peerDependencies: - oxc-parser: '>=0.72.0' + oxc-parser: '>=0.98.0' p-limit@3.1.0: resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} @@ -5880,6 +6518,10 @@ packages: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} + parse-json@4.0.0: + resolution: {integrity: sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==} + engines: {node: '>=4'} + parse-ms@4.0.0: resolution: {integrity: sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==} engines: {node: '>=18'} @@ -5905,6 +6547,10 @@ packages: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} + path-key@2.0.1: + resolution: {integrity: sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==} + engines: {node: '>=4'} + path-key@3.1.1: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} @@ -5927,6 +6573,10 @@ packages: path-to-regexp@8.3.0: resolution: {integrity: sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==} + path-type@3.0.0: + resolution: {integrity: sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==} + engines: {node: '>=4'} + path-type@6.0.0: resolution: {integrity: sha512-Vj7sf++t5pBD637NSfkxpHSMfWaeig5+DKWLhcqIYx6mWQz5hdJTGDVMQiJcw1ZYkhs7AazKDGpRVji1LJCZUQ==} engines: {node: '>=18'} @@ -6000,6 +6650,15 @@ packages: resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} engines: {node: '>=12'} + pidtree@0.3.1: + resolution: {integrity: sha512-qQbW94hLHEqCg7nhby4yRC7G2+jYHY4Rguc2bjw7Uug4GIJuu1tvf2uHaZv5Q8zdt+WKJ6qK1FOI6amaWUo5FA==} + engines: {node: '>=0.10'} + hasBin: true + + pify@3.0.0: + resolution: {integrity: sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==} + engines: {node: '>=4'} + pino-abstract-transport@2.0.0: resolution: {integrity: sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==} @@ -6267,6 +6926,61 @@ packages: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} + prettier-plugin-tailwindcss@0.7.2: + resolution: {integrity: sha512-LkphyK3Fw+q2HdMOoiEHWf93fNtYJwfamoKPl7UwtjFQdei/iIBoX11G6j706FzN3ymX9mPVi97qIY8328vdnA==} + engines: {node: '>=20.19'} + peerDependencies: + '@ianvs/prettier-plugin-sort-imports': '*' + '@prettier/plugin-hermes': '*' + '@prettier/plugin-oxc': '*' + '@prettier/plugin-pug': '*' + '@shopify/prettier-plugin-liquid': '*' + '@trivago/prettier-plugin-sort-imports': '*' + '@zackad/prettier-plugin-twig': '*' + prettier: ^3.0 + prettier-plugin-astro: '*' + prettier-plugin-css-order: '*' + prettier-plugin-jsdoc: '*' + prettier-plugin-marko: '*' + prettier-plugin-multiline-arrays: '*' + prettier-plugin-organize-attributes: '*' + prettier-plugin-organize-imports: '*' + prettier-plugin-sort-imports: '*' + prettier-plugin-svelte: '*' + peerDependenciesMeta: + '@ianvs/prettier-plugin-sort-imports': + optional: true + '@prettier/plugin-hermes': + optional: true + '@prettier/plugin-oxc': + optional: true + '@prettier/plugin-pug': + optional: true + '@shopify/prettier-plugin-liquid': + optional: true + '@trivago/prettier-plugin-sort-imports': + optional: true + '@zackad/prettier-plugin-twig': + optional: true + prettier-plugin-astro: + optional: true + prettier-plugin-css-order: + optional: true + prettier-plugin-jsdoc: + optional: true + prettier-plugin-marko: + optional: true + prettier-plugin-multiline-arrays: + optional: true + prettier-plugin-organize-attributes: + optional: true + prettier-plugin-organize-imports: + optional: true + prettier-plugin-sort-imports: + optional: true + prettier-plugin-svelte: + optional: true + prettier@3.5.3: resolution: {integrity: sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==} engines: {node: '>=14'} @@ -6398,6 +7112,10 @@ packages: resolution: {integrity: sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==} engines: {node: '>=0.10.0'} + read-pkg@3.0.0: + resolution: {integrity: sha512-BLq/cCO9two+lBgiTYNqD6GdtK8s4NpaWrl6/rCO9w0TUS8oJl7cmToOZfRYllKTISY6nt1U7jQ53brmKqY6BA==} + engines: {node: '>=4'} + readable-stream@2.3.8: resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} @@ -6416,6 +7134,10 @@ packages: resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} engines: {node: '>= 14.18.0'} + readdirp@5.0.0: + resolution: {integrity: sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==} + engines: {node: '>= 20.19.0'} + real-require@0.2.0: resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==} engines: {node: '>= 12.13.0'} @@ -6574,6 +7296,10 @@ packages: secure-json-parse@4.1.0: resolution: {integrity: sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA==} + semver@5.7.2: + resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==} + hasBin: true + semver@6.3.1: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true @@ -6595,8 +7321,8 @@ packages: serialize-javascript@6.0.2: resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==} - seroval@1.3.2: - resolution: {integrity: sha512-RbcPH1n5cfwKrru7v7+zrZvjLurgHhGyso3HTyGtRivGWgYjbOmGuivCQaORNELjNONoK35nj28EoWul9sb1zQ==} + seroval@1.4.1: + resolution: {integrity: sha512-9GOc+8T6LN4aByLN75uRvMbrwY5RDBW6lSlknsY4LEa9ZmWcxKcRe1G/Q3HZXjltxMHTrStnvrwAICxZrhldtg==} engines: {node: '>=10'} serve-placeholder@2.0.2: @@ -6628,10 +7354,18 @@ packages: resolution: {integrity: sha512-FUH39xp3SBPnxWvd5iib1X8XY7J0K0X7d93sie9CJg2PO8/7gmg89Nve6OjItK53/MlAushNNxteBYfM6DEuoA==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + shebang-command@1.2.0: + resolution: {integrity: sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==} + engines: {node: '>=0.10.0'} + shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} + shebang-regex@1.0.0: + resolution: {integrity: sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==} + engines: {node: '>=0.10.0'} + shebang-regex@3.0.0: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} @@ -6672,8 +7406,8 @@ packages: simple-get@4.0.1: resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==} - simple-git@3.28.0: - resolution: {integrity: sha512-Rs/vQRwsn1ILH1oBUy8NucJlXmnnLeLCfcvbSehkPzbv3wwoFWIdtfd6Ndo6ZPhlPsCZ60CPI4rxurnwAa+a2w==} + simple-git@3.30.0: + resolution: {integrity: sha512-q6lxyDsCmEal/MEGhP1aVyQ3oxnagGlBDOVSIB4XUVLl1iZh0Pah6ebC9V4xBap/RfgP2WlI8EKs0WS0rMEJHg==} sirv@3.0.2: resolution: {integrity: sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==} @@ -6712,6 +7446,18 @@ packages: engines: {node: '>= 8'} deprecated: The work that was done in this beta branch won't be included in future versions + spdx-correct@3.2.0: + resolution: {integrity: sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==} + + spdx-exceptions@2.5.0: + resolution: {integrity: sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==} + + spdx-expression-parse@3.0.1: + resolution: {integrity: sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==} + + spdx-license-ids@3.0.22: + resolution: {integrity: sha512-4PRT4nh1EImPbt2jASOKHX7PB7I+e4IWNLvkKFDxNhJlfjbYlleYQh285Z/3mPTHSAK/AvdMmw5BNNuYH8ShgQ==} + speakingurl@14.0.1: resolution: {integrity: sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==} engines: {node: '>=0.10.0'} @@ -6723,8 +7469,8 @@ packages: sql.js@1.13.0: resolution: {integrity: sha512-RJbVP1HRDlUUXahJ7VMTcu9Rm1Nzw+EBpoPr94vnbD4LwR715F3CcxE2G2k45PewcaZ57pjetYa+LoSJLAASgA==} - srvx@0.8.16: - resolution: {integrity: sha512-hmcGW4CgroeSmzgF1Ihwgl+Ths0JqAJ7HwjP2X7e3JzY7u4IydLMcdnlqGQiQGUswz+PO9oh/KtCpOISIvs9QQ==} + srvx@0.9.8: + resolution: {integrity: sha512-RZaxTKJEE/14HYn8COLuUOJAt0U55N9l1Xf6jj+T0GoA01EUH1Xz5JtSUOI+EHn+AEgPCVn7gk6jHJffrr06fQ==} engines: {node: '>=20.16.0'} hasBin: true @@ -6770,6 +7516,10 @@ packages: resolution: {integrity: sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==} engines: {node: '>= 0.4'} + string.prototype.padend@3.1.6: + resolution: {integrity: sha512-XZpspuSB7vJWhvJc9DLSlrXl1mcA2BdoY5jjnS135ydXqLoqhs96JjDtCkjJEQHvfqZIp9hBuBMgI589peyx9Q==} + engines: {node: '>= 0.4'} + string.prototype.repeat@1.0.0: resolution: {integrity: sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==} @@ -6869,6 +7619,10 @@ packages: resolution: {integrity: sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==} engines: {node: '>=18'} + supports-color@5.5.0: + resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} + engines: {node: '>=4'} + supports-color@7.2.0: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} @@ -6877,8 +7631,26 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} - svelte@5.43.3: - resolution: {integrity: sha512-kjkAjCk41mJfvJZG56XcJNOdJSke94JxtcX8zFzzz2vrt47E0LnoBzU6azIZ1aBxJgUep8qegAkguSf1GjxLXQ==} + svelte-check@4.3.5: + resolution: {integrity: sha512-e4VWZETyXaKGhpkxOXP+B/d0Fp/zKViZoJmneZWe/05Y2aqSKj3YN2nLfYPJBQ87WEiY4BQCQ9hWGu9mPT1a1Q==} + engines: {node: '>= 18.0.0'} + hasBin: true + peerDependencies: + svelte: ^4.0.0 || ^5.0.0-next.0 + typescript: '>=5.0.0' + + svelte2tsx@0.7.46: + resolution: {integrity: sha512-S++Vw3w47a8rBuhbz4JK0fcGea8tOoX1boT53Aib8+oUO2EKeOG+geXprJVTDfBlvR+IJdf3jIpR2RGwT6paQA==} + peerDependencies: + svelte: ^3.55 || ^4.0.0-next.0 || ^4.0 || ^5.0.0-next.0 + typescript: ^4.9.4 || ^5.0.0 + + svelte@5.45.6: + resolution: {integrity: sha512-V3aVXthzPyPt1UB1wLEoXnEXpwPsvs7NHrR0xkCor8c11v71VqBj477MClqPZYyrcXrAH21sNGhOj9FJvSwXfQ==} + engines: {node: '>=18'} + + svelte@5.46.1: + resolution: {integrity: sha512-ynjfCHD3nP2el70kN5Pmg37sSi0EjOm9FgHYQdC4giWG/hzO3AatzXXJJgP305uIhGQxSufJLuYWtkY8uK/8RA==} engines: {node: '>=18'} svgo@4.0.0: @@ -6900,6 +7672,9 @@ packages: tailwindcss@4.1.16: resolution: {integrity: sha512-pONL5awpaQX4LN5eiv7moSiSPd/DLDzKVRJz8Q9PgzmAdd1R4307GQS2ZpfiN7ZmekdQrfhZZiSE5jkLR4WNaA==} + tailwindcss@4.1.18: + resolution: {integrity: sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==} + tapable@2.3.0: resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==} engines: {node: '>=6'} @@ -6948,6 +7723,10 @@ packages: tinyexec@1.0.1: resolution: {integrity: sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw==} + tinyexec@1.0.2: + resolution: {integrity: sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==} + engines: {node: '>=18'} + tinyglobby@0.2.14: resolution: {integrity: sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==} engines: {node: '>=12.0.0'} @@ -7182,10 +7961,6 @@ packages: undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} - undici@7.16.0: - resolution: {integrity: sha512-QEg3HPMll0o3t2ourKwOeUAZ159Kn9mx5pnzHRQO8+Wixmh88YdZRiIwat0iNzNNXn0yoEtXJqFpyW7eM8BV7g==} - engines: {node: '>=20.18.1'} - unenv@2.0.0-rc.24: resolution: {integrity: sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==} @@ -7216,8 +7991,8 @@ packages: resolution: {integrity: sha512-5lWVjgi6vuHhJ526bI4nlCOmkCIF3nnfXkCMDeMJrtdvxTs6ZFCM8oNufGTsDbKv/tJ/xj8RpvXjRuPBZJuJog==} engines: {node: '>=20.19.0'} - unplugin-vue-router@0.16.0: - resolution: {integrity: sha512-yFmUQoN07KABkbxMSaNvfjnflwSi3nkSVKi7v6FTwXlzXyRDSx63vQ8Se4ho0T9Ao9I8U5FJB12fzmrGxFB0CA==} + unplugin-vue-router@0.19.1: + resolution: {integrity: sha512-LJVRzfxS4j34K4sx4pggzhqpfAtXNZ6mLLRHvlSbDw11lWKLluuLXRbSWLXfiVj4RHeNHXu/+XxsGX65Ogu07Q==} peerDependencies: '@vue/compiler-sfc': ^3.5.17 vue-router: ^4.6.0 @@ -7225,15 +8000,15 @@ packages: vue-router: optional: true - unplugin@2.3.10: - resolution: {integrity: sha512-6NCPkv1ClwH+/BGE9QeoTIl09nuiAt0gS28nn1PvYXsGKRwM2TCbFA2QiilmehPDTXIe684k4rZI1yl3A1PCUw==} + unplugin@2.3.11: + resolution: {integrity: sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww==} engines: {node: '>=18.12.0'} unrs-resolver@1.11.1: resolution: {integrity: sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==} - unstorage@1.17.1: - resolution: {integrity: sha512-KKGwRTT0iVBCErKemkJCLs7JdxNVfqTPc/85ae1XES0+bsHbc/sFBfVi5kJp156cc51BHinIH2l3k0EZ24vOBQ==} + unstorage@1.17.3: + resolution: {integrity: sha512-i+JYyy0DoKmQ3FximTHbGadmIYb8JEpq7lxUjnjeB702bCPum0vzo6oy5Mfu0lpqISw7hCyMW2yj4nWC8bqJ3Q==} peerDependencies: '@azure/app-configuration': ^1.8.0 '@azure/cosmos': ^4.2.0 @@ -7311,6 +8086,12 @@ packages: peerDependencies: browserslist: '>= 4.21.0' + update-browserslist-db@1.2.3: + resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + uqr@0.1.2: resolution: {integrity: sha512-MJu7ypHq6QasgF5YRTjqscSzQp/W11zoUk6kvmlH+fmWEs63Y0Eib13hYFwAzagRJcVY8WVnlV+eBDUGMJ5IbA==} @@ -7328,6 +8109,9 @@ packages: resolution: {integrity: sha512-508e6IcKLrhxKdBbcA2b4KQZlLVp2+J5UwQ6F7Drckkc5N9ZJwFa4TgWtsww9UG8fGHbm6gbV19TdM5pQ4GaIA==} hasBin: true + validate-npm-package-license@3.0.4: + resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==} + vary@1.1.2: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} @@ -7342,23 +8126,23 @@ packages: peerDependencies: vite: ^2.6.0 || ^3.0.0 || ^4.0.0 || ^5.0.0-0 || ^6.0.0-0 || ^7.0.0-0 - vite-node@3.2.4: - resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==} - engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + vite-node@5.2.0: + resolution: {integrity: sha512-7UT39YxUukIA97zWPXUGb0SGSiLexEGlavMwU3HDE6+d/HJhKLjLqu4eX2qv6SQiocdhKLRcusroDwXHQ6CnRQ==} + engines: {node: ^20.19.0 || >=22.12.0} hasBin: true - vite-plugin-checker@0.11.0: - resolution: {integrity: sha512-iUdO9Pl9UIBRPAragwi3as/BXXTtRu4G12L3CMrjx+WVTd9g/MsqNakreib9M/2YRVkhZYiTEwdH2j4Dm0w7lw==} + vite-plugin-checker@0.12.0: + resolution: {integrity: sha512-CmdZdDOGss7kdQwv73UyVgLPv0FVYe5czAgnmRX2oKljgEvSrODGuClaV3PDR2+3ou7N/OKGauDDBjy2MB07Rg==} engines: {node: '>=16.11'} peerDependencies: '@biomejs/biome': '>=1.7' - eslint: '>=7' + eslint: '>=9.39.1' meow: ^13.2.0 optionator: ^0.9.4 oxlint: '>=1' stylelint: '>=16' typescript: '*' - vite: '>=5.4.20' + vite: '>=5.4.21' vls: '*' vti: '*' vue-tsc: ~2.2.10 || ^3.0.0 @@ -7394,14 +8178,14 @@ packages: '@nuxt/kit': optional: true - vite-plugin-vue-tracer@1.0.1: - resolution: {integrity: sha512-L5/vAhT6oYbH4RSQYGLN9VfHexWe7SGzca1pJ7oPkL6KtxWA1jbGeb3Ri1JptKzqtd42HinOq4uEYqzhVWrzig==} + vite-plugin-vue-tracer@1.2.0: + resolution: {integrity: sha512-a9Z/TLpxwmoE9kIcv28wqQmiszM7ec4zgndXWEsVD/2lEZLRGzcg7ONXmplzGF/UP5W59QNtS809OdywwpUWQQ==} peerDependencies: vite: ^6.0.0 || ^7.0.0 vue: ^3.5.0 - vite@7.1.12: - resolution: {integrity: sha512-ZWyE8YXEXqJrrSLvYgrRP7p62OziLW7xI5HYGWFzOvupfAlrLvURSzv/FyGyy0eidogEM3ujU+kUG1zuHgb6Ug==} + vite@7.3.0: + resolution: {integrity: sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: @@ -7526,8 +8310,8 @@ packages: vue-devtools-stub@0.1.0: resolution: {integrity: sha512-RutnB7X8c5hjq39NceArgXg28WZtZpGc3+J16ljMiYnFhKvd8hITxSWQSQ5bvldxMDU6gG5mkxl1MTQLXckVSQ==} - vue-router@4.6.3: - resolution: {integrity: sha512-ARBedLm9YlbvQomnmq91Os7ck6efydTSpRP3nuOKCvgJOHNrhRoJDSKtee8kcL1Vf7nz6U+PMBL+hTvR3bTVQg==} + vue-router@4.6.4: + resolution: {integrity: sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==} peerDependencies: vue: ^3.5.0 @@ -7539,6 +8323,14 @@ packages: typescript: optional: true + vue@3.5.26: + resolution: {integrity: sha512-SJ/NTccVyAoNUJmkM9KUqPcYlY+u8OVL1X5EW9RIs3ch5H2uERxyyIUI4MRxVCSOiEcupX9xNGde1tL9ZKpimA==} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + w3c-xmlserializer@5.0.0: resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} engines: {node: '>=18'} @@ -7562,6 +8354,7 @@ packages: whatwg-encoding@3.1.1: resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==} engines: {node: '>=18'} + deprecated: Use @exodus/bytes instead for a more spec-conformant and faster implementation whatwg-mimetype@3.0.0: resolution: {integrity: sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==} @@ -7597,6 +8390,10 @@ packages: resolution: {integrity: sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==} engines: {node: '>= 0.4'} + which@1.3.1: + resolution: {integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==} + hasBin: true + which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} @@ -7670,8 +8467,8 @@ packages: engines: {node: '>= 14.6'} hasBin: true - yaml@2.8.1: - resolution: {integrity: sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==} + yaml@2.8.2: + resolution: {integrity: sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==} engines: {node: '>= 14.6'} hasBin: true @@ -7694,8 +8491,8 @@ packages: youch-core@0.3.3: resolution: {integrity: sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA==} - youch@4.1.0-beta.11: - resolution: {integrity: sha512-sQi6PERyO/mT8w564ojOVeAlYTtVQmC2GaktQAf+IdI75/GKIggosBuvyVXvEV+FATAT6RbLdIjFoiIId4ozoQ==} + youch@4.1.0-beta.13: + resolution: {integrity: sha512-3+AG1Xvt+R7M7PSDudhbfbwiyveW6B8PLBIwTyEC598biEYIjHhC89i6DBEvR0EZUjGY3uGSnC429HpIa2Z09g==} zimmerframe@1.1.4: resolution: {integrity: sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==} @@ -7910,6 +8707,8 @@ snapshots: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.28.5 + '@bcoe/v8-coverage@1.0.2': {} + '@better-auth/core@1.3.34(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.18)(better-call@1.0.19)(jose@6.1.2)(kysely@0.28.8)(nanostores@1.0.1)': dependencies: '@better-auth/utils': 0.3.0 @@ -7935,6 +8734,11 @@ snapshots: '@better-fetch/fetch@1.1.18': {} + '@bomb.sh/tab@0.0.10(cac@6.7.14)(citty@0.1.6)': + optionalDependencies: + cac: 6.7.14 + citty: 0.1.6 + '@borewit/text-codec@0.1.1': {} '@chevrotain/cst-dts-gen@11.0.3': @@ -7954,6 +8758,17 @@ snapshots: '@chevrotain/utils@11.0.3': {} + '@clack/core@1.0.0-alpha.7': + dependencies: + picocolors: 1.1.1 + sisteransi: 1.0.5 + + '@clack/prompts@1.0.0-alpha.8': + dependencies: + '@clack/core': 1.0.0-alpha.7 + picocolors: 1.1.1 + sisteransi: 1.0.5 + '@cloudflare/kv-asset-handler@0.4.0': dependencies: mime: 3.0.0 @@ -7986,17 +8801,17 @@ snapshots: '@csstools/css-tokenizer@3.0.4': optional: true - '@dxup/nuxt@0.2.0(magicast@0.5.0)': + '@dxup/nuxt@0.2.2(magicast@0.5.1)': dependencies: - '@dxup/unimport': 0.1.0 - '@nuxt/kit': 4.2.0(magicast@0.5.0) + '@dxup/unimport': 0.1.2 + '@nuxt/kit': 4.2.2(magicast@0.5.1) chokidar: 4.0.3 pathe: 2.0.3 tinyglobby: 0.2.15 transitivePeerDependencies: - magicast - '@dxup/unimport@0.1.0': {} + '@dxup/unimport@0.1.2': {} '@edge-runtime/primitives@6.0.0': {} @@ -8010,7 +8825,13 @@ snapshots: tslib: 2.8.1 optional: true - '@emnapi/runtime@1.6.0': + '@emnapi/core@1.7.1': + dependencies: + '@emnapi/wasi-threads': 1.1.0 + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.7.1': dependencies: tslib: 2.8.1 optional: true @@ -8026,153 +8847,231 @@ snapshots: '@esbuild/aix-ppc64@0.25.5': optional: true + '@esbuild/aix-ppc64@0.27.2': + optional: true + '@esbuild/android-arm64@0.25.11': optional: true '@esbuild/android-arm64@0.25.5': optional: true + '@esbuild/android-arm64@0.27.2': + optional: true + '@esbuild/android-arm@0.25.11': optional: true '@esbuild/android-arm@0.25.5': optional: true + '@esbuild/android-arm@0.27.2': + optional: true + '@esbuild/android-x64@0.25.11': optional: true '@esbuild/android-x64@0.25.5': optional: true + '@esbuild/android-x64@0.27.2': + optional: true + '@esbuild/darwin-arm64@0.25.11': optional: true '@esbuild/darwin-arm64@0.25.5': optional: true + '@esbuild/darwin-arm64@0.27.2': + optional: true + '@esbuild/darwin-x64@0.25.11': optional: true '@esbuild/darwin-x64@0.25.5': optional: true + '@esbuild/darwin-x64@0.27.2': + optional: true + '@esbuild/freebsd-arm64@0.25.11': optional: true '@esbuild/freebsd-arm64@0.25.5': optional: true + '@esbuild/freebsd-arm64@0.27.2': + optional: true + '@esbuild/freebsd-x64@0.25.11': optional: true '@esbuild/freebsd-x64@0.25.5': optional: true + '@esbuild/freebsd-x64@0.27.2': + optional: true + '@esbuild/linux-arm64@0.25.11': optional: true '@esbuild/linux-arm64@0.25.5': optional: true + '@esbuild/linux-arm64@0.27.2': + optional: true + '@esbuild/linux-arm@0.25.11': optional: true '@esbuild/linux-arm@0.25.5': optional: true + '@esbuild/linux-arm@0.27.2': + optional: true + '@esbuild/linux-ia32@0.25.11': optional: true '@esbuild/linux-ia32@0.25.5': optional: true + '@esbuild/linux-ia32@0.27.2': + optional: true + '@esbuild/linux-loong64@0.25.11': optional: true '@esbuild/linux-loong64@0.25.5': optional: true + '@esbuild/linux-loong64@0.27.2': + optional: true + '@esbuild/linux-mips64el@0.25.11': optional: true '@esbuild/linux-mips64el@0.25.5': optional: true + '@esbuild/linux-mips64el@0.27.2': + optional: true + '@esbuild/linux-ppc64@0.25.11': optional: true '@esbuild/linux-ppc64@0.25.5': optional: true + '@esbuild/linux-ppc64@0.27.2': + optional: true + '@esbuild/linux-riscv64@0.25.11': optional: true '@esbuild/linux-riscv64@0.25.5': optional: true + '@esbuild/linux-riscv64@0.27.2': + optional: true + '@esbuild/linux-s390x@0.25.11': optional: true '@esbuild/linux-s390x@0.25.5': optional: true + '@esbuild/linux-s390x@0.27.2': + optional: true + '@esbuild/linux-x64@0.25.11': optional: true '@esbuild/linux-x64@0.25.5': optional: true + '@esbuild/linux-x64@0.27.2': + optional: true + '@esbuild/netbsd-arm64@0.25.11': optional: true '@esbuild/netbsd-arm64@0.25.5': optional: true + '@esbuild/netbsd-arm64@0.27.2': + optional: true + '@esbuild/netbsd-x64@0.25.11': optional: true '@esbuild/netbsd-x64@0.25.5': optional: true + '@esbuild/netbsd-x64@0.27.2': + optional: true + '@esbuild/openbsd-arm64@0.25.11': optional: true '@esbuild/openbsd-arm64@0.25.5': optional: true + '@esbuild/openbsd-arm64@0.27.2': + optional: true + '@esbuild/openbsd-x64@0.25.11': optional: true '@esbuild/openbsd-x64@0.25.5': optional: true + '@esbuild/openbsd-x64@0.27.2': + optional: true + '@esbuild/openharmony-arm64@0.25.11': optional: true + '@esbuild/openharmony-arm64@0.27.2': + optional: true + '@esbuild/sunos-x64@0.25.11': optional: true '@esbuild/sunos-x64@0.25.5': optional: true + '@esbuild/sunos-x64@0.27.2': + optional: true + '@esbuild/win32-arm64@0.25.11': optional: true '@esbuild/win32-arm64@0.25.5': optional: true + '@esbuild/win32-arm64@0.27.2': + optional: true + '@esbuild/win32-ia32@0.25.11': optional: true '@esbuild/win32-ia32@0.25.5': optional: true + '@esbuild/win32-ia32@0.27.2': + optional: true + '@esbuild/win32-x64@0.25.11': optional: true '@esbuild/win32-x64@0.25.5': optional: true + '@esbuild/win32-x64@0.27.2': + optional: true + '@eslint-community/eslint-utils@4.7.0(eslint@9.29.0(jiti@2.6.1))': dependencies: eslint: 9.29.0(jiti@2.6.1) @@ -8336,7 +9235,7 @@ snapshots: '@img/sharp-wasm32@0.34.4': dependencies: - '@emnapi/runtime': 1.6.0 + '@emnapi/runtime': 1.7.1 optional: true '@img/sharp-win32-arm64@0.34.4': @@ -8410,7 +9309,7 @@ snapshots: '@kwsites/file-exists@1.1.1': dependencies: - debug: 4.4.1 + debug: 4.4.3 transitivePeerDependencies: - supports-color @@ -8443,14 +9342,14 @@ snapshots: '@napi-rs/wasm-runtime@0.2.12': dependencies: '@emnapi/core': 1.6.0 - '@emnapi/runtime': 1.6.0 + '@emnapi/runtime': 1.7.1 '@tybys/wasm-util': 0.10.1 optional: true - '@napi-rs/wasm-runtime@1.0.7': + '@napi-rs/wasm-runtime@1.1.0': dependencies: - '@emnapi/core': 1.6.0 - '@emnapi/runtime': 1.6.0 + '@emnapi/core': 1.7.1 + '@emnapi/runtime': 1.7.1 '@tybys/wasm-util': 0.10.1 optional: true @@ -8504,67 +9403,70 @@ snapshots: '@nolyfill/is-core-module@1.0.39': {} - '@nuxt/cli@3.29.3(magicast@0.5.0)': + '@nuxt/cli@3.31.3(cac@6.7.14)(magicast@0.5.1)': dependencies: - c12: 3.3.1(magicast@0.5.0) + '@bomb.sh/tab': 0.0.10(cac@6.7.14)(citty@0.1.6) + '@clack/prompts': 1.0.0-alpha.8 + c12: 3.3.3(magicast@0.5.1) citty: 0.1.6 - clipboardy: 5.0.0 confbox: 0.2.2 consola: 3.4.2 + copy-paste: 2.2.0 + debug: 4.4.3 defu: 6.1.4 - exsolve: 1.0.7 + exsolve: 1.0.8 fuse.js: 7.1.0 - get-port-please: 3.2.0 giget: 2.0.0 - h3: 1.15.4 jiti: 2.6.1 listhen: 1.9.0 nypm: 0.6.2 - ofetch: 1.5.0 + ofetch: 1.5.1 ohash: 2.0.11 pathe: 2.0.3 perfect-debounce: 2.0.0 pkg-types: 2.3.0 scule: 1.3.0 semver: 7.7.3 - srvx: 0.8.16 + srvx: 0.9.8 std-env: 3.10.0 - tinyexec: 1.0.1 + tinyexec: 1.0.2 ufo: 1.6.1 - undici: 7.16.0 - youch: 4.1.0-beta.11 + youch: 4.1.0-beta.13 transitivePeerDependencies: + - cac + - commander - magicast + - supports-color '@nuxt/devalue@2.0.2': {} - '@nuxt/devtools-kit@2.7.0(magicast@0.3.5)(vite@7.1.12(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.1))': + '@nuxt/devtools-kit@3.1.1(magicast@0.5.1)(vite@7.3.0(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.2))': dependencies: - '@nuxt/kit': 3.20.0(magicast@0.3.5) + '@nuxt/kit': 4.2.2(magicast@0.5.1) execa: 8.0.1 - vite: 7.1.12(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.1) + vite: 7.3.0(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.2) transitivePeerDependencies: - magicast - '@nuxt/devtools-wizard@2.7.0': + '@nuxt/devtools-wizard@3.1.1': dependencies: consola: 3.4.2 diff: 8.0.2 execa: 8.0.1 - magicast: 0.3.5 + magicast: 0.5.1 pathe: 2.0.3 pkg-types: 2.3.0 prompts: 2.4.2 semver: 7.7.3 - '@nuxt/devtools@2.7.0(vite@7.1.12(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.1))(vue@3.5.22(typescript@5.9.3))': + '@nuxt/devtools@3.1.1(vite@7.3.0(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.2))(vue@3.5.26(typescript@5.9.3))': dependencies: - '@nuxt/devtools-kit': 2.7.0(magicast@0.3.5)(vite@7.1.12(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.1)) - '@nuxt/devtools-wizard': 2.7.0 - '@nuxt/kit': 3.20.0(magicast@0.3.5) - '@vue/devtools-core': 7.7.7(vite@7.1.12(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.1))(vue@3.5.22(typescript@5.9.3)) - '@vue/devtools-kit': 7.7.7 - birpc: 2.6.1 + '@nuxt/devtools-kit': 3.1.1(magicast@0.5.1)(vite@7.3.0(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.2)) + '@nuxt/devtools-wizard': 3.1.1 + '@nuxt/kit': 4.2.2(magicast@0.5.1) + '@vue/devtools-core': 8.0.5(vite@7.3.0(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.2))(vue@3.5.26(typescript@5.9.3)) + '@vue/devtools-kit': 8.0.5 + birpc: 2.9.0 consola: 3.4.2 destr: 2.0.5 error-stack-parser-es: 1.0.5 @@ -8576,20 +9478,20 @@ snapshots: is-installed-globally: 1.0.0 launch-editor: 2.12.0 local-pkg: 1.1.2 - magicast: 0.3.5 + magicast: 0.5.1 nypm: 0.6.2 ohash: 2.0.11 pathe: 2.0.3 - perfect-debounce: 1.0.0 + perfect-debounce: 2.0.0 pkg-types: 2.3.0 semver: 7.7.3 - simple-git: 3.28.0 + simple-git: 3.30.0 sirv: 3.0.2 structured-clone-es: 1.0.0 tinyglobby: 0.2.15 - vite: 7.1.12(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.1) - vite-plugin-inspect: 11.3.3(@nuxt/kit@3.20.0(magicast@0.3.5))(vite@7.1.12(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.1)) - vite-plugin-vue-tracer: 1.0.1(vite@7.1.12(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.1))(vue@3.5.22(typescript@5.9.3)) + vite: 7.3.0(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.2) + vite-plugin-inspect: 11.3.3(@nuxt/kit@4.2.2(magicast@0.5.1))(vite@7.3.0(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.2)) + vite-plugin-vue-tracer: 1.2.0(vite@7.3.0(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.2))(vue@3.5.26(typescript@5.9.3)) which: 5.0.0 ws: 8.18.3 transitivePeerDependencies: @@ -8598,44 +9500,18 @@ snapshots: - utf-8-validate - vue - '@nuxt/kit@3.20.0(magicast@0.3.5)': - dependencies: - c12: 3.3.1(magicast@0.3.5) - consola: 3.4.2 - defu: 6.1.4 - destr: 2.0.5 - errx: 0.1.0 - exsolve: 1.0.7 - ignore: 7.0.5 - jiti: 2.6.1 - klona: 2.0.6 - knitwork: 1.2.0 - mlly: 1.8.0 - ohash: 2.0.11 - pathe: 2.0.3 - pkg-types: 2.3.0 - rc9: 2.1.2 - scule: 1.3.0 - semver: 7.7.3 - tinyglobby: 0.2.15 - ufo: 1.6.1 - unctx: 2.4.1 - untyped: 2.0.0 - transitivePeerDependencies: - - magicast - - '@nuxt/kit@3.20.0(magicast@0.5.0)': + '@nuxt/kit@3.20.0(magicast@0.5.1)': dependencies: - c12: 3.3.1(magicast@0.5.0) + c12: 3.3.3(magicast@0.5.1) consola: 3.4.2 defu: 6.1.4 destr: 2.0.5 errx: 0.1.0 - exsolve: 1.0.7 + exsolve: 1.0.8 ignore: 7.0.5 jiti: 2.6.1 klona: 2.0.6 - knitwork: 1.2.0 + knitwork: 1.3.0 mlly: 1.8.0 ohash: 2.0.11 pathe: 2.0.3 @@ -8650,14 +9526,14 @@ snapshots: transitivePeerDependencies: - magicast - '@nuxt/kit@4.2.0(magicast@0.5.0)': + '@nuxt/kit@4.2.2(magicast@0.5.1)': dependencies: - c12: 3.3.1(magicast@0.5.0) + c12: 3.3.3(magicast@0.5.1) consola: 3.4.2 defu: 6.1.4 destr: 2.0.5 errx: 0.1.0 - exsolve: 1.0.7 + exsolve: 1.0.8 ignore: 7.0.5 jiti: 2.6.1 klona: 2.0.6 @@ -8675,33 +9551,33 @@ snapshots: transitivePeerDependencies: - magicast - '@nuxt/nitro-server@4.2.0(better-sqlite3@12.5.0)(db0@0.3.4(better-sqlite3@12.5.0))(ioredis@5.8.2)(magicast@0.5.0)(nuxt@4.2.0(@parcel/watcher@2.5.1)(@types/node@20.19.24)(@vue/compiler-sfc@3.5.22)(better-sqlite3@12.5.0)(db0@0.3.4(better-sqlite3@12.5.0))(eslint@9.29.0(jiti@2.6.1))(ioredis@5.8.2)(lightningcss@1.30.2)(magicast@0.5.0)(optionator@0.9.4)(rollup@4.52.5)(terser@5.44.0)(tsx@4.20.3)(typescript@5.9.3)(vite@7.1.12(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.1))(yaml@2.8.1))(typescript@5.9.3)': + '@nuxt/nitro-server@4.2.2(better-sqlite3@12.5.0)(db0@0.3.4(better-sqlite3@12.5.0))(ioredis@5.8.2)(magicast@0.5.1)(nuxt@4.2.2(@parcel/watcher@2.5.1)(@types/node@20.19.24)(@vue/compiler-sfc@3.5.26)(better-sqlite3@12.5.0)(cac@6.7.14)(db0@0.3.4(better-sqlite3@12.5.0))(eslint@9.29.0(jiti@2.6.1))(ioredis@5.8.2)(lightningcss@1.30.2)(magicast@0.5.1)(optionator@0.9.4)(rollup@4.52.5)(terser@5.44.0)(tsx@4.20.3)(typescript@5.9.3)(vite@7.3.0(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.2))(yaml@2.8.2))(typescript@5.9.3)': dependencies: '@nuxt/devalue': 2.0.2 - '@nuxt/kit': 4.2.0(magicast@0.5.0) - '@unhead/vue': 2.0.19(vue@3.5.22(typescript@5.9.3)) - '@vue/shared': 3.5.22 + '@nuxt/kit': 4.2.2(magicast@0.5.1) + '@unhead/vue': 2.0.19(vue@3.5.26(typescript@5.9.3)) + '@vue/shared': 3.5.26 consola: 3.4.2 defu: 6.1.4 destr: 2.0.5 - devalue: 5.4.2 + devalue: 5.6.1 errx: 0.1.0 escape-string-regexp: 5.0.0 - exsolve: 1.0.7 + exsolve: 1.0.8 h3: 1.15.4 impound: 1.0.0 klona: 2.0.6 mocked-exports: 0.1.1 nitropack: 2.12.9(better-sqlite3@12.5.0) - nuxt: 4.2.0(@parcel/watcher@2.5.1)(@types/node@20.19.24)(@vue/compiler-sfc@3.5.22)(better-sqlite3@12.5.0)(db0@0.3.4(better-sqlite3@12.5.0))(eslint@9.29.0(jiti@2.6.1))(ioredis@5.8.2)(lightningcss@1.30.2)(magicast@0.5.0)(optionator@0.9.4)(rollup@4.52.5)(terser@5.44.0)(tsx@4.20.3)(typescript@5.9.3)(vite@7.1.12(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.1))(yaml@2.8.1) + nuxt: 4.2.2(@parcel/watcher@2.5.1)(@types/node@20.19.24)(@vue/compiler-sfc@3.5.26)(better-sqlite3@12.5.0)(cac@6.7.14)(db0@0.3.4(better-sqlite3@12.5.0))(eslint@9.29.0(jiti@2.6.1))(ioredis@5.8.2)(lightningcss@1.30.2)(magicast@0.5.1)(optionator@0.9.4)(rollup@4.52.5)(terser@5.44.0)(tsx@4.20.3)(typescript@5.9.3)(vite@7.3.0(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.2))(yaml@2.8.2) pathe: 2.0.3 pkg-types: 2.3.0 radix3: 1.1.2 std-env: 3.10.0 ufo: 1.6.1 unctx: 2.4.1 - unstorage: 1.17.1(db0@0.3.4(better-sqlite3@12.5.0))(ioredis@5.8.2) - vue: 3.5.22(typescript@5.9.3) + unstorage: 1.17.3(db0@0.3.4(better-sqlite3@12.5.0))(ioredis@5.8.2) + vue: 3.5.26(typescript@5.9.3) vue-bundle-renderer: 2.2.0 vue-devtools-stub: 0.1.0 transitivePeerDependencies: @@ -8739,24 +9615,24 @@ snapshots: - uploadthing - xml2js - '@nuxt/schema@4.2.0': + '@nuxt/schema@4.2.2': dependencies: - '@vue/shared': 3.5.22 + '@vue/shared': 3.5.26 defu: 6.1.4 pathe: 2.0.3 pkg-types: 2.3.0 std-env: 3.10.0 - '@nuxt/telemetry@2.6.6(magicast@0.5.0)': + '@nuxt/telemetry@2.6.6(magicast@0.5.1)': dependencies: - '@nuxt/kit': 3.20.0(magicast@0.5.0) + '@nuxt/kit': 3.20.0(magicast@0.5.1) citty: 0.1.6 consola: 3.4.2 destr: 2.0.5 dotenv: 16.6.1 git-url-parse: 16.1.0 is-docker: 3.0.0 - ofetch: 1.5.0 + ofetch: 1.5.1 package-manager-detector: 1.3.0 pathe: 2.0.3 rc9: 2.1.2 @@ -8764,39 +9640,39 @@ snapshots: transitivePeerDependencies: - magicast - '@nuxt/vite-builder@4.2.0(@types/node@20.19.24)(eslint@9.29.0(jiti@2.6.1))(lightningcss@1.30.2)(magicast@0.5.0)(nuxt@4.2.0(@parcel/watcher@2.5.1)(@types/node@20.19.24)(@vue/compiler-sfc@3.5.22)(better-sqlite3@12.5.0)(db0@0.3.4(better-sqlite3@12.5.0))(eslint@9.29.0(jiti@2.6.1))(ioredis@5.8.2)(lightningcss@1.30.2)(magicast@0.5.0)(optionator@0.9.4)(rollup@4.52.5)(terser@5.44.0)(tsx@4.20.3)(typescript@5.9.3)(vite@7.1.12(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.1))(yaml@2.8.1))(optionator@0.9.4)(rollup@4.52.5)(terser@5.44.0)(tsx@4.20.3)(typescript@5.9.3)(vue@3.5.22(typescript@5.9.3))(yaml@2.8.1)': + '@nuxt/vite-builder@4.2.2(@types/node@20.19.24)(eslint@9.29.0(jiti@2.6.1))(lightningcss@1.30.2)(magicast@0.5.1)(nuxt@4.2.2(@parcel/watcher@2.5.1)(@types/node@20.19.24)(@vue/compiler-sfc@3.5.26)(better-sqlite3@12.5.0)(cac@6.7.14)(db0@0.3.4(better-sqlite3@12.5.0))(eslint@9.29.0(jiti@2.6.1))(ioredis@5.8.2)(lightningcss@1.30.2)(magicast@0.5.1)(optionator@0.9.4)(rollup@4.52.5)(terser@5.44.0)(tsx@4.20.3)(typescript@5.9.3)(vite@7.3.0(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.2))(yaml@2.8.2))(optionator@0.9.4)(rollup@4.52.5)(terser@5.44.0)(tsx@4.20.3)(typescript@5.9.3)(vue@3.5.26(typescript@5.9.3))(yaml@2.8.2)': dependencies: - '@nuxt/kit': 4.2.0(magicast@0.5.0) + '@nuxt/kit': 4.2.2(magicast@0.5.1) '@rollup/plugin-replace': 6.0.3(rollup@4.52.5) - '@vitejs/plugin-vue': 6.0.1(vite@7.1.12(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.1))(vue@3.5.22(typescript@5.9.3)) - '@vitejs/plugin-vue-jsx': 5.1.1(vite@7.1.12(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.1))(vue@3.5.22(typescript@5.9.3)) - autoprefixer: 10.4.21(postcss@8.5.6) + '@vitejs/plugin-vue': 6.0.3(vite@7.3.0(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.2))(vue@3.5.26(typescript@5.9.3)) + '@vitejs/plugin-vue-jsx': 5.1.3(vite@7.3.0(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.2))(vue@3.5.26(typescript@5.9.3)) + autoprefixer: 10.4.23(postcss@8.5.6) consola: 3.4.2 cssnano: 7.1.2(postcss@8.5.6) defu: 6.1.4 - esbuild: 0.25.11 + esbuild: 0.27.2 escape-string-regexp: 5.0.0 - exsolve: 1.0.7 + exsolve: 1.0.8 get-port-please: 3.2.0 h3: 1.15.4 jiti: 2.6.1 - knitwork: 1.2.0 + knitwork: 1.3.0 magic-string: 0.30.21 mlly: 1.8.0 mocked-exports: 0.1.1 - nuxt: 4.2.0(@parcel/watcher@2.5.1)(@types/node@20.19.24)(@vue/compiler-sfc@3.5.22)(better-sqlite3@12.5.0)(db0@0.3.4(better-sqlite3@12.5.0))(eslint@9.29.0(jiti@2.6.1))(ioredis@5.8.2)(lightningcss@1.30.2)(magicast@0.5.0)(optionator@0.9.4)(rollup@4.52.5)(terser@5.44.0)(tsx@4.20.3)(typescript@5.9.3)(vite@7.1.12(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.1))(yaml@2.8.1) + nuxt: 4.2.2(@parcel/watcher@2.5.1)(@types/node@20.19.24)(@vue/compiler-sfc@3.5.26)(better-sqlite3@12.5.0)(cac@6.7.14)(db0@0.3.4(better-sqlite3@12.5.0))(eslint@9.29.0(jiti@2.6.1))(ioredis@5.8.2)(lightningcss@1.30.2)(magicast@0.5.1)(optionator@0.9.4)(rollup@4.52.5)(terser@5.44.0)(tsx@4.20.3)(typescript@5.9.3)(vite@7.3.0(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.2))(yaml@2.8.2) pathe: 2.0.3 pkg-types: 2.3.0 postcss: 8.5.6 rollup-plugin-visualizer: 6.0.5(rollup@4.52.5) - seroval: 1.3.2 + seroval: 1.4.1 std-env: 3.10.0 ufo: 1.6.1 unenv: 2.0.0-rc.24 - vite: 7.1.12(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.1) - vite-node: 3.2.4(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.1) - vite-plugin-checker: 0.11.0(eslint@9.29.0(jiti@2.6.1))(optionator@0.9.4)(typescript@5.9.3)(vite@7.1.12(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.1)) - vue: 3.5.22(typescript@5.9.3) + vite: 7.3.0(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.2) + vite-node: 5.2.0(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.2) + vite-plugin-checker: 0.12.0(eslint@9.29.0(jiti@2.6.1))(optionator@0.9.4)(typescript@5.9.3)(vite@7.3.0(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.2)) + vue: 3.5.26(typescript@5.9.3) vue-bundle-renderer: 2.2.0 transitivePeerDependencies: - '@biomejs/biome' @@ -8832,147 +9708,147 @@ snapshots: '@open-draft/until@2.1.0': {} - '@oxc-minify/binding-android-arm64@0.95.0': + '@oxc-minify/binding-android-arm64@0.102.0': optional: true - '@oxc-minify/binding-darwin-arm64@0.95.0': + '@oxc-minify/binding-darwin-arm64@0.102.0': optional: true - '@oxc-minify/binding-darwin-x64@0.95.0': + '@oxc-minify/binding-darwin-x64@0.102.0': optional: true - '@oxc-minify/binding-freebsd-x64@0.95.0': + '@oxc-minify/binding-freebsd-x64@0.102.0': optional: true - '@oxc-minify/binding-linux-arm-gnueabihf@0.95.0': + '@oxc-minify/binding-linux-arm-gnueabihf@0.102.0': optional: true - '@oxc-minify/binding-linux-arm-musleabihf@0.95.0': + '@oxc-minify/binding-linux-arm64-gnu@0.102.0': optional: true - '@oxc-minify/binding-linux-arm64-gnu@0.95.0': + '@oxc-minify/binding-linux-arm64-musl@0.102.0': optional: true - '@oxc-minify/binding-linux-arm64-musl@0.95.0': + '@oxc-minify/binding-linux-riscv64-gnu@0.102.0': optional: true - '@oxc-minify/binding-linux-riscv64-gnu@0.95.0': + '@oxc-minify/binding-linux-s390x-gnu@0.102.0': optional: true - '@oxc-minify/binding-linux-s390x-gnu@0.95.0': + '@oxc-minify/binding-linux-x64-gnu@0.102.0': optional: true - '@oxc-minify/binding-linux-x64-gnu@0.95.0': + '@oxc-minify/binding-linux-x64-musl@0.102.0': optional: true - '@oxc-minify/binding-linux-x64-musl@0.95.0': + '@oxc-minify/binding-openharmony-arm64@0.102.0': optional: true - '@oxc-minify/binding-wasm32-wasi@0.95.0': + '@oxc-minify/binding-wasm32-wasi@0.102.0': dependencies: - '@napi-rs/wasm-runtime': 1.0.7 + '@napi-rs/wasm-runtime': 1.1.0 optional: true - '@oxc-minify/binding-win32-arm64-msvc@0.95.0': + '@oxc-minify/binding-win32-arm64-msvc@0.102.0': optional: true - '@oxc-minify/binding-win32-x64-msvc@0.95.0': + '@oxc-minify/binding-win32-x64-msvc@0.102.0': optional: true - '@oxc-parser/binding-android-arm64@0.95.0': + '@oxc-parser/binding-android-arm64@0.102.0': optional: true - '@oxc-parser/binding-darwin-arm64@0.95.0': + '@oxc-parser/binding-darwin-arm64@0.102.0': optional: true - '@oxc-parser/binding-darwin-x64@0.95.0': + '@oxc-parser/binding-darwin-x64@0.102.0': optional: true - '@oxc-parser/binding-freebsd-x64@0.95.0': + '@oxc-parser/binding-freebsd-x64@0.102.0': optional: true - '@oxc-parser/binding-linux-arm-gnueabihf@0.95.0': + '@oxc-parser/binding-linux-arm-gnueabihf@0.102.0': optional: true - '@oxc-parser/binding-linux-arm-musleabihf@0.95.0': + '@oxc-parser/binding-linux-arm64-gnu@0.102.0': optional: true - '@oxc-parser/binding-linux-arm64-gnu@0.95.0': + '@oxc-parser/binding-linux-arm64-musl@0.102.0': optional: true - '@oxc-parser/binding-linux-arm64-musl@0.95.0': + '@oxc-parser/binding-linux-riscv64-gnu@0.102.0': optional: true - '@oxc-parser/binding-linux-riscv64-gnu@0.95.0': + '@oxc-parser/binding-linux-s390x-gnu@0.102.0': optional: true - '@oxc-parser/binding-linux-s390x-gnu@0.95.0': + '@oxc-parser/binding-linux-x64-gnu@0.102.0': optional: true - '@oxc-parser/binding-linux-x64-gnu@0.95.0': + '@oxc-parser/binding-linux-x64-musl@0.102.0': optional: true - '@oxc-parser/binding-linux-x64-musl@0.95.0': + '@oxc-parser/binding-openharmony-arm64@0.102.0': optional: true - '@oxc-parser/binding-wasm32-wasi@0.95.0': + '@oxc-parser/binding-wasm32-wasi@0.102.0': dependencies: - '@napi-rs/wasm-runtime': 1.0.7 + '@napi-rs/wasm-runtime': 1.1.0 optional: true - '@oxc-parser/binding-win32-arm64-msvc@0.95.0': + '@oxc-parser/binding-win32-arm64-msvc@0.102.0': optional: true - '@oxc-parser/binding-win32-x64-msvc@0.95.0': + '@oxc-parser/binding-win32-x64-msvc@0.102.0': optional: true - '@oxc-project/types@0.95.0': {} + '@oxc-project/types@0.102.0': {} - '@oxc-transform/binding-android-arm64@0.95.0': + '@oxc-transform/binding-android-arm64@0.102.0': optional: true - '@oxc-transform/binding-darwin-arm64@0.95.0': + '@oxc-transform/binding-darwin-arm64@0.102.0': optional: true - '@oxc-transform/binding-darwin-x64@0.95.0': + '@oxc-transform/binding-darwin-x64@0.102.0': optional: true - '@oxc-transform/binding-freebsd-x64@0.95.0': + '@oxc-transform/binding-freebsd-x64@0.102.0': optional: true - '@oxc-transform/binding-linux-arm-gnueabihf@0.95.0': + '@oxc-transform/binding-linux-arm-gnueabihf@0.102.0': optional: true - '@oxc-transform/binding-linux-arm-musleabihf@0.95.0': + '@oxc-transform/binding-linux-arm64-gnu@0.102.0': optional: true - '@oxc-transform/binding-linux-arm64-gnu@0.95.0': + '@oxc-transform/binding-linux-arm64-musl@0.102.0': optional: true - '@oxc-transform/binding-linux-arm64-musl@0.95.0': + '@oxc-transform/binding-linux-riscv64-gnu@0.102.0': optional: true - '@oxc-transform/binding-linux-riscv64-gnu@0.95.0': + '@oxc-transform/binding-linux-s390x-gnu@0.102.0': optional: true - '@oxc-transform/binding-linux-s390x-gnu@0.95.0': + '@oxc-transform/binding-linux-x64-gnu@0.102.0': optional: true - '@oxc-transform/binding-linux-x64-gnu@0.95.0': + '@oxc-transform/binding-linux-x64-musl@0.102.0': optional: true - '@oxc-transform/binding-linux-x64-musl@0.95.0': + '@oxc-transform/binding-openharmony-arm64@0.102.0': optional: true - '@oxc-transform/binding-wasm32-wasi@0.95.0': + '@oxc-transform/binding-wasm32-wasi@0.102.0': dependencies: - '@napi-rs/wasm-runtime': 1.0.7 + '@napi-rs/wasm-runtime': 1.1.0 optional: true - '@oxc-transform/binding-win32-arm64-msvc@0.95.0': + '@oxc-transform/binding-win32-arm64-msvc@0.102.0': optional: true - '@oxc-transform/binding-win32-x64-msvc@0.95.0': + '@oxc-transform/binding-win32-x64-msvc@0.102.0': optional: true '@paralleldrive/cuid2@2.2.2': @@ -9151,7 +10027,7 @@ snapshots: dependencies: kleur: 4.1.5 - '@poppinss/dumper@0.6.4': + '@poppinss/dumper@0.6.5': dependencies: '@poppinss/colors': 4.1.5 '@sindresorhus/is': 7.1.0 @@ -9189,9 +10065,9 @@ snapshots: dependencies: '@prisma/debug': 6.19.0 - '@rolldown/pluginutils@1.0.0-beta.29': {} + '@rolldown/pluginutils@1.0.0-beta.53': {} - '@rolldown/pluginutils@1.0.0-beta.45': {} + '@rolldown/pluginutils@1.0.0-beta.57': {} '@rollup/plugin-alias@5.1.1(rollup@4.52.5)': optionalDependencies: @@ -9405,7 +10281,7 @@ snapshots: '@sindresorhus/merge-streams@4.0.0': {} - '@speed-highlight/core@1.2.8': {} + '@speed-highlight/core@1.2.12': {} '@standard-schema/spec@1.0.0': {} @@ -9413,11 +10289,15 @@ snapshots: dependencies: acorn: 8.15.0 - '@sveltejs/kit@2.48.3(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.43.3)(vite@7.1.12(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.1)))(svelte@5.43.3)(vite@7.1.12(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.1))': + '@sveltejs/adapter-auto@7.0.0(@sveltejs/kit@2.49.1(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.45.6)(vite@7.3.0(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.2)))(svelte@5.45.6)(vite@7.3.0(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.2)))': + dependencies: + '@sveltejs/kit': 2.49.1(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.45.6)(vite@7.3.0(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.2)))(svelte@5.45.6)(vite@7.3.0(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.2)) + + '@sveltejs/kit@2.48.3(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.46.1)(vite@7.3.0(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.2)))(svelte@5.46.1)(vite@7.3.0(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.2))': dependencies: '@standard-schema/spec': 1.0.0 '@sveltejs/acorn-typescript': 1.0.6(acorn@8.15.0) - '@sveltejs/vite-plugin-svelte': 6.2.1(svelte@5.43.3)(vite@7.1.12(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.1)) + '@sveltejs/vite-plugin-svelte': 6.2.1(svelte@5.46.1)(vite@7.3.0(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.2)) '@types/cookie': 0.6.0 acorn: 8.15.0 cookie: 0.6.0 @@ -9429,27 +10309,78 @@ snapshots: sade: 1.8.1 set-cookie-parser: 2.7.2 sirv: 3.0.2 - svelte: 5.43.3 - vite: 7.1.12(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.1) + svelte: 5.46.1 + vite: 7.3.0(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.2) + + '@sveltejs/kit@2.49.1(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.45.6)(vite@7.3.0(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.2)))(svelte@5.45.6)(vite@7.3.0(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.2))': + dependencies: + '@standard-schema/spec': 1.0.0 + '@sveltejs/acorn-typescript': 1.0.6(acorn@8.15.0) + '@sveltejs/vite-plugin-svelte': 6.2.1(svelte@5.45.6)(vite@7.3.0(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.2)) + '@types/cookie': 0.6.0 + acorn: 8.15.0 + cookie: 0.6.0 + devalue: 5.6.1 + esm-env: 1.2.2 + kleur: 4.1.5 + magic-string: 0.30.21 + mrmime: 2.0.1 + sade: 1.8.1 + set-cookie-parser: 2.7.2 + sirv: 3.0.2 + svelte: 5.45.6 + vite: 7.3.0(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.2) + + '@sveltejs/package@2.5.7(svelte@5.45.6)(typescript@5.9.3)': + dependencies: + chokidar: 5.0.0 + kleur: 4.1.5 + sade: 1.8.1 + semver: 7.7.3 + svelte: 5.45.6 + svelte2tsx: 0.7.46(svelte@5.45.6)(typescript@5.9.3) + transitivePeerDependencies: + - typescript + + '@sveltejs/vite-plugin-svelte-inspector@5.0.1(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.45.6)(vite@7.3.0(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.2)))(svelte@5.45.6)(vite@7.3.0(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.2))': + dependencies: + '@sveltejs/vite-plugin-svelte': 6.2.1(svelte@5.45.6)(vite@7.3.0(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.2)) + debug: 4.4.3 + svelte: 5.45.6 + vite: 7.3.0(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.2) + transitivePeerDependencies: + - supports-color + + '@sveltejs/vite-plugin-svelte-inspector@5.0.1(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.46.1)(vite@7.3.0(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.2)))(svelte@5.46.1)(vite@7.3.0(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.2))': + dependencies: + '@sveltejs/vite-plugin-svelte': 6.2.1(svelte@5.46.1)(vite@7.3.0(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.2)) + debug: 4.4.3 + svelte: 5.46.1 + vite: 7.3.0(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.2) + transitivePeerDependencies: + - supports-color - '@sveltejs/vite-plugin-svelte-inspector@5.0.1(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.43.3)(vite@7.1.12(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.1)))(svelte@5.43.3)(vite@7.1.12(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.1))': + '@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.45.6)(vite@7.3.0(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.2))': dependencies: - '@sveltejs/vite-plugin-svelte': 6.2.1(svelte@5.43.3)(vite@7.1.12(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.1)) + '@sveltejs/vite-plugin-svelte-inspector': 5.0.1(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.45.6)(vite@7.3.0(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.2)))(svelte@5.45.6)(vite@7.3.0(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.2)) debug: 4.4.3 - svelte: 5.43.3 - vite: 7.1.12(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.1) + deepmerge: 4.3.1 + magic-string: 0.30.21 + svelte: 5.45.6 + vite: 7.3.0(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.2) + vitefu: 1.1.1(vite@7.3.0(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.2)) transitivePeerDependencies: - supports-color - '@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.43.3)(vite@7.1.12(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.1))': + '@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.46.1)(vite@7.3.0(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.2))': dependencies: - '@sveltejs/vite-plugin-svelte-inspector': 5.0.1(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.43.3)(vite@7.1.12(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.1)))(svelte@5.43.3)(vite@7.1.12(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.1)) + '@sveltejs/vite-plugin-svelte-inspector': 5.0.1(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.46.1)(vite@7.3.0(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.2)))(svelte@5.46.1)(vite@7.3.0(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.2)) debug: 4.4.3 deepmerge: 4.3.1 magic-string: 0.30.21 - svelte: 5.43.3 - vite: 7.1.12(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.1) - vitefu: 1.1.1(vite@7.1.12(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.1)) + svelte: 5.46.1 + vite: 7.3.0(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.2) + vitefu: 1.1.1(vite@7.3.0(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.2)) transitivePeerDependencies: - supports-color @@ -9522,42 +10453,88 @@ snapshots: source-map-js: 1.2.1 tailwindcss: 4.1.16 + '@tailwindcss/node@4.1.18': + dependencies: + '@jridgewell/remapping': 2.3.5 + enhanced-resolve: 5.18.3 + jiti: 2.6.1 + lightningcss: 1.30.2 + magic-string: 0.30.21 + source-map-js: 1.2.1 + tailwindcss: 4.1.18 + '@tailwindcss/oxide-android-arm64@4.1.16': optional: true + '@tailwindcss/oxide-android-arm64@4.1.18': + optional: true + '@tailwindcss/oxide-darwin-arm64@4.1.16': optional: true + '@tailwindcss/oxide-darwin-arm64@4.1.18': + optional: true + '@tailwindcss/oxide-darwin-x64@4.1.16': optional: true + '@tailwindcss/oxide-darwin-x64@4.1.18': + optional: true + '@tailwindcss/oxide-freebsd-x64@4.1.16': optional: true + '@tailwindcss/oxide-freebsd-x64@4.1.18': + optional: true + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.16': optional: true + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.18': + optional: true + '@tailwindcss/oxide-linux-arm64-gnu@4.1.16': optional: true + '@tailwindcss/oxide-linux-arm64-gnu@4.1.18': + optional: true + '@tailwindcss/oxide-linux-arm64-musl@4.1.16': optional: true + '@tailwindcss/oxide-linux-arm64-musl@4.1.18': + optional: true + '@tailwindcss/oxide-linux-x64-gnu@4.1.16': optional: true + '@tailwindcss/oxide-linux-x64-gnu@4.1.18': + optional: true + '@tailwindcss/oxide-linux-x64-musl@4.1.16': optional: true + '@tailwindcss/oxide-linux-x64-musl@4.1.18': + optional: true + '@tailwindcss/oxide-wasm32-wasi@4.1.16': optional: true + '@tailwindcss/oxide-wasm32-wasi@4.1.18': + optional: true + '@tailwindcss/oxide-win32-arm64-msvc@4.1.16': optional: true + '@tailwindcss/oxide-win32-arm64-msvc@4.1.18': + optional: true + '@tailwindcss/oxide-win32-x64-msvc@4.1.16': optional: true + '@tailwindcss/oxide-win32-x64-msvc@4.1.18': + optional: true + '@tailwindcss/oxide@4.1.16': optionalDependencies: '@tailwindcss/oxide-android-arm64': 4.1.16 @@ -9573,6 +10550,21 @@ snapshots: '@tailwindcss/oxide-win32-arm64-msvc': 4.1.16 '@tailwindcss/oxide-win32-x64-msvc': 4.1.16 + '@tailwindcss/oxide@4.1.18': + optionalDependencies: + '@tailwindcss/oxide-android-arm64': 4.1.18 + '@tailwindcss/oxide-darwin-arm64': 4.1.18 + '@tailwindcss/oxide-darwin-x64': 4.1.18 + '@tailwindcss/oxide-freebsd-x64': 4.1.18 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.1.18 + '@tailwindcss/oxide-linux-arm64-gnu': 4.1.18 + '@tailwindcss/oxide-linux-arm64-musl': 4.1.18 + '@tailwindcss/oxide-linux-x64-gnu': 4.1.18 + '@tailwindcss/oxide-linux-x64-musl': 4.1.18 + '@tailwindcss/oxide-wasm32-wasi': 4.1.18 + '@tailwindcss/oxide-win32-arm64-msvc': 4.1.18 + '@tailwindcss/oxide-win32-x64-msvc': 4.1.18 + '@tailwindcss/postcss@4.1.16': dependencies: '@alloc/quick-lru': 5.2.0 @@ -9581,28 +10573,35 @@ snapshots: postcss: 8.5.6 tailwindcss: 4.1.16 + '@tailwindcss/vite@4.1.18(vite@7.3.0(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.2))': + dependencies: + '@tailwindcss/node': 4.1.18 + '@tailwindcss/oxide': 4.1.18 + tailwindcss: 4.1.18 + vite: 7.3.0(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.2) + '@tanstack/match-sorter-utils@8.19.4': dependencies: remove-accents: 0.5.0 - '@tanstack/query-core@5.90.2': {} + '@tanstack/query-core@5.90.12': {} - '@tanstack/query-core@5.90.6': {} + '@tanstack/query-core@5.90.2': {} - '@tanstack/react-query@5.90.6(react@19.2.0)': + '@tanstack/react-query@5.90.2(react@19.2.0)': dependencies: - '@tanstack/query-core': 5.90.6 + '@tanstack/query-core': 5.90.2 react: 19.2.0 - '@tanstack/svelte-query@5.90.2(svelte@5.43.3)': + '@tanstack/svelte-query@6.0.10(svelte@5.45.6)': dependencies: - '@tanstack/query-core': 5.90.2 - svelte: 5.43.3 + '@tanstack/query-core': 5.90.12 + svelte: 5.45.6 - '@tanstack/vue-query@5.90.6(vue@3.5.22(typescript@5.9.3))': + '@tanstack/vue-query@5.90.2(vue@3.5.22(typescript@5.9.3))': dependencies: '@tanstack/match-sorter-utils': 8.19.4 - '@tanstack/query-core': 5.90.6 + '@tanstack/query-core': 5.90.2 '@vue/devtools-api': 6.6.4 vue: 3.5.22(typescript@5.9.3) vue-demi: 0.14.10(vue@3.5.22(typescript@5.9.3)) @@ -9647,7 +10646,7 @@ snapshots: '@types/better-sqlite3@7.6.13': dependencies: - '@types/node': 20.17.24 + '@types/node': 20.19.24 '@types/body-parser@1.19.6': dependencies: @@ -9731,7 +10730,7 @@ snapshots: '@types/react@19.2.0': dependencies: - csstype: 3.1.3 + csstype: 3.2.3 '@types/resolve@1.20.2': {} @@ -9962,11 +10961,11 @@ snapshots: '@typescript-eslint/types': 8.46.2 eslint-visitor-keys: 4.2.1 - '@unhead/vue@2.0.19(vue@3.5.22(typescript@5.9.3))': + '@unhead/vue@2.0.19(vue@3.5.26(typescript@5.9.3))': dependencies: hookable: 5.5.3 unhead: 2.0.19 - vue: 3.5.22(typescript@5.9.3) + vue: 3.5.26(typescript@5.9.3) '@unrs/resolver-binding-android-arm-eabi@1.11.1': optional: true @@ -10046,23 +11045,40 @@ snapshots: - rollup - supports-color - '@vitejs/plugin-vue-jsx@5.1.1(vite@7.1.12(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.1))(vue@3.5.22(typescript@5.9.3))': + '@vitejs/plugin-vue-jsx@5.1.3(vite@7.3.0(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.2))(vue@3.5.26(typescript@5.9.3))': dependencies: '@babel/core': 7.28.5 '@babel/plugin-syntax-typescript': 7.27.1(@babel/core@7.28.5) '@babel/plugin-transform-typescript': 7.28.5(@babel/core@7.28.5) - '@rolldown/pluginutils': 1.0.0-beta.45 - '@vue/babel-plugin-jsx': 1.5.0(@babel/core@7.28.5) - vite: 7.1.12(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.1) - vue: 3.5.22(typescript@5.9.3) + '@rolldown/pluginutils': 1.0.0-beta.57 + '@vue/babel-plugin-jsx': 2.0.1(@babel/core@7.28.5) + vite: 7.3.0(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.2) + vue: 3.5.26(typescript@5.9.3) transitivePeerDependencies: - supports-color - '@vitejs/plugin-vue@6.0.1(vite@7.1.12(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.1))(vue@3.5.22(typescript@5.9.3))': + '@vitejs/plugin-vue@6.0.3(vite@7.3.0(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.2))(vue@3.5.26(typescript@5.9.3))': + dependencies: + '@rolldown/pluginutils': 1.0.0-beta.53 + vite: 7.3.0(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.2) + vue: 3.5.26(typescript@5.9.3) + + '@vitest/coverage-v8@4.0.16(vitest@4.0.14(@edge-runtime/vm@5.0.0)(@types/node@20.19.24)(happy-dom@20.0.10)(jiti@2.6.1)(jsdom@27.1.0)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0))': dependencies: - '@rolldown/pluginutils': 1.0.0-beta.29 - vite: 7.1.12(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.1) - vue: 3.5.22(typescript@5.9.3) + '@bcoe/v8-coverage': 1.0.2 + '@vitest/utils': 4.0.16 + ast-v8-to-istanbul: 0.3.9 + istanbul-lib-coverage: 3.2.2 + istanbul-lib-report: 3.0.1 + istanbul-lib-source-maps: 5.0.6 + istanbul-reports: 3.2.0 + magicast: 0.5.1 + obug: 2.1.1 + std-env: 3.10.0 + tinyrainbow: 3.0.3 + vitest: 4.0.14(@edge-runtime/vm@5.0.0)(@types/node@20.19.24)(happy-dom@20.0.10)(jiti@2.6.1)(jsdom@27.1.0)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0) + transitivePeerDependencies: + - supports-color '@vitest/expect@4.0.14': dependencies: @@ -10073,18 +11089,22 @@ snapshots: chai: 6.2.1 tinyrainbow: 3.0.3 - '@vitest/mocker@4.0.14(vite@7.1.12(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0))': + '@vitest/mocker@4.0.14(vite@7.3.0(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0))': dependencies: '@vitest/spy': 4.0.14 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 7.1.12(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0) + vite: 7.3.0(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0) '@vitest/pretty-format@4.0.14': dependencies: tinyrainbow: 3.0.3 + '@vitest/pretty-format@4.0.16': + dependencies: + tinyrainbow: 3.0.3 + '@vitest/runner@4.0.14': dependencies: '@vitest/utils': 4.0.14 @@ -10103,25 +11123,30 @@ snapshots: '@vitest/pretty-format': 4.0.14 tinyrainbow: 3.0.3 - '@volar/language-core@2.4.23': + '@vitest/utils@4.0.16': + dependencies: + '@vitest/pretty-format': 4.0.16 + tinyrainbow: 3.0.3 + + '@volar/language-core@2.4.27': dependencies: - '@volar/source-map': 2.4.23 + '@volar/source-map': 2.4.27 - '@volar/source-map@2.4.23': {} + '@volar/source-map@2.4.27': {} - '@vue-macros/common@3.1.1(vue@3.5.22(typescript@5.9.3))': + '@vue-macros/common@3.1.1(vue@3.5.26(typescript@5.9.3))': dependencies: - '@vue/compiler-sfc': 3.5.22 + '@vue/compiler-sfc': 3.5.26 ast-kit: 2.1.3 local-pkg: 1.1.2 magic-string-ast: 1.0.3 unplugin-utils: 0.3.1 optionalDependencies: - vue: 3.5.22(typescript@5.9.3) + vue: 3.5.26(typescript@5.9.3) - '@vue/babel-helper-vue-transform-on@1.5.0': {} + '@vue/babel-helper-vue-transform-on@2.0.1': {} - '@vue/babel-plugin-jsx@1.5.0(@babel/core@7.28.5)': + '@vue/babel-plugin-jsx@2.0.1(@babel/core@7.28.5)': dependencies: '@babel/helper-module-imports': 7.27.1 '@babel/helper-plugin-utils': 7.27.1 @@ -10129,22 +11154,22 @@ snapshots: '@babel/template': 7.27.2 '@babel/traverse': 7.28.5 '@babel/types': 7.28.5 - '@vue/babel-helper-vue-transform-on': 1.5.0 - '@vue/babel-plugin-resolve-type': 1.5.0(@babel/core@7.28.5) - '@vue/shared': 3.5.22 + '@vue/babel-helper-vue-transform-on': 2.0.1 + '@vue/babel-plugin-resolve-type': 2.0.1(@babel/core@7.28.5) + '@vue/shared': 3.5.26 optionalDependencies: '@babel/core': 7.28.5 transitivePeerDependencies: - supports-color - '@vue/babel-plugin-resolve-type@1.5.0(@babel/core@7.28.5)': + '@vue/babel-plugin-resolve-type@2.0.1(@babel/core@7.28.5)': dependencies: '@babel/code-frame': 7.27.1 '@babel/core': 7.28.5 '@babel/helper-module-imports': 7.27.1 '@babel/helper-plugin-utils': 7.27.1 '@babel/parser': 7.28.5 - '@vue/compiler-sfc': 3.5.22 + '@vue/compiler-sfc': 3.5.26 transitivePeerDependencies: - supports-color @@ -10156,11 +11181,24 @@ snapshots: estree-walker: 2.0.2 source-map-js: 1.2.1 + '@vue/compiler-core@3.5.26': + dependencies: + '@babel/parser': 7.28.5 + '@vue/shared': 3.5.26 + entities: 7.0.0 + estree-walker: 2.0.2 + source-map-js: 1.2.1 + '@vue/compiler-dom@3.5.22': dependencies: '@vue/compiler-core': 3.5.22 '@vue/shared': 3.5.22 + '@vue/compiler-dom@3.5.26': + dependencies: + '@vue/compiler-core': 3.5.26 + '@vue/shared': 3.5.26 + '@vue/compiler-sfc@3.5.22': dependencies: '@babel/parser': 7.28.5 @@ -10173,66 +11211,97 @@ snapshots: postcss: 8.5.6 source-map-js: 1.2.1 + '@vue/compiler-sfc@3.5.26': + dependencies: + '@babel/parser': 7.28.5 + '@vue/compiler-core': 3.5.26 + '@vue/compiler-dom': 3.5.26 + '@vue/compiler-ssr': 3.5.26 + '@vue/shared': 3.5.26 + estree-walker: 2.0.2 + magic-string: 0.30.21 + postcss: 8.5.6 + source-map-js: 1.2.1 + '@vue/compiler-ssr@3.5.22': dependencies: '@vue/compiler-dom': 3.5.22 '@vue/shared': 3.5.22 + '@vue/compiler-ssr@3.5.26': + dependencies: + '@vue/compiler-dom': 3.5.26 + '@vue/shared': 3.5.26 + '@vue/devtools-api@6.6.4': {} - '@vue/devtools-core@7.7.7(vite@7.1.12(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.1))(vue@3.5.22(typescript@5.9.3))': + '@vue/devtools-core@8.0.5(vite@7.3.0(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.2))(vue@3.5.26(typescript@5.9.3))': dependencies: - '@vue/devtools-kit': 7.7.7 - '@vue/devtools-shared': 7.7.7 + '@vue/devtools-kit': 8.0.5 + '@vue/devtools-shared': 8.0.5 mitt: 3.0.1 nanoid: 5.1.6 pathe: 2.0.3 - vite-hot-client: 2.1.0(vite@7.1.12(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.1)) - vue: 3.5.22(typescript@5.9.3) + vite-hot-client: 2.1.0(vite@7.3.0(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.2)) + vue: 3.5.26(typescript@5.9.3) transitivePeerDependencies: - vite - '@vue/devtools-kit@7.7.7': + '@vue/devtools-kit@8.0.5': dependencies: - '@vue/devtools-shared': 7.7.7 - birpc: 2.6.1 + '@vue/devtools-shared': 8.0.5 + birpc: 2.9.0 hookable: 5.5.3 mitt: 3.0.1 - perfect-debounce: 1.0.0 + perfect-debounce: 2.0.0 speakingurl: 14.0.1 superjson: 2.2.3 - '@vue/devtools-shared@7.7.7': + '@vue/devtools-shared@8.0.5': dependencies: rfdc: 1.4.1 - '@vue/language-core@3.1.2(typescript@5.9.3)': + '@vue/language-core@3.2.1': dependencies: - '@volar/language-core': 2.4.23 - '@vue/compiler-dom': 3.5.22 - '@vue/shared': 3.5.22 + '@volar/language-core': 2.4.27 + '@vue/compiler-dom': 3.5.26 + '@vue/shared': 3.5.26 alien-signals: 3.0.3 muggle-string: 0.4.1 path-browserify: 1.0.1 picomatch: 4.0.3 - optionalDependencies: - typescript: 5.9.3 '@vue/reactivity@3.5.22': dependencies: '@vue/shared': 3.5.22 + '@vue/reactivity@3.5.26': + dependencies: + '@vue/shared': 3.5.26 + '@vue/runtime-core@3.5.22': dependencies: '@vue/reactivity': 3.5.22 '@vue/shared': 3.5.22 + '@vue/runtime-core@3.5.26': + dependencies: + '@vue/reactivity': 3.5.26 + '@vue/shared': 3.5.26 + '@vue/runtime-dom@3.5.22': dependencies: '@vue/reactivity': 3.5.22 '@vue/runtime-core': 3.5.22 '@vue/shared': 3.5.22 - csstype: 3.1.3 + csstype: 3.2.3 + + '@vue/runtime-dom@3.5.26': + dependencies: + '@vue/reactivity': 3.5.26 + '@vue/runtime-core': 3.5.26 + '@vue/shared': 3.5.26 + csstype: 3.2.3 '@vue/server-renderer@3.5.22(vue@3.5.22(typescript@5.9.3))': dependencies: @@ -10240,8 +11309,16 @@ snapshots: '@vue/shared': 3.5.22 vue: 3.5.22(typescript@5.9.3) + '@vue/server-renderer@3.5.26(vue@3.5.26(typescript@5.9.3))': + dependencies: + '@vue/compiler-ssr': 3.5.26 + '@vue/shared': 3.5.26 + vue: 3.5.26(typescript@5.9.3) + '@vue/shared@3.5.22': {} + '@vue/shared@3.5.26': {} + abbrev@3.0.1: {} abort-controller@3.0.0: @@ -10297,6 +11374,10 @@ snapshots: ansi-regex@6.1.0: {} + ansi-styles@3.2.1: + dependencies: + color-convert: 1.9.3 + ansi-styles@4.3.0: dependencies: color-convert: 2.0.1 @@ -10427,6 +11508,12 @@ snapshots: ast-types-flow@0.0.8: {} + ast-v8-to-istanbul@0.3.9: + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + estree-walker: 3.0.3 + js-tokens: 9.0.1 + ast-walker-scope@0.8.3: dependencies: '@babel/parser': 7.28.5 @@ -10442,12 +11529,11 @@ snapshots: atomic-sleep@1.0.0: {} - autoprefixer@10.4.21(postcss@8.5.6): + autoprefixer@10.4.23(postcss@8.5.6): dependencies: - browserslist: 4.27.0 - caniuse-lite: 1.0.30001751 - fraction.js: 4.3.7 - normalize-range: 0.1.2 + browserslist: 4.28.1 + caniuse-lite: 1.0.30001761 + fraction.js: 5.3.4 picocolors: 1.1.1 postcss: 8.5.6 postcss-value-parser: 4.2.0 @@ -10475,6 +11561,8 @@ snapshots: baseline-browser-mapping@2.8.21: {} + baseline-browser-mapping@2.9.11: {} + better-auth@1.3.34: dependencies: '@better-auth/core': 1.3.34(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.18)(better-call@1.0.19)(jose@6.1.2)(kysely@0.28.8)(nanostores@1.0.1) @@ -10514,7 +11602,7 @@ snapshots: dependencies: file-uri-to-path: 1.0.0 - birpc@2.6.1: {} + birpc@2.9.0: {} bl@4.1.0: dependencies: @@ -10558,11 +11646,19 @@ snapshots: browserslist@4.27.0: dependencies: baseline-browser-mapping: 2.8.21 - caniuse-lite: 1.0.30001751 + caniuse-lite: 1.0.30001761 electron-to-chromium: 1.5.243 node-releases: 2.0.27 update-browserslist-db: 1.1.4(browserslist@4.27.0) + browserslist@4.28.1: + dependencies: + baseline-browser-mapping: 2.9.11 + caniuse-lite: 1.0.30001761 + electron-to-chromium: 1.5.267 + node-releases: 2.0.27 + update-browserslist-db: 1.2.3(browserslist@4.28.1) + buffer-crc32@1.0.0: {} buffer-from@1.1.2: {} @@ -10609,30 +11705,13 @@ snapshots: optionalDependencies: magicast: 0.3.5 - c12@3.3.1(magicast@0.3.5): - dependencies: - chokidar: 4.0.3 - confbox: 0.2.2 - defu: 6.1.4 - dotenv: 17.2.3 - exsolve: 1.0.7 - giget: 2.0.0 - jiti: 2.6.1 - ohash: 2.0.11 - pathe: 2.0.3 - perfect-debounce: 2.0.0 - pkg-types: 2.3.0 - rc9: 2.1.2 - optionalDependencies: - magicast: 0.3.5 - - c12@3.3.1(magicast@0.5.0): + c12@3.3.3(magicast@0.5.1): dependencies: - chokidar: 4.0.3 + chokidar: 5.0.0 confbox: 0.2.2 defu: 6.1.4 dotenv: 17.2.3 - exsolve: 1.0.7 + exsolve: 1.0.8 giget: 2.0.0 jiti: 2.6.1 ohash: 2.0.11 @@ -10641,7 +11720,7 @@ snapshots: pkg-types: 2.3.0 rc9: 2.1.2 optionalDependencies: - magicast: 0.5.0 + magicast: 0.5.1 cac@6.7.14: {} @@ -10666,15 +11745,21 @@ snapshots: caniuse-api@3.0.0: dependencies: - browserslist: 4.27.0 - caniuse-lite: 1.0.30001751 + browserslist: 4.28.1 + caniuse-lite: 1.0.30001761 lodash.memoize: 4.1.2 lodash.uniq: 4.5.0 - caniuse-lite@1.0.30001751: {} + caniuse-lite@1.0.30001761: {} chai@6.2.1: {} + chalk@2.4.2: + dependencies: + ansi-styles: 3.2.1 + escape-string-regexp: 1.0.5 + supports-color: 5.5.0 + chalk@4.1.2: dependencies: ansi-styles: 4.3.0 @@ -10700,6 +11785,10 @@ snapshots: dependencies: readdirp: 4.1.2 + chokidar@5.0.0: + dependencies: + readdirp: 5.0.0 + chownr@1.1.4: {} chownr@3.0.0: {} @@ -10722,13 +11811,6 @@ snapshots: is-wsl: 3.1.0 is64bit: 2.0.0 - clipboardy@5.0.0: - dependencies: - execa: 9.6.0 - is-wayland: 0.1.0 - is-wsl: 3.1.0 - is64bit: 2.0.0 - cliui@8.0.1: dependencies: string-width: 4.2.3 @@ -10741,10 +11823,16 @@ snapshots: cluster-key-slot@1.1.2: {} + color-convert@1.9.3: + dependencies: + color-name: 1.1.3 + color-convert@2.0.1: dependencies: color-name: 1.1.4 + color-name@1.1.3: {} + color-name@1.1.4: {} colord@2.9.3: {} @@ -10815,6 +11903,10 @@ snapshots: dependencies: is-what: 5.5.0 + copy-paste@2.2.0: + dependencies: + iconv-lite: 0.4.24 + core-util-is@1.0.3: {} crc-32@1.2.2: {} @@ -10826,6 +11918,14 @@ snapshots: croner@9.1.0: {} + cross-spawn@6.0.6: + dependencies: + nice-try: 1.0.5 + path-key: 2.0.1 + semver: 5.7.2 + shebang-command: 1.2.0 + which: 1.3.1 + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 @@ -10864,7 +11964,7 @@ snapshots: cssnano-preset-default@7.0.10(postcss@8.5.6): dependencies: - browserslist: 4.27.0 + browserslist: 4.28.1 css-declaration-sorter: 7.3.0(postcss@8.5.6) cssnano-utils: 5.0.1(postcss@8.5.6) postcss: 8.5.6 @@ -10917,7 +12017,9 @@ snapshots: css-tree: 3.1.0 optional: true - csstype@3.1.3: {} + csstype@3.2.3: {} + + cuid@3.0.0: {} damerau-levenshtein@1.0.8: {} @@ -10967,6 +12069,8 @@ snapshots: dependencies: mimic-response: 3.1.0 + dedent-js@1.0.1: {} + deep-extend@0.6.0: {} deep-is@0.1.4: {} @@ -11020,6 +12124,8 @@ snapshots: devalue@5.4.2: {} + devalue@5.6.1: {} + dezalgo@1.0.4: dependencies: asap: 2.0.6 @@ -11078,6 +12184,8 @@ snapshots: electron-to-chromium@1.5.243: {} + electron-to-chromium@1.5.267: {} + elysia@1.4.13(@sinclair/typebox@0.34.41)(exact-mirror@0.2.2(@sinclair/typebox@0.34.41))(file-type@21.0.0)(openapi-types@12.1.3)(typescript@5.9.3): dependencies: '@sinclair/typebox': 0.34.41 @@ -11112,6 +12220,12 @@ snapshots: entities@6.0.1: optional: true + entities@7.0.0: {} + + error-ex@1.3.4: + dependencies: + is-arrayish: 0.2.1 + error-stack-parser-es@1.0.5: {} errx@0.1.0: {} @@ -11276,10 +12390,41 @@ snapshots: '@esbuild/win32-ia32': 0.25.5 '@esbuild/win32-x64': 0.25.5 + esbuild@0.27.2: + optionalDependencies: + '@esbuild/aix-ppc64': 0.27.2 + '@esbuild/android-arm': 0.27.2 + '@esbuild/android-arm64': 0.27.2 + '@esbuild/android-x64': 0.27.2 + '@esbuild/darwin-arm64': 0.27.2 + '@esbuild/darwin-x64': 0.27.2 + '@esbuild/freebsd-arm64': 0.27.2 + '@esbuild/freebsd-x64': 0.27.2 + '@esbuild/linux-arm': 0.27.2 + '@esbuild/linux-arm64': 0.27.2 + '@esbuild/linux-ia32': 0.27.2 + '@esbuild/linux-loong64': 0.27.2 + '@esbuild/linux-mips64el': 0.27.2 + '@esbuild/linux-ppc64': 0.27.2 + '@esbuild/linux-riscv64': 0.27.2 + '@esbuild/linux-s390x': 0.27.2 + '@esbuild/linux-x64': 0.27.2 + '@esbuild/netbsd-arm64': 0.27.2 + '@esbuild/netbsd-x64': 0.27.2 + '@esbuild/openbsd-arm64': 0.27.2 + '@esbuild/openbsd-x64': 0.27.2 + '@esbuild/openharmony-arm64': 0.27.2 + '@esbuild/sunos-x64': 0.27.2 + '@esbuild/win32-arm64': 0.27.2 + '@esbuild/win32-ia32': 0.27.2 + '@esbuild/win32-x64': 0.27.2 + escalade@3.2.0: {} escape-html@1.0.3: {} + escape-string-regexp@1.0.5: {} + escape-string-regexp@4.0.0: {} escape-string-regexp@5.0.0: {} @@ -11290,7 +12435,7 @@ snapshots: eslint: 9.29.0(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.46.2(eslint@9.29.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.29.0(jiti@2.6.1)))(eslint@9.29.0(jiti@2.6.1)) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.46.2(eslint@9.29.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.29.0(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.46.2(eslint@9.29.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.46.2(eslint@9.29.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.29.0(jiti@2.6.1)))(eslint@9.29.0(jiti@2.6.1)))(eslint@9.29.0(jiti@2.6.1)) eslint-plugin-jsx-a11y: 6.10.2(eslint@9.29.0(jiti@2.6.1)) eslint-plugin-react: 7.37.5(eslint@9.29.0(jiti@2.6.1)) eslint-plugin-react-hooks: 7.0.1(eslint@9.29.0(jiti@2.6.1)) @@ -11323,7 +12468,7 @@ snapshots: tinyglobby: 0.2.15 unrs-resolver: 1.11.1 optionalDependencies: - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.46.2(eslint@9.29.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.29.0(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.46.2(eslint@9.29.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.46.2(eslint@9.29.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.29.0(jiti@2.6.1)))(eslint@9.29.0(jiti@2.6.1)))(eslint@9.29.0(jiti@2.6.1)) transitivePeerDependencies: - supports-color @@ -11338,7 +12483,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.46.2(eslint@9.29.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.29.0(jiti@2.6.1)): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.46.2(eslint@9.29.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.46.2(eslint@9.29.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.29.0(jiti@2.6.1)))(eslint@9.29.0(jiti@2.6.1)))(eslint@9.29.0(jiti@2.6.1)): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -11482,7 +12627,7 @@ snapshots: dependencies: estraverse: 5.3.0 - esrap@2.1.1: + esrap@2.2.1: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -11581,6 +12726,8 @@ snapshots: exsolve@1.0.7: {} + exsolve@1.0.8: {} + fast-check@3.23.2: dependencies: pure-rand: 6.1.0 @@ -11747,7 +12894,7 @@ snapshots: forwarded@0.2.0: {} - fraction.js@4.3.7: {} + fraction.js@5.3.4: {} fresh@2.0.0: {} @@ -11912,12 +13059,14 @@ snapshots: happy-dom@20.0.10: dependencies: - '@types/node': 20.17.24 + '@types/node': 20.19.24 '@types/whatwg-mimetype': 3.0.2 whatwg-mimetype: 3.0.0 has-bigints@1.1.0: {} + has-flag@3.0.0: {} + has-flag@4.0.0: {} has-property-descriptors@1.0.2: @@ -11948,11 +13097,15 @@ snapshots: hookable@5.5.3: {} + hosted-git-info@2.8.9: {} + html-encoding-sniffer@4.0.0: dependencies: whatwg-encoding: 3.1.1 optional: true + html-escaper@2.0.2: {} + http-errors@2.0.0: dependencies: depd: 2.0.0 @@ -11981,7 +13134,7 @@ snapshots: https-proxy-agent@7.0.6: dependencies: agent-base: 7.1.4 - debug: 4.4.1 + debug: 4.4.3 transitivePeerDependencies: - supports-color @@ -11991,6 +13144,10 @@ snapshots: human-signals@8.0.1: {} + iconv-lite@0.4.24: + dependencies: + safer-buffer: 2.1.2 + iconv-lite@0.6.3: dependencies: safer-buffer: 2.1.2 @@ -12014,10 +13171,10 @@ snapshots: impound@1.0.0: dependencies: - exsolve: 1.0.7 + exsolve: 1.0.8 mocked-exports: 0.1.1 pathe: 2.0.3 - unplugin: 2.3.10 + unplugin: 2.3.11 unplugin-utils: 0.2.5 imurmurhash@0.1.4: {} @@ -12038,7 +13195,7 @@ snapshots: dependencies: '@ioredis/commands': 1.4.0 cluster-key-slot: 1.1.2 - debug: 4.4.1 + debug: 4.4.3 denque: 2.1.0 lodash.defaults: 4.2.0 lodash.isarguments: 3.1.0 @@ -12060,6 +13217,8 @@ snapshots: call-bound: 1.0.4 get-intrinsic: 1.3.0 + is-arrayish@0.2.1: {} + is-async-function@2.1.1: dependencies: async-function: 1.0.0 @@ -12207,8 +13366,6 @@ snapshots: is-unicode-supported@2.1.0: {} - is-wayland@0.1.0: {} - is-weakmap@2.0.2: {} is-weakref@1.1.1: @@ -12242,6 +13399,27 @@ snapshots: isexe@3.1.1: {} + istanbul-lib-coverage@3.2.2: {} + + istanbul-lib-report@3.0.1: + dependencies: + istanbul-lib-coverage: 3.2.2 + make-dir: 4.0.0 + supports-color: 7.2.0 + + istanbul-lib-source-maps@5.0.6: + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + debug: 4.4.3 + istanbul-lib-coverage: 3.2.2 + transitivePeerDependencies: + - supports-color + + istanbul-reports@3.2.0: + dependencies: + html-escaper: 2.0.2 + istanbul-lib-report: 3.0.1 + iterator.prototype@1.1.5: dependencies: define-data-property: 1.1.4 @@ -12307,6 +13485,8 @@ snapshots: json-buffer@3.0.1: {} + json-parse-better-errors@1.0.2: {} + json-schema-ref-resolver@3.0.0: dependencies: dequal: 2.0.3 @@ -12360,7 +13540,7 @@ snapshots: klona@2.0.6: {} - knitwork@1.2.0: {} + knitwork@1.3.0: {} kysely-bun-sqlite@0.4.0(kysely@0.28.8): dependencies: @@ -12492,6 +13672,13 @@ snapshots: untun: 0.1.3 uqr: 0.1.2 + load-json-file@4.0.0: + dependencies: + graceful-fs: 4.2.11 + parse-json: 4.0.0 + pify: 3.0.0 + strip-bom: 3.0.0 + load-tsconfig@0.2.5: {} local-pkg@1.1.2: @@ -12553,7 +13740,7 @@ snapshots: regexp-tree: 0.1.27 type-level-regexp: 0.1.17 ufo: 1.6.1 - unplugin: 2.3.10 + unplugin: 2.3.11 magic-string-ast@1.0.3: dependencies: @@ -12572,13 +13759,18 @@ snapshots: '@babel/parser': 7.28.5 '@babel/types': 7.28.5 source-map-js: 1.2.1 + optional: true - magicast@0.5.0: + magicast@0.5.1: dependencies: '@babel/parser': 7.28.5 '@babel/types': 7.28.5 source-map-js: 1.2.1 + make-dir@4.0.0: + dependencies: + semver: 7.7.3 + math-intrinsics@1.1.0: {} mdn-data@2.0.28: {} @@ -12589,6 +13781,8 @@ snapshots: memoirist@0.4.0: {} + memorystream@0.3.1: {} + merge-descriptors@2.0.0: {} merge-stream@2.0.0: {} @@ -12712,7 +13906,7 @@ snapshots: dependencies: '@next/env': 16.0.10 '@swc/helpers': 0.5.15 - caniuse-lite: 1.0.30001751 + caniuse-lite: 1.0.30001761 postcss: 8.4.31 react: 19.2.0 react-dom: 19.2.0(react@19.2.0) @@ -12731,6 +13925,8 @@ snapshots: - '@babel/core' - babel-plugin-macros + nice-try@1.0.5: {} + nitropack@2.12.9(better-sqlite3@12.5.0): dependencies: '@cloudflare/kv-asset-handler': 0.4.0 @@ -12743,7 +13939,7 @@ snapshots: '@rollup/plugin-terser': 0.4.4(rollup@4.52.5) '@vercel/nft': 0.30.3(rollup@4.52.5) archiver: 7.0.1 - c12: 3.3.1(magicast@0.5.0) + c12: 3.3.3(magicast@0.5.1) chokidar: 4.0.3 citty: 0.1.6 compatx: 0.2.0 @@ -12759,7 +13955,7 @@ snapshots: esbuild: 0.25.11 escape-string-regexp: 5.0.0 etag: 1.8.1 - exsolve: 1.0.7 + exsolve: 1.0.8 globby: 15.0.0 gzip-size: 7.0.0 h3: 1.15.4 @@ -12768,15 +13964,15 @@ snapshots: ioredis: 5.8.2 jiti: 2.6.1 klona: 2.0.6 - knitwork: 1.2.0 + knitwork: 1.3.0 listhen: 1.9.0 magic-string: 0.30.21 - magicast: 0.5.0 + magicast: 0.5.1 mime: 4.1.0 mlly: 1.8.0 node-fetch-native: 1.6.7 node-mock-http: 1.0.3 - ofetch: 1.5.0 + ofetch: 1.5.1 ohash: 2.0.11 pathe: 2.0.3 perfect-debounce: 2.0.0 @@ -12798,10 +13994,10 @@ snapshots: unenv: 2.0.0-rc.24 unimport: 5.5.0 unplugin-utils: 0.3.1 - unstorage: 1.17.1(db0@0.3.4(better-sqlite3@12.5.0))(ioredis@5.8.2) + unstorage: 1.17.3(db0@0.3.4(better-sqlite3@12.5.0))(ioredis@5.8.2) untyped: 2.0.0 unwasm: 0.3.11 - youch: 4.1.0-beta.11 + youch: 4.1.0-beta.13 youch-core: 0.3.3 transitivePeerDependencies: - '@azure/app-configuration' @@ -12863,9 +14059,26 @@ snapshots: dependencies: abbrev: 3.0.1 + normalize-package-data@2.5.0: + dependencies: + hosted-git-info: 2.8.9 + resolve: 1.22.11 + semver: 5.7.2 + validate-npm-package-license: 3.0.4 + normalize-path@3.0.0: {} - normalize-range@0.1.2: {} + npm-run-all@4.1.5: + dependencies: + ansi-styles: 3.2.1 + chalk: 2.4.2 + cross-spawn: 6.0.6 + memorystream: 0.3.1 + minimatch: 3.1.2 + pidtree: 0.3.1 + read-pkg: 3.0.0 + shell-quote: 1.8.3 + string.prototype.padend: 3.1.6 npm-run-path@5.3.0: dependencies: @@ -12880,47 +14093,47 @@ snapshots: dependencies: boolbase: 1.0.0 - nuxt@4.2.0(@parcel/watcher@2.5.1)(@types/node@20.19.24)(@vue/compiler-sfc@3.5.22)(better-sqlite3@12.5.0)(db0@0.3.4(better-sqlite3@12.5.0))(eslint@9.29.0(jiti@2.6.1))(ioredis@5.8.2)(lightningcss@1.30.2)(magicast@0.5.0)(optionator@0.9.4)(rollup@4.52.5)(terser@5.44.0)(tsx@4.20.3)(typescript@5.9.3)(vite@7.1.12(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.1))(yaml@2.8.1): - dependencies: - '@dxup/nuxt': 0.2.0(magicast@0.5.0) - '@nuxt/cli': 3.29.3(magicast@0.5.0) - '@nuxt/devtools': 2.7.0(vite@7.1.12(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.1))(vue@3.5.22(typescript@5.9.3)) - '@nuxt/kit': 4.2.0(magicast@0.5.0) - '@nuxt/nitro-server': 4.2.0(better-sqlite3@12.5.0)(db0@0.3.4(better-sqlite3@12.5.0))(ioredis@5.8.2)(magicast@0.5.0)(nuxt@4.2.0(@parcel/watcher@2.5.1)(@types/node@20.19.24)(@vue/compiler-sfc@3.5.22)(better-sqlite3@12.5.0)(db0@0.3.4(better-sqlite3@12.5.0))(eslint@9.29.0(jiti@2.6.1))(ioredis@5.8.2)(lightningcss@1.30.2)(magicast@0.5.0)(optionator@0.9.4)(rollup@4.52.5)(terser@5.44.0)(tsx@4.20.3)(typescript@5.9.3)(vite@7.1.12(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.1))(yaml@2.8.1))(typescript@5.9.3) - '@nuxt/schema': 4.2.0 - '@nuxt/telemetry': 2.6.6(magicast@0.5.0) - '@nuxt/vite-builder': 4.2.0(@types/node@20.19.24)(eslint@9.29.0(jiti@2.6.1))(lightningcss@1.30.2)(magicast@0.5.0)(nuxt@4.2.0(@parcel/watcher@2.5.1)(@types/node@20.19.24)(@vue/compiler-sfc@3.5.22)(better-sqlite3@12.5.0)(db0@0.3.4(better-sqlite3@12.5.0))(eslint@9.29.0(jiti@2.6.1))(ioredis@5.8.2)(lightningcss@1.30.2)(magicast@0.5.0)(optionator@0.9.4)(rollup@4.52.5)(terser@5.44.0)(tsx@4.20.3)(typescript@5.9.3)(vite@7.1.12(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.1))(yaml@2.8.1))(optionator@0.9.4)(rollup@4.52.5)(terser@5.44.0)(tsx@4.20.3)(typescript@5.9.3)(vue@3.5.22(typescript@5.9.3))(yaml@2.8.1) - '@unhead/vue': 2.0.19(vue@3.5.22(typescript@5.9.3)) - '@vue/shared': 3.5.22 - c12: 3.3.1(magicast@0.5.0) - chokidar: 4.0.3 + nuxt@4.2.2(@parcel/watcher@2.5.1)(@types/node@20.19.24)(@vue/compiler-sfc@3.5.26)(better-sqlite3@12.5.0)(cac@6.7.14)(db0@0.3.4(better-sqlite3@12.5.0))(eslint@9.29.0(jiti@2.6.1))(ioredis@5.8.2)(lightningcss@1.30.2)(magicast@0.5.1)(optionator@0.9.4)(rollup@4.52.5)(terser@5.44.0)(tsx@4.20.3)(typescript@5.9.3)(vite@7.3.0(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.2))(yaml@2.8.2): + dependencies: + '@dxup/nuxt': 0.2.2(magicast@0.5.1) + '@nuxt/cli': 3.31.3(cac@6.7.14)(magicast@0.5.1) + '@nuxt/devtools': 3.1.1(vite@7.3.0(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.2))(vue@3.5.26(typescript@5.9.3)) + '@nuxt/kit': 4.2.2(magicast@0.5.1) + '@nuxt/nitro-server': 4.2.2(better-sqlite3@12.5.0)(db0@0.3.4(better-sqlite3@12.5.0))(ioredis@5.8.2)(magicast@0.5.1)(nuxt@4.2.2(@parcel/watcher@2.5.1)(@types/node@20.19.24)(@vue/compiler-sfc@3.5.26)(better-sqlite3@12.5.0)(cac@6.7.14)(db0@0.3.4(better-sqlite3@12.5.0))(eslint@9.29.0(jiti@2.6.1))(ioredis@5.8.2)(lightningcss@1.30.2)(magicast@0.5.1)(optionator@0.9.4)(rollup@4.52.5)(terser@5.44.0)(tsx@4.20.3)(typescript@5.9.3)(vite@7.3.0(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.2))(yaml@2.8.2))(typescript@5.9.3) + '@nuxt/schema': 4.2.2 + '@nuxt/telemetry': 2.6.6(magicast@0.5.1) + '@nuxt/vite-builder': 4.2.2(@types/node@20.19.24)(eslint@9.29.0(jiti@2.6.1))(lightningcss@1.30.2)(magicast@0.5.1)(nuxt@4.2.2(@parcel/watcher@2.5.1)(@types/node@20.19.24)(@vue/compiler-sfc@3.5.26)(better-sqlite3@12.5.0)(cac@6.7.14)(db0@0.3.4(better-sqlite3@12.5.0))(eslint@9.29.0(jiti@2.6.1))(ioredis@5.8.2)(lightningcss@1.30.2)(magicast@0.5.1)(optionator@0.9.4)(rollup@4.52.5)(terser@5.44.0)(tsx@4.20.3)(typescript@5.9.3)(vite@7.3.0(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.2))(yaml@2.8.2))(optionator@0.9.4)(rollup@4.52.5)(terser@5.44.0)(tsx@4.20.3)(typescript@5.9.3)(vue@3.5.26(typescript@5.9.3))(yaml@2.8.2) + '@unhead/vue': 2.0.19(vue@3.5.26(typescript@5.9.3)) + '@vue/shared': 3.5.26 + c12: 3.3.3(magicast@0.5.1) + chokidar: 5.0.0 compatx: 0.2.0 consola: 3.4.2 cookie-es: 2.0.0 defu: 6.1.4 destr: 2.0.5 - devalue: 5.4.2 + devalue: 5.6.1 errx: 0.1.0 escape-string-regexp: 5.0.0 - exsolve: 1.0.7 + exsolve: 1.0.8 h3: 1.15.4 hookable: 5.5.3 ignore: 7.0.5 impound: 1.0.0 jiti: 2.6.1 klona: 2.0.6 - knitwork: 1.2.0 + knitwork: 1.3.0 magic-string: 0.30.21 mlly: 1.8.0 nanotar: 0.2.0 nypm: 0.6.2 - ofetch: 1.5.0 + ofetch: 1.5.1 ohash: 2.0.11 on-change: 6.0.1 - oxc-minify: 0.95.0 - oxc-parser: 0.95.0 - oxc-transform: 0.95.0 - oxc-walker: 0.5.2(oxc-parser@0.95.0) + oxc-minify: 0.102.0 + oxc-parser: 0.102.0 + oxc-transform: 0.102.0 + oxc-walker: 0.6.0(oxc-parser@0.102.0) pathe: 2.0.3 perfect-debounce: 2.0.0 pkg-types: 2.3.0 @@ -12934,11 +14147,11 @@ snapshots: uncrypto: 0.1.3 unctx: 2.4.1 unimport: 5.5.0 - unplugin: 2.3.10 - unplugin-vue-router: 0.16.0(@vue/compiler-sfc@3.5.22)(typescript@5.9.3)(vue-router@4.6.3(vue@3.5.22(typescript@5.9.3)))(vue@3.5.22(typescript@5.9.3)) + unplugin: 2.3.11 + unplugin-vue-router: 0.19.1(@vue/compiler-sfc@3.5.26)(vue-router@4.6.4(vue@3.5.26(typescript@5.9.3)))(vue@3.5.26(typescript@5.9.3)) untyped: 2.0.0 - vue: 3.5.22(typescript@5.9.3) - vue-router: 4.6.3(vue@3.5.22(typescript@5.9.3)) + vue: 3.5.26(typescript@5.9.3) + vue-router: 4.6.4(vue@3.5.26(typescript@5.9.3)) optionalDependencies: '@parcel/watcher': 2.5.1 '@types/node': 20.19.24 @@ -12960,11 +14173,14 @@ snapshots: - '@vercel/blob' - '@vercel/functions' - '@vercel/kv' + - '@vitejs/devtools' - '@vue/compiler-sfc' - aws4fetch - bare-abort-controller - better-sqlite3 - bufferutil + - cac + - commander - db0 - drizzle-orm - encoding @@ -13054,7 +14270,7 @@ snapshots: obug@2.1.1: {} - ofetch@1.5.0: + ofetch@1.5.1: dependencies: destr: 2.0.5 node-fetch-native: 1.6.7 @@ -13126,66 +14342,66 @@ snapshots: object-keys: 1.1.1 safe-push-apply: 1.0.0 - oxc-minify@0.95.0: + oxc-minify@0.102.0: optionalDependencies: - '@oxc-minify/binding-android-arm64': 0.95.0 - '@oxc-minify/binding-darwin-arm64': 0.95.0 - '@oxc-minify/binding-darwin-x64': 0.95.0 - '@oxc-minify/binding-freebsd-x64': 0.95.0 - '@oxc-minify/binding-linux-arm-gnueabihf': 0.95.0 - '@oxc-minify/binding-linux-arm-musleabihf': 0.95.0 - '@oxc-minify/binding-linux-arm64-gnu': 0.95.0 - '@oxc-minify/binding-linux-arm64-musl': 0.95.0 - '@oxc-minify/binding-linux-riscv64-gnu': 0.95.0 - '@oxc-minify/binding-linux-s390x-gnu': 0.95.0 - '@oxc-minify/binding-linux-x64-gnu': 0.95.0 - '@oxc-minify/binding-linux-x64-musl': 0.95.0 - '@oxc-minify/binding-wasm32-wasi': 0.95.0 - '@oxc-minify/binding-win32-arm64-msvc': 0.95.0 - '@oxc-minify/binding-win32-x64-msvc': 0.95.0 - - oxc-parser@0.95.0: - dependencies: - '@oxc-project/types': 0.95.0 + '@oxc-minify/binding-android-arm64': 0.102.0 + '@oxc-minify/binding-darwin-arm64': 0.102.0 + '@oxc-minify/binding-darwin-x64': 0.102.0 + '@oxc-minify/binding-freebsd-x64': 0.102.0 + '@oxc-minify/binding-linux-arm-gnueabihf': 0.102.0 + '@oxc-minify/binding-linux-arm64-gnu': 0.102.0 + '@oxc-minify/binding-linux-arm64-musl': 0.102.0 + '@oxc-minify/binding-linux-riscv64-gnu': 0.102.0 + '@oxc-minify/binding-linux-s390x-gnu': 0.102.0 + '@oxc-minify/binding-linux-x64-gnu': 0.102.0 + '@oxc-minify/binding-linux-x64-musl': 0.102.0 + '@oxc-minify/binding-openharmony-arm64': 0.102.0 + '@oxc-minify/binding-wasm32-wasi': 0.102.0 + '@oxc-minify/binding-win32-arm64-msvc': 0.102.0 + '@oxc-minify/binding-win32-x64-msvc': 0.102.0 + + oxc-parser@0.102.0: + dependencies: + '@oxc-project/types': 0.102.0 optionalDependencies: - '@oxc-parser/binding-android-arm64': 0.95.0 - '@oxc-parser/binding-darwin-arm64': 0.95.0 - '@oxc-parser/binding-darwin-x64': 0.95.0 - '@oxc-parser/binding-freebsd-x64': 0.95.0 - '@oxc-parser/binding-linux-arm-gnueabihf': 0.95.0 - '@oxc-parser/binding-linux-arm-musleabihf': 0.95.0 - '@oxc-parser/binding-linux-arm64-gnu': 0.95.0 - '@oxc-parser/binding-linux-arm64-musl': 0.95.0 - '@oxc-parser/binding-linux-riscv64-gnu': 0.95.0 - '@oxc-parser/binding-linux-s390x-gnu': 0.95.0 - '@oxc-parser/binding-linux-x64-gnu': 0.95.0 - '@oxc-parser/binding-linux-x64-musl': 0.95.0 - '@oxc-parser/binding-wasm32-wasi': 0.95.0 - '@oxc-parser/binding-win32-arm64-msvc': 0.95.0 - '@oxc-parser/binding-win32-x64-msvc': 0.95.0 - - oxc-transform@0.95.0: + '@oxc-parser/binding-android-arm64': 0.102.0 + '@oxc-parser/binding-darwin-arm64': 0.102.0 + '@oxc-parser/binding-darwin-x64': 0.102.0 + '@oxc-parser/binding-freebsd-x64': 0.102.0 + '@oxc-parser/binding-linux-arm-gnueabihf': 0.102.0 + '@oxc-parser/binding-linux-arm64-gnu': 0.102.0 + '@oxc-parser/binding-linux-arm64-musl': 0.102.0 + '@oxc-parser/binding-linux-riscv64-gnu': 0.102.0 + '@oxc-parser/binding-linux-s390x-gnu': 0.102.0 + '@oxc-parser/binding-linux-x64-gnu': 0.102.0 + '@oxc-parser/binding-linux-x64-musl': 0.102.0 + '@oxc-parser/binding-openharmony-arm64': 0.102.0 + '@oxc-parser/binding-wasm32-wasi': 0.102.0 + '@oxc-parser/binding-win32-arm64-msvc': 0.102.0 + '@oxc-parser/binding-win32-x64-msvc': 0.102.0 + + oxc-transform@0.102.0: optionalDependencies: - '@oxc-transform/binding-android-arm64': 0.95.0 - '@oxc-transform/binding-darwin-arm64': 0.95.0 - '@oxc-transform/binding-darwin-x64': 0.95.0 - '@oxc-transform/binding-freebsd-x64': 0.95.0 - '@oxc-transform/binding-linux-arm-gnueabihf': 0.95.0 - '@oxc-transform/binding-linux-arm-musleabihf': 0.95.0 - '@oxc-transform/binding-linux-arm64-gnu': 0.95.0 - '@oxc-transform/binding-linux-arm64-musl': 0.95.0 - '@oxc-transform/binding-linux-riscv64-gnu': 0.95.0 - '@oxc-transform/binding-linux-s390x-gnu': 0.95.0 - '@oxc-transform/binding-linux-x64-gnu': 0.95.0 - '@oxc-transform/binding-linux-x64-musl': 0.95.0 - '@oxc-transform/binding-wasm32-wasi': 0.95.0 - '@oxc-transform/binding-win32-arm64-msvc': 0.95.0 - '@oxc-transform/binding-win32-x64-msvc': 0.95.0 - - oxc-walker@0.5.2(oxc-parser@0.95.0): + '@oxc-transform/binding-android-arm64': 0.102.0 + '@oxc-transform/binding-darwin-arm64': 0.102.0 + '@oxc-transform/binding-darwin-x64': 0.102.0 + '@oxc-transform/binding-freebsd-x64': 0.102.0 + '@oxc-transform/binding-linux-arm-gnueabihf': 0.102.0 + '@oxc-transform/binding-linux-arm64-gnu': 0.102.0 + '@oxc-transform/binding-linux-arm64-musl': 0.102.0 + '@oxc-transform/binding-linux-riscv64-gnu': 0.102.0 + '@oxc-transform/binding-linux-s390x-gnu': 0.102.0 + '@oxc-transform/binding-linux-x64-gnu': 0.102.0 + '@oxc-transform/binding-linux-x64-musl': 0.102.0 + '@oxc-transform/binding-openharmony-arm64': 0.102.0 + '@oxc-transform/binding-wasm32-wasi': 0.102.0 + '@oxc-transform/binding-win32-arm64-msvc': 0.102.0 + '@oxc-transform/binding-win32-x64-msvc': 0.102.0 + + oxc-walker@0.6.0(oxc-parser@0.102.0): dependencies: magic-regexp: 0.10.0 - oxc-parser: 0.95.0 + oxc-parser: 0.102.0 p-limit@3.1.0: dependencies: @@ -13203,6 +14419,11 @@ snapshots: dependencies: callsites: 3.1.0 + parse-json@4.0.0: + dependencies: + error-ex: 1.3.4 + json-parse-better-errors: 1.0.2 + parse-ms@4.0.0: {} parse-path@7.1.0: @@ -13225,6 +14446,8 @@ snapshots: path-exists@4.0.0: {} + path-key@2.0.1: {} + path-key@3.1.1: {} path-key@4.0.0: {} @@ -13243,6 +14466,10 @@ snapshots: path-to-regexp@8.3.0: {} + path-type@3.0.0: + dependencies: + pify: 3.0.0 + path-type@6.0.0: {} pathe@1.1.2: {} @@ -13308,6 +14535,10 @@ snapshots: picomatch@4.0.3: {} + pidtree@0.3.1: {} + + pify@3.0.0: {} + pino-abstract-transport@2.0.0: dependencies: split2: 4.2.0 @@ -13354,7 +14585,7 @@ snapshots: postcss-colormin@7.0.5(postcss@8.5.6): dependencies: - browserslist: 4.27.0 + browserslist: 4.28.1 caniuse-api: 3.0.0 colord: 2.9.3 postcss: 8.5.6 @@ -13362,7 +14593,7 @@ snapshots: postcss-convert-values@7.0.8(postcss@8.5.6): dependencies: - browserslist: 4.27.0 + browserslist: 4.28.1 postcss: 8.5.6 postcss-value-parser: 4.2.0 @@ -13400,7 +14631,7 @@ snapshots: postcss-merge-rules@7.0.7(postcss@8.5.6): dependencies: - browserslist: 4.27.0 + browserslist: 4.28.1 caniuse-api: 3.0.0 cssnano-utils: 5.0.1(postcss@8.5.6) postcss: 8.5.6 @@ -13420,7 +14651,7 @@ snapshots: postcss-minify-params@7.0.5(postcss@8.5.6): dependencies: - browserslist: 4.27.0 + browserslist: 4.28.1 cssnano-utils: 5.0.1(postcss@8.5.6) postcss: 8.5.6 postcss-value-parser: 4.2.0 @@ -13462,7 +14693,7 @@ snapshots: postcss-normalize-unicode@7.0.5(postcss@8.5.6): dependencies: - browserslist: 4.27.0 + browserslist: 4.28.1 postcss: 8.5.6 postcss-value-parser: 4.2.0 @@ -13484,7 +14715,7 @@ snapshots: postcss-reduce-initial@7.0.5(postcss@8.5.6): dependencies: - browserslist: 4.27.0 + browserslist: 4.28.1 caniuse-api: 3.0.0 postcss: 8.5.6 @@ -13562,6 +14793,10 @@ snapshots: prelude-ls@1.2.1: {} + prettier-plugin-tailwindcss@0.7.2(prettier@3.5.3): + dependencies: + prettier: 3.5.3 + prettier@3.5.3: {} pretty-bytes@7.1.0: {} @@ -13682,6 +14917,12 @@ snapshots: react@19.2.0: {} + read-pkg@3.0.0: + dependencies: + load-json-file: 4.0.0 + normalize-package-data: 2.5.0 + path-type: 3.0.0 + readable-stream@2.3.8: dependencies: core-util-is: 1.0.3 @@ -13712,6 +14953,8 @@ snapshots: readdirp@4.1.2: {} + readdirp@5.0.0: {} + real-require@0.2.0: {} redis-errors@1.2.0: {} @@ -13908,6 +15151,8 @@ snapshots: secure-json-parse@4.1.0: {} + semver@5.7.2: {} + semver@6.3.1: {} semver@7.7.2: {} @@ -13934,7 +15179,7 @@ snapshots: dependencies: randombytes: 2.1.0 - seroval@1.3.2: {} + seroval@1.4.1: {} serve-placeholder@2.0.2: dependencies: @@ -14005,10 +15250,16 @@ snapshots: '@img/sharp-win32-x64': 0.34.4 optional: true + shebang-command@1.2.0: + dependencies: + shebang-regex: 1.0.0 + shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 + shebang-regex@1.0.0: {} + shebang-regex@3.0.0: {} shell-quote@1.8.3: {} @@ -14055,11 +15306,11 @@ snapshots: once: 1.4.0 simple-concat: 1.0.1 - simple-git@3.28.0: + simple-git@3.30.0: dependencies: '@kwsites/file-exists': 1.1.1 '@kwsites/promise-deferred': 1.1.1 - debug: 4.4.1 + debug: 4.4.3 transitivePeerDependencies: - supports-color @@ -14094,13 +15345,27 @@ snapshots: dependencies: whatwg-url: 7.1.0 + spdx-correct@3.2.0: + dependencies: + spdx-expression-parse: 3.0.1 + spdx-license-ids: 3.0.22 + + spdx-exceptions@2.5.0: {} + + spdx-expression-parse@3.0.1: + dependencies: + spdx-exceptions: 2.5.0 + spdx-license-ids: 3.0.22 + + spdx-license-ids@3.0.22: {} + speakingurl@14.0.1: {} split2@4.2.0: {} sql.js@1.13.0: {} - srvx@0.8.16: {} + srvx@0.9.8: {} stable-hash@0.0.5: {} @@ -14162,6 +15427,13 @@ snapshots: set-function-name: 2.0.2 side-channel: 1.1.0 + string.prototype.padend@3.1.6: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.0 + es-object-atoms: 1.1.1 + string.prototype.repeat@1.0.0: dependencies: define-properties: 1.2.1 @@ -14235,7 +15507,7 @@ snapshots: stylehacks@7.0.7(postcss@8.5.6): dependencies: - browserslist: 4.27.0 + browserslist: 4.28.1 postcss: 8.5.6 postcss-selector-parser: 7.1.0 @@ -14276,13 +15548,36 @@ snapshots: supports-color@10.2.2: {} + supports-color@5.5.0: + dependencies: + has-flag: 3.0.0 + supports-color@7.2.0: dependencies: has-flag: 4.0.0 supports-preserve-symlinks-flag@1.0.0: {} - svelte@5.43.3: + svelte-check@4.3.5(picomatch@4.0.3)(svelte@5.45.6)(typescript@5.9.3): + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + chokidar: 4.0.3 + fdir: 6.5.0(picomatch@4.0.3) + picocolors: 1.1.1 + sade: 1.8.1 + svelte: 5.45.6 + typescript: 5.9.3 + transitivePeerDependencies: + - picomatch + + svelte2tsx@0.7.46(svelte@5.45.6)(typescript@5.9.3): + dependencies: + dedent-js: 1.0.1 + scule: 1.3.0 + svelte: 5.45.6 + typescript: 5.9.3 + + svelte@5.45.6: dependencies: '@jridgewell/remapping': 2.3.5 '@jridgewell/sourcemap-codec': 1.5.5 @@ -14292,8 +15587,27 @@ snapshots: aria-query: 5.3.2 axobject-query: 4.1.0 clsx: 2.1.1 + devalue: 5.6.1 esm-env: 1.2.2 - esrap: 2.1.1 + esrap: 2.2.1 + is-reference: 3.0.3 + locate-character: 3.0.0 + magic-string: 0.30.21 + zimmerframe: 1.1.4 + + svelte@5.46.1: + dependencies: + '@jridgewell/remapping': 2.3.5 + '@jridgewell/sourcemap-codec': 1.5.5 + '@sveltejs/acorn-typescript': 1.0.6(acorn@8.15.0) + '@types/estree': 1.0.8 + acorn: 8.15.0 + aria-query: 5.3.2 + axobject-query: 4.1.0 + clsx: 2.1.1 + devalue: 5.6.1 + esm-env: 1.2.2 + esrap: 2.2.1 is-reference: 3.0.3 locate-character: 3.0.0 magic-string: 0.30.21 @@ -14318,6 +15632,8 @@ snapshots: tailwindcss@4.1.16: {} + tailwindcss@4.1.18: {} + tapable@2.3.0: {} tar-fs@2.1.2: @@ -14385,6 +15701,8 @@ snapshots: tinyexec@1.0.1: {} + tinyexec@1.0.2: {} + tinyglobby@0.2.14: dependencies: fdir: 6.4.6(picomatch@4.0.2) @@ -14629,14 +15947,12 @@ snapshots: acorn: 8.15.0 estree-walker: 3.0.3 magic-string: 0.30.21 - unplugin: 2.3.10 + unplugin: 2.3.11 undici-types@6.19.8: {} undici-types@6.21.0: {} - undici@7.16.0: {} - unenv@2.0.0-rc.24: dependencies: pathe: 2.0.3 @@ -14661,7 +15977,7 @@ snapshots: scule: 1.3.0 strip-literal: 3.1.0 tinyglobby: 0.2.15 - unplugin: 2.3.10 + unplugin: 2.3.11 unplugin-utils: 0.3.1 universalify@2.0.1: {} @@ -14678,14 +15994,14 @@ snapshots: pathe: 2.0.3 picomatch: 4.0.3 - unplugin-vue-router@0.16.0(@vue/compiler-sfc@3.5.22)(typescript@5.9.3)(vue-router@4.6.3(vue@3.5.22(typescript@5.9.3)))(vue@3.5.22(typescript@5.9.3)): + unplugin-vue-router@0.19.1(@vue/compiler-sfc@3.5.26)(vue-router@4.6.4(vue@3.5.26(typescript@5.9.3)))(vue@3.5.26(typescript@5.9.3)): dependencies: '@babel/generator': 7.28.5 - '@vue-macros/common': 3.1.1(vue@3.5.22(typescript@5.9.3)) - '@vue/compiler-sfc': 3.5.22 - '@vue/language-core': 3.1.2(typescript@5.9.3) + '@vue-macros/common': 3.1.1(vue@3.5.26(typescript@5.9.3)) + '@vue/compiler-sfc': 3.5.26 + '@vue/language-core': 3.2.1 ast-walker-scope: 0.8.3 - chokidar: 4.0.3 + chokidar: 5.0.0 json5: 2.2.3 local-pkg: 1.1.2 magic-string: 0.30.21 @@ -14695,16 +16011,15 @@ snapshots: picomatch: 4.0.3 scule: 1.3.0 tinyglobby: 0.2.15 - unplugin: 2.3.10 + unplugin: 2.3.11 unplugin-utils: 0.3.1 - yaml: 2.8.1 + yaml: 2.8.2 optionalDependencies: - vue-router: 4.6.3(vue@3.5.22(typescript@5.9.3)) + vue-router: 4.6.4(vue@3.5.26(typescript@5.9.3)) transitivePeerDependencies: - - typescript - vue - unplugin@2.3.10: + unplugin@2.3.11: dependencies: '@jridgewell/remapping': 2.3.5 acorn: 8.15.0 @@ -14735,7 +16050,7 @@ snapshots: '@unrs/resolver-binding-win32-ia32-msvc': 1.11.1 '@unrs/resolver-binding-win32-x64-msvc': 1.11.1 - unstorage@1.17.1(db0@0.3.4(better-sqlite3@12.5.0))(ioredis@5.8.2): + unstorage@1.17.3(db0@0.3.4(better-sqlite3@12.5.0))(ioredis@5.8.2): dependencies: anymatch: 3.1.3 chokidar: 4.0.3 @@ -14743,7 +16058,7 @@ snapshots: h3: 1.15.4 lru-cache: 10.4.3 node-fetch-native: 1.6.7 - ofetch: 1.5.0 + ofetch: 1.5.1 ufo: 1.6.1 optionalDependencies: db0: 0.3.4(better-sqlite3@12.5.0) @@ -14760,17 +16075,17 @@ snapshots: citty: 0.1.6 defu: 6.1.4 jiti: 2.6.1 - knitwork: 1.2.0 + knitwork: 1.3.0 scule: 1.3.0 unwasm@0.3.11: dependencies: - knitwork: 1.2.0 + knitwork: 1.3.0 magic-string: 0.30.21 mlly: 1.8.0 pathe: 2.0.3 pkg-types: 2.3.0 - unplugin: 2.3.10 + unplugin: 2.3.11 update-browserslist-db@1.1.4(browserslist@4.27.0): dependencies: @@ -14778,6 +16093,12 @@ snapshots: escalade: 3.2.0 picocolors: 1.1.1 + update-browserslist-db@1.2.3(browserslist@4.28.1): + dependencies: + browserslist: 4.28.1 + escalade: 3.2.0 + picocolors: 1.1.1 + uqr@0.1.2: {} uri-js@4.4.1: @@ -14790,25 +16111,30 @@ snapshots: uuid@11.0.5: {} + validate-npm-package-license@3.0.4: + dependencies: + spdx-correct: 3.2.0 + spdx-expression-parse: 3.0.1 + vary@1.1.2: {} - vite-dev-rpc@1.1.0(vite@7.1.12(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.1)): + vite-dev-rpc@1.1.0(vite@7.3.0(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.2)): dependencies: - birpc: 2.6.1 - vite: 7.1.12(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.1) - vite-hot-client: 2.1.0(vite@7.1.12(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.1)) + birpc: 2.9.0 + vite: 7.3.0(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.2) + vite-hot-client: 2.1.0(vite@7.3.0(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.2)) - vite-hot-client@2.1.0(vite@7.1.12(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.1)): + vite-hot-client@2.1.0(vite@7.3.0(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.2)): dependencies: - vite: 7.1.12(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.1) + vite: 7.3.0(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.2) - vite-node@3.2.4(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.1): + vite-node@5.2.0(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.2): dependencies: cac: 6.7.14 - debug: 4.4.1 es-module-lexer: 1.7.0 + obug: 2.1.1 pathe: 2.0.3 - vite: 7.1.12(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.1) + vite: 7.3.0(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.2) transitivePeerDependencies: - '@types/node' - jiti @@ -14818,12 +16144,11 @@ snapshots: - sass-embedded - stylus - sugarss - - supports-color - terser - tsx - yaml - vite-plugin-checker@0.11.0(eslint@9.29.0(jiti@2.6.1))(optionator@0.9.4)(typescript@5.9.3)(vite@7.1.12(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.1)): + vite-plugin-checker@0.12.0(eslint@9.29.0(jiti@2.6.1))(optionator@0.9.4)(typescript@5.9.3)(vite@7.3.0(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.2)): dependencies: '@babel/code-frame': 7.27.1 chokidar: 4.0.3 @@ -14832,43 +16157,43 @@ snapshots: picomatch: 4.0.3 tiny-invariant: 1.3.3 tinyglobby: 0.2.15 - vite: 7.1.12(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.1) + vite: 7.3.0(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.2) vscode-uri: 3.1.0 optionalDependencies: eslint: 9.29.0(jiti@2.6.1) optionator: 0.9.4 typescript: 5.9.3 - vite-plugin-inspect@11.3.3(@nuxt/kit@3.20.0(magicast@0.3.5))(vite@7.1.12(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.1)): + vite-plugin-inspect@11.3.3(@nuxt/kit@4.2.2(magicast@0.5.1))(vite@7.3.0(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.2)): dependencies: ansis: 4.2.0 - debug: 4.4.1 + debug: 4.4.3 error-stack-parser-es: 1.0.5 ohash: 2.0.11 open: 10.2.0 perfect-debounce: 2.0.0 sirv: 3.0.2 unplugin-utils: 0.3.1 - vite: 7.1.12(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.1) - vite-dev-rpc: 1.1.0(vite@7.1.12(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.1)) + vite: 7.3.0(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.2) + vite-dev-rpc: 1.1.0(vite@7.3.0(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.2)) optionalDependencies: - '@nuxt/kit': 3.20.0(magicast@0.3.5) + '@nuxt/kit': 4.2.2(magicast@0.5.1) transitivePeerDependencies: - supports-color - vite-plugin-vue-tracer@1.0.1(vite@7.1.12(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.1))(vue@3.5.22(typescript@5.9.3)): + vite-plugin-vue-tracer@1.2.0(vite@7.3.0(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.2))(vue@3.5.26(typescript@5.9.3)): dependencies: estree-walker: 3.0.3 - exsolve: 1.0.7 + exsolve: 1.0.8 magic-string: 0.30.21 pathe: 2.0.3 source-map-js: 1.2.1 - vite: 7.1.12(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.1) - vue: 3.5.22(typescript@5.9.3) + vite: 7.3.0(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.2) + vue: 3.5.26(typescript@5.9.3) - vite@7.1.12(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0): + vite@7.3.0(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0): dependencies: - esbuild: 0.25.11 + esbuild: 0.27.2 fdir: 6.5.0(picomatch@4.0.3) picomatch: 4.0.3 postcss: 8.5.6 @@ -14883,9 +16208,9 @@ snapshots: tsx: 4.20.3 yaml: 2.8.0 - vite@7.1.12(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.1): + vite@7.3.0(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.2): dependencies: - esbuild: 0.25.11 + esbuild: 0.27.2 fdir: 6.5.0(picomatch@4.0.3) picomatch: 4.0.3 postcss: 8.5.6 @@ -14898,16 +16223,16 @@ snapshots: lightningcss: 1.30.2 terser: 5.44.0 tsx: 4.20.3 - yaml: 2.8.1 + yaml: 2.8.2 - vitefu@1.1.1(vite@7.1.12(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.1)): + vitefu@1.1.1(vite@7.3.0(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.2)): optionalDependencies: - vite: 7.1.12(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.1) + vite: 7.3.0(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.2) vitest@4.0.14(@edge-runtime/vm@5.0.0)(@types/node@20.19.24)(happy-dom@20.0.10)(jiti@2.6.1)(jsdom@27.1.0)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0): dependencies: '@vitest/expect': 4.0.14 - '@vitest/mocker': 4.0.14(vite@7.1.12(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0)) + '@vitest/mocker': 4.0.14(vite@7.3.0(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0)) '@vitest/pretty-format': 4.0.14 '@vitest/runner': 4.0.14 '@vitest/snapshot': 4.0.14 @@ -14924,7 +16249,7 @@ snapshots: tinyexec: 0.3.2 tinyglobby: 0.2.15 tinyrainbow: 3.0.3 - vite: 7.1.12(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0) + vite: 7.3.0(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0) why-is-node-running: 2.3.0 optionalDependencies: '@edge-runtime/vm': 5.0.0 @@ -14979,11 +16304,16 @@ snapshots: vue-devtools-stub@0.1.0: {} - vue-router@4.6.3(vue@3.5.22(typescript@5.9.3)): + vue-router@4.6.4(vue@3.5.22(typescript@5.9.3)): dependencies: '@vue/devtools-api': 6.6.4 vue: 3.5.22(typescript@5.9.3) + vue-router@4.6.4(vue@3.5.26(typescript@5.9.3)): + dependencies: + '@vue/devtools-api': 6.6.4 + vue: 3.5.26(typescript@5.9.3) + vue@3.5.22(typescript@5.9.3): dependencies: '@vue/compiler-dom': 3.5.22 @@ -14994,6 +16324,16 @@ snapshots: optionalDependencies: typescript: 5.9.3 + vue@3.5.26(typescript@5.9.3): + dependencies: + '@vue/compiler-dom': 3.5.26 + '@vue/compiler-sfc': 3.5.26 + '@vue/runtime-dom': 3.5.26 + '@vue/server-renderer': 3.5.26(vue@3.5.26(typescript@5.9.3)) + '@vue/shared': 3.5.26 + optionalDependencies: + typescript: 5.9.3 + w3c-xmlserializer@5.0.0: dependencies: xml-name-validator: 5.0.0 @@ -15080,6 +16420,10 @@ snapshots: gopd: 1.2.0 has-tostringtag: 1.0.2 + which@1.3.1: + dependencies: + isexe: 2.0.0 + which@2.0.2: dependencies: isexe: 2.0.0 @@ -15131,7 +16475,7 @@ snapshots: yaml@2.8.0: {} - yaml@2.8.1: {} + yaml@2.8.2: {} yargs-parser@21.1.1: {} @@ -15154,12 +16498,12 @@ snapshots: '@poppinss/exception': 1.2.2 error-stack-parser-es: 1.0.5 - youch@4.1.0-beta.11: + youch@4.1.0-beta.13: dependencies: '@poppinss/colors': 4.1.5 - '@poppinss/dumper': 0.6.4 - '@speed-highlight/core': 1.2.8 - cookie: 1.0.2 + '@poppinss/dumper': 0.6.5 + '@speed-highlight/core': 1.2.12 + cookie-es: 2.0.0 youch-core: 0.3.3 zimmerframe@1.1.4: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index d4e30a0f..f5987bd8 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -4,7 +4,10 @@ packages: - tests/** catalog: - '@tanstack/react-query': 5.90.6 + '@tanstack/query-core': 5.90.2 + '@tanstack/react-query': 5.90.2 + '@tanstack/vue-query': 5.90.2 + '@tanstack/svelte-query': 6.0.10 '@types/better-sqlite3': ^7.6.13 '@types/node': ^20.19.0 '@types/react': 19.2.0 @@ -16,12 +19,14 @@ catalog: langium: 3.5.0 langium-cli: 3.5.0 next: 16.0.10 + nuxt: 4.2.2 + '@sveltejs/kit': 2.49.1 pg: ^8.13.1 prisma: ^6.19.0 react: 19.2.0 react-dom: 19.2.0 sql.js: ^1.13.0 - svelte: 5.43.3 + svelte: 5.45.6 tmp: ^0.2.3 ts-pattern: ^5.7.1 typescript: ^5.9.3 diff --git a/samples/next.js/app/page.tsx b/samples/next.js/app/page.tsx index 86300fbe..4372f2a2 100644 --- a/samples/next.js/app/page.tsx +++ b/samples/next.js/app/page.tsx @@ -1,21 +1,40 @@ 'use client'; +import { Post } from '@/zenstack/models'; import { schema } from '@/zenstack/schema-lite'; -import { useClientQueries } from '@zenstackhq/tanstack-query/react'; +import { FetchFn, useClientQueries } from '@zenstackhq/tanstack-query/react'; import { LoremIpsum } from 'lorem-ipsum'; import Image from 'next/image'; +import { useState } from 'react'; const lorem = new LoremIpsum({ wordsPerSentence: { max: 6, min: 4 } }); export default function Home() { - const clientQueries = useClientQueries(schema); + const [showPublishedOnly, setShowPublishedOnly] = useState(false); + const [enableFetch, setEnableFetch] = useState(true); + const [optimistic, setOptimistic] = useState(false); + + const fetch: FetchFn = async (url, init) => { + // simulate a delay for showing optimistic update effect + await new Promise((resolve) => setTimeout(resolve, 1000)); + return globalThis.fetch(url, init); + }; + + const clientQueries = useClientQueries(schema, { fetch }); const { data: users, isFetched: isUsersFetched } = clientQueries.user.useFindMany(); - const { data: posts } = clientQueries.post.useFindMany({ - orderBy: { createdAt: 'desc' }, - include: { author: true }, - }); - const createPost = clientQueries.post.useCreate(); - const deletePost = clientQueries.post.useDelete(); + + const { data: posts } = clientQueries.post.useFindMany( + { + where: showPublishedOnly ? { published: true } : undefined, + orderBy: { createdAt: 'desc' }, + include: { author: true }, + }, + { enabled: enableFetch }, + ); + + const createPost = clientQueries.post.useCreate({ optimisticUpdate: optimistic }); + const deletePost = clientQueries.post.useDelete({ optimisticUpdate: optimistic }); + const updatePost = clientQueries.post.useUpdate({ optimisticUpdate: optimistic }); const onCreatePost = () => { if (!users) { @@ -43,6 +62,13 @@ export default function Home() { }); }; + const onTogglePublishPost = (post: Post) => { + updatePost.mutate({ + where: { id: post.id }, + data: { published: !post.published }, + }); + }; + if (isUsersFetched && (!users || users.length === 0)) { return
No users found. Please run "pnpm db:init" to seed the database.
; } @@ -74,22 +100,69 @@ export default function Home() { -
+
+ + + + + +
+ +
    {posts?.map((post) => ( -
    +
  • -

    {post.title}

    - +
    +

    {post.title}

    + {post.$optimistic ? pending : null} +
    +
    + + +
    -

    by {post.author.name}

    -
  • + {post.$optimistic ? null : ( +

    + by {post.author.name} {!post.published ? '(Draft)' : ''} +

    + )} + ))} -
+ diff --git a/samples/next.js/app/providers.tsx b/samples/next.js/app/providers.tsx index 065bd039..49b126ce 100644 --- a/samples/next.js/app/providers.tsx +++ b/samples/next.js/app/providers.tsx @@ -9,7 +9,7 @@ const queryClient = new QueryClient(); export default function Providers({ children }: { children: ReactNode }) { return ( - {children} + {children} ); } diff --git a/samples/next.js/next.config.ts b/samples/next.js/next.config.ts index e9ffa308..b22af960 100644 --- a/samples/next.js/next.config.ts +++ b/samples/next.js/next.config.ts @@ -1,7 +1,5 @@ -import type { NextConfig } from "next"; +import type { NextConfig } from 'next'; -const nextConfig: NextConfig = { - /* config options here */ -}; +const nextConfig: NextConfig = {}; export default nextConfig; diff --git a/samples/next.js/package.json b/samples/next.js/package.json index 08d2945e..88839b7c 100644 --- a/samples/next.js/package.json +++ b/samples/next.js/package.json @@ -1,11 +1,10 @@ { "name": "next.js", - "version": "3.0.0", "private": true, "scripts": { "generate": "zen generate --lite", "db:init": "pnpm generate && zen db push && npx tsx zenstack/seed.ts", - "dev": "next dev", + "dev": "next dev --port 3301", "build": "pnpm generate && next build", "start": "next start", "lint": "eslint" diff --git a/samples/nuxt/.gitignore b/samples/nuxt/.gitignore new file mode 100644 index 00000000..0de6f9c4 --- /dev/null +++ b/samples/nuxt/.gitignore @@ -0,0 +1,26 @@ +# Nuxt dev/build outputs +.output +.data +.nuxt +.nitro +.cache +dist + +# Node dependencies +node_modules + +# Logs +logs +*.log + +# Misc +.DS_Store +.fleet +.idea + +# Local env files +.env +.env.* +!.env.example + +*.db \ No newline at end of file diff --git a/samples/nuxt/README.md b/samples/nuxt/README.md new file mode 100644 index 00000000..b7700ace --- /dev/null +++ b/samples/nuxt/README.md @@ -0,0 +1,47 @@ +# ZenStack Nuxt Blog Sample + +A simple blog application built with Nuxt, ZenStack ORM, and TanStack Query Vue integration. + +## Features + +- Create, read, update, and delete blog posts +- User management +- Published/draft post filtering +- Optimistic updates +- TanStack Query Vue integration with ZenStack + +## Getting Started + +1. Install dependencies: +```bash +pnpm install +``` + +2. Initialize the database and seed data: +```bash +pnpm db:init +``` + +3. Start the development server: +```bash +pnpm dev +``` + +The app will be available at http://localhost:3302 + +## Project Structure + +- `app/` - Nuxt app components and pages +- `server/` - Nuxt server files + - `api/model/[...path].ts` - ZenStack API endpoint + - `utils/db.ts` - Database client +- `zenstack/` - ZenStack schema and generated files + - `schema.zmodel` - Database schema definition + - `seed.ts` - Database seeding script + +## Available Scripts + +- `pnpm dev` - Start development server +- `pnpm build` - Build for production +- `pnpm generate` - Generate ZenStack TypeScript schema +- `pnpm db:init` - Initialize database and seed data diff --git a/samples/nuxt/app/app.vue b/samples/nuxt/app/app.vue new file mode 100644 index 00000000..4512f245 --- /dev/null +++ b/samples/nuxt/app/app.vue @@ -0,0 +1,147 @@ + + + diff --git a/samples/nuxt/app/assets/css/main.css b/samples/nuxt/app/assets/css/main.css new file mode 100644 index 00000000..a8e6f63e --- /dev/null +++ b/samples/nuxt/app/assets/css/main.css @@ -0,0 +1,24 @@ +@import "tailwindcss"; + +:root { + --background: #ffffff; + --foreground: #171717; +} + +@theme inline { + --color-background: var(--background); + --color-foreground: var(--foreground); +} + +@media (prefers-color-scheme: dark) { + :root { + --background: #0a0a0a; + --foreground: #ededed; + } +} + +body { + background: var(--background); + color: var(--foreground); + font-family: Arial, Helvetica, sans-serif; +} diff --git a/samples/nuxt/app/plugins/tanstack-query.ts b/samples/nuxt/app/plugins/tanstack-query.ts new file mode 100644 index 00000000..2c380de0 --- /dev/null +++ b/samples/nuxt/app/plugins/tanstack-query.ts @@ -0,0 +1,18 @@ +import { QueryClient, VueQueryPlugin } from '@tanstack/vue-query'; +import { provideQuerySettingsContext } from '@zenstackhq/tanstack-query/vue'; + +export default defineNuxtPlugin((nuxtApp) => { + const queryClient = new QueryClient(); + + nuxtApp.vueApp.use(VueQueryPlugin, { queryClient }); + + // Provide ZenStack query settings + nuxtApp.vueApp.mixin({ + setup() { + provideQuerySettingsContext({ + endpoint: '/api/model', + logging: true + }); + } + }); +}); diff --git a/samples/nuxt/nuxt.config.ts b/samples/nuxt/nuxt.config.ts new file mode 100644 index 00000000..3cd094c1 --- /dev/null +++ b/samples/nuxt/nuxt.config.ts @@ -0,0 +1,12 @@ +import tailwindcss from '@tailwindcss/vite'; + +// https://nuxt.com/docs/api/configuration/nuxt-config +export default defineNuxtConfig({ + compatibilityDate: '2025-07-15', + devtools: { enabled: true }, + css: ['./app/assets/css/main.css'], + vite: { + // @ts-ignore + plugins: [tailwindcss()], + }, +}); diff --git a/samples/nuxt/package.json b/samples/nuxt/package.json new file mode 100644 index 00000000..6c0ec8c4 --- /dev/null +++ b/samples/nuxt/package.json @@ -0,0 +1,30 @@ +{ + "name": "nuxt", + "type": "module", + "private": true, + "scripts": { + "generate": "zen generate --lite", + "db:init": "pnpm generate && zen db push && npx tsx zenstack/seed.ts", + "dev": "nuxt dev --port 3302", + "build": "pnpm generate && nuxt build", + "preview": "nuxt preview", + "postinstall": "nuxt prepare" + }, + "dependencies": { + "@tailwindcss/vite": "^4.1.18", + "@tanstack/vue-query": "catalog:", + "@zenstackhq/orm": "workspace:*", + "@zenstackhq/server": "workspace:*", + "@zenstackhq/tanstack-query": "workspace:*", + "better-sqlite3": "catalog:", + "lorem-ipsum": "^2.0.8", + "nuxt": "catalog:", + "tailwindcss": "^4.1.18", + "vue": "catalog:", + "vue-router": "^4.6.4" + }, + "devDependencies": { + "@types/better-sqlite3": "catalog:", + "@zenstackhq/cli": "workspace:*" + } +} diff --git a/samples/nuxt/public/favicon.ico b/samples/nuxt/public/favicon.ico new file mode 100644 index 00000000..18993ad9 Binary files /dev/null and b/samples/nuxt/public/favicon.ico differ diff --git a/samples/nuxt/public/nuxt.svg b/samples/nuxt/public/nuxt.svg new file mode 100644 index 00000000..ead151fc --- /dev/null +++ b/samples/nuxt/public/nuxt.svg @@ -0,0 +1,3 @@ + + + diff --git a/samples/nuxt/public/robots.txt b/samples/nuxt/public/robots.txt new file mode 100644 index 00000000..0ad279c7 --- /dev/null +++ b/samples/nuxt/public/robots.txt @@ -0,0 +1,2 @@ +User-Agent: * +Disallow: diff --git a/samples/nuxt/server/api/model/[...].ts b/samples/nuxt/server/api/model/[...].ts new file mode 100644 index 00000000..7483280a --- /dev/null +++ b/samples/nuxt/server/api/model/[...].ts @@ -0,0 +1,13 @@ +import { RPCApiHandler } from '@zenstackhq/server/api'; +import { createEventHandler } from '@zenstackhq/server/nuxt'; +import { db } from '~~/server/utils/db'; +import { schema } from '~~/zenstack/schema'; + +const handler = createEventHandler({ + apiHandler: new RPCApiHandler({ schema }), + // fully open ZenStackClient is used here for demo purposes only, in a real application, + // you should use one with access policies enabled + getClient: () => db, +}); + +export default handler; diff --git a/samples/nuxt/server/utils/db.ts b/samples/nuxt/server/utils/db.ts new file mode 100644 index 00000000..86aa5248 --- /dev/null +++ b/samples/nuxt/server/utils/db.ts @@ -0,0 +1,10 @@ +import { ZenStackClient } from '@zenstackhq/orm'; +import SQLite from 'better-sqlite3'; +import { SqliteDialect } from 'kysely'; +import { schema } from '~~/zenstack/schema'; + +export const db = new ZenStackClient(schema, { + dialect: new SqliteDialect({ + database: new SQLite('./zenstack/dev.db'), + }), +}); diff --git a/samples/nuxt/tsconfig.json b/samples/nuxt/tsconfig.json new file mode 100644 index 00000000..307b2134 --- /dev/null +++ b/samples/nuxt/tsconfig.json @@ -0,0 +1,18 @@ +{ + // https://nuxt.com/docs/guide/concepts/typescript + "files": [], + "references": [ + { + "path": "./.nuxt/tsconfig.app.json" + }, + { + "path": "./.nuxt/tsconfig.server.json" + }, + { + "path": "./.nuxt/tsconfig.shared.json" + }, + { + "path": "./.nuxt/tsconfig.node.json" + } + ] +} diff --git a/samples/nuxt/zenstack/input.ts b/samples/nuxt/zenstack/input.ts new file mode 100644 index 00000000..b2cd96ee --- /dev/null +++ b/samples/nuxt/zenstack/input.ts @@ -0,0 +1,50 @@ +////////////////////////////////////////////////////////////////////////////////////////////// +// DO NOT MODIFY THIS FILE // +// This file is automatically generated by ZenStack CLI and should not be manually updated. // +////////////////////////////////////////////////////////////////////////////////////////////// + +/* eslint-disable */ + +import { type SchemaType as $Schema } from "./schema-lite"; +import type { FindManyArgs as $FindManyArgs, FindUniqueArgs as $FindUniqueArgs, FindFirstArgs as $FindFirstArgs, CreateArgs as $CreateArgs, CreateManyArgs as $CreateManyArgs, CreateManyAndReturnArgs as $CreateManyAndReturnArgs, UpdateArgs as $UpdateArgs, UpdateManyArgs as $UpdateManyArgs, UpdateManyAndReturnArgs as $UpdateManyAndReturnArgs, UpsertArgs as $UpsertArgs, DeleteArgs as $DeleteArgs, DeleteManyArgs as $DeleteManyArgs, CountArgs as $CountArgs, AggregateArgs as $AggregateArgs, GroupByArgs as $GroupByArgs, WhereInput as $WhereInput, SelectInput as $SelectInput, IncludeInput as $IncludeInput, OmitInput as $OmitInput, QueryOptions as $QueryOptions } from "@zenstackhq/orm"; +import type { SimplifiedPlainResult as $Result, SelectIncludeOmit as $SelectIncludeOmit } from "@zenstackhq/orm"; +export type UserFindManyArgs = $FindManyArgs<$Schema, "User">; +export type UserFindUniqueArgs = $FindUniqueArgs<$Schema, "User">; +export type UserFindFirstArgs = $FindFirstArgs<$Schema, "User">; +export type UserCreateArgs = $CreateArgs<$Schema, "User">; +export type UserCreateManyArgs = $CreateManyArgs<$Schema, "User">; +export type UserCreateManyAndReturnArgs = $CreateManyAndReturnArgs<$Schema, "User">; +export type UserUpdateArgs = $UpdateArgs<$Schema, "User">; +export type UserUpdateManyArgs = $UpdateManyArgs<$Schema, "User">; +export type UserUpdateManyAndReturnArgs = $UpdateManyAndReturnArgs<$Schema, "User">; +export type UserUpsertArgs = $UpsertArgs<$Schema, "User">; +export type UserDeleteArgs = $DeleteArgs<$Schema, "User">; +export type UserDeleteManyArgs = $DeleteManyArgs<$Schema, "User">; +export type UserCountArgs = $CountArgs<$Schema, "User">; +export type UserAggregateArgs = $AggregateArgs<$Schema, "User">; +export type UserGroupByArgs = $GroupByArgs<$Schema, "User">; +export type UserWhereInput = $WhereInput<$Schema, "User">; +export type UserSelect = $SelectInput<$Schema, "User">; +export type UserInclude = $IncludeInput<$Schema, "User">; +export type UserOmit = $OmitInput<$Schema, "User">; +export type UserGetPayload, Options extends $QueryOptions<$Schema> = $QueryOptions<$Schema>> = $Result<$Schema, "User", Args, Options>; +export type PostFindManyArgs = $FindManyArgs<$Schema, "Post">; +export type PostFindUniqueArgs = $FindUniqueArgs<$Schema, "Post">; +export type PostFindFirstArgs = $FindFirstArgs<$Schema, "Post">; +export type PostCreateArgs = $CreateArgs<$Schema, "Post">; +export type PostCreateManyArgs = $CreateManyArgs<$Schema, "Post">; +export type PostCreateManyAndReturnArgs = $CreateManyAndReturnArgs<$Schema, "Post">; +export type PostUpdateArgs = $UpdateArgs<$Schema, "Post">; +export type PostUpdateManyArgs = $UpdateManyArgs<$Schema, "Post">; +export type PostUpdateManyAndReturnArgs = $UpdateManyAndReturnArgs<$Schema, "Post">; +export type PostUpsertArgs = $UpsertArgs<$Schema, "Post">; +export type PostDeleteArgs = $DeleteArgs<$Schema, "Post">; +export type PostDeleteManyArgs = $DeleteManyArgs<$Schema, "Post">; +export type PostCountArgs = $CountArgs<$Schema, "Post">; +export type PostAggregateArgs = $AggregateArgs<$Schema, "Post">; +export type PostGroupByArgs = $GroupByArgs<$Schema, "Post">; +export type PostWhereInput = $WhereInput<$Schema, "Post">; +export type PostSelect = $SelectInput<$Schema, "Post">; +export type PostInclude = $IncludeInput<$Schema, "Post">; +export type PostOmit = $OmitInput<$Schema, "Post">; +export type PostGetPayload, Options extends $QueryOptions<$Schema> = $QueryOptions<$Schema>> = $Result<$Schema, "Post", Args, Options>; diff --git a/samples/nuxt/zenstack/models.ts b/samples/nuxt/zenstack/models.ts new file mode 100644 index 00000000..3314c7d4 --- /dev/null +++ b/samples/nuxt/zenstack/models.ts @@ -0,0 +1,17 @@ +////////////////////////////////////////////////////////////////////////////////////////////// +// DO NOT MODIFY THIS FILE // +// This file is automatically generated by ZenStack CLI and should not be manually updated. // +////////////////////////////////////////////////////////////////////////////////////////////// + +/* eslint-disable */ + +import { type SchemaType as $Schema } from "./schema-lite"; +import { type ModelResult as $ModelResult } from "@zenstackhq/orm"; +/** + * User model + */ +export type User = $ModelResult<$Schema, "User">; +/** + * Post model + */ +export type Post = $ModelResult<$Schema, "Post">; diff --git a/samples/nuxt/zenstack/schema-lite.ts b/samples/nuxt/zenstack/schema-lite.ts new file mode 100644 index 00000000..6153abe9 --- /dev/null +++ b/samples/nuxt/zenstack/schema-lite.ts @@ -0,0 +1,106 @@ +////////////////////////////////////////////////////////////////////////////////////////////// +// DO NOT MODIFY THIS FILE // +// This file is automatically generated by ZenStack CLI and should not be manually updated. // +////////////////////////////////////////////////////////////////////////////////////////////// + +/* eslint-disable */ + +import { type SchemaDef, ExpressionUtils } from "@zenstackhq/orm/schema"; +export class SchemaType implements SchemaDef { + provider = { + type: "sqlite" + } as const; + models = { + User: { + name: "User", + fields: { + id: { + name: "id", + type: "String", + id: true, + default: ExpressionUtils.call("cuid") + }, + createdAt: { + name: "createdAt", + type: "DateTime", + default: ExpressionUtils.call("now") + }, + updatedAt: { + name: "updatedAt", + type: "DateTime", + updatedAt: true + }, + email: { + name: "email", + type: "String", + unique: true + }, + name: { + name: "name", + type: "String", + optional: true + }, + posts: { + name: "posts", + type: "Post", + array: true, + relation: { opposite: "author" } + } + }, + idFields: ["id"], + uniqueFields: { + id: { type: "String" }, + email: { type: "String" } + } + }, + Post: { + name: "Post", + fields: { + id: { + name: "id", + type: "String", + id: true, + default: ExpressionUtils.call("cuid") + }, + createdAt: { + name: "createdAt", + type: "DateTime", + default: ExpressionUtils.call("now") + }, + updatedAt: { + name: "updatedAt", + type: "DateTime", + updatedAt: true + }, + title: { + name: "title", + type: "String" + }, + published: { + name: "published", + type: "Boolean", + default: false + }, + author: { + name: "author", + type: "User", + relation: { opposite: "posts", fields: ["authorId"], references: ["id"], onUpdate: "Cascade", onDelete: "Cascade" } + }, + authorId: { + name: "authorId", + type: "String", + foreignKeyFor: [ + "author" + ] + } + }, + idFields: ["id"], + uniqueFields: { + id: { type: "String" } + } + } + } as const; + authType = "User" as const; + plugins = {}; +} +export const schema = new SchemaType(); diff --git a/samples/nuxt/zenstack/schema.ts b/samples/nuxt/zenstack/schema.ts new file mode 100644 index 00000000..c7d690ed --- /dev/null +++ b/samples/nuxt/zenstack/schema.ts @@ -0,0 +1,115 @@ +////////////////////////////////////////////////////////////////////////////////////////////// +// DO NOT MODIFY THIS FILE // +// This file is automatically generated by ZenStack CLI and should not be manually updated. // +////////////////////////////////////////////////////////////////////////////////////////////// + +/* eslint-disable */ + +import { type SchemaDef, ExpressionUtils } from "@zenstackhq/orm/schema"; +export class SchemaType implements SchemaDef { + provider = { + type: "sqlite" + } as const; + models = { + User: { + name: "User", + fields: { + id: { + name: "id", + type: "String", + id: true, + attributes: [{ name: "@id" }, { name: "@default", args: [{ name: "value", value: ExpressionUtils.call("cuid") }] }], + default: ExpressionUtils.call("cuid") + }, + createdAt: { + name: "createdAt", + type: "DateTime", + attributes: [{ name: "@default", args: [{ name: "value", value: ExpressionUtils.call("now") }] }], + default: ExpressionUtils.call("now") + }, + updatedAt: { + name: "updatedAt", + type: "DateTime", + updatedAt: true, + attributes: [{ name: "@updatedAt" }] + }, + email: { + name: "email", + type: "String", + unique: true, + attributes: [{ name: "@unique" }] + }, + name: { + name: "name", + type: "String", + optional: true + }, + posts: { + name: "posts", + type: "Post", + array: true, + relation: { opposite: "author" } + } + }, + idFields: ["id"], + uniqueFields: { + id: { type: "String" }, + email: { type: "String" } + } + }, + Post: { + name: "Post", + fields: { + id: { + name: "id", + type: "String", + id: true, + attributes: [{ name: "@id" }, { name: "@default", args: [{ name: "value", value: ExpressionUtils.call("cuid") }] }], + default: ExpressionUtils.call("cuid") + }, + createdAt: { + name: "createdAt", + type: "DateTime", + attributes: [{ name: "@default", args: [{ name: "value", value: ExpressionUtils.call("now") }] }], + default: ExpressionUtils.call("now") + }, + updatedAt: { + name: "updatedAt", + type: "DateTime", + updatedAt: true, + attributes: [{ name: "@updatedAt" }] + }, + title: { + name: "title", + type: "String" + }, + published: { + name: "published", + type: "Boolean", + attributes: [{ name: "@default", args: [{ name: "value", value: ExpressionUtils.literal(false) }] }], + default: false + }, + author: { + name: "author", + type: "User", + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("authorId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + relation: { opposite: "posts", fields: ["authorId"], references: ["id"], onUpdate: "Cascade", onDelete: "Cascade" } + }, + authorId: { + name: "authorId", + type: "String", + foreignKeyFor: [ + "author" + ] + } + }, + idFields: ["id"], + uniqueFields: { + id: { type: "String" } + } + } + } as const; + authType = "User" as const; + plugins = {}; +} +export const schema = new SchemaType(); diff --git a/samples/nuxt/zenstack/schema.zmodel b/samples/nuxt/zenstack/schema.zmodel new file mode 100644 index 00000000..e1775e12 --- /dev/null +++ b/samples/nuxt/zenstack/schema.zmodel @@ -0,0 +1,25 @@ +datasource db { + provider = 'sqlite' + url = 'file:./dev.db' +} + +/// User model +model User { + id String @id @default(cuid()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + email String @unique + name String? + posts Post[] +} + +/// Post model +model Post { + id String @id @default(cuid()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + title String + published Boolean @default(false) + author User @relation(fields: [authorId], references: [id], onUpdate: Cascade, onDelete: Cascade) + authorId String +} diff --git a/samples/nuxt/zenstack/seed.ts b/samples/nuxt/zenstack/seed.ts new file mode 100644 index 00000000..6e02d80d --- /dev/null +++ b/samples/nuxt/zenstack/seed.ts @@ -0,0 +1,23 @@ +import { ZenStackClient } from '@zenstackhq/orm'; +import SQLite from 'better-sqlite3'; +import { SqliteDialect } from 'kysely'; +import { schema } from './schema'; + +async function main() { + const db = new ZenStackClient(schema, { + dialect: new SqliteDialect({ + database: new SQLite('./zenstack/dev.db'), + }), + }); + + await db.user.deleteMany(); + + await db.user.createMany({ + data: [ + { id: '1', name: 'Alice', email: 'alice@example.com' }, + { id: '2', name: 'Bob', email: 'bob@example.com' }, + ], + }); +} + +main(); diff --git a/samples/orm/package.json b/samples/orm/package.json index e5d02a16..b9ffc2df 100644 --- a/samples/orm/package.json +++ b/samples/orm/package.json @@ -1,6 +1,6 @@ { "name": "sample-blog", - "version": "3.0.0", + "version": "3.1.0", "description": "", "main": "index.js", "private": true, diff --git a/samples/sveltekit/.gitignore b/samples/sveltekit/.gitignore new file mode 100644 index 00000000..c7bddb84 --- /dev/null +++ b/samples/sveltekit/.gitignore @@ -0,0 +1,25 @@ +node_modules + +# Output +.output +.vercel +.netlify +.wrangler +/.svelte-kit +/build + +# OS +.DS_Store +Thumbs.db + +# Env +.env +.env.* +!.env.example +!.env.test + +# Vite +vite.config.js.timestamp-* +vite.config.ts.timestamp-* + +*.db diff --git a/samples/sveltekit/.prettierrc b/samples/sveltekit/.prettierrc new file mode 100644 index 00000000..0cd5679c --- /dev/null +++ b/samples/sveltekit/.prettierrc @@ -0,0 +1,4 @@ +{ + "plugins": ["prettier-plugin-tailwindcss"], + "tailwindStylesheet": "./src/routes/layout.css" +} diff --git a/samples/sveltekit/README.md b/samples/sveltekit/README.md new file mode 100644 index 00000000..75842c40 --- /dev/null +++ b/samples/sveltekit/README.md @@ -0,0 +1,38 @@ +# sv + +Everything you need to build a Svelte project, powered by [`sv`](https://github.com/sveltejs/cli). + +## Creating a project + +If you're seeing this, you've probably already done this step. Congrats! + +```sh +# create a new project in the current directory +npx sv create + +# create a new project in my-app +npx sv create my-app +``` + +## Developing + +Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server: + +```sh +npm run dev + +# or start the server and open the app in a new browser tab +npm run dev -- --open +``` + +## Building + +To create a production version of your app: + +```sh +npm run build +``` + +You can preview the production build with `npm run preview`. + +> To deploy your app, you may need to install an [adapter](https://svelte.dev/docs/kit/adapters) for your target environment. diff --git a/samples/sveltekit/package.json b/samples/sveltekit/package.json new file mode 100644 index 00000000..503b59b4 --- /dev/null +++ b/samples/sveltekit/package.json @@ -0,0 +1,43 @@ +{ + "name": "sveltekit", + "private": true, + "type": "module", + "scripts": { + "generate": "zen generate --lite", + "db:init": "pnpm generate && zen db push && npx tsx src/zenstack/seed.ts", + "dev": "vite dev --port 3303", + "build": "pnpm db:init && vite build", + "preview": "vite preview", + "prepare": "svelte-kit sync || echo ''", + "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", + "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch" + }, + "dependencies": { + "@tanstack/svelte-query": "catalog:", + "@zenstackhq/orm": "workspace:*", + "@zenstackhq/server": "workspace:*", + "@zenstackhq/tanstack-query": "workspace:*", + "better-sqlite3": "catalog:", + "kysely": "catalog:", + "lorem-ipsum": "^2.0.8" + }, + "devDependencies": { + "@sveltejs/adapter-auto": "^7.0.0", + "@sveltejs/kit": "catalog:", + "@sveltejs/vite-plugin-svelte": "^6.2.1", + "@tailwindcss/vite": "^4.1.17", + "@types/better-sqlite3": "catalog:", + "@types/node": "catalog:", + "@zenstackhq/cli": "workspace:*", + "prettier-plugin-tailwindcss": "^0.7.2", + "svelte": "catalog:", + "svelte-check": "^4.3.4", + "tailwindcss": "^4.1.17", + "tsx": "^4.19.2", + "typescript": "^5.9.3", + "vite": "^7.2.6" + }, + "zenstack": { + "schema": "./src/zenstack/schema.zmodel" + } +} diff --git a/samples/sveltekit/src/app.d.ts b/samples/sveltekit/src/app.d.ts new file mode 100644 index 00000000..da08e6da --- /dev/null +++ b/samples/sveltekit/src/app.d.ts @@ -0,0 +1,13 @@ +// See https://svelte.dev/docs/kit/types#app.d.ts +// for information about these interfaces +declare global { + namespace App { + // interface Error {} + // interface Locals {} + // interface PageData {} + // interface PageState {} + // interface Platform {} + } +} + +export {}; diff --git a/samples/sveltekit/src/app.html b/samples/sveltekit/src/app.html new file mode 100644 index 00000000..f273cc58 --- /dev/null +++ b/samples/sveltekit/src/app.html @@ -0,0 +1,11 @@ + + + + + + %sveltekit.head% + + +
%sveltekit.body%
+ + diff --git a/samples/sveltekit/src/lib/assets/favicon.svg b/samples/sveltekit/src/lib/assets/favicon.svg new file mode 100644 index 00000000..cc5dc66a --- /dev/null +++ b/samples/sveltekit/src/lib/assets/favicon.svg @@ -0,0 +1 @@ +svelte-logo \ No newline at end of file diff --git a/samples/sveltekit/src/lib/db.ts b/samples/sveltekit/src/lib/db.ts new file mode 100644 index 00000000..bbf70b8c --- /dev/null +++ b/samples/sveltekit/src/lib/db.ts @@ -0,0 +1,10 @@ +import { ZenStackClient } from "@zenstackhq/orm"; +import SQLite from "better-sqlite3"; +import { SqliteDialect } from "kysely"; +import { schema } from "../zenstack/schema"; + +export const db = new ZenStackClient(schema, { + dialect: new SqliteDialect({ + database: new SQLite("./src/zenstack/dev.db"), + }), +}); diff --git a/samples/sveltekit/src/lib/index.ts b/samples/sveltekit/src/lib/index.ts new file mode 100644 index 00000000..856f2b6c --- /dev/null +++ b/samples/sveltekit/src/lib/index.ts @@ -0,0 +1 @@ +// place files you want to import through the `$lib` alias in this folder. diff --git a/samples/sveltekit/src/routes/+layout.svelte b/samples/sveltekit/src/routes/+layout.svelte new file mode 100644 index 00000000..77a8e800 --- /dev/null +++ b/samples/sveltekit/src/routes/+layout.svelte @@ -0,0 +1,23 @@ + + + + + + {@render children()} + diff --git a/samples/sveltekit/src/routes/+page.svelte b/samples/sveltekit/src/routes/+page.svelte new file mode 100644 index 00000000..5725bf79 --- /dev/null +++ b/samples/sveltekit/src/routes/+page.svelte @@ -0,0 +1,169 @@ + + +{#if users.isFetched && (!users.data || users.data.length === 0)} +
No users found. Please run "pnpm db:init" to seed the database.
+{:else} +
+
+ SvelteKit logo +
+

+ My Awesome Blog +

+ + + +
+
Current users
+
+ {#if users.isLoading} +
Loading users...
+ {:else if users.isError} +
Error loading users: {users.error.message}
+ {:else} + {#each users.data as user} +
+ {user.email} +
+ {/each} + {/if} +
+
+ +
+ + + + + +
+ +
    + {#if posts.data} + {#each posts.data as post} +
  • +
    +
    +

    {post.title}

    + {#if post.$optimistic} + pending + {/if} +
    +
    + + +
    +
    + {#if !post.$optimistic} +

    + by {post.author.name} + {!post.published ? '(Draft)' : ''} +

    + {/if} +
  • + {/each} + {/if} +
+
+
+
+{/if} diff --git a/samples/sveltekit/src/routes/api/model/[...path]/+server.ts b/samples/sveltekit/src/routes/api/model/[...path]/+server.ts new file mode 100644 index 00000000..1248aa03 --- /dev/null +++ b/samples/sveltekit/src/routes/api/model/[...path]/+server.ts @@ -0,0 +1,17 @@ +import { db } from "$lib/db"; +import { RPCApiHandler } from "@zenstackhq/server/api"; +import { SvelteKitRouteHandler } from "@zenstackhq/server/sveltekit"; +import { schema } from "../../../../zenstack/schema"; + +const handler = SvelteKitRouteHandler({ + apiHandler: new RPCApiHandler({ schema }), + // fully open ZenStackClient is used here for demo purposes only, in a real application, + // you should use one with access policies enabled + getClient: () => db, +}); + +export const GET = handler; +export const POST = handler; +export const PUT = handler; +export const DELETE = handler; +export const PATCH = handler; diff --git a/samples/sveltekit/src/routes/layout.css b/samples/sveltekit/src/routes/layout.css new file mode 100644 index 00000000..a8e6f63e --- /dev/null +++ b/samples/sveltekit/src/routes/layout.css @@ -0,0 +1,24 @@ +@import "tailwindcss"; + +:root { + --background: #ffffff; + --foreground: #171717; +} + +@theme inline { + --color-background: var(--background); + --color-foreground: var(--foreground); +} + +@media (prefers-color-scheme: dark) { + :root { + --background: #0a0a0a; + --foreground: #ededed; + } +} + +body { + background: var(--background); + color: var(--foreground); + font-family: Arial, Helvetica, sans-serif; +} diff --git a/samples/sveltekit/src/zenstack/input.ts b/samples/sveltekit/src/zenstack/input.ts new file mode 100644 index 00000000..b2cd96ee --- /dev/null +++ b/samples/sveltekit/src/zenstack/input.ts @@ -0,0 +1,50 @@ +////////////////////////////////////////////////////////////////////////////////////////////// +// DO NOT MODIFY THIS FILE // +// This file is automatically generated by ZenStack CLI and should not be manually updated. // +////////////////////////////////////////////////////////////////////////////////////////////// + +/* eslint-disable */ + +import { type SchemaType as $Schema } from "./schema-lite"; +import type { FindManyArgs as $FindManyArgs, FindUniqueArgs as $FindUniqueArgs, FindFirstArgs as $FindFirstArgs, CreateArgs as $CreateArgs, CreateManyArgs as $CreateManyArgs, CreateManyAndReturnArgs as $CreateManyAndReturnArgs, UpdateArgs as $UpdateArgs, UpdateManyArgs as $UpdateManyArgs, UpdateManyAndReturnArgs as $UpdateManyAndReturnArgs, UpsertArgs as $UpsertArgs, DeleteArgs as $DeleteArgs, DeleteManyArgs as $DeleteManyArgs, CountArgs as $CountArgs, AggregateArgs as $AggregateArgs, GroupByArgs as $GroupByArgs, WhereInput as $WhereInput, SelectInput as $SelectInput, IncludeInput as $IncludeInput, OmitInput as $OmitInput, QueryOptions as $QueryOptions } from "@zenstackhq/orm"; +import type { SimplifiedPlainResult as $Result, SelectIncludeOmit as $SelectIncludeOmit } from "@zenstackhq/orm"; +export type UserFindManyArgs = $FindManyArgs<$Schema, "User">; +export type UserFindUniqueArgs = $FindUniqueArgs<$Schema, "User">; +export type UserFindFirstArgs = $FindFirstArgs<$Schema, "User">; +export type UserCreateArgs = $CreateArgs<$Schema, "User">; +export type UserCreateManyArgs = $CreateManyArgs<$Schema, "User">; +export type UserCreateManyAndReturnArgs = $CreateManyAndReturnArgs<$Schema, "User">; +export type UserUpdateArgs = $UpdateArgs<$Schema, "User">; +export type UserUpdateManyArgs = $UpdateManyArgs<$Schema, "User">; +export type UserUpdateManyAndReturnArgs = $UpdateManyAndReturnArgs<$Schema, "User">; +export type UserUpsertArgs = $UpsertArgs<$Schema, "User">; +export type UserDeleteArgs = $DeleteArgs<$Schema, "User">; +export type UserDeleteManyArgs = $DeleteManyArgs<$Schema, "User">; +export type UserCountArgs = $CountArgs<$Schema, "User">; +export type UserAggregateArgs = $AggregateArgs<$Schema, "User">; +export type UserGroupByArgs = $GroupByArgs<$Schema, "User">; +export type UserWhereInput = $WhereInput<$Schema, "User">; +export type UserSelect = $SelectInput<$Schema, "User">; +export type UserInclude = $IncludeInput<$Schema, "User">; +export type UserOmit = $OmitInput<$Schema, "User">; +export type UserGetPayload, Options extends $QueryOptions<$Schema> = $QueryOptions<$Schema>> = $Result<$Schema, "User", Args, Options>; +export type PostFindManyArgs = $FindManyArgs<$Schema, "Post">; +export type PostFindUniqueArgs = $FindUniqueArgs<$Schema, "Post">; +export type PostFindFirstArgs = $FindFirstArgs<$Schema, "Post">; +export type PostCreateArgs = $CreateArgs<$Schema, "Post">; +export type PostCreateManyArgs = $CreateManyArgs<$Schema, "Post">; +export type PostCreateManyAndReturnArgs = $CreateManyAndReturnArgs<$Schema, "Post">; +export type PostUpdateArgs = $UpdateArgs<$Schema, "Post">; +export type PostUpdateManyArgs = $UpdateManyArgs<$Schema, "Post">; +export type PostUpdateManyAndReturnArgs = $UpdateManyAndReturnArgs<$Schema, "Post">; +export type PostUpsertArgs = $UpsertArgs<$Schema, "Post">; +export type PostDeleteArgs = $DeleteArgs<$Schema, "Post">; +export type PostDeleteManyArgs = $DeleteManyArgs<$Schema, "Post">; +export type PostCountArgs = $CountArgs<$Schema, "Post">; +export type PostAggregateArgs = $AggregateArgs<$Schema, "Post">; +export type PostGroupByArgs = $GroupByArgs<$Schema, "Post">; +export type PostWhereInput = $WhereInput<$Schema, "Post">; +export type PostSelect = $SelectInput<$Schema, "Post">; +export type PostInclude = $IncludeInput<$Schema, "Post">; +export type PostOmit = $OmitInput<$Schema, "Post">; +export type PostGetPayload, Options extends $QueryOptions<$Schema> = $QueryOptions<$Schema>> = $Result<$Schema, "Post", Args, Options>; diff --git a/samples/sveltekit/src/zenstack/models.ts b/samples/sveltekit/src/zenstack/models.ts new file mode 100644 index 00000000..3314c7d4 --- /dev/null +++ b/samples/sveltekit/src/zenstack/models.ts @@ -0,0 +1,17 @@ +////////////////////////////////////////////////////////////////////////////////////////////// +// DO NOT MODIFY THIS FILE // +// This file is automatically generated by ZenStack CLI and should not be manually updated. // +////////////////////////////////////////////////////////////////////////////////////////////// + +/* eslint-disable */ + +import { type SchemaType as $Schema } from "./schema-lite"; +import { type ModelResult as $ModelResult } from "@zenstackhq/orm"; +/** + * User model + */ +export type User = $ModelResult<$Schema, "User">; +/** + * Post model + */ +export type Post = $ModelResult<$Schema, "Post">; diff --git a/samples/sveltekit/src/zenstack/schema-lite.ts b/samples/sveltekit/src/zenstack/schema-lite.ts new file mode 100644 index 00000000..6153abe9 --- /dev/null +++ b/samples/sveltekit/src/zenstack/schema-lite.ts @@ -0,0 +1,106 @@ +////////////////////////////////////////////////////////////////////////////////////////////// +// DO NOT MODIFY THIS FILE // +// This file is automatically generated by ZenStack CLI and should not be manually updated. // +////////////////////////////////////////////////////////////////////////////////////////////// + +/* eslint-disable */ + +import { type SchemaDef, ExpressionUtils } from "@zenstackhq/orm/schema"; +export class SchemaType implements SchemaDef { + provider = { + type: "sqlite" + } as const; + models = { + User: { + name: "User", + fields: { + id: { + name: "id", + type: "String", + id: true, + default: ExpressionUtils.call("cuid") + }, + createdAt: { + name: "createdAt", + type: "DateTime", + default: ExpressionUtils.call("now") + }, + updatedAt: { + name: "updatedAt", + type: "DateTime", + updatedAt: true + }, + email: { + name: "email", + type: "String", + unique: true + }, + name: { + name: "name", + type: "String", + optional: true + }, + posts: { + name: "posts", + type: "Post", + array: true, + relation: { opposite: "author" } + } + }, + idFields: ["id"], + uniqueFields: { + id: { type: "String" }, + email: { type: "String" } + } + }, + Post: { + name: "Post", + fields: { + id: { + name: "id", + type: "String", + id: true, + default: ExpressionUtils.call("cuid") + }, + createdAt: { + name: "createdAt", + type: "DateTime", + default: ExpressionUtils.call("now") + }, + updatedAt: { + name: "updatedAt", + type: "DateTime", + updatedAt: true + }, + title: { + name: "title", + type: "String" + }, + published: { + name: "published", + type: "Boolean", + default: false + }, + author: { + name: "author", + type: "User", + relation: { opposite: "posts", fields: ["authorId"], references: ["id"], onUpdate: "Cascade", onDelete: "Cascade" } + }, + authorId: { + name: "authorId", + type: "String", + foreignKeyFor: [ + "author" + ] + } + }, + idFields: ["id"], + uniqueFields: { + id: { type: "String" } + } + } + } as const; + authType = "User" as const; + plugins = {}; +} +export const schema = new SchemaType(); diff --git a/samples/sveltekit/src/zenstack/schema.ts b/samples/sveltekit/src/zenstack/schema.ts new file mode 100644 index 00000000..c7d690ed --- /dev/null +++ b/samples/sveltekit/src/zenstack/schema.ts @@ -0,0 +1,115 @@ +////////////////////////////////////////////////////////////////////////////////////////////// +// DO NOT MODIFY THIS FILE // +// This file is automatically generated by ZenStack CLI and should not be manually updated. // +////////////////////////////////////////////////////////////////////////////////////////////// + +/* eslint-disable */ + +import { type SchemaDef, ExpressionUtils } from "@zenstackhq/orm/schema"; +export class SchemaType implements SchemaDef { + provider = { + type: "sqlite" + } as const; + models = { + User: { + name: "User", + fields: { + id: { + name: "id", + type: "String", + id: true, + attributes: [{ name: "@id" }, { name: "@default", args: [{ name: "value", value: ExpressionUtils.call("cuid") }] }], + default: ExpressionUtils.call("cuid") + }, + createdAt: { + name: "createdAt", + type: "DateTime", + attributes: [{ name: "@default", args: [{ name: "value", value: ExpressionUtils.call("now") }] }], + default: ExpressionUtils.call("now") + }, + updatedAt: { + name: "updatedAt", + type: "DateTime", + updatedAt: true, + attributes: [{ name: "@updatedAt" }] + }, + email: { + name: "email", + type: "String", + unique: true, + attributes: [{ name: "@unique" }] + }, + name: { + name: "name", + type: "String", + optional: true + }, + posts: { + name: "posts", + type: "Post", + array: true, + relation: { opposite: "author" } + } + }, + idFields: ["id"], + uniqueFields: { + id: { type: "String" }, + email: { type: "String" } + } + }, + Post: { + name: "Post", + fields: { + id: { + name: "id", + type: "String", + id: true, + attributes: [{ name: "@id" }, { name: "@default", args: [{ name: "value", value: ExpressionUtils.call("cuid") }] }], + default: ExpressionUtils.call("cuid") + }, + createdAt: { + name: "createdAt", + type: "DateTime", + attributes: [{ name: "@default", args: [{ name: "value", value: ExpressionUtils.call("now") }] }], + default: ExpressionUtils.call("now") + }, + updatedAt: { + name: "updatedAt", + type: "DateTime", + updatedAt: true, + attributes: [{ name: "@updatedAt" }] + }, + title: { + name: "title", + type: "String" + }, + published: { + name: "published", + type: "Boolean", + attributes: [{ name: "@default", args: [{ name: "value", value: ExpressionUtils.literal(false) }] }], + default: false + }, + author: { + name: "author", + type: "User", + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("authorId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + relation: { opposite: "posts", fields: ["authorId"], references: ["id"], onUpdate: "Cascade", onDelete: "Cascade" } + }, + authorId: { + name: "authorId", + type: "String", + foreignKeyFor: [ + "author" + ] + } + }, + idFields: ["id"], + uniqueFields: { + id: { type: "String" } + } + } + } as const; + authType = "User" as const; + plugins = {}; +} +export const schema = new SchemaType(); diff --git a/samples/sveltekit/src/zenstack/schema.zmodel b/samples/sveltekit/src/zenstack/schema.zmodel new file mode 100644 index 00000000..e1775e12 --- /dev/null +++ b/samples/sveltekit/src/zenstack/schema.zmodel @@ -0,0 +1,25 @@ +datasource db { + provider = 'sqlite' + url = 'file:./dev.db' +} + +/// User model +model User { + id String @id @default(cuid()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + email String @unique + name String? + posts Post[] +} + +/// Post model +model Post { + id String @id @default(cuid()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + title String + published Boolean @default(false) + author User @relation(fields: [authorId], references: [id], onUpdate: Cascade, onDelete: Cascade) + authorId String +} diff --git a/samples/sveltekit/src/zenstack/seed.ts b/samples/sveltekit/src/zenstack/seed.ts new file mode 100644 index 00000000..6170cf32 --- /dev/null +++ b/samples/sveltekit/src/zenstack/seed.ts @@ -0,0 +1,26 @@ +import { ZenStackClient } from "@zenstackhq/orm"; +import SQLite from "better-sqlite3"; +import { SqliteDialect } from "kysely"; +import path from "node:path"; +import { schema } from "./schema"; + +const _dirname = path.dirname(new URL(import.meta.url).pathname); + +async function main() { + const db = new ZenStackClient(schema, { + dialect: new SqliteDialect({ + database: new SQLite(path.resolve(_dirname, "./dev.db")), + }), + }); + + await db.user.deleteMany(); + + await db.user.createMany({ + data: [ + { id: "1", name: "Alice", email: "alice@example.com" }, + { id: "2", name: "Bob", email: "bob@example.com" }, + ], + }); +} + +main(); diff --git a/samples/sveltekit/static/svelte.png b/samples/sveltekit/static/svelte.png new file mode 100644 index 00000000..0a95ea17 Binary files /dev/null and b/samples/sveltekit/static/svelte.png differ diff --git a/samples/sveltekit/svelte.config.js b/samples/sveltekit/svelte.config.js new file mode 100644 index 00000000..1295460d --- /dev/null +++ b/samples/sveltekit/svelte.config.js @@ -0,0 +1,18 @@ +import adapter from '@sveltejs/adapter-auto'; +import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; + +/** @type {import('@sveltejs/kit').Config} */ +const config = { + // Consult https://svelte.dev/docs/kit/integrations + // for more information about preprocessors + preprocess: vitePreprocess(), + + kit: { + // adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list. + // If your environment is not supported, or you settled on a specific environment, switch out the adapter. + // See https://svelte.dev/docs/kit/adapters for more information about adapters. + adapter: adapter() + } +}; + +export default config; diff --git a/samples/sveltekit/tsconfig.json b/samples/sveltekit/tsconfig.json new file mode 100644 index 00000000..2c2ed3c4 --- /dev/null +++ b/samples/sveltekit/tsconfig.json @@ -0,0 +1,20 @@ +{ + "extends": "./.svelte-kit/tsconfig.json", + "compilerOptions": { + "rewriteRelativeImportExtensions": true, + "allowJs": true, + "checkJs": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "sourceMap": true, + "strict": true, + "moduleResolution": "bundler" + } + // Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias + // except $lib which is handled by https://svelte.dev/docs/kit/configuration#files + // + // To make changes to top-level options such as include and exclude, we recommend extending + // the generated config; see https://svelte.dev/docs/kit/configuration#typescript +} diff --git a/samples/sveltekit/vite.config.ts b/samples/sveltekit/vite.config.ts new file mode 100644 index 00000000..23797dad --- /dev/null +++ b/samples/sveltekit/vite.config.ts @@ -0,0 +1,5 @@ +import tailwindcss from "@tailwindcss/vite"; +import { sveltekit } from "@sveltejs/kit/vite"; +import { defineConfig } from "vite"; + +export default defineConfig({ plugins: [tailwindcss(), sveltekit()] }); diff --git a/scripts/bump-version.ts b/scripts/bump-version.ts index 5fdda3fd..204c4a38 100644 --- a/scripts/bump-version.ts +++ b/scripts/bump-version.ts @@ -32,13 +32,26 @@ function getWorkspacePackageJsonFiles(workspaceFile: string): string[] { return result; } -function incrementVersion(version: string): string { +function incrementVersion(version: string, type: 'patch' | 'minor' = 'patch'): string { const parts = version.split('.'); - const last = parts.length - 1; - const lastNum = parseInt(parts[last], 10); - if (isNaN(lastNum)) throw new Error(`Invalid version: ${version}`); - parts[last] = (lastNum + 1).toString(); - return parts.join('.'); + if (parts.length !== 3) throw new Error(`Invalid version format: ${version}`); + + const [major, minor, patch] = parts.map(p => parseInt(p, 10)); + if (isNaN(major) || isNaN(minor) || isNaN(patch)) { + throw new Error(`Invalid version: ${version}`); + } + + if (type === 'minor') { + return `${major}.${minor + 1}.0`; + } else { + return `${major}.${minor}.${patch + 1}`; + } +} + +// get version type from command line argument +const versionType = process.argv[2] as 'patch' | 'minor' | undefined; +if (versionType && versionType !== 'patch' && versionType !== 'minor') { + throw new Error(`Invalid version type: ${versionType}. Expected 'patch' or 'minor'.`); } // find all package.json files in the workspace @@ -50,7 +63,7 @@ const rootPackageJson = path.resolve(_dirname, '../package.json'); const rootPkg = JSON.parse(fs.readFileSync(rootPackageJson, 'utf8')) as { version?: string }; if (!rootPkg.version) throw new Error('No "version" key found in package.json'); const rootVersion = rootPkg.version; -const newVersion = incrementVersion(rootVersion); +const newVersion = incrementVersion(rootVersion, versionType || 'patch'); for (const file of packageFiles) { const content = fs.readFileSync(file, 'utf8'); diff --git a/tests/e2e/orm/client-api/default-values.test.ts b/tests/e2e/orm/client-api/default-values.test.ts index dea5a3bc..c9d104e8 100644 --- a/tests/e2e/orm/client-api/default-values.test.ts +++ b/tests/e2e/orm/client-api/default-values.test.ts @@ -1,92 +1,38 @@ -import { isCuid } from '@paralleldrive/cuid2'; -import { ZenStackClient } from '@zenstackhq/orm'; -import { ExpressionUtils, type SchemaDef } from '@zenstackhq/orm/schema'; -import SQLite from 'better-sqlite3'; -import { SqliteDialect } from 'kysely'; +import { isCuid as isCuidV2 } from '@paralleldrive/cuid2'; +import { isCuid as isCuidV1 } from 'cuid'; +import { createTestClient } from '@zenstackhq/testtools'; import { isValid as isValidUlid } from 'ulid'; -import { validate as isValidUuid } from 'uuid'; +import { validate as isValidUuid, version as getUuidVersion } from 'uuid'; import { describe, expect, it } from 'vitest'; -const schema = { - provider: { - type: 'sqlite', - }, - models: { - Model: { - name: 'Model', - fields: { - id: { - name: 'id', - type: 'Int', - id: true, - }, - uuid: { - name: 'uuid', - type: 'String', - default: ExpressionUtils.call('uuid'), - }, - uuid7: { - name: 'uuid7', - type: 'String', - default: ExpressionUtils.call('uuid', [ExpressionUtils.literal(7)]), - }, - cuid: { - name: 'cuid', - type: 'String', - default: ExpressionUtils.call('cuid'), - }, - cuid2: { - name: 'cuid2', - type: 'String', - default: ExpressionUtils.call('cuid', [ExpressionUtils.literal(2)]), - }, - nanoid: { - name: 'nanoid', - type: 'String', - default: ExpressionUtils.call('nanoid'), - }, - nanoid8: { - name: 'nanoid8', - type: 'String', - default: ExpressionUtils.call('nanoid', [ExpressionUtils.literal(8)]), - }, - ulid: { - name: 'ulid', - type: 'String', - default: ExpressionUtils.call('ulid'), - }, - dt: { - name: 'dt', - type: 'DateTime', - default: ExpressionUtils.call('now'), - }, - bool: { - name: 'bool', - type: 'Boolean', - default: false, - }, - }, - idFields: ['id'], - uniqueFields: { - id: { type: 'Int' }, - }, - }, - }, - plugins: {}, -} as const satisfies SchemaDef; +const schema = ` +model Model { + id Int @id + uuid String @default(uuid()) + uuid4 String @default(uuid(4)) + uuid7 String @default(uuid(7)) + cuid String @default(cuid()) + cuid1 String @default(cuid(1)) + cuid2 String @default(cuid(2)) + nanoid String @default(nanoid()) + nanoid8 String @default(nanoid(8)) + ulid String @default(ulid()) + dt DateTime @default(now()) + bool Boolean @default(false) +} +`; describe('default values tests', () => { it('supports defaults', async () => { - const client = new ZenStackClient(schema, { - dialect: new SqliteDialect({ database: new SQLite(':memory:') }), - }); - await client.$pushSchema(); + const client = await createTestClient(schema); const entity = await client.model.create({ data: { id: 1 } }); - expect(entity.uuid).toSatisfy(isValidUuid); - expect(entity.uuid7).toSatisfy(isValidUuid); - expect(entity.cuid).toSatisfy(isCuid); - expect(entity.cuid2).toSatisfy(isCuid); + expect(entity.uuid).toSatisfy((id) => isValidUuid(id) && getUuidVersion(id) === 4); + expect(entity.uuid4).toSatisfy((id) => isValidUuid(id) && getUuidVersion(id) === 4); + expect(entity.uuid7).toSatisfy((id) => isValidUuid(id) && getUuidVersion(id) === 7); + expect(entity.cuid).toSatisfy(isCuidV1); + expect(entity.cuid1).toSatisfy(isCuidV1); + expect(entity.cuid2).toSatisfy(isCuidV2); expect(entity.nanoid).toSatisfy((id) => id.length >= 21); expect(entity.nanoid8).toSatisfy((id) => id.length === 8); expect(entity.ulid).toSatisfy(isValidUlid); diff --git a/tests/e2e/orm/client-api/generated-id-format-strings.test.ts b/tests/e2e/orm/client-api/generated-id-format-strings.test.ts new file mode 100644 index 00000000..db81603a --- /dev/null +++ b/tests/e2e/orm/client-api/generated-id-format-strings.test.ts @@ -0,0 +1,180 @@ +import { createTestClient } from '@zenstackhq/testtools'; +import { describe, expect, it } from 'vitest'; + +const schema = ` +model User { + id Int @id + uuid String @default(uuid(4, "user_uuid_%s")) + uuid7 String @default(uuid(7, "user_uuid7_%s")) + cuid String @default(cuid(2, "user_cuid_%s")) + cuid2 String @default(cuid(2, "user_cuid2_%s")) + nanoid String @default(nanoid(21, "user_nanoid_%s")) + nanoid8 String @default(nanoid(8, "user_nanoid8_%s")) + ulid String @default(ulid("user_ulid_%s")) + posts Post[] +} + +model Post { + id Int @id + uuid String @default(uuid(4, "post_uuid_%s")) + uuid7 String @default(uuid(7, "post_uuid7_%s")) + cuid String @default(cuid(2, "post_cuid_%s")) + cuid2 String @default(cuid(2, "post_cuid2_%s")) + nanoid String @default(nanoid(21, "post_nanoid_%s")) + nanoid8 String @default(nanoid(8, "post_nanoid8_%s")) + ulid String @default(ulid("post_ulid_%s")) + userId Int + user User @relation(fields: [userId], references: [id]) + comments Comment[] +} + +model Comment { + id Int @id + uuid String @default(uuid(4, "comment_uuid_%s")) + uuid7 String @default(uuid(7, "comment_uuid7_%s")) + cuid String @default(cuid(2, "comment_cuid_%s")) + cuid2 String @default(cuid(2, "comment_cuid2_%s")) + nanoid String @default(nanoid(21, "comment_nanoid_%s")) + nanoid8 String @default(nanoid(8, "comment_nanoid8_%s")) + ulid String @default(ulid("comment_ulid_%s")) + postId Int + post Post @relation(fields: [postId], references: [id]) +} +`; + +describe('generated id format strings', () => { + it('supports top-level ids', async () => { + const client = await createTestClient(schema); + + const user = await client.user.create({ + data: { + id: 1, + }, + }); + expect(user.uuid).toMatch(/^user_uuid_/); + expect(user.uuid7).toMatch(/^user_uuid7_/); + expect(user.cuid).toMatch(/^user_cuid_/); + expect(user.cuid2).toMatch(/^user_cuid2_/); + expect(user.nanoid).toMatch(/^user_nanoid_/); + expect(user.nanoid8).toMatch(/^user_nanoid8_/); + expect(user.ulid).toMatch(/^user_ulid_/); + }); + + it('supports nested ids', async () => { + const client = await createTestClient(schema); + + const user = await client.user.create({ + data: { + id: 1, + + posts: { + create: { + id: 1, + }, + }, + }, + }); + expect(user.uuid).toMatch(/^user_uuid_/); + expect(user.uuid7).toMatch(/^user_uuid7_/); + expect(user.cuid).toMatch(/^user_cuid_/); + expect(user.cuid2).toMatch(/^user_cuid2_/); + expect(user.nanoid).toMatch(/^user_nanoid_/); + expect(user.nanoid8).toMatch(/^user_nanoid8_/); + expect(user.ulid).toMatch(/^user_ulid_/); + + const post = await client.post.findUniqueOrThrow({ where: { id: 1 } }); + expect(post.uuid).toMatch(/^post_uuid_/); + expect(post.uuid7).toMatch(/^post_uuid7_/); + expect(post.cuid).toMatch(/^post_cuid_/); + expect(post.cuid2).toMatch(/^post_cuid2_/); + expect(post.nanoid).toMatch(/^post_nanoid_/); + expect(post.nanoid8).toMatch(/^post_nanoid8_/); + expect(post.ulid).toMatch(/^post_ulid_/); + }); + + it('supports deeply nested ids', async () => { + const client = await createTestClient(schema); + + const user = await client.user.create({ + data: { + id: 1, + + posts: { + create: { + id: 1, + + comments: { + create: { + id: 1, + }, + }, + }, + }, + }, + }); + expect(user.uuid).toMatch(/^user_uuid_/); + expect(user.uuid7).toMatch(/^user_uuid7_/); + expect(user.cuid).toMatch(/^user_cuid_/); + expect(user.cuid2).toMatch(/^user_cuid2_/); + expect(user.nanoid).toMatch(/^user_nanoid_/); + expect(user.nanoid8).toMatch(/^user_nanoid8_/); + expect(user.ulid).toMatch(/^user_ulid_/); + + const post = await client.post.findUniqueOrThrow({ where: { id: 1 } }); + expect(post.uuid).toMatch(/^post_uuid_/); + expect(post.uuid7).toMatch(/^post_uuid7_/); + expect(post.cuid).toMatch(/^post_cuid_/); + expect(post.cuid2).toMatch(/^post_cuid2_/); + expect(post.nanoid).toMatch(/^post_nanoid_/); + expect(post.nanoid8).toMatch(/^post_nanoid8_/); + expect(post.ulid).toMatch(/^post_ulid_/); + + const comment = await client.comment.findUniqueOrThrow({ where: { id: 1 } }); + expect(comment.uuid).toMatch(/^comment_uuid_/); + expect(comment.uuid7).toMatch(/^comment_uuid7_/); + expect(comment.cuid).toMatch(/^comment_cuid_/); + expect(comment.cuid2).toMatch(/^comment_cuid2_/); + expect(comment.nanoid).toMatch(/^comment_nanoid_/); + expect(comment.nanoid8).toMatch(/^comment_nanoid8_/); + expect(comment.ulid).toMatch(/^comment_ulid_/); + }); + + it('supports escaped placeholders and edge cases', async () => { + const escapedSchema = ` +model EscapedTest { + id Int @id + consecutive String @default(uuid(4, "%s%s")) + mixedEscaped String @default(uuid(4, "\\\\%s_%s_end")) + mixedEscaped2 String @default(uuid(4, "%s_\\\\%s_end")) + mixedEscaped3 String @default(uuid(4, "\\\\%s_\\\\%s_%s")) + startWithPattern String @default(uuid(4, "%s_suffix")) + endWithPattern String @default(uuid(4, "prefix_%s")) +} +`; + const client = await createTestClient(escapedSchema); + + const record = await client.escapedTest.create({ + data: { + id: 1, + }, + }); + + // Consecutive %s%s should both be replaced + expect(record.consecutive).toMatch(/^[0-9a-f-]{36}[0-9a-f-]{36}$/); + + // Mixed: first \%s stays as %s, second %s is replaced + expect(record.mixedEscaped).toMatch(/^%s_[0-9a-f-]{36}_end$/); + + // Mixed: first %s is replaced, second \%s stays as %s + expect(record.mixedEscaped2).toMatch(/^[0-9a-f-]{36}_%s_end$/); + + // Mixed: first and second \%s stays as %s, third %s is replaced + expect(record.mixedEscaped3).toMatch(/^%s_%s_[0-9a-f-]{36}$/); + + // Pattern at start + expect(record.startWithPattern).toMatch(/^[0-9a-f-]{36}_suffix$/); + + // Pattern at end + expect(record.endWithPattern).toMatch(/^prefix_[0-9a-f-]{36}$/); + }); +}); diff --git a/tests/e2e/orm/client-api/name-mapping.test.ts b/tests/e2e/orm/client-api/name-mapping.test.ts index 5fd699a9..2b70cd46 100644 --- a/tests/e2e/orm/client-api/name-mapping.test.ts +++ b/tests/e2e/orm/client-api/name-mapping.test.ts @@ -8,10 +8,10 @@ describe('Name mapping tests', () => { let db: ClientContract; beforeEach(async () => { - db = (await createTestClient(schema, { + db = await createTestClient(schema, { usePrismaPush: true, schemaFile: path.join(__dirname, '../schemas/name-mapping/schema.zmodel'), - })) as any; + }); }); afterEach(async () => { @@ -46,6 +46,16 @@ describe('Name mapping tests', () => { user_role: 'role_user', }); + rawRead = await db.$qbRaw + .selectFrom('users') + .where('user_role', '=', 'role_user') + .selectAll() + .executeTakeFirst(); + await expect(rawRead).toMatchObject({ + user_email: 'u1@test.com', + user_role: 'role_user', + }); + await expect( db.user.create({ data: { @@ -66,6 +76,15 @@ describe('Name mapping tests', () => { user_role: 'MODERATOR', }); + rawRead = await db.$qbRaw + .selectFrom('users') + .where('user_role', '=', 'MODERATOR') + .selectAll() + .executeTakeFirst(); + await expect(rawRead).toMatchObject({ + user_role: 'MODERATOR', + }); + await expect( db.$qb .insertInto('User') @@ -146,6 +165,64 @@ describe('Name mapping tests', () => { posts: [{ title: 'Post1' }], }); + await expect( + db.user.findFirst({ + where: { role: 'USER' }, + select: { + email: true, + role: true, + }, + }), + ).resolves.toMatchObject({ + email: 'u1@test.com', + role: 'USER', + }); + + await expect( + db.user.findMany({ + where: { role: 'USER' }, + select: { + email: true, + role: true, + }, + }), + ).resolves.toEqual([expect.objectContaining({ email: 'u1@test.com', role: 'USER' })]); + + await expect( + db.user.findFirst({ + where: { role: { in: ['USER'] } }, + select: { + email: true, + role: true, + }, + }), + ).resolves.toMatchObject({ + email: 'u1@test.com', + role: 'USER', + }); + + await expect( + db.user.findMany({ + where: { role: { in: ['USER'] } }, + select: { + email: true, + role: true, + }, + }), + ).resolves.toEqual([expect.objectContaining({ email: 'u1@test.com', role: 'USER' })]); + + await expect( + db.user.findMany({ + where: { + AND: [{ role: { in: ['USER'] } }, { role: { in: ['USER'] } }, { OR: [{ role: { in: ['USER'] } }] }], + }, + select: { + email: true, + role: true, + }, + }), + ).resolves.toEqual([expect.objectContaining({ email: 'u1@test.com', role: 'USER' })]); + // select all await expect( db.user.findFirst({ @@ -185,6 +262,38 @@ describe('Name mapping tests', () => { role: 'USER', }); + // name mapping for enum value in where clause, with unqualified column name + await expect( + db.$qb.selectFrom('User').select(['User.email', 'User.role']).where('role', '=', 'USER').executeTakeFirst(), + ).resolves.toMatchObject({ + email: 'u1@test.com', + role: 'USER', + }); + + // name mapping for enum value in simple where clause, with qualified column name + await expect( + db.$qb + .selectFrom('User as u') + .select(['u.email', 'u.role']) + .where('u.role', '=', 'USER') + .executeTakeFirst(), + ).resolves.toMatchObject({ + email: 'u1@test.com', + role: 'USER', + }); + + // enum value in list + await expect( + db.$qb + .selectFrom('User') + .select(['User.email', 'User.role']) + .where('role', 'in', ['USER', 'ADMIN']) + .executeTakeFirst(), + ).resolves.toMatchObject({ + email: 'u1@test.com', + role: 'USER', + }); + await expect( db.$qb .selectFrom('User') diff --git a/tests/e2e/package.json b/tests/e2e/package.json index 201a9211..d1127361 100644 --- a/tests/e2e/package.json +++ b/tests/e2e/package.json @@ -1,6 +1,6 @@ { "name": "e2e", - "version": "3.0.0", + "version": "3.1.0", "private": true, "type": "module", "scripts": { @@ -15,16 +15,17 @@ "@paralleldrive/cuid2": "^2.2.2", "@zenstackhq/cli": "workspace:*", "@zenstackhq/language": "workspace:*", - "@zenstackhq/schema": "workspace:*", "@zenstackhq/orm": "workspace:*", "@zenstackhq/plugin-policy": "workspace:*", + "@zenstackhq/schema": "workspace:*", "@zenstackhq/sdk": "workspace:*", "@zenstackhq/testtools": "workspace:*", "better-sqlite3": "catalog:", "decimal.js": "catalog:", "kysely": "catalog:", "ulid": "^3.0.0", - "uuid": "^11.0.5" + "uuid": "^11.0.5", + "cuid": "^3.0.0" }, "devDependencies": { "@zenstackhq/cli": "workspace:*", diff --git a/tests/regression/package.json b/tests/regression/package.json index c3568f37..3ada51c3 100644 --- a/tests/regression/package.json +++ b/tests/regression/package.json @@ -1,6 +1,6 @@ { "name": "regression", - "version": "3.0.0", + "version": "3.1.0", "private": true, "type": "module", "scripts": { diff --git a/tests/runtimes/bun/package.json b/tests/runtimes/bun/package.json index cf799384..240c5b7a 100644 --- a/tests/runtimes/bun/package.json +++ b/tests/runtimes/bun/package.json @@ -1,6 +1,6 @@ { "name": "bun-e2e", - "version": "3.0.0", + "version": "3.1.0", "private": true, "type": "module", "scripts": { diff --git a/tests/runtimes/edge-runtime/package.json b/tests/runtimes/edge-runtime/package.json index 85669a53..ab9a8457 100644 --- a/tests/runtimes/edge-runtime/package.json +++ b/tests/runtimes/edge-runtime/package.json @@ -1,6 +1,6 @@ { "name": "edge-runtime-e2e", - "version": "3.0.0", + "version": "3.1.0", "private": true, "type": "module", "scripts": { diff --git a/vitest.config.ts b/vitest.config.ts index e4c2666a..60851642 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -2,6 +2,6 @@ import { defineConfig } from 'vitest/config'; export default defineConfig({ test: { - projects: ['packages/**', 'tests/**'], + projects: ['packages/**/vitest.config.ts', 'tests/**/vitest.config.ts'], }, });