@firsthandjs/data-apollo 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 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,68 @@
1
+ # @firsthandjs/data-apollo
2
+
3
+ Apollo Client documents as loaders for [`@firsthandjs/data`](https://www.npmjs.com/package/@firsthandjs/data).
4
+
5
+ ```
6
+ npm install @firsthandjs/data-apollo
7
+ ```
8
+
9
+ 0.37 kB gzip. It has **no dependency on Apollo** and no peer dependency either —
10
+ the three methods it uses are declared structurally. That also keeps your copy
11
+ of `graphql` the only copy, which is a category of afternoon worth avoiding.
12
+
13
+ ```tsx
14
+ import { gql } from '@apollo/client';
15
+ import { apolloLoader } from '@firsthandjs/data-apollo';
16
+ import { useResource } from '@firsthandjs/data';
17
+ import InvoicesDocument from './invoices.gql';
18
+
19
+ const billing = apolloLoader(apollo, gql);
20
+
21
+ function Invoices() {
22
+ const invoices = useResource((context) =>
23
+ billing(InvoicesDocument, { month: month.value })(context),
24
+ );
25
+ return <List items={invoices.data.value?.invoices ?? []} />;
26
+ }
27
+ ```
28
+
29
+ `apolloLoader(client, parse)` takes the parse function too — `gql` from
30
+ `@apollo/client`, or `parse` from `graphql` — because Apollo wants a parsed
31
+ `DocumentNode` and that parser should be yours, not ours.
32
+
33
+ ## The decision this package leaves to you: the cache
34
+
35
+ Apollo requires an `InMemoryCache`, and it is a _normalising_ cache — a
36
+ different thing from a store of resources. There are two ways to run them
37
+ together, and only two:
38
+
39
+ - **Apollo as transport.** The resources are your state. `force` is passed on
40
+ as `fetchPolicy: 'network-only'`, so an invalidation reaches past the cache;
41
+ set `no-cache` on the client if you want it out of the way entirely.
42
+ - **Apollo as the store.** Use `apolloObservable` instead. One write in
43
+ Apollo's cache then updates every view of that entity at the same moment,
44
+ which is what a per-call-site resource cannot do:
45
+
46
+ ```tsx
47
+ const user = fromObservable(...apolloObservable(apollo, gql)(UserDocument, { id: props.id }));
48
+ ```
49
+
50
+ What is not on the list is both at once, with the same data living in two
51
+ places under two invalidation rules. See
52
+ [ADR-0022](https://github.com/firsthandjs/firsthand/blob/main/docs/adr/0022-resources-not-a-cache.md).
53
+
54
+ ## Tags come out of the document
55
+
56
+ `@firsthandjs/data/vite` reads `@tag` and `@invalidates` at build time and
57
+ strips them, so what reaches Apollo is a plain GraphQL document. A query
58
+ declares its `@tag` directives before the request goes out — so an invalidation
59
+ arriving mid-flight still finds it — and a mutation declares its `@invalidates`,
60
+ which an action hands straight to the store:
61
+
62
+ ```tsx
63
+ const pay = useAction((id: string, { signal, invalidates }) =>
64
+ billing(PayDocument, { id })({ signal, force: true, tags: invalidates }),
65
+ );
66
+ ```
67
+
68
+ MIT licensed. See the [data guide](https://github.com/firsthandjs/firsthand/blob/main/docs/guide/09-data.md).
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Apollo Client, as loaders for `@firsthandjs/data`.
3
+ *
4
+ * It takes a client you built and touches three of its methods. Your links,
5
+ * your authentication, your uploads stay where they are — and so does the
6
+ * decision that matters most here: what to do with Apollo's own cache.
7
+ *
8
+ * Apollo requires an `InMemoryCache` instance, and it is a *normalising* cache,
9
+ * which is a different thing from a store of resources. Two ways to run them
10
+ * together, and only these two:
11
+ *
12
+ * - **Apollo as transport.** `fetchPolicy: 'no-cache'`, or the `force` this
13
+ * package passes on, and the resources are your state. One source of
14
+ * truth.
15
+ * - **Apollo as the store.** Use `apolloObservable` instead: one write in
16
+ * Apollo's cache updates every view of that entity at once, which is what
17
+ * a per-call-site resource cannot do. Also one source of truth.
18
+ *
19
+ * What is not on the list is both at once (ADR-0022).
20
+ *
21
+ * There is no dependency on Apollo here, and no peer dependency either: the
22
+ * shapes below are declared structurally, so this package has no opinion about
23
+ * which version you run, and nothing to follow when that version changes.
24
+ */
25
+ import { type DocumentArguments, type GraphQLDocument, type LoadContext, type Variables } from '@firsthandjs/data';
26
+ /** The part of an Apollo client this package uses. Nothing else. */
27
+ export interface ApolloLike {
28
+ query(options: Record<string, unknown>): Promise<{
29
+ data: unknown;
30
+ }>;
31
+ mutate(options: Record<string, unknown>): Promise<{
32
+ data?: unknown;
33
+ }>;
34
+ }
35
+ /**
36
+ * How to turn a document's source into whatever Apollo wants.
37
+ *
38
+ * Apollo takes a parsed `DocumentNode`, which means `gql` from `@apollo/client`
39
+ * or `parse` from `graphql` — your copy of it, not ours, since two copies of
40
+ * `graphql` in one application is its own kind of afternoon.
41
+ */
42
+ export type Parse = (source: string) => unknown;
43
+ /**
44
+ * Binds an Apollo client so a `.gql` document can be a resource's loader.
45
+ *
46
+ * ```ts
47
+ * import { gql } from '@apollo/client';
48
+ * import { apolloLoader } from '@firsthandjs/data-apollo';
49
+ *
50
+ * const billing = apolloLoader(apollo, gql);
51
+ *
52
+ * const invoices = useResource((context) => billing(InvoicesDocument, { month: month.value })(context));
53
+ * ```
54
+ *
55
+ * The tags come from the document's directives, declared before the request
56
+ * goes out. `force` becomes `fetchPolicy: 'network-only'`, which is what makes
57
+ * an invalidation reach past Apollo's cache.
58
+ */
59
+ export declare function apolloLoader(client: ApolloLike, parse: Parse): <T, V extends Variables>(document: GraphQLDocument<T, V>, ...rest: DocumentArguments<V>) => ({ tags, force }: LoadContext) => Promise<T>;
60
+ /** A watched query: what Apollo pushes when its cache changes. */
61
+ export interface WatchLike {
62
+ watchQuery(options: Record<string, unknown>): {
63
+ subscribe(observer: {
64
+ next?: (value: {
65
+ data: unknown;
66
+ }) => void;
67
+ error?: (error: unknown) => void;
68
+ }): {
69
+ unsubscribe: () => void;
70
+ };
71
+ refetch(): Promise<unknown>;
72
+ };
73
+ }
74
+ /**
75
+ * A document as something that pushes, for `fromObservable`.
76
+ *
77
+ * This is the shape to reach for when the same entity is shown in many places
78
+ * and must stay consistent: Apollo's cache is then the one source of truth,
79
+ * and every view of it updates from the same write at the same moment.
80
+ *
81
+ * ```ts
82
+ * const user = fromObservable(...apolloObservable(apollo, gql)(UserDocument, { id }));
83
+ * ```
84
+ */
85
+ export declare function apolloObservable(client: WatchLike, parse: Parse): <T, V extends Variables>(document: GraphQLDocument<T, V>, ...rest: DocumentArguments<V>) => readonly [{
86
+ readonly subscribe: (observer: {
87
+ next?: (value: T) => void;
88
+ error?: (error: unknown) => void;
89
+ }) => {
90
+ unsubscribe: () => void;
91
+ };
92
+ }, {
93
+ readonly reload: () => Promise<unknown>;
94
+ }];
95
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,OAAO,EAEL,KAAK,iBAAiB,EACtB,KAAK,eAAe,EACpB,KAAK,WAAW,EAChB,KAAK,SAAS,EACf,MAAM,mBAAmB,CAAC;AAE3B,oEAAoE;AACpE,MAAM,WAAW,UAAU;IACzB,KAAK,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC;QAAE,IAAI,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;IACpE,MAAM,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC;QAAE,IAAI,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;CACvE;AAED;;;;;;GAMG;AACH,MAAM,MAAM,KAAK,GAAG,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC;AAEhD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,UAAU,EAAE,KAAK,EAAE,KAAK,IAInD,CAAC,EAAE,CAAC,SAAS,SAAS,EAAE,UAAU,eAAe,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,GAAG,MAAM,iBAAiB,CAAC,CAAC,CAAC,MACrF,iBAAiB,WAAW,KAAG,OAAO,CAAC,CAAC,CAAC,CAoBnD;AAED,kEAAkE;AAClE,MAAM,WAAW,SAAS;IACxB,UAAU,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG;QAC5C,SAAS,CAAC,QAAQ,EAAE;YAClB,IAAI,CAAC,EAAE,CAAC,KAAK,EAAE;gBAAE,IAAI,EAAE,OAAO,CAAA;aAAE,KAAK,IAAI,CAAC;YAC1C,KAAK,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;SAClC,GAAG;YAAE,WAAW,EAAE,MAAM,IAAI,CAAA;SAAE,CAAC;QAChC,OAAO,IAAI,OAAO,CAAC,OAAO,CAAC,CAAC;KAC7B,CAAC;CACH;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,SAAS,EAAE,KAAK,EAAE,KAAK,IACtD,CAAC,EAAE,CAAC,SAAS,SAAS,EAC5B,UAAU,eAAe,CAAC,CAAC,EAAE,CAAC,CAAC,EAC/B,GAAG,MAAM,iBAAiB,CAAC,CAAC,CAAC;mCAKH;QAAE,IAAI,CAAC,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,IAAI,CAAC;QAAC,KAAK,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAA;KAAE;qBAxBtE,MAAM,IAAI;;;;GAiChC"}
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ import{resolveTags as c}from"@firsthandjs/data";function k(n,a){return(e,...s)=>async({tags:o,force:r})=>{let t=s[0]??{},i=e.kind==="mutation"?e.invalidates:e.tags;o(...c(i,t));let u=a(e.source);return e.kind==="mutation"?(await n.mutate({mutation:u,variables:t})).data:(await n.query({query:u,variables:t,fetchPolicy:r?"network-only":"cache-first"})).data}}function m(n,a){return(e,...s)=>{let o=n.watchQuery({query:a(e.source),variables:s[0]??{}});return[{subscribe:r=>o.subscribe({next:t=>r.next?.(t.data),...r.error===void 0?{}:{error:r.error}})},{reload:()=>o.refetch()}]}}export{k as apolloLoader,m as apolloObservable};
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "@firsthandjs/data-apollo",
3
+ "version": "0.5.0",
4
+ "description": "Loaders for Apollo Client, for @firsthandjs/data. Takes your client; never configures it.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "sideEffects": false,
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "default": "./dist/index.js"
12
+ }
13
+ },
14
+ "types": "./dist/index.d.ts",
15
+ "main": "./dist/index.js",
16
+ "files": [
17
+ "dist",
18
+ "README.md",
19
+ "LICENSE"
20
+ ],
21
+ "dependencies": {
22
+ "@firsthandjs/data": "0.5.0"
23
+ },
24
+ "engines": {
25
+ "node": ">=20.11.0"
26
+ },
27
+ "publishConfig": {
28
+ "access": "public",
29
+ "provenance": true
30
+ },
31
+ "repository": {
32
+ "type": "git",
33
+ "url": "git+https://github.com/firsthandjs/firsthand.git",
34
+ "directory": "packages/data-apollo"
35
+ },
36
+ "bugs": {
37
+ "url": "https://github.com/firsthandjs/firsthand/issues"
38
+ },
39
+ "homepage": "https://github.com/firsthandjs/firsthand#readme",
40
+ "keywords": [
41
+ "firsthand",
42
+ "data",
43
+ "apollo",
44
+ "resources",
45
+ "tags"
46
+ ]
47
+ }