@loynazkovacs/theitemapp-backend-sdk 0.2.0 → 0.3.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 +21 -2
- package/dist/auth.d.ts +16 -1
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +45 -1
- package/dist/core-api-client.d.ts +111 -17
- package/dist/core-api-client.d.ts.map +1 -1
- package/dist/core-api-client.js +139 -21
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -75,11 +75,30 @@ if (!user) return reply401();
|
|
|
75
75
|
|
|
76
76
|
| Export | Purpose |
|
|
77
77
|
| --- | --- |
|
|
78
|
-
| `CoreApiClient`, `CoreApiError` | Typed CRUD over `/api/dynamic`
|
|
78
|
+
| `CoreApiClient`, `CoreApiError` | Typed CRUD over `/api/dynamic`: `list`/`get`/`findBy`/`count`/`create`/`bulkCreate`/`update`/`updateAsUser`/`delete`/`hardDeleteByFilter`/`upsert`/`upsertOn`/`bulkUpsert`/`uploadFile`/`downloadFile`/`decrypt`. |
|
|
79
|
+
| `client.asUser(creds, opts?)` | Scoped client that acts AS an end user — forwards their `Authorization`/`Cookie` (optionally alongside the functional key). Full method surface. |
|
|
79
80
|
| `startAppRegistration` | Registration retry loop, API-key rotation capture, heartbeat, deregister, signal handlers. |
|
|
80
|
-
| `verifyUser`, `normalizeId`, `userInAnyGroup` | Framework-agnostic auth primitives. |
|
|
81
|
+
| `verifyUser`, `normalizeId`, `userInAnyGroup`, `verifyJwtLocal` | Framework-agnostic auth primitives (+ offline HS256 verification). |
|
|
81
82
|
| `createExpressAuthMiddleware`, `createFastifyAuthPreHandler` | Drop-in middleware/preHandler. |
|
|
82
83
|
|
|
84
|
+
### Read/write controls
|
|
85
|
+
|
|
86
|
+
- `get(c, id, { populate: false })` — keep x-ref fields as id strings (`?populate=0`).
|
|
87
|
+
- `create`/`update`/`upsertOn`/`bulkCreate` take an optional `WriteOptions` (`{ skipWebhooks?, headers? }`) for per-call control.
|
|
88
|
+
- `upsertOn(c, matchOn[], doc)` — composite-key atomic upsert returning `{ ok, created, doc }`.
|
|
89
|
+
- `decrypt(c, id, { throwOnError: true })` — throw instead of returning `null`.
|
|
90
|
+
|
|
91
|
+
### Acting as an end user
|
|
92
|
+
|
|
93
|
+
```ts
|
|
94
|
+
// forward the caller's session so core attributes the write to them:
|
|
95
|
+
await coreApi.asUser({ authorization: req.headers.authorization, cookie: req.headers.cookie })
|
|
96
|
+
.create('notes', { text: 'hi' });
|
|
97
|
+
|
|
98
|
+
// keep the functional key too (app identity + user session):
|
|
99
|
+
await coreApi.asUser({ jwt }, { keepApiKey: true }).uploadFile(bytes, { filename, mimeType });
|
|
100
|
+
```
|
|
101
|
+
|
|
83
102
|
## Not yet covered (roadmap)
|
|
84
103
|
|
|
85
104
|
- `aggregate()` — core exposes pivot aggregation through the dynamic list route
|
package/dist/auth.d.ts
CHANGED
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
* session cookie / `Authorization` header to core's `GET /api/auth/me`, which
|
|
6
6
|
* is the single source of truth for identity and group membership. Every
|
|
7
7
|
* backend reimplemented this proxy (`authMiddleware.ts` / `auth.ts`); this is
|
|
8
|
-
* the canonical version plus thin Express and Fastify adapters
|
|
8
|
+
* the canonical version plus thin Express and Fastify adapters, and an offline
|
|
9
|
+
* HS256 `verifyJwtLocal` for backends that hold core's JWT secret.
|
|
9
10
|
*/
|
|
10
11
|
export type AuthUser = {
|
|
11
12
|
_id: string;
|
|
@@ -78,5 +79,19 @@ export declare function createFastifyAuthPreHandler(coreUrl: string, options?: {
|
|
|
78
79
|
} & VerifyOptions): (request: FastifyReqLike, reply: FastifyReplyLike) => Promise<void>;
|
|
79
80
|
/** True if the user belongs to any of the given group ids. */
|
|
80
81
|
export declare function userInAnyGroup(user: Pick<AuthUser, 'groupIds'>, groupIds: Iterable<string>): boolean;
|
|
82
|
+
export type JwtPayload = Record<string, unknown> & {
|
|
83
|
+
sub?: string;
|
|
84
|
+
username?: string;
|
|
85
|
+
groupIds?: string[];
|
|
86
|
+
exp?: number;
|
|
87
|
+
iat?: number;
|
|
88
|
+
};
|
|
89
|
+
/**
|
|
90
|
+
* Verify an **HS256** JWT locally against a shared `secret` (no network call).
|
|
91
|
+
* Returns the decoded payload, or `null` if the token is malformed, the
|
|
92
|
+
* signature is invalid, the algorithm isn't HS256, or it has expired. The
|
|
93
|
+
* caller supplies the secret matching core's `JWT_SECRET`.
|
|
94
|
+
*/
|
|
95
|
+
export declare function verifyJwtLocal(token: string, secret: string): JwtPayload | null;
|
|
81
96
|
export {};
|
|
82
97
|
//# sourceMappingURL=auth.d.ts.map
|
package/dist/auth.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAIH,MAAM,MAAM,QAAQ,GAAG;IACrB,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,0EAA0E;IAC1E,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC9B,CAAC;AAEF,MAAM,MAAM,WAAW,GAAG;IACxB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG;IAC1B,4DAA4D;IAC5D,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF;;;;GAIG;AACH,wBAAsB,UAAU,CAC9B,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,WAAW,EAClB,IAAI,GAAE,aAAkB,GACvB,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC,CAsC1B;AAED;;;;;;;;GAQG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAUlD;AASD,KAAK,cAAc,GAAG;IAAE,OAAO,EAAE;QAAE,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,aAAa,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;CAAE,CAAC;AAE/E,UAAU,cAAc;IACtB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAChC,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG;QAAE,IAAI,CAAC,IAAI,EAAE,OAAO,GAAG,OAAO,CAAA;KAAE,CAAC;CACxD;AACD,KAAK,WAAW,GAAG,CAAC,GAAG,CAAC,EAAE,OAAO,KAAK,IAAI,CAAC;AAE3C;;;;GAIG;AACH,wBAAgB,2BAA2B,CACzC,OAAO,EAAE,MAAM,EACf,OAAO,GAAE;IAAE,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,aAAkB,IAGtC,KAAK,cAAc,EAAE,KAAK,cAAc,EAAE,MAAM,WAAW,KAAG,OAAO,CAAC,IAAI,CAAC,CAiB1F;AAED,UAAU,cAAe,SAAQ,cAAc;IAC7C,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;CACtB;AACD,UAAU,gBAAgB;IACxB,IAAI,CAAC,MAAM,EAAE,MAAM,GAAG;QAAE,IAAI,CAAC,IAAI,EAAE,OAAO,GAAG,OAAO,CAAA;KAAE,CAAC;CACxD;AAED;;;;GAIG;AACH,wBAAgB,2BAA2B,CACzC,OAAO,EAAE,MAAM,EACf,OAAO,GAAE;IAAE,UAAU,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,aAAkB,IAGvC,SAAS,cAAc,EAAE,OAAO,gBAAgB,KAAG,OAAO,CAAC,IAAI,CAAC,CAgB/E;AAED,8DAA8D;AAC9D,wBAAgB,cAAc,CAAC,IAAI,EAAE,IAAI,CAAC,QAAQ,EAAE,UAAU,CAAC,EAAE,QAAQ,EAAE,QAAQ,CAAC,MAAM,CAAC,GAAG,OAAO,CAGpG;AAUD,MAAM,MAAM,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG;IACjD,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;CACd,CAAC;AAEF;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,UAAU,GAAG,IAAI,CAyB/E"}
|
package/dist/auth.js
CHANGED
|
@@ -5,8 +5,10 @@
|
|
|
5
5
|
* session cookie / `Authorization` header to core's `GET /api/auth/me`, which
|
|
6
6
|
* is the single source of truth for identity and group membership. Every
|
|
7
7
|
* backend reimplemented this proxy (`authMiddleware.ts` / `auth.ts`); this is
|
|
8
|
-
* the canonical version plus thin Express and Fastify adapters
|
|
8
|
+
* the canonical version plus thin Express and Fastify adapters, and an offline
|
|
9
|
+
* HS256 `verifyJwtLocal` for backends that hold core's JWT secret.
|
|
9
10
|
*/
|
|
11
|
+
import { createHmac, timingSafeEqual } from 'node:crypto';
|
|
10
12
|
/**
|
|
11
13
|
* Verify a caller against core. Returns the `AuthUser` on success, or `null`
|
|
12
14
|
* if no credentials were supplied or core rejected them. Throws only when core
|
|
@@ -121,3 +123,45 @@ export function userInAnyGroup(user, groupIds) {
|
|
|
121
123
|
const set = groupIds instanceof Set ? groupIds : new Set(groupIds);
|
|
122
124
|
return user.groupIds.some((id) => set.has(id));
|
|
123
125
|
}
|
|
126
|
+
/**
|
|
127
|
+
* Verify an **HS256** JWT locally against a shared `secret` (no network call).
|
|
128
|
+
* Returns the decoded payload, or `null` if the token is malformed, the
|
|
129
|
+
* signature is invalid, the algorithm isn't HS256, or it has expired. The
|
|
130
|
+
* caller supplies the secret matching core's `JWT_SECRET`.
|
|
131
|
+
*/
|
|
132
|
+
export function verifyJwtLocal(token, secret) {
|
|
133
|
+
if (!token || !secret)
|
|
134
|
+
return null;
|
|
135
|
+
const parts = token.split('.');
|
|
136
|
+
if (parts.length !== 3)
|
|
137
|
+
return null;
|
|
138
|
+
const [headerB64, payloadB64, sigB64] = parts;
|
|
139
|
+
let header;
|
|
140
|
+
let payload;
|
|
141
|
+
try {
|
|
142
|
+
header = JSON.parse(base64UrlDecode(headerB64));
|
|
143
|
+
payload = JSON.parse(base64UrlDecode(payloadB64));
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
if (header.alg !== 'HS256')
|
|
149
|
+
return null;
|
|
150
|
+
const expected = base64ToUrl(createHmac('sha256', secret).update(`${headerB64}.${payloadB64}`).digest('base64'));
|
|
151
|
+
const a = new TextEncoder().encode(expected);
|
|
152
|
+
const b = new TextEncoder().encode(sigB64);
|
|
153
|
+
if (a.length !== b.length || !timingSafeEqual(a, b))
|
|
154
|
+
return null;
|
|
155
|
+
if (typeof payload.exp === 'number' && Date.now() / 1000 >= payload.exp)
|
|
156
|
+
return null;
|
|
157
|
+
return payload;
|
|
158
|
+
}
|
|
159
|
+
function base64UrlDecode(s) {
|
|
160
|
+
const b64 = s.replace(/-/g, '+').replace(/_/g, '/').padEnd(Math.ceil(s.length / 4) * 4, '=');
|
|
161
|
+
const bin = atob(b64); // DOM lib: base64 → latin1 binary string
|
|
162
|
+
const bytes = Uint8Array.from(bin, (c) => c.charCodeAt(0));
|
|
163
|
+
return new TextDecoder().decode(bytes); // reinterpret as UTF-8
|
|
164
|
+
}
|
|
165
|
+
function base64ToUrl(b64) {
|
|
166
|
+
return b64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
|
167
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* CoreApiClient — a thin, typed client over core's `/api/dynamic/<collection>`
|
|
3
|
-
* CRUD surface (plus
|
|
3
|
+
* CRUD surface (plus file upload/download and decrypt endpoints).
|
|
4
4
|
*
|
|
5
5
|
* Every app backend on the platform previously hand-rolled its own copy of
|
|
6
6
|
* this class (`coreApiClient.ts`), each drifting in features and error
|
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
* Auth: writes use the app's functional `x-api-key` (auto-provisioned by core
|
|
10
10
|
* on registration — see `startAppRegistration`), and by default set
|
|
11
11
|
* `x-theitemapp-skip-webhooks` so high-frequency system writes don't fan out
|
|
12
|
-
* through the webhook pipeline.
|
|
13
|
-
*
|
|
12
|
+
* through the webhook pipeline. To act as an end user (forwarding their
|
|
13
|
+
* session), use `asUser(...)` for a scoped client, or `updateAsUser`.
|
|
14
14
|
*/
|
|
15
15
|
export declare class CoreApiError extends Error {
|
|
16
16
|
readonly status: number;
|
|
@@ -28,6 +28,27 @@ export type CoreApiConfig = {
|
|
|
28
28
|
*/
|
|
29
29
|
skipWebhooks?: boolean;
|
|
30
30
|
};
|
|
31
|
+
/** Per-call write controls (create/update/upsert/bulk). */
|
|
32
|
+
export type WriteOptions = {
|
|
33
|
+
/** Override the client's skip-webhooks behaviour for this single call. */
|
|
34
|
+
skipWebhooks?: boolean;
|
|
35
|
+
/** Extra headers merged into this request (e.g. an end-user Authorization). */
|
|
36
|
+
headers?: Record<string, string>;
|
|
37
|
+
};
|
|
38
|
+
/** Read controls for `get`/`list`. */
|
|
39
|
+
export type GetOptions = {
|
|
40
|
+
/** `false` appends `?populate=0` so x-ref fields stay as id strings. */
|
|
41
|
+
populate?: boolean;
|
|
42
|
+
};
|
|
43
|
+
/** Credentials to act as an end user (forwarded to core). */
|
|
44
|
+
export type UserCredentials = {
|
|
45
|
+
/** Full `Authorization` header value (e.g. `Bearer <jwt>`). */
|
|
46
|
+
authorization?: string;
|
|
47
|
+
/** Raw `Cookie` header value (forwards the user's session cookie). */
|
|
48
|
+
cookie?: string;
|
|
49
|
+
/** Bare JWT — sent as `Authorization: Bearer <jwt>` if `authorization` is unset. */
|
|
50
|
+
jwt?: string;
|
|
51
|
+
};
|
|
31
52
|
export type BulkUpsertResult = {
|
|
32
53
|
upsertedCount: number;
|
|
33
54
|
modifiedCount: number;
|
|
@@ -36,6 +57,22 @@ export type BulkUpsertResult = {
|
|
|
36
57
|
error: string;
|
|
37
58
|
}>;
|
|
38
59
|
};
|
|
60
|
+
export type BulkCreateResult<T = Record<string, unknown>> = {
|
|
61
|
+
ok: boolean;
|
|
62
|
+
created: number;
|
|
63
|
+
failed: number;
|
|
64
|
+
results: Array<{
|
|
65
|
+
index: number;
|
|
66
|
+
ok: boolean;
|
|
67
|
+
doc?: T;
|
|
68
|
+
error?: string;
|
|
69
|
+
}>;
|
|
70
|
+
};
|
|
71
|
+
export type UpsertResult<T = Record<string, unknown>> = {
|
|
72
|
+
ok: boolean;
|
|
73
|
+
created: boolean;
|
|
74
|
+
doc: T;
|
|
75
|
+
};
|
|
39
76
|
export type UploadFileOptions = {
|
|
40
77
|
filename: string;
|
|
41
78
|
mimeType: string;
|
|
@@ -44,6 +81,11 @@ export type UploadFileOptions = {
|
|
|
44
81
|
groupIds?: string[];
|
|
45
82
|
title?: string;
|
|
46
83
|
};
|
|
84
|
+
export type DownloadedFile = {
|
|
85
|
+
data: Uint8Array;
|
|
86
|
+
contentType: string;
|
|
87
|
+
filename?: string;
|
|
88
|
+
};
|
|
47
89
|
export declare class CoreApiClient {
|
|
48
90
|
private readonly baseUrl;
|
|
49
91
|
private readonly skipWebhooks;
|
|
@@ -55,19 +97,40 @@ export declare class CoreApiClient {
|
|
|
55
97
|
getApiKey(): string;
|
|
56
98
|
/** Whether a non-empty API key is present — gate writes on this post-registration. */
|
|
57
99
|
isReady(): boolean;
|
|
100
|
+
/**
|
|
101
|
+
* Return a client scoped to act AS an end user — forwards their
|
|
102
|
+
* `Authorization`/`Cookie` so core attributes reads/writes (and RBAC) to
|
|
103
|
+
* that human. By default the functional `x-api-key` is dropped (core treats
|
|
104
|
+
* an api-key match as the system actor, overriding the user identity). Pass
|
|
105
|
+
* `{ keepApiKey: true }` to send BOTH (some apps authorise with the user
|
|
106
|
+
* session while still proving the app's identity). The returned client has
|
|
107
|
+
* the full method surface (list/get/create/update/delete/upsert/uploadFile…).
|
|
108
|
+
*/
|
|
109
|
+
asUser(creds: UserCredentials, opts?: {
|
|
110
|
+
keepApiKey?: boolean;
|
|
111
|
+
}): CoreApiClient;
|
|
58
112
|
/** Headers minus `Content-Type` — for requests with no JSON body (DELETE) or multipart. */
|
|
59
113
|
private headersWithoutContentType;
|
|
114
|
+
/** Build write headers with per-call skip-webhooks / extra-header overrides. */
|
|
115
|
+
private writeHeaders;
|
|
60
116
|
/**
|
|
61
117
|
* List documents. `query` maps to core's dynamic list params, e.g.
|
|
62
|
-
* `{ _l: '50', _s: '-createdAt', someField: 'x' }`. Defaults
|
|
118
|
+
* `{ _l: '50', _s: '-createdAt', populate: '0', someField: 'x' }`. Defaults
|
|
119
|
+
* to `_l=500`; override by passing your own `_l`.
|
|
63
120
|
*/
|
|
64
121
|
list<T = Record<string, unknown>>(collection: string, query?: Record<string, string>): Promise<T[]>;
|
|
122
|
+
/**
|
|
123
|
+
* Count documents matching `filter` (field filters + optional `_f` advanced
|
|
124
|
+
* JSON) without fetching them — `GET /api/dynamic/:collection/count`.
|
|
125
|
+
*/
|
|
126
|
+
count(collection: string, filter?: Record<string, string>): Promise<number>;
|
|
65
127
|
/**
|
|
66
128
|
* Fetch one document by id. Returns `null` for a genuine 404; throws
|
|
67
129
|
* `CoreApiError` for transient failures (401/403/429/5xx) so callers can
|
|
68
|
-
* distinguish "doesn't exist" from "couldn't read it".
|
|
130
|
+
* distinguish "doesn't exist" from "couldn't read it". Pass
|
|
131
|
+
* `{ populate: false }` to keep x-ref fields as id strings (`?populate=0`).
|
|
69
132
|
*/
|
|
70
|
-
get<T = Record<string, unknown>>(collection: string, id: string): Promise<T | null>;
|
|
133
|
+
get<T = Record<string, unknown>>(collection: string, id: string, opts?: GetOptions): Promise<T | null>;
|
|
71
134
|
/**
|
|
72
135
|
* Find a single document by an indexed field. Returns `null` for no match;
|
|
73
136
|
* throws `CoreApiError` on transient failure (never masks a read error as
|
|
@@ -75,27 +138,47 @@ export declare class CoreApiClient {
|
|
|
75
138
|
*/
|
|
76
139
|
findBy<T = Record<string, unknown>>(collection: string, field: string, value: string): Promise<T | null>;
|
|
77
140
|
/** Create a document. Throws `CoreApiError` on failure. */
|
|
78
|
-
create<T = Record<string, unknown>>(collection: string, body: Record<string, unknown
|
|
141
|
+
create<T = Record<string, unknown>>(collection: string, body: Record<string, unknown>, opts?: WriteOptions): Promise<T>;
|
|
142
|
+
/**
|
|
143
|
+
* Create up to 500 documents in one call — `POST /api/dynamic/:collection/bulk`.
|
|
144
|
+
* Returns core's `{ ok, created, failed, results }` (per-doc success/error).
|
|
145
|
+
*/
|
|
146
|
+
bulkCreate<T = Record<string, unknown>>(collection: string, documents: Record<string, unknown>[], opts?: WriteOptions): Promise<BulkCreateResult<T>>;
|
|
79
147
|
/** Update a document by id (PUT). Throws `CoreApiError` on failure. */
|
|
80
|
-
update<T = Record<string, unknown>>(collection: string, id: string, body: Record<string, unknown
|
|
148
|
+
update<T = Record<string, unknown>>(collection: string, id: string, body: Record<string, unknown>, opts?: WriteOptions): Promise<T>;
|
|
81
149
|
/**
|
|
82
150
|
* Update a document AS the given user, by sending their short-lived JWT
|
|
83
151
|
* (`Authorization: Bearer`) instead of the functional `x-api-key`. Core then
|
|
84
152
|
* attributes the change to that human in the audit log and
|
|
85
|
-
* `_meta.lastModifiedByUserId`.
|
|
86
|
-
*
|
|
87
|
-
* the asserted user identity.
|
|
153
|
+
* `_meta.lastModifiedByUserId`. For richer user-scoped flows (cookie
|
|
154
|
+
* forwarding, multipart upload, reads) prefer `asUser(...)`.
|
|
88
155
|
*/
|
|
89
156
|
updateAsUser<T = Record<string, unknown>>(collection: string, id: string, body: Record<string, unknown>, userJwt: string): Promise<T>;
|
|
90
157
|
/** Soft-delete a document by id. Returns `true` on success or 404 (already gone). */
|
|
91
158
|
delete(collection: string, id: string): Promise<boolean>;
|
|
92
159
|
/**
|
|
93
|
-
*
|
|
94
|
-
* `POST /api/dynamic/:collection/
|
|
95
|
-
*
|
|
96
|
-
|
|
160
|
+
* Permanently delete documents matching `query` —
|
|
161
|
+
* `POST /api/dynamic/:collection/hard-delete-by-filter`. API-key auth only
|
|
162
|
+
* (system operation); the query must be non-empty. Returns `{ ok, deletedCount }`.
|
|
163
|
+
*/
|
|
164
|
+
hardDeleteByFilter(collection: string, query: Record<string, unknown>): Promise<{
|
|
165
|
+
ok: boolean;
|
|
166
|
+
deletedCount: number;
|
|
167
|
+
}>;
|
|
168
|
+
/**
|
|
169
|
+
* Atomically upsert a document by a single unique field, via core's
|
|
170
|
+
* server-side `POST /api/dynamic/:collection/upsert`. Returns the document
|
|
171
|
+
* (or `null`). Falls back to findBy + update/create on older cores that 404
|
|
172
|
+
* the upsert route. For composite keys / the full `{ ok, created }` result,
|
|
173
|
+
* use `upsertOn`.
|
|
97
174
|
*/
|
|
98
175
|
upsert<T = Record<string, unknown>>(collection: string, matchField: string, matchValue: string, body: Record<string, unknown>): Promise<T | null>;
|
|
176
|
+
/**
|
|
177
|
+
* Atomically upsert by a COMPOSITE match key — `POST .../upsert` with
|
|
178
|
+
* `{ matchOn, document }`. Returns the full `{ ok, created, doc }` result so
|
|
179
|
+
* callers can branch on whether a row was created vs updated.
|
|
180
|
+
*/
|
|
181
|
+
upsertOn<T = Record<string, unknown>>(collection: string, matchOn: string[], document: Record<string, unknown>, opts?: WriteOptions): Promise<UpsertResult<T>>;
|
|
99
182
|
private upsertLegacy;
|
|
100
183
|
/**
|
|
101
184
|
* Bulk-upsert up to 500 documents, matched by a composite key (typically
|
|
@@ -107,15 +190,26 @@ export declare class CoreApiClient {
|
|
|
107
190
|
* Upload a file to core's file API (`POST /api/files/uploadDirect`). Returns
|
|
108
191
|
* the created file document (`{ _id, originalName }`) for referencing.
|
|
109
192
|
* Accepts a `Uint8Array` (a Node `Buffer` is a `Uint8Array`, so it works too).
|
|
193
|
+
* To upload AS an end user, call `asUser(creds).uploadFile(...)`.
|
|
110
194
|
*/
|
|
111
195
|
uploadFile(data: Uint8Array, opts: UploadFileOptions): Promise<{
|
|
112
196
|
_id: string;
|
|
113
197
|
originalName: string;
|
|
114
198
|
}>;
|
|
199
|
+
/**
|
|
200
|
+
* Download a file's bytes — `GET /api/files/:id/content`. Returns the bytes
|
|
201
|
+
* plus content-type/filename, or `null` on 404. To download AS an end user,
|
|
202
|
+
* call `asUser(creds).downloadFile(...)`.
|
|
203
|
+
*/
|
|
204
|
+
downloadFile(fileId: string): Promise<DownloadedFile | null>;
|
|
115
205
|
/**
|
|
116
206
|
* Decrypt `x-encrypted` fields for a record via core's internal decrypt
|
|
117
|
-
* endpoint. Returns a map of field name → plaintext
|
|
207
|
+
* endpoint. Returns a map of field name → plaintext. By default returns
|
|
208
|
+
* `null` on failure; pass `{ throwOnError: true }` to throw `CoreApiError`
|
|
209
|
+
* instead. To decrypt AS an end user, call `asUser(creds).decrypt(...)`.
|
|
118
210
|
*/
|
|
119
|
-
decrypt(collection: string, id: string
|
|
211
|
+
decrypt(collection: string, id: string, opts?: {
|
|
212
|
+
throwOnError?: boolean;
|
|
213
|
+
}): Promise<Record<string, string | null> | null>;
|
|
120
214
|
}
|
|
121
215
|
//# sourceMappingURL=core-api-client.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"core-api-client.d.ts","sourceRoot":"","sources":["../src/core-api-client.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,qBAAa,YAAa,SAAQ,KAAK;IACrC,SAAgB,MAAM,EAAE,MAAM,CAAC;IAC/B,SAAgB,IAAI,EAAE,MAAM,CAAC;gBACjB,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM;CAMzE;AAED,MAAM,MAAM,aAAa,GAAG;IAC1B,gFAAgF;IAChF,OAAO,EAAE,MAAM,CAAC;IAChB,2EAA2E;IAC3E,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB;;;OAGG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB,CAAC;AAEF,MAAM,MAAM,gBAAgB,GAAG;IAC7B,aAAa,EAAE,MAAM,CAAC;IACtB,aAAa,EAAE,MAAM,CAAC;IACtB,MAAM,EAAE,KAAK,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CACjD,CAAC;AAEF,MAAM,MAAM,iBAAiB,GAAG;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;IACxB,UAAU,CAAC,EAAE,SAAS,GAAG,QAAQ,GAAG,UAAU,CAAC;IAC/C,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,qBAAa,aAAa;IACxB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAU;IACvC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAyB;gBAErC,MAAM,EAAE,aAAa;IAUjC,sFAAsF;IACtF,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI;IAIlC,iEAAiE;IACjE,SAAS,IAAI,MAAM;IAInB,sFAAsF;IACtF,OAAO,IAAI,OAAO;IAIlB,2FAA2F;IAC3F,OAAO,CAAC,yBAAyB;IAKjC
|
|
1
|
+
{"version":3,"file":"core-api-client.d.ts","sourceRoot":"","sources":["../src/core-api-client.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,qBAAa,YAAa,SAAQ,KAAK;IACrC,SAAgB,MAAM,EAAE,MAAM,CAAC;IAC/B,SAAgB,IAAI,EAAE,MAAM,CAAC;gBACjB,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM;CAMzE;AAED,MAAM,MAAM,aAAa,GAAG;IAC1B,gFAAgF;IAChF,OAAO,EAAE,MAAM,CAAC;IAChB,2EAA2E;IAC3E,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB;;;OAGG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB,CAAC;AAEF,2DAA2D;AAC3D,MAAM,MAAM,YAAY,GAAG;IACzB,0EAA0E;IAC1E,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,+EAA+E;IAC/E,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAClC,CAAC;AAEF,sCAAsC;AACtC,MAAM,MAAM,UAAU,GAAG;IACvB,wEAAwE;IACxE,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB,CAAC;AAEF,6DAA6D;AAC7D,MAAM,MAAM,eAAe,GAAG;IAC5B,+DAA+D;IAC/D,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,sEAAsE;IACtE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,oFAAoF;IACpF,GAAG,CAAC,EAAE,MAAM,CAAC;CACd,CAAC;AAEF,MAAM,MAAM,gBAAgB,GAAG;IAC7B,aAAa,EAAE,MAAM,CAAC;IACtB,aAAa,EAAE,MAAM,CAAC;IACtB,MAAM,EAAE,KAAK,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CACjD,CAAC;AAEF,MAAM,MAAM,gBAAgB,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,IAAI;IAC1D,EAAE,EAAE,OAAO,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,KAAK,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,OAAO,CAAC;QAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CACzE,CAAC;AAEF,MAAM,MAAM,YAAY,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,IAAI;IACtD,EAAE,EAAE,OAAO,CAAC;IACZ,OAAO,EAAE,OAAO,CAAC;IACjB,GAAG,EAAE,CAAC,CAAC;CACR,CAAC;AAEF,MAAM,MAAM,iBAAiB,GAAG;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;IACxB,UAAU,CAAC,EAAE,SAAS,GAAG,QAAQ,GAAG,UAAU,CAAC;IAC/C,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,MAAM,MAAM,cAAc,GAAG;IAC3B,IAAI,EAAE,UAAU,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF,qBAAa,aAAa;IACxB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAU;IACvC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAyB;gBAErC,MAAM,EAAE,aAAa;IAUjC,sFAAsF;IACtF,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI;IAIlC,iEAAiE;IACjE,SAAS,IAAI,MAAM;IAInB,sFAAsF;IACtF,OAAO,IAAI,OAAO;IAIlB;;;;;;;;OAQG;IACH,MAAM,CAAC,KAAK,EAAE,eAAe,EAAE,IAAI,GAAE;QAAE,UAAU,CAAC,EAAE,OAAO,CAAA;KAAO,GAAG,aAAa;IAYlF,2FAA2F;IAC3F,OAAO,CAAC,yBAAyB;IAKjC,gFAAgF;IAChF,OAAO,CAAC,YAAY;IAQpB;;;;OAIG;IACG,IAAI,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACpC,UAAU,EAAE,MAAM,EAClB,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAC7B,OAAO,CAAC,CAAC,EAAE,CAAC;IASf;;;OAGG;IACG,KAAK,CAAC,UAAU,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC;IASjF;;;;;OAKG;IACG,GAAG,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACnC,UAAU,EAAE,MAAM,EAClB,EAAE,EAAE,MAAM,EACV,IAAI,CAAC,EAAE,UAAU,GAChB,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC;IASpB;;;;OAIG;IACG,MAAM,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACtC,UAAU,EAAE,MAAM,EAClB,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,MAAM,GACZ,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC;IASpB,2DAA2D;IACrD,MAAM,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACtC,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,IAAI,CAAC,EAAE,YAAY,GAClB,OAAO,CAAC,CAAC,CAAC;IAOb;;;OAGG;IACG,UAAU,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC1C,UAAU,EAAE,MAAM,EAClB,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,EACpC,IAAI,CAAC,EAAE,YAAY,GAClB,OAAO,CAAC,gBAAgB,CAAC,CAAC,CAAC,CAAC;IAQ/B,uEAAuE;IACjE,MAAM,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACtC,UAAU,EAAE,MAAM,EAClB,EAAE,EAAE,MAAM,EACV,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,IAAI,CAAC,EAAE,YAAY,GAClB,OAAO,CAAC,CAAC,CAAC;IAOb;;;;;;OAMG;IACG,YAAY,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC5C,UAAU,EAAE,MAAM,EAClB,EAAE,EAAE,MAAM,EACV,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,CAAC,CAAC;IAiBb,qFAAqF;IAC/E,MAAM,CAAC,UAAU,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAS9D;;;;OAIG;IACG,kBAAkB,CACtB,UAAU,EAAE,MAAM,EAClB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC7B,OAAO,CAAC;QAAE,EAAE,EAAE,OAAO,CAAC;QAAC,YAAY,EAAE,MAAM,CAAA;KAAE,CAAC;IAUjD;;;;;;OAMG;IACG,MAAM,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACtC,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC5B,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC;IAgBpB;;;;OAIG;IACG,QAAQ,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACxC,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,MAAM,EAAE,EACjB,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACjC,IAAI,CAAC,EAAE,YAAY,GAClB,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;YAWb,YAAY;IAa1B;;;;OAIG;IACG,UAAU,CACd,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,MAAM,EAAE,EACjB,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,GACnC,OAAO,CAAC,gBAAgB,CAAC;IAY5B;;;;;OAKG;IACG,UAAU,CACd,IAAI,EAAE,UAAU,EAChB,IAAI,EAAE,iBAAiB,GACtB,OAAO,CAAC;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,YAAY,EAAE,MAAM,CAAA;KAAE,CAAC;IAmBjD;;;;OAIG;IACG,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC;IAYlE;;;;;OAKG;IACG,OAAO,CACX,UAAU,EAAE,MAAM,EAClB,EAAE,EAAE,MAAM,EACV,IAAI,CAAC,EAAE;QAAE,YAAY,CAAC,EAAE,OAAO,CAAA;KAAE,GAChC,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC,GAAG,IAAI,CAAC;CASjD"}
|
package/dist/core-api-client.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* CoreApiClient — a thin, typed client over core's `/api/dynamic/<collection>`
|
|
3
|
-
* CRUD surface (plus
|
|
3
|
+
* CRUD surface (plus file upload/download and decrypt endpoints).
|
|
4
4
|
*
|
|
5
5
|
* Every app backend on the platform previously hand-rolled its own copy of
|
|
6
6
|
* this class (`coreApiClient.ts`), each drifting in features and error
|
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
* Auth: writes use the app's functional `x-api-key` (auto-provisioned by core
|
|
10
10
|
* on registration — see `startAppRegistration`), and by default set
|
|
11
11
|
* `x-theitemapp-skip-webhooks` so high-frequency system writes don't fan out
|
|
12
|
-
* through the webhook pipeline.
|
|
13
|
-
*
|
|
12
|
+
* through the webhook pipeline. To act as an end user (forwarding their
|
|
13
|
+
* session), use `asUser(...)` for a scoped client, or `updateAsUser`.
|
|
14
14
|
*/
|
|
15
15
|
export class CoreApiError extends Error {
|
|
16
16
|
status;
|
|
@@ -47,14 +47,49 @@ export class CoreApiClient {
|
|
|
47
47
|
isReady() {
|
|
48
48
|
return (this.headers['x-api-key']?.length ?? 0) > 0;
|
|
49
49
|
}
|
|
50
|
+
/**
|
|
51
|
+
* Return a client scoped to act AS an end user — forwards their
|
|
52
|
+
* `Authorization`/`Cookie` so core attributes reads/writes (and RBAC) to
|
|
53
|
+
* that human. By default the functional `x-api-key` is dropped (core treats
|
|
54
|
+
* an api-key match as the system actor, overriding the user identity). Pass
|
|
55
|
+
* `{ keepApiKey: true }` to send BOTH (some apps authorise with the user
|
|
56
|
+
* session while still proving the app's identity). The returned client has
|
|
57
|
+
* the full method surface (list/get/create/update/delete/upsert/uploadFile…).
|
|
58
|
+
*/
|
|
59
|
+
asUser(creds, opts = {}) {
|
|
60
|
+
const scoped = new CoreApiClient({
|
|
61
|
+
baseUrl: this.baseUrl,
|
|
62
|
+
apiKey: opts.keepApiKey ? (this.headers['x-api-key'] ?? null) : null,
|
|
63
|
+
skipWebhooks: this.skipWebhooks,
|
|
64
|
+
});
|
|
65
|
+
if (creds.authorization)
|
|
66
|
+
scoped.headers['Authorization'] = creds.authorization;
|
|
67
|
+
else if (creds.jwt)
|
|
68
|
+
scoped.headers['Authorization'] = `Bearer ${creds.jwt}`;
|
|
69
|
+
if (creds.cookie)
|
|
70
|
+
scoped.headers['Cookie'] = creds.cookie;
|
|
71
|
+
return scoped;
|
|
72
|
+
}
|
|
50
73
|
/** Headers minus `Content-Type` — for requests with no JSON body (DELETE) or multipart. */
|
|
51
74
|
headersWithoutContentType() {
|
|
52
75
|
const { 'Content-Type': _ct, ...rest } = this.headers;
|
|
53
76
|
return rest;
|
|
54
77
|
}
|
|
78
|
+
/** Build write headers with per-call skip-webhooks / extra-header overrides. */
|
|
79
|
+
writeHeaders(opts) {
|
|
80
|
+
const h = { ...this.headers };
|
|
81
|
+
if (opts?.skipWebhooks === false)
|
|
82
|
+
delete h['x-theitemapp-skip-webhooks'];
|
|
83
|
+
else if (opts?.skipWebhooks === true)
|
|
84
|
+
h['x-theitemapp-skip-webhooks'] = '1';
|
|
85
|
+
if (opts?.headers)
|
|
86
|
+
Object.assign(h, opts.headers);
|
|
87
|
+
return h;
|
|
88
|
+
}
|
|
55
89
|
/**
|
|
56
90
|
* List documents. `query` maps to core's dynamic list params, e.g.
|
|
57
|
-
* `{ _l: '50', _s: '-createdAt', someField: 'x' }`. Defaults
|
|
91
|
+
* `{ _l: '50', _s: '-createdAt', populate: '0', someField: 'x' }`. Defaults
|
|
92
|
+
* to `_l=500`; override by passing your own `_l`.
|
|
58
93
|
*/
|
|
59
94
|
async list(collection, query) {
|
|
60
95
|
const params = new URLSearchParams({ _l: '500', ...query });
|
|
@@ -65,13 +100,28 @@ export class CoreApiClient {
|
|
|
65
100
|
const data = await res.json();
|
|
66
101
|
return Array.isArray(data) ? data : [];
|
|
67
102
|
}
|
|
103
|
+
/**
|
|
104
|
+
* Count documents matching `filter` (field filters + optional `_f` advanced
|
|
105
|
+
* JSON) without fetching them — `GET /api/dynamic/:collection/count`.
|
|
106
|
+
*/
|
|
107
|
+
async count(collection, filter) {
|
|
108
|
+
const qs = new URLSearchParams({ ...filter }).toString();
|
|
109
|
+
const url = `${this.baseUrl}/api/dynamic/${collection}/count${qs ? `?${qs}` : ''}`;
|
|
110
|
+
const res = await fetch(url, { headers: this.headers });
|
|
111
|
+
if (!res.ok)
|
|
112
|
+
throw new CoreApiError('COUNT', collection, res.status, await safeText(res));
|
|
113
|
+
const data = (await res.json());
|
|
114
|
+
return typeof data === 'number' ? data : Number(data?.count ?? 0);
|
|
115
|
+
}
|
|
68
116
|
/**
|
|
69
117
|
* Fetch one document by id. Returns `null` for a genuine 404; throws
|
|
70
118
|
* `CoreApiError` for transient failures (401/403/429/5xx) so callers can
|
|
71
|
-
* distinguish "doesn't exist" from "couldn't read it".
|
|
119
|
+
* distinguish "doesn't exist" from "couldn't read it". Pass
|
|
120
|
+
* `{ populate: false }` to keep x-ref fields as id strings (`?populate=0`).
|
|
72
121
|
*/
|
|
73
|
-
async get(collection, id) {
|
|
74
|
-
const
|
|
122
|
+
async get(collection, id, opts) {
|
|
123
|
+
const qs = opts?.populate === false ? '?populate=0' : '';
|
|
124
|
+
const url = `${this.baseUrl}/api/dynamic/${collection}/${encodeURIComponent(id)}${qs}`;
|
|
75
125
|
const res = await fetch(url, { headers: this.headers });
|
|
76
126
|
if (res.ok)
|
|
77
127
|
return (await res.json());
|
|
@@ -94,17 +144,30 @@ export class CoreApiClient {
|
|
|
94
144
|
return Array.isArray(data) ? (data[0] ?? null) : null;
|
|
95
145
|
}
|
|
96
146
|
/** Create a document. Throws `CoreApiError` on failure. */
|
|
97
|
-
async create(collection, body) {
|
|
147
|
+
async create(collection, body, opts) {
|
|
98
148
|
const url = `${this.baseUrl}/api/dynamic/${collection}`;
|
|
99
|
-
const res = await fetch(url, { method: 'POST', headers: this.
|
|
149
|
+
const res = await fetch(url, { method: 'POST', headers: this.writeHeaders(opts), body: JSON.stringify(body) });
|
|
100
150
|
if (!res.ok)
|
|
101
151
|
throw new CoreApiError('CREATE', collection, res.status, await safeText(res));
|
|
102
152
|
return (await res.json());
|
|
103
153
|
}
|
|
154
|
+
/**
|
|
155
|
+
* Create up to 500 documents in one call — `POST /api/dynamic/:collection/bulk`.
|
|
156
|
+
* Returns core's `{ ok, created, failed, results }` (per-doc success/error).
|
|
157
|
+
*/
|
|
158
|
+
async bulkCreate(collection, documents, opts) {
|
|
159
|
+
if (documents.length === 0)
|
|
160
|
+
return { ok: true, created: 0, failed: 0, results: [] };
|
|
161
|
+
const url = `${this.baseUrl}/api/dynamic/${collection}/bulk`;
|
|
162
|
+
const res = await fetch(url, { method: 'POST', headers: this.writeHeaders(opts), body: JSON.stringify({ documents }) });
|
|
163
|
+
if (!res.ok)
|
|
164
|
+
throw new CoreApiError('BULK_CREATE', collection, res.status, await safeText(res));
|
|
165
|
+
return (await res.json());
|
|
166
|
+
}
|
|
104
167
|
/** Update a document by id (PUT). Throws `CoreApiError` on failure. */
|
|
105
|
-
async update(collection, id, body) {
|
|
168
|
+
async update(collection, id, body, opts) {
|
|
106
169
|
const url = `${this.baseUrl}/api/dynamic/${collection}/${encodeURIComponent(id)}`;
|
|
107
|
-
const res = await fetch(url, { method: 'PUT', headers: this.
|
|
170
|
+
const res = await fetch(url, { method: 'PUT', headers: this.writeHeaders(opts), body: JSON.stringify(body) });
|
|
108
171
|
if (!res.ok)
|
|
109
172
|
throw new CoreApiError('UPDATE', `${collection}/${id}`, res.status, await safeText(res));
|
|
110
173
|
return (await res.json());
|
|
@@ -113,9 +176,8 @@ export class CoreApiClient {
|
|
|
113
176
|
* Update a document AS the given user, by sending their short-lived JWT
|
|
114
177
|
* (`Authorization: Bearer`) instead of the functional `x-api-key`. Core then
|
|
115
178
|
* attributes the change to that human in the audit log and
|
|
116
|
-
* `_meta.lastModifiedByUserId`.
|
|
117
|
-
*
|
|
118
|
-
* the asserted user identity.
|
|
179
|
+
* `_meta.lastModifiedByUserId`. For richer user-scoped flows (cookie
|
|
180
|
+
* forwarding, multipart upload, reads) prefer `asUser(...)`.
|
|
119
181
|
*/
|
|
120
182
|
async updateAsUser(collection, id, body, userJwt) {
|
|
121
183
|
const url = `${this.baseUrl}/api/dynamic/${collection}/${encodeURIComponent(id)}`;
|
|
@@ -144,10 +206,26 @@ export class CoreApiClient {
|
|
|
144
206
|
throw new CoreApiError('DELETE', `${collection}/${id}`, res.status, await safeText(res));
|
|
145
207
|
}
|
|
146
208
|
/**
|
|
147
|
-
*
|
|
148
|
-
* `POST /api/dynamic/:collection/
|
|
149
|
-
*
|
|
150
|
-
|
|
209
|
+
* Permanently delete documents matching `query` —
|
|
210
|
+
* `POST /api/dynamic/:collection/hard-delete-by-filter`. API-key auth only
|
|
211
|
+
* (system operation); the query must be non-empty. Returns `{ ok, deletedCount }`.
|
|
212
|
+
*/
|
|
213
|
+
async hardDeleteByFilter(collection, query) {
|
|
214
|
+
if (!query || Object.keys(query).length === 0) {
|
|
215
|
+
throw new Error('[coreApi] hardDeleteByFilter requires a non-empty query');
|
|
216
|
+
}
|
|
217
|
+
const url = `${this.baseUrl}/api/dynamic/${collection}/hard-delete-by-filter`;
|
|
218
|
+
const res = await fetch(url, { method: 'POST', headers: this.headers, body: JSON.stringify({ query }) });
|
|
219
|
+
if (!res.ok)
|
|
220
|
+
throw new CoreApiError('HARD_DELETE_BY_FILTER', collection, res.status, await safeText(res));
|
|
221
|
+
return (await res.json());
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Atomically upsert a document by a single unique field, via core's
|
|
225
|
+
* server-side `POST /api/dynamic/:collection/upsert`. Returns the document
|
|
226
|
+
* (or `null`). Falls back to findBy + update/create on older cores that 404
|
|
227
|
+
* the upsert route. For composite keys / the full `{ ok, created }` result,
|
|
228
|
+
* use `upsertOn`.
|
|
151
229
|
*/
|
|
152
230
|
async upsert(collection, matchField, matchValue, body) {
|
|
153
231
|
const fullDoc = { ...body, [matchField]: matchValue };
|
|
@@ -165,6 +243,22 @@ export class CoreApiClient {
|
|
|
165
243
|
return this.upsertLegacy(collection, matchField, matchValue, body);
|
|
166
244
|
throw new CoreApiError('UPSERT', collection, res.status, await safeText(res));
|
|
167
245
|
}
|
|
246
|
+
/**
|
|
247
|
+
* Atomically upsert by a COMPOSITE match key — `POST .../upsert` with
|
|
248
|
+
* `{ matchOn, document }`. Returns the full `{ ok, created, doc }` result so
|
|
249
|
+
* callers can branch on whether a row was created vs updated.
|
|
250
|
+
*/
|
|
251
|
+
async upsertOn(collection, matchOn, document, opts) {
|
|
252
|
+
const url = `${this.baseUrl}/api/dynamic/${collection}/upsert`;
|
|
253
|
+
const res = await fetch(url, {
|
|
254
|
+
method: 'POST',
|
|
255
|
+
headers: this.writeHeaders(opts),
|
|
256
|
+
body: JSON.stringify({ matchOn, document }),
|
|
257
|
+
});
|
|
258
|
+
if (!res.ok)
|
|
259
|
+
throw new CoreApiError('UPSERT_ON', collection, res.status, await safeText(res));
|
|
260
|
+
return (await res.json());
|
|
261
|
+
}
|
|
168
262
|
async upsertLegacy(collection, matchField, matchValue, body) {
|
|
169
263
|
const existing = await this.findBy(collection, matchField, matchValue);
|
|
170
264
|
if (existing && typeof existing._id === 'string') {
|
|
@@ -194,6 +288,7 @@ export class CoreApiClient {
|
|
|
194
288
|
* Upload a file to core's file API (`POST /api/files/uploadDirect`). Returns
|
|
195
289
|
* the created file document (`{ _id, originalName }`) for referencing.
|
|
196
290
|
* Accepts a `Uint8Array` (a Node `Buffer` is a `Uint8Array`, so it works too).
|
|
291
|
+
* To upload AS an end user, call `asUser(creds).uploadFile(...)`.
|
|
197
292
|
*/
|
|
198
293
|
async uploadFile(data, opts) {
|
|
199
294
|
const form = new FormData();
|
|
@@ -215,15 +310,38 @@ export class CoreApiClient {
|
|
|
215
310
|
throw new CoreApiError('UPLOAD_FILE', opts.filename, res.status, await safeText(res));
|
|
216
311
|
return (await res.json());
|
|
217
312
|
}
|
|
313
|
+
/**
|
|
314
|
+
* Download a file's bytes — `GET /api/files/:id/content`. Returns the bytes
|
|
315
|
+
* plus content-type/filename, or `null` on 404. To download AS an end user,
|
|
316
|
+
* call `asUser(creds).downloadFile(...)`.
|
|
317
|
+
*/
|
|
318
|
+
async downloadFile(fileId) {
|
|
319
|
+
const url = `${this.baseUrl}/api/files/${encodeURIComponent(fileId)}/content`;
|
|
320
|
+
const res = await fetch(url, { headers: this.headersWithoutContentType() });
|
|
321
|
+
if (res.status === 404)
|
|
322
|
+
return null;
|
|
323
|
+
if (!res.ok)
|
|
324
|
+
throw new CoreApiError('DOWNLOAD_FILE', fileId, res.status, await safeText(res));
|
|
325
|
+
const data = new Uint8Array(await res.arrayBuffer());
|
|
326
|
+
const contentType = res.headers.get('content-type') ?? 'application/octet-stream';
|
|
327
|
+
const cd = res.headers.get('content-disposition') ?? '';
|
|
328
|
+
const m = /filename\*?=(?:UTF-8'')?"?([^";]+)"?/i.exec(cd);
|
|
329
|
+
return { data, contentType, filename: m?.[1] };
|
|
330
|
+
}
|
|
218
331
|
/**
|
|
219
332
|
* Decrypt `x-encrypted` fields for a record via core's internal decrypt
|
|
220
|
-
* endpoint. Returns a map of field name → plaintext
|
|
333
|
+
* endpoint. Returns a map of field name → plaintext. By default returns
|
|
334
|
+
* `null` on failure; pass `{ throwOnError: true }` to throw `CoreApiError`
|
|
335
|
+
* instead. To decrypt AS an end user, call `asUser(creds).decrypt(...)`.
|
|
221
336
|
*/
|
|
222
|
-
async decrypt(collection, id) {
|
|
337
|
+
async decrypt(collection, id, opts) {
|
|
223
338
|
const url = `${this.baseUrl}/api/internal/decrypt/${collection}/${encodeURIComponent(id)}`;
|
|
224
339
|
const res = await fetch(url, { headers: this.headers });
|
|
225
|
-
if (!res.ok)
|
|
340
|
+
if (!res.ok) {
|
|
341
|
+
if (opts?.throwOnError)
|
|
342
|
+
throw new CoreApiError('DECRYPT', `${collection}/${id}`, res.status, await safeText(res));
|
|
226
343
|
return null;
|
|
344
|
+
}
|
|
227
345
|
return (await res.json());
|
|
228
346
|
}
|
|
229
347
|
}
|
package/package.json
CHANGED