@manablox/api-rpc 0.3.0 → 0.4.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.
Files changed (3) hide show
  1. package/dist/index.d.ts +3272 -1233
  2. package/dist/index.js +244 -11
  3. package/package.json +7 -7
package/dist/index.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import { z } from "zod";
2
- import { MIN_PASSWORD_LENGTH, actorRoles, allowedTypeIds, assertCan, effectiveGrants, normaliseGrants } from "@manablox/auth";
3
- import { ManabloxError, TRANSPORT_CODE, WORKFLOW_CONDITION_OPERATORS, WORKFLOW_EVENTS } from "@manablox/core";
2
+ import { MIN_PASSWORD_LENGTH, actorRoles, allowedTypeIds, assertCan, can, effectiveGrants, normaliseGrants } from "@manablox/auth";
3
+ import { AUDIT_ACTIONS, ManabloxError, NOTIFICATION_CHANNELS, NOTIFICATION_KINDS, TRANSPORT_CODE, WORKFLOW_CONDITION_OPERATORS, WORKFLOW_EVENTS } from "@manablox/core";
4
4
  import { ORPCError, os } from "@orpc/server";
5
- import { renderContentTypeConfig } from "@manablox/services";
5
+ import { SPACE_EXPORT_SECTIONS, applySpaceStarter, renderContentTypeConfig } from "@manablox/services";
6
6
  //#region src/base.ts
7
7
  /**
8
8
  * Base procedure builder. Every management procedure runs through it, so the
@@ -136,7 +136,7 @@ const assetRouter = {
136
136
  title: z.string().max(500).nullable().optional()
137
137
  })).handler(async ({ input, context }) => {
138
138
  const { spaceId: _spaceId, id, ...data } = input;
139
- return context.repos.assets.update(id, data);
139
+ return context.media.present(await context.media.update(id, data));
140
140
  }),
141
141
  /**
142
142
  * An image's crop and focal point. `null` clears one; omitting it also clears it, so
@@ -163,6 +163,105 @@ const assetRouter = {
163
163
  })
164
164
  };
165
165
  //#endregion
166
+ //#region src/routers/audit.ts
167
+ const actorKind = z.enum([
168
+ "user",
169
+ "apikey",
170
+ "workflow",
171
+ "system"
172
+ ]);
173
+ const targetKind = z.enum([
174
+ "content",
175
+ "contentType",
176
+ "space",
177
+ "member",
178
+ "user",
179
+ "apiKey",
180
+ "menu",
181
+ "role",
182
+ "workflow",
183
+ "workflowRun",
184
+ "asset",
185
+ "session"
186
+ ]);
187
+ const filterSchema$1 = z.object({
188
+ actorKind: actorKind.optional(),
189
+ actorId: z.string().max(200).optional(),
190
+ actions: z.array(z.enum(AUDIT_ACTIONS)).max(60).optional(),
191
+ targetKind: targetKind.optional(),
192
+ targetId: z.string().max(200).optional(),
193
+ from: z.coerce.date().optional(),
194
+ to: z.coerce.date().optional(),
195
+ search: searchTerm.optional()
196
+ });
197
+ const sortSchema$1 = z.object({
198
+ by: z.enum([
199
+ "at",
200
+ "action",
201
+ "actorLabel",
202
+ "targetKind",
203
+ "targetLabel"
204
+ ]).default("at"),
205
+ direction: z.enum(["asc", "desc"]).default("desc")
206
+ });
207
+ const paginationSchema$1 = pagination({
208
+ limit: 50,
209
+ max: 200
210
+ });
211
+ /**
212
+ * The activity log. Reading a space's log takes `audit:read` there; the instance-wide
213
+ * view, which includes actions outside any space (accounts, keys, creating spaces), is a
214
+ * superadmin's. Nothing here writes: the log is appended where the actions happen.
215
+ */
216
+ const auditRouter = {
217
+ /** The actions, actor kinds and target kinds the filter can name. */
218
+ catalog: authed.handler(async ({ context }) => context.audit.catalog()),
219
+ list: scoped("audit:read").input(z.object({
220
+ spaceId: uuid,
221
+ filter: filterSchema$1.default({}),
222
+ sort: sortSchema$1.default({
223
+ by: "at",
224
+ direction: "desc"
225
+ }),
226
+ pagination: paginationSchema$1.default({
227
+ limit: 50,
228
+ offset: 0
229
+ })
230
+ })).handler(async ({ input, context }) => context.audit.list(input.spaceId, input.filter, input.sort, input.pagination)),
231
+ /**
232
+ * Every entry on the instance, or with `instanceOnly` just those outside any space.
233
+ * Superadmin, because it crosses spaces.
234
+ */
235
+ listInstance: superadmin.input(z.object({
236
+ instanceOnly: z.boolean().default(false),
237
+ filter: filterSchema$1.default({}),
238
+ sort: sortSchema$1.default({
239
+ by: "at",
240
+ direction: "desc"
241
+ }),
242
+ pagination: paginationSchema$1.default({
243
+ limit: 50,
244
+ offset: 0
245
+ })
246
+ })).handler(async ({ input, context }) => context.audit.listInstance({
247
+ ...input.filter,
248
+ ...input.instanceOnly ? { spaceId: null } : {}
249
+ }, input.sort, input.pagination)),
250
+ get: scoped("audit:read").input(z.object({
251
+ spaceId: uuid,
252
+ id: uuid
253
+ })).handler(async ({ input, context }) => context.audit.get(input.spaceId, input.id)),
254
+ /** What has been recorded about one thing in the space: a document's history. */
255
+ forTarget: scoped("audit:read").input(z.object({
256
+ spaceId: uuid,
257
+ targetKind,
258
+ targetId: z.string().max(200),
259
+ limit: z.number().int().min(1).max(200).default(100)
260
+ })).handler(async ({ input, context }) => context.audit.forTarget(input.spaceId, input.targetKind, input.targetId, input.limit)),
261
+ /** Walks the whole chain and says whether every entry still hashes to what it claims. */
262
+ verify: superadmin.handler(async ({ context }) => context.audit.verify())
263
+ };
264
+ //#endregion
166
265
  //#region src/routers/content.ts
167
266
  const filterSchema = z.object({
168
267
  spaceId: uuid,
@@ -202,6 +301,8 @@ const paginationSchema = pagination({
202
301
  limit: 25,
203
302
  max: 200
204
303
  });
304
+ /** What an author says when asking, or a reviewer when answering. */
305
+ const note = z.string().max(2e3);
205
306
  const sortSchema = z.array(z.object({
206
307
  by: z.enum([
207
308
  "position",
@@ -270,7 +371,10 @@ const contentRouter = {
270
371
  }),
271
372
  create: scoped("content:write").input(saveSchema).handler(async ({ input, context }) => {
272
373
  assertOnType(context, input.spaceId, "content:write", input.typeId);
273
- return context.content.create(input, toActor(context, input.spaceId));
374
+ const actor = toActor(context, input.spaceId);
375
+ const row = await context.content.create(input, actor);
376
+ if (context.manablox.contentTypes.get(row.typeId).requiresApproval && !input.localizationId && !can(context.principal, input.spaceId, "content:publish", row.typeId)) await context.approvals.request(input.spaceId, row.id, actor);
377
+ return row;
274
378
  }),
275
379
  update: scoped("content:write").input(saveSchema.extend({ id: uuid })).handler(async ({ input, context }) => {
276
380
  assertOnType(context, input.spaceId, "content:write", input.typeId);
@@ -322,6 +426,49 @@ const contentRouter = {
322
426
  await assertOnDocument(context, input.spaceId, "content:write", input.id);
323
427
  return context.content.createTranslation(input.spaceId, input.id, input.locale, toActor(context, input.spaceId));
324
428
  }),
429
+ /** Where a document stands in review: the open request, the last decision, the history. */
430
+ approval: scoped("content:read").input(z.object({
431
+ spaceId: uuid,
432
+ id: uuid
433
+ })).handler(async ({ input, context }) => {
434
+ await assertOnDocument(context, input.spaceId, "content:read", input.id);
435
+ return context.approvals.state(input.spaceId, input.id);
436
+ }),
437
+ /** The open requests the caller may decide on: those of the types they can publish. */
438
+ pendingApprovals: scoped("content:publish").input(z.object({ spaceId: uuid })).handler(async ({ input, context }) => context.approvals.pending(input.spaceId, allowedTypeIds(context.principal, input.spaceId, "content:publish"))),
439
+ requestApproval: scoped("content:write").input(z.object({
440
+ spaceId: uuid,
441
+ id: uuid,
442
+ note: note.optional()
443
+ })).handler(async ({ input, context }) => {
444
+ await assertOnDocument(context, input.spaceId, "content:write", input.id);
445
+ return context.approvals.request(input.spaceId, input.id, toActor(context, input.spaceId), input.note?.trim() || null);
446
+ }),
447
+ withdrawApproval: scoped("content:write").input(z.object({
448
+ spaceId: uuid,
449
+ id: uuid
450
+ })).handler(async ({ input, context }) => {
451
+ await assertOnDocument(context, input.spaceId, "content:write", input.id);
452
+ return context.approvals.withdraw(input.spaceId, input.id, toActor(context, input.spaceId));
453
+ }),
454
+ /** Approves and publishes; the requester is told. */
455
+ approve: scoped("content:publish").input(z.object({
456
+ spaceId: uuid,
457
+ id: uuid,
458
+ note: note.optional()
459
+ })).handler(async ({ input, context }) => {
460
+ await assertOnDocument(context, input.spaceId, "content:publish", input.id);
461
+ return context.approvals.approve(input.spaceId, input.id, toActor(context, input.spaceId), input.note?.trim() || null);
462
+ }),
463
+ /** Sends the document back with a note; the requester is told. */
464
+ reject: scoped("content:publish").input(z.object({
465
+ spaceId: uuid,
466
+ id: uuid,
467
+ note: note.optional()
468
+ })).handler(async ({ input, context }) => {
469
+ await assertOnDocument(context, input.spaceId, "content:publish", input.id);
470
+ return context.approvals.reject(input.spaceId, input.id, toActor(context, input.spaceId), input.note?.trim() || null);
471
+ }),
325
472
  versions: scoped("content:read").input(z.object({
326
473
  spaceId: uuid,
327
474
  id: uuid
@@ -403,6 +550,7 @@ const contentTypeSchema = z.object({
403
550
  isPublishable: z.boolean().optional(),
404
551
  isVisibleInTree: z.boolean().optional(),
405
552
  canBeVisibleInMenu: z.boolean().optional(),
553
+ requiresApproval: z.boolean().optional(),
406
554
  fields: z.array(fieldSchema).default([])
407
555
  });
408
556
  const contentTypeRouter = {
@@ -519,6 +667,51 @@ const menuRouter = {
519
667
  })).handler(async ({ input, context }) => context.menus.usedIn(input.spaceId, input.localizationId))
520
668
  };
521
669
  //#endregion
670
+ //#region src/routers/notification.ts
671
+ const kind = z.enum(NOTIFICATION_KINDS);
672
+ const ids = z.array(uuid).min(1).max(200);
673
+ /**
674
+ * Each kind's channels, every one optional: absent means the default. A partial record,
675
+ * so a kind not named keeps its default; strict objects, so a misspelt channel is an
676
+ * error rather than a silently dropped choice.
677
+ */
678
+ const preferencesSchema = z.partialRecord(kind, z.strictObject(Object.fromEntries(NOTIFICATION_CHANNELS.map((channel) => [channel, z.boolean().optional()]))));
679
+ /**
680
+ * The caller's own inbox and preferences. Nothing here takes a `userId`: every
681
+ * procedure acts as the signed-in account, so there is no way to read or change what
682
+ * belongs to someone else.
683
+ */
684
+ const notificationRouter = {
685
+ /** The kinds, the channels, and which channels this instance can actually send on. */
686
+ catalog: authed.handler(async ({ context }) => context.notifications.catalog()),
687
+ list: authed.input(z.object({
688
+ unreadOnly: z.boolean().default(false),
689
+ kinds: z.array(kind).optional(),
690
+ /** A space's notifications, `null` for instance-wide ones, absent for all. */
691
+ spaceId: uuid.nullable().optional(),
692
+ pagination: pagination({
693
+ limit: 25,
694
+ max: 100
695
+ }).optional()
696
+ }).default({ unreadOnly: false })).handler(async ({ input, context }) => context.notifications.list(context.principal.userId, {
697
+ unreadOnly: input.unreadOnly,
698
+ kinds: input.kinds,
699
+ spaceId: input.spaceId
700
+ }, input.pagination ?? {
701
+ limit: 25,
702
+ offset: 0
703
+ })),
704
+ unreadCount: authed.handler(async ({ context }) => ({ count: await context.notifications.unreadCount(context.principal.userId) })),
705
+ markRead: authed.input(z.object({ ids })).handler(async ({ input, context }) => ({ changed: await context.notifications.markRead(context.principal.userId, input.ids) })),
706
+ markUnread: authed.input(z.object({ ids })).handler(async ({ input, context }) => ({ changed: await context.notifications.markUnread(context.principal.userId, input.ids) })),
707
+ markAllRead: authed.handler(async ({ context }) => ({ changed: await context.notifications.markAllRead(context.principal.userId) })),
708
+ delete: authed.input(z.object({ ids })).handler(async ({ input, context }) => ({ deleted: await context.notifications.delete(context.principal.userId, input.ids) })),
709
+ deleteRead: authed.handler(async ({ context }) => ({ deleted: await context.notifications.deleteRead(context.principal.userId) })),
710
+ /** Every kind with the caller's effective choice per channel, defaults filled in. */
711
+ preferences: authed.handler(async ({ context }) => context.notifications.preferences(context.principal.userId)),
712
+ setPreferences: authed.input(z.object({ preferences: preferencesSchema })).handler(async ({ input, context }) => context.notifications.setPreferences(context.principal.userId, input.preferences))
713
+ };
714
+ //#endregion
522
715
  //#region src/routers/role.ts
523
716
  const roleSchema = z.object({
524
717
  spaceId: uuid,
@@ -559,6 +752,8 @@ const roleRouter = {
559
752
  };
560
753
  //#endregion
561
754
  //#region src/routers/space.ts
755
+ /** The parts of a space an export carries or an import restores; all of them when absent. */
756
+ const exportSections = z.array(z.enum(SPACE_EXPORT_SECTIONS)).min(1);
562
757
  const spaceSchema = z.object({
563
758
  name: z.string().min(1).max(200),
564
759
  machineName,
@@ -586,7 +781,21 @@ const spaceRouter = {
586
781
  return allowed ? all.filter((space) => allowed.includes(space.id)) : all;
587
782
  }),
588
783
  get: scoped("space:read").input(z.object({ spaceId: uuid })).handler(async ({ input, context }) => context.repos.spaces.findById(input.spaceId)),
589
- create: superadmin.input(spaceSchema).handler(async ({ input, context }) => context.spaces.create(input, context.principal?.userId ?? null)),
784
+ /**
785
+ * `starter` fills the new space with the basic setup, a content model, a few
786
+ * published pages and a main menu, so the first thing its owner sees is a site rather
787
+ * than an empty tree. See `applySpaceStarter`.
788
+ */
789
+ create: superadmin.input(spaceSchema.extend({ starter: z.boolean().default(false) })).handler(async ({ input, context }) => {
790
+ const { starter, ...data } = input;
791
+ const { principal } = context;
792
+ const space = await context.spaces.create(data, principal.userId);
793
+ if (starter) await applySpaceStarter(context, space, {
794
+ userId: principal.userId,
795
+ roles: actorRoles(principal, space.id)
796
+ });
797
+ return space;
798
+ }),
590
799
  update: scoped("space:write").input(spaceSchema.partial().extend({ spaceId: uuid })).handler(async ({ input, context }) => {
591
800
  const { spaceId, ...data } = input;
592
801
  return context.spaces.update(spaceId, data);
@@ -617,9 +826,15 @@ const spaceRouter = {
617
826
  * an export is every field of every document in one file, regardless of who may read
618
827
  * what.
619
828
  */
620
- export: scoped("space:write").input(z.object({ spaceId: uuid })).handler(async ({ input, context }) => context.spaces.export(input.spaceId)),
829
+ export: scoped("space:write").input(z.object({
830
+ spaceId: uuid,
831
+ sections: exportSections.optional()
832
+ })).handler(async ({ input, context }) => context.spaces.export(input.spaceId, input.sections)),
621
833
  /** Restores such a document into an instance that does not hold the space yet. Superadmin, because it creates a space. */
622
- import: superadmin.input(z.object({ payload: z.unknown() })).handler(async ({ input, context }) => context.spaces.import(input.payload, context.principal.userId)),
834
+ import: superadmin.input(z.object({
835
+ payload: z.unknown(),
836
+ sections: exportSections.optional()
837
+ })).handler(async ({ input, context }) => context.spaces.import(input.payload, context.principal.userId, input.sections)),
623
838
  members: scoped("user:read").input(z.object({ spaceId: uuid })).handler(async ({ input, context }) => context.spaces.members(input.spaceId)),
624
839
  grant: scoped("user:write").input(z.object({
625
840
  spaceId: uuid,
@@ -730,6 +945,19 @@ const userRouter = {
730
945
  await context.users.delete(context.principal.userId, input.userId);
731
946
  return { ok: true };
732
947
  }),
948
+ /** The caller's own profile: name and email. The instance role is not theirs to set. */
949
+ updateProfile: authed.input(z.object({
950
+ name: displayName.optional(),
951
+ email: email.optional()
952
+ })).handler(async ({ input, context }) => context.users.update(context.principal.userId, input)),
953
+ /** A new password for the caller, given the current one. Other sessions stay signed in. */
954
+ changePassword: authed.input(z.object({
955
+ currentPassword: z.string().min(1).max(200),
956
+ password
957
+ })).handler(async ({ input, context }) => {
958
+ await context.users.changePassword(context.principal.userId, input.currentPassword, input.password);
959
+ return { ok: true };
960
+ }),
733
961
  apiKeys: authed.handler(async ({ context }) => context.apiKeys.list(context.principal.userId)),
734
962
  issueApiKey: authed.input(z.object({
735
963
  name: z.string().min(1).max(100),
@@ -958,7 +1186,7 @@ const workflowRouter = {
958
1186
  //#region src/context.ts
959
1187
  /** The runtime's RPC-facing slice, without whatever else the host keeps on it. */
960
1188
  function pickRpcRuntime(runtime) {
961
- const { manablox, repos, auth, apiKeys, media, content, contentTypes, spaces, users, menus, roles, workflows } = runtime;
1189
+ const { manablox, repos, auth, apiKeys, media, content, contentTypes, spaces, users, menus, roles, workflows, audit, notifications, approvals } = runtime;
962
1190
  return {
963
1191
  manablox,
964
1192
  repos,
@@ -971,7 +1199,10 @@ function pickRpcRuntime(runtime) {
971
1199
  users,
972
1200
  menus,
973
1201
  roles,
974
- workflows
1202
+ workflows,
1203
+ audit,
1204
+ notifications,
1205
+ approvals
975
1206
  };
976
1207
  }
977
1208
  //#endregion
@@ -988,7 +1219,9 @@ const router = {
988
1219
  users: userRouter,
989
1220
  menus: menuRouter,
990
1221
  roles: roleRouter,
991
- workflows: workflowRouter
1222
+ workflows: workflowRouter,
1223
+ audit: auditRouter,
1224
+ notifications: notificationRouter
992
1225
  };
993
1226
  //#endregion
994
1227
  export { authed, base, pickRpcRuntime, router, scoped, superadmin, toOrpcError };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@manablox/api-rpc",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {
@@ -11,12 +11,12 @@
11
11
  "main": "./dist/index.js",
12
12
  "types": "./dist/index.d.ts",
13
13
  "dependencies": {
14
- "@manablox/auth": "0.3.0",
15
- "@manablox/core": "0.3.0",
16
- "@manablox/db": "0.3.0",
17
- "@manablox/media": "0.3.0",
18
- "@manablox/services": "0.3.0",
19
- "@manablox/workflows": "0.3.0",
14
+ "@manablox/auth": "0.4.0",
15
+ "@manablox/core": "0.4.0",
16
+ "@manablox/db": "0.4.0",
17
+ "@manablox/media": "0.4.0",
18
+ "@manablox/services": "0.4.0",
19
+ "@manablox/workflows": "0.4.0",
20
20
  "@orpc/server": "^1.15.0",
21
21
  "@orpc/openapi": "^1.15.0",
22
22
  "zod": "^4.5.4"