@go-labs-sg/rsvp 0.15.0 → 0.17.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/README.md +240 -0
  2. package/dist/index.js +23 -2
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -135,6 +135,87 @@ rsvp event flow versions list --organization-id org_123 --event-id evt_123
135
135
  rsvp event flow resources get --organization-id org_123 --event-id evt_123
136
136
  ```
137
137
 
138
+ ### Registration-flow image upload lifecycle
139
+
140
+ Registration-flow images are **named, event-local assets**. Create one with an
141
+ optional `altText`, upload the bytes to the returned presigned URL, and then
142
+ finalize that same asset. Creation and finalization store the asset; neither
143
+ changes the draft or publishes a registration flow.
144
+
145
+ The API validator accepts `image/jpeg`, `image/png`, and `image/webp`. `byteSize`
146
+ must be a positive integer no larger than **10 MiB (10,485,760 bytes)**. The
147
+ server rechecks the uploaded object's content type and exact declared size when
148
+ finalizing. Use `event flow resources get` to verify the finalized asset ID in
149
+ that event's catalog; never reuse an actual event's asset ID in a test event.
150
+
151
+ ```sh
152
+ created="$(rsvp event flow image upload create \
153
+ --allow-external-write \
154
+ --allow-state-change \
155
+ --target evt_123 \
156
+ --payload '{"organizationId":"org_123","eventId":"evt_123","mimeType":"image/png","byteSize":2048,"altText":"Beef entrée"}')"
157
+
158
+ # Read data.asset.id and data.uploadUrl from the JSON response, then PUT the exact
159
+ # bytes to uploadUrl using the same MIME type and declared byteSize.
160
+ curl --fail --upload-file ./beef-entrée.png \
161
+ -H 'Content-Type: image/png' \
162
+ "<uploadUrl from the create response>"
163
+
164
+ rsvp event flow image finalize \
165
+ --allow-state-change \
166
+ --target '<asset.id from the create response>' \
167
+ --payload '{"organizationId":"org_123","eventId":"evt_123","assetId":"<asset.id from the create response>"}'
168
+ ```
169
+
170
+ The CLI does not perform the external `PUT`, retry uploads automatically, or
171
+ bypass authentication, event permissions, storage validation, or the existing
172
+ server contract. If the upload or finalization fails, inspect the error and
173
+ perform an explicit operator action; do not invent a new storage or retry
174
+ protocol. Reference the finalized asset from the appropriate form choice, then
175
+ save and publish the flow separately when the complete draft is approved.
176
+
177
+
178
+ ## Per-form confirmation presentation
179
+
180
+ In the form builder's **Flow settings → Confirmation and check-in pass**, set
181
+ lucky-draw visibility, primary/additional participant labels, and organiser
182
+ support-contact visibility. Save and publish the exact form to apply changes.
183
+ Draft edits do not affect live passes.
184
+
185
+ The same settings are supported in `definition.confirmation` through
186
+ `event flow draft save`:
187
+
188
+ ```json
189
+ {
190
+ "confirmation": {
191
+ "showLuckyDrawId": false,
192
+ "showSupportContact": false,
193
+ "primaryAttendeeLabel": "Participant 1",
194
+ "additionalAttendeeLabel": "Participant 2"
195
+ }
196
+ }
197
+ ```
198
+
199
+ Merge these fields into the complete current definition; this is not a complete
200
+ save payload. Include `organizationId`, `eventId`, the exact `flowId`, and the
201
+ latest `expectedRevision`, then publish with `expectedDraftRevision`. Use the
202
+ event ID as `--target` and pass `--allow-state-change` for both mutations.
203
+
204
+ Omitted visibility settings retain legacy behavior (shown). Omitted labels use
205
+ the stored attendee-type label; `null` hides a label. Custom labels must be
206
+ nonempty after trimming and at most 120 characters. Completion `contactEmail`
207
+ is optional; hiding support contact does not remove an email written into the
208
+ completion message or hide the participant's confirmation-recipient address.
209
+
210
+ Published display settings and completion message/contact updates apply to
211
+ existing registrations of the same form. Their saved questions, answer scope,
212
+ registration outcomes, attendee roles, lucky-draw records, QR credentials, and
213
+ check-in references remain unchanged. `event flow published get` accepts
214
+ `confirmation: true` in its payload for this saved-response presentation; omit
215
+ it for the active registration/edit definition. Deploy the updated server before
216
+ saving these settings because older validators discard unsupported fields.
217
+ No database schema migration or attendee backfill is required.
218
+
138
219
  ## Administrator registration for an invitee
139
220
 
140
221
  Inspect the invitee's active registration form and current registration version
@@ -221,6 +302,51 @@ rsvp check-in by-reference \
221
302
 
222
303
  High-impact payment and retention commands remain protected by their matching financial, deletion, external-write, and sensitive-data gates. The API always re-evaluates the authenticated user's current Registration role, organization membership, event assignment, and resource permissions.
223
304
 
305
+ ## Operational report exports
306
+
307
+ `export request` accepts `format: "XLSX"` (the default when omitted) or `"CSV"`.
308
+ Use `filters.view` to name the population rather than relying on a dataset label:
309
+
310
+ - RSVP: `MASTER` (all invitees, grouped employee/guest responses; default),
311
+ `CURRENT` (current response rows per person), `HISTORY` (all registration
312
+ records, not every edit revision), or `CLIENT_REGISTRATION` (current completed
313
+ employee/guest responses, including declines; excludes nonrespondents/drafts).
314
+ The client XLSX has **Attending**, **Not Attending**, **Master**, and **Metadata**
315
+ sheets; its CSV contains Master data only. Guest staff/department fields
316
+ describe the host, while menu and dietary answers remain person-scoped.
317
+ - SEATING: `CURRENT` (active bookings; default) or `HISTORY` (active and released).
318
+ - ATTENDANCE: `CURRENT` (expected people; default) or `CHECKED_IN` (expected people
319
+ with a check-in that has not been undone), or `ALL_INVITEES` (roster invitees,
320
+ including nonrespondents/declines, plus current guests). Registered, submitted,
321
+ attending, and active check-in states remain separate. None defines draw
322
+ eligibility.
323
+
324
+ Form, registration-status and submission-date filters apply only to RSVP.
325
+ Other datasets do not accept `view`. These combinations are validated by the API.
326
+
327
+ ```sh
328
+ rsvp export request \
329
+ --ack-sensitive-data \
330
+ --allow-state-change \
331
+ --target evt_123 \
332
+ --payload '{"organizationId":"org_123","eventId":"evt_123","dataset":"RSVP","format":"XLSX","filters":{"view":"MASTER"},"reason":"Organizer master list"}'
333
+ ```
334
+
335
+ Use `export list` to check the requester-scoped job, then `export download` for
336
+ its current authorized download URL. For the client workbook, replace `MASTER`
337
+ in the example with `CLIENT_REGISTRATION`. For the all-invitee attendance report,
338
+ set `dataset` to `ATTENDANCE` and `filters.view` to `ALL_INVITEES`.
339
+
340
+ Operational check-in accepts `ATTENDING` and legacy `SUBMITTED`; roster/detail,
341
+ manual/QR/reference entry, `report overview`, and `event people overview` use that same
342
+ population. Non-operational statuses remain excluded, and permissions, payment
343
+ gates, duplicate protection, and undo behavior are unchanged. The web panel's
344
+ realtime connection and manual refresh controls do not add a CLI subscription.
345
+
346
+ Deploy matching API and export-worker versions before requesting the new views;
347
+ old servers/workers do not understand them. Existing CLI commands pass the JSON
348
+ payload to the shared typed API, so no new command or client-only rules are needed.
349
+
224
350
  ## Seating updates
225
351
 
226
352
  `event seating layout update` moves tables, changes table geometry, and can save a floor plan. It requires `--allow-state-change`; its `--target` must match the payload's top-level `eventId`, not a nested table or seat-map ID:
@@ -242,6 +368,106 @@ rsvp event seating table update \
242
368
  --payload '{"organizationId":"org_123","eventId":"evt_123","tableId":"table_123","label":"Table 1"}'
243
369
  ```
244
370
 
371
+ ## Live seating and seat-change notifications
372
+
373
+ Deploy the matching API and worker before using these commands from an updated
374
+ CLI. They retain browser authentication, event permissions and sensitive-field
375
+ grants; the CLI does not implement separate seating rules.
376
+
377
+ ```sh
378
+ rsvp event seating live get --ack-sensitive-data \
379
+ --payload '{"organizationId":"org_123","eventId":"evt_123"}'
380
+ rsvp event seating occupants list --ack-sensitive-data \
381
+ --payload '{"organizationId":"org_123","eventId":"evt_123","seatMapId":"map_123","categoryValue":"finance","limit":25,"cursor":0}'
382
+ rsvp people registration edit get --ack-sensitive-data \
383
+ --payload '{"organizationId":"org_123","eventId":"evt_123","registrationId":"reg_123"}'
384
+ ```
385
+
386
+ Occupants supports optional `tableId`, `department`, `categoryValue` and `query`
387
+ filters. `categoryValue` is the canonical (trimmed, case-normalized) value
388
+ returned by the occupant response's category legend; it is not an arbitrary
389
+ field lookup. Follow `nextCursor` until null; counts and department/category
390
+ context contain only permitted data. The API projects all active occupants of the
391
+ selected map in batches before filtering; small pages are not database-level
392
+ filtered pagination.
393
+
394
+ ### Imported-field seating categories
395
+
396
+ Category coloring is optional and disabled by default. An authorized organizer
397
+ can read the eligible imported fields and their actual event values, then save one
398
+ selected field and a unique six-digit hex color for each canonical value. The
399
+ server includes unregistered invitees when it discovers the imported values;
400
+ guests inherit the host's imported value. It excludes identity, contact, system,
401
+ nested and multi-value data, and enforces sensitive-field grants before it returns
402
+ field keys, values, legends, filters or seat colors.
403
+
404
+ ```sh
405
+ rsvp event seating category config get --ack-sensitive-data \
406
+ --payload '{"organizationId":"org_123","eventId":"evt_123"}'
407
+
408
+ rsvp event seating category config update \
409
+ --ack-sensitive-data --allow-state-change --target evt_123 \
410
+ --payload '{"organizationId":"org_123","eventId":"evt_123","configurationVersion":3,"config":{"enabled":true,"fieldKey":"department","seed":"chosen-once","colors":[{"value":"finance","color":"#155e75"},{"value":"operations","color":"#7c2d12"}]}}'
411
+ ```
412
+
413
+ The get command is sensitive-data acknowledged. The update command additionally
414
+ requires explicit state-change confirmation and a target matching `eventId`; it
415
+ forwards `configurationVersion` for optimistic concurrency and the complete
416
+ configuration payload unchanged. It never assigns colors, interprets imported
417
+ values, or bypasses authorization in the CLI.
418
+
419
+ When categories are enabled, the API preserves saved mappings while new imported
420
+ values arrive and assigns only unused colors. Distinct groups never share a color;
421
+ there is an explicit capacity error instead of palette reuse or truncation. An
422
+ explicit auto-assign-all save is the only intentional re-randomization. Blank,
423
+ missing, unreadable and unauthorized values stay neutral. Disable the setting to
424
+ return ordinary seating status colors while retaining the selected field and
425
+ stable mappings. Clearing the selected field explicitly resets its mappings. Deploy matching web, API and CLI releases, then
426
+ perform separate live acceptance before enabling it for an event.
427
+
428
+ `people registration update` forwards the organizer-edit API. Supply the current
429
+ `expectedVersion`, audit `reason`, and complete `seats` array preserving unchanged
430
+ party assignments. Realtime-enabled changes additionally require valid exact-seat
431
+ holds through the existing realtime protocol; the CLI does not acquire holds.
432
+ Use `--ack-sensitive-data --allow-state-change --target <registration-id>`.
433
+ Saving does not send email unless the payload opts in. Re-read the revision
434
+ after conflicts.
435
+
436
+ To notify for a seat-only move, add `seatChangeNotice` to the same save. The
437
+ notice is queued in the move's database transaction, so the move and notice
438
+ request commit together or not at all; this additionally requires
439
+ `--allow-email`. It is rejected alongside identity, contact or response edits.
440
+
441
+ ```sh
442
+ rsvp people registration update \
443
+ --ack-sensitive-data --allow-state-change --allow-email --target reg_123 \
444
+ --payload '{"organizationId":"org_123","eventId":"evt_123","registrationId":"reg_123","expectedVersion":4,"reason":"Moved to finance table","seats":[{"registrationAttendeeId":"att_1","seatId":"seat_9"},{"registrationAttendeeId":"att_2","seatId":"seat_2"}],"seatChangeNotice":{"idempotencyKey":"4d68a320-013d-43c7-bfcc-d1126827d8c5"}}'
445
+ ```
446
+
447
+ A separate manual notice for an already committed revision remains available:
448
+
449
+ ```sh
450
+ rsvp people registration confirmation resend \
451
+ --allow-email --allow-state-change --target reg_123 \
452
+ --payload '{"organizationId":"org_123","eventId":"evt_123","registrationId":"reg_123","purpose":"SEAT_CHANGE","expectedRegistrationVersion":5,"idempotencyKey":"4d68a320-013d-43c7-bfcc-d1126827d8c5"}'
453
+ ```
454
+
455
+ Replace the sample UUID with a new UUID for a new request; reuse that same UUID
456
+ for retries of that request. Omitted `purpose` remains `CONFIRMATION`; seat-change
457
+ requests require the committed `expectedRegistrationVersion`. Queue acceptance
458
+ is not delivery. Stale revisions, unavailable copy and current permissions are
459
+ checked by the server, and delivery rechecks the current recipient and active
460
+ seats without including names, private answers or QR codes.
461
+
462
+ Legacy copy is `seatChangeNotification: {enabled, subject, body}` in the
463
+ `REGISTRATION_RULES` payload for `event setup step update`, with the current
464
+ configuration version. Configurable copy is
465
+ `definition.communications.participant.seatChange: {enabled, subject, body}`;
466
+ merge into the complete named draft, preview, save with its revision, read back,
467
+ then publish only with separate approval. Existing responses use their saved
468
+ published version. Both default disabled/blank; missing copy never falls back to
469
+ confirmation copy. No event-specific copy is supplied or enabled by this change.
470
+
245
471
  ## Audience tiers
246
472
 
247
473
  Audience tiers control which invitees can use a seat or table. List the tiers and their current invitee, table, and seat counts with:
@@ -309,6 +535,20 @@ rsvp template create \
309
535
 
310
536
  Instantiate the returned template version through `rsvp template instantiate`, then use the existing staged roster import. The example expects roster columns `email`, `name`, `staffId`, and `employmentType`; only `FTE` and `INTERN` values are configured as eligible profiles. Rules for other events should be expressed in their own template payloads using the same generic contracts.
311
537
 
538
+ ## Monorepo development
539
+
540
+ Follow the [development workflow](../../../docs/development-workflow.md#required-cli-coverage). App/API changes affecting this CLI must update its commands, types/input-output contracts, help/docs and verification in the same change. Use the existing authenticated product API rather than CLI-only business logic. Presentation-only changes do not require CLI edits; record why when no CLI update was needed.
541
+
542
+ From the repository root after isolated-worktree initialization:
543
+
544
+ ```bash
545
+ bun --filter @go-labs-sg/rsvp typecheck
546
+ bun --filter @go-labs-sg/rsvp build
547
+ bun run packages/rsvp/cli/src/index.ts help
548
+ ```
549
+
550
+ Building is not publication or permission to mutate live data. Use focused temporary business-behavior tests and the final pre-push gate from the shared workflow. Use this package's actual scripts; do not assume it has a command-artifact generator.
551
+
312
552
  ## Managed organization provisioning
313
553
 
314
554
  `organization create` requires an `ownerEmail` in its JSON payload and the
package/dist/index.js CHANGED
@@ -6549,7 +6549,7 @@ var commands = {
6549
6549
  effects: ["sensitive-data", "state-change"],
6550
6550
  operation: (api, input) => api.export.requestExport.mutate(input),
6551
6551
  payloadOnly: true,
6552
- summary: "Request an export (--payload JSON)."
6552
+ summary: "Request an operational report (--payload JSON; format CSV/XLSX, default XLSX; RSVP views MASTER/CURRENT/HISTORY/CLIENT_REGISTRATION; ATTENDANCE views CURRENT/CHECKED_IN/ALL_INVITEES; SEATING views CURRENT/HISTORY)."
6553
6553
  },
6554
6554
  "export list": {
6555
6555
  effects: ["sensitive-data"],
@@ -6626,6 +6626,8 @@ Object.assign(commands, {
6626
6626
  "admission identity-exception resolve": payloadCommand((api, input) => api.admission.resolveIdentityException.mutate(input), "Resolve an identity exception (--payload JSON).", ["sensitive-data", "state-change"]),
6627
6627
  "audit list": payloadCommand((api, input) => api.audit.listEventActivity.query(input), "List event audit activity (--payload JSON).", ["sensitive-data"]),
6628
6628
  "event check-in qr": payloadCommand((api, input) => api.event.checkInByQr.mutate(input), "Check in using a QR token (--payload JSON).", ["state-change"]),
6629
+ "event flow image upload create": payloadCommand((api, input) => api.eventFlow.createImageAssetUpload.mutate(input), "Create an event-local registration-flow image upload URL (--payload JSON).", ["external-write", "state-change"], "eventId"),
6630
+ "event flow image finalize": payloadCommand((api, input) => api.eventFlow.finalizeImageAsset.mutate(input), "Finalize an event-local registration-flow image after the external upload (--payload JSON).", ["state-change"], "assetId"),
6629
6631
  "event logo upload create": payloadCommand((api, input) => api.event.createLogoUpload.mutate(input), "Create an event-logo upload URL (--payload JSON).", ["external-write", "state-change"], "eventId"),
6630
6632
  "event logo update": payloadCommand((api, input) => api.event.updateLogo.mutate(input), "Set or clear the event logo (--payload JSON).", ["state-change"], "eventId"),
6631
6633
  "event banner upload create": payloadCommand((api, input) => api.event.createBannerUpload.mutate(input), "Create a banner-image upload URL (--payload JSON).", ["external-write", "state-change"], "eventId"),
@@ -6652,6 +6654,18 @@ Object.assign(commands, {
6652
6654
  "event reminder counts": payloadCommand((api, input) => api.event.getReminderSegmentCounts.query(input), "Get reminder segment counts (--payload JSON).", ["sensitive-data"]),
6653
6655
  "event rsvp manifest": payloadCommand((api, input) => api.event.getRsvpManifest.query(input), "Get the caller's registration manifest (--payload JSON).", ["sensitive-data"]),
6654
6656
  "event seating availability": payloadCommand((api, input) => api.event.getSeatAvailability.query(input), "Get seat availability (--payload JSON)."),
6657
+ "event seating live get": payloadCommand((api, input) => api.event.getLiveSeating.query(input), "Get the selected live seating map (--payload JSON).", ["sensitive-data"]),
6658
+ "event seating category config get": payloadCommand((api, input) => api.event.getSeatingCategoryConfig.query(input), "Get authorized imported-field seating category settings (--payload JSON).", ["sensitive-data"]),
6659
+ "event seating category config update": payloadCommand((api, input) => api.event.updateSeatingCategoryConfig.mutate(input), "Save imported-field seating category settings with configurationVersion (--payload JSON).", ["sensitive-data", "state-change"], "eventId"),
6660
+ "event seating occupants list": payloadCommand((api, input) => api.event.getLiveSeatingOccupants.query(input), "List permitted occupants by seatMapId, tableId, department, categoryValue and query; follow nextCursor (--payload JSON).", ["sensitive-data"]),
6661
+ "people registration edit get": payloadCommand((api, input) => api.event.getRegistrationForEdit.query(input), "Get current revision, permitted attendees, eligible seats and notification readiness (--payload JSON).", ["sensitive-data"]),
6662
+ "people registration update": {
6663
+ ...payloadCommand((api, input) => api.event.updateRegistration.mutate(input), "Save organizer edits with expectedVersion and reason; seating requires full-party seats and configured holds. Optional seatChangeNotice queues a seat-change notice atomically with a seat-only save (--payload JSON).", ["sensitive-data", "state-change"], "registrationId"),
6664
+ conditionalEffects: [
6665
+ { effect: "email", whenPayloadField: "seatChangeNotice" }
6666
+ ]
6667
+ },
6668
+ "people registration confirmation resend": payloadCommand((api, input) => api.event.resendRegistrationConfirmation.mutate(input), "Queue CONFIRMATION (default) or SEAT_CHANGE with expectedRegistrationVersion and idempotencyKey; retries reuse that key (--payload JSON).", ["email", "state-change"], "registrationId"),
6655
6669
  "event realtime-token issue": payloadCommand((api, input) => api.event.issueRealtimeToken.mutate(input), "Issue a realtime token (--payload JSON).", ["sensitive-data", "state-change"]),
6656
6670
  "event mine list": payloadCommand((api, input) => api.event.listMyEvents.query(input), "List the caller's events (--payload JSON)."),
6657
6671
  "event reminder list": payloadCommand((api, input) => api.event.listReminderSegments.query(input), "List reminder segments (--payload JSON).", ["sensitive-data"]),
@@ -6725,6 +6739,7 @@ var getCommandManifest = () => ({
6725
6739
  summary: "Show the CLI version and configured API base URL."
6726
6740
  },
6727
6741
  ...Object.entries(commands).map(([path, command]) => ({
6742
+ ...command.conditionalEffects ? { conditionalEffects: command.conditionalEffects } : {},
6728
6743
  effects: command.effects,
6729
6744
  path,
6730
6745
  summary: command.summary
@@ -6966,6 +6981,7 @@ var collectTargets = (input, targetKey) => {
6966
6981
  }
6967
6982
  return [];
6968
6983
  };
6984
+ var getConditionalEffects = (definition, input) => (definition.conditionalEffects ?? []).flatMap(({ effect, whenPayloadField }) => input && Object.hasOwn(input, whenPayloadField) && Reflect.get(input, whenPayloadField) != null ? [effect] : []);
6969
6985
  var requireEffects = async (effects, flags, confirm, input, targetKey) => {
6970
6986
  if (effects.length === 0)
6971
6987
  return;
@@ -7129,7 +7145,12 @@ Waiting for approval\u2026
7129
7145
  if (!definition)
7130
7146
  throw new CliUsageError("Unknown command. Run 'rsvp help' to list commands.");
7131
7147
  const input = getInput(parsed.flags, definition.payloadOnly, definition.scope);
7132
- await requireEffects(definition.effects, parsed.flags, dependencies.confirm, input, definition.targetKey);
7148
+ await requireEffects([
7149
+ ...new Set([
7150
+ ...definition.effects,
7151
+ ...getConditionalEffects(definition, input)
7152
+ ])
7153
+ ], parsed.flags, dependencies.confirm, input, definition.targetKey);
7133
7154
  const accessToken = !dependencies.apiClient && !definition.public ? await getAuthenticatedAccessToken({
7134
7155
  apiBaseUrl,
7135
7156
  store: getCredentialStore()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@go-labs-sg/rsvp",
3
- "version": "0.15.0",
3
+ "version": "0.17.0",
4
4
  "description": "RSVP CLI for authorized event operations.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",