@content-island/vscode-api-client 0.2.3 → 0.2.5

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.d.ts CHANGED
@@ -5,12 +5,30 @@ declare type AllowedFields<M extends Model, Type extends 'sort' | 'filter'> = Pa
5
5
  }, 'fields.id' | 'fields.language'>>;
6
6
 
7
7
  declare interface ApiClient {
8
+ /**
9
+ * Resolves the project. Routed by the client-level mode only (no per-query override): served from
10
+ * the snapshot's `project` block in `'snapshot'` mode, from the API in `'api'` mode.
11
+ */
8
12
  getProject: () => Promise<Project>;
9
13
  getContentList: <M extends Model = Model & Record<string, any>>(queryParam?: ContentListQueryParams<M>) => Promise<M[]>;
10
14
  getContent: <M extends Model = Model & Record<string, any>>(queryParam: ContentQueryParams<M>) => Promise<M>;
11
15
  getRawContentList: <M extends Model = Model & Record<string, any>>(queryParam?: ContentListQueryParams<M>) => Promise<Content[]>;
12
16
  getRawContent: <M extends Model = Model & Record<string, any>>(queryParam: ContentQueryParams<M>) => Promise<Content>;
13
17
  getContentListSize: <M extends Model = Model & Record<string, any>>(queryParam?: ContentListSizeQueryParams<M>) => Promise<number>;
18
+ /**
19
+ * Resolves the snapshot's `meta` block, lazily loading the snapshot regardless of mode (from
20
+ * `snapshotPath`, or `DEFAULT_SNAPSHOT_PATH` when omitted). Rejects with an `ApiClientError` from
21
+ * the loader on a missing/unreadable/invalid file.
22
+ */
23
+ getSnapshotInfo: () => Promise<SnapshotMeta>;
24
+ /**
25
+ * Pulls a fresh snapshot via the configured `snapshotLoader`, validates and guards it, and atomically
26
+ * swaps the active snapshot when the incoming one is newer. Returns `{ status: 'updated', meta }` on
27
+ * adoption or `{ status: 'unchanged', meta }` when the anti-regression guard retains the current one.
28
+ * Throws `ApiClientError` in `'api'` mode, without a configured `snapshotLoader`, or on a
29
+ * loader/validation/identity failure (the active snapshot is left untouched).
30
+ */
31
+ refreshSnapshot: () => Promise<SnapshotRefreshResult>;
14
32
  /**
15
33
  * Updates (or upserts, when selected by `fieldName`+`language`) a field value on an existing content.
16
34
  *
@@ -58,6 +76,8 @@ export declare type ClientFilter<Type = string | boolean | number> = Type | {
58
76
  nin?: Type[];
59
77
  };
60
78
 
79
+ declare type ClientMode = 'api' | 'snapshot';
80
+
61
81
  export declare interface Content {
62
82
  id: string;
63
83
  name: string;
@@ -73,6 +93,17 @@ export declare type ContentListSizeQueryParams<M extends Model = Model & Record<
73
93
 
74
94
  export declare type ContentQueryParams<M extends Model = Model & Record<string, any>> = Omit<QueryParams<M>, 'sort' | 'pagination'>;
75
95
 
96
+ /**
97
+ * The full content snapshot document produced by `GET /project/export` and consumed
98
+ * by the api-client in static mode. `contents` is serialized in the existing API
99
+ * `Content` shape at related-content depth 0.
100
+ */
101
+ declare interface ContentSnapshot {
102
+ meta: SnapshotMeta;
103
+ project: Project;
104
+ contents: Content[];
105
+ }
106
+
76
107
  declare interface ContentTypeField extends Lookup {
77
108
  type: FieldType;
78
109
  tsType: string;
@@ -420,6 +451,26 @@ declare interface Options {
420
451
  secureProtocol?: boolean;
421
452
  apiVersion?: string;
422
453
  metadata?: string;
454
+ /**
455
+ * Client-level mode. `'api'` (default) performs network requests against the B2B API;
456
+ * `'snapshot'` serves reads from a snapshot file loaded from `snapshotPath`.
457
+ */
458
+ mode?: ClientMode;
459
+ /**
460
+ * Path to a content snapshot file (produced by `content-island export`). Optional and cwd-relative;
461
+ * defaults to `DEFAULT_SNAPSHOT_PATH` — the same path the CLI writes by default, so export followed
462
+ * by `createClient({ mode: 'snapshot' })` resolves the same file with zero config. Independent of
463
+ * `mode`: an `'api'` client may still set it to expose `getSnapshotInfo()`. A missing/unreadable/
464
+ * invalid file at the resolved path rejects with an `ApiClientError` from the loader.
465
+ */
466
+ snapshotPath?: string;
467
+ /**
468
+ * Optional loader for a refreshable snapshot in `'snapshot'` mode. When set without `snapshotPath`
469
+ * (loader-only), the client resolves the active snapshot via the loader on first read. Required for
470
+ * `refreshSnapshot()`; calling `refreshSnapshot()` without it throws an `ApiClientError`. A
471
+ * `snapshotPath`-only client (no loader) keeps the static 0.1.0 behavior.
472
+ */
473
+ snapshotLoader?: SnapshotLoader;
423
474
  }
424
475
 
425
476
  declare type Pagination = {
@@ -437,6 +488,26 @@ export declare interface Project {
437
488
  declare type QueryParams<M extends Model = Model & Record<string, any>> = FilterableFields<M> & {
438
489
  sort?: SortableFields<M>;
439
490
  pagination?: Pagination;
491
+ /**
492
+ * Per-query mode override (effective mode = per-query `mode` ?? client-level `mode` ?? `'api'`).
493
+ * Client-only key — never serialized into the query string.
494
+ */
495
+ mode?: ClientMode;
496
+ /**
497
+ * Called exactly once with the related-content resolution metadata; when omitted, behavior and
498
+ * return shapes are unchanged. Client-only key — never serialized into the query string.
499
+ */
500
+ onRelatedContentMeta?: (meta: RelatedContentMeta) => void;
501
+ };
502
+
503
+ /**
504
+ * Related-content resolution metadata for a read: how deep the BFS resolved (`resolvedDepth`) and
505
+ * whether a depth/budget cap left the graph `partial`. Identical values in both modes for the same
506
+ * data and query (header-sourced in api mode, engine-sourced in snapshot mode).
507
+ */
508
+ declare type RelatedContentMeta = {
509
+ resolvedDepth: number;
510
+ partial: boolean;
440
511
  };
441
512
 
442
513
  declare type RelatedModelType = `${string}|${string}`;
@@ -446,6 +517,32 @@ declare interface SaveModelResponse {
446
517
  id: string;
447
518
  }
448
519
 
520
+ /**
521
+ * User-provided fetch for a refreshable snapshot. Opaque to the core: it may read a blob/S3/file or
522
+ * call `exportSnapshot()`. Returns either raw snapshot JSON text (which the client `JSON.parse`s) or
523
+ * an already-parsed `ContentSnapshot` object (passed through as-is). Invoked only on the first read in
524
+ * the loader-only case and on `refreshSnapshot()` — never during `createClient` construction.
525
+ */
526
+ declare type SnapshotLoader = () => Promise<string | ContentSnapshot>;
527
+
528
+ declare interface SnapshotMeta {
529
+ schemaVersion: number;
530
+ /** ISO-8601 timestamp of when the snapshot was generated on the server. */
531
+ exportedAt: string;
532
+ projectId: string;
533
+ view: 'published' | 'preview';
534
+ }
535
+
536
+ /**
537
+ * Outcome of a `refreshSnapshot()` call. `'updated'` when the incoming snapshot was adopted as the new
538
+ * active snapshot (its `meta` is returned); `'unchanged'` when the anti-regression guard kept the
539
+ * current active snapshot (the retained snapshot's `meta` is returned).
540
+ */
541
+ declare type SnapshotRefreshResult = {
542
+ status: 'updated' | 'unchanged';
543
+ meta: SnapshotMeta;
544
+ };
545
+
449
546
  declare type SortableFields<M extends Model> = {
450
547
  contentType?: SortOrder;
451
548
  lastUpdate?: SortOrder;
@@ -466,7 +563,7 @@ declare interface UploadMediaParams {
466
563
 
467
564
  declare type Validation = { name: string; customArgs?: any };
468
565
 
469
- export declare interface VSCodeApiClient extends Omit<ApiClient, 'createModel' | 'updateModel' | 'deleteModel' | 'createEnum' | 'updateEnum' | 'deleteEnum'> {
566
+ export declare interface VSCodeApiClient extends Omit<ApiClient, 'createModel' | 'updateModel' | 'deleteModel' | 'createEnum' | 'updateEnum' | 'deleteEnum' | 'refreshSnapshot'> {
470
567
  setVSCodeExtensionContext: (context: vscode.ExtensionContext) => void;
471
568
  authorize: (authorizationCode: string, metadata: string) => Promise<void>;
472
569
  authorizeByProjectId: (projectId: string) => Promise<void>;
package/dist/index.js CHANGED
@@ -2,52 +2,52 @@ import { isApiClientError as I, createClient as A } from "@content-island/api-cl
2
2
  import { mapContentToModel as z } from "@content-island/api-client";
3
3
  import * as c from "vscode";
4
4
  import _ from "node:crypto";
5
- import P from "node:util";
6
- let g;
7
- const S = () => {
8
- if (!g)
5
+ import S from "node:util";
6
+ let C;
7
+ const P = () => {
8
+ if (!C)
9
9
  throw new Error("Extension context has not been set.");
10
- return g;
11
- }, L = (t) => {
12
- g = t;
10
+ return C;
11
+ }, f = (t) => {
12
+ C = t;
13
13
  };
14
14
  let l = {
15
- getContext: S,
16
- setContext: L
15
+ getContext: P,
16
+ setContext: f
17
17
  };
18
18
  const d = {
19
19
  IS_PRODUCTION: !0,
20
20
  DEFAULT_API_CLIENT_DOMAIN: "api.contentisland.net",
21
21
  DEFAULT_API_CLIENT_VERSION: "1.0",
22
22
  DEFAULT_LOGIN_DOMAIN: "app.contentisland.net"
23
- }, C = "content-island-vscode", u = {
24
- SALT: `${C}.salt`,
25
- ACCESS_TOKEN_BY_PROJECT_ID: (t) => `${C}.access-token.${t}`,
26
- METADATA_BY_PROJECT_ID: (t) => `${C}.metadata.${t}`
27
- }, E = {
23
+ }, w = "content-island-vscode", u = {
24
+ SALT: `${w}.salt`,
25
+ ACCESS_TOKEN_BY_PROJECT_ID: (t) => `${w}.access-token.${t}`,
26
+ METADATA_BY_PROJECT_ID: (t) => `${w}.metadata.${t}`
27
+ }, h = {
28
28
  get: async (t) => await l.getContext().secrets.get(u.METADATA_BY_PROJECT_ID(t)),
29
29
  set: async (t, o) => {
30
30
  await l.getContext().secrets.store(u.METADATA_BY_PROJECT_ID(t), o);
31
31
  }
32
- }, h = {
32
+ }, E = {
33
33
  get: async (t) => await l.getContext().secrets.get(u.ACCESS_TOKEN_BY_PROJECT_ID(t)),
34
34
  set: async (t, o) => {
35
35
  await l.getContext().secrets.store(u.ACCESS_TOKEN_BY_PROJECT_ID(t), o);
36
36
  }
37
- }, f = (t) => {
38
- const s = (t.secureProtocol === void 0 ? d.IS_PRODUCTION : t.secureProtocol) ? "https" : "http", e = t.domain ? t.domain : d.DEFAULT_API_CLIENT_DOMAIN, n = t.apiVersion ? t.apiVersion : d.DEFAULT_API_CLIENT_VERSION;
39
- return `${s}://${e}/api/${n}`;
37
+ }, L = (t) => {
38
+ const a = (t.secureProtocol === void 0 ? d.IS_PRODUCTION : t.secureProtocol) ? "https" : "http", e = t.domain ? t.domain : d.DEFAULT_API_CLIENT_DOMAIN, n = t.apiVersion ? t.apiVersion : d.DEFAULT_API_CLIENT_VERSION;
39
+ return `${a}://${e}/api/${n}`;
40
40
  }, p = (t) => {
41
- const s = (t.secureProtocol === void 0 ? d.IS_PRODUCTION : t.secureProtocol) ? "https" : "http", e = t.loginDomain ? t.loginDomain : d.DEFAULT_LOGIN_DOMAIN;
42
- return `${s}://${e}/#/?redirect=vscode`;
43
- }, y = 16, O = (t = y) => _.randomBytes(t).toString("hex"), x = 64, D = "sha512", m = 1e5, N = async (t, o, s = x) => (await P.promisify(_.pbkdf2)(t, o, m, s, D)).toString("hex"), k = async () => {
41
+ const a = (t.secureProtocol === void 0 ? d.IS_PRODUCTION : t.secureProtocol) ? "https" : "http", e = t.loginDomain ? t.loginDomain : d.DEFAULT_LOGIN_DOMAIN;
42
+ return `${a}://${e}/#/?redirect=vscode`;
43
+ }, y = 16, O = (t = y) => _.randomBytes(t).toString("hex"), x = 64, D = "sha512", m = 1e5, N = async (t, o, a = x) => (await S.promisify(_.pbkdf2)(t, o, m, a, D)).toString("hex"), k = async () => {
44
44
  const t = l.getContext();
45
45
  let o = await t.secrets.get(u.SALT);
46
46
  return o || (o = O(32), await t.secrets.store(u.SALT, o)), o;
47
47
  }, M = 32, R = async (t) => {
48
48
  const o = await k();
49
49
  return await N(t, o, M);
50
- }, w = async (t) => {
50
+ }, g = async (t) => {
51
51
  const o = "Open Login Page";
52
52
  if (await c.window.showInformationMessage(
53
53
  "You need to log in to Content Island to continue. Do you want to open the login page?",
@@ -56,23 +56,23 @@ const d = {
56
56
  return;
57
57
  const e = c.Uri.parse(p(t));
58
58
  await c.env.openExternal(e);
59
- }, r = (t, o) => async (...s) => {
59
+ }, s = (t, o) => async (...a) => {
60
60
  try {
61
- return await t(...s);
61
+ return await t(...a);
62
62
  } catch (e) {
63
63
  if (I(e)) {
64
64
  if (e.status === 401)
65
- throw await w(o), new Error("Unauthorized: Please log in to continue.");
65
+ throw await g(o), new Error("Unauthorized: Please log in to continue.");
66
66
  e.status === 403 && c.window.showErrorMessage("Access forbidden: You do not have permission to perform this action.");
67
67
  }
68
68
  throw e;
69
69
  }
70
- }, U = async (t, o, s) => {
71
- const e = f(t);
70
+ }, U = async (t, o, a) => {
71
+ const e = L(t);
72
72
  let n;
73
- const a = {
73
+ const r = {
74
74
  authorizationCode: o,
75
- metadata: s
75
+ metadata: a
76
76
  };
77
77
  try {
78
78
  n = await fetch(`${e}/security/vscode/token`, {
@@ -80,7 +80,7 @@ const d = {
80
80
  headers: {
81
81
  "Content-Type": "application/json"
82
82
  },
83
- body: JSON.stringify(a)
83
+ body: JSON.stringify(r)
84
84
  });
85
85
  } catch {
86
86
  c.window.showErrorMessage(
@@ -89,7 +89,7 @@ const d = {
89
89
  return;
90
90
  }
91
91
  if (!n.ok) {
92
- n.status === 401 ? await w(t) : n.status === 403 && c.window.showErrorMessage(
92
+ n.status === 401 ? await g(t) : n.status === 403 && c.window.showErrorMessage(
93
93
  "You do not have permission to access this resource. Please check your access rights in Content Island."
94
94
  ), c.window.showErrorMessage(
95
95
  "Failed to obtain access token. Please complete the authorization in Content Island."
@@ -98,16 +98,16 @@ const d = {
98
98
  }
99
99
  try {
100
100
  const i = await n.json();
101
- return (!i?.accessToken || typeof i.accessToken != "string") && (c.window.showErrorMessage("Invalid response from the authorization server."), await w(t)), i.accessToken;
101
+ return (!i?.accessToken || typeof i.accessToken != "string") && (c.window.showErrorMessage("Invalid response from the authorization server."), await g(t)), i.accessToken;
102
102
  } catch {
103
103
  c.window.showErrorMessage("Error processing response from the authorization server.");
104
104
  }
105
105
  }, T = (t) => {
106
- const o = `PREVIEW_${t.accessToken}`, s = A({ ...t, accessToken: o });
106
+ const o = `PREVIEW_${t.accessToken}`, a = A({ ...t, accessToken: o });
107
107
  return {
108
- ...s,
108
+ ...a,
109
109
  getProject: async () => {
110
- const e = await s.getProject();
110
+ const e = await a.getProject();
111
111
  return {
112
112
  ...e,
113
113
  id: await R(e.id)
@@ -116,42 +116,43 @@ const d = {
116
116
  };
117
117
  }, B = (t = {}) => {
118
118
  let o = T({ ...t, accessToken: "" });
119
- const s = (e, n) => {
119
+ const a = (e, n) => {
120
120
  o = T({ ...t, accessToken: e, metadata: n });
121
121
  };
122
122
  return {
123
123
  authorize: async (e, n) => {
124
- const a = await U(t, e, n);
125
- if (a) {
126
- s(a, n);
124
+ const r = await U(t, e, n);
125
+ if (r) {
126
+ a(r, n);
127
127
  const i = await o.getProject();
128
- await h.set(i.id, a), await E.set(i.id, n);
128
+ await E.set(i.id, r), await h.set(i.id, n);
129
129
  }
130
130
  },
131
131
  authorizeByProjectId: async (e) => {
132
- const n = await h.get(e), a = await E.get(e);
133
- if (!n || !a) {
134
- await w(t);
132
+ const n = await E.get(e), r = await h.get(e);
133
+ if (!n || !r) {
134
+ await g(t);
135
135
  return;
136
136
  }
137
- s(n, a);
137
+ a(n, r);
138
138
  },
139
139
  setVSCodeExtensionContext: (e) => {
140
140
  l.setContext(e);
141
141
  },
142
- getProject: (...e) => r(() => o.getProject(...e), t)(),
143
- getContentList: (...e) => r(() => o.getContentList(...e), t)(),
144
- getContent: (...e) => r(() => o.getContent(...e), t)(),
145
- getRawContentList: (...e) => r(() => o.getRawContentList(...e), t)(),
146
- getRawContent: (...e) => r(() => o.getRawContent(...e), t)(),
147
- getContentListSize: (...e) => r(() => o.getContentListSize(...e), t)(),
148
- updateContentFieldValue: (e, n, a) => r(
149
- () => o.updateContentFieldValue(e, n, a),
142
+ getProject: (...e) => s(() => o.getProject(...e), t)(),
143
+ getContentList: (...e) => s(() => o.getContentList(...e), t)(),
144
+ getContent: (...e) => s(() => o.getContent(...e), t)(),
145
+ getRawContentList: (...e) => s(() => o.getRawContentList(...e), t)(),
146
+ getRawContent: (...e) => s(() => o.getRawContent(...e), t)(),
147
+ getContentListSize: (...e) => s(() => o.getContentListSize(...e), t)(),
148
+ getSnapshotInfo: (...e) => s(() => o.getSnapshotInfo(...e), t)(),
149
+ updateContentFieldValue: (e, n, r) => s(
150
+ () => o.updateContentFieldValue(e, n, r),
150
151
  t
151
152
  )(),
152
- uploadMedia: (...e) => r(() => o.uploadMedia(...e), t)(),
153
- createContent: (...e) => r(() => o.createContent(...e), t)(),
154
- publishContent: (...e) => r(() => o.publishContent(...e), t)()
153
+ uploadMedia: (...e) => s(() => o.uploadMedia(...e), t)(),
154
+ createContent: (...e) => s(() => o.createContent(...e), t)(),
155
+ publishContent: (...e) => s(() => o.publishContent(...e), t)()
155
156
  };
156
157
  };
157
158
  export {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@content-island/vscode-api-client",
3
- "version": "0.2.3",
3
+ "version": "0.2.5",
4
4
  "description": "Content Island - VSCode Extension API Client",
5
5
  "private": false,
6
6
  "sideEffects": false,
@@ -32,7 +32,7 @@
32
32
  "test:watch": "vitest -c ./config/test/config.ts"
33
33
  },
34
34
  "dependencies": {
35
- "@content-island/api-client": "0.22.0"
35
+ "@content-island/api-client": "0.24.0"
36
36
  },
37
37
  "devDependencies": {
38
38
  "@content-island/common-backend": "*",