@codecks/fetch 0.1.7 → 1.0.0-rc.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 CHANGED
@@ -11,26 +11,51 @@ npm install @codecks/fetch
11
11
  ## Getting started
12
12
 
13
13
  ```ts
14
- import {buildFetchersWithSimpleLoader} from "@codecks/fetch";
15
-
16
- const {fetchFromRoot, fetchInstance, fetchInstances, fetchFromInstance} =
17
- buildFetchersWithSimpleLoader({
18
- baseUrl: "https://api.codecks.io/",
19
- subdomain: "my-org",
20
- accessToken: "your-token",
21
- });
14
+ import {buildFetchers} from "@codecks/fetch";
15
+
16
+ const {fetchFromRoot, fetchInstance, fetchInstances, fetchFromInstance} = buildFetchers({
17
+ token: "cdxat_…",
18
+ });
22
19
  ```
23
20
 
21
+ `token` is an organization token (`cdxat_…`) or a personal token (`cdxut_…`). Create one under
22
+ **Organization Settings → Integrations → API Tokens** or **Your Profile → API Tokens**. It is sent
23
+ as `Authorization: Bearer <token>` and already names its organization, so no subdomain is needed.
24
+
24
25
  ### Configuration options
25
26
 
26
- | Option | Type | Description |
27
- | ------------- | ------------------------ | ------------------------------ |
28
- | `baseUrl` | `string` | API base URL |
29
- | `subdomain` | `string` | Sets the `X-Account` header |
30
- | `accessToken` | `string` | Sets the `X-Auth-Token` header |
31
- | `headers` | `Record<string, string>` | Additional request headers |
32
- | `timeout` | `number` | Request timeout in ms |
33
- | `fetch` | `typeof fetch` | Custom fetch implementation |
27
+ | Option | Type | Description |
28
+ | --------- | ------------------------ | ---------------------------------------------------- |
29
+ | `token` | `string` | API token, `cdxat_…` or `cdxut_…` (required) |
30
+ | `baseUrl` | `string` | API base URL, defaults to `https://api.codecks.io/` |
31
+ | `headers` | `Record<string, string>` | Additional request headers |
32
+ | `timeout` | `number` | Request timeout in ms; the request aborts after that |
33
+ | `fetch` | `typeof fetch` | Custom fetch implementation |
34
+
35
+ ### Errors
36
+
37
+ A non-2xx answer throws a `CodecksApiError`:
38
+
39
+ ```ts
40
+ import {CodecksApiError} from "@codecks/fetch";
41
+
42
+ try {
43
+ await fetchFromRoot({account: {fields: ["name"]}});
44
+ } catch (e) {
45
+ if (e instanceof CodecksApiError) {
46
+ e.status; // 400, 401, 403, 429, …
47
+ e.code; // "invalid_token", "token_expired", "missing_scope", "unknown_field", …
48
+ e.path; // "_root.account.cards.titel" for a query error
49
+ e.body; // the full response body, e.g. `hint` or `requiredScope`
50
+ }
51
+ }
52
+ ```
53
+
54
+ ### Legacy tokens
55
+
56
+ `buildLegacyFetchers({accessToken, subdomain, baseUrl, …})` sends the old `X-Auth-Token` and
57
+ `X-Account` headers. The API stops accepting `X-Auth-Token` on **2026-12-31**; move to an API token
58
+ before then.
34
59
 
35
60
  ## Fetching data
36
61
 
@@ -324,9 +349,9 @@ Point your LLM's project instructions (e.g. `CLAUDE.md`) at `schema/overview.md`
324
349
  For advanced use cases (batching, caching, custom transports), you can provide your own `DataLoader`:
325
350
 
326
351
  ```ts
327
- import {buildFetchers} from "@codecks/fetch";
352
+ import {buildFetchersFromLoader} from "@codecks/fetch";
328
353
 
329
- const {fetchFromRoot} = buildFetchers({
354
+ const {fetchFromRoot} = buildFetchersFromLoader({
330
355
  fetchModel: async (model, ids, query) => {
331
356
  // your custom loading logic
332
357
  return recordOfResults;
@@ -1,6 +1,6 @@
1
1
  import "../query-type-BqQ9oX_U.js";
2
2
  import "../index-k-KPWV9R.js";
3
- import { FetchOptions } from "../loader-utils-BqZKAn_v.js";
3
+ import { FetchOptions } from "../loader-utils-hvdC-xsQ.js";
4
4
  import { BaseRequester, MissingDataRequest } from "../loader-types-fd5tV0FD.js";
5
5
 
6
6
  //#region src/_exploration/api-requester.d.ts
@@ -1,4 +1,4 @@
1
- import { configuredFetch, ensureMapValue } from "../collection-utils-CYJaA1a9.js";
1
+ import { bearerTransport, configuredFetch, ensureMapValue } from "../collection-utils-T3bx0BS1.js";
2
2
  import { serializeModel } from "../query-helpers-iX1fiqLv.js";
3
3
 
4
4
  //#region src/_exploration/utils/concurrency-limiter.ts
@@ -66,7 +66,7 @@ var ApiRequester = class {
66
66
  fetchOptions;
67
67
  limiter;
68
68
  constructor(fetchOptions, maxConcurrent = 3) {
69
- this.fetchOptions = fetchOptions;
69
+ this.fetchOptions = bearerTransport(fetchOptions);
70
70
  this.limiter = new ConcurrencyLimiter(maxConcurrent);
71
71
  }
72
72
  async request(requests) {
@@ -108,11 +108,23 @@ var ApiRequester = class {
108
108
  key: "",
109
109
  partialInstance: instances
110
110
  });
111
- else for (const [id, data] of Object.entries(instances)) results.push({
112
- model: modelName,
113
- key: id,
114
- partialInstance: data
115
- });
111
+ else for (const [id, data] of Object.entries(instances)) {
112
+ if (data == null) {
113
+ const requested = dataByModel.get(modelName)?.get(id);
114
+ if (!requested) continue;
115
+ results.push({
116
+ model: modelName,
117
+ key: id,
118
+ partialInstance: Object.fromEntries([...requested.fields, ...requested.relations.keys()].map((key) => [key, null]))
119
+ });
120
+ continue;
121
+ }
122
+ results.push({
123
+ model: modelName,
124
+ key: id,
125
+ partialInstance: data
126
+ });
127
+ }
116
128
  return results;
117
129
  }
118
130
  };
@@ -118,6 +118,9 @@ var Store = class {
118
118
  data[`~${fieldName}`] = cachedValue.value.map((v) => `${v}`);
119
119
  data[fieldName] = relationResults.map((result) => result.data);
120
120
  if (relationResults.some((result) => !result.allPresent)) allPresent = false;
121
+ } else if (cachedValue.value == null) {
122
+ data[`~${fieldName}`] = null;
123
+ data[fieldName] = null;
121
124
  } else {
122
125
  const relationResult = this.checkQueryRecursive(relatedModelDesc, relatedModel, `${cachedValue.value}`, relQuery, missingRequests, acceptDirty);
123
126
  data[`~${fieldName}`] = cachedValue.value != null ? `${cachedValue.value}` : null;
@@ -0,0 +1,78 @@
1
+ //#region src/loaders/loader-utils.ts
2
+ const DEFAULT_BASE_URL = "https://api.codecks.io/";
3
+ const bearerTransport = (opts) => {
4
+ const { token,...rest } = opts;
5
+ if (!/^cdx[au]t_/.test(token)) throw new Error("Expected an API token starting with `cdxat_` or `cdxut_`. Legacy tokens need `buildLegacyFetchers`.");
6
+ return {
7
+ baseUrl: DEFAULT_BASE_URL,
8
+ ...rest,
9
+ authHeaders: { Authorization: `Bearer ${token}` }
10
+ };
11
+ };
12
+ const legacyTransport = (opts) => {
13
+ const { accessToken, subdomain,...rest } = opts;
14
+ const authHeaders = {};
15
+ if (accessToken) authHeaders["X-Auth-Token"] = accessToken;
16
+ if (subdomain) authHeaders["X-Account"] = subdomain;
17
+ return {
18
+ ...rest,
19
+ authHeaders
20
+ };
21
+ };
22
+ /** A non-2xx answer. `code`, `path` and the rest of the body follow the API's error format. */
23
+ var CodecksApiError = class extends Error {
24
+ status;
25
+ /** e.g. `invalid_token`, `token_expired`, `missing_scope`, `unknown_field`, `rate_limit` */
26
+ code;
27
+ /** where in the query the error sits, e.g. `_root.account.cards.titel` */
28
+ path;
29
+ /** the parsed JSON body, or the raw text if it wasn't JSON — holds `hint`, `requiredScope`, … */
30
+ body;
31
+ constructor(status, body) {
32
+ const obj = typeof body === "object" && body !== null ? body : {};
33
+ const str = (v) => typeof v === "string" ? v : null;
34
+ const code = str(obj.error);
35
+ super(`[${status}] ${str(obj.message) ?? code ?? (str(body) || "request failed")}`);
36
+ this.name = "CodecksApiError";
37
+ this.status = status;
38
+ this.code = code;
39
+ this.path = str(obj.path);
40
+ this.body = body;
41
+ }
42
+ };
43
+ const readErrorBody = async (r) => {
44
+ const text = await r.text();
45
+ try {
46
+ return JSON.parse(text);
47
+ } catch {
48
+ return text;
49
+ }
50
+ };
51
+ const configuredFetch = async (opts, url, init = {}) => {
52
+ const fetchImpl = opts.fetch || globalThis.fetch;
53
+ const headers = new Headers(init.headers);
54
+ for (const [key, value] of Object.entries(opts.authHeaders)) headers.set(key, value);
55
+ if (opts.headers) for (const [key, value] of Object.entries(opts.headers)) headers.set(key, value);
56
+ const fullUrl = opts.baseUrl ? `${opts.baseUrl}${url}` : url;
57
+ const signal = opts.timeout ? AbortSignal.timeout(opts.timeout) : init.signal;
58
+ const r = await fetchImpl(fullUrl, {
59
+ ...init,
60
+ headers,
61
+ signal
62
+ });
63
+ if (r.status < 200 || r.status >= 300) throw new CodecksApiError(r.status, await readErrorBody(r));
64
+ return await r.json();
65
+ };
66
+
67
+ //#endregion
68
+ //#region src/collection-utils.ts
69
+ const ensureMapValue = (map, key, fallback) => {
70
+ const exist = map.get(key);
71
+ if (exist) return exist;
72
+ const newValue = fallback();
73
+ map.set(key, newValue);
74
+ return newValue;
75
+ };
76
+
77
+ //#endregion
78
+ export { CodecksApiError, bearerTransport, configuredFetch, ensureMapValue, legacyTransport };
package/dist/index.d.ts CHANGED
@@ -1,11 +1,8 @@
1
1
  import { InferModelQuery, InferRelQuery, Instance, ModelQuery, RelQuery } from "./query-type-BqQ9oX_U.js";
2
2
  import { modelMap$1 as modelMap } from "./index-k-KPWV9R.js";
3
- import { DataLoader, FetchOptions } from "./loader-utils-BqZKAn_v.js";
3
+ import { CodecksApiError$1 as CodecksApiError, DataLoader, FetchOptions, LegacyFetchOptions } from "./loader-utils-hvdC-xsQ.js";
4
4
  import { _rootDesc$1 as _rootDesc } from "./_root-BW9uzB79.js";
5
5
 
6
- //#region src/loaders/simple-loader.d.ts
7
- type SimpleLoaderOptions = FetchOptions;
8
- //#endregion
9
6
  //#region src/index.d.ts
10
7
  type ModelMap = typeof modelMap;
11
8
  type Fetchers = {
@@ -14,7 +11,9 @@ type Fetchers = {
14
11
  fetchInstance: <K extends keyof ModelMap, const Q extends ModelQuery<ModelMap[K], ModelMap>>(model: K, id: string, q: Q) => Promise<InferModelQuery<ModelMap[K], Q, ModelMap>>;
15
12
  fetchInstances: <K extends keyof ModelMap, Id extends string, const Q extends ModelQuery<ModelMap[K], ModelMap>>(model: K, id: Id[], q: Q) => Promise<Record<Id, InferModelQuery<ModelMap[K], Q, ModelMap>>>;
16
13
  };
17
- declare const buildFetchersWithSimpleLoader: (opts: SimpleLoaderOptions) => Fetchers;
18
- declare const buildFetchers: (loader: DataLoader) => Fetchers;
14
+ declare const buildFetchers: (opts: FetchOptions) => Fetchers;
15
+ /** @deprecated `X-Auth-Token` stops working on 2026-12-31. Use `buildFetchers` with an API token. */
16
+ declare const buildLegacyFetchers: (opts: LegacyFetchOptions) => Fetchers;
17
+ declare const buildFetchersFromLoader: (loader: DataLoader) => Fetchers;
19
18
  //#endregion
20
- export { buildFetchers, buildFetchersWithSimpleLoader };
19
+ export { CodecksApiError, type DataLoader, type FetchOptions, type LegacyFetchOptions, buildFetchers, buildFetchersFromLoader, buildLegacyFetchers };
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { configuredFetch, ensureMapValue } from "./collection-utils-CYJaA1a9.js";
1
+ import { CodecksApiError, bearerTransport, configuredFetch, ensureMapValue, legacyTransport } from "./collection-utils-T3bx0BS1.js";
2
2
  import { _rootDesc, getRelKey, makeModelQuerySerializable, modelMap, serializeInstanceQuery } from "./query-helpers-iX1fiqLv.js";
3
3
 
4
4
  //#region src/model-pool.ts
@@ -41,6 +41,7 @@ var ModelPool = class {
41
41
  Object.entries(data).forEach(([modelName, payload]) => {
42
42
  if (modelName === _rootDesc.name) this.addModelInstance(modelName, ROOT_ID, payload);
43
43
  else Object.entries(payload).forEach(([id, payload$1]) => {
44
+ if (payload$1 == null) return;
44
45
  this.addModelInstance(modelName, id, payload$1);
45
46
  });
46
47
  });
@@ -53,10 +54,16 @@ var ModelPool = class {
53
54
 
54
55
  //#endregion
55
56
  //#region src/reconcile-query.ts
57
+ /**
58
+ * The API names an id but answers `null` for it when the token may not read that
59
+ * record. Such an id never enters the pool, and the caller can do nothing about it —
60
+ * unlike an id the response never mentioned at all, which points at a bug worth a warning.
61
+ */
62
+ const isWithheldByApi = (response, model, key) => response[model]?.[key] === null;
56
63
  const reconcileInstanceQuery = (query, response, instanceModel, key, store) => {
57
64
  const instance = store.get(instanceModel.name, key);
58
65
  if (!instance) {
59
- console.warn(`no instance found in pool: [${instanceModel.name}, ${key}]`);
66
+ if (!isWithheldByApi(response, instanceModel.name, key)) console.warn(`no instance found in pool: [${instanceModel.name}, ${key}]`);
60
67
  return null;
61
68
  }
62
69
  const result = instanceModel.name === "_root" ? {} : {
@@ -80,7 +87,7 @@ const reconcileInstanceQuery = (query, response, instanceModel, key, store) => {
80
87
  break;
81
88
  case "hasOne":
82
89
  result[`~${relName}`] = instance[relName] != null ? `${instance[relName]}` : null;
83
- result[relName] = reconcileInstanceQuery(relEntry, response, relModel, `${instance[relName]}`, store);
90
+ result[relName] = instance[relName] != null ? reconcileInstanceQuery(relEntry, response, relModel, `${instance[relName]}`, store) : null;
84
91
  break;
85
92
  case "hasMany":
86
93
  const asName = relEntry.as ?? relName;
@@ -94,12 +101,20 @@ const reconcileInstanceQuery = (query, response, instanceModel, key, store) => {
94
101
  }
95
102
  case "first":
96
103
  result[`~${asName}`] = val != null ? `${val}` : null;
97
- result[asName] = reconcileInstanceQuery(relEntry, response, relModel, `${val}`, store);
104
+ result[asName] = val != null ? reconcileInstanceQuery(relEntry, response, relModel, `${val}`, store) : null;
98
105
  break;
99
106
  default: {
100
107
  const ids = val ?? [];
101
- result[`~${asName}`] = ids.map((v) => `${v}`);
102
- result[asName] = ids.map((id) => reconcileInstanceQuery(relEntry, response, relModel, `${id}`, store));
108
+ const members = [];
109
+ for (const id of ids) {
110
+ const instance$1 = reconcileInstanceQuery(relEntry, response, relModel, `${id}`, store);
111
+ if (instance$1 != null) members.push({
112
+ key: `${id}`,
113
+ instance: instance$1
114
+ });
115
+ }
116
+ result[`~${asName}`] = members.map((m) => m.key);
117
+ result[asName] = members.map((m) => m.instance);
103
118
  }
104
119
  }
105
120
  break;
@@ -111,7 +126,7 @@ const reconcileInstanceQuery = (query, response, instanceModel, key, store) => {
111
126
 
112
127
  //#endregion
113
128
  //#region src/loaders/simple-loader.ts
114
- const createSimpleLoader = (opts = {}) => {
129
+ const createSimpleLoader = (opts) => {
115
130
  const fetchWithQuery = async (query) => {
116
131
  return configuredFetch(opts, "", {
117
132
  method: "POST",
@@ -134,11 +149,10 @@ const createSimpleLoader = (opts = {}) => {
134
149
 
135
150
  //#endregion
136
151
  //#region src/index.ts
137
- const buildFetchersWithSimpleLoader = (opts) => {
138
- const loader = createSimpleLoader(opts);
139
- return buildFetchers(loader);
140
- };
141
- const buildFetchers = (loader) => {
152
+ const buildFetchers = (opts) => buildFetchersFromLoader(createSimpleLoader(bearerTransport(opts)));
153
+ /** @deprecated `X-Auth-Token` stops working on 2026-12-31. Use `buildFetchers` with an API token. */
154
+ const buildLegacyFetchers = (opts) => buildFetchersFromLoader(createSimpleLoader(legacyTransport(opts)));
155
+ const buildFetchersFromLoader = (loader) => {
142
156
  return {
143
157
  fetchFromRoot: async (q) => {
144
158
  const res = await loader.fetchModel("_root", [""], { relations: q });
@@ -158,4 +172,4 @@ const buildFetchers = (loader) => {
158
172
  };
159
173
 
160
174
  //#endregion
161
- export { buildFetchers, buildFetchersWithSimpleLoader };
175
+ export { CodecksApiError, buildFetchers, buildFetchersFromLoader, buildLegacyFetchers };
@@ -0,0 +1,42 @@
1
+ import { InferModelQuery, ModelQuery } from "./query-type-BqQ9oX_U.js";
2
+ import { modelMap$1 as modelMap } from "./index-k-KPWV9R.js";
3
+
4
+ //#region src/loaders/loader-utils.d.ts
5
+ type ModelMap = typeof modelMap;
6
+ type DataLoader = {
7
+ fetchModel: <K extends keyof ModelMap, Id extends string, const Q extends ModelQuery<ModelMap[K], ModelMap>>(model: K, id: Id[], q: Q) => Promise<Record<Id, InferModelQuery<ModelMap[K], Q, ModelMap>>>;
8
+ };
9
+ type FetchFunction = (url: string, init?: RequestInit) => Promise<Response>;
10
+ type BaseFetchOptions = {
11
+ fetch?: FetchFunction;
12
+ baseUrl?: string;
13
+ headers?: Record<string, string>;
14
+ /** in milliseconds */
15
+ timeout?: number;
16
+ };
17
+ type FetchOptions = BaseFetchOptions & {
18
+ /** an organization (`cdxat_…`) or personal (`cdxut_…`) API token */
19
+ token: string;
20
+ };
21
+ /** @deprecated `X-Auth-Token` stops working on 2026-12-31. Use `FetchOptions` with an API token. */
22
+ type LegacyFetchOptions = BaseFetchOptions & {
23
+ /** sent as `X-Auth-Token` */
24
+ accessToken?: string;
25
+ /** sent as `X-Account` */
26
+ subdomain?: string;
27
+ };
28
+ /** Fetch options with the auth headers already folded into `headers`. */
29
+
30
+ /** A non-2xx answer. `code`, `path` and the rest of the body follow the API's error format. */
31
+ declare class CodecksApiError extends Error {
32
+ readonly status: number;
33
+ /** e.g. `invalid_token`, `token_expired`, `missing_scope`, `unknown_field`, `rate_limit` */
34
+ readonly code: string | null;
35
+ /** where in the query the error sits, e.g. `_root.account.cards.titel` */
36
+ readonly path: string | null;
37
+ /** the parsed JSON body, or the raw text if it wasn't JSON — holds `hint`, `requiredScope`, … */
38
+ readonly body: unknown;
39
+ constructor(status: number, body: unknown);
40
+ }
41
+ //#endregion
42
+ export { CodecksApiError as CodecksApiError$1, DataLoader, FetchOptions, LegacyFetchOptions };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codecks/fetch",
3
- "version": "0.1.7",
3
+ "version": "1.0.0-rc.0",
4
4
  "description": "Quickly create deeply nested queries for the Codecks API with advanced TypeScript support",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -3,8 +3,8 @@
3
3
  ## Basic Query
4
4
 
5
5
  ```ts
6
- const {fetchFromRoot, fetchInstance} = buildFetchersWithSimpleLoader({
7
- token: "your-api-token",
6
+ const {fetchFromRoot, fetchInstance} = buildFetchers({
7
+ token: "cdxat_…",
8
8
  });
9
9
 
10
10
  // Fetch from root entry points
@@ -1,32 +0,0 @@
1
- //#region src/loaders/loader-utils.ts
2
- const configuredFetch = async (opts, url, init = {}) => {
3
- const fetchImpl = opts.fetch || globalThis.fetch;
4
- const headers = new Headers(init.headers);
5
- if (opts.accessToken) headers.set("X-Auth-Token", opts.accessToken);
6
- if (opts.subdomain) headers.set("X-Account", opts.subdomain);
7
- if (opts.headers) Object.entries(opts.headers).forEach(([key, value]) => {
8
- headers.set(key, value);
9
- });
10
- const fullUrl = opts.baseUrl ? `${opts.baseUrl}${url}` : url;
11
- return fetchImpl(fullUrl, {
12
- ...init,
13
- headers
14
- }).then(async (r) => {
15
- const content = await r.json();
16
- if (r.status !== 200) throw new Error(`[${r.status}] ${JSON.stringify(content)}`);
17
- return content;
18
- });
19
- };
20
-
21
- //#endregion
22
- //#region src/collection-utils.ts
23
- const ensureMapValue = (map, key, fallback) => {
24
- const exist = map.get(key);
25
- if (exist) return exist;
26
- const newValue = fallback();
27
- map.set(key, newValue);
28
- return newValue;
29
- };
30
-
31
- //#endregion
32
- export { configuredFetch, ensureMapValue };
@@ -1,19 +0,0 @@
1
- import { InferModelQuery, ModelQuery } from "./query-type-BqQ9oX_U.js";
2
- import { modelMap$1 as modelMap } from "./index-k-KPWV9R.js";
3
-
4
- //#region src/loaders/loader-utils.d.ts
5
- type ModelMap = typeof modelMap;
6
- type DataLoader = {
7
- fetchModel: <K extends keyof ModelMap, Id extends string, const Q extends ModelQuery<ModelMap[K], ModelMap>>(model: K, id: Id[], q: Q) => Promise<Record<Id, InferModelQuery<ModelMap[K], Q, ModelMap>>>;
8
- };
9
- type FetchFunction = (url: string, init?: RequestInit) => Promise<Response>;
10
- type FetchOptions = {
11
- fetch?: FetchFunction;
12
- accessToken?: string;
13
- subdomain?: string;
14
- baseUrl?: string;
15
- headers?: Record<string, string>;
16
- timeout?: number;
17
- };
18
- //#endregion
19
- export { DataLoader, FetchOptions };