@firsthandjs/data 0.5.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/LICENSE +21 -0
- package/README.md +99 -0
- package/dist/codegen.d.ts +77 -0
- package/dist/codegen.d.ts.map +1 -0
- package/dist/codegen.dev.js +67 -0
- package/dist/codegen.js +67 -0
- package/dist/dev.d.ts +10 -0
- package/dist/dev.d.ts.map +1 -0
- package/dist/dev.prod.d.ts +3 -0
- package/dist/dev.prod.d.ts.map +1 -0
- package/dist/document.d.ts +98 -0
- package/dist/document.d.ts.map +1 -0
- package/dist/http.d.ts +64 -0
- package/dist/http.d.ts.map +1 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.dev.js +562 -0
- package/dist/index.js +2 -0
- package/dist/resource.d.ts +82 -0
- package/dist/resource.d.ts.map +1 -0
- package/dist/store.d.ts +121 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/tags.d.ts +42 -0
- package/dist/tags.d.ts.map +1 -0
- package/dist/vite.d.ts +25 -0
- package/dist/vite.d.ts.map +1 -0
- package/dist/vite.dev.js +223 -0
- package/dist/vite.js +223 -0
- package/package.json +59 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Firsthand contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# @firsthandjs/data
|
|
2
|
+
|
|
3
|
+
Resources, actions and invalidation for Firsthand: what is loaded, as reactive
|
|
4
|
+
state, and when it has to be loaded again.
|
|
5
|
+
|
|
6
|
+
**Documentation:** [guide](https://github.com/firsthandjs/firsthand/blob/main/docs/guide/09-data.md) · [API reference](https://github.com/firsthandjs/firsthand/blob/main/docs/reference/data.md) · [ADR-0022](https://github.com/firsthandjs/firsthand/blob/main/docs/adr/0022-resources-not-a-cache.md)
|
|
7
|
+
|
|
8
|
+
```
|
|
9
|
+
npm install @firsthandjs/data
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
2.81 kB gzip. It depends on `@firsthandjs/core` and `@firsthandjs/dom`.
|
|
13
|
+
|
|
14
|
+
```tsx
|
|
15
|
+
const user = useResource(async ({ signal, tags }) => {
|
|
16
|
+
tags(tag('user', { id: props.id }));
|
|
17
|
+
return json<User>(`/api/users/${props.id}`)({ signal });
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
return <h1>{user.data.value?.name ?? '…'}</h1>;
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
No key, no name, no variables object. `props.id` is read inside the loader, so
|
|
24
|
+
changing it runs the loader again and aborts what was in flight — exactly as an
|
|
25
|
+
`effect` behaves, and for the same reason.
|
|
26
|
+
|
|
27
|
+
## What it is not
|
|
28
|
+
|
|
29
|
+
**It is not a cache.** A cache is defined by a second lookup for the same thing
|
|
30
|
+
finding the first one's result, and that needs identity: a key, a name,
|
|
31
|
+
something two callers agree on. Every form of that is a thing to forget or
|
|
32
|
+
collide on. A resource belongs to its call site instead, and two call sites are
|
|
33
|
+
two resources whatever their tags say.
|
|
34
|
+
|
|
35
|
+
Deduplication, response caching and normalisation belong one layer down, where
|
|
36
|
+
the knowledge of what is _the same thing_ actually lives — Apollo's cache, urql's
|
|
37
|
+
`cacheExchange`, the browser's HTTP cache, or three lines of your own.
|
|
38
|
+
|
|
39
|
+
| Layer | Owns | Who |
|
|
40
|
+
| ------------- | ---------------------------------------- | ----------------------------- |
|
|
41
|
+
| Normalisation | one entity, one truth, everywhere | Apollo, urql-graphcache |
|
|
42
|
+
| Request cache | not asking twice | those clients, the HTTP cache |
|
|
43
|
+
| **Resources** | **reactive state, status, invalidation** | **this package** |
|
|
44
|
+
|
|
45
|
+
## Tags are for invalidation
|
|
46
|
+
|
|
47
|
+
They may be coarse, they may overlap, and two unrelated sources may share one.
|
|
48
|
+
That is the feature — and it is safe, because they are not identity.
|
|
49
|
+
|
|
50
|
+
```tsx
|
|
51
|
+
const rename = useAction(async (input: Rename, { signal, invalidates }) => {
|
|
52
|
+
const changed = await json<Changed>(`/api/users/by-name/${input.name}`, {
|
|
53
|
+
method: 'PATCH',
|
|
54
|
+
json: input,
|
|
55
|
+
signal,
|
|
56
|
+
})({ signal });
|
|
57
|
+
// The client knew a name; only the server knows which id that was.
|
|
58
|
+
invalidates(...changed.tags);
|
|
59
|
+
return changed.user;
|
|
60
|
+
});
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Declare them where you know them: before the `await` when the caller does,
|
|
64
|
+
after it when only the server does. `tags()` **replaces**, so a run says what
|
|
65
|
+
it is about rather than accumulating what it used to be about.
|
|
66
|
+
|
|
67
|
+
## Reaching past a cache
|
|
68
|
+
|
|
69
|
+
The loader is told _why_ it is running. Without that, a transport cache would
|
|
70
|
+
hand back the answer that was just invalidated:
|
|
71
|
+
|
|
72
|
+
```tsx
|
|
73
|
+
useResource(async ({ signal, force }) =>
|
|
74
|
+
json<User>('/api/users/5', { cache: force ? 'reload' : 'default' })({ signal }),
|
|
75
|
+
);
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
`fetchPolicy: 'network-only'` for Apollo, `requestPolicy` for urql — the
|
|
79
|
+
helper packages do it for you.
|
|
80
|
+
|
|
81
|
+
## Bringing a client
|
|
82
|
+
|
|
83
|
+
`@firsthandjs/data-axios`, `-urql` and `-apollo` bind an instance **you** built:
|
|
84
|
+
your interceptors, your links, your authentication. None of them depends on the
|
|
85
|
+
client it binds, so none of them has a version to follow.
|
|
86
|
+
|
|
87
|
+
For GraphQL, tags come out of the `.gql` file as `@tag` / `@invalidates`
|
|
88
|
+
directives, read at build time by `@firsthandjs/data/vite` and typed by
|
|
89
|
+
`@firsthandjs/data/codegen`. The document that reaches your client is a plain
|
|
90
|
+
object with the directives already removed.
|
|
91
|
+
|
|
92
|
+
## What it deliberately does not do
|
|
93
|
+
|
|
94
|
+
Interceptors, retries, backoff, token refresh, request de-duplication, progress
|
|
95
|
+
events, XSRF, a Node adapter, normalisation, optimistic cache surgery,
|
|
96
|
+
pagination helpers. Each belongs to a transport or to an application's own
|
|
97
|
+
policy — and a loader takes any client, because it takes any function.
|
|
98
|
+
|
|
99
|
+
MIT licensed.
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A GraphQL Code Generator plugin that types your `.gql` imports.
|
|
3
|
+
*
|
|
4
|
+
* `@graphql-codegen/typescript-operations` writes `NotesQuery` and
|
|
5
|
+
* `NotesQueryVariables` from your schema. This writes the line that connects
|
|
6
|
+
* them to the file:
|
|
7
|
+
*
|
|
8
|
+
* ```ts
|
|
9
|
+
* declare module '*\/notes.gql' {
|
|
10
|
+
* const document: GraphQLDocument<NotesQuery, NotesQueryVariables>;
|
|
11
|
+
* export default document;
|
|
12
|
+
* }
|
|
13
|
+
* ```
|
|
14
|
+
*
|
|
15
|
+
* With that, nothing at a call site carries a type argument, and nothing can
|
|
16
|
+
* drift: the variables are checked against the operation, and the result is
|
|
17
|
+
* the shape the server actually returns.
|
|
18
|
+
*
|
|
19
|
+
* ```ts
|
|
20
|
+
* // codegen.ts
|
|
21
|
+
* import type { CodegenConfig } from '@graphql-codegen/cli';
|
|
22
|
+
*
|
|
23
|
+
* const config: CodegenConfig = {
|
|
24
|
+
* schema: 'schema.graphql',
|
|
25
|
+
* documents: 'src/**\/*.gql',
|
|
26
|
+
* generates: {
|
|
27
|
+
* 'src/graphql-types.ts': { plugins: ['typescript', 'typescript-operations'] },
|
|
28
|
+
* 'src/graphql-modules.d.ts': {
|
|
29
|
+
* plugins: ['@firsthandjs/data/codegen'],
|
|
30
|
+
* config: { typesPath: './graphql-types' },
|
|
31
|
+
* },
|
|
32
|
+
* },
|
|
33
|
+
* };
|
|
34
|
+
* export default config;
|
|
35
|
+
* ```
|
|
36
|
+
*
|
|
37
|
+
* The declarations use wildcard module names (`*\/notes.gql`), which is how
|
|
38
|
+
* every codegen plugin of this kind works: TypeScript picks the most specific
|
|
39
|
+
* pattern, and the file stays a script rather than a module so the wildcards
|
|
40
|
+
* are legal.
|
|
41
|
+
*/
|
|
42
|
+
/** The bits of codegen's document shape this needs. Typed here to avoid a dependency. */
|
|
43
|
+
export interface CodegenDocument {
|
|
44
|
+
readonly location?: string | undefined;
|
|
45
|
+
readonly document?: {
|
|
46
|
+
readonly definitions: readonly {
|
|
47
|
+
readonly kind: string;
|
|
48
|
+
readonly operation?: string;
|
|
49
|
+
readonly name?: {
|
|
50
|
+
readonly value: string;
|
|
51
|
+
} | undefined;
|
|
52
|
+
}[];
|
|
53
|
+
} | undefined;
|
|
54
|
+
}
|
|
55
|
+
export interface CodegenConfig {
|
|
56
|
+
/**
|
|
57
|
+
* Where `typescript-operations` wrote its types, relative to this file.
|
|
58
|
+
*
|
|
59
|
+
* Defaults to `./graphql-types`, which is what the example above generates.
|
|
60
|
+
*/
|
|
61
|
+
readonly typesPath?: string;
|
|
62
|
+
/** Import path for the document type. Only useful when testing this plugin. */
|
|
63
|
+
readonly documentTypePath?: string;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Generates the module declarations.
|
|
67
|
+
*
|
|
68
|
+
* The signature is GraphQL Code Generator's: `(schema, documents, config)`.
|
|
69
|
+
* The schema is unused — the operation types come from the plugins that ran
|
|
70
|
+
* before this one.
|
|
71
|
+
*/
|
|
72
|
+
export declare function plugin(_schema: unknown, documents: readonly CodegenDocument[], config?: CodegenConfig): string;
|
|
73
|
+
declare const _default: {
|
|
74
|
+
plugin: typeof plugin;
|
|
75
|
+
};
|
|
76
|
+
export default _default;
|
|
77
|
+
//# sourceMappingURL=codegen.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"codegen.d.ts","sourceRoot":"","sources":["../src/codegen.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AAEH,yFAAyF;AACzF,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACvC,QAAQ,CAAC,QAAQ,CAAC,EACd;QACE,QAAQ,CAAC,WAAW,EAAE,SAAS;YAC7B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;YACtB,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;YAC5B,QAAQ,CAAC,IAAI,CAAC,EAAE;gBAAE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;aAAE,GAAG,SAAS,CAAC;SACxD,EAAE,CAAC;KACL,GACD,SAAS,CAAC;CACf;AAED,MAAM,WAAW,aAAa;IAC5B;;;;OAIG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,+EAA+E;IAC/E,QAAQ,CAAC,gBAAgB,CAAC,EAAE,MAAM,CAAC;CACpC;AA8BD;;;;;;GAMG;AACH,wBAAgB,MAAM,CACpB,OAAO,EAAE,OAAO,EAChB,SAAS,EAAE,SAAS,eAAe,EAAE,EACrC,MAAM,GAAE,aAAkB,GACzB,MAAM,CAgDR;;;;AAED,wBAA0B"}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// packages/data/src/codegen.ts
|
|
2
|
+
var SUFFIX = {
|
|
3
|
+
query: "Query",
|
|
4
|
+
mutation: "Mutation",
|
|
5
|
+
subscription: "Subscription"
|
|
6
|
+
};
|
|
7
|
+
function moduleName(location) {
|
|
8
|
+
const normalized = location.split("\\").join("/");
|
|
9
|
+
const file = normalized.slice(normalized.lastIndexOf("/") + 1);
|
|
10
|
+
return `*/${file}`;
|
|
11
|
+
}
|
|
12
|
+
function operationOf(document) {
|
|
13
|
+
for (const definition of document?.definitions ?? []) {
|
|
14
|
+
if (definition.kind === "OperationDefinition" && definition.name !== void 0) {
|
|
15
|
+
return { name: definition.name.value, kind: definition.operation ?? "query" };
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
function plugin(_schema, documents, config = {}) {
|
|
21
|
+
const typesPath = config.typesPath ?? "./graphql-types";
|
|
22
|
+
const documentType = config.documentTypePath ?? "@firsthandjs/data";
|
|
23
|
+
const blocks = [];
|
|
24
|
+
const declared = /* @__PURE__ */ new Set();
|
|
25
|
+
for (const entry of documents) {
|
|
26
|
+
const location = entry.location;
|
|
27
|
+
if (location === void 0 || location === "") {
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
const operation = operationOf(entry.document);
|
|
31
|
+
if (operation === null) {
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
const name = moduleName(location);
|
|
35
|
+
if (declared.has(name)) {
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
declared.add(name);
|
|
39
|
+
const base = `${operation.name}${SUFFIX[operation.kind] ?? "Query"}`;
|
|
40
|
+
blocks.push(
|
|
41
|
+
[
|
|
42
|
+
`declare module '${name}' {`,
|
|
43
|
+
` const document: import('${documentType}').GraphQLDocument<`,
|
|
44
|
+
` import('${typesPath}').${base},`,
|
|
45
|
+
` import('${typesPath}').${base}Variables`,
|
|
46
|
+
` >;`,
|
|
47
|
+
` export default document;`,
|
|
48
|
+
`}`
|
|
49
|
+
].join("\n")
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
return [
|
|
53
|
+
"// Generated by @firsthandjs/data/codegen. Do not edit.",
|
|
54
|
+
"//",
|
|
55
|
+
"// Wildcard module declarations, so that importing a .gql file gives a",
|
|
56
|
+
"// document that knows its own result and variable types. Nothing here is",
|
|
57
|
+
"// a module: wildcards are only legal in a script.",
|
|
58
|
+
"",
|
|
59
|
+
...blocks,
|
|
60
|
+
""
|
|
61
|
+
].join("\n");
|
|
62
|
+
}
|
|
63
|
+
var codegen_default = { plugin };
|
|
64
|
+
export {
|
|
65
|
+
codegen_default as default,
|
|
66
|
+
plugin
|
|
67
|
+
};
|
package/dist/codegen.js
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// packages/data/src/codegen.ts
|
|
2
|
+
var SUFFIX = {
|
|
3
|
+
query: "Query",
|
|
4
|
+
mutation: "Mutation",
|
|
5
|
+
subscription: "Subscription"
|
|
6
|
+
};
|
|
7
|
+
function moduleName(location) {
|
|
8
|
+
const normalized = location.split("\\").join("/");
|
|
9
|
+
const file = normalized.slice(normalized.lastIndexOf("/") + 1);
|
|
10
|
+
return `*/${file}`;
|
|
11
|
+
}
|
|
12
|
+
function operationOf(document) {
|
|
13
|
+
for (const definition of document?.definitions ?? []) {
|
|
14
|
+
if (definition.kind === "OperationDefinition" && definition.name !== void 0) {
|
|
15
|
+
return { name: definition.name.value, kind: definition.operation ?? "query" };
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
function plugin(_schema, documents, config = {}) {
|
|
21
|
+
const typesPath = config.typesPath ?? "./graphql-types";
|
|
22
|
+
const documentType = config.documentTypePath ?? "@firsthandjs/data";
|
|
23
|
+
const blocks = [];
|
|
24
|
+
const declared = /* @__PURE__ */ new Set();
|
|
25
|
+
for (const entry of documents) {
|
|
26
|
+
const location = entry.location;
|
|
27
|
+
if (location === void 0 || location === "") {
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
const operation = operationOf(entry.document);
|
|
31
|
+
if (operation === null) {
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
const name = moduleName(location);
|
|
35
|
+
if (declared.has(name)) {
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
declared.add(name);
|
|
39
|
+
const base = `${operation.name}${SUFFIX[operation.kind] ?? "Query"}`;
|
|
40
|
+
blocks.push(
|
|
41
|
+
[
|
|
42
|
+
`declare module '${name}' {`,
|
|
43
|
+
` const document: import('${documentType}').GraphQLDocument<`,
|
|
44
|
+
` import('${typesPath}').${base},`,
|
|
45
|
+
` import('${typesPath}').${base}Variables`,
|
|
46
|
+
` >;`,
|
|
47
|
+
` export default document;`,
|
|
48
|
+
`}`
|
|
49
|
+
].join("\n")
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
return [
|
|
53
|
+
"// Generated by @firsthandjs/data/codegen. Do not edit.",
|
|
54
|
+
"//",
|
|
55
|
+
"// Wildcard module declarations, so that importing a .gql file gives a",
|
|
56
|
+
"// document that knows its own result and variable types. Nothing here is",
|
|
57
|
+
"// a module: wildcards are only legal in a script.",
|
|
58
|
+
"",
|
|
59
|
+
...blocks,
|
|
60
|
+
""
|
|
61
|
+
].join("\n");
|
|
62
|
+
}
|
|
63
|
+
var codegen_default = { plugin };
|
|
64
|
+
export {
|
|
65
|
+
codegen_default as default,
|
|
66
|
+
plugin
|
|
67
|
+
};
|
package/dist/dev.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Tag } from './tags.js';
|
|
2
|
+
/**
|
|
3
|
+
* Reports what the cache just did.
|
|
4
|
+
*
|
|
5
|
+
* The cache is the one part of the framework whose behaviour is not visible in
|
|
6
|
+
* the reactive graph: a tag match is a decision, not an edge, and an
|
|
7
|
+
* invalidation that hits nothing looks exactly like one that was never sent.
|
|
8
|
+
*/
|
|
9
|
+
export declare function devQuery(event: string, key: string, tags: readonly Tag[]): void;
|
|
10
|
+
//# sourceMappingURL=dev.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dev.d.ts","sourceRoot":"","sources":["../src/dev.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,WAAW,CAAC;AAOrC;;;;;;GAMG;AACH,wBAAgB,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,GAAG,EAAE,GAAG,IAAI,CAE/E"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dev.prod.d.ts","sourceRoot":"","sources":["../src/dev.prod.ts"],"names":[],"mappings":"AAAA,2EAA2E;AAE3E,wBAAgB,QAAQ,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,OAAO,EAAE,GAAG,IAAI,CAEtF"}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GraphQL, with the tags written where the query is.
|
|
3
|
+
*
|
|
4
|
+
* A `.graphql` file already says what it reads. Repeating that in TypeScript as
|
|
5
|
+
* a cache key is how caches drift out of sync with the queries they hold, so
|
|
6
|
+
* the tag assignment lives in the document, as directives:
|
|
7
|
+
*
|
|
8
|
+
* ```graphql
|
|
9
|
+
* query User($id: ID!) @tag(name: "user", id: $id) @tag(name: "permissions") {
|
|
10
|
+
* user(id: $id) {
|
|
11
|
+
* id
|
|
12
|
+
* name
|
|
13
|
+
* }
|
|
14
|
+
* }
|
|
15
|
+
* ```
|
|
16
|
+
*
|
|
17
|
+
* and a mutation says what it breaks:
|
|
18
|
+
*
|
|
19
|
+
* ```graphql
|
|
20
|
+
* mutation RenameUser($id: ID!, $name: String!)
|
|
21
|
+
* @invalidates(name: "user", id: $id)
|
|
22
|
+
* @invalidates(name: "users") {
|
|
23
|
+
* renameUser(id: $id, name: $name) {
|
|
24
|
+
* id
|
|
25
|
+
* }
|
|
26
|
+
* }
|
|
27
|
+
* ```
|
|
28
|
+
*
|
|
29
|
+
* `name` is the tag's name; every other argument is a tag variable. `$id` is
|
|
30
|
+
* bound from the variables the call passes, so one directive covers every user.
|
|
31
|
+
* A variable the call leaves out is dropped from the tag, which widens it —
|
|
32
|
+
* `user(id: $id)` with no `id` means "every user", which is the right answer
|
|
33
|
+
* for "I changed something and I do not know which one".
|
|
34
|
+
*
|
|
35
|
+
* The directives are **removed** from the document before it is sent. They are
|
|
36
|
+
* instructions to the cache, not to the server, and a server that has not
|
|
37
|
+
* declared them in its schema would reject the whole query — the same reason
|
|
38
|
+
* Apollo strips `@connection`. Put them on the operation, or on a field if
|
|
39
|
+
* that reads better; they are recognised and stripped wherever they appear.
|
|
40
|
+
*/
|
|
41
|
+
import { type Tag, type TagVars, type Variables } from './tags.js';
|
|
42
|
+
/** A tag variable: either bound from a call's variables, or fixed. */
|
|
43
|
+
export type TagValue = {
|
|
44
|
+
readonly variable: string;
|
|
45
|
+
} | {
|
|
46
|
+
readonly literal: TagVars[string];
|
|
47
|
+
};
|
|
48
|
+
export interface TagTemplate {
|
|
49
|
+
readonly name: string;
|
|
50
|
+
readonly vars: Readonly<Record<string, TagValue>>;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* A parsed operation, carrying what it returns and what it needs.
|
|
54
|
+
*
|
|
55
|
+
* `TData` and `TVariables` exist only in the type system — nothing writes
|
|
56
|
+
* them, and `JSON.stringify` of a document does not mention them. They are
|
|
57
|
+
* what lets `useGraphQL(NotesDocument)` know its own result type without a
|
|
58
|
+
* type argument at the call site, once `@firsthandjs/data/codegen` has written
|
|
59
|
+
* the declaration for the `.gql` file.
|
|
60
|
+
*/
|
|
61
|
+
export interface GraphQLDocument<TData = unknown, TVariables extends Variables = Record<string, never>> {
|
|
62
|
+
/** What is sent to the server: the document with the tag directives removed. */
|
|
63
|
+
readonly source: string;
|
|
64
|
+
/** The operation name, or `''` for an anonymous operation. */
|
|
65
|
+
readonly operation: string;
|
|
66
|
+
readonly kind: 'query' | 'mutation' | 'subscription';
|
|
67
|
+
readonly tags: readonly TagTemplate[];
|
|
68
|
+
readonly invalidates: readonly TagTemplate[];
|
|
69
|
+
/** Phantom: the shape this operation returns. Never present at runtime. */
|
|
70
|
+
readonly data?: TData;
|
|
71
|
+
/** Phantom: the variables this operation takes. Never present at runtime. */
|
|
72
|
+
readonly variables?: TVariables;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* How a document's variables are passed: required when the operation has some,
|
|
76
|
+
* omitted when it has none.
|
|
77
|
+
*
|
|
78
|
+
* A tuple rather than `variables?:` so that an operation with required
|
|
79
|
+
* variables cannot be called without them, while one without may be called
|
|
80
|
+
* with the document alone. The helper packages take this, which is what checks
|
|
81
|
+
* a call against the schema it was generated from.
|
|
82
|
+
*/
|
|
83
|
+
export type DocumentArguments<TVariables extends Variables> = Record<string, never> extends TVariables ? [variables?: TVariables] : [variables: TVariables];
|
|
84
|
+
/** Thrown for a tag directive the cache cannot make sense of. */
|
|
85
|
+
export declare class FirsthandDirectiveError extends Error {
|
|
86
|
+
constructor(message: string);
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Reads a document's tag directives and returns it without them.
|
|
90
|
+
*
|
|
91
|
+
* Malformed directives throw. With the `@firsthandjs/data/vite` loader that
|
|
92
|
+
* happens at build time, which is where a typo in a cache instruction should
|
|
93
|
+
* be found.
|
|
94
|
+
*/
|
|
95
|
+
export declare function parseGraphQL<TData = unknown, TVariables extends Variables = Record<string, never>>(source: string): GraphQLDocument<TData, TVariables>;
|
|
96
|
+
/** Binds tag templates against a call's variables. */
|
|
97
|
+
export declare function resolveTags(templates: readonly TagTemplate[], variables: Variables): Tag[];
|
|
98
|
+
//# sourceMappingURL=document.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"document.d.ts","sourceRoot":"","sources":["../src/document.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AACH,OAAO,EAAO,KAAK,GAAG,EAAE,KAAK,OAAO,EAAE,KAAK,SAAS,EAAE,MAAM,WAAW,CAAC;AAGxE,sEAAsE;AACtE,MAAM,MAAM,QAAQ,GAAG;IAAE,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC,MAAM,CAAC,CAAA;CAAE,CAAC;AAE7F,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC,CAAC;CACnD;AAED;;;;;;;;GAQG;AACH,MAAM,WAAW,eAAe,CAC9B,KAAK,GAAG,OAAO,EACf,UAAU,SAAS,SAAS,GAAG,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC;IAEpD,gFAAgF;IAChF,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,8DAA8D;IAC9D,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,IAAI,EAAE,OAAO,GAAG,UAAU,GAAG,cAAc,CAAC;IACrD,QAAQ,CAAC,IAAI,EAAE,SAAS,WAAW,EAAE,CAAC;IACtC,QAAQ,CAAC,WAAW,EAAE,SAAS,WAAW,EAAE,CAAC;IAC7C,2EAA2E;IAC3E,QAAQ,CAAC,IAAI,CAAC,EAAE,KAAK,CAAC;IACtB,6EAA6E;IAC7E,QAAQ,CAAC,SAAS,CAAC,EAAE,UAAU,CAAC;CACjC;AAED;;;;;;;;GAQG;AACH,MAAM,MAAM,iBAAiB,CAAC,UAAU,SAAS,SAAS,IACxD,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,SAAS,UAAU,GAAG,CAAC,SAAS,CAAC,EAAE,UAAU,CAAC,GAAG,CAAC,SAAS,EAAE,UAAU,CAAC,CAAC;AAMhG,iEAAiE;AACjE,qBAAa,uBAAwB,SAAQ,KAAK;gBACpC,OAAO,EAAE,MAAM;CAI5B;AAqND;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,KAAK,GAAG,OAAO,EAAE,UAAU,SAAS,SAAS,GAAG,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,EAChG,MAAM,EAAE,MAAM,GACb,eAAe,CAAC,KAAK,EAAE,UAAU,CAAC,CAYpC;AAED,sDAAsD;AACtD,wBAAgB,WAAW,CAAC,SAAS,EAAE,SAAS,WAAW,EAAE,EAAE,SAAS,EAAE,SAAS,GAAG,GAAG,EAAE,CAiB1F"}
|
package/dist/http.d.ts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* REST, which needs almost nothing.
|
|
3
|
+
*
|
|
4
|
+
* `fetch` is already the API; what a cache needs from it is an abort signal
|
|
5
|
+
* wired up and a failed status turned into a thrown error, because a promise
|
|
6
|
+
* that resolves with a 500 makes every caller write the same four lines.
|
|
7
|
+
*/
|
|
8
|
+
import type { LoadContext } from './store.js';
|
|
9
|
+
/**
|
|
10
|
+
* Thrown for any response outside 2xx.
|
|
11
|
+
*
|
|
12
|
+
* Prefixed like every other error here, so that `instanceof` cannot be confused
|
|
13
|
+
* by an `HttpError` from somewhere else in an application.
|
|
14
|
+
*/
|
|
15
|
+
export declare class FirsthandHttpError extends Error {
|
|
16
|
+
readonly status: number;
|
|
17
|
+
readonly url: string;
|
|
18
|
+
/** The parsed body, if there was one. */
|
|
19
|
+
readonly body: unknown;
|
|
20
|
+
constructor(status: number, url: string,
|
|
21
|
+
/** The parsed body, if there was one. */
|
|
22
|
+
body: unknown);
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* What `json` takes: `RequestInit`, plus the one thing the platform lacks.
|
|
26
|
+
*
|
|
27
|
+
* `method`, `credentials`, `mode`, `headers` and the rest are passed straight
|
|
28
|
+
* through. So is `body`: a `FormData`, a `URLSearchParams` or a `Blob` is
|
|
29
|
+
* labelled by the platform itself, boundary and all, and nothing here could
|
|
30
|
+
* improve on that.
|
|
31
|
+
*
|
|
32
|
+
* JSON is the exception, and the only one — measured: a stringified object is
|
|
33
|
+
* a string, so `fetch` labels it `text/plain`. `json:` is the two lines every
|
|
34
|
+
* codebase writes to fix that.
|
|
35
|
+
*/
|
|
36
|
+
export interface JsonRequest extends Omit<RequestInit, 'body'> {
|
|
37
|
+
readonly body?: BodyInit | null;
|
|
38
|
+
/** Sent as JSON, with the content type the platform will not set for you. */
|
|
39
|
+
readonly json?: unknown;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* `fetch` for a JSON endpoint: the abort signal wired up, a failed status
|
|
43
|
+
* thrown, and the body parsed.
|
|
44
|
+
*
|
|
45
|
+
* ```ts
|
|
46
|
+
* const user = useResource(async ({ signal, tags }) => {
|
|
47
|
+
* tags(tag('user', { id: props.id }));
|
|
48
|
+
* return json<User>(`/api/users/${props.id}`)({ signal });
|
|
49
|
+
* });
|
|
50
|
+
* ```
|
|
51
|
+
*
|
|
52
|
+
* A write is the same call with a method and a body:
|
|
53
|
+
*
|
|
54
|
+
* ```ts
|
|
55
|
+
* json<Note>('/api/notes', { method: 'POST', json: { title } });
|
|
56
|
+
* json<Upload>('/api/files', { method: 'POST', body: formData });
|
|
57
|
+
* ```
|
|
58
|
+
*
|
|
59
|
+
* This is the platform, not a client. There is no base URL, no instance, no
|
|
60
|
+
* interceptor and no retry here, and there will not be: those belong to a
|
|
61
|
+
* client, and a loader takes any client because it takes any function.
|
|
62
|
+
*/
|
|
63
|
+
export declare function json<T>(input: string, init?: JsonRequest): (context: Pick<LoadContext, 'signal'>) => Promise<T>;
|
|
64
|
+
//# sourceMappingURL=http.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../src/http.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE9C;;;;;GAKG;AACH,qBAAa,kBAAmB,SAAQ,KAAK;IAEzC,QAAQ,CAAC,MAAM,EAAE,MAAM;IACvB,QAAQ,CAAC,GAAG,EAAE,MAAM;IACpB,yCAAyC;IACzC,QAAQ,CAAC,IAAI,EAAE,OAAO;gBAHb,MAAM,EAAE,MAAM,EACd,GAAG,EAAE,MAAM;IACpB,yCAAyC;IAChC,IAAI,EAAE,OAAO;CAKzB;AAED;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,WAAY,SAAQ,IAAI,CAAC,WAAW,EAAE,MAAM,CAAC;IAC5D,QAAQ,CAAC,IAAI,CAAC,EAAE,QAAQ,GAAG,IAAI,CAAC;IAChC,6EAA6E;IAC7E,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,CAAC;CACzB;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,IAAI,CAAC,CAAC,EACpB,KAAK,EAAE,MAAM,EACb,IAAI,GAAE,WAAgB,GACrB,CAAC,OAAO,EAAE,IAAI,CAAC,WAAW,EAAE,QAAQ,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC,CAwBtD"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@firsthandjs/data` — resources, actions and invalidation.
|
|
3
|
+
*
|
|
4
|
+
* What is loaded, as reactive state; what it is about, as tags; and when it
|
|
5
|
+
* has to be loaded again. Not a cache: deduplication, response caching and
|
|
6
|
+
* normalisation belong to the transport, where the knowledge of what is the
|
|
7
|
+
* same thing lives (ADR-0022).
|
|
8
|
+
*/
|
|
9
|
+
export { createData, DataContext, useData, useInvalidate } from './resource.js';
|
|
10
|
+
export { useResource, useAction, fromObservable, fromPromise } from './resource.js';
|
|
11
|
+
export type { Action, ObservableLike, BridgeOptions, ResourceOptions } from './resource.js';
|
|
12
|
+
export type { ActionContext, DataOptions, DataStore, LoadContext, Resource, Status, Storage, } from './store.js';
|
|
13
|
+
export { tag, tagMatches, anyTagMatches } from './tags.js';
|
|
14
|
+
export type { Tag, TagVars, Variables } from './tags.js';
|
|
15
|
+
export { parseGraphQL, resolveTags, FirsthandDirectiveError } from './document.js';
|
|
16
|
+
export type { DocumentArguments, GraphQLDocument, TagTemplate, TagValue } from './document.js';
|
|
17
|
+
/**
|
|
18
|
+
* `fetch`, with the three things a resource needs from it.
|
|
19
|
+
*
|
|
20
|
+
* The platform, not a vendor: no base URLs, no instances, no interceptors, no
|
|
21
|
+
* retry. Those belong to a client, and a loader takes any client because it
|
|
22
|
+
* takes any function.
|
|
23
|
+
*/
|
|
24
|
+
export { json, FirsthandHttpError } from './http.js';
|
|
25
|
+
export type { JsonRequest } from './http.js';
|
|
26
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAChF,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AACpF,YAAY,EAAE,MAAM,EAAE,cAAc,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAC5F,YAAY,EACV,aAAa,EACb,WAAW,EACX,SAAS,EACT,WAAW,EACX,QAAQ,EACR,MAAM,EACN,OAAO,GACR,MAAM,YAAY,CAAC;AAEpB,OAAO,EAAE,GAAG,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAC3D,YAAY,EAAE,GAAG,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAEzD,OAAO,EAAE,YAAY,EAAE,WAAW,EAAE,uBAAuB,EAAE,MAAM,eAAe,CAAC;AACnF,YAAY,EAAE,iBAAiB,EAAE,eAAe,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAE/F;;;;;;GAMG;AACH,OAAO,EAAE,IAAI,EAAE,kBAAkB,EAAE,MAAM,WAAW,CAAC;AACrD,YAAY,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC"}
|