@rippledb/client-query 0.1.0
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/README.md +78 -0
- package/dist/index.js +60 -0
- package/dist/index.js.map +1 -0
- package/package.json +65 -0
package/README.md
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# @rippledb/client-query
|
|
2
|
+
|
|
3
|
+
Final DX package combining:
|
|
4
|
+
|
|
5
|
+
- `@rippledb/client-controllers` (CRUD controllers)
|
|
6
|
+
- `@rippledb/bind-tanstack-query` (TanStack Query invalidation wiring)
|
|
7
|
+
|
|
8
|
+
This package is intended to be the ergonomic “one import” for client apps using RippleDB + TanStack Query.
|
|
9
|
+
|
|
10
|
+
📚 **Documentation:** [rippledb.dev/docs/reference/client-query](https://rippledb.dev/docs/reference/client-query)
|
|
11
|
+
|
|
12
|
+
## Installation
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
pnpm add @rippledb/client-query @tanstack/query-core
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
If you use a framework adapter (e.g. `@tanstack/react-query`), you already have `@tanstack/query-core`.
|
|
19
|
+
|
|
20
|
+
## Usage
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
import { QueryClient } from '@tanstack/query-core';
|
|
24
|
+
import { MemoryStore } from '@rippledb/store-memory';
|
|
25
|
+
import { defineSchema } from '@rippledb/core';
|
|
26
|
+
import { createClientQueryApi } from '@rippledb/client-query';
|
|
27
|
+
|
|
28
|
+
type Schema = {
|
|
29
|
+
todos: { id: string; title: string; done: boolean };
|
|
30
|
+
users: { id: string; name: string; email: string };
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const store = new MemoryStore<Schema>();
|
|
34
|
+
const queryClient = new QueryClient();
|
|
35
|
+
|
|
36
|
+
// Runtime descriptor (entity + field discovery)
|
|
37
|
+
const schema = defineSchema({
|
|
38
|
+
todos: { id: '', title: '', done: false },
|
|
39
|
+
users: { id: '', name: '', email: '' },
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const api = createClientQueryApi({
|
|
43
|
+
store,
|
|
44
|
+
stream: 'user-123',
|
|
45
|
+
queryClient,
|
|
46
|
+
schema,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// CRUD via dynamic controllers
|
|
50
|
+
const todo = await api.todos.create({ title: 'Buy milk', done: false });
|
|
51
|
+
await api.todos.update(todo.id, { done: true });
|
|
52
|
+
|
|
53
|
+
// Cached query helper + automatic invalidation
|
|
54
|
+
const todos = await api.query({
|
|
55
|
+
key: ['todos'],
|
|
56
|
+
deps: ['todos'],
|
|
57
|
+
fn: () => api.todos.list({ entity: 'todos' }),
|
|
58
|
+
});
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Query registry
|
|
62
|
+
|
|
63
|
+
If you want to register “list queries” up-front (recommended), build a registry and pass it in:
|
|
64
|
+
|
|
65
|
+
```ts
|
|
66
|
+
import { defineListRegistry } from '@rippledb/bind-tanstack-query';
|
|
67
|
+
|
|
68
|
+
const registry = defineListRegistry()
|
|
69
|
+
.list(['todos'], { deps: ['todos'] })
|
|
70
|
+
.list(['dashboard'], { deps: ['todos', 'users'] });
|
|
71
|
+
|
|
72
|
+
const api = createClientQueryApi({ store, stream, queryClient, schema, registry });
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## License
|
|
76
|
+
|
|
77
|
+
MIT
|
|
78
|
+
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { createEntityController } from "@rippledb/client-controllers";
|
|
3
|
+
import { wireTanstackInvalidation } from "@rippledb/bind-tanstack-query";
|
|
4
|
+
function createClientQueryApi(options) {
|
|
5
|
+
const {
|
|
6
|
+
store,
|
|
7
|
+
stream,
|
|
8
|
+
queryClient,
|
|
9
|
+
schema,
|
|
10
|
+
registry: providedRegistry,
|
|
11
|
+
debounceMs = 50
|
|
12
|
+
} = options;
|
|
13
|
+
const controllers = {};
|
|
14
|
+
for (const entity of schema.entities) {
|
|
15
|
+
controllers[entity] = createEntityController({
|
|
16
|
+
store,
|
|
17
|
+
entity,
|
|
18
|
+
stream
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
const registry = providedRegistry ?? { entries: [] };
|
|
22
|
+
const cleanup = wireTanstackInvalidation({
|
|
23
|
+
queryClient,
|
|
24
|
+
store,
|
|
25
|
+
registry,
|
|
26
|
+
debounceMs
|
|
27
|
+
});
|
|
28
|
+
const registerQueryIfNeeded = (opts) => {
|
|
29
|
+
const exists = registry.entries.some((e) => {
|
|
30
|
+
if (e.queryKey.length !== opts.key.length) return false;
|
|
31
|
+
for (let i = 0; i < e.queryKey.length; i += 1) {
|
|
32
|
+
if (!Object.is(e.queryKey[i], opts.key[i])) return false;
|
|
33
|
+
}
|
|
34
|
+
return true;
|
|
35
|
+
});
|
|
36
|
+
if (!exists) {
|
|
37
|
+
registry.entries.push({
|
|
38
|
+
queryKey: opts.key,
|
|
39
|
+
deps: opts.deps
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
const api = {
|
|
44
|
+
...controllers,
|
|
45
|
+
async query(queryOptions) {
|
|
46
|
+
registerQueryIfNeeded(queryOptions);
|
|
47
|
+
return await queryClient.fetchQuery({
|
|
48
|
+
// TanStack queryKey expects readonly unknown[]; our key matches.
|
|
49
|
+
queryKey: queryOptions.key,
|
|
50
|
+
queryFn: async () => queryOptions.fn()
|
|
51
|
+
});
|
|
52
|
+
},
|
|
53
|
+
cleanup
|
|
54
|
+
};
|
|
55
|
+
return api;
|
|
56
|
+
}
|
|
57
|
+
export {
|
|
58
|
+
createClientQueryApi
|
|
59
|
+
};
|
|
60
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +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":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@rippledb/client-query",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"sideEffects": false,
|
|
7
|
+
"description": "Final DX package combining controllers + bind-tanstack-query for RippleDB",
|
|
8
|
+
"keywords": [
|
|
9
|
+
"rippledb",
|
|
10
|
+
"tanstack-query",
|
|
11
|
+
"react-query",
|
|
12
|
+
"controllers",
|
|
13
|
+
"crud",
|
|
14
|
+
"local-first",
|
|
15
|
+
"offline-first"
|
|
16
|
+
],
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "https://github.com/eckerlein/rippledb.git",
|
|
20
|
+
"directory": "packages/client-query"
|
|
21
|
+
},
|
|
22
|
+
"exports": {
|
|
23
|
+
".": {
|
|
24
|
+
"types": "./dist/index.d.ts",
|
|
25
|
+
"import": "./dist/index.js"
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
"files": [
|
|
29
|
+
"dist"
|
|
30
|
+
],
|
|
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"
|
|
36
|
+
},
|
|
37
|
+
"peerDependencies": {
|
|
38
|
+
"@tanstack/query-core": "^5.0.0"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"@tanstack/query-core": "^5.60.5",
|
|
42
|
+
"eslint": "^9.37.0",
|
|
43
|
+
"tsup": "^8.5.0",
|
|
44
|
+
"typescript": "^5.9.3",
|
|
45
|
+
"vitest": "^3.2.4",
|
|
46
|
+
"@rippledb/store-memory": "0.1.1"
|
|
47
|
+
},
|
|
48
|
+
"tsup": {
|
|
49
|
+
"entry": [
|
|
50
|
+
"src/index.ts"
|
|
51
|
+
],
|
|
52
|
+
"format": [
|
|
53
|
+
"esm"
|
|
54
|
+
],
|
|
55
|
+
"dts": false,
|
|
56
|
+
"sourcemap": true,
|
|
57
|
+
"clean": true
|
|
58
|
+
},
|
|
59
|
+
"scripts": {
|
|
60
|
+
"build": "tsup && tsc -p tsconfig.build.json",
|
|
61
|
+
"test": "vitest run --passWithNoTests",
|
|
62
|
+
"test:watch": "vitest --passWithNoTests",
|
|
63
|
+
"lint": "eslint ."
|
|
64
|
+
}
|
|
65
|
+
}
|