Per 10696 add post archives endpoint - #873
liam-lloyd wants to merge 2 commits into
Conversation
| ( | ||
| :archiveId, | ||
| 'profile.timezone', | ||
| NULL, | ||
| 88, | ||
| 'status.generic.ok', | ||
| 'type.profile_item.timezone', | ||
| CURRENT_TIMESTAMP, | ||
| CURRENT_TIMESTAMP, | ||
| CURRENT_TIMESTAMP | ||
| ); |
There was a problem hiding this comment.
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.
e4728cd to
f27a46b
Compare
Codecov Report✅ All modified and coverable lines are covered by tests. 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
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
🟡 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.accessroleand the derivedshareNamecan be NULL here, andprocessPendingInvitesexplicitly accepts NULL fields and unrecognized roles. Those rows reach the notification service, where the role mapping returnsundefinedand 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 tostatus.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_subtreeusesUNION ALL, inviting the same account to both a folder link and one of its descendants emits the descendant twice. TheNOT EXISTSpredicate does not see duplicate source rows produced by this INSERT, andaccesshas 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 EXISTScheck is not atomic with the insert. Two concurrent archive creations processing the same pending invite can both observe no matching share and insert duplicatesharerows; the database has no uniqueness constraint for this logical key. Use a unique constraint withINSERT ... 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.
| SELECT COALESCE(INCREMENT_BASE36(MAX(archivepart)), '0000') AS archive_part | ||
| FROM archive_nbr |
| 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.
f27a46b to
200285a
Compare
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:
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.
intend to remove and replace the relationships feature entirely.