@rippledb/client-query 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -11,7 +11,8 @@ function createClientQueryApi(options) {
11
11
  debounceMs = 50
12
12
  } = options;
13
13
  const controllers = {};
14
- for (const entity of schema.entities) {
14
+ for (const entityName of schema.entities) {
15
+ const entity = entityName;
15
16
  controllers[entity] = createEntityController({
16
17
  store,
17
18
  entity,
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"sourcesContent":["import type { Store } from '@rippledb/client';\nimport type { RippleSchema, EntityName, SchemaDescriptor } from '@rippledb/core';\nimport type { QueryClient } from '@tanstack/query-core';\nimport { createEntityController, type EntityController } from '@rippledb/client-controllers';\nimport { wireTanstackInvalidation, type ListRegistry } from '@rippledb/bind-tanstack-query';\n\n/**\n * Query helper options for registering list queries with automatic invalidation.\n */\nexport type QueryOptions<\n S extends RippleSchema = RippleSchema,\n T = unknown,\n> = {\n /**\n * The query key prefix to invalidate (e.g. ['todos'], ['todoList']).\n */\n key: readonly unknown[];\n /**\n * Entity names this query depends on.\n * When a DbEvent for any of these entities fires, the query is invalidated.\n */\n deps: readonly EntityName<S>[];\n /**\n * The query function to execute.\n */\n fn: () => Promise<T>;\n};\n\n/**\n * Client Query API that combines controllers with TanStack Query invalidation.\n * \n * Provides:\n * - Dynamic entity controllers (api.todos, api.users, etc.)\n * - Automatic cache invalidation\n * - Query helpers with dependency tracking\n * \n * @example\n * ```ts\n * const api = createClientQueryApi({\n * store,\n * stream: 'user-123',\n * queryClient,\n * schema: schemaDescriptor,\n * });\n * \n * // CRUD operations\n * const todo = await api.todos.create({ title: 'Buy milk' });\n * const fetched = await api.todos.read('todo-1');\n * \n * // Query helpers with automatic invalidation\n * const todos = await api.query({\n * key: ['todos'],\n * deps: ['todos'],\n * fn: () => api.todos.list({ entity: 'todos' }),\n * });\n * ```\n */\nexport type ClientQueryApi<\n S extends RippleSchema = RippleSchema,\n ListQuery = unknown,\n> = {\n /**\n * Entity controllers, dynamically created from schema.entities.\n * Each entity gets a controller with CRUD operations.\n */\n [K in EntityName<S>]: EntityController<S, K, ListQuery>;\n} & {\n /**\n * Query helper that registers a query with automatic invalidation.\n * \n * @param options - Query options with key, deps, and fn\n * @returns The result of the query function\n */\n query<T>(options: QueryOptions<S, T>): Promise<T>;\n \n /**\n * Cleanup function to unsubscribe from invalidation events.\n */\n cleanup(): void;\n};\n\nexport type CreateClientQueryApiOptions<\n S extends RippleSchema = RippleSchema,\n ListQuery = unknown,\n> = {\n /**\n * The Store instance to operate on.\n */\n store: Store<S, ListQuery>;\n \n /**\n * The stream ID for all changes created by controllers.\n */\n stream: string;\n \n /**\n * TanStack QueryClient instance.\n */\n queryClient: QueryClient;\n \n /**\n * Schema descriptor for runtime entity discovery.\n */\n schema: SchemaDescriptor<S>;\n \n /**\n * Optional list registry for custom query key mappings.\n * If not provided, a default registry is created from query() calls.\n */\n registry?: ListRegistry;\n \n /**\n * Debounce time in milliseconds for invalidation coalescing.\n * @default 50\n */\n debounceMs?: number;\n};\n\n/**\n * Creates a Client Query API that combines controllers with TanStack Query invalidation.\n * \n * The API dynamically creates entity controllers from the schema descriptor,\n * allowing you to use `api.todos`, `api.users`, etc. without manually creating\n * controllers for each entity.\n * \n * @example\n * ```ts\n * import { defineSchema } from '@rippledb/core';\n * import { createClientQueryApi } from '@rippledb/client-query';\n * \n * const schema = defineSchema({\n * todos: { id: '', title: '', done: false },\n * users: { id: '', name: '', email: '' },\n * });\n * \n * const api = createClientQueryApi({\n * store,\n * stream: 'user-123',\n * queryClient,\n * schema,\n * });\n * \n * // Use dynamic controllers\n * const todo = await api.todos.create({ title: 'Buy milk' });\n * const user = await api.users.read('user-1');\n * ```\n */\nexport function createClientQueryApi<\n S extends RippleSchema = RippleSchema,\n ListQuery = unknown,\n>(options: CreateClientQueryApiOptions<S, ListQuery>): ClientQueryApi<S, ListQuery> {\n const {\n store,\n stream,\n queryClient,\n schema,\n registry: providedRegistry,\n debounceMs = 50,\n } = options;\n\n // Create controllers for each entity dynamically\n const controllers = {} as Record<EntityName<S>, EntityController<S, EntityName<S>, ListQuery>>;\n \n for (const entity of schema.entities) {\n controllers[entity] = createEntityController({\n store,\n entity,\n stream,\n });\n }\n\n // If the caller doesn't provide a registry, we create a new mutable registry.\n // If a registry *is* provided, it is expected to be mutable and `api.query()`\n // will add entries to it dynamically (so invalidation works automatically).\n const registry: ListRegistry = providedRegistry ?? { entries: [] };\n\n // Wire up invalidation once\n const cleanup = wireTanstackInvalidation({\n queryClient,\n store,\n registry,\n debounceMs,\n });\n\n const registerQueryIfNeeded = (opts: QueryOptions<S, unknown>) => {\n // Avoid duplicate registrations for identical query keys (shallow compare)\n const exists = registry.entries.some((e) => {\n if (e.queryKey.length !== opts.key.length) return false;\n for (let i = 0; i < e.queryKey.length; i += 1) {\n if (!Object.is(e.queryKey[i], opts.key[i])) return false;\n }\n return true;\n });\n if (!exists) {\n registry.entries.push({\n queryKey: opts.key,\n deps: opts.deps as readonly string[],\n });\n }\n };\n\n // Create the API object with dynamic entity controllers\n const api = {\n ...controllers,\n \n async query<T>(queryOptions: QueryOptions<S, T>): Promise<T> {\n registerQueryIfNeeded(queryOptions);\n\n // Use TanStack Query for caching + consistent invalidation\n return await queryClient.fetchQuery({\n // TanStack queryKey expects readonly unknown[]; our key matches.\n queryKey: queryOptions.key,\n queryFn: async () => queryOptions.fn(),\n });\n },\n \n cleanup,\n } as ClientQueryApi<S, ListQuery>;\n\n return api;\n}\n\n// Re-export types for convenience\nexport type { EntityController } from '@rippledb/client-controllers';\nexport type { ListRegistry } from '@rippledb/bind-tanstack-query';\n"],"mappings":";AAGA,SAAS,8BAAqD;AAC9D,SAAS,gCAAmD;AA+IrD,SAAS,qBAGd,SAAkF;AAClF,QAAM;AAAA,IACJ;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,UAAU;AAAA,IACV,aAAa;AAAA,EACf,IAAI;AAGJ,QAAM,cAAc,CAAC;AAErB,aAAW,UAAU,OAAO,UAAU;AACpC,gBAAY,MAAM,IAAI,uBAAuB;AAAA,MAC3C;AAAA,MACA;AAAA,MACA;AAAA,IACF,CAAC;AAAA,EACH;AAKA,QAAM,WAAyB,oBAAoB,EAAE,SAAS,CAAC,EAAE;AAGjE,QAAM,UAAU,yBAAyB;AAAA,IACvC;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF,CAAC;AAED,QAAM,wBAAwB,CAAC,SAAmC;AAEhE,UAAM,SAAS,SAAS,QAAQ,KAAK,CAAC,MAAM;AAC1C,UAAI,EAAE,SAAS,WAAW,KAAK,IAAI,OAAQ,QAAO;AAClD,eAAS,IAAI,GAAG,IAAI,EAAE,SAAS,QAAQ,KAAK,GAAG;AAC7C,YAAI,CAAC,OAAO,GAAG,EAAE,SAAS,CAAC,GAAG,KAAK,IAAI,CAAC,CAAC,EAAG,QAAO;AAAA,MACrD;AACA,aAAO;AAAA,IACT,CAAC;AACD,QAAI,CAAC,QAAQ;AACX,eAAS,QAAQ,KAAK;AAAA,QACpB,UAAU,KAAK;AAAA,QACf,MAAM,KAAK;AAAA,MACb,CAAC;AAAA,IACH;AAAA,EACF;AAGA,QAAM,MAAM;AAAA,IACV,GAAG;AAAA,IAEH,MAAM,MAAS,cAA8C;AAC3D,4BAAsB,YAAY;AAGlC,aAAO,MAAM,YAAY,WAAW;AAAA;AAAA,QAElC,UAAU,aAAa;AAAA,QACvB,SAAS,YAAY,aAAa,GAAG;AAAA,MACvC,CAAC;AAAA,IACH;AAAA,IAEA;AAAA,EACF;AAEA,SAAO;AACT;","names":[]}
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["import type { Store } from '@rippledb/client';\nimport type {\n RippleSchema,\n EntityName,\n SchemaDescriptor,\n DescriptorSchema,\n InferSchema,\n} from '@rippledb/core';\nimport type { QueryClient } from '@tanstack/query-core';\nimport { createEntityController, type EntityController } from '@rippledb/client-controllers';\nimport { wireTanstackInvalidation, type ListRegistry } from '@rippledb/bind-tanstack-query';\n\n/**\n * Query helper options for registering list queries with automatic invalidation.\n */\nexport type QueryOptions<\n S extends RippleSchema = RippleSchema,\n T = unknown,\n> = {\n /**\n * The query key prefix to invalidate (e.g. ['todos'], ['todoList']).\n */\n key: readonly unknown[];\n /**\n * Entity names this query depends on.\n * When a DbEvent for any of these entities fires, the query is invalidated.\n */\n deps: readonly EntityName<S>[];\n /**\n * The query function to execute.\n */\n fn: () => Promise<T>;\n};\n\n/**\n * Client Query API that combines controllers with TanStack Query invalidation.\n * \n * Provides:\n * - Dynamic entity controllers (api.todos, api.users, etc.)\n * - Automatic cache invalidation\n * - Query helpers with dependency tracking\n * \n * @example\n * ```ts\n * const api = createClientQueryApi({\n * store,\n * stream: 'user-123',\n * queryClient,\n * schema: schemaDescriptor,\n * });\n * \n * // CRUD operations\n * const todo = await api.todos.create({ title: 'Buy milk' });\n * const fetched = await api.todos.read('todo-1');\n * \n * // Query helpers with automatic invalidation\n * const todos = await api.query({\n * key: ['todos'],\n * deps: ['todos'],\n * fn: () => api.todos.list({ entity: 'todos' }),\n * });\n * ```\n */\nexport type ClientQueryApi<\n S extends RippleSchema = RippleSchema,\n ListQuery = unknown,\n> = {\n /**\n * Entity controllers, dynamically created from schema.entities.\n * Each entity gets a controller with CRUD operations.\n */\n [K in EntityName<S>]: EntityController<S, K, ListQuery>;\n} & {\n /**\n * Query helper that registers a query with automatic invalidation.\n * \n * @param options - Query options with key, deps, and fn\n * @returns The result of the query function\n */\n query<T>(options: QueryOptions<S, T>): Promise<T>;\n \n /**\n * Cleanup function to unsubscribe from invalidation events.\n */\n cleanup(): void;\n};\n\nexport type CreateClientQueryApiOptions<\n D extends DescriptorSchema = DescriptorSchema,\n ListQuery = unknown,\n> = {\n /**\n * The Store instance to operate on.\n * Must be typed with the inferred schema type.\n */\n store: Store<InferSchema<SchemaDescriptor<D>>, ListQuery>;\n \n /**\n * The stream ID for all changes created by controllers.\n */\n stream: string;\n \n /**\n * TanStack QueryClient instance.\n */\n queryClient: QueryClient;\n \n /**\n * Schema descriptor for runtime entity discovery.\n */\n schema: SchemaDescriptor<D>;\n \n /**\n * Optional list registry for custom query key mappings.\n * If not provided, a default registry is created from query() calls.\n */\n registry?: ListRegistry;\n \n /**\n * Debounce time in milliseconds for invalidation coalescing.\n * @default 50\n */\n debounceMs?: number;\n};\n\n/**\n * Creates a Client Query API that combines controllers with TanStack Query invalidation.\n * \n * The API dynamically creates entity controllers from the schema descriptor,\n * allowing you to use `api.todos`, `api.users`, etc. without manually creating\n * controllers for each entity.\n * \n * @example\n * ```ts\n * import { defineSchema, s, InferSchema } from '@rippledb/core';\n * import { createClientQueryApi } from '@rippledb/client-query';\n * \n * const schema = defineSchema({\n * todos: {\n * id: s.string(),\n * title: s.string(),\n * done: s.boolean(),\n * },\n * users: {\n * id: s.string(),\n * name: s.string(),\n * email: s.string(),\n * },\n * });\n * \n * type MySchema = InferSchema<typeof schema>;\n * const store = new MemoryStore<MySchema>();\n * \n * const api = createClientQueryApi({\n * store,\n * stream: 'user-123',\n * queryClient,\n * schema,\n * });\n * \n * // Use dynamic controllers\n * const todo = await api.todos.create({ title: 'Buy milk' });\n * const user = await api.users.read('user-1');\n * ```\n */\nexport function createClientQueryApi<\n D extends DescriptorSchema,\n ListQuery = unknown,\n>(\n options: CreateClientQueryApiOptions<D, ListQuery>,\n): ClientQueryApi<InferSchema<SchemaDescriptor<D>>, ListQuery> {\n type S = InferSchema<SchemaDescriptor<D>>;\n \n const {\n store,\n stream,\n queryClient,\n schema,\n registry: providedRegistry,\n debounceMs = 50,\n } = options;\n\n // Create controllers for each entity dynamically\n const controllers = {} as Record<EntityName<S>, EntityController<S, EntityName<S>, ListQuery>>;\n \n for (const entityName of schema.entities) {\n // Cast entity name to the correct type\n const entity = entityName as EntityName<S>;\n controllers[entity] = createEntityController({\n store,\n entity,\n stream,\n });\n }\n\n // If the caller doesn't provide a registry, we create a new mutable registry.\n // If a registry *is* provided, it is expected to be mutable and `api.query()`\n // will add entries to it dynamically (so invalidation works automatically).\n const registry: ListRegistry = providedRegistry ?? { entries: [] };\n\n // Wire up invalidation once\n const cleanup = wireTanstackInvalidation({\n queryClient,\n store,\n registry,\n debounceMs,\n });\n\n const registerQueryIfNeeded = (opts: QueryOptions<S, unknown>) => {\n // Avoid duplicate registrations for identical query keys (shallow compare)\n const exists = registry.entries.some((e) => {\n if (e.queryKey.length !== opts.key.length) return false;\n for (let i = 0; i < e.queryKey.length; i += 1) {\n if (!Object.is(e.queryKey[i], opts.key[i])) return false;\n }\n return true;\n });\n if (!exists) {\n registry.entries.push({\n queryKey: opts.key,\n deps: opts.deps as readonly string[],\n });\n }\n };\n\n // Create the API object with dynamic entity controllers\n const api = {\n ...controllers,\n \n async query<T>(queryOptions: QueryOptions<S, T>): Promise<T> {\n registerQueryIfNeeded(queryOptions);\n\n // Use TanStack Query for caching + consistent invalidation\n return await queryClient.fetchQuery({\n // TanStack queryKey expects readonly unknown[]; our key matches.\n queryKey: queryOptions.key,\n queryFn: async () => queryOptions.fn(),\n });\n },\n \n cleanup,\n } as ClientQueryApi<S, ListQuery>;\n\n return api;\n}\n\n// Re-export types for convenience\nexport type { EntityController } from '@rippledb/client-controllers';\nexport type { ListRegistry } from '@rippledb/bind-tanstack-query';\n"],"mappings":";AASA,SAAS,8BAAqD;AAC9D,SAAS,gCAAmD;AA2JrD,SAAS,qBAId,SAC6D;AAG7D,QAAM;AAAA,IACJ;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,UAAU;AAAA,IACV,aAAa;AAAA,EACf,IAAI;AAGJ,QAAM,cAAc,CAAC;AAErB,aAAW,cAAc,OAAO,UAAU;AAExC,UAAM,SAAS;AACf,gBAAY,MAAM,IAAI,uBAAuB;AAAA,MAC3C;AAAA,MACA;AAAA,MACA;AAAA,IACF,CAAC;AAAA,EACH;AAKA,QAAM,WAAyB,oBAAoB,EAAE,SAAS,CAAC,EAAE;AAGjE,QAAM,UAAU,yBAAyB;AAAA,IACvC;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF,CAAC;AAED,QAAM,wBAAwB,CAAC,SAAmC;AAEhE,UAAM,SAAS,SAAS,QAAQ,KAAK,CAAC,MAAM;AAC1C,UAAI,EAAE,SAAS,WAAW,KAAK,IAAI,OAAQ,QAAO;AAClD,eAAS,IAAI,GAAG,IAAI,EAAE,SAAS,QAAQ,KAAK,GAAG;AAC7C,YAAI,CAAC,OAAO,GAAG,EAAE,SAAS,CAAC,GAAG,KAAK,IAAI,CAAC,CAAC,EAAG,QAAO;AAAA,MACrD;AACA,aAAO;AAAA,IACT,CAAC;AACD,QAAI,CAAC,QAAQ;AACX,eAAS,QAAQ,KAAK;AAAA,QACpB,UAAU,KAAK;AAAA,QACf,MAAM,KAAK;AAAA,MACb,CAAC;AAAA,IACH;AAAA,EACF;AAGA,QAAM,MAAM;AAAA,IACV,GAAG;AAAA,IAEH,MAAM,MAAS,cAA8C;AAC3D,4BAAsB,YAAY;AAGlC,aAAO,MAAM,YAAY,WAAW;AAAA;AAAA,QAElC,UAAU,aAAa;AAAA,QACvB,SAAS,YAAY,aAAa,GAAG;AAAA,MACvC,CAAC;AAAA,IACH;AAAA,IAEA;AAAA,EACF;AAEA,SAAO;AACT;","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rippledb/client-query",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -29,10 +29,10 @@
29
29
  "dist"
30
30
  ],
31
31
  "dependencies": {
32
- "@rippledb/core": "0.1.1",
33
- "@rippledb/client": "0.1.1",
34
- "@rippledb/client-controllers": "0.1.0",
35
- "@rippledb/bind-tanstack-query": "0.1.1"
32
+ "@rippledb/core": "0.2.0",
33
+ "@rippledb/client-controllers": "0.1.1",
34
+ "@rippledb/bind-tanstack-query": "0.1.2",
35
+ "@rippledb/client": "0.1.2"
36
36
  },
37
37
  "peerDependencies": {
38
38
  "@tanstack/query-core": "^5.0.0"
@@ -43,7 +43,7 @@
43
43
  "tsup": "^8.5.0",
44
44
  "typescript": "^5.9.3",
45
45
  "vitest": "^3.2.4",
46
- "@rippledb/store-memory": "0.1.1"
46
+ "@rippledb/store-memory": "0.1.2"
47
47
  },
48
48
  "tsup": {
49
49
  "entry": [