Skip to content

Per 10696 add post archives endpoint - #873

Open
liam-lloyd wants to merge 2 commits into
mainfrom
per-10696_add_post_archives_endpoint
Open

liam-lloyd wants to merge 2 commits into
mainfrom
per-10696_add_post_archives_endpoint

Conversation

@liam-lloyd

@liam-lloyd liam-lloyd commented Sep 15, 2026

Copy link
Copy Markdown
Member

This PR migrates our archive creation endpoint to stela, in
preparation for the 2030 dashboard which we want to build exclusively
with stela endpoints. Some functionality changes worthy of note:

  • This POST /archives does not trigger generation of a default archive
    profile image. We've determined that going forward it will be a
    client-side responsibility to decide what to display when the user has
    not set a profile image.
  • This POST /archives does not act on Relationship invitations; we
    intend to remove and replace the relationships feature entirely.

Comment on lines +23 to +33
(
:archiveId,
'profile.timezone',
NULL,
88,
'status.generic.ok',
'type.profile_item.timezone',
CURRENT_TIMESTAMP,
CURRENT_TIMESTAMP,
CURRENT_TIMESTAMP
);

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried to omit this because it doesn't actually make sense for all archives to have a default timezone, but it turns out that you can't upload to an archive without a timezone, so it has to stay for now.

@liam-lloyd
liam-lloyd force-pushed the per-10696_add_post_archives_endpoint branch from e4728cd to f27a46b Compare September 15, 2026 23:51
@slifty
slifty requested a lite review from Copilot September 15, 2026 23:56
@codecov

codecov Bot commented Sep 15, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 98.66%. Comparing base (d32a7a9) to head (200285a).

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #873      +/-   ##
==========================================
+ Coverage   98.64%   98.66%   +0.02%     
==========================================
  Files          99      105       +6     
  Lines        2808     2929     +121     
  Branches      540      551      +11     
==========================================
+ Hits         2770     2890     +120     
- Misses         38       39       +1     
Flag Coverage Δ
api 98.66% <ø> (+0.02%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Unresolved moderate issues affect archive-number allocation, invite notifications, and concurrent share/access-grant creation.

Get a fresh assessment by requesting another Copilot review.

Pull request overview

Adds the authenticated POST /api/v2/archives endpoint with archive initialization, invite acceptance, notifications, validation, documentation, and tests.

Changes:

  • Implements archive persistence, numbering, default folders, membership, profiles, and events.
  • Processes pending share invites, access grants, and acceptance notifications.
  • Refactors folder services and adds supporting fixtures and tests.
File summaries
File Reviewed changes
packages/api/src/record/service.ts Updates the folder service import.
packages/api/src/invite/service.ts Processes pending invites and grants access. Moderate (1 vote): duplicate folder-link invites use whichever role is encountered first instead of merging permissions.
packages/api/src/invite/service.test.ts Tests invite processing.
packages/api/src/invite/queries/update_invite_status.sql Updates invite statuses.
packages/api/src/invite/queries/update_invite_share_status.sql Updates invite-share statuses.
packages/api/src/invite/queries/get_pending_invites_by_email.sql Retrieves pending share invites.
packages/api/src/invite/queries/get_pending_invite_shares_by_invite_ids.sql Looks up pending shares by invite IDs.
packages/api/src/invite/queries/create_share.sql Creates archive shares. Moderate (1 vote): check-then-insert races can duplicate shares; soft-deleted shares can block replacement.
packages/api/src/invite/queries/create_access_grants_recursive.sql Creates recursive access grants. Moderate (1 vote each): UNION ALL can duplicate grants, concurrent inserts can race, and revoked rows can block new active grants.
packages/api/src/invite/models.ts Defines pending invite-share models.
packages/api/src/invite/fixtures/create_test_records.sql Adds invite test records.
packages/api/src/invite/fixtures/create_test_invites.sql Adds invite fixtures.
packages/api/src/invite/fixtures/create_test_folders.sql Adds invite test folders.
packages/api/src/invite/fixtures/create_test_folder_links.sql Adds invite folder links.
packages/api/src/invite/fixtures/create_test_archives.sql Adds invite test archives.
packages/api/src/invite/fixtures/create_test_accounts.sql Adds invite test accounts.
packages/api/src/folder/service/update_folder.ts Extracts folder update logic.
packages/api/src/folder/service/index.ts Aggregates folder services.
packages/api/src/folder/service/get_folder.ts Houses folder retrieval logic.
packages/api/src/folder/service/get_folder.test.ts Tests folder retrieval.
packages/api/src/folder/service/get_folder_share_links.ts Extracts share-link retrieval.
packages/api/src/folder/service/create_default_folders.ts Creates default archive folders.
packages/api/src/folder/service/create_default_folders.test.ts Tests default folder creation.
packages/api/src/folder/queries/create_folder.sql Creates folders and links.
packages/api/src/folder/controller/controller.ts Uses the aggregated folder services.
packages/api/src/email/service.ts Sends invite acceptance notifications. Moderate (1 vote): malformed roles can produce undefined email merge values.
packages/api/src/email/service.test.ts Tests acceptance emails.
packages/api/src/email/queries/get_share_invitation_acceptance_details.sql Retrieves notification details. Moderate (1 vote each): results are not scoped to the current acceptance batch, non-accepted share rows can be included, and malformed roles or names can reach notifications.
packages/api/src/email/index.ts Exports the notification service.
packages/api/src/email/fixtures/create_test_share_invitation_acceptance.sql Adds email fixtures.
packages/api/src/archive/validators.ts Validates archive creation requests.
packages/api/src/archive/validators.test.ts Tests archive validation.
packages/api/src/archive/service/index.ts Exposes archive creation.
packages/api/src/archive/service/get_shared_folders.ts Updates the folder service import.
packages/api/src/archive/service/create_archive.ts Implements the archive creation workflow. Moderate (2 votes): acceptance notifications are fetched for all accepted invites for the email rather than only the current transaction.
packages/api/src/archive/queries/query_tests/create_archive_query.test.ts Tests archive creation SQL.
packages/api/src/archive/queries/create_initial_profile_item.sql Creates initial profile data.
packages/api/src/archive/queries/create_archive.sql Creates archives and archive numbers. Moderate (3 votes): unsynchronized MAX(archivepart) allocation can collide during concurrent requests.
packages/api/src/archive/queries/create_account_archive.sql Links the owner account.
packages/api/src/archive/models.ts Defines archive creation types and mappings.
packages/api/src/archive/fixtures/create_test_pending_share_invite.sql Adds pending-share fixtures.
packages/api/src/archive/fixtures/create_test_accounts_for_post.sql Adds POST endpoint accounts.
packages/api/src/archive/fixtures/create_test_accounts_for_create_archive.sql Adds archive creation accounts.
packages/api/src/archive/controller/create_archive.test.ts Tests the POST endpoint.
packages/api/src/archive/controller/controller.ts Registers the POST endpoint.
packages/api/docs/src/paths/archive.yaml Documents archive creation.
Review details

Suppressed comments (10)

packages/api/src/email/queries/get_share_invitation_acceptance_details.sql:27

  • This notification query selects every accepted share invite for the email, even though it is called after one specific transaction's pending invites were accepted. When the user accepts another invite while creating a later archive, all previously accepted invites are returned and their acceptance emails are sent again. Pass the IDs accepted by this transaction and constrain the query to those invites.
  LOWER(invite.email) = LOWER(:newAccountEmail)
  AND invite.type = 'type.invite.share'
  AND invite.status = 'status.invite.accepted'

packages/api/src/email/queries/get_share_invitation_acceptance_details.sql:8

  • invite_share.accessrole and the derived shareName can be NULL here, and processPendingInvites explicitly accepts NULL fields and unrecognized roles. Those rows reach the notification service, where the role mapping returns undefined and NULL/undefined merge variables are passed to Mailchimp despite being typed as strings. Exclude malformed shares from this notification query or skip them before constructing the email.
  invite_share.accessrole AS "accessRole",
  COALESCE(
    shared_record.displayname, shared_folder.displayname
  ) AS "shareName"

packages/api/src/email/queries/get_share_invitation_acceptance_details.sql:27

  • This query filters the parent invite by accepted status but not invite_share.status. If an invite has multiple share rows and one is revoked or still pending, accepting another row marks the invite accepted and this query will still generate an email for the non-accepted row. Restrict the joined invite share to status.invite.accepted.
WHERE
  LOWER(invite.email) = LOWER(:newAccountEmail)
  AND invite.type = 'type.invite.share'
  AND invite.status = 'status.invite.accepted'

packages/api/src/email/service.ts:211

  • processPendingInvites explicitly accepts invite shares with null or unrecognized roles after skipping their grant, and still returns those invite IDs. That path triggers this notification, but accessRoleToArchiveMembershipRole returns undefined for such values, so the email receives a non-string access_role merge value. Filter notification rows to valid accepted roles or skip malformed invites before sending.
				const prettyAccessRole = accessRoleToArchiveMembershipRole(
					details.accessRole,
				);

packages/api/src/invite/queries/create_access_grants_recursive.sql:61

  • Because shared_subtree uses UNION ALL, inviting the same account to both a folder link and one of its descendants emits the descendant twice. The NOT EXISTS predicate does not see duplicate source rows produced by this INSERT, and access has no uniqueness constraint, so duplicate grants are inserted. Deduplicate by (archiveid, folder_linkid) before inserting and define how conflicting roles are resolved.
    packages/api/src/invite/queries/create_access_grants_recursive.sql:62
  • The same check-then-insert race exists for access grants: concurrent acceptance transactions can both see no existing (folder_linkid, archiveid) row and create duplicate grants. Since access resolution aggregates these rows, this can produce duplicate/conflicting permissions; enforce the logical key at the database or serialize the insert.
    packages/api/src/invite/queries/create_access_grants_recursive.sql:63
  • The access existence check has the same soft-delete problem: a previously revoked access row prevents creation of a new active grant, while the invite is still consumed. Restrict this check to active access rows or update the deleted row when accepting the invite.
    packages/api/src/invite/queries/create_share.sql:46
  • This NOT EXISTS check is not atomic with the insert. Two concurrent archive creations processing the same pending invite can both observe no matching share and insert duplicate share rows; the database has no uniqueness constraint for this logical key. Use a unique constraint with INSERT ... ON CONFLICT, or otherwise serialize this operation.
    packages/api/src/invite/queries/create_share.sql:48
  • This existence check also matches soft-deleted shares. If this archive previously had the same folder shared and that share was revoked, accepting a new invite inserts no replacement active share, then the invite is marked accepted and the user still cannot see the folder. Exclude deleted rows from the check or reactivate the existing row.
    packages/api/src/invite/service.ts:46
  • When multiple pending invite shares reference the same folder link, this keeps whichever role happens to be encountered first. The fixture has viewer and editor invites for link 1, and the query has no ordering, so this can grant only viewer even when editor was also invited. Merge duplicate roles with the existing most-permissive-role logic before creating the share and access rows.
  • Files reviewed: 46/46 changed files
  • Comments generated: 2
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +2 to +3
SELECT COALESCE(INCREMENT_BASE36(MAX(archivepart)), '0000') AS archive_part
FROM archive_nbr
Comment on lines +101 to +104
if (acceptedInviteIds.length > 0) {
await sendShareInvitationAcceptanceNotification(
requestData.emailFromAuthToken,
).catch((err: unknown) => {
In preparation for adding an archive creation endpoint, this commit adds
logic for accepting share invites, which is sometimes triggered by
archive creation.
This commit migrates our archive creation endpoint to stela, in
preparation for the 2030 dashboard which we want to build exclusively
with stela endpoints. Some functionality changes worthy of note:
- This POST /archives does not trigger generation of a default archive
  profile image. We've determined that going forward it will be a
  client-side responsibility to decide what to display when the user has
  not set a profile image.
- This POST /archives does not act on Relationship invitations; we
  intend to remove and replace the relationships feature entirely.
@liam-lloyd
liam-lloyd force-pushed the per-10696_add_post_archives_endpoint branch from f27a46b to 200285a Compare September 18, 2026 00:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants