Add trust features: history, trash, export, and collection sharing
release gate / check (push) Failing after 6m36s
release gate / check (push) Failing after 6m36s
Implement Notion-like upgrade phase 4: append-only page revisions with restore-as-a-new-version; recoverable bookmark/page/collection deletions whose restores never resurrect public flags or shares; owner-scoped JSON export; and explicit, revocable read-only collection sharing with owner-only mutations and no transitive third-party bookmark leaks. Add migration 009, API/docs, integration coverage, and page/collections/trash UI.
This commit is contained in:
@@ -116,8 +116,11 @@ test-only). CardDAV keeps the configured HTTPS port.
|
||||
| PUT | `/api/pages` | `{ok:true}`; complete `{order:[id,...]}` |
|
||||
| GET | `/api/pages/{id}` | Page plus ordered `bookmarks:[{bookmark}]`, owner-scoped `backlinks:[{id,title,updatedAt}]`, resolved outgoing `links:[{id,title}]`, and `embeds:[{collectionId,viewId,position,collection,view}]` |
|
||||
| PATCH | `/api/pages/{id}` | Updated page; `{title?,content?,pinned?,icon?,parentId?,version?,addBookmarkId?,removeBookmarkId?}`. `version` (the base revision from GET) is REQUIRED with `content`: 400 when missing, 409 when stale. Metadata-only patches (including moving a page) stay version-free and do not bump the revision. Moving under itself or a descendant is rejected 400 (no cycles) |
|
||||
| DELETE | `/api/pages/{id}` | `{ok:true}`; keeps bookmarks and promotes child pages to the top level |
|
||||
| GET | `/api/pages/{id}/backlinks` | Owner-scoped `[{id,title,updatedAt}]` from `PageLink` — the pages whose Markdown links here |
|
||||
| GET | `/api/pages/{id}/revisions` | Version history metadata `[{id,version,title,icon,createdAt,length}]`, newest first; bodies fetched separately |
|
||||
| GET | `/api/pages/{id}/revisions/{revisionId}` | Full immutable revision snapshot `{...,content}` |
|
||||
| POST | `/api/pages/{id}/revisions/{revisionId}/restore` | Restores a snapshot as a **new** version; body `{version}` (current base) — 400 missing, 409 stale. The restore itself is recorded in history |
|
||||
| DELETE | `/api/pages/{id}` | Moves the page to Trash; keeps bookmarks and promotes child pages. Restoring also restores its revisions and creates a new revision |
|
||||
| GET | `/api/pages/{id}/markdown` | Markdown attachment |
|
||||
|
||||
### Connected pages (phase 3)
|
||||
@@ -139,15 +142,39 @@ in the same transaction as every content save:
|
||||
its bookmarks. Deleting a collection removes its embeds; deleting a view
|
||||
falls the embed back to the collection's default view.
|
||||
|
||||
### Trust (phase 4)
|
||||
|
||||
Deleting a bookmark, page or collection now moves it to the owner's Trash.
|
||||
The snapshot is taken in the same transaction as the hard delete and includes
|
||||
intrinsic content plus owner-scoped links (tags, page links, collection
|
||||
memberships, page revisions). It deliberately excludes access grants:
|
||||
bookmark public flags, bookmark/tag shares and collection shares are never
|
||||
restored, so revoking access while an item is in the Trash cannot be undone by
|
||||
restoring it. Deleted bookmarks keep their existing delete endpoints; the UI
|
||||
and docs now call this “move to trash”.
|
||||
|
||||
| Method | Path | Returns / behavior |
|
||||
|---|---|---|
|
||||
| GET | `/api/trash` | Owner's deleted items `[{id,type,originalId,title,deletedAt}]`, newest first |
|
||||
| POST | `/api/trash/{id}/restore` | Restores the item; 404 for another owner. Page restores recreate their history and append a new revision. Neither bookmark `isPublic` nor any share is restored |
|
||||
| DELETE | `/api/trash/{id}` | Permanently removes one Trash entry (explicit purge) |
|
||||
| GET | `/api/export` | Owner-scoped structured JSON archive: user, bookmarks, tags, pages (content + revisions), collections (properties, views, items, shares) and contacts. Credentials are never exported |
|
||||
|
||||
## Collections
|
||||
|
||||
Contextual workflows over existing bookmarks (phase 2 of the Notion-like
|
||||
upgrades). Collections are owner-scoped; membership references bookmarks
|
||||
without owning them — removing an item or a whole collection never touches
|
||||
the bookmark. Membership never broadens access: items whose bookmark the
|
||||
caller cannot see are omitted from lists/counts and behave as not-found for
|
||||
mutations; re-sharing restores the membership. Every mutating PATCH requires
|
||||
the expected `version` (400 missing, 409 stale). Indirect mutations advance
|
||||
upgrades). Collections are owner-scoped unless explicitly shared; membership
|
||||
references bookmarks without owning them — removing an item or a whole
|
||||
collection never touches the bookmark. An explicit `CollectionShare` gives a
|
||||
recipient read-only access to the collection and the **narrowed** bookmark
|
||||
projection for member bookmarks **owned by the collection owner**. A bookmark
|
||||
that the collection owner can only see through someone else's share is
|
||||
omitted, so collection sharing never passes a third-party grant onwards.
|
||||
Revoking the share or removing the item removes that access immediately, and
|
||||
mutations remain owner-only. Items whose
|
||||
bookmark the caller cannot see are omitted from lists/counts and behave as
|
||||
not-found for mutations; re-sharing restores the membership. Every mutating
|
||||
PATCH requires the expected `version` (400 missing, 409 stale). Indirect mutations advance
|
||||
revisions too (review CR-09): item reorder bumps every affected item's
|
||||
version, and property deletion bumps the versions of items whose value it
|
||||
dropped and views whose filter/sort definition it changed. Deletions are
|
||||
@@ -156,11 +183,13 @@ from a client snapshot.
|
||||
|
||||
| Method | Path | Returns / behavior |
|
||||
|---|---|---|
|
||||
| GET | `/api/collections` | Owned collections with `_count.{items,properties,views}`; item counts include only bookmarks the caller can see |
|
||||
| GET | `/api/collections` | Owned **and explicitly shared** collections with `shared`, `owner`, `_count.{items,properties,views}`; item counts include only bookmarks the caller can see |
|
||||
| POST | `/api/collections` | 201 detail `{...collection,properties:[],views:[default Table view]}`; body `{name,description?,icon?}` |
|
||||
| GET | `/api/collections/{id}` | Collection plus ordered `properties`, `views` and authorized `_count.{items,properties,views}` |
|
||||
| PATCH | `/api/collections/{id}` | Updated collection; `{version,name?,description?,icon?}`, bumps `version` |
|
||||
| DELETE | `/api/collections/{id}` | `{ok:true}`; memberships/properties/views cascade, bookmarks kept |
|
||||
| GET | `/api/collections/{id}` | Readable collection detail (owner or share recipient) plus ordered `properties`, `views`, authorized `_count`, `shared`; owner also gets `sharedWith` |
|
||||
| PATCH | `/api/collections/{id}` | Updated collection; `{version,name?,description?,icon?}`, bumps `version`; owner only |
|
||||
| DELETE | `/api/collections/{id}` | Moves the collection to Trash; memberships/properties/views cascade, bookmarks kept; owner only |
|
||||
| POST | `/api/collections/{id}/share` | Owner only; `{userIds:[...]}` grants read-only access, idempotent |
|
||||
| DELETE | `/api/collections/{id}/share/{userId}` | Owner only; revokes read-only access immediately |
|
||||
| POST | `/api/collections/{id}/properties` | 201 property; `{name,type,config?}`. Types: `TEXT,NUMBER,SELECT,MULTI_SELECT,DATE,CHECKBOX,URL` (`RELATION` reserved, rejected). SELECT config `{options:[{label,color?}]}` — the server assigns stable option ids; DATE config `{mode:"date"\|"datetime"}`. Duplicate name → 409 |
|
||||
| PATCH | `/api/collections/{id}/properties/{propertyId}` | Updated property; `{version,name?,position?,config?}`. Type and date-mode changes are rejected (400) — conversion must never destroy values. Option updates send the full list: existing ids rename/recolor (identity preserved), omitted ids are removed but refused (400 with the using-item count) while any item still carries them |
|
||||
| DELETE | `/api/collections/{id}/properties/{propertyId}` | `{ok:true}`; strips the property's values from all items and its clauses/sorts/grouping from saved views |
|
||||
|
||||
Reference in New Issue
Block a user