diff --git a/package.json b/package.json index 7eebb630..eca11cee 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "zenstack-v3", - "version": "3.0.0-beta.17", + "version": "3.0.0-beta.18", "description": "ZenStack", "packageManager": "pnpm@10.20.0", "scripts": { diff --git a/packages/cli/package.json b/packages/cli/package.json index b4585b17..0fcf9242 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-beta.17", + "version": "3.0.0-beta.18", "type": "module", "author": { "name": "ZenStack Team" diff --git a/packages/clients/tanstack-query/package.json b/packages/clients/tanstack-query/package.json index caba300f..60810ad0 100644 --- a/packages/clients/tanstack-query/package.json +++ b/packages/clients/tanstack-query/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/tanstack-query", - "version": "3.0.0-beta.17", + "version": "3.0.0-beta.18", "description": "TanStack Query Client for consuming ZenStack v3's CRUD service", "main": "index.js", "type": "module", @@ -32,6 +32,30 @@ "types": "./dist/react.d.cts", "default": "./dist/react.cjs" } + }, + "./vue": { + "import": { + "types": "./dist/vue.d.ts", + "default": "./dist/vue.js" + }, + "require": { + "types": "./dist/vue.d.cts", + "default": "./dist/vue.cjs" + } + }, + "./svelte": { + "import": { + "types": "./dist/svelte.d.ts", + "default": "./dist/svelte.js" + }, + "require": { + "types": "./dist/svelte.d.cts", + "default": "./dist/svelte.cjs" + } + }, + "./package.json": { + "import": "./package.json", + "require": "./package.json" } }, "dependencies": { @@ -43,6 +67,8 @@ }, "devDependencies": { "@tanstack/react-query": "catalog:", + "@tanstack/vue-query": "5.90.6", + "@tanstack/svelte-query": "5.90.2", "@testing-library/dom": "^10.4.1", "@testing-library/react": "^16.3.0", "@types/react": "catalog:", @@ -53,17 +79,23 @@ "@zenstackhq/vitest-config": "workspace:*", "happy-dom": "^20.0.10", "nock": "^14.0.10", - "react": "catalog:" + "react": "catalog:", + "vue": "catalog:", + "svelte": "catalog:" }, "peerDependencies": { "@tanstack/react-query": "^5.0.0", - "react": "^18 || ^19" + "@tanstack/vue-query": "^5.0.0", + "@tanstack/svelte-query": "^5.0.0" }, "peerDependenciesMeta": { "@tanstack/react-query": { "optional": true }, - "react": { + "@tanstack/vue-query": { + "optional": true + }, + "@tanstack/svelte-query": { "optional": true } } diff --git a/packages/clients/tanstack-query/src/react.ts b/packages/clients/tanstack-query/src/react.ts index d860f5b5..15bfbb51 100644 --- a/packages/clients/tanstack-query/src/react.ts +++ b/packages/clients/tanstack-query/src/react.ts @@ -36,7 +36,9 @@ import type { GroupByArgs, GroupByResult, ModelResult, + SelectIncludeOmit, SelectSubset, + Subset, UpdateArgs, UpdateManyAndReturnArgs, UpdateManyArgs, @@ -55,6 +57,7 @@ import { type ExtraMutationOptions, type ExtraQueryOptions, } from './utils/common'; +import type { TrimDelegateModelOperations } from './utils/types'; export type { FetchFn } from './utils/common'; @@ -79,7 +82,7 @@ export const QuerySettingsProvider = QuerySettingsContext.Provider; /** * React context provider for configuring query settings. * - * @deprecated Use `QuerySettingsProvider` instead. + * @deprecated Use {@link QuerySettingsProvider} instead. */ export const Provider = QuerySettingsProvider; @@ -118,120 +121,144 @@ export type ModelMutationOptions = Omit = UseMutationResult; -export type SchemaHooks = { - [Model in GetModels as `${Uncapitalize}`]: ModelQueryHooks; -}; - -export type ModelQueryHooks> = { - useFindUnique>( - args: SelectSubset>, - options?: ModelQueryOptions | null>, - ): ModelQueryResult | null>; - - useSuspenseFindUnique>( - args: SelectSubset>, - options?: ModelSuspenseQueryOptions | null>, - ): ModelSuspenseQueryResult | null>; - - useFindFirst>( - args?: SelectSubset>, - options?: ModelQueryOptions | null>, - ): ModelQueryResult | null>; - - useSuspenseFindFirst>( - args?: SelectSubset>, - options?: ModelSuspenseQueryOptions | null>, - ): ModelSuspenseQueryResult | null>; - - useFindMany>( - args?: SelectSubset>, - options?: ModelQueryOptions[]>, - ): ModelQueryResult[]>; - - useSuspenseFindMany>( - args?: SelectSubset>, - options?: ModelSuspenseQueryOptions[]>, - ): ModelSuspenseQueryResult[]>; - - useInfiniteFindMany>( - args?: SelectSubset>, - options?: ModelInfiniteQueryOptions[]>, - ): ModelInfiniteQueryResult[]>>; - - useSuspenseInfiniteFindMany>( - args?: SelectSubset>, - options?: ModelSuspenseInfiniteQueryOptions[]>, - ): ModelSuspenseInfiniteQueryResult[]>>; - - useCreate>( - options?: ModelMutationOptions, T>, - ): ModelMutationResult, T>; - - useCreateMany>( - options?: ModelMutationOptions, - ): ModelMutationResult; - - useCreateManyAndReturn>( - options?: ModelMutationOptions[], T>, - ): ModelMutationResult[], T>; - - useUpdate>( +export type ModelMutationModelResult< + Schema extends SchemaDef, + Model extends GetModels, + TArgs extends SelectIncludeOmit, + Array extends boolean = false, +> = Omit, TArgs>, 'mutateAsync'> & { + mutateAsync( + args: T, options?: ModelMutationOptions, T>, - ): ModelMutationResult, T>; - - useUpdateMany>( - options?: ModelMutationOptions, - ): ModelMutationResult; - - useUpdateManyAndReturn>( - options?: ModelMutationOptions[], T>, - ): ModelMutationResult[], T>; - - useUpsert>( - options?: ModelMutationOptions, T>, - ): ModelMutationResult, T>; - - useDelete>( - options?: ModelMutationOptions, T>, - ): ModelMutationResult, T>; - - useDeleteMany>( - options?: ModelMutationOptions, - ): ModelMutationResult; - - useCount>( - options?: ModelQueryOptions>, - ): ModelQueryResult>; - - useSuspenseCount>( - options?: ModelSuspenseQueryOptions>, - ): ModelSuspenseQueryResult>; - - useAggregate>( - options?: ModelQueryOptions>, - ): ModelQueryResult>; - - useSuspenseAggregate>( - options?: ModelSuspenseQueryOptions>, - ): ModelSuspenseQueryResult>; - - useGroupBy>( - options?: ModelQueryOptions>, - ): ModelQueryResult>; + ): Promise[] : ModelResult>; +}; - useSuspenseGroupBy>( - options?: ModelSuspenseQueryOptions>, - ): ModelSuspenseQueryResult>; +export type ClientHooks = { + [Model in GetModels as `${Uncapitalize}`]: ModelQueryHooks; }; +// Note that we can potentially use TypeScript's mapped type to directly map from ORM contract, but that seems +// to significantly slow down tsc performance ... +export type ModelQueryHooks> = TrimDelegateModelOperations< + Schema, + Model, + { + useFindUnique>( + args: SelectSubset>, + options?: ModelQueryOptions | null>, + ): ModelQueryResult | null>; + + useSuspenseFindUnique>( + args: SelectSubset>, + options?: ModelSuspenseQueryOptions | null>, + ): ModelSuspenseQueryResult | null>; + + useFindFirst>( + args?: SelectSubset>, + options?: ModelQueryOptions | null>, + ): ModelQueryResult | null>; + + useSuspenseFindFirst>( + args?: SelectSubset>, + options?: ModelSuspenseQueryOptions | null>, + ): ModelSuspenseQueryResult | null>; + + useFindMany>( + args?: SelectSubset>, + options?: ModelQueryOptions[]>, + ): ModelQueryResult[]>; + + useSuspenseFindMany>( + args?: SelectSubset>, + options?: ModelSuspenseQueryOptions[]>, + ): ModelSuspenseQueryResult[]>; + + useInfiniteFindMany>( + args?: SelectSubset>, + options?: ModelInfiniteQueryOptions[]>, + ): ModelInfiniteQueryResult[]>>; + + useSuspenseInfiniteFindMany>( + args?: SelectSubset>, + options?: ModelSuspenseInfiniteQueryOptions[]>, + ): ModelSuspenseInfiniteQueryResult[]>>; + + useCreate>( + options?: ModelMutationOptions, T>, + ): ModelMutationModelResult; + + useCreateMany>( + options?: ModelMutationOptions, + ): ModelMutationResult; + + useCreateManyAndReturn>( + options?: ModelMutationOptions[], T>, + ): ModelMutationModelResult; + + useUpdate>( + options?: ModelMutationOptions, T>, + ): ModelMutationModelResult; + + useUpdateMany>( + options?: ModelMutationOptions, + ): ModelMutationResult; + + useUpdateManyAndReturn>( + options?: ModelMutationOptions[], T>, + ): ModelMutationModelResult; + + useUpsert>( + options?: ModelMutationOptions, T>, + ): ModelMutationModelResult; + + useDelete>( + options?: ModelMutationOptions, T>, + ): ModelMutationModelResult; + + useDeleteMany>( + options?: ModelMutationOptions, + ): ModelMutationResult; + + useCount>( + args?: Subset>, + options?: ModelQueryOptions>, + ): ModelQueryResult>; + + useSuspenseCount>( + args?: Subset>, + options?: ModelSuspenseQueryOptions>, + ): ModelSuspenseQueryResult>; + + useAggregate>( + args: Subset>, + options?: ModelQueryOptions>, + ): ModelQueryResult>; + + useSuspenseAggregate>( + args: Subset>, + options?: ModelSuspenseQueryOptions>, + ): ModelSuspenseQueryResult>; + + useGroupBy>( + args: Subset>, + options?: ModelQueryOptions>, + ): ModelQueryResult>; + + useSuspenseGroupBy>( + args: Subset>, + options?: ModelSuspenseQueryOptions>, + ): ModelSuspenseQueryResult>; + } +>; + /** * Gets data query hooks for all models in the schema. */ -export function useClientQueries(schema: Schema): SchemaHooks { +export function useClientQueries(schema: Schema): ClientHooks { return Object.keys(schema.models).reduce((acc, model) => { (acc as any)[lowerCaseFirst(model)] = useModelQueries(schema, model as GetModels); return acc; - }, {} as SchemaHooks); + }, {} as ClientHooks); } /** @@ -282,63 +309,63 @@ export function useModelQueries { - return useInternalMutation(schema, modelName, 'POST', 'create', options, true); + return useInternalMutation(schema, modelName, 'POST', 'create', options); }, useCreateMany: (options?: any) => { - return useInternalMutation(schema, modelName, 'POST', 'createMany', options, false); + return useInternalMutation(schema, modelName, 'POST', 'createMany', options); }, useCreateManyAndReturn: (options?: any) => { - return useInternalMutation(schema, modelName, 'POST', 'createManyAndReturn', options, true); + return useInternalMutation(schema, modelName, 'POST', 'createManyAndReturn', options); }, useUpdate: (options?: any) => { - return useInternalMutation(schema, modelName, 'PUT', 'update', options, true); + return useInternalMutation(schema, modelName, 'PUT', 'update', options); }, useUpdateMany: (options?: any) => { - return useInternalMutation(schema, modelName, 'PUT', 'updateMany', options, false); + return useInternalMutation(schema, modelName, 'PUT', 'updateMany', options); }, useUpdateManyAndReturn: (options?: any) => { - return useInternalMutation(schema, modelName, 'PUT', 'updateManyAndReturn', options, true); + return useInternalMutation(schema, modelName, 'PUT', 'updateManyAndReturn', options); }, useUpsert: (options?: any) => { - return useInternalMutation(schema, modelName, 'POST', 'upsert', options, true); + return useInternalMutation(schema, modelName, 'POST', 'upsert', options); }, useDelete: (options?: any) => { - return useInternalMutation(schema, modelName, 'DELETE', 'delete', options, true); + return useInternalMutation(schema, modelName, 'DELETE', 'delete', options); }, useDeleteMany: (options?: any) => { - return useInternalMutation(schema, modelName, 'DELETE', 'deleteMany', options, false); + return useInternalMutation(schema, modelName, 'DELETE', 'deleteMany', options); }, - useCount: (options?: any) => { - return useInternalQuery(schema, modelName, 'count', undefined, options); + useCount: (args: any, options?: any) => { + return useInternalQuery(schema, modelName, 'count', args, options); }, - useSuspenseCount: (options?: any) => { - return useInternalSuspenseQuery(schema, modelName, 'count', undefined, options); + useSuspenseCount: (args: any, options?: any) => { + return useInternalSuspenseQuery(schema, modelName, 'count', args, options); }, - useAggregate: (options?: any) => { - return useInternalQuery(schema, modelName, 'aggregate', undefined, options); + useAggregate: (args: any, options?: any) => { + return useInternalQuery(schema, modelName, 'aggregate', args, options); }, - useSuspenseAggregate: (options?: any) => { - return useInternalSuspenseQuery(schema, modelName, 'aggregate', undefined, options); + useSuspenseAggregate: (args: any, options?: any) => { + return useInternalSuspenseQuery(schema, modelName, 'aggregate', args, options); }, - useGroupBy: (options?: any) => { - return useInternalQuery(schema, modelName, 'groupBy', undefined, options); + useGroupBy: (args: any, options?: any) => { + return useInternalQuery(schema, modelName, 'groupBy', args, options); }, - useSuspenseGroupBy: (options?: any) => { - return useInternalSuspenseQuery(schema, modelName, 'groupBy', undefined, options); + useSuspenseGroupBy: (args: any, options?: any) => { + return useInternalSuspenseQuery(schema, modelName, 'groupBy', args, options); }, } as ModelQueryHooks; } @@ -360,7 +387,7 @@ export function useInternalQuery( queryKey, ...useQuery({ queryKey, - queryFn: ({ signal }) => fetcher(reqUrl, { signal }, fetch, false), + queryFn: ({ signal }) => fetcher(reqUrl, { signal }, fetch), ...options, }), }; @@ -383,7 +410,7 @@ export function useInternalSuspenseQuery( queryKey, ...useSuspenseQuery({ queryKey, - queryFn: ({ signal }) => fetcher(reqUrl, { signal }, fetch, false), + queryFn: ({ signal }) => fetcher(reqUrl, { signal }, fetch), ...options, }), }; @@ -394,11 +421,14 @@ export function useInternalInfiniteQuery( model: string, operation: string, args: unknown, - options: Omit< - UseInfiniteQueryOptions>, - 'queryKey' | 'initialPageParam' - >, + options: + | Omit< + UseInfiniteQueryOptions>, + 'queryKey' | 'initialPageParam' + > + | undefined, ) { + options = options ?? { getNextPageParam: () => undefined }; const { endpoint, fetch } = useHooksContext(); const queryKey = getQueryKey(model, operation, args, { infinite: true, optimisticUpdate: false }); return { @@ -406,12 +436,7 @@ export function useInternalInfiniteQuery( ...useInfiniteQuery({ queryKey, queryFn: ({ pageParam, signal }) => { - return fetcher( - makeUrl(endpoint, model, operation, pageParam ?? args), - { signal }, - fetch, - false, - ); + return fetcher(makeUrl(endpoint, model, operation, pageParam ?? args), { signal }, fetch); }, initialPageParam: args, ...options, @@ -436,12 +461,7 @@ export function useInternalSuspenseInfiniteQuery( ...useSuspenseInfiniteQuery({ queryKey, queryFn: ({ pageParam, signal }) => { - return fetcher( - makeUrl(endpoint, model, operation, pageParam ?? args), - { signal }, - fetch, - false, - ); + return fetcher(makeUrl(endpoint, model, operation, pageParam ?? args), { signal }, fetch); }, initialPageParam: args, ...options, @@ -460,20 +480,14 @@ export function useInternalSuspenseInfiniteQuery( * @param options The react-query options. * @param checkReadBack Whether to check for read back errors and return undefined if found. */ -export function useInternalMutation< - TArgs, - R = any, - C extends boolean = boolean, - Result = C extends true ? R | undefined : R, ->( +export function useInternalMutation( schema: SchemaDef, model: string, method: 'POST' | 'PUT' | 'DELETE', operation: string, - options?: Omit, 'mutationFn'> & ExtraMutationOptions, - checkReadBack?: C, + options?: Omit, 'mutationFn'> & ExtraMutationOptions, ) { - const { endpoint, fetch } = useHooksContext(); + const { endpoint, fetch, logging } = useHooksContext(); const queryClient = useQueryClient(); const mutationFn = (data: any) => { const reqUrl = @@ -487,13 +501,12 @@ export function useInternalMutation< body: marshal(data), }), }; - return fetcher(reqUrl, fetchInit, fetch, checkReadBack) as Promise; + return fetcher(reqUrl, fetchInit, fetch) as Promise; }; const finalOptions = { ...options, mutationFn }; const invalidateQueries = options?.invalidateQueries !== false; const optimisticUpdate = !!options?.optimisticUpdate; - const { logging } = useContext(QuerySettingsContext); if (operation) { if (invalidateQueries) { diff --git a/packages/clients/tanstack-query/src/svelte.ts b/packages/clients/tanstack-query/src/svelte.ts new file mode 100644 index 00000000..33cf86bd --- /dev/null +++ b/packages/clients/tanstack-query/src/svelte.ts @@ -0,0 +1,483 @@ +import { + createInfiniteQuery, + createMutation, + createQuery, + useQueryClient, + type CreateInfiniteQueryOptions, + type CreateInfiniteQueryResult, + type CreateMutationOptions, + type CreateMutationResult, + type CreateQueryOptions, + type CreateQueryResult, + type DefaultError, + type InfiniteData, + type QueryFunction, + type QueryKey, + type StoreOrVal, +} from '@tanstack/svelte-query'; +import { lowerCaseFirst } from '@zenstackhq/common-helpers'; +import type { + AggregateArgs, + AggregateResult, + BatchResult, + CountArgs, + CountResult, + CreateArgs, + CreateManyAndReturnArgs, + CreateManyArgs, + DeleteArgs, + DeleteManyArgs, + FindArgs, + FindUniqueArgs, + GroupByArgs, + GroupByResult, + ModelResult, + SelectIncludeOmit, + SelectSubset, + Subset, + UpdateArgs, + UpdateManyAndReturnArgs, + UpdateManyArgs, + UpsertArgs, +} 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 } from './utils/types'; + +export type { FetchFn } from './utils/common'; + +/** + * The default query endpoint. + */ +export const DEFAULT_QUERY_ENDPOINT = '/api/model'; + +/** + * Key for setting and getting the global query context. + */ +export const SvelteQueryContextKey = 'zenstack-svelte-query-context'; + +/** + * Set context for query settings. + * + * @deprecated use {@link setQuerySettingsContext} instead. + */ +export function setHooksContext(context: APIContext) { + setContext(SvelteQueryContextKey, context); +} + +/** + * Set context for query settings. + */ +export function setQuerySettingsContext(context: APIContext) { + setContext(SvelteQueryContextKey, context); +} + +function getQuerySettings() { + const { endpoint, ...rest } = getContext(SvelteQueryContextKey) ?? {}; + return { endpoint: endpoint ?? DEFAULT_QUERY_ENDPOINT, ...rest }; +} + +export type ModelQueryOptions = Omit, 'queryKey'> & ExtraQueryOptions; + +export type ModelQueryResult = Readable> & { queryKey: QueryKey }>; + +export type ModelInfiniteQueryOptions = Omit< + CreateInfiniteQueryOptions>, + 'queryKey' | 'initialPageParam' +>; + +export type ModelInfiniteQueryResult = Readable< + UnwrapStore> & { + queryKey: QueryKey; + } +>; + +export type ModelMutationOptions = Omit, 'mutationFn'> & + ExtraMutationOptions; + +export type ModelMutationResult = CreateMutationResult; + +export type ModelMutationModelResult< + Schema extends SchemaDef, + Model extends GetModels, + TArgs extends SelectIncludeOmit, + Array extends boolean = false, +> = Readable< + Omit, TArgs>>, 'mutateAsync'> & { + mutateAsync( + args: T, + options?: ModelMutationOptions, T>, + ): Promise[] : ModelResult>; + } +>; + +export type ClientHooks = { + [Model in GetModels as `${Uncapitalize}`]: ModelQueryHooks; +}; + +// Note that we can potentially use TypeScript's mapped type to directly map from ORM contract, but that seems +// to significantly slow down tsc performance ... +export type ModelQueryHooks> = TrimDelegateModelOperations< + Schema, + Model, + { + useFindUnique>( + args: SelectSubset>, + options?: ModelQueryOptions | null>, + ): ModelQueryResult | null>; + + useFindFirst>( + args?: SelectSubset>, + options?: ModelQueryOptions | null>, + ): ModelQueryResult | null>; + + useFindMany>( + args?: SelectSubset>, + options?: ModelQueryOptions[]>, + ): ModelQueryResult[]>; + + useInfiniteFindMany>( + args?: SelectSubset>, + options?: ModelInfiniteQueryOptions[]>, + ): ModelInfiniteQueryResult[]>>; + + useCreate>( + options?: ModelMutationOptions, T>, + ): ModelMutationModelResult; + + useCreateMany>( + options?: ModelMutationOptions, + ): ModelMutationResult; + + useCreateManyAndReturn>( + options?: ModelMutationOptions[], T>, + ): ModelMutationModelResult; + + useUpdate>( + options?: ModelMutationOptions, T>, + ): ModelMutationModelResult; + + useUpdateMany>( + options?: ModelMutationOptions, + ): ModelMutationResult; + + useUpdateManyAndReturn>( + options?: ModelMutationOptions[], T>, + ): ModelMutationModelResult; + + useUpsert>( + options?: ModelMutationOptions, T>, + ): ModelMutationModelResult; + + useDelete>( + options?: ModelMutationOptions, T>, + ): ModelMutationModelResult; + + useDeleteMany>( + options?: ModelMutationOptions, + ): ModelMutationResult; + + useCount>( + args?: Subset>, + options?: ModelQueryOptions>, + ): ModelQueryResult>; + + useAggregate>( + args: Subset>, + options?: ModelQueryOptions>, + ): ModelQueryResult>; + + useGroupBy>( + args: Subset>, + options?: ModelQueryOptions>, + ): ModelQueryResult>; + } +>; + +/** + * Gets data query hooks for all models in the schema. + */ +export function useClientQueries(schema: Schema): ClientHooks { + return Object.keys(schema.models).reduce((acc, model) => { + (acc as any)[lowerCaseFirst(model)] = useModelQueries(schema, model as GetModels); + return acc; + }, {} as ClientHooks); +} + +/** + * Gets data query hooks for a specific model in the schema. + */ +export function useModelQueries>( + schema: Schema, + model: Model, +): 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`); + } + + const modelName = modelDef.name; + + return { + useFindUnique: (args: any, options?: any) => { + return useInternalQuery(schema, modelName, 'findUnique', args, options); + }, + + useFindFirst: (args: any, options?: any) => { + return useInternalQuery(schema, modelName, 'findFirst', args, options); + }, + + useFindMany: (args: any, options?: any) => { + return useInternalQuery(schema, modelName, 'findMany', args, options); + }, + + useInfiniteFindMany: (args: any, options?: any) => { + return useInternalInfiniteQuery(schema, modelName, 'findMany', args, options); + }, + + useCreate: (options?: any) => { + return useInternalMutation(schema, modelName, 'POST', 'create', options); + }, + + useCreateMany: (options?: any) => { + return useInternalMutation(schema, modelName, 'POST', 'createMany', options); + }, + + useCreateManyAndReturn: (options?: any) => { + return useInternalMutation(schema, modelName, 'POST', 'createManyAndReturn', options); + }, + + useUpdate: (options?: any) => { + return useInternalMutation(schema, modelName, 'PUT', 'update', options); + }, + + useUpdateMany: (options?: any) => { + return useInternalMutation(schema, modelName, 'PUT', 'updateMany', options); + }, + + useUpdateManyAndReturn: (options?: any) => { + return useInternalMutation(schema, modelName, 'PUT', 'updateManyAndReturn', options); + }, + + useUpsert: (options?: any) => { + return useInternalMutation(schema, modelName, 'POST', 'upsert', options); + }, + + useDelete: (options?: any) => { + return useInternalMutation(schema, modelName, 'DELETE', 'delete', options); + }, + + useDeleteMany: (options?: any) => { + return useInternalMutation(schema, modelName, 'DELETE', 'deleteMany', options); + }, + + useCount: (args: any, options?: any) => { + return useInternalQuery(schema, modelName, 'count', args, options); + }, + + useAggregate: (args: any, options?: any) => { + return useInternalQuery(schema, modelName, 'aggregate', args, options); + }, + + useGroupBy: (args: any, options?: any) => { + return useInternalQuery(schema, modelName, 'groupBy', args, options); + }, + } as ModelQueryHooks; +} + +export function useInternalQuery( + _schema: SchemaDef, + model: string, + operation: string, + args?: StoreOrVal, + options?: StoreOrVal, '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 = { + queryKey, + queryFn, + ...options, + }; + } + + const result = createQuery(mergedOpt); + return derived(result, (r) => ({ + queryKey, + ...r, + })); +} + +export function useInternalInfiniteQuery( + _schema: SchemaDef, + model: string, + operation: string, + args: StoreOrVal, + options: + | StoreOrVal< + Omit< + CreateInfiniteQueryOptions>, + 'queryKey' | 'initialPageParam' + > + > + | undefined, +) { + 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 = { + queryKey, + queryFn, + initialPageParam: argsValue, + ...options, + }; + } + + const result = createInfiniteQuery>(mergedOpt); + return derived(result, (r) => ({ + queryKey, + ...r, + })); +} + +/** + * Creates a svelte-query mutation + * + * @private + * + * @param model The name of the model under mutation. + * @param method The HTTP method. + * @param operation The mutation operation (e.g. `create`). + * @param options The svelte-query options. + * @param checkReadBack Whether to check for read back errors and return undefined if found. + */ +export function useInternalMutation( + schema: SchemaDef, + model: string, + method: 'POST' | 'PUT' | 'DELETE', + operation: string, + options?: StoreOrVal, 'mutationFn'> & ExtraMutationOptions>, +) { + const { endpoint, fetch, logging } = getQuerySettings(); + const queryClient = useQueryClient(); + const optionsValue = unwrapStore(options); + const mutationFn = (data: any) => { + const reqUrl = + method === 'DELETE' ? makeUrl(endpoint, model, operation, data) : makeUrl(endpoint, model, operation); + const fetchInit: RequestInit = { + method, + ...(method !== 'DELETE' && { + headers: { + 'content-type': 'application/json', + }, + body: marshal(data), + }), + }; + return fetcher(reqUrl, fetchInit, fetch) as Promise; + }; + + let mergedOpt: StoreOrVal>; + + if (isStore(options)) { + mergedOpt = derived([options], ([$opt]) => ({ + ...$opt, + mutationFn, + })); + } else { + mergedOpt = { + ...options, + mutationFn, + }; + } + + const invalidateQueries = optionsValue?.invalidateQueries !== false; + const optimisticUpdate = !!optionsValue?.optimisticUpdate; + + if (operation) { + if (invalidateQueries) { + setupInvalidation( + model, + operation, + schema, + unwrapStore(mergedOpt), + (predicate) => queryClient.invalidateQueries({ predicate }), + 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, + ); + } + } + + return createMutation(mergedOpt); +} + +function isStore(opt: unknown): opt is Readable { + return typeof (opt as any)?.subscribe === 'function'; +} + +function unwrapStore(storeOrValue: StoreOrVal): T { + return isStore(storeOrValue) ? get(storeOrValue) : storeOrValue; +} + +type UnwrapStore = T extends Readable ? U : T; diff --git a/packages/clients/tanstack-query/src/utils/common.ts b/packages/clients/tanstack-query/src/utils/common.ts index b26720ee..28710d25 100644 --- a/packages/clients/tanstack-query/src/utils/common.ts +++ b/packages/clients/tanstack-query/src/utils/common.ts @@ -125,21 +125,12 @@ export type APIContext = { logging?: boolean; }; -export async function fetcher( - url: string, - options?: RequestInit, - customFetch?: FetchFn, - checkReadBack?: C, -): Promise { +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 ( - checkReadBack !== false && - errData.error?.rejectedByPolicy && - errData.error?.rejectReason === 'cannot-read-back' - ) { + 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; } @@ -208,8 +199,8 @@ export function unmarshal(value: string) { } } -export function makeUrl(url: string, model: string, operation: string, args?: unknown) { - const baseUrl = `${url}/${lowerCaseFirst(model)}/${operation}`; +export function makeUrl(endpoint: string, model: string, operation: string, args?: unknown) { + const baseUrl = `${endpoint}/${lowerCaseFirst(model)}/${operation}`; if (!args) { return baseUrl; } diff --git a/packages/clients/tanstack-query/src/utils/types.ts b/packages/clients/tanstack-query/src/utils/types.ts index 1ebd2d25..ac7829e9 100644 --- a/packages/clients/tanstack-query/src/utils/types.ts +++ b/packages/clients/tanstack-query/src/utils/types.ts @@ -1,3 +1,6 @@ +import type { OperationsIneligibleForDelegateModels } from '@zenstackhq/orm'; +import type { GetModels, IsDelegateModel, SchemaDef } from '@zenstackhq/schema'; + export type MaybePromise = T | Promise | PromiseLike; export const ORMWriteActions = [ @@ -17,3 +20,13 @@ export const ORMWriteActions = [ ] 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; diff --git a/packages/clients/tanstack-query/src/vue.ts b/packages/clients/tanstack-query/src/vue.ts new file mode 100644 index 00000000..8239ce97 --- /dev/null +++ b/packages/clients/tanstack-query/src/vue.ts @@ -0,0 +1,429 @@ +import { + useInfiniteQuery, + useMutation, + useQuery, + useQueryClient, + type DefaultError, + type InfiniteData, + type QueryKey, + type UseInfiniteQueryOptions, + type UseInfiniteQueryReturnType, + type UseMutationOptions, + type UseMutationReturnType, + type UseQueryOptions, + type UseQueryReturnType, +} from '@tanstack/vue-query'; +import { lowerCaseFirst } from '@zenstackhq/common-helpers'; +import type { + AggregateArgs, + AggregateResult, + BatchResult, + CountArgs, + CountResult, + CreateArgs, + CreateManyAndReturnArgs, + CreateManyArgs, + DeleteArgs, + DeleteManyArgs, + FindArgs, + FindUniqueArgs, + GroupByArgs, + GroupByResult, + ModelResult, + SelectIncludeOmit, + SelectSubset, + Subset, + UpdateArgs, + UpdateManyAndReturnArgs, + UpdateManyArgs, + 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 } from './utils/types'; + +export type { FetchFn } from './utils/common'; +export const VueQueryContextKey = 'zenstack-vue-query-context'; + +/** + * Provide context for query settings. + * + * @deprecated Use {@link provideQuerySettingsContext} instead. + */ +export function provideHooksContext(context: APIContext) { + provide(VueQueryContextKey, context); +} + +/** + * Provide context for query settings. + */ +export function provideQuerySettingsContext(context: APIContext) { + provide(VueQueryContextKey, context); +} + +function getQuerySettings() { + const { endpoint, ...rest } = inject(VueQueryContextKey, { + endpoint: DEFAULT_QUERY_ENDPOINT, + fetch: undefined, + logging: false, + }); + return { endpoint: endpoint ?? DEFAULT_QUERY_ENDPOINT, ...rest }; +} + +export type ModelQueryOptions = MaybeRefOrGetter< + Omit>, 'queryKey'> & ExtraQueryOptions +>; + +export type ModelQueryResult = UseQueryReturnType & { queryKey: QueryKey }; + +export type ModelInfiniteQueryOptions = MaybeRefOrGetter< + Omit>>, 'queryKey' | 'initialPageParam'> +>; + +export type ModelInfiniteQueryResult = UseInfiniteQueryReturnType & { queryKey: QueryKey }; + +export type ModelMutationOptions = MaybeRefOrGetter< + Omit>, 'mutationFn'> & ExtraMutationOptions +>; + +export type ModelMutationResult = UseMutationReturnType; + +export type ModelMutationModelResult< + Schema extends SchemaDef, + Model extends GetModels, + TArgs extends SelectIncludeOmit, + Array extends boolean = false, +> = Omit, TArgs>, 'mutateAsync'> & { + mutateAsync( + args: T, + options?: ModelMutationOptions, T>, + ): Promise[] : ModelResult>; +}; + +export type ClientHooks = { + [Model in GetModels as `${Uncapitalize}`]: ModelQueryHooks; +}; + +// Note that we can potentially use TypeScript's mapped type to directly map from ORM contract, but that seems +// to significantly slow down tsc performance ... +export type ModelQueryHooks> = TrimDelegateModelOperations< + Schema, + Model, + { + useFindUnique>( + args: SelectSubset>, + options?: ModelQueryOptions | null>, + ): ModelQueryResult | null>; + + useFindFirst>( + args?: SelectSubset>, + options?: ModelQueryOptions | null>, + ): ModelQueryResult | null>; + + useFindMany>( + args?: SelectSubset>, + options?: ModelQueryOptions[]>, + ): ModelQueryResult[]>; + + useInfiniteFindMany>( + args?: SelectSubset>, + options?: ModelInfiniteQueryOptions[]>, + ): ModelInfiniteQueryResult[]>>; + + useCreate>( + options?: ModelMutationOptions, T>, + ): ModelMutationModelResult; + + useCreateMany>( + options?: ModelMutationOptions, + ): ModelMutationResult; + + useCreateManyAndReturn>( + options?: ModelMutationOptions[], T>, + ): ModelMutationModelResult; + + useUpdate>( + options?: ModelMutationOptions, T>, + ): ModelMutationModelResult; + + useUpdateMany>( + options?: ModelMutationOptions, + ): ModelMutationResult; + + useUpdateManyAndReturn>( + options?: ModelMutationOptions[], T>, + ): ModelMutationModelResult; + + useUpsert>( + options?: ModelMutationOptions, T>, + ): ModelMutationModelResult; + + useDelete>( + options?: ModelMutationOptions, T>, + ): ModelMutationModelResult; + + useDeleteMany>( + options?: ModelMutationOptions, + ): ModelMutationResult; + + useCount>( + args?: Subset>, + options?: ModelQueryOptions>, + ): ModelQueryResult>; + + useAggregate>( + args: Subset>, + options?: ModelQueryOptions>, + ): ModelQueryResult>; + + useGroupBy>( + args: Subset>, + options?: ModelQueryOptions>, + ): ModelQueryResult>; + } +>; + +/** + * Gets data query hooks for all models in the schema. + */ +export function useClientQueries(schema: Schema): ClientHooks { + return Object.keys(schema.models).reduce((acc, model) => { + (acc as any)[lowerCaseFirst(model)] = useModelQueries(schema, model as GetModels); + return acc; + }, {} as ClientHooks); +} + +/** + * Gets data query hooks for a specific model in the schema. + */ +export function useModelQueries>( + schema: Schema, + model: Model, +): 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`); + } + + const modelName = modelDef.name; + + return { + useFindUnique: (args: any, options?: any) => { + return useInternalQuery(schema, modelName, 'findUnique', args, options); + }, + + useFindFirst: (args: any, options?: any) => { + return useInternalQuery(schema, modelName, 'findFirst', args, options); + }, + + useFindMany: (args: any, options?: any) => { + return useInternalQuery(schema, modelName, 'findMany', args, options); + }, + + useInfiniteFindMany: (args: any, options?: any) => { + return useInternalInfiniteQuery(schema, modelName, 'findMany', args, options); + }, + + useCreate: (options?: any) => { + return useInternalMutation(schema, modelName, 'POST', 'create', options); + }, + + useCreateMany: (options?: any) => { + return useInternalMutation(schema, modelName, 'POST', 'createMany', options); + }, + + useCreateManyAndReturn: (options?: any) => { + return useInternalMutation(schema, modelName, 'POST', 'createManyAndReturn', options); + }, + + useUpdate: (options?: any) => { + return useInternalMutation(schema, modelName, 'PUT', 'update', options); + }, + + useUpdateMany: (options?: any) => { + return useInternalMutation(schema, modelName, 'PUT', 'updateMany', options); + }, + + useUpdateManyAndReturn: (options?: any) => { + return useInternalMutation(schema, modelName, 'PUT', 'updateManyAndReturn', options); + }, + + useUpsert: (options?: any) => { + return useInternalMutation(schema, modelName, 'POST', 'upsert', options); + }, + + useDelete: (options?: any) => { + return useInternalMutation(schema, modelName, 'DELETE', 'delete', options); + }, + + useDeleteMany: (options?: any) => { + return useInternalMutation(schema, modelName, 'DELETE', 'deleteMany', options); + }, + + useCount: (args: any, options?: any) => { + return useInternalQuery(schema, modelName, 'count', args, options); + }, + + useAggregate: (args: any, options?: any) => { + return useInternalQuery(schema, modelName, 'aggregate', args, options); + }, + + useGroupBy: (args: any, options?: any) => { + return useInternalQuery(schema, modelName, 'groupBy', args, options); + }, + } as ModelQueryHooks; +} + +export function useInternalQuery( + _schema: SchemaDef, + model: string, + operation: string, + args?: MaybeRefOrGetter, + options?: MaybeRefOrGetter< + Omit>, 'queryKey'> & ExtraQueryOptions + >, +) { + const argsValue = toValue(args); + const { optimisticUpdate, ...restOptions } = toValue(options) ?? {}; + const queryKey = 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, + }; + return { queryKey, ...useQuery(finalOptions) }; +} + +export function useInternalInfiniteQuery( + _schema: SchemaDef, + model: string, + operation: string, + args: MaybeRefOrGetter, + options: + | MaybeRefOrGetter< + Omit< + UnwrapRef>>, + 'queryKey' | 'initialPageParam' + > + > + | 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, + }; + return { + queryKey, + ...useInfiniteQuery(finalOptions), + }; +} + +/** + * Creates a vue-query mutation + * + * @private + * + * @param model The name of the model under mutation. + * @param method The HTTP method. + * @param operation The mutation operation (e.g. `create`). + * @param options The vue-query options. + * @param checkReadBack Whether to check for read back errors and return undefined if found. + */ +export function useInternalMutation( + schema: SchemaDef, + model: string, + method: 'POST' | 'PUT' | 'DELETE', + operation: string, + options?: MaybeRefOrGetter< + Omit>, 'mutationFn'> & ExtraMutationOptions + >, +) { + const { endpoint, fetch, logging } = getQuerySettings(); + const queryClient = useQueryClient(); + const mutationFn = (data: any) => { + const reqUrl = + method === 'DELETE' ? makeUrl(endpoint, model, operation, data) : makeUrl(endpoint, model, operation); + const fetchInit: RequestInit = { + method, + ...(method !== 'DELETE' && { + headers: { + 'content-type': 'application/json', + }, + body: marshal(data), + }), + }; + 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( + model, + operation, + schema, + finalOptions, + (predicate) => queryClient.invalidateQueries({ predicate }), + 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, + ); + } + } + + return useMutation(finalOptions); +} diff --git a/packages/clients/tanstack-query/test/react-typing-test.ts b/packages/clients/tanstack-query/test/react-typing-test.ts new file mode 100644 index 00000000..02d7a2e3 --- /dev/null +++ b/packages/clients/tanstack-query/test/react-typing-test.ts @@ -0,0 +1,109 @@ +import { useClientQueries } from '../src/react'; +import { schema } from './schemas/basic/schema-lite'; + +const client = useClientQueries(schema); + +// @ts-expect-error missing args +client.user.useFindUnique(); + +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(client.user.useFindUnique({ select: { email: true } }).data.name); + +check(client.user.useFindUnique({ where: { id: '1' }, include: { posts: true } }).data?.posts[0]?.title); + +check(client.user.useFindFirst().data?.email); + +check(client.user.useFindMany().data?.[0]?.email); + +check(client.user.useInfiniteFindMany().data?.pages[0]?.[0]?.email); +check( + client.user.useInfiniteFindMany( + {}, + { + getNextPageParam: () => ({ id: '2' }), + }, + ).data?.pages[1]?.[0]?.email, +); + +check(client.user.useSuspenseFindMany().data[0]?.email); +check(client.user.useSuspenseInfiniteFindMany().data.pages[0]?.[0]?.email); +check(client.user.useCount().data?.toFixed(2)); +check(client.user.useCount({ select: { email: true } }).data?.email.toFixed(2)); + +check(client.user.useAggregate({ _max: { email: true } }).data?._max.email); + +check(client.user.useGroupBy({ by: ['email'], _max: { name: true } }).data?.[0]?._max.name); + +// @ts-expect-error missing args +client.user.useCreate().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' } }); + +client.user + .useCreate() + .mutateAsync({ data: { email: 'test@example.com' }, include: { posts: true } }) + .then((d) => check(d.posts[0]?.title)); + +client.user + .useCreateMany() + .mutateAsync({ + data: [{ email: 'test@example.com' }, { email: 'test2@example.com' }], + skipDuplicates: true, + }) + .then((d) => d.count); + +client.user + .useCreateManyAndReturn() + .mutateAsync({ + data: [{ email: 'test@example.com' }], + }) + .then((d) => check(d[0]?.name)); + +client.user + .useCreateManyAndReturn() + .mutateAsync({ + data: [{ email: 'test@example.com' }], + select: { email: true }, + }) + // @ts-expect-error unselected field + .then((d) => check(d[0].name)); + +client.user.useUpdate().mutate( + { data: { email: 'updated@example.com' }, where: { id: '1' } }, + { + onSuccess: (d) => { + check(d.email); + }, + }, +); + +client.user.useUpdateMany().mutate({ data: { email: 'updated@example.com' } }); + +client.user + .useUpdateManyAndReturn() + .mutateAsync({ data: { email: 'updated@example.com' } }) + .then((d) => check(d[0]?.email)); + +client.user + .useUpsert() + .mutate({ where: { id: '1' }, create: { email: 'new@example.com' }, update: { email: 'updated@example.com' } }); + +client.user.useDelete().mutate({ where: { id: '1' }, include: { posts: true } }); + +client.user.useDeleteMany().mutate({ where: { email: 'test@example.com' } }); + +function check(_value: unknown) { + // noop +} + +// @ts-expect-error delegate model +client.foo.useCreate(); + +client.foo.useUpdate(); +client.bar.useCreate(); diff --git a/packages/clients/tanstack-query/test/schemas/basic/input.ts b/packages/clients/tanstack-query/test/schemas/basic/input.ts index e7b6da66..755f3b2b 100644 --- a/packages/clients/tanstack-query/test/schemas/basic/input.ts +++ b/packages/clients/tanstack-query/test/schemas/basic/input.ts @@ -68,3 +68,43 @@ export type CategorySelect = $SelectInput<$Schema, "Category">; export type CategoryInclude = $IncludeInput<$Schema, "Category">; export type CategoryOmit = $OmitInput<$Schema, "Category">; export type CategoryGetPayload> = $SimplifiedModelResult<$Schema, "Category", Args>; +export type FooFindManyArgs = $FindManyArgs<$Schema, "Foo">; +export type FooFindUniqueArgs = $FindUniqueArgs<$Schema, "Foo">; +export type FooFindFirstArgs = $FindFirstArgs<$Schema, "Foo">; +export type FooCreateArgs = $CreateArgs<$Schema, "Foo">; +export type FooCreateManyArgs = $CreateManyArgs<$Schema, "Foo">; +export type FooCreateManyAndReturnArgs = $CreateManyAndReturnArgs<$Schema, "Foo">; +export type FooUpdateArgs = $UpdateArgs<$Schema, "Foo">; +export type FooUpdateManyArgs = $UpdateManyArgs<$Schema, "Foo">; +export type FooUpdateManyAndReturnArgs = $UpdateManyAndReturnArgs<$Schema, "Foo">; +export type FooUpsertArgs = $UpsertArgs<$Schema, "Foo">; +export type FooDeleteArgs = $DeleteArgs<$Schema, "Foo">; +export type FooDeleteManyArgs = $DeleteManyArgs<$Schema, "Foo">; +export type FooCountArgs = $CountArgs<$Schema, "Foo">; +export type FooAggregateArgs = $AggregateArgs<$Schema, "Foo">; +export type FooGroupByArgs = $GroupByArgs<$Schema, "Foo">; +export type FooWhereInput = $WhereInput<$Schema, "Foo">; +export type FooSelect = $SelectInput<$Schema, "Foo">; +export type FooInclude = $IncludeInput<$Schema, "Foo">; +export type FooOmit = $OmitInput<$Schema, "Foo">; +export type FooGetPayload> = $SimplifiedModelResult<$Schema, "Foo", Args>; +export type BarFindManyArgs = $FindManyArgs<$Schema, "Bar">; +export type BarFindUniqueArgs = $FindUniqueArgs<$Schema, "Bar">; +export type BarFindFirstArgs = $FindFirstArgs<$Schema, "Bar">; +export type BarCreateArgs = $CreateArgs<$Schema, "Bar">; +export type BarCreateManyArgs = $CreateManyArgs<$Schema, "Bar">; +export type BarCreateManyAndReturnArgs = $CreateManyAndReturnArgs<$Schema, "Bar">; +export type BarUpdateArgs = $UpdateArgs<$Schema, "Bar">; +export type BarUpdateManyArgs = $UpdateManyArgs<$Schema, "Bar">; +export type BarUpdateManyAndReturnArgs = $UpdateManyAndReturnArgs<$Schema, "Bar">; +export type BarUpsertArgs = $UpsertArgs<$Schema, "Bar">; +export type BarDeleteArgs = $DeleteArgs<$Schema, "Bar">; +export type BarDeleteManyArgs = $DeleteManyArgs<$Schema, "Bar">; +export type BarCountArgs = $CountArgs<$Schema, "Bar">; +export type BarAggregateArgs = $AggregateArgs<$Schema, "Bar">; +export type BarGroupByArgs = $GroupByArgs<$Schema, "Bar">; +export type BarWhereInput = $WhereInput<$Schema, "Bar">; +export type BarSelect = $SelectInput<$Schema, "Bar">; +export type BarInclude = $IncludeInput<$Schema, "Bar">; +export type BarOmit = $OmitInput<$Schema, "Bar">; +export type BarGetPayload> = $SimplifiedModelResult<$Schema, "Bar", Args>; diff --git a/packages/clients/tanstack-query/test/schemas/basic/models.ts b/packages/clients/tanstack-query/test/schemas/basic/models.ts index da3e5d00..a4bcedbb 100644 --- a/packages/clients/tanstack-query/test/schemas/basic/models.ts +++ b/packages/clients/tanstack-query/test/schemas/basic/models.ts @@ -10,3 +10,5 @@ import { type ModelResult as $ModelResult } from "@zenstackhq/orm"; export type User = $ModelResult<$Schema, "User">; export type Post = $ModelResult<$Schema, "Post">; export type Category = $ModelResult<$Schema, "Category">; +export type Foo = $ModelResult<$Schema, "Foo">; +export type Bar = $ModelResult<$Schema, "Bar">; diff --git a/packages/clients/tanstack-query/test/schemas/basic/schema-lite.ts b/packages/clients/tanstack-query/test/schemas/basic/schema-lite.ts index 347ccbe1..ea44666f 100644 --- a/packages/clients/tanstack-query/test/schemas/basic/schema-lite.ts +++ b/packages/clients/tanstack-query/test/schemas/basic/schema-lite.ts @@ -116,6 +116,54 @@ export const schema = { id: { type: "String" }, name: { type: "String" } } + }, + Foo: { + name: "Foo", + fields: { + id: { + name: "id", + type: "String", + id: true, + default: ExpressionUtils.call("cuid") + }, + type: { + name: "type", + type: "String", + isDiscriminator: true + } + }, + idFields: ["id"], + uniqueFields: { + id: { type: "String" } + }, + isDelegate: true, + subModels: ["Bar"] + }, + Bar: { + name: "Bar", + baseModel: "Foo", + fields: { + id: { + name: "id", + type: "String", + id: true, + default: ExpressionUtils.call("cuid") + }, + type: { + name: "type", + type: "String", + originModel: "Foo", + isDiscriminator: true + }, + title: { + name: "title", + type: "String" + } + }, + idFields: ["id"], + uniqueFields: { + id: { type: "String" } + } } }, authType: "User", diff --git a/packages/clients/tanstack-query/test/schemas/basic/schema.zmodel b/packages/clients/tanstack-query/test/schemas/basic/schema.zmodel index 039051df..d274e95c 100644 --- a/packages/clients/tanstack-query/test/schemas/basic/schema.zmodel +++ b/packages/clients/tanstack-query/test/schemas/basic/schema.zmodel @@ -22,4 +22,14 @@ model Category { id String @id @default(cuid()) name String @unique posts Post[] -} \ No newline at end of file +} + +model Foo { + id String @id @default(cuid()) + type String + @@delegate(type) +} + +model Bar extends Foo { + title String +} diff --git a/packages/clients/tanstack-query/test/svelte-typing-test.ts b/packages/clients/tanstack-query/test/svelte-typing-test.ts new file mode 100644 index 00000000..62735112 --- /dev/null +++ b/packages/clients/tanstack-query/test/svelte-typing-test.ts @@ -0,0 +1,106 @@ +import { get } from 'svelte/store'; +import { useClientQueries } from '../src/svelte'; +import { schema } from './schemas/basic/schema-lite'; + +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 }))); + +// @ts-expect-error unselected field +check(get(client.user.useFindUnique({ select: { email: true } })).data.name); + +check(get(client.user.useFindUnique({ where: { id: '1' }, include: { posts: true } })).data?.posts[0]?.title); + +check(get(client.user.useFindFirst()).data?.email); + +check(get(client.user.useFindMany()).data?.[0]?.email); +check(get(client.user.useInfiniteFindMany()).data?.pages[0]?.[0]?.email); +check( + get( + client.user.useInfiniteFindMany( + {}, + { + getNextPageParam: () => ({ id: '2' }), + }, + ), + ).data?.pages[1]?.[0]?.email, +); + +check(get(client.user.useCount()).data?.toFixed(2)); +check(get(client.user.useCount({ select: { email: true } })).data?.email.toFixed(2)); + +check(get(client.user.useAggregate({ _max: { email: true } })).data?._max.email); + +check(get(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({ + data: { email: 'test@example.com' }, +}); + +get(client.user.useCreate()) + .mutateAsync({ data: { email: 'test@example.com' }, include: { posts: true } }) + .then((d) => check(d.posts[0]?.title)); + +get(client.user.useCreateMany()) + .mutateAsync({ + data: [{ email: 'test@example.com' }, { email: 'test2@example.com' }], + skipDuplicates: true, + }) + .then((d) => d.count); + +get(client.user.useCreateManyAndReturn()) + .mutateAsync({ + data: [{ email: 'test@example.com' }], + }) + .then((d) => check(d[0]?.name)); + +get(client.user.useCreateManyAndReturn()) + .mutateAsync({ + data: [{ email: 'test@example.com' }], + select: { email: true }, + }) + // @ts-expect-error unselected field + .then((d) => check(d[0].name)); + +get(client.user.useUpdate()).mutate( + { data: { email: 'updated@example.com' }, where: { id: '1' } }, + { + onSuccess: (d) => { + check(d.email); + }, + }, +); + +get(client.user.useUpdateMany()).mutate({ data: { email: 'updated@example.com' } }); + +get(client.user.useUpdateManyAndReturn()) + .mutateAsync({ data: { email: 'updated@example.com' } }) + .then((d) => check(d[0]?.email)); + +get(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 } }); + +get(client.user.useDeleteMany()).mutate({ where: { email: 'test@example.com' } }); + +function check(_value: unknown) { + // noop +} + +// @ts-expect-error delegate model +client.foo.useCreate(); + +client.foo.useUpdate(); +client.bar.useCreate(); diff --git a/packages/clients/tanstack-query/test/vue-typing-test.ts b/packages/clients/tanstack-query/test/vue-typing-test.ts new file mode 100644 index 00000000..07b980c5 --- /dev/null +++ b/packages/clients/tanstack-query/test/vue-typing-test.ts @@ -0,0 +1,107 @@ +import { useClientQueries } from '../src/vue'; +import { schema } from './schemas/basic/schema-lite'; + +const client = useClientQueries(schema); + +// @ts-expect-error missing args +client.user.useFindUnique(); + +check(client.user.useFindUnique({ where: { id: '1' } }).data.value?.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(client.user.useFindUnique({ select: { email: true } }).data.name); + +check(client.user.useFindUnique({ where: { id: '1' }, include: { posts: true } }).data.value?.posts[0]?.title); + +check(client.user.useFindFirst().data.value?.email); + +check(client.user.useFindMany().data.value?.[0]?.email); + +check(client.user.useInfiniteFindMany().data.value?.pages[0]?.[0]?.email); +check( + client.user.useInfiniteFindMany( + {}, + { + getNextPageParam: () => ({ id: '2' }), + }, + ).data.value?.pages[1]?.[0]?.email, +); + +check(client.user.useCount().data.value?.toFixed(2)); +check(client.user.useCount({ select: { email: true } }).data.value?.email.toFixed(2)); + +check(client.user.useAggregate({ _max: { email: true } }).data.value?._max.email); + +check(client.user.useGroupBy({ by: ['email'], _max: { name: true } }).data.value?.[0]?._max.name); + +// @ts-expect-error missing args +client.user.useCreate().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' } }); + +client.user + .useCreate() + .mutateAsync({ data: { email: 'test@example.com' }, include: { posts: true } }) + .then((d) => check(d.posts[0]?.title)); + +client.user + .useCreateMany() + .mutateAsync({ + data: [{ email: 'test@example.com' }, { email: 'test2@example.com' }], + skipDuplicates: true, + }) + .then((d) => d.count); + +client.user + .useCreateManyAndReturn() + .mutateAsync({ + data: [{ email: 'test@example.com' }], + }) + .then((d) => check(d[0]?.name)); + +client.user + .useCreateManyAndReturn() + .mutateAsync({ + data: [{ email: 'test@example.com' }], + select: { email: true }, + }) + // @ts-expect-error unselected field + .then((d) => check(d[0].name)); + +client.user.useUpdate().mutate( + { data: { email: 'updated@example.com' }, where: { id: '1' } }, + { + onSuccess: (d) => { + check(d.email); + }, + }, +); + +client.user.useUpdateMany().mutate({ data: { email: 'updated@example.com' } }); + +client.user + .useUpdateManyAndReturn() + .mutateAsync({ data: { email: 'updated@example.com' } }) + .then((d) => check(d[0]?.email)); + +client.user + .useUpsert() + .mutate({ where: { id: '1' }, create: { email: 'new@example.com' }, update: { email: 'updated@example.com' } }); + +client.user.useDelete().mutate({ where: { id: '1' }, include: { posts: true } }); + +client.user.useDeleteMany().mutate({ where: { email: 'test@example.com' } }); + +function check(_value: unknown) { + // noop +} + +// @ts-expect-error delegate model +client.foo.useCreate(); + +client.foo.useUpdate(); +client.bar.useCreate(); diff --git a/packages/clients/tanstack-query/tsup.config.ts b/packages/clients/tanstack-query/tsup.config.ts index 41c31736..1bfafb93 100644 --- a/packages/clients/tanstack-query/tsup.config.ts +++ b/packages/clients/tanstack-query/tsup.config.ts @@ -3,6 +3,8 @@ import { defineConfig } from 'tsup'; export default defineConfig({ entry: { react: 'src/react.ts', + vue: 'src/vue.ts', + svelte: 'src/svelte.ts', }, outDir: 'dist', splitting: false, diff --git a/packages/common-helpers/package.json b/packages/common-helpers/package.json index 11c25565..7a76561b 100644 --- a/packages/common-helpers/package.json +++ b/packages/common-helpers/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/common-helpers", - "version": "3.0.0-beta.17", + "version": "3.0.0-beta.18", "description": "ZenStack Common Helpers", "type": "module", "scripts": { diff --git a/packages/config/eslint-config/package.json b/packages/config/eslint-config/package.json index c441e7ca..9e8c6eb3 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-beta.17", + "version": "3.0.0-beta.18", "type": "module", "private": true, "license": "MIT" diff --git a/packages/config/typescript-config/package.json b/packages/config/typescript-config/package.json index 6b66b6e6..db7ca194 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-beta.17", + "version": "3.0.0-beta.18", "private": true, "license": "MIT" } diff --git a/packages/config/vitest-config/package.json b/packages/config/vitest-config/package.json index 1b9ce13f..40631187 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-beta.17", + "version": "3.0.0-beta.18", "private": true, "license": "MIT", "exports": { diff --git a/packages/create-zenstack/package.json b/packages/create-zenstack/package.json index d85f0e88..a53ba53a 100644 --- a/packages/create-zenstack/package.json +++ b/packages/create-zenstack/package.json @@ -1,6 +1,6 @@ { "name": "create-zenstack", - "version": "3.0.0-beta.17", + "version": "3.0.0-beta.18", "description": "Create a new ZenStack project", "type": "module", "scripts": { diff --git a/packages/dialects/sql.js/package.json b/packages/dialects/sql.js/package.json index 05d6d05d..14566cbc 100644 --- a/packages/dialects/sql.js/package.json +++ b/packages/dialects/sql.js/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/kysely-sql-js", - "version": "3.0.0-beta.17", + "version": "3.0.0-beta.18", "description": "Kysely dialect for sql.js", "type": "module", "scripts": { diff --git a/packages/language/package.json b/packages/language/package.json index 20f5296d..d8fa2c21 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-beta.17", + "version": "3.0.0-beta.18", "license": "MIT", "author": "ZenStack Team", "files": [ diff --git a/packages/orm/package.json b/packages/orm/package.json index 1d2232b5..bca7e4c4 100644 --- a/packages/orm/package.json +++ b/packages/orm/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/orm", - "version": "3.0.0-beta.17", + "version": "3.0.0-beta.18", "description": "ZenStack ORM", "type": "module", "scripts": { diff --git a/packages/orm/src/client/contract.ts b/packages/orm/src/client/contract.ts index 81d7c016..8e30795a 100644 --- a/packages/orm/src/client/contract.ts +++ b/packages/orm/src/client/contract.ts @@ -795,10 +795,12 @@ export type AllModelOperations>>; }; +export type OperationsIneligibleForDelegateModels = 'create' | 'createMany' | 'createManyAndReturn' | 'upsert'; + export type ModelOperations> = Omit< AllModelOperations, // exclude operations not applicable to delegate models - IsDelegateModel extends true ? 'create' | 'createMany' | 'createManyAndReturn' | 'upsert' : never + IsDelegateModel extends true ? OperationsIneligibleForDelegateModels : never >; //#endregion diff --git a/packages/plugins/policy/package.json b/packages/plugins/policy/package.json index 8d94f8ce..8ac4783a 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-beta.17", + "version": "3.0.0-beta.18", "description": "ZenStack Policy Plugin", "type": "module", "scripts": { diff --git a/packages/schema/package.json b/packages/schema/package.json index 82f87dd6..9e3d7953 100644 --- a/packages/schema/package.json +++ b/packages/schema/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/schema", - "version": "3.0.0-beta.17", + "version": "3.0.0-beta.18", "description": "ZenStack Runtime Schema", "type": "module", "scripts": { diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 5a0cb71c..2b23750b 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/sdk", - "version": "3.0.0-beta.17", + "version": "3.0.0-beta.18", "description": "ZenStack SDK", "type": "module", "scripts": { diff --git a/packages/server/package.json b/packages/server/package.json index 66efbb46..5d5dd970 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/server", - "version": "3.0.0-beta.17", + "version": "3.0.0-beta.18", "description": "ZenStack automatic CRUD API handlers and server adapters", "type": "module", "scripts": { diff --git a/packages/testtools/package.json b/packages/testtools/package.json index 2a7b5160..ff3d7b95 100644 --- a/packages/testtools/package.json +++ b/packages/testtools/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/testtools", - "version": "3.0.0-beta.17", + "version": "3.0.0-beta.18", "description": "ZenStack Test Tools", "type": "module", "scripts": { diff --git a/packages/zod/package.json b/packages/zod/package.json index 3cf58ca7..9ef0386e 100644 --- a/packages/zod/package.json +++ b/packages/zod/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/zod", - "version": "3.0.0-beta.17", + "version": "3.0.0-beta.18", "description": "", "type": "module", "main": "index.js", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fb54f39b..cea73983 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,15 +9,69 @@ catalogs: '@tanstack/react-query': specifier: 5.90.6 version: 5.90.6 + '@types/better-sqlite3': + specifier: ^7.6.13 + version: 7.6.13 + '@types/node': + specifier: ^20.17.24 + version: 20.17.24 '@types/react': specifier: 19.2.0 version: 19.2.0 + '@types/react-dom': + specifier: 19.2.0 + version: 19.2.0 + '@types/tmp': + specifier: ^0.2.6 + version: 0.2.6 + better-sqlite3: + specifier: ^12.2.0 + version: 12.2.0 decimal.js: specifier: ^10.4.3 - version: 10.4.3 + version: 10.6.0 + kysely: + specifier: ^0.27.6 + version: 0.27.6 + langium: + specifier: 3.5.0 + version: 3.5.0 + langium-cli: + specifier: 3.5.0 + version: 3.5.0 + next: + specifier: 16.0.1 + version: 16.0.1 + pg: + specifier: ^8.13.1 + version: 8.16.3 + prisma: + specifier: ^6.10.0 + version: 6.14.0 react: specifier: 19.2.0 version: 19.2.0 + react-dom: + specifier: 19.2.0 + version: 19.2.0 + svelte: + specifier: 5.43.3 + version: 5.43.3 + tmp: + specifier: ^0.2.3 + version: 0.2.3 + ts-pattern: + specifier: ^5.7.1 + version: 5.7.1 + typescript: + specifier: ^5.8.0 + version: 5.8.3 + vue: + specifier: 3.5.22 + version: 3.5.22 + zod-validation-error: + specifier: ^4.0.1 + version: 4.0.1 importers: @@ -146,7 +200,7 @@ importers: version: link:../../schema decimal.js: specifier: 'catalog:' - version: 10.4.3 + version: 10.6.0 superjson: specifier: ^2.2.3 version: 2.2.3 @@ -154,6 +208,12 @@ importers: '@tanstack/react-query': specifier: 'catalog:' version: 5.90.6(react@19.2.0) + '@tanstack/svelte-query': + specifier: 5.90.2 + version: 5.90.2(svelte@5.43.3) + '@tanstack/vue-query': + specifier: 5.90.6 + version: 5.90.6(vue@3.5.22(typescript@5.8.3)) '@testing-library/dom': specifier: ^10.4.1 version: 10.4.1 @@ -187,6 +247,12 @@ importers: react: specifier: 'catalog:' version: 19.2.0 + svelte: + specifier: 'catalog:' + version: 5.43.3 + vue: + specifier: 'catalog:' + version: 3.5.22(typescript@5.8.3) packages/common-helpers: devDependencies: @@ -324,7 +390,7 @@ importers: version: 12.2.0 decimal.js: specifier: 'catalog:' - version: 10.4.3 + version: 10.6.0 json-stable-stringify: specifier: ^1.3.0 version: 1.3.0 @@ -413,7 +479,7 @@ importers: dependencies: decimal.js: specifier: 'catalog:' - version: 10.4.3 + version: 10.6.0 devDependencies: '@zenstackhq/eslint-config': specifier: workspace:* @@ -448,7 +514,7 @@ importers: version: link:../config/typescript-config decimal.js: specifier: 'catalog:' - version: 10.4.3 + version: 10.6.0 kysely: specifier: 'catalog:' version: 0.27.6 @@ -463,7 +529,7 @@ importers: version: link:../orm decimal.js: specifier: 'catalog:' - version: 10.4.3 + version: 10.6.0 superjson: specifier: ^2.2.3 version: 2.2.3 @@ -482,7 +548,7 @@ importers: devDependencies: '@sveltejs/kit': specifier: ^2.48.3 - version: 2.48.3(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.43.0)(vite@7.1.12(@types/node@20.17.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.1)))(svelte@5.43.0)(vite@7.1.12(@types/node@20.17.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.43.3)(vite@7.1.12(@types/node@20.17.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.17.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.1)) '@types/body-parser': specifier: ^1.19.6 version: 1.19.6 @@ -744,7 +810,7 @@ importers: version: 12.2.0 decimal.js: specifier: 'catalog:' - version: 10.4.3 + version: 10.6.0 kysely: specifier: 'catalog:' version: 0.27.6 @@ -772,7 +838,7 @@ importers: version: link:../../packages/testtools decimal.js: specifier: 'catalog:' - version: 10.4.3 + version: 10.6.0 devDependencies: '@zenstackhq/cli': specifier: workspace:* @@ -2626,6 +2692,13 @@ packages: '@tailwindcss/postcss@4.1.16': resolution: {integrity: sha512-Qn3SFGPXYQMKR/UtqS+dqvPrzEeBZHrFA92maT4zijCVggdsXnDBMsPFJo1eArX3J+O+Gi+8pV4PkqjLCNBk3A==} + '@tanstack/match-sorter-utils@8.19.4': + resolution: {integrity: sha512-Wo1iKt2b9OT7d+YGhvEPD3DXvPv2etTusIMhMUoG7fbhmxcXCtIjJDEygy91Y2JFlwGyjqiBPRozme7UD8hoqg==} + engines: {node: '>=12'} + + '@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==} @@ -2634,6 +2707,20 @@ packages: peerDependencies: react: ^18 || ^19 + '@tanstack/svelte-query@5.90.2': + resolution: {integrity: sha512-owjnp0w8sOXlMhLZhucHrsYvCjgjHrVyII/wlqMGefxKFyroZS3xCwTee+IUx7UHbL+QmKr/HQTeTqhgxmxPQw==} + peerDependencies: + svelte: ^3.54.0 || ^4.0.0 || ^5.0.0 + + '@tanstack/vue-query@5.90.6': + resolution: {integrity: sha512-7lKXKuTkX8XPjNd3g71B39JDE5B83Gtrr+yDPs5DiHm5wsM80OcwolLYkXumOM8+7VSm6ZYCrI9HSWszTIx5Gw==} + peerDependencies: + '@vue/composition-api': ^1.1.2 + vue: ^2.6.0 || ^3.3.0 + peerDependenciesMeta: + '@vue/composition-api': + optional: true + '@testing-library/dom@10.4.1': resolution: {integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==} engines: {node: '>=18'} @@ -3775,9 +3862,6 @@ packages: supports-color: optional: true - decimal.js@10.4.3: - resolution: {integrity: sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==} - decimal.js@10.6.0: resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} @@ -4841,10 +4925,6 @@ packages: resolution: {integrity: sha512-9DDdhb5j6cpeitCbvLO7n7J4IxnbM6hoF6O1g4HQ5TfhvvKN8ywDM7668ZhMHRqVmxqhps/F6syWK2KcPxYlkw==} engines: {node: 20 || >=22} - jiti@2.4.2: - resolution: {integrity: sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==} - hasBin: true - jiti@2.6.1: resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} hasBin: true @@ -6123,6 +6203,9 @@ packages: resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} engines: {node: '>= 0.4'} + remove-accents@0.5.0: + resolution: {integrity: sha512-8g3/Otx1eJaVD12e31UbJj1YzdtVvzH85HV7t+9MJYk/u3XmkOUJ5Ys9wQrf9PCPK8+xn4ymzqYCiZl6QWKn+A==} + require-directory@2.1.1: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} @@ -6557,8 +6640,8 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} - svelte@5.43.0: - resolution: {integrity: sha512-1sRxVbgJAB+UGzwkc3GUoiBSzEOf0jqzccMaVoI2+pI+kASUe9qubslxace8+Mzhqw19k4syTA5niCIJwfXpOA==} + svelte@5.43.3: + resolution: {integrity: sha512-kjkAjCk41mJfvJZG56XcJNOdJSke94JxtcX8zFzzz2vrt47E0LnoBzU6azIZ1aBxJgUep8qegAkguSf1GjxLXQ==} engines: {node: '>=18'} svgo@4.0.0: @@ -7224,6 +7307,17 @@ packages: vue-bundle-renderer@2.2.0: resolution: {integrity: sha512-sz/0WEdYH1KfaOm0XaBmRZOWgYTEvUDt6yPYaUzl4E52qzgWLlknaPPTTZmp6benaPTlQAI/hN1x3tAzZygycg==} + vue-demi@0.14.10: + resolution: {integrity: sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==} + engines: {node: '>=12'} + hasBin: true + peerDependencies: + '@vue/composition-api': ^1.0.0-rc.1 + vue: ^3.0.0-0 || ^2.6.0 + peerDependenciesMeta: + '@vue/composition-api': + optional: true + vue-devtools-stub@0.1.0: resolution: {integrity: sha512-RutnB7X8c5hjq39NceArgXg28WZtZpGc3+J16ljMiYnFhKvd8hITxSWQSQ5bvldxMDU6gG5mkxl1MTQLXckVSQ==} @@ -8044,8 +8138,8 @@ snapshots: '@jridgewell/remapping@2.3.5': dependencies: - '@jridgewell/gen-mapping': 0.3.8 - '@jridgewell/trace-mapping': 0.3.25 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 '@jridgewell/resolve-uri@3.1.2': {} @@ -8053,8 +8147,8 @@ snapshots: '@jridgewell/source-map@0.3.11': dependencies: - '@jridgewell/gen-mapping': 0.3.8 - '@jridgewell/trace-mapping': 0.3.25 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 '@jridgewell/sourcemap-codec@1.5.0': {} @@ -8960,11 +9054,11 @@ snapshots: dependencies: acorn: 8.15.0 - '@sveltejs/kit@2.48.3(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.43.0)(vite@7.1.12(@types/node@20.17.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.1)))(svelte@5.43.0)(vite@7.1.12(@types/node@20.17.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.1))': + '@sveltejs/kit@2.48.3(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.43.3)(vite@7.1.12(@types/node@20.17.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.17.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.1))': 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.0)(vite@7.1.12(@types/node@20.17.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.43.3)(vite@7.1.12(@types/node@20.17.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.1)) '@types/cookie': 0.6.0 acorn: 8.15.0 cookie: 0.6.0 @@ -8976,25 +9070,25 @@ snapshots: sade: 1.8.1 set-cookie-parser: 2.7.2 sirv: 3.0.2 - svelte: 5.43.0 + svelte: 5.43.3 vite: 7.1.12(@types/node@20.17.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.43.0)(vite@7.1.12(@types/node@20.17.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.1)))(svelte@5.43.0)(vite@7.1.12(@types/node@20.17.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.43.3)(vite@7.1.12(@types/node@20.17.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.17.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.1))': dependencies: - '@sveltejs/vite-plugin-svelte': 6.2.1(svelte@5.43.0)(vite@7.1.12(@types/node@20.17.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.43.3)(vite@7.1.12(@types/node@20.17.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.1)) debug: 4.4.1 - svelte: 5.43.0 + svelte: 5.43.3 vite: 7.1.12(@types/node@20.17.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.1) transitivePeerDependencies: - supports-color - '@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.43.0)(vite@7.1.12(@types/node@20.17.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.43.3)(vite@7.1.12(@types/node@20.17.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.1))': dependencies: - '@sveltejs/vite-plugin-svelte-inspector': 5.0.1(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.43.0)(vite@7.1.12(@types/node@20.17.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.1)))(svelte@5.43.0)(vite@7.1.12(@types/node@20.17.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.43.3)(vite@7.1.12(@types/node@20.17.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.17.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.1)) debug: 4.4.1 deepmerge: 4.3.1 magic-string: 0.30.21 - svelte: 5.43.0 + svelte: 5.43.3 vite: 7.1.12(@types/node@20.17.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.17.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.1)) transitivePeerDependencies: @@ -9128,6 +9222,12 @@ snapshots: postcss: 8.5.6 tailwindcss: 4.1.16 + '@tanstack/match-sorter-utils@8.19.4': + dependencies: + remove-accents: 0.5.0 + + '@tanstack/query-core@5.90.2': {} + '@tanstack/query-core@5.90.6': {} '@tanstack/react-query@5.90.6(react@19.2.0)': @@ -9135,6 +9235,19 @@ snapshots: '@tanstack/query-core': 5.90.6 react: 19.2.0 + '@tanstack/svelte-query@5.90.2(svelte@5.43.3)': + dependencies: + '@tanstack/query-core': 5.90.2 + svelte: 5.43.3 + + '@tanstack/vue-query@5.90.6(vue@3.5.22(typescript@5.8.3))': + dependencies: + '@tanstack/match-sorter-utils': 8.19.4 + '@tanstack/query-core': 5.90.6 + '@vue/devtools-api': 6.6.4 + vue: 3.5.22(typescript@5.8.3) + vue-demi: 0.14.10(vue@3.5.22(typescript@5.8.3)) + '@testing-library/dom@10.4.1': dependencies: '@babel/code-frame': 7.27.1 @@ -10092,11 +10205,11 @@ snapshots: dotenv: 16.6.1 exsolve: 1.0.7 giget: 2.0.0 - jiti: 2.4.2 + jiti: 2.6.1 ohash: 2.0.11 pathe: 2.0.3 perfect-debounce: 1.0.0 - pkg-types: 2.2.0 + pkg-types: 2.3.0 rc9: 2.1.2 optionalDependencies: magicast: 0.3.5 @@ -10457,10 +10570,7 @@ snapshots: dependencies: ms: 2.1.3 - decimal.js@10.4.3: {} - - decimal.js@10.6.0: - optional: true + decimal.js@10.6.0: {} decompress-response@6.0.0: dependencies: @@ -10791,7 +10901,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.8.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.8.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.8.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.8.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)) @@ -10824,7 +10934,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.8.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.8.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.8.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 @@ -10839,7 +10949,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.8.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.8.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.8.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 @@ -11762,8 +11872,6 @@ snapshots: dependencies: '@isaacs/cliui': 8.0.2 - jiti@2.4.2: {} - jiti@2.6.1: {} joycon@3.1.1: {} @@ -13249,6 +13357,8 @@ snapshots: gopd: 1.2.0 set-function-name: 2.0.2 + remove-accents@0.5.0: {} + require-directory@2.1.1: {} require-from-string@2.0.2: {} @@ -13789,7 +13899,7 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} - svelte@5.43.0: + svelte@5.43.3: dependencies: '@jridgewell/remapping': 2.3.5 '@jridgewell/sourcemap-codec': 1.5.5 @@ -14517,6 +14627,10 @@ snapshots: dependencies: ufo: 1.6.1 + vue-demi@0.14.10(vue@3.5.22(typescript@5.8.3)): + dependencies: + vue: 3.5.22(typescript@5.8.3) + vue-devtools-stub@0.1.0: {} vue-router@4.6.3(vue@3.5.22(typescript@5.8.3)): diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 2fbbf861..85916fa7 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -24,3 +24,5 @@ catalog: '@types/react-dom': 19.2.0 '@tanstack/react-query': 5.90.6 'next': 16.0.1 + 'vue': 3.5.22 + 'svelte': 5.43.3 diff --git a/samples/next.js/package.json b/samples/next.js/package.json index 1040f2f5..eb446539 100644 --- a/samples/next.js/package.json +++ b/samples/next.js/package.json @@ -1,6 +1,6 @@ { "name": "next.js", - "version": "3.0.0-beta.17", + "version": "3.0.0-beta.18", "private": true, "scripts": { "generate": "zen generate --lite", diff --git a/samples/orm/package.json b/samples/orm/package.json index b13e0520..c1d61b9c 100644 --- a/samples/orm/package.json +++ b/samples/orm/package.json @@ -1,6 +1,6 @@ { "name": "sample-blog", - "version": "3.0.0-beta.17", + "version": "3.0.0-beta.18", "description": "", "main": "index.js", "private": true, diff --git a/tests/e2e/package.json b/tests/e2e/package.json index 6dae784d..c30b1cbf 100644 --- a/tests/e2e/package.json +++ b/tests/e2e/package.json @@ -1,6 +1,6 @@ { "name": "e2e", - "version": "3.0.0-beta.17", + "version": "3.0.0-beta.18", "private": true, "type": "module", "scripts": { diff --git a/tests/regression/package.json b/tests/regression/package.json index 30108081..420b56ac 100644 --- a/tests/regression/package.json +++ b/tests/regression/package.json @@ -1,6 +1,6 @@ { "name": "regression", - "version": "3.0.0-beta.17", + "version": "3.0.0-beta.18", "private": true, "type": "module", "scripts": {