@protobase/client 0.12.2 → 0.12.3

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/client.d.ts CHANGED
@@ -36,6 +36,10 @@ export type UploadOptions = {
36
36
  onProgress?: UploadProgress;
37
37
  signal?: AbortSignal;
38
38
  };
39
+ /**
40
+ * The client of one API. With an absolute `baseUrl`, the URL of each file in a record it answers is absolute too, on
41
+ * that API's origin; with a path, it stays the path the API answers.
42
+ */
39
43
  export declare const createClient: (options?: ClientOptions) => {
40
44
  list: <R extends ResourceRef>(resource: R, params?: ListParams) => Promise<ListPage<RecordOf<R>>>;
41
45
  search: <R extends ResourceRef>(resource: R, params?: ListParams) => Promise<ListPage<RecordOf<R>>>;
@@ -64,7 +68,9 @@ export declare const createClient: (options?: ClientOptions) => {
64
68
  seek: <R extends ResourceRef>(resource: R, params: ListParams & {
65
69
  position: number;
66
70
  }) => Promise<ListPage<RecordOf<R>>>;
67
- meta: (etag?: string) => Promise<MetaResult>;
71
+ meta: (etag?: string, { signal }?: {
72
+ signal?: AbortSignal;
73
+ }) => Promise<MetaResult>;
68
74
  invoke: <T = unknown>(name: string, { method, body, path, query, headers }?: InvokeOptions) => Promise<T>;
69
75
  upload: (resource: ResourceRef, field: string, file: Blob, { onProgress, signal }?: UploadOptions) => Promise<UploadResult>;
70
76
  };
package/dist/client.js CHANGED
@@ -1,12 +1,13 @@
1
1
  import { filterText, listWire, queryString } from "./query-string.js";
2
+ import { originOf, withFileOrigin } from "./file-urls.js";
2
3
  import { createTransport } from "./transport.js";
3
4
  import { fetchWithProgress } from "./upload-request.js";
4
5
  import { encodeKey } from "@protobase/schema";
5
6
  //#region src/client.ts
6
7
  const nameOf = (resource) => typeof resource === "string" ? resource : resource.toModel().name;
7
8
  const keyPath = (resource, key) => `/${nameOf(resource)}/${encodeKey(key)}`;
8
- const toPage = (body, response) => ({
9
- items: body.items,
9
+ const toPage = (body, response, origin) => ({
10
+ items: body.items.map((item) => withFileOrigin(item, origin)),
10
11
  nextPageToken: body.next_page_token,
11
12
  totalSizeEstimate: body.total_size_estimate,
12
13
  ...body.prev_page_token !== void 0 && { prevPageToken: body.prev_page_token },
@@ -15,23 +16,42 @@ const toPage = (body, response) => ({
15
16
  });
16
17
  const etagOf = (response) => response.headers.get("etag") ?? "";
17
18
  /** A single-record response: the record without its `permissions` member, which travels beside it. */
18
- const storedOf = async (response) => {
19
+ const storedOf = async (response, origin) => {
19
20
  const { permissions, ...record } = await response.json();
20
21
  return {
21
- record,
22
+ record: withFileOrigin(record, origin),
22
23
  etag: etagOf(response),
23
24
  ...permissions && { permissions }
24
25
  };
25
26
  };
27
+ const batchResultOf = (body, origin) => ({ results: (body.results ?? []).map((result) => {
28
+ if (result.op === "create" || result.op === "update") return {
29
+ ...result,
30
+ record: withFileOrigin(result.record, origin)
31
+ };
32
+ if (result.op === "reorder") return {
33
+ ...result,
34
+ records: result.records.map((entry) => ({
35
+ ...entry,
36
+ record: withFileOrigin(entry.record, origin)
37
+ }))
38
+ };
39
+ return result;
40
+ }) });
41
+ /**
42
+ * The client of one API. With an absolute `baseUrl`, the URL of each file in a record it answers is absolute too, on
43
+ * that API's origin; with a path, it stays the path the API answers.
44
+ */
26
45
  const createClient = (options = {}) => {
27
46
  const send = createTransport(options);
47
+ const origin = originOf(options.baseUrl);
28
48
  const list = async (resource, params) => {
29
49
  const response = await send({
30
50
  method: "GET",
31
51
  path: `/${nameOf(resource)}`,
32
52
  query: queryString(listWire(params))
33
53
  });
34
- return toPage(await response.json(), response);
54
+ return toPage(await response.json(), response, origin);
35
55
  };
36
56
  /** Same as `list`, with the parameters in a JSON body, for long filters. */
37
57
  const search = async (resource, params) => {
@@ -44,13 +64,13 @@ const createClient = (options = {}) => {
44
64
  fields: params?.fields
45
65
  }
46
66
  });
47
- return toPage(await response.json(), response);
67
+ return toPage(await response.json(), response, origin);
48
68
  };
49
69
  const get = async (resource, key) => {
50
70
  return storedOf(await send({
51
71
  method: "GET",
52
72
  path: keyPath(resource, key)
53
- }));
73
+ }), origin);
54
74
  };
55
75
  const create = async (resource, body) => {
56
76
  const response = await send({
@@ -59,7 +79,7 @@ const createClient = (options = {}) => {
59
79
  body
60
80
  });
61
81
  return {
62
- record: await response.json(),
82
+ record: withFileOrigin(await response.json(), origin),
63
83
  etag: etagOf(response),
64
84
  location: response.headers.get("location") ?? ""
65
85
  };
@@ -71,7 +91,7 @@ const createClient = (options = {}) => {
71
91
  path: keyPath(resource, key),
72
92
  body: patch,
73
93
  headers: { "if-match": etag }
74
- }));
94
+ }), origin);
75
95
  };
76
96
  /** `etag` is the version the user saw (`*` for any); a stale one rejects with `PreconditionFailedError`. */
77
97
  const remove = async (resource, key, { etag } = {}) => {
@@ -88,7 +108,7 @@ const createClient = (options = {}) => {
88
108
  path: `${keyPath(resource, key)}:undelete`
89
109
  });
90
110
  return {
91
- record: await response.json(),
111
+ record: withFileOrigin(await response.json(), origin),
92
112
  etag: etagOf(response)
93
113
  };
94
114
  };
@@ -109,11 +129,12 @@ const createClient = (options = {}) => {
109
129
  ..."key" in operation && { key: keyText(operation.key) },
110
130
  ...operation.op === "reorder" && { keys: operation.keys.map(keyText) }
111
131
  }));
112
- return { results: (await (await send({
132
+ const response = await send({
113
133
  method: "POST",
114
134
  path: ":batchWrite",
115
135
  body: { ops: wireOps }
116
- })).json()).results ?? [] };
136
+ });
137
+ return batchResultOf(await response.json(), origin);
117
138
  };
118
139
  const facets = async (resource, field, o = {}) => {
119
140
  return (await (await send({
@@ -168,16 +189,17 @@ const createClient = (options = {}) => {
168
189
  position: params.position
169
190
  })
170
191
  });
171
- return toPage(await response.json(), response);
192
+ return toPage(await response.json(), response, origin);
172
193
  };
173
- /** Pass the `etag` of the last result to get `{ status: 'unchanged' }` on a `304`. */
174
- const meta = async (etag) => {
194
+ /** Pass the `etag` of the last result to get `{ status: 'unchanged' }` on a `304`; `signal` gives up waiting. */
195
+ const meta = async (etag, { signal } = {}) => {
175
196
  const response = await send({
176
197
  method: "GET",
177
198
  path: "/meta",
178
199
  system: true,
179
200
  headers: etag ? { "if-none-match": etag } : void 0,
180
- accept: [304]
201
+ accept: [304],
202
+ ...signal && { signal }
181
203
  });
182
204
  if (response.status === 304) return { status: "unchanged" };
183
205
  return {
@@ -0,0 +1,21 @@
1
+ //#region src/file-urls.ts
2
+ const isFileAtPath = (value) => {
3
+ if (typeof value !== "object" || value === null) return false;
4
+ const { uri, url } = value;
5
+ return typeof uri === "string" && typeof url === "string" && url.startsWith("/") && !url.startsWith("//");
6
+ };
7
+ /** The origin of an absolute `baseUrl`; `undefined` for a path on the page's own origin. */
8
+ const originOf = (baseUrl) => baseUrl !== void 0 && URL.canParse(baseUrl) ? new URL(baseUrl).origin : void 0;
9
+ /**
10
+ * Makes each file of a record download from `origin`: the API answers a file's URL as a path on its own origin, which a
11
+ * page on another origin, or code outside a page, cannot follow. Without an origin the record is returned as it is.
12
+ */
13
+ const withFileOrigin = (record, origin) => {
14
+ if (origin === void 0 || typeof record !== "object" || record === null) return record;
15
+ return Object.fromEntries(Object.entries(record).map(([name, value]) => [name, isFileAtPath(value) ? {
16
+ ...value,
17
+ url: `${origin}${value.url}`
18
+ } : value]));
19
+ };
20
+ //#endregion
21
+ export { originOf, withFileOrigin };
package/dist/index.d.ts CHANGED
@@ -1,9 +1,11 @@
1
1
  import { FilterInput, ListParams, filterText, listWire } from "./query-string.js";
2
2
  import { ClientOptions } from "./transport.js";
3
3
  import { UploadProgress } from "./upload-request.js";
4
- import { BatchOp, BatchRef, BatchResult, Facet, Histogram, HistogramBucket, ListPage, Meta, MetaResult, PluginRef, RecordOf, RecordPermissions, ResourcePermissions, ResourceRef, Series, SeriesParams, SeriesPoint, Stored, StoredFile } from "./types.js";
4
+ import { BatchOp, BatchRef, BatchResult, Facet, Histogram, HistogramBucket, ListPage, Meta, MetaResult, PluginRef, RecordOf, RecordPermissions, ResourcePermissions, ResourceRef, Series, SeriesParams, SeriesPoint, SourceRef, Stored, StoredFile } from "./types.js";
5
5
  import { Client, InvokeOptions, UploadOptions, UploadResult, createClient } from "./client.js";
6
6
  import { ApiError, PreconditionFailedError, ProblemDetails, ProblemError, isApiError } from "./problem.js";
7
+ import { routedClient } from "./routed-client.js";
8
+ import { SourceMeta, sourceMeta } from "./source-meta.js";
7
9
  import { SignInPolicy, SignInPolicyClient, SignInPolicyState, SignInRule, StaffAccessRule } from "./sign-in-policy.js";
8
10
  import { AccountSecurity, AccountSignIn, AuthenticatorSetup, Passkey, RequiredSetup } from "./account-security.js";
9
11
  import { CurrentOrganization, InvitationPreview, InvitationSent, Organization, OrganizationInvitation, OrganizationMember, OrganizationRole, OrganizationsClient } from "./organizations.js";
@@ -15,4 +17,4 @@ import { AuthSession, AuthSessionOptions, createAuthSession, tokenExpiry } from
15
17
  import { createStaticSession } from "./static-session.js";
16
18
  import { AssistantClient, AssistantClientOptions, AssistantConnection, createAssistantClient } from "./assistant.js";
17
19
  import { RuntimeClient, RuntimeClientOptions, RuntimePolicy, RuntimeStatus, createRuntimeClient } from "./runtime.js";
18
- export { type AccountSecurity, type AccountSignIn, type ActiveRole, ApiError, type AssistantClient, type AssistantClientOptions, type AssistantConnection, AuthError, type AuthSession, type AuthSessionOptions, type AuthUser, type AuthenticatorSetup, type BatchOp, type BatchRef, type BatchResult, type Client, type ClientOptions, type CurrentOrganization, type Facet, type FilterInput, type Histogram, type HistogramBucket, type InvitationPreview, type InvitationSent, type InvokeOptions, type ListPage, type ListParams, type Meta, type MetaResult, type Organization, type OrganizationInvitation, type OrganizationMember, type OrganizationRole, type OrganizationsClient, type Passkey, type PlatformSignIn, type PluginRef, PreconditionFailedError, type ProblemDetails, type ProblemError, type RecordOf, type RecordPermissions, type RequiredSetup, type ResourcePermissions, type ResourceRef, type RoleLabel, type RoleOption, type RolesClient, type RuntimeClient, type RuntimeClientOptions, type RuntimePolicy, type RuntimeStatus, type Series, type SeriesParams, type SeriesPoint, type SetupStatus, type SignInMethod, type SignInPolicy, type SignInPolicyClient, type SignInPolicyState, type SignInResult, type SignInRule, type StaffAccessRule, type StaffSignIn, type StaffSignInClient, type Stored, type StoredFile, type TwoFactorMethod, type UploadOptions, type UploadProgress, type UploadResult, createAssistantClient, createAuthSession, createClient, createRuntimeClient, createStaticSession, filterText, isApiError, listWire, tokenExpiry };
20
+ export { type AccountSecurity, type AccountSignIn, type ActiveRole, ApiError, type AssistantClient, type AssistantClientOptions, type AssistantConnection, AuthError, type AuthSession, type AuthSessionOptions, type AuthUser, type AuthenticatorSetup, type BatchOp, type BatchRef, type BatchResult, type Client, type ClientOptions, type CurrentOrganization, type Facet, type FilterInput, type Histogram, type HistogramBucket, type InvitationPreview, type InvitationSent, type InvokeOptions, type ListPage, type ListParams, type Meta, type MetaResult, type Organization, type OrganizationInvitation, type OrganizationMember, type OrganizationRole, type OrganizationsClient, type Passkey, type PlatformSignIn, type PluginRef, PreconditionFailedError, type ProblemDetails, type ProblemError, type RecordOf, type RecordPermissions, type RequiredSetup, type ResourcePermissions, type ResourceRef, type RoleLabel, type RoleOption, type RolesClient, type RuntimeClient, type RuntimeClientOptions, type RuntimePolicy, type RuntimeStatus, type Series, type SeriesParams, type SeriesPoint, type SetupStatus, type SignInMethod, type SignInPolicy, type SignInPolicyClient, type SignInPolicyState, type SignInResult, type SignInRule, type SourceMeta, type SourceRef, type StaffAccessRule, type StaffSignIn, type StaffSignInClient, type Stored, type StoredFile, type TwoFactorMethod, type UploadOptions, type UploadProgress, type UploadResult, createAssistantClient, createAuthSession, createClient, createRuntimeClient, createStaticSession, filterText, isApiError, listWire, routedClient, sourceMeta, tokenExpiry };
package/dist/index.js CHANGED
@@ -1,9 +1,11 @@
1
1
  import { filterText, listWire } from "./query-string.js";
2
2
  import { ApiError, PreconditionFailedError, isApiError } from "./problem.js";
3
3
  import { createClient } from "./client.js";
4
+ import { routedClient } from "./routed-client.js";
5
+ import { sourceMeta } from "./source-meta.js";
4
6
  import { AuthError } from "./auth-error.js";
5
7
  import { createAuthSession, tokenExpiry } from "./auth.js";
6
8
  import { createStaticSession } from "./static-session.js";
7
9
  import { createAssistantClient } from "./assistant.js";
8
10
  import { createRuntimeClient } from "./runtime.js";
9
- export { ApiError, AuthError, PreconditionFailedError, createAssistantClient, createAuthSession, createClient, createRuntimeClient, createStaticSession, filterText, isApiError, listWire, tokenExpiry };
11
+ export { ApiError, AuthError, PreconditionFailedError, createAssistantClient, createAuthSession, createClient, createRuntimeClient, createStaticSession, filterText, isApiError, listWire, routedClient, sourceMeta, tokenExpiry };
@@ -0,0 +1,10 @@
1
+ import { Client } from "./client.js";
2
+ //#region src/routed-client.d.ts
3
+ /**
4
+ * One client over the app's own API and the sources it draws resources from: a call about a resource goes to the API
5
+ * that serves it (`sourceOf` gives a source's client, or `undefined` for the app's own), and `meta` and `invoke` go to
6
+ * the app's own. A batch goes to the one API all its writes are for; one that writes to two cannot be atomic, so it is
7
+ * refused before anything is sent.
8
+ */
9
+ export declare const routedClient: (own: Client, sourceOf: (resource: string) => Client | undefined) => Client;
10
+ //#endregion
@@ -0,0 +1,35 @@
1
+ //#region src/routed-client.ts
2
+ const nameOf = (resource) => typeof resource === "string" ? resource : resource.toModel().name;
3
+ /**
4
+ * One client over the app's own API and the sources it draws resources from: a call about a resource goes to the API
5
+ * that serves it (`sourceOf` gives a source's client, or `undefined` for the app's own), and `meta` and `invoke` go to
6
+ * the app's own. A batch goes to the one API all its writes are for; one that writes to two cannot be atomic, so it is
7
+ * refused before anything is sent.
8
+ */
9
+ const routedClient = (own, sourceOf) => {
10
+ const to = (resource) => sourceOf(nameOf(resource)) ?? own;
11
+ return {
12
+ list: (resource, params) => to(resource).list(resource, params),
13
+ search: (resource, params) => to(resource).search(resource, params),
14
+ get: (resource, key) => to(resource).get(resource, key),
15
+ create: (resource, body) => to(resource).create(resource, body),
16
+ update: (resource, key, patch, etag) => to(resource).update(resource, key, patch, etag),
17
+ remove: (resource, key, options) => to(resource).remove(resource, key, options),
18
+ undelete: (resource, key) => to(resource).undelete(resource, key),
19
+ reveal: (resource, key, field) => to(resource).reveal(resource, key, field),
20
+ batchWrite: (ops) => {
21
+ const apis = [...new Set(ops.map((op) => to(op.resource)))];
22
+ if (apis.length > 1) return Promise.reject(/* @__PURE__ */ new Error(`A batch writes to one API, but these writes are for ${[...new Set(ops.map((op) => nameOf(op.resource)))].join(", ")}, which different APIs serve`));
23
+ return (apis[0] ?? own).batchWrite(ops);
24
+ },
25
+ facets: (resource, field, options) => to(resource).facets(resource, field, options),
26
+ series: (resource, params) => to(resource).series(resource, params),
27
+ histogram: (resource, field, options) => to(resource).histogram(resource, field, options),
28
+ seek: (resource, params) => to(resource).seek(resource, params),
29
+ meta: (etag, options) => own.meta(etag, options),
30
+ invoke: (name, options) => own.invoke(name, options),
31
+ upload: (resource, field, file, options) => to(resource).upload(resource, field, file, options)
32
+ };
33
+ };
34
+ //#endregion
35
+ export { routedClient };
@@ -0,0 +1,16 @@
1
+ import { Meta, ResourcePermissions, SourceRef } from "./types.js";
2
+ import { ResourceModel, ViewModel } from "@protobase/schema";
3
+ //#region src/source-meta.d.ts
4
+ /** What the app draws of a source: the resources it lists that the user may see there, their views and what the user may do. */
5
+ export type SourceMeta = {
6
+ resources: ResourceModel[];
7
+ views: ViewModel[];
8
+ permissions: Record<string, ResourcePermissions>;
9
+ };
10
+ /**
11
+ * The part of a source's `/meta` the app draws: the resources `source` lists, of those the source names to the user,
12
+ * with their views and permissions. Nothing in them leads out of the source: a relation to a resource the app does not
13
+ * draw from it shows its key, and related records of one, columns through one and links to one or to a page go.
14
+ */
15
+ export declare const sourceMeta: (meta: Meta, source: SourceRef) => SourceMeta;
16
+ //#endregion
@@ -0,0 +1,49 @@
1
+ import { addressErrors } from "@protobase/schema";
2
+ import { resolveFieldPath } from "@protobase/layout";
3
+ //#region src/source-meta.ts
4
+ const withinSource = (model, drawn) => ({
5
+ ...model,
6
+ fields: Object.fromEntries(Object.entries(model.fields).map(([name, field]) => {
7
+ if (!field.relation || Object.hasOwn(drawn, field.relation.resource)) return [name, field];
8
+ const { relation: _outside, ...plain } = field;
9
+ return [name, plain];
10
+ }))
11
+ });
12
+ const keptLayoutItem = (item, models) => {
13
+ if (item.kind !== "related") return [item];
14
+ if (!Object.hasOwn(models, item.resource) || item.through && !Object.hasOwn(models, item.through.resource)) return [];
15
+ const { columns, ...rest } = item;
16
+ const kept = columns?.filter((column) => resolveFieldPath(models, item.resource, column).ok);
17
+ return [kept && kept.length > 0 ? {
18
+ ...rest,
19
+ columns: kept
20
+ } : rest];
21
+ };
22
+ const keptAction = (action, model, models) => action.run?.kind !== "link" || !("to" in action.run) || addressErrors(action.run.to, {
23
+ models,
24
+ pages: /* @__PURE__ */ new Set(),
25
+ source: model
26
+ }).length === 0;
27
+ const withinSourceView = (view, models) => ({
28
+ ...view,
29
+ layout: view.layout.flatMap((item) => keptLayoutItem(item, models)),
30
+ actions: view.actions.filter((action) => keptAction(action, models[view.resource], models))
31
+ });
32
+ /**
33
+ * The part of a source's `/meta` the app draws: the resources `source` lists, of those the source names to the user,
34
+ * with their views and permissions. Nothing in them leads out of the source: a relation to a resource the app does not
35
+ * draw from it shows its key, and related records of one, columns through one and links to one or to a page go.
36
+ */
37
+ const sourceMeta = (meta, source) => {
38
+ const listed = meta.resources.filter((model) => source.resources.includes(model.name));
39
+ const models = Object.fromEntries(listed.map((model) => [model.name, model]));
40
+ const resources = listed.map((model) => withinSource(model, models));
41
+ const scoped = Object.fromEntries(resources.map((model) => [model.name, model]));
42
+ return {
43
+ resources,
44
+ views: meta.views.filter((view) => Object.hasOwn(scoped, view.resource)).map((view) => withinSourceView(view, scoped)),
45
+ permissions: Object.fromEntries(listed.flatMap((model) => meta.permissions?.[model.name] ? [[model.name, meta.permissions[model.name]]] : []))
46
+ };
47
+ };
48
+ //#endregion
49
+ export { sourceMeta };
package/dist/types.d.ts CHANGED
@@ -67,6 +67,7 @@ export type ResourcePermissions = {
67
67
  * `runtime`: the runtime updates endpoint, for a caller with the `admin` role when the app has one.
68
68
  * `bottomBar`: the tabs on phones, of the bottom bar that names the caller's active role.
69
69
  * `plugins`: the plugins the admin app loads for the caller, each by its plugin.json.
70
+ * `sources`: the other APIs whose resources the admin app draws for the caller.
70
71
  */
71
72
  export type Meta = {
72
73
  resources: ResourceModel[];
@@ -82,12 +83,22 @@ export type Meta = {
82
83
  url: string;
83
84
  };
84
85
  plugins?: PluginRef[];
86
+ sources?: SourceRef[];
85
87
  };
86
88
  /** A plugin as `/meta` names it: the URL of its plugin.json, and the settings it reads with `usePlugin()`. */
87
89
  export type PluginRef = {
88
90
  url: string;
89
91
  settings?: Record<string, unknown>;
90
92
  };
93
+ /**
94
+ * Another Protobase API as `/meta` names it: `url` is its resources API (`.../api/v1`), `label` its name as people read
95
+ * it, and `resources` the ones the app draws from it, with its own `/meta` and data, at their own addresses.
96
+ */
97
+ export type SourceRef = {
98
+ url: string;
99
+ label: string;
100
+ resources: string[];
101
+ };
91
102
  export type MetaResult = {
92
103
  status: 'modified';
93
104
  meta: Meta;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@protobase/client",
3
- "version": "0.12.2",
3
+ "version": "0.12.3",
4
4
  "description": "Typed fetch client for the Protobase REST API.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -30,8 +30,8 @@
30
30
  },
31
31
  "dependencies": {
32
32
  "@better-auth/passkey": "^1.7.7",
33
- "@protobase/layout": "0.12.2",
34
- "@protobase/schema": "0.12.2",
33
+ "@protobase/layout": "0.12.3",
34
+ "@protobase/schema": "0.12.3",
35
35
  "@simplewebauthn/browser": "^13.3.0",
36
36
  "better-auth": "^1.7.7"
37
37
  }