@milaboratories/pl-middle-layer 1.68.2 → 1.69.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.
- package/dist/index.cjs +10 -0
- package/dist/index.d.ts +6 -2
- package/dist/index.js +5 -2
- package/dist/middle_layer/build_stamp.cjs +1 -1
- package/dist/middle_layer/build_stamp.js +1 -1
- package/dist/middle_layer/index.cjs +2 -0
- package/dist/middle_layer/index.d.ts +2 -1
- package/dist/middle_layer/index.js +2 -1
- package/dist/middle_layer/middle_layer.cjs +333 -26
- package/dist/middle_layer/middle_layer.cjs.map +1 -1
- package/dist/middle_layer/middle_layer.d.ts +125 -7
- package/dist/middle_layer/middle_layer.d.ts.map +1 -1
- package/dist/middle_layer/middle_layer.js +335 -28
- package/dist/middle_layer/middle_layer.js.map +1 -1
- package/dist/middle_layer/project_list.d.ts +1 -1
- package/dist/middle_layer/sharing_list.cjs +13 -5
- package/dist/middle_layer/sharing_list.cjs.map +1 -1
- package/dist/middle_layer/sharing_list.d.ts +16 -2
- package/dist/middle_layer/sharing_list.d.ts.map +1 -1
- package/dist/middle_layer/sharing_list.js +14 -6
- package/dist/middle_layer/sharing_list.js.map +1 -1
- package/dist/middle_layer/template_list.cjs +72 -0
- package/dist/middle_layer/template_list.cjs.map +1 -0
- package/dist/middle_layer/template_list.d.ts +77 -0
- package/dist/middle_layer/template_list.d.ts.map +1 -0
- package/dist/middle_layer/template_list.js +64 -0
- package/dist/middle_layer/template_list.js.map +1 -0
- package/dist/model/index.cjs +8 -0
- package/dist/model/index.d.ts +5 -2
- package/dist/model/index.js +4 -2
- package/dist/model/sharing_model.cjs +59 -2
- package/dist/model/sharing_model.cjs.map +1 -1
- package/dist/model/sharing_model.d.ts +70 -7
- package/dist/model/sharing_model.d.ts.map +1 -1
- package/dist/model/sharing_model.js +57 -3
- package/dist/model/sharing_model.js.map +1 -1
- package/dist/model/template_serializer.d.ts +36 -0
- package/dist/model/template_serializer.d.ts.map +1 -1
- package/dist/model/template_share.cjs +42 -0
- package/dist/model/template_share.cjs.map +1 -0
- package/dist/model/template_share.d.ts +27 -0
- package/dist/model/template_share.d.ts.map +1 -0
- package/dist/model/template_share.js +42 -0
- package/dist/model/template_share.js.map +1 -0
- package/dist/mutator/project.cjs +2 -2
- package/dist/mutator/project.js +2 -2
- package/dist/mutator/sharing.cjs +40 -2
- package/dist/mutator/sharing.cjs.map +1 -1
- package/dist/mutator/sharing.js +40 -3
- package/dist/mutator/sharing.js.map +1 -1
- package/dist/mutator/template.cjs +54 -0
- package/dist/mutator/template.cjs.map +1 -0
- package/dist/mutator/template.js +52 -0
- package/dist/mutator/template.js.map +1 -0
- package/package.json +10 -10
- package/src/middle_layer/index.ts +9 -0
- package/src/middle_layer/middle_layer.ts +449 -36
- package/src/middle_layer/sharing_list.ts +37 -7
- package/src/middle_layer/template_list.ts +177 -0
- package/src/middle_layer/templates.test.ts +301 -0
- package/src/model/index.ts +14 -0
- package/src/model/sharing_model.test.ts +115 -1
- package/src/model/sharing_model.ts +134 -9
- package/src/model/template_share.test.ts +78 -0
- package/src/model/template_share.ts +52 -0
- package/src/mutator/sharing.ts +55 -3
- package/src/mutator/template.ts +75 -0
- package/src/test/with_ml.ts +38 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sharing_list.cjs","names":["SharingOutboxResourceType","SharedEnvelopeResourceType","treeFilter","Computable","AcceptanceFieldPrefix","SynchronizedTreeState","asShareId","SharingStateResourceType"],"sources":["../../src/middle_layer/sharing_list.ts"],"sourcesContent":["import type { PruningFunction } from \"@milaboratories/pl-tree\";\nimport { SynchronizedTreeState } from \"@milaboratories/pl-tree\";\nimport type { Filter, PlClient, SignedResourceId } from \"@milaboratories/pl-client\";\nimport {\n isEveryoneUserLogin,\n resourceIdToString,\n resourceType,\n resourceTypesEqual,\n treeFilter,\n} from \"@milaboratories/pl-client\";\nimport { Computable } from \"@milaboratories/computable\";\nimport type { MiddleLayerEnvironment } from \"./middle_layer\";\nimport type { ProjectId } from \"@milaboratories/pl-model-common\";\nimport type {\n EnvelopeAcceptance,\n EnvelopeData,\n EnvelopeMode,\n ShareId,\n} from \"../model/sharing_model\";\nimport {\n AcceptanceFieldPrefix,\n asShareId,\n SharedEnvelopeResourceType,\n SharingOutboxResourceType,\n SharingStateResourceType,\n} from \"../model/sharing_model\";\n\n/** Donor-facing view of one outgoing share. */\nexport interface OutgoingShare {\n shareId: ShareId; // stable logical identity of the share, preserved across changes\n sharedAt: number; // this instance's creation time (ms epoch)\n expiresAt?: number; // EnvelopeData.expiresAt; null maps to undefined = never expires\n mode: EnvelopeMode;\n title: string; // display name shown to recipients; defaults to the first project's name\n /** One entry per project in the pack, so the change UI can offer a per-project decision.\n * `projectId` is the donor's source project id; `updatedAt` is when this project's\n * snapshot was last (re)taken. */\n projects: { projectId: ProjectId; label: string; updatedAt: number }[];\n /** Full recipient logins, from `ListGrants` on the envelope; `[\"*\"]` for everyone-shares. */\n recipients: string[];\n /** Per recipient who has responded: their decision and when, from acceptance/{login}. */\n responses: Record<string, { action: \"accepted\" | \"rejected\"; timestamp: number }>;\n}\n\n/** Per-project view for the donor's change UI, from {@link EnvelopeData.projects}. */\nfunction envelopeProjects(data: EnvelopeData): OutgoingShare[\"projects\"] {\n return Object.values(data.projects).map((p) => ({\n projectId: p.source,\n label: p.label,\n updatedAt: p.updatedAt,\n }));\n}\n\n/** Acceptor-facing view of one pending share. */\nexport interface PendingShare {\n shareId: ShareId;\n sender: string; // EnvelopeData.sender, display only\n title: string; // display name shown to recipients; defaults to the first project's name\n mode: EnvelopeMode; // v1 renders only \"copy\" entries\n grantedAt: number;\n}\n\nconst SharingOutboxPruningFunction: PruningFunction = (resource) => {\n if (\n !resourceTypesEqual(resource.type, SharingOutboxResourceType) &&\n !resourceTypesEqual(resource.type, SharedEnvelopeResourceType)\n )\n return [];\n return resource.fields;\n};\n\n// Server-side traversal scope (modern resourceTree path). Pruning is client-side ONLY and does\n// NOT stop the backend walk — without a fieldFilter the backend descends through the envelope's\n// project/{uuid} snapshots into the whole project graph (StreamManager etc.), whose field-driven\n// finality predicate then throws on the pruned-to-[] fields. Following fields only FROM the outbox\n// and the envelope stops the walk at the project snapshots (UserProject), which we never traverse.\nconst SharingOutboxFieldFilter: Filter = treeFilter.or(\n treeFilter.resourceTypeEq(SharingOutboxResourceType.name),\n treeFilter.resourceTypeEq(SharedEnvelopeResourceType.name),\n);\n\n/** Intermediate of an outgoing share before its full recipient list is fetched: everything the\n * tree carries synchronously, plus the envelope's signed id for the async `ListGrants` enrich. */\ntype OutgoingShareDraft = Omit<OutgoingShare, \"recipients\"> & { envelopeRid: SignedResourceId };\n\n/**\n * Reactive view of the donor's own outbox. Reads each live envelope's immutable\n * {@link EnvelopeData} plus the per-recipient `acceptance/{login}` records from the tree, then\n * enriches each share's full recipient list via `ListGrants` on the envelope (`[\"*\"]` for an\n * everyone-share). `ListGrants` is gated backend-side to the envelope owner (the donor), which is\n * exactly who reads this view.\n *\n * API-level only in M1 (no UI).\n */\nexport function createOutgoingSharesComputable(\n pl: PlClient,\n tree: SynchronizedTreeState,\n): Computable<OutgoingShare[] | undefined> {\n return Computable.make(\n (ctx) => {\n const node = ctx.accessor(tree.entry()).node();\n if (node === undefined) return undefined;\n\n const drafts: OutgoingShareDraft[] = [];\n // Outbox fields are keyed by shareId; each value is a SharedEnvelope.\n for (const fieldName of node.listDynamicFields()) {\n const envelope = node.traverse(fieldName);\n if (envelope === undefined) continue;\n if (!resourceTypesEqual(envelope.resourceType, SharedEnvelopeResourceType)) continue;\n\n const data = envelope.getDataAsJson<EnvelopeData>();\n if (data === undefined) continue;\n\n const responses: OutgoingShare[\"responses\"] = {};\n for (const f of envelope.listDynamicFields()) {\n if (!f.startsWith(AcceptanceFieldPrefix)) continue;\n const login = f.slice(AcceptanceFieldPrefix.length);\n const acc = envelope.traverse(f);\n const accData = acc?.getDataAsJson<EnvelopeAcceptance>();\n if (accData === undefined) continue;\n responses[login] = { action: accData.action, timestamp: accData.timestamp };\n }\n\n drafts.push({\n shareId: data.shareId,\n sharedAt: data.sharedAt,\n ...(data.expiresAt !== null ? { expiresAt: data.expiresAt } : {}),\n mode: data.mode,\n title: data.title,\n projects: envelopeProjects(data),\n responses,\n envelopeRid: envelope.id,\n });\n }\n drafts.sort((a, b) => b.sharedAt - a.sharedAt);\n return drafts;\n },\n {\n // Resolve each share's recipients via listGrants. An everyone-grant maps to \"*\"; the donor's\n // own grant is dropped. One tx per envelope so a just-revoked rid faults only its own read,\n // which allSettled degrades to []. Recipients aren't cached — a live share's recipient set can\n // change. Transactional listGrants when available, else the standalone gRPC-only RPC.\n postprocessValue: async (\n drafts: OutgoingShareDraft[] | undefined,\n ): Promise<OutgoingShare[] | undefined> => {\n if (drafts === undefined) return undefined;\n const self = pl.userResources.authUser;\n const toRecipients = (grants: { user: string }[]): string[] =>\n grants.some((g) => isEveryoneUserLogin(g.user))\n ? [\"*\"]\n : grants.map((g) => g.user).filter((u) => u !== self);\n\n const txGrants = pl.hasCapability(\"txListGrants:v1\");\n const settled = await Promise.allSettled(\n drafts.map((d) =>\n txGrants\n ? pl.withReadTx(\"ListShareGrant\", (tx) => tx.listGrants(d.envelopeRid))\n : pl.userResources.listGrants(d.envelopeRid),\n ),\n );\n\n return drafts.map(({ envelopeRid: _envelopeRid, ...share }, i): OutgoingShare => {\n const r = settled[i];\n return { ...share, recipients: r.status === \"fulfilled\" ? toRecipients(r.value) : [] };\n });\n },\n },\n );\n}\n\n/**\n * Creates the donor's outbox synchronized tree (single explicit root) plus the\n * {@link OutgoingShare} computable over it.\n */\nexport async function createOutgoingShares(\n pl: PlClient,\n outboxRid: SignedResourceId,\n env: MiddleLayerEnvironment,\n): Promise<{ tree: SynchronizedTreeState; computable: Computable<OutgoingShare[] | undefined> }> {\n const tree = await SynchronizedTreeState.init(\n pl,\n outboxRid,\n {\n ...env.ops.defaultTreeOptions,\n pruning: SharingOutboxPruningFunction,\n fieldFilter: SharingOutboxFieldFilter,\n },\n env.logger,\n );\n return { computable: createOutgoingSharesComputable(pl, tree), tree };\n}\n\nconst DecisionFieldPrefix = \"decision/\";\n\nconst SharedEnvelopePruningFunction: PruningFunction = (resource) => {\n if (!resourceTypesEqual(resource.type, SharedEnvelopeResourceType)) return [];\n return resource.fields;\n};\n\n// Discovery only needs each envelope's immutable EnvelopeData (basic resource data); it must NOT\n// descend into the project snapshots. Following fields only FROM the envelope stops the walk at\n// the UserProject snapshots (their fields are never followed).\nconst PendingSharesFieldFilter: Filter = treeFilter.resourceTypeEq(SharedEnvelopeResourceType.name);\n\n/**\n * Creates the acceptor's shared-resource discovery tree (a `{kind:'shared'}` seed over\n * {@link SharedEnvelopeResourceType}) plus the {@link PendingShare} computable over it.\n *\n * An envelope surfaces only when its shareId has NO decision/{shareId} on SharingState —\n * the dedup is applied by the caller, which holds the SharingState tree.\n */\nexport async function createPendingSharesTree(\n pl: PlClient,\n env: MiddleLayerEnvironment,\n): Promise<SynchronizedTreeState> {\n return await SynchronizedTreeState.init(\n pl,\n {\n kind: \"shared\",\n resourceType: resourceType(\n SharedEnvelopeResourceType.name,\n SharedEnvelopeResourceType.version,\n ),\n },\n {\n ...env.ops.defaultTreeOptions,\n pruning: SharedEnvelopePruningFunction,\n fieldFilter: PendingSharesFieldFilter,\n },\n env.logger,\n );\n}\n\n/** A live envelope discovered in the acceptor's shared-resource tree: its signed resource id and\n * decoded {@link EnvelopeData}. The accept/reject flow keys these by `data.shareId`. */\nexport type LiveEnvelope = { rid: SignedResourceId; data: EnvelopeData };\n\n/**\n * Builds a Computable yielding the acceptor's currently-live envelopes, read from the\n * shared-resource discovery tree the ML already maintains — the single discovery mechanism.\n * Accept/reject `.getValue()` this instead of re-streaming `ListUserResources`, so there is no\n * second discovery path. The envelope's signed `rid` (from the tree node) is what the write tx\n * needs; `copyEnvelopeProjectsIntoList` reads the project snapshots itself inside the tx.\n *\n * Returns `undefined` while the tree is still empty/unresolved (mirrors the other tree-backed\n * computables here). Yields a flat list; the caller dedups by `shareId` — at most one live\n * envelope per shareId is expected (the donor keeps one), and a replace tears the old one down.\n */\nexport function createLiveEnvelopesComputable(\n sharedTree: SynchronizedTreeState,\n): Computable<LiveEnvelope[] | undefined> {\n return Computable.make((ctx) => {\n const roots = ctx.accessor(sharedTree.rootsEntry()).nodes();\n const result: LiveEnvelope[] = [];\n for (const envelope of roots) {\n if (envelope === undefined) continue;\n if (!resourceTypesEqual(envelope.resourceType, SharedEnvelopeResourceType)) continue;\n const data = envelope.getDataAsJson<EnvelopeData>();\n if (data === undefined) continue;\n result.push({ rid: envelope.id, data });\n }\n return result;\n });\n}\n\n/**\n * Builds the {@link PendingShare} computable over the shared-resource discovery tree, filtered\n * against the set of already-handled shareIds (those with a decision/{shareId} in the acceptor's\n * SharingState). Both trees feed one Computable so it recomputes when either changes.\n *\n * `currentUserLogin` (when known) suppresses the user's own shares: a share-with-everybody grants\n * the everyone-user, so the donor discovers their own envelope as a pending share. There is no\n * scenario where a user accepts their own share, so they are dropped from the pending view.\n */\nexport function createPendingSharesComputable(\n sharedTree: SynchronizedTreeState,\n sharingStateTree: SynchronizedTreeState,\n currentUserLogin: string | null,\n): Computable<PendingShare[] | undefined> {\n return Computable.make((ctx) => {\n const roots = ctx.accessor(sharedTree.rootsEntry()).nodes();\n\n // Set of shareIds already handled (accepted or rejected) — dedup discovery on the logical share.\n const stateNode = ctx.accessor(sharingStateTree.entry()).node();\n const handled = new Set<ShareId>();\n if (stateNode !== undefined)\n for (const f of stateNode.listDynamicFields())\n if (f.startsWith(DecisionFieldPrefix))\n handled.add(asShareId(f.slice(DecisionFieldPrefix.length)));\n\n const result: PendingShare[] = [];\n for (const envelope of roots) {\n if (envelope === undefined) continue;\n if (!resourceTypesEqual(envelope.resourceType, SharedEnvelopeResourceType)) continue;\n const data = envelope.getDataAsJson<EnvelopeData>();\n if (data === undefined) continue;\n if (handled.has(data.shareId)) continue; // already accepted or rejected\n if (currentUserLogin !== null && data.sender === currentUserLogin) continue; // own share\n if (data.expiresAt !== null && data.expiresAt <= Date.now()) continue;\n\n result.push({\n shareId: data.shareId,\n sender: data.sender,\n title: data.title,\n mode: data.mode,\n grantedAt: data.sharedAt,\n });\n }\n result.sort((a, b) => b.grantedAt - a.grantedAt);\n return result;\n });\n}\n\nconst SharingStatePruningFunction: PruningFunction = (resource) => {\n if (!resourceTypesEqual(resource.type, SharingStateResourceType)) return [];\n return resource.fields;\n};\n\n// decision/{shareId} values are leaf JSON resources; follow fields only from SharingState.\nconst SharingStateFieldFilter: Filter = treeFilter.resourceTypeEq(SharingStateResourceType.name);\n\n/** Creates the acceptor's SharingState synchronized tree (single explicit root). */\nexport async function createSharingStateTree(\n pl: PlClient,\n stateRid: SignedResourceId,\n env: MiddleLayerEnvironment,\n): Promise<SynchronizedTreeState> {\n return await SynchronizedTreeState.init(\n pl,\n stateRid,\n {\n ...env.ops.defaultTreeOptions,\n pruning: SharingStatePruningFunction,\n fieldFilter: SharingStateFieldFilter,\n },\n env.logger,\n );\n}\n\n/** Maps an envelope resource id (signed) to its string form, for failure reporting. */\nexport function envelopeIdString(rid: SignedResourceId): string {\n return resourceIdToString(rid);\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"sharing_list.cjs","names":["envelopeProjectMap","SharingOutboxResourceType","SharedEnvelopeResourceType","treeFilter","Computable","normalizeEnvelopeData","AcceptanceFieldPrefix","SynchronizedTreeState","asShareId","SharingStateResourceType"],"sources":["../../src/middle_layer/sharing_list.ts"],"sourcesContent":["import type { PruningFunction } from \"@milaboratories/pl-tree\";\nimport { SynchronizedTreeState } from \"@milaboratories/pl-tree\";\nimport type { Filter, PlClient, SignedResourceId } from \"@milaboratories/pl-client\";\nimport {\n isEveryoneUserLogin,\n resourceIdToString,\n resourceType,\n resourceTypesEqual,\n treeFilter,\n} from \"@milaboratories/pl-client\";\nimport { Computable } from \"@milaboratories/computable\";\nimport type { MiddleLayerEnvironment } from \"./middle_layer\";\nimport type { ProjectId } from \"@milaboratories/pl-model-common\";\nimport type {\n EnvelopeAcceptance,\n EnvelopeData,\n EnvelopeMode,\n EnvelopePayloadKind,\n ShareId,\n} from \"../model/sharing_model\";\nimport {\n AcceptanceFieldPrefix,\n asShareId,\n envelopeProjectMap,\n normalizeEnvelopeData,\n SharedEnvelopeResourceType,\n SharingOutboxResourceType,\n SharingStateResourceType,\n} from \"../model/sharing_model\";\n\n/** Donor-facing view of one outgoing share. */\nexport interface OutgoingShare {\n shareId: ShareId; // stable logical identity of the share, preserved across changes\n sharedAt: number; // this instance's creation time (ms epoch)\n expiresAt?: number; // EnvelopeData.expiresAt; null maps to undefined = never expires\n mode: EnvelopeMode;\n title: string; // display name shown to recipients; defaults to the first project's name\n /** What the share carries — a pack of projects, or one template document. */\n payloadKind: EnvelopePayloadKind;\n /** One entry per project in the pack, so the change UI can offer a per-project decision.\n * `projectId` is the donor's source project id; `updatedAt` is when this project's\n * snapshot was last (re)taken. Empty for a template share. */\n projects: { projectId: ProjectId; label: string; updatedAt: number }[];\n /** The shared template, for a template share; absent for a project share. */\n template?: { label: string; blockCount: number };\n /** Full recipient logins, from `ListGrants` on the envelope; `[\"*\"]` for everyone-shares. */\n recipients: string[];\n /** Whether {@link responses} can ever be populated for this share. A template share is granted\n * read-only, so no recipient can record a reply on the envelope and the donor never learns who\n * accepted — a view must say so rather than render an empty response list as \"nobody yet\". */\n responsesAvailable: boolean;\n /** Per recipient who has responded: their decision and when, from acceptance/{login}. */\n responses: Record<string, { action: \"accepted\" | \"rejected\"; timestamp: number }>;\n}\n\n/** Per-project view for the donor's change UI, from the envelope's `projects` payload; empty\n * for a payload that carries no project. */\nfunction envelopeProjects(data: EnvelopeData): OutgoingShare[\"projects\"] {\n return Object.values(envelopeProjectMap(data)).map((p) => ({\n projectId: p.source,\n label: p.label,\n updatedAt: p.updatedAt,\n }));\n}\n\n/** Acceptor-facing view of one pending share. */\nexport interface PendingShare {\n shareId: ShareId;\n sender: string; // EnvelopeData.sender, display only\n title: string; // display name shown to recipients; defaults to the first project's name\n mode: EnvelopeMode; // v1 renders only \"copy\" entries\n /** What the offer carries, so the recipient is told what they are being offered — projects to\n * copy, or a template for their own shelf. */\n payloadKind: EnvelopePayloadKind;\n grantedAt: number;\n}\n\nconst SharingOutboxPruningFunction: PruningFunction = (resource) => {\n if (\n !resourceTypesEqual(resource.type, SharingOutboxResourceType) &&\n !resourceTypesEqual(resource.type, SharedEnvelopeResourceType)\n )\n return [];\n return resource.fields;\n};\n\n// Server-side traversal scope (modern resourceTree path). Pruning is client-side ONLY and does\n// NOT stop the backend walk — without a fieldFilter the backend descends through the envelope's\n// project/{uuid} snapshots into the whole project graph (StreamManager etc.), whose field-driven\n// finality predicate then throws on the pruned-to-[] fields. Following fields only FROM the outbox\n// and the envelope stops the walk at the project snapshots (UserProject), which we never traverse.\nconst SharingOutboxFieldFilter: Filter = treeFilter.or(\n treeFilter.resourceTypeEq(SharingOutboxResourceType.name),\n treeFilter.resourceTypeEq(SharedEnvelopeResourceType.name),\n);\n\n/** Intermediate of an outgoing share before its full recipient list is fetched: everything the\n * tree carries synchronously, plus the envelope's signed id for the async `ListGrants` enrich. */\ntype OutgoingShareDraft = Omit<OutgoingShare, \"recipients\"> & { envelopeRid: SignedResourceId };\n\n/**\n * Reactive view of the donor's own outbox. Reads each live envelope's immutable\n * {@link EnvelopeData} plus the per-recipient `acceptance/{login}` records from the tree, then\n * enriches each share's full recipient list via `ListGrants` on the envelope (`[\"*\"]` for an\n * everyone-share). `ListGrants` is gated backend-side to the envelope owner (the donor), which is\n * exactly who reads this view.\n *\n * API-level only in M1 (no UI).\n */\nexport function createOutgoingSharesComputable(\n pl: PlClient,\n tree: SynchronizedTreeState,\n): Computable<OutgoingShare[] | undefined> {\n return Computable.make(\n (ctx) => {\n const node = ctx.accessor(tree.entry()).node();\n if (node === undefined) return undefined;\n\n const drafts: OutgoingShareDraft[] = [];\n // Outbox fields are keyed by shareId; each value is a SharedEnvelope.\n for (const fieldName of node.listDynamicFields()) {\n const envelope = node.traverse(fieldName);\n if (envelope === undefined) continue;\n if (!resourceTypesEqual(envelope.resourceType, SharedEnvelopeResourceType)) continue;\n\n const data = normalizeEnvelopeData(envelope.getDataAsJson<unknown>());\n if (data === undefined) continue; // unknown version or payload kind — not ours to show\n\n const responses: OutgoingShare[\"responses\"] = {};\n for (const f of envelope.listDynamicFields()) {\n if (!f.startsWith(AcceptanceFieldPrefix)) continue;\n const login = f.slice(AcceptanceFieldPrefix.length);\n const acc = envelope.traverse(f);\n const accData = acc?.getDataAsJson<EnvelopeAcceptance>();\n if (accData === undefined) continue;\n responses[login] = { action: accData.action, timestamp: accData.timestamp };\n }\n\n drafts.push({\n shareId: data.shareId,\n sharedAt: data.sharedAt,\n ...(data.expiresAt !== null ? { expiresAt: data.expiresAt } : {}),\n mode: data.mode,\n title: data.title,\n payloadKind: data.payload.kind,\n projects: envelopeProjects(data),\n ...(data.payload.kind === \"template\"\n ? {\n template: {\n label: data.payload.label,\n blockCount: data.payload.document.blocks.length,\n },\n }\n : {}),\n responsesAvailable: data.payload.kind !== \"template\",\n responses,\n envelopeRid: envelope.id,\n });\n }\n drafts.sort((a, b) => b.sharedAt - a.sharedAt);\n return drafts;\n },\n {\n // Resolve each share's recipients via listGrants. An everyone-grant maps to \"*\"; the donor's\n // own grant is dropped. One tx per envelope so a just-revoked rid faults only its own read,\n // which allSettled degrades to []. Recipients aren't cached — a live share's recipient set can\n // change. Transactional listGrants when available, else the standalone gRPC-only RPC.\n postprocessValue: async (\n drafts: OutgoingShareDraft[] | undefined,\n ): Promise<OutgoingShare[] | undefined> => {\n if (drafts === undefined) return undefined;\n const self = pl.userResources.authUser;\n const toRecipients = (grants: { user: string }[]): string[] =>\n grants.some((g) => isEveryoneUserLogin(g.user))\n ? [\"*\"]\n : grants.map((g) => g.user).filter((u) => u !== self);\n\n const txGrants = pl.hasCapability(\"txListGrants:v1\");\n const settled = await Promise.allSettled(\n drafts.map((d) =>\n txGrants\n ? pl.withReadTx(\"ListShareGrant\", (tx) => tx.listGrants(d.envelopeRid))\n : pl.userResources.listGrants(d.envelopeRid),\n ),\n );\n\n return drafts.map(({ envelopeRid: _envelopeRid, ...share }, i): OutgoingShare => {\n const r = settled[i];\n return { ...share, recipients: r.status === \"fulfilled\" ? toRecipients(r.value) : [] };\n });\n },\n },\n );\n}\n\n/**\n * Creates the donor's outbox synchronized tree (single explicit root) plus the\n * {@link OutgoingShare} computable over it.\n */\nexport async function createOutgoingShares(\n pl: PlClient,\n outboxRid: SignedResourceId,\n env: MiddleLayerEnvironment,\n): Promise<{ tree: SynchronizedTreeState; computable: Computable<OutgoingShare[] | undefined> }> {\n const tree = await SynchronizedTreeState.init(\n pl,\n outboxRid,\n {\n ...env.ops.defaultTreeOptions,\n pruning: SharingOutboxPruningFunction,\n fieldFilter: SharingOutboxFieldFilter,\n },\n env.logger,\n );\n return { computable: createOutgoingSharesComputable(pl, tree), tree };\n}\n\nconst DecisionFieldPrefix = \"decision/\";\n\nconst SharedEnvelopePruningFunction: PruningFunction = (resource) => {\n if (!resourceTypesEqual(resource.type, SharedEnvelopeResourceType)) return [];\n return resource.fields;\n};\n\n// Discovery only needs each envelope's immutable EnvelopeData (basic resource data); it must NOT\n// descend into the project snapshots. Following fields only FROM the envelope stops the walk at\n// the UserProject snapshots (their fields are never followed).\nconst PendingSharesFieldFilter: Filter = treeFilter.resourceTypeEq(SharedEnvelopeResourceType.name);\n\n/**\n * Creates the acceptor's shared-resource discovery tree (a `{kind:'shared'}` seed over\n * {@link SharedEnvelopeResourceType}) plus the {@link PendingShare} computable over it.\n *\n * An envelope surfaces only when its shareId has NO decision/{shareId} on SharingState —\n * the dedup is applied by the caller, which holds the SharingState tree.\n */\nexport async function createPendingSharesTree(\n pl: PlClient,\n env: MiddleLayerEnvironment,\n): Promise<SynchronizedTreeState> {\n return await SynchronizedTreeState.init(\n pl,\n {\n kind: \"shared\",\n resourceType: resourceType(\n SharedEnvelopeResourceType.name,\n SharedEnvelopeResourceType.version,\n ),\n },\n {\n ...env.ops.defaultTreeOptions,\n pruning: SharedEnvelopePruningFunction,\n fieldFilter: PendingSharesFieldFilter,\n },\n env.logger,\n );\n}\n\n/** A live envelope discovered in the acceptor's shared-resource tree: its signed resource id and\n * decoded {@link EnvelopeData}. The accept/reject flow keys these by `data.shareId`. */\nexport type LiveEnvelope = { rid: SignedResourceId; data: EnvelopeData };\n\n/**\n * Builds a Computable yielding the acceptor's currently-live envelopes, read from the\n * shared-resource discovery tree the ML already maintains — the single discovery mechanism.\n * Accept/reject `.getValue()` this instead of re-streaming `ListUserResources`, so there is no\n * second discovery path. The envelope's signed `rid` (from the tree node) is what the write tx\n * needs; `copyEnvelopeProjectsIntoList` reads the project snapshots itself inside the tx.\n *\n * Returns `undefined` while the tree is still empty/unresolved (mirrors the other tree-backed\n * computables here). Yields a flat list; the caller dedups by `shareId` — at most one live\n * envelope per shareId is expected (the donor keeps one), and a replace tears the old one down.\n */\nexport function createLiveEnvelopesComputable(\n sharedTree: SynchronizedTreeState,\n): Computable<LiveEnvelope[] | undefined> {\n return Computable.make((ctx) => {\n const roots = ctx.accessor(sharedTree.rootsEntry()).nodes();\n const result: LiveEnvelope[] = [];\n for (const envelope of roots) {\n if (envelope === undefined) continue;\n if (!resourceTypesEqual(envelope.resourceType, SharedEnvelopeResourceType)) continue;\n const data = normalizeEnvelopeData(envelope.getDataAsJson<unknown>());\n // Same recognise-or-hide rule as the pending view, and for the same envelope: hiding an\n // offer while accept could still resolve it would only move the problem.\n if (data === undefined) continue;\n result.push({ rid: envelope.id, data });\n }\n return result;\n });\n}\n\n/**\n * Builds the {@link PendingShare} computable over the shared-resource discovery tree, filtered\n * against the set of already-handled shareIds (those with a decision/{shareId} in the acceptor's\n * SharingState). Both trees feed one Computable so it recomputes when either changes.\n *\n * `currentUserLogin` (when known) suppresses the user's own shares: a share-with-everybody grants\n * the everyone-user, so the donor discovers their own envelope as a pending share. There is no\n * scenario where a user accepts their own share, so they are dropped from the pending view.\n */\nexport function createPendingSharesComputable(\n sharedTree: SynchronizedTreeState,\n sharingStateTree: SynchronizedTreeState,\n currentUserLogin: string | null,\n): Computable<PendingShare[] | undefined> {\n return Computable.make((ctx) => {\n const roots = ctx.accessor(sharedTree.rootsEntry()).nodes();\n\n // Set of shareIds already handled (accepted or rejected) — dedup discovery on the logical share.\n const stateNode = ctx.accessor(sharingStateTree.entry()).node();\n const handled = new Set<ShareId>();\n if (stateNode !== undefined)\n for (const f of stateNode.listDynamicFields())\n if (f.startsWith(DecisionFieldPrefix))\n handled.add(asShareId(f.slice(DecisionFieldPrefix.length)));\n\n const result: PendingShare[] = [];\n for (const envelope of roots) {\n if (envelope === undefined) continue;\n if (!resourceTypesEqual(envelope.resourceType, SharedEnvelopeResourceType)) continue;\n const data = normalizeEnvelopeData(envelope.getDataAsJson<unknown>());\n // An envelope whose schemaVersion or payload kind this build does not know is hidden, not\n // offered: there is nothing useful to do with a share we cannot read.\n if (data === undefined) continue;\n if (handled.has(data.shareId)) continue; // already accepted or rejected\n if (currentUserLogin !== null && data.sender === currentUserLogin) continue; // own share\n if (data.expiresAt !== null && data.expiresAt <= Date.now()) continue;\n\n result.push({\n shareId: data.shareId,\n sender: data.sender,\n title: data.title,\n mode: data.mode,\n payloadKind: data.payload.kind,\n grantedAt: data.sharedAt,\n });\n }\n result.sort((a, b) => b.grantedAt - a.grantedAt);\n return result;\n });\n}\n\nconst SharingStatePruningFunction: PruningFunction = (resource) => {\n if (!resourceTypesEqual(resource.type, SharingStateResourceType)) return [];\n return resource.fields;\n};\n\n// decision/{shareId} values are leaf JSON resources; follow fields only from SharingState.\nconst SharingStateFieldFilter: Filter = treeFilter.resourceTypeEq(SharingStateResourceType.name);\n\n/** Creates the acceptor's SharingState synchronized tree (single explicit root). */\nexport async function createSharingStateTree(\n pl: PlClient,\n stateRid: SignedResourceId,\n env: MiddleLayerEnvironment,\n): Promise<SynchronizedTreeState> {\n return await SynchronizedTreeState.init(\n pl,\n stateRid,\n {\n ...env.ops.defaultTreeOptions,\n pruning: SharingStatePruningFunction,\n fieldFilter: SharingStateFieldFilter,\n },\n env.logger,\n );\n}\n\n/** Maps an envelope resource id (signed) to its string form, for failure reporting. */\nexport function envelopeIdString(rid: SignedResourceId): string {\n return resourceIdToString(rid);\n}\n"],"mappings":"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"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { EnvelopeMode, ShareId } from "../model/sharing_model.js";
|
|
1
|
+
import { EnvelopeMode, EnvelopePayloadKind, ShareId } from "../model/sharing_model.js";
|
|
2
2
|
import "./middle_layer.js";
|
|
3
3
|
import { ProjectId } from "@milaboratories/pl-model-common";
|
|
4
4
|
import { PlClient, SignedResourceId } from "@milaboratories/pl-client";
|
|
@@ -12,16 +12,27 @@ export interface OutgoingShare {
|
|
|
12
12
|
expiresAt?: number;
|
|
13
13
|
mode: EnvelopeMode;
|
|
14
14
|
title: string;
|
|
15
|
+
/** What the share carries — a pack of projects, or one template document. */
|
|
16
|
+
payloadKind: EnvelopePayloadKind;
|
|
15
17
|
/** One entry per project in the pack, so the change UI can offer a per-project decision.
|
|
16
18
|
* `projectId` is the donor's source project id; `updatedAt` is when this project's
|
|
17
|
-
* snapshot was last (re)taken. */
|
|
19
|
+
* snapshot was last (re)taken. Empty for a template share. */
|
|
18
20
|
projects: {
|
|
19
21
|
projectId: ProjectId;
|
|
20
22
|
label: string;
|
|
21
23
|
updatedAt: number;
|
|
22
24
|
}[];
|
|
25
|
+
/** The shared template, for a template share; absent for a project share. */
|
|
26
|
+
template?: {
|
|
27
|
+
label: string;
|
|
28
|
+
blockCount: number;
|
|
29
|
+
};
|
|
23
30
|
/** Full recipient logins, from `ListGrants` on the envelope; `["*"]` for everyone-shares. */
|
|
24
31
|
recipients: string[];
|
|
32
|
+
/** Whether {@link responses} can ever be populated for this share. A template share is granted
|
|
33
|
+
* read-only, so no recipient can record a reply on the envelope and the donor never learns who
|
|
34
|
+
* accepted — a view must say so rather than render an empty response list as "nobody yet". */
|
|
35
|
+
responsesAvailable: boolean;
|
|
25
36
|
/** Per recipient who has responded: their decision and when, from acceptance/{login}. */
|
|
26
37
|
responses: Record<string, {
|
|
27
38
|
action: "accepted" | "rejected";
|
|
@@ -34,6 +45,9 @@ export interface PendingShare {
|
|
|
34
45
|
sender: string;
|
|
35
46
|
title: string;
|
|
36
47
|
mode: EnvelopeMode;
|
|
48
|
+
/** What the offer carries, so the recipient is told what they are being offered — projects to
|
|
49
|
+
* copy, or a template for their own shelf. */
|
|
50
|
+
payloadKind: EnvelopePayloadKind;
|
|
37
51
|
grantedAt: number;
|
|
38
52
|
}
|
|
39
53
|
//#endregion
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sharing_list.d.ts","names":[],"sources":["../../src/middle_layer/sharing_list.ts"],"mappings":";;;;;;;;
|
|
1
|
+
{"version":3,"file":"sharing_list.d.ts","names":[],"sources":["../../src/middle_layer/sharing_list.ts"],"mappings":";;;;;;;;iBA+BiB;EACf,SAAS;EACT;EACA;EACA,MAAM;EACN;;EAEA,aAAa;;;;EAIb;IAAY,WAAW;IAAW;IAAe;;;EAEjD;IAAa;IAAe;;;EAE5B;;;;EAIA;;EAEA,WAAW;IAAiB;IAAiC;;;;iBAc9C;EACf,SAAS;EACT;EACA;EACA,MAAM;;;EAGN,aAAa;EACb"}
|
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
import { AcceptanceFieldPrefix, SharedEnvelopeResourceType, SharingOutboxResourceType, SharingStateResourceType, asShareId } from "../model/sharing_model.js";
|
|
1
|
+
import { AcceptanceFieldPrefix, SharedEnvelopeResourceType, SharingOutboxResourceType, SharingStateResourceType, asShareId, envelopeProjectMap, normalizeEnvelopeData } from "../model/sharing_model.js";
|
|
2
2
|
import { isEveryoneUserLogin, resourceType, resourceTypesEqual, treeFilter } from "@milaboratories/pl-client";
|
|
3
3
|
import { SynchronizedTreeState } from "@milaboratories/pl-tree";
|
|
4
4
|
import { Computable } from "@milaboratories/computable";
|
|
5
5
|
//#region src/middle_layer/sharing_list.ts
|
|
6
|
-
/** Per-project view for the donor's change UI, from
|
|
6
|
+
/** Per-project view for the donor's change UI, from the envelope's `projects` payload; empty
|
|
7
|
+
* for a payload that carries no project. */
|
|
7
8
|
function envelopeProjects(data) {
|
|
8
|
-
return Object.values(data
|
|
9
|
+
return Object.values(envelopeProjectMap(data)).map((p) => ({
|
|
9
10
|
projectId: p.source,
|
|
10
11
|
label: p.label,
|
|
11
12
|
updatedAt: p.updatedAt
|
|
@@ -34,7 +35,7 @@ function createOutgoingSharesComputable(pl, tree) {
|
|
|
34
35
|
const envelope = node.traverse(fieldName);
|
|
35
36
|
if (envelope === void 0) continue;
|
|
36
37
|
if (!resourceTypesEqual(envelope.resourceType, SharedEnvelopeResourceType)) continue;
|
|
37
|
-
const data = envelope.getDataAsJson();
|
|
38
|
+
const data = normalizeEnvelopeData(envelope.getDataAsJson());
|
|
38
39
|
if (data === void 0) continue;
|
|
39
40
|
const responses = {};
|
|
40
41
|
for (const f of envelope.listDynamicFields()) {
|
|
@@ -53,7 +54,13 @@ function createOutgoingSharesComputable(pl, tree) {
|
|
|
53
54
|
...data.expiresAt !== null ? { expiresAt: data.expiresAt } : {},
|
|
54
55
|
mode: data.mode,
|
|
55
56
|
title: data.title,
|
|
57
|
+
payloadKind: data.payload.kind,
|
|
56
58
|
projects: envelopeProjects(data),
|
|
59
|
+
...data.payload.kind === "template" ? { template: {
|
|
60
|
+
label: data.payload.label,
|
|
61
|
+
blockCount: data.payload.document.blocks.length
|
|
62
|
+
} } : {},
|
|
63
|
+
responsesAvailable: data.payload.kind !== "template",
|
|
57
64
|
responses,
|
|
58
65
|
envelopeRid: envelope.id
|
|
59
66
|
});
|
|
@@ -131,7 +138,7 @@ function createLiveEnvelopesComputable(sharedTree) {
|
|
|
131
138
|
for (const envelope of roots) {
|
|
132
139
|
if (envelope === void 0) continue;
|
|
133
140
|
if (!resourceTypesEqual(envelope.resourceType, SharedEnvelopeResourceType)) continue;
|
|
134
|
-
const data = envelope.getDataAsJson();
|
|
141
|
+
const data = normalizeEnvelopeData(envelope.getDataAsJson());
|
|
135
142
|
if (data === void 0) continue;
|
|
136
143
|
result.push({
|
|
137
144
|
rid: envelope.id,
|
|
@@ -162,7 +169,7 @@ function createPendingSharesComputable(sharedTree, sharingStateTree, currentUser
|
|
|
162
169
|
for (const envelope of roots) {
|
|
163
170
|
if (envelope === void 0) continue;
|
|
164
171
|
if (!resourceTypesEqual(envelope.resourceType, SharedEnvelopeResourceType)) continue;
|
|
165
|
-
const data = envelope.getDataAsJson();
|
|
172
|
+
const data = normalizeEnvelopeData(envelope.getDataAsJson());
|
|
166
173
|
if (data === void 0) continue;
|
|
167
174
|
if (handled.has(data.shareId)) continue;
|
|
168
175
|
if (currentUserLogin !== null && data.sender === currentUserLogin) continue;
|
|
@@ -172,6 +179,7 @@ function createPendingSharesComputable(sharedTree, sharingStateTree, currentUser
|
|
|
172
179
|
sender: data.sender,
|
|
173
180
|
title: data.title,
|
|
174
181
|
mode: data.mode,
|
|
182
|
+
payloadKind: data.payload.kind,
|
|
175
183
|
grantedAt: data.sharedAt
|
|
176
184
|
});
|
|
177
185
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sharing_list.js","names":[],"sources":["../../src/middle_layer/sharing_list.ts"],"sourcesContent":["import type { PruningFunction } from \"@milaboratories/pl-tree\";\nimport { SynchronizedTreeState } from \"@milaboratories/pl-tree\";\nimport type { Filter, PlClient, SignedResourceId } from \"@milaboratories/pl-client\";\nimport {\n isEveryoneUserLogin,\n resourceIdToString,\n resourceType,\n resourceTypesEqual,\n treeFilter,\n} from \"@milaboratories/pl-client\";\nimport { Computable } from \"@milaboratories/computable\";\nimport type { MiddleLayerEnvironment } from \"./middle_layer\";\nimport type { ProjectId } from \"@milaboratories/pl-model-common\";\nimport type {\n EnvelopeAcceptance,\n EnvelopeData,\n EnvelopeMode,\n ShareId,\n} from \"../model/sharing_model\";\nimport {\n AcceptanceFieldPrefix,\n asShareId,\n SharedEnvelopeResourceType,\n SharingOutboxResourceType,\n SharingStateResourceType,\n} from \"../model/sharing_model\";\n\n/** Donor-facing view of one outgoing share. */\nexport interface OutgoingShare {\n shareId: ShareId; // stable logical identity of the share, preserved across changes\n sharedAt: number; // this instance's creation time (ms epoch)\n expiresAt?: number; // EnvelopeData.expiresAt; null maps to undefined = never expires\n mode: EnvelopeMode;\n title: string; // display name shown to recipients; defaults to the first project's name\n /** One entry per project in the pack, so the change UI can offer a per-project decision.\n * `projectId` is the donor's source project id; `updatedAt` is when this project's\n * snapshot was last (re)taken. */\n projects: { projectId: ProjectId; label: string; updatedAt: number }[];\n /** Full recipient logins, from `ListGrants` on the envelope; `[\"*\"]` for everyone-shares. */\n recipients: string[];\n /** Per recipient who has responded: their decision and when, from acceptance/{login}. */\n responses: Record<string, { action: \"accepted\" | \"rejected\"; timestamp: number }>;\n}\n\n/** Per-project view for the donor's change UI, from {@link EnvelopeData.projects}. */\nfunction envelopeProjects(data: EnvelopeData): OutgoingShare[\"projects\"] {\n return Object.values(data.projects).map((p) => ({\n projectId: p.source,\n label: p.label,\n updatedAt: p.updatedAt,\n }));\n}\n\n/** Acceptor-facing view of one pending share. */\nexport interface PendingShare {\n shareId: ShareId;\n sender: string; // EnvelopeData.sender, display only\n title: string; // display name shown to recipients; defaults to the first project's name\n mode: EnvelopeMode; // v1 renders only \"copy\" entries\n grantedAt: number;\n}\n\nconst SharingOutboxPruningFunction: PruningFunction = (resource) => {\n if (\n !resourceTypesEqual(resource.type, SharingOutboxResourceType) &&\n !resourceTypesEqual(resource.type, SharedEnvelopeResourceType)\n )\n return [];\n return resource.fields;\n};\n\n// Server-side traversal scope (modern resourceTree path). Pruning is client-side ONLY and does\n// NOT stop the backend walk — without a fieldFilter the backend descends through the envelope's\n// project/{uuid} snapshots into the whole project graph (StreamManager etc.), whose field-driven\n// finality predicate then throws on the pruned-to-[] fields. Following fields only FROM the outbox\n// and the envelope stops the walk at the project snapshots (UserProject), which we never traverse.\nconst SharingOutboxFieldFilter: Filter = treeFilter.or(\n treeFilter.resourceTypeEq(SharingOutboxResourceType.name),\n treeFilter.resourceTypeEq(SharedEnvelopeResourceType.name),\n);\n\n/** Intermediate of an outgoing share before its full recipient list is fetched: everything the\n * tree carries synchronously, plus the envelope's signed id for the async `ListGrants` enrich. */\ntype OutgoingShareDraft = Omit<OutgoingShare, \"recipients\"> & { envelopeRid: SignedResourceId };\n\n/**\n * Reactive view of the donor's own outbox. Reads each live envelope's immutable\n * {@link EnvelopeData} plus the per-recipient `acceptance/{login}` records from the tree, then\n * enriches each share's full recipient list via `ListGrants` on the envelope (`[\"*\"]` for an\n * everyone-share). `ListGrants` is gated backend-side to the envelope owner (the donor), which is\n * exactly who reads this view.\n *\n * API-level only in M1 (no UI).\n */\nexport function createOutgoingSharesComputable(\n pl: PlClient,\n tree: SynchronizedTreeState,\n): Computable<OutgoingShare[] | undefined> {\n return Computable.make(\n (ctx) => {\n const node = ctx.accessor(tree.entry()).node();\n if (node === undefined) return undefined;\n\n const drafts: OutgoingShareDraft[] = [];\n // Outbox fields are keyed by shareId; each value is a SharedEnvelope.\n for (const fieldName of node.listDynamicFields()) {\n const envelope = node.traverse(fieldName);\n if (envelope === undefined) continue;\n if (!resourceTypesEqual(envelope.resourceType, SharedEnvelopeResourceType)) continue;\n\n const data = envelope.getDataAsJson<EnvelopeData>();\n if (data === undefined) continue;\n\n const responses: OutgoingShare[\"responses\"] = {};\n for (const f of envelope.listDynamicFields()) {\n if (!f.startsWith(AcceptanceFieldPrefix)) continue;\n const login = f.slice(AcceptanceFieldPrefix.length);\n const acc = envelope.traverse(f);\n const accData = acc?.getDataAsJson<EnvelopeAcceptance>();\n if (accData === undefined) continue;\n responses[login] = { action: accData.action, timestamp: accData.timestamp };\n }\n\n drafts.push({\n shareId: data.shareId,\n sharedAt: data.sharedAt,\n ...(data.expiresAt !== null ? { expiresAt: data.expiresAt } : {}),\n mode: data.mode,\n title: data.title,\n projects: envelopeProjects(data),\n responses,\n envelopeRid: envelope.id,\n });\n }\n drafts.sort((a, b) => b.sharedAt - a.sharedAt);\n return drafts;\n },\n {\n // Resolve each share's recipients via listGrants. An everyone-grant maps to \"*\"; the donor's\n // own grant is dropped. One tx per envelope so a just-revoked rid faults only its own read,\n // which allSettled degrades to []. Recipients aren't cached — a live share's recipient set can\n // change. Transactional listGrants when available, else the standalone gRPC-only RPC.\n postprocessValue: async (\n drafts: OutgoingShareDraft[] | undefined,\n ): Promise<OutgoingShare[] | undefined> => {\n if (drafts === undefined) return undefined;\n const self = pl.userResources.authUser;\n const toRecipients = (grants: { user: string }[]): string[] =>\n grants.some((g) => isEveryoneUserLogin(g.user))\n ? [\"*\"]\n : grants.map((g) => g.user).filter((u) => u !== self);\n\n const txGrants = pl.hasCapability(\"txListGrants:v1\");\n const settled = await Promise.allSettled(\n drafts.map((d) =>\n txGrants\n ? pl.withReadTx(\"ListShareGrant\", (tx) => tx.listGrants(d.envelopeRid))\n : pl.userResources.listGrants(d.envelopeRid),\n ),\n );\n\n return drafts.map(({ envelopeRid: _envelopeRid, ...share }, i): OutgoingShare => {\n const r = settled[i];\n return { ...share, recipients: r.status === \"fulfilled\" ? toRecipients(r.value) : [] };\n });\n },\n },\n );\n}\n\n/**\n * Creates the donor's outbox synchronized tree (single explicit root) plus the\n * {@link OutgoingShare} computable over it.\n */\nexport async function createOutgoingShares(\n pl: PlClient,\n outboxRid: SignedResourceId,\n env: MiddleLayerEnvironment,\n): Promise<{ tree: SynchronizedTreeState; computable: Computable<OutgoingShare[] | undefined> }> {\n const tree = await SynchronizedTreeState.init(\n pl,\n outboxRid,\n {\n ...env.ops.defaultTreeOptions,\n pruning: SharingOutboxPruningFunction,\n fieldFilter: SharingOutboxFieldFilter,\n },\n env.logger,\n );\n return { computable: createOutgoingSharesComputable(pl, tree), tree };\n}\n\nconst DecisionFieldPrefix = \"decision/\";\n\nconst SharedEnvelopePruningFunction: PruningFunction = (resource) => {\n if (!resourceTypesEqual(resource.type, SharedEnvelopeResourceType)) return [];\n return resource.fields;\n};\n\n// Discovery only needs each envelope's immutable EnvelopeData (basic resource data); it must NOT\n// descend into the project snapshots. Following fields only FROM the envelope stops the walk at\n// the UserProject snapshots (their fields are never followed).\nconst PendingSharesFieldFilter: Filter = treeFilter.resourceTypeEq(SharedEnvelopeResourceType.name);\n\n/**\n * Creates the acceptor's shared-resource discovery tree (a `{kind:'shared'}` seed over\n * {@link SharedEnvelopeResourceType}) plus the {@link PendingShare} computable over it.\n *\n * An envelope surfaces only when its shareId has NO decision/{shareId} on SharingState —\n * the dedup is applied by the caller, which holds the SharingState tree.\n */\nexport async function createPendingSharesTree(\n pl: PlClient,\n env: MiddleLayerEnvironment,\n): Promise<SynchronizedTreeState> {\n return await SynchronizedTreeState.init(\n pl,\n {\n kind: \"shared\",\n resourceType: resourceType(\n SharedEnvelopeResourceType.name,\n SharedEnvelopeResourceType.version,\n ),\n },\n {\n ...env.ops.defaultTreeOptions,\n pruning: SharedEnvelopePruningFunction,\n fieldFilter: PendingSharesFieldFilter,\n },\n env.logger,\n );\n}\n\n/** A live envelope discovered in the acceptor's shared-resource tree: its signed resource id and\n * decoded {@link EnvelopeData}. The accept/reject flow keys these by `data.shareId`. */\nexport type LiveEnvelope = { rid: SignedResourceId; data: EnvelopeData };\n\n/**\n * Builds a Computable yielding the acceptor's currently-live envelopes, read from the\n * shared-resource discovery tree the ML already maintains — the single discovery mechanism.\n * Accept/reject `.getValue()` this instead of re-streaming `ListUserResources`, so there is no\n * second discovery path. The envelope's signed `rid` (from the tree node) is what the write tx\n * needs; `copyEnvelopeProjectsIntoList` reads the project snapshots itself inside the tx.\n *\n * Returns `undefined` while the tree is still empty/unresolved (mirrors the other tree-backed\n * computables here). Yields a flat list; the caller dedups by `shareId` — at most one live\n * envelope per shareId is expected (the donor keeps one), and a replace tears the old one down.\n */\nexport function createLiveEnvelopesComputable(\n sharedTree: SynchronizedTreeState,\n): Computable<LiveEnvelope[] | undefined> {\n return Computable.make((ctx) => {\n const roots = ctx.accessor(sharedTree.rootsEntry()).nodes();\n const result: LiveEnvelope[] = [];\n for (const envelope of roots) {\n if (envelope === undefined) continue;\n if (!resourceTypesEqual(envelope.resourceType, SharedEnvelopeResourceType)) continue;\n const data = envelope.getDataAsJson<EnvelopeData>();\n if (data === undefined) continue;\n result.push({ rid: envelope.id, data });\n }\n return result;\n });\n}\n\n/**\n * Builds the {@link PendingShare} computable over the shared-resource discovery tree, filtered\n * against the set of already-handled shareIds (those with a decision/{shareId} in the acceptor's\n * SharingState). Both trees feed one Computable so it recomputes when either changes.\n *\n * `currentUserLogin` (when known) suppresses the user's own shares: a share-with-everybody grants\n * the everyone-user, so the donor discovers their own envelope as a pending share. There is no\n * scenario where a user accepts their own share, so they are dropped from the pending view.\n */\nexport function createPendingSharesComputable(\n sharedTree: SynchronizedTreeState,\n sharingStateTree: SynchronizedTreeState,\n currentUserLogin: string | null,\n): Computable<PendingShare[] | undefined> {\n return Computable.make((ctx) => {\n const roots = ctx.accessor(sharedTree.rootsEntry()).nodes();\n\n // Set of shareIds already handled (accepted or rejected) — dedup discovery on the logical share.\n const stateNode = ctx.accessor(sharingStateTree.entry()).node();\n const handled = new Set<ShareId>();\n if (stateNode !== undefined)\n for (const f of stateNode.listDynamicFields())\n if (f.startsWith(DecisionFieldPrefix))\n handled.add(asShareId(f.slice(DecisionFieldPrefix.length)));\n\n const result: PendingShare[] = [];\n for (const envelope of roots) {\n if (envelope === undefined) continue;\n if (!resourceTypesEqual(envelope.resourceType, SharedEnvelopeResourceType)) continue;\n const data = envelope.getDataAsJson<EnvelopeData>();\n if (data === undefined) continue;\n if (handled.has(data.shareId)) continue; // already accepted or rejected\n if (currentUserLogin !== null && data.sender === currentUserLogin) continue; // own share\n if (data.expiresAt !== null && data.expiresAt <= Date.now()) continue;\n\n result.push({\n shareId: data.shareId,\n sender: data.sender,\n title: data.title,\n mode: data.mode,\n grantedAt: data.sharedAt,\n });\n }\n result.sort((a, b) => b.grantedAt - a.grantedAt);\n return result;\n });\n}\n\nconst SharingStatePruningFunction: PruningFunction = (resource) => {\n if (!resourceTypesEqual(resource.type, SharingStateResourceType)) return [];\n return resource.fields;\n};\n\n// decision/{shareId} values are leaf JSON resources; follow fields only from SharingState.\nconst SharingStateFieldFilter: Filter = treeFilter.resourceTypeEq(SharingStateResourceType.name);\n\n/** Creates the acceptor's SharingState synchronized tree (single explicit root). */\nexport async function createSharingStateTree(\n pl: PlClient,\n stateRid: SignedResourceId,\n env: MiddleLayerEnvironment,\n): Promise<SynchronizedTreeState> {\n return await SynchronizedTreeState.init(\n pl,\n stateRid,\n {\n ...env.ops.defaultTreeOptions,\n pruning: SharingStatePruningFunction,\n fieldFilter: SharingStateFieldFilter,\n },\n env.logger,\n );\n}\n\n/** Maps an envelope resource id (signed) to its string form, for failure reporting. */\nexport function envelopeIdString(rid: SignedResourceId): string {\n return resourceIdToString(rid);\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"sharing_list.js","names":[],"sources":["../../src/middle_layer/sharing_list.ts"],"sourcesContent":["import type { PruningFunction } from \"@milaboratories/pl-tree\";\nimport { SynchronizedTreeState } from \"@milaboratories/pl-tree\";\nimport type { Filter, PlClient, SignedResourceId } from \"@milaboratories/pl-client\";\nimport {\n isEveryoneUserLogin,\n resourceIdToString,\n resourceType,\n resourceTypesEqual,\n treeFilter,\n} from \"@milaboratories/pl-client\";\nimport { Computable } from \"@milaboratories/computable\";\nimport type { MiddleLayerEnvironment } from \"./middle_layer\";\nimport type { ProjectId } from \"@milaboratories/pl-model-common\";\nimport type {\n EnvelopeAcceptance,\n EnvelopeData,\n EnvelopeMode,\n EnvelopePayloadKind,\n ShareId,\n} from \"../model/sharing_model\";\nimport {\n AcceptanceFieldPrefix,\n asShareId,\n envelopeProjectMap,\n normalizeEnvelopeData,\n SharedEnvelopeResourceType,\n SharingOutboxResourceType,\n SharingStateResourceType,\n} from \"../model/sharing_model\";\n\n/** Donor-facing view of one outgoing share. */\nexport interface OutgoingShare {\n shareId: ShareId; // stable logical identity of the share, preserved across changes\n sharedAt: number; // this instance's creation time (ms epoch)\n expiresAt?: number; // EnvelopeData.expiresAt; null maps to undefined = never expires\n mode: EnvelopeMode;\n title: string; // display name shown to recipients; defaults to the first project's name\n /** What the share carries — a pack of projects, or one template document. */\n payloadKind: EnvelopePayloadKind;\n /** One entry per project in the pack, so the change UI can offer a per-project decision.\n * `projectId` is the donor's source project id; `updatedAt` is when this project's\n * snapshot was last (re)taken. Empty for a template share. */\n projects: { projectId: ProjectId; label: string; updatedAt: number }[];\n /** The shared template, for a template share; absent for a project share. */\n template?: { label: string; blockCount: number };\n /** Full recipient logins, from `ListGrants` on the envelope; `[\"*\"]` for everyone-shares. */\n recipients: string[];\n /** Whether {@link responses} can ever be populated for this share. A template share is granted\n * read-only, so no recipient can record a reply on the envelope and the donor never learns who\n * accepted — a view must say so rather than render an empty response list as \"nobody yet\". */\n responsesAvailable: boolean;\n /** Per recipient who has responded: their decision and when, from acceptance/{login}. */\n responses: Record<string, { action: \"accepted\" | \"rejected\"; timestamp: number }>;\n}\n\n/** Per-project view for the donor's change UI, from the envelope's `projects` payload; empty\n * for a payload that carries no project. */\nfunction envelopeProjects(data: EnvelopeData): OutgoingShare[\"projects\"] {\n return Object.values(envelopeProjectMap(data)).map((p) => ({\n projectId: p.source,\n label: p.label,\n updatedAt: p.updatedAt,\n }));\n}\n\n/** Acceptor-facing view of one pending share. */\nexport interface PendingShare {\n shareId: ShareId;\n sender: string; // EnvelopeData.sender, display only\n title: string; // display name shown to recipients; defaults to the first project's name\n mode: EnvelopeMode; // v1 renders only \"copy\" entries\n /** What the offer carries, so the recipient is told what they are being offered — projects to\n * copy, or a template for their own shelf. */\n payloadKind: EnvelopePayloadKind;\n grantedAt: number;\n}\n\nconst SharingOutboxPruningFunction: PruningFunction = (resource) => {\n if (\n !resourceTypesEqual(resource.type, SharingOutboxResourceType) &&\n !resourceTypesEqual(resource.type, SharedEnvelopeResourceType)\n )\n return [];\n return resource.fields;\n};\n\n// Server-side traversal scope (modern resourceTree path). Pruning is client-side ONLY and does\n// NOT stop the backend walk — without a fieldFilter the backend descends through the envelope's\n// project/{uuid} snapshots into the whole project graph (StreamManager etc.), whose field-driven\n// finality predicate then throws on the pruned-to-[] fields. Following fields only FROM the outbox\n// and the envelope stops the walk at the project snapshots (UserProject), which we never traverse.\nconst SharingOutboxFieldFilter: Filter = treeFilter.or(\n treeFilter.resourceTypeEq(SharingOutboxResourceType.name),\n treeFilter.resourceTypeEq(SharedEnvelopeResourceType.name),\n);\n\n/** Intermediate of an outgoing share before its full recipient list is fetched: everything the\n * tree carries synchronously, plus the envelope's signed id for the async `ListGrants` enrich. */\ntype OutgoingShareDraft = Omit<OutgoingShare, \"recipients\"> & { envelopeRid: SignedResourceId };\n\n/**\n * Reactive view of the donor's own outbox. Reads each live envelope's immutable\n * {@link EnvelopeData} plus the per-recipient `acceptance/{login}` records from the tree, then\n * enriches each share's full recipient list via `ListGrants` on the envelope (`[\"*\"]` for an\n * everyone-share). `ListGrants` is gated backend-side to the envelope owner (the donor), which is\n * exactly who reads this view.\n *\n * API-level only in M1 (no UI).\n */\nexport function createOutgoingSharesComputable(\n pl: PlClient,\n tree: SynchronizedTreeState,\n): Computable<OutgoingShare[] | undefined> {\n return Computable.make(\n (ctx) => {\n const node = ctx.accessor(tree.entry()).node();\n if (node === undefined) return undefined;\n\n const drafts: OutgoingShareDraft[] = [];\n // Outbox fields are keyed by shareId; each value is a SharedEnvelope.\n for (const fieldName of node.listDynamicFields()) {\n const envelope = node.traverse(fieldName);\n if (envelope === undefined) continue;\n if (!resourceTypesEqual(envelope.resourceType, SharedEnvelopeResourceType)) continue;\n\n const data = normalizeEnvelopeData(envelope.getDataAsJson<unknown>());\n if (data === undefined) continue; // unknown version or payload kind — not ours to show\n\n const responses: OutgoingShare[\"responses\"] = {};\n for (const f of envelope.listDynamicFields()) {\n if (!f.startsWith(AcceptanceFieldPrefix)) continue;\n const login = f.slice(AcceptanceFieldPrefix.length);\n const acc = envelope.traverse(f);\n const accData = acc?.getDataAsJson<EnvelopeAcceptance>();\n if (accData === undefined) continue;\n responses[login] = { action: accData.action, timestamp: accData.timestamp };\n }\n\n drafts.push({\n shareId: data.shareId,\n sharedAt: data.sharedAt,\n ...(data.expiresAt !== null ? { expiresAt: data.expiresAt } : {}),\n mode: data.mode,\n title: data.title,\n payloadKind: data.payload.kind,\n projects: envelopeProjects(data),\n ...(data.payload.kind === \"template\"\n ? {\n template: {\n label: data.payload.label,\n blockCount: data.payload.document.blocks.length,\n },\n }\n : {}),\n responsesAvailable: data.payload.kind !== \"template\",\n responses,\n envelopeRid: envelope.id,\n });\n }\n drafts.sort((a, b) => b.sharedAt - a.sharedAt);\n return drafts;\n },\n {\n // Resolve each share's recipients via listGrants. An everyone-grant maps to \"*\"; the donor's\n // own grant is dropped. One tx per envelope so a just-revoked rid faults only its own read,\n // which allSettled degrades to []. Recipients aren't cached — a live share's recipient set can\n // change. Transactional listGrants when available, else the standalone gRPC-only RPC.\n postprocessValue: async (\n drafts: OutgoingShareDraft[] | undefined,\n ): Promise<OutgoingShare[] | undefined> => {\n if (drafts === undefined) return undefined;\n const self = pl.userResources.authUser;\n const toRecipients = (grants: { user: string }[]): string[] =>\n grants.some((g) => isEveryoneUserLogin(g.user))\n ? [\"*\"]\n : grants.map((g) => g.user).filter((u) => u !== self);\n\n const txGrants = pl.hasCapability(\"txListGrants:v1\");\n const settled = await Promise.allSettled(\n drafts.map((d) =>\n txGrants\n ? pl.withReadTx(\"ListShareGrant\", (tx) => tx.listGrants(d.envelopeRid))\n : pl.userResources.listGrants(d.envelopeRid),\n ),\n );\n\n return drafts.map(({ envelopeRid: _envelopeRid, ...share }, i): OutgoingShare => {\n const r = settled[i];\n return { ...share, recipients: r.status === \"fulfilled\" ? toRecipients(r.value) : [] };\n });\n },\n },\n );\n}\n\n/**\n * Creates the donor's outbox synchronized tree (single explicit root) plus the\n * {@link OutgoingShare} computable over it.\n */\nexport async function createOutgoingShares(\n pl: PlClient,\n outboxRid: SignedResourceId,\n env: MiddleLayerEnvironment,\n): Promise<{ tree: SynchronizedTreeState; computable: Computable<OutgoingShare[] | undefined> }> {\n const tree = await SynchronizedTreeState.init(\n pl,\n outboxRid,\n {\n ...env.ops.defaultTreeOptions,\n pruning: SharingOutboxPruningFunction,\n fieldFilter: SharingOutboxFieldFilter,\n },\n env.logger,\n );\n return { computable: createOutgoingSharesComputable(pl, tree), tree };\n}\n\nconst DecisionFieldPrefix = \"decision/\";\n\nconst SharedEnvelopePruningFunction: PruningFunction = (resource) => {\n if (!resourceTypesEqual(resource.type, SharedEnvelopeResourceType)) return [];\n return resource.fields;\n};\n\n// Discovery only needs each envelope's immutable EnvelopeData (basic resource data); it must NOT\n// descend into the project snapshots. Following fields only FROM the envelope stops the walk at\n// the UserProject snapshots (their fields are never followed).\nconst PendingSharesFieldFilter: Filter = treeFilter.resourceTypeEq(SharedEnvelopeResourceType.name);\n\n/**\n * Creates the acceptor's shared-resource discovery tree (a `{kind:'shared'}` seed over\n * {@link SharedEnvelopeResourceType}) plus the {@link PendingShare} computable over it.\n *\n * An envelope surfaces only when its shareId has NO decision/{shareId} on SharingState —\n * the dedup is applied by the caller, which holds the SharingState tree.\n */\nexport async function createPendingSharesTree(\n pl: PlClient,\n env: MiddleLayerEnvironment,\n): Promise<SynchronizedTreeState> {\n return await SynchronizedTreeState.init(\n pl,\n {\n kind: \"shared\",\n resourceType: resourceType(\n SharedEnvelopeResourceType.name,\n SharedEnvelopeResourceType.version,\n ),\n },\n {\n ...env.ops.defaultTreeOptions,\n pruning: SharedEnvelopePruningFunction,\n fieldFilter: PendingSharesFieldFilter,\n },\n env.logger,\n );\n}\n\n/** A live envelope discovered in the acceptor's shared-resource tree: its signed resource id and\n * decoded {@link EnvelopeData}. The accept/reject flow keys these by `data.shareId`. */\nexport type LiveEnvelope = { rid: SignedResourceId; data: EnvelopeData };\n\n/**\n * Builds a Computable yielding the acceptor's currently-live envelopes, read from the\n * shared-resource discovery tree the ML already maintains — the single discovery mechanism.\n * Accept/reject `.getValue()` this instead of re-streaming `ListUserResources`, so there is no\n * second discovery path. The envelope's signed `rid` (from the tree node) is what the write tx\n * needs; `copyEnvelopeProjectsIntoList` reads the project snapshots itself inside the tx.\n *\n * Returns `undefined` while the tree is still empty/unresolved (mirrors the other tree-backed\n * computables here). Yields a flat list; the caller dedups by `shareId` — at most one live\n * envelope per shareId is expected (the donor keeps one), and a replace tears the old one down.\n */\nexport function createLiveEnvelopesComputable(\n sharedTree: SynchronizedTreeState,\n): Computable<LiveEnvelope[] | undefined> {\n return Computable.make((ctx) => {\n const roots = ctx.accessor(sharedTree.rootsEntry()).nodes();\n const result: LiveEnvelope[] = [];\n for (const envelope of roots) {\n if (envelope === undefined) continue;\n if (!resourceTypesEqual(envelope.resourceType, SharedEnvelopeResourceType)) continue;\n const data = normalizeEnvelopeData(envelope.getDataAsJson<unknown>());\n // Same recognise-or-hide rule as the pending view, and for the same envelope: hiding an\n // offer while accept could still resolve it would only move the problem.\n if (data === undefined) continue;\n result.push({ rid: envelope.id, data });\n }\n return result;\n });\n}\n\n/**\n * Builds the {@link PendingShare} computable over the shared-resource discovery tree, filtered\n * against the set of already-handled shareIds (those with a decision/{shareId} in the acceptor's\n * SharingState). Both trees feed one Computable so it recomputes when either changes.\n *\n * `currentUserLogin` (when known) suppresses the user's own shares: a share-with-everybody grants\n * the everyone-user, so the donor discovers their own envelope as a pending share. There is no\n * scenario where a user accepts their own share, so they are dropped from the pending view.\n */\nexport function createPendingSharesComputable(\n sharedTree: SynchronizedTreeState,\n sharingStateTree: SynchronizedTreeState,\n currentUserLogin: string | null,\n): Computable<PendingShare[] | undefined> {\n return Computable.make((ctx) => {\n const roots = ctx.accessor(sharedTree.rootsEntry()).nodes();\n\n // Set of shareIds already handled (accepted or rejected) — dedup discovery on the logical share.\n const stateNode = ctx.accessor(sharingStateTree.entry()).node();\n const handled = new Set<ShareId>();\n if (stateNode !== undefined)\n for (const f of stateNode.listDynamicFields())\n if (f.startsWith(DecisionFieldPrefix))\n handled.add(asShareId(f.slice(DecisionFieldPrefix.length)));\n\n const result: PendingShare[] = [];\n for (const envelope of roots) {\n if (envelope === undefined) continue;\n if (!resourceTypesEqual(envelope.resourceType, SharedEnvelopeResourceType)) continue;\n const data = normalizeEnvelopeData(envelope.getDataAsJson<unknown>());\n // An envelope whose schemaVersion or payload kind this build does not know is hidden, not\n // offered: there is nothing useful to do with a share we cannot read.\n if (data === undefined) continue;\n if (handled.has(data.shareId)) continue; // already accepted or rejected\n if (currentUserLogin !== null && data.sender === currentUserLogin) continue; // own share\n if (data.expiresAt !== null && data.expiresAt <= Date.now()) continue;\n\n result.push({\n shareId: data.shareId,\n sender: data.sender,\n title: data.title,\n mode: data.mode,\n payloadKind: data.payload.kind,\n grantedAt: data.sharedAt,\n });\n }\n result.sort((a, b) => b.grantedAt - a.grantedAt);\n return result;\n });\n}\n\nconst SharingStatePruningFunction: PruningFunction = (resource) => {\n if (!resourceTypesEqual(resource.type, SharingStateResourceType)) return [];\n return resource.fields;\n};\n\n// decision/{shareId} values are leaf JSON resources; follow fields only from SharingState.\nconst SharingStateFieldFilter: Filter = treeFilter.resourceTypeEq(SharingStateResourceType.name);\n\n/** Creates the acceptor's SharingState synchronized tree (single explicit root). */\nexport async function createSharingStateTree(\n pl: PlClient,\n stateRid: SignedResourceId,\n env: MiddleLayerEnvironment,\n): Promise<SynchronizedTreeState> {\n return await SynchronizedTreeState.init(\n pl,\n stateRid,\n {\n ...env.ops.defaultTreeOptions,\n pruning: SharingStatePruningFunction,\n fieldFilter: SharingStateFieldFilter,\n },\n env.logger,\n );\n}\n\n/** Maps an envelope resource id (signed) to its string form, for failure reporting. */\nexport function envelopeIdString(rid: SignedResourceId): string {\n return resourceIdToString(rid);\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
let _milaboratories_ts_helpers = require("@milaboratories/ts-helpers");
|
|
2
|
+
let _milaboratories_pl_client = require("@milaboratories/pl-client");
|
|
3
|
+
let _milaboratories_pl_tree = require("@milaboratories/pl-tree");
|
|
4
|
+
let _milaboratories_computable = require("@milaboratories/computable");
|
|
5
|
+
//#region src/middle_layer/template_list.ts
|
|
6
|
+
const TemplatesField = "templates";
|
|
7
|
+
const TemplatesResourceType = {
|
|
8
|
+
name: "Templates",
|
|
9
|
+
version: "1"
|
|
10
|
+
};
|
|
11
|
+
const TemplateResourceType = {
|
|
12
|
+
name: "UserTemplate",
|
|
13
|
+
version: "1"
|
|
14
|
+
};
|
|
15
|
+
/** Mutable: the only part of a stored template a rename may touch. */
|
|
16
|
+
const TemplateLabelKey = "TemplateLabel";
|
|
17
|
+
const TemplateCreatedTimestamp = "TemplateCreated";
|
|
18
|
+
/** Decodes the immutable `data` blob of a `UserTemplate` read through a transaction. The
|
|
19
|
+
* single raw-decode site; the tree side reads the same JSON with `getDataAsJson`. */
|
|
20
|
+
function decodeStoredTemplateData(data) {
|
|
21
|
+
return JSON.parse(Buffer.from(data).toString("utf-8"));
|
|
22
|
+
}
|
|
23
|
+
const TemplatesListTreePruningFunction = (resource) => {
|
|
24
|
+
if (!(0, _milaboratories_pl_client.resourceTypesEqual)(resource.type, TemplatesResourceType)) return [];
|
|
25
|
+
return resource.fields;
|
|
26
|
+
};
|
|
27
|
+
const templatesListFieldFilter = _milaboratories_pl_client.treeFilter.resourceTypeEq(TemplatesResourceType.name);
|
|
28
|
+
async function createTemplateList(pl, rid, env) {
|
|
29
|
+
const tree = await _milaboratories_pl_tree.SynchronizedTreeState.init(pl, rid, {
|
|
30
|
+
...env.ops.defaultTreeOptions,
|
|
31
|
+
pruning: TemplatesListTreePruningFunction,
|
|
32
|
+
fieldFilter: templatesListFieldFilter
|
|
33
|
+
}, env.logger);
|
|
34
|
+
return {
|
|
35
|
+
computable: _milaboratories_computable.Computable.make((ctx) => {
|
|
36
|
+
const node = ctx.accessor(tree.entry()).node();
|
|
37
|
+
if (node === void 0) return void 0;
|
|
38
|
+
const result = [];
|
|
39
|
+
for (const field of node.listDynamicFields()) {
|
|
40
|
+
const tpl = node.traverse(field);
|
|
41
|
+
if (tpl === void 0) continue;
|
|
42
|
+
const data = tpl.getDataAsJson();
|
|
43
|
+
if (data === void 0) continue;
|
|
44
|
+
const label = (0, _milaboratories_ts_helpers.notEmpty)(tpl.getKeyValueAsJson(TemplateLabelKey));
|
|
45
|
+
const created = (0, _milaboratories_ts_helpers.notEmpty)(tpl.getKeyValueAsJson(TemplateCreatedTimestamp));
|
|
46
|
+
result.push({
|
|
47
|
+
id: (0, _milaboratories_pl_client.resourceIdToString)(tpl.id),
|
|
48
|
+
label,
|
|
49
|
+
created: new Date(created),
|
|
50
|
+
blockCount: data.document.blocks.length,
|
|
51
|
+
...data.sourceProjectLabel !== void 0 ? { sourceProjectLabel: data.sourceProjectLabel } : {},
|
|
52
|
+
...data.sender !== void 0 ? { sender: data.sender } : {}
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
result.sort((a, b) => b.created.valueOf() - a.created.valueOf());
|
|
56
|
+
return result;
|
|
57
|
+
}).withStableType(),
|
|
58
|
+
tree
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
//#endregion
|
|
62
|
+
exports.TemplateCreatedTimestamp = TemplateCreatedTimestamp;
|
|
63
|
+
exports.TemplateLabelKey = TemplateLabelKey;
|
|
64
|
+
exports.TemplateResourceType = TemplateResourceType;
|
|
65
|
+
exports.TemplatesField = TemplatesField;
|
|
66
|
+
exports.TemplatesListTreePruningFunction = TemplatesListTreePruningFunction;
|
|
67
|
+
exports.TemplatesResourceType = TemplatesResourceType;
|
|
68
|
+
exports.createTemplateList = createTemplateList;
|
|
69
|
+
exports.decodeStoredTemplateData = decodeStoredTemplateData;
|
|
70
|
+
exports.templatesListFieldFilter = templatesListFieldFilter;
|
|
71
|
+
|
|
72
|
+
//# sourceMappingURL=template_list.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"template_list.cjs","names":["treeFilter","SynchronizedTreeState","Computable"],"sources":["../../src/middle_layer/template_list.ts"],"sourcesContent":["import type { PruningFunction } from \"@milaboratories/pl-tree\";\nimport { SynchronizedTreeState } from \"@milaboratories/pl-tree\";\nimport type {\n Filter,\n PlClient,\n PlTransaction,\n ResourceType,\n SignedResourceId,\n} from \"@milaboratories/pl-client\";\nimport {\n field,\n isNullSignedResourceId,\n resourceIdToString,\n resourceTypesEqual,\n treeFilter,\n} from \"@milaboratories/pl-client\";\nimport type { TreeAndComputableU } from \"./types\";\nimport { Computable } from \"@milaboratories/computable\";\nimport type { MiddleLayerEnvironment } from \"./middle_layer\";\nimport { notEmpty } from \"@milaboratories/ts-helpers\";\nimport type { Branded, ProjectTemplateV1 } from \"@milaboratories/pl-model-common\";\nimport type { TemplateExportProblem } from \"../model/template_export\";\nimport type { TemplateShareProblem } from \"../model/template_share\";\nimport type { ShareId } from \"../model/sharing_model\";\nimport type { AppliedEntry, TemplateApplyProblem } from \"../model/template_apply\";\nimport type { ProjectId } from \"../model/project_model\";\n\nexport const TemplatesField = \"templates\";\nexport const TemplatesResourceType: ResourceType = { name: \"Templates\", version: \"1\" };\nexport const TemplateResourceType: ResourceType = { name: \"UserTemplate\", version: \"1\" };\n\n/** Mutable: the only part of a stored template a rename may touch. */\nexport const TemplateLabelKey = \"TemplateLabel\";\nexport const TemplateCreatedTimestamp = \"TemplateCreated\";\n\n/**\n * Unique template identifier in middle layer, the stringified signed resource id of the\n * `UserTemplate`. Branded so it cannot be confused with a {@link ProjectId} — both are\n * stringified resource ids and every template method takes one of them.\n */\nexport type TemplateId = Branded<string, \"TemplateId\">;\n\n/**\n * Immutable `data` on a UserTemplate: the document plus what was true when it was taken.\n *\n * The document lives here and never in KV: KV is listed and fully re-read on every poll,\n * while resource data syncs incrementally, and a template document is a multi-kilobyte\n * value that never changes.\n */\nexport interface StoredTemplateData {\n schemaVersion: 1;\n document: ProjectTemplateV1;\n /** Provenance, display only; absent for a template that arrived as a share. */\n sourceProjectLabel?: string;\n /** Login of the sender, when it arrived as a share. */\n sender?: string;\n}\n\n/** Decodes the immutable `data` blob of a `UserTemplate` read through a transaction. The\n * single raw-decode site; the tree side reads the same JSON with `getDataAsJson`. */\nexport function decodeStoredTemplateData(data: Uint8Array): StoredTemplateData {\n return JSON.parse(Buffer.from(data).toString(\"utf-8\")) as StoredTemplateData;\n}\n\n/** One template as the template list surfaces it. */\nexport interface TemplateListEntry {\n /** Unique template identifier in middle layer. Use to operate with the given template. */\n id: TemplateId;\n /** The mutable label, the only part a rename changes. */\n label: string;\n created: Date;\n /** Number of blocks the stored document lists — derived, not stored. */\n blockCount: number;\n sourceProjectLabel?: string;\n sender?: string;\n}\n\n/** What saving a project as a template yields: the stored template, or every block in the way. */\nexport type SaveProjectAsTemplateOutcome =\n | { readonly ok: true; readonly templateId: TemplateId }\n | { readonly ok: false; readonly problems: readonly TemplateExportProblem[] };\n\n/** What sharing a stored template yields: the share's logical id, or every entry in the way. */\nexport type ShareTemplateOutcome =\n | { readonly ok: true; readonly shareId: ShareId }\n | { readonly ok: false; readonly problems: readonly TemplateShareProblem[] };\n\n/**\n * What applying a stored template yields.\n *\n * `ok: false` carries no project id because no project was created: nothing is written\n * until every entry has an installable block.\n */\nexport type CreateProjectFromTemplateOutcome =\n | {\n readonly ok: true;\n readonly projectId: ProjectId;\n readonly added: readonly AppliedEntry[];\n }\n | { readonly ok: false; readonly problems: readonly TemplateApplyProblem[] };\n\n/**\n * Resolves the templates-list resource on the transaction's client root, lazily creating (and\n * locking) an empty one when the {@link TemplatesField} is not yet populated. Returns its signed\n * id. Used when writing into a root that may have no templates list yet, e.g. a template landing\n * in a recipient's root.\n */\nexport async function ensureTemplateListRid(tx: PlTransaction): Promise<SignedResourceId> {\n const templatesField = field(tx.clientRoot, TemplatesField);\n tx.createField(templatesField, \"Dynamic\");\n const fData = await tx.getField(templatesField);\n if (isNullSignedResourceId(fData.value)) {\n const ref = tx.createEphemeral(TemplatesResourceType);\n tx.lock(ref);\n tx.setField(templatesField, ref);\n return await ref.globalId;\n }\n return fData.value;\n}\n\nexport const TemplatesListTreePruningFunction: PruningFunction = (resource) => {\n if (!resourceTypesEqual(resource.type, TemplatesResourceType)) return [];\n return resource.fields;\n};\n\nexport const templatesListFieldFilter: Filter = treeFilter.resourceTypeEq(\n TemplatesResourceType.name,\n);\n\nexport async function createTemplateList(\n pl: PlClient,\n rid: SignedResourceId,\n env: MiddleLayerEnvironment,\n): Promise<TreeAndComputableU<TemplateListEntry[]>> {\n const tree = await SynchronizedTreeState.init(\n pl,\n rid,\n {\n ...env.ops.defaultTreeOptions,\n pruning: TemplatesListTreePruningFunction,\n fieldFilter: templatesListFieldFilter,\n },\n env.logger,\n );\n\n const c = Computable.make((ctx) => {\n const node = ctx.accessor(tree.entry()).node();\n if (node === undefined) return undefined;\n const result: TemplateListEntry[] = [];\n\n // Templates list resource keeps templates assigned to fields. Each field name is a UUID\n for (const field of node.listDynamicFields()) {\n const tpl = node.traverse(field);\n if (tpl === undefined) continue;\n const data = tpl.getDataAsJson<StoredTemplateData>();\n // A template whose data has not synced yet is not an entry with unknown content —\n // it is an entry we cannot describe at all, so it stays out of the list until it has.\n if (data === undefined) continue;\n const label = notEmpty(tpl.getKeyValueAsJson<string>(TemplateLabelKey));\n const created = notEmpty(tpl.getKeyValueAsJson<number>(TemplateCreatedTimestamp));\n result.push({\n id: resourceIdToString(tpl.id) as TemplateId,\n label,\n created: new Date(created),\n blockCount: data.document.blocks.length,\n ...(data.sourceProjectLabel !== undefined\n ? { sourceProjectLabel: data.sourceProjectLabel }\n : {}),\n ...(data.sender !== undefined ? { sender: data.sender } : {}),\n });\n }\n result.sort((a, b) => b.created.valueOf() - a.created.valueOf());\n return result;\n }).withStableType();\n\n return { computable: c, tree };\n}\n"],"mappings":";;;;;AA2BA,MAAa,iBAAiB;AAC9B,MAAa,wBAAsC;CAAE,MAAM;CAAa,SAAS;AAAI;AACrF,MAAa,uBAAqC;CAAE,MAAM;CAAgB,SAAS;AAAI;;AAGvF,MAAa,mBAAmB;AAChC,MAAa,2BAA2B;;;AA2BxC,SAAgB,yBAAyB,MAAsC;CAC7E,OAAO,KAAK,MAAM,OAAO,KAAK,IAAI,CAAC,CAAC,SAAS,OAAO,CAAC;AACvD;AA0DA,MAAa,oCAAqD,aAAa;CAC7E,IAAI,EAAA,GAAA,0BAAA,mBAAA,CAAoB,SAAS,MAAM,qBAAqB,GAAG,OAAO,CAAC;CACvE,OAAO,SAAS;AAClB;AAEA,MAAa,2BAAmCA,0BAAAA,WAAW,eACzD,sBAAsB,IACxB;AAEA,eAAsB,mBACpB,IACA,KACA,KACkD;CAClD,MAAM,OAAO,MAAMC,wBAAAA,sBAAsB,KACvC,IACA,KACA;EACE,GAAG,IAAI,IAAI;EACX,SAAS;EACT,aAAa;CACf,GACA,IAAI,MACN;CAgCA,OAAO;EAAE,YA9BCC,2BAAAA,WAAW,MAAM,QAAQ;GACjC,MAAM,OAAO,IAAI,SAAS,KAAK,MAAM,CAAC,CAAC,CAAC,KAAK;GAC7C,IAAI,SAAS,KAAA,GAAW,OAAO,KAAA;GAC/B,MAAM,SAA8B,CAAC;GAGrC,KAAK,MAAM,SAAS,KAAK,kBAAkB,GAAG;IAC5C,MAAM,MAAM,KAAK,SAAS,KAAK;IAC/B,IAAI,QAAQ,KAAA,GAAW;IACvB,MAAM,OAAO,IAAI,cAAkC;IAGnD,IAAI,SAAS,KAAA,GAAW;IACxB,MAAM,SAAA,GAAA,2BAAA,SAAA,CAAiB,IAAI,kBAA0B,gBAAgB,CAAC;IACtE,MAAM,WAAA,GAAA,2BAAA,SAAA,CAAmB,IAAI,kBAA0B,wBAAwB,CAAC;IAChF,OAAO,KAAK;KACV,KAAA,GAAA,0BAAA,mBAAA,CAAuB,IAAI,EAAE;KAC7B;KACA,SAAS,IAAI,KAAK,OAAO;KACzB,YAAY,KAAK,SAAS,OAAO;KACjC,GAAI,KAAK,uBAAuB,KAAA,IAC5B,EAAE,oBAAoB,KAAK,mBAAmB,IAC9C,CAAC;KACL,GAAI,KAAK,WAAW,KAAA,IAAY,EAAE,QAAQ,KAAK,OAAO,IAAI,CAAC;IAC7D,CAAC;GACH;GACA,OAAO,MAAM,GAAG,MAAM,EAAE,QAAQ,QAAQ,IAAI,EAAE,QAAQ,QAAQ,CAAC;GAC/D,OAAO;EACT,CAAC,CAAC,CAAC,eAEkB;EAAG;CAAK;AAC/B"}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { AppliedEntry, TemplateApplyProblem } from "../model/template_apply.js";
|
|
2
|
+
import "./types.js";
|
|
3
|
+
import { ProjectId as ProjectId$1 } from "../model/project_model.js";
|
|
4
|
+
import { TemplateExportProblem } from "../model/template_export.js";
|
|
5
|
+
import { TemplateShareProblem } from "../model/template_share.js";
|
|
6
|
+
import { ShareId } from "../model/sharing_model.js";
|
|
7
|
+
import "./middle_layer.js";
|
|
8
|
+
import { Branded, ProjectTemplateV1 } from "@milaboratories/pl-model-common";
|
|
9
|
+
import { PlClient, PlTransaction, ResourceType, SignedResourceId } from "@milaboratories/pl-client";
|
|
10
|
+
import "@milaboratories/pl-tree";
|
|
11
|
+
//#region src/middle_layer/template_list.d.ts
|
|
12
|
+
export declare const TemplatesField = "templates";
|
|
13
|
+
/**
|
|
14
|
+
* Unique template identifier in middle layer, the stringified signed resource id of the
|
|
15
|
+
* `UserTemplate`. Branded so it cannot be confused with a {@link ProjectId} — both are
|
|
16
|
+
* stringified resource ids and every template method takes one of them.
|
|
17
|
+
*/
|
|
18
|
+
export type TemplateId = Branded<string, "TemplateId">;
|
|
19
|
+
/**
|
|
20
|
+
* Immutable `data` on a UserTemplate: the document plus what was true when it was taken.
|
|
21
|
+
*
|
|
22
|
+
* The document lives here and never in KV: KV is listed and fully re-read on every poll,
|
|
23
|
+
* while resource data syncs incrementally, and a template document is a multi-kilobyte
|
|
24
|
+
* value that never changes.
|
|
25
|
+
*/
|
|
26
|
+
export interface StoredTemplateData {
|
|
27
|
+
schemaVersion: 1;
|
|
28
|
+
document: ProjectTemplateV1;
|
|
29
|
+
/** Provenance, display only; absent for a template that arrived as a share. */
|
|
30
|
+
sourceProjectLabel?: string;
|
|
31
|
+
/** Login of the sender, when it arrived as a share. */
|
|
32
|
+
sender?: string;
|
|
33
|
+
}
|
|
34
|
+
/** One template as the template list surfaces it. */
|
|
35
|
+
export interface TemplateListEntry {
|
|
36
|
+
/** Unique template identifier in middle layer. Use to operate with the given template. */
|
|
37
|
+
id: TemplateId;
|
|
38
|
+
/** The mutable label, the only part a rename changes. */
|
|
39
|
+
label: string;
|
|
40
|
+
created: Date;
|
|
41
|
+
/** Number of blocks the stored document lists — derived, not stored. */
|
|
42
|
+
blockCount: number;
|
|
43
|
+
sourceProjectLabel?: string;
|
|
44
|
+
sender?: string;
|
|
45
|
+
}
|
|
46
|
+
/** What saving a project as a template yields: the stored template, or every block in the way. */
|
|
47
|
+
export type SaveProjectAsTemplateOutcome = {
|
|
48
|
+
readonly ok: true;
|
|
49
|
+
readonly templateId: TemplateId;
|
|
50
|
+
} | {
|
|
51
|
+
readonly ok: false;
|
|
52
|
+
readonly problems: readonly TemplateExportProblem[];
|
|
53
|
+
};
|
|
54
|
+
/** What sharing a stored template yields: the share's logical id, or every entry in the way. */
|
|
55
|
+
export type ShareTemplateOutcome = {
|
|
56
|
+
readonly ok: true;
|
|
57
|
+
readonly shareId: ShareId;
|
|
58
|
+
} | {
|
|
59
|
+
readonly ok: false;
|
|
60
|
+
readonly problems: readonly TemplateShareProblem[];
|
|
61
|
+
};
|
|
62
|
+
/**
|
|
63
|
+
* What applying a stored template yields.
|
|
64
|
+
*
|
|
65
|
+
* `ok: false` carries no project id because no project was created: nothing is written
|
|
66
|
+
* until every entry has an installable block.
|
|
67
|
+
*/
|
|
68
|
+
export type CreateProjectFromTemplateOutcome = {
|
|
69
|
+
readonly ok: true;
|
|
70
|
+
readonly projectId: ProjectId$1;
|
|
71
|
+
readonly added: readonly AppliedEntry[];
|
|
72
|
+
} | {
|
|
73
|
+
readonly ok: false;
|
|
74
|
+
readonly problems: readonly TemplateApplyProblem[];
|
|
75
|
+
};
|
|
76
|
+
//#endregion
|
|
77
|
+
//# sourceMappingURL=template_list.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"template_list.d.ts","names":[],"sources":["../../src/middle_layer/template_list.ts"],"mappings":";;;;;;;;;;;qBA2Ba;;;;;;YAaD,aAAa;;;;;;;;iBASR;EACf;EACA,UAAU;;EAEV;;EAEA;;;iBAUe;;EAEf,IAAI;;EAEJ;EACA,SAAS;;EAET;EACA;EACA;;;YAIU;WACG;WAAmB,YAAY;;WAC/B;WAAoB,mBAAmB;;;YAG1C;WACG;WAAmB,SAAS;;WAC5B;WAAoB,mBAAmB;;;;;;;;YAQ1C;WAEG;WACA,WAAW;WACX,gBAAgB;;WAEhB;WAAoB,mBAAmB"}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { notEmpty } from "@milaboratories/ts-helpers";
|
|
2
|
+
import { resourceIdToString, resourceTypesEqual, treeFilter } from "@milaboratories/pl-client";
|
|
3
|
+
import { SynchronizedTreeState } from "@milaboratories/pl-tree";
|
|
4
|
+
import { Computable } from "@milaboratories/computable";
|
|
5
|
+
//#region src/middle_layer/template_list.ts
|
|
6
|
+
const TemplatesField = "templates";
|
|
7
|
+
const TemplatesResourceType = {
|
|
8
|
+
name: "Templates",
|
|
9
|
+
version: "1"
|
|
10
|
+
};
|
|
11
|
+
const TemplateResourceType = {
|
|
12
|
+
name: "UserTemplate",
|
|
13
|
+
version: "1"
|
|
14
|
+
};
|
|
15
|
+
/** Mutable: the only part of a stored template a rename may touch. */
|
|
16
|
+
const TemplateLabelKey = "TemplateLabel";
|
|
17
|
+
const TemplateCreatedTimestamp = "TemplateCreated";
|
|
18
|
+
/** Decodes the immutable `data` blob of a `UserTemplate` read through a transaction. The
|
|
19
|
+
* single raw-decode site; the tree side reads the same JSON with `getDataAsJson`. */
|
|
20
|
+
function decodeStoredTemplateData(data) {
|
|
21
|
+
return JSON.parse(Buffer.from(data).toString("utf-8"));
|
|
22
|
+
}
|
|
23
|
+
const TemplatesListTreePruningFunction = (resource) => {
|
|
24
|
+
if (!resourceTypesEqual(resource.type, TemplatesResourceType)) return [];
|
|
25
|
+
return resource.fields;
|
|
26
|
+
};
|
|
27
|
+
const templatesListFieldFilter = treeFilter.resourceTypeEq(TemplatesResourceType.name);
|
|
28
|
+
async function createTemplateList(pl, rid, env) {
|
|
29
|
+
const tree = await SynchronizedTreeState.init(pl, rid, {
|
|
30
|
+
...env.ops.defaultTreeOptions,
|
|
31
|
+
pruning: TemplatesListTreePruningFunction,
|
|
32
|
+
fieldFilter: templatesListFieldFilter
|
|
33
|
+
}, env.logger);
|
|
34
|
+
return {
|
|
35
|
+
computable: Computable.make((ctx) => {
|
|
36
|
+
const node = ctx.accessor(tree.entry()).node();
|
|
37
|
+
if (node === void 0) return void 0;
|
|
38
|
+
const result = [];
|
|
39
|
+
for (const field of node.listDynamicFields()) {
|
|
40
|
+
const tpl = node.traverse(field);
|
|
41
|
+
if (tpl === void 0) continue;
|
|
42
|
+
const data = tpl.getDataAsJson();
|
|
43
|
+
if (data === void 0) continue;
|
|
44
|
+
const label = notEmpty(tpl.getKeyValueAsJson(TemplateLabelKey));
|
|
45
|
+
const created = notEmpty(tpl.getKeyValueAsJson(TemplateCreatedTimestamp));
|
|
46
|
+
result.push({
|
|
47
|
+
id: resourceIdToString(tpl.id),
|
|
48
|
+
label,
|
|
49
|
+
created: new Date(created),
|
|
50
|
+
blockCount: data.document.blocks.length,
|
|
51
|
+
...data.sourceProjectLabel !== void 0 ? { sourceProjectLabel: data.sourceProjectLabel } : {},
|
|
52
|
+
...data.sender !== void 0 ? { sender: data.sender } : {}
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
result.sort((a, b) => b.created.valueOf() - a.created.valueOf());
|
|
56
|
+
return result;
|
|
57
|
+
}).withStableType(),
|
|
58
|
+
tree
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
//#endregion
|
|
62
|
+
export { TemplateCreatedTimestamp, TemplateLabelKey, TemplateResourceType, TemplatesField, TemplatesListTreePruningFunction, TemplatesResourceType, createTemplateList, decodeStoredTemplateData, templatesListFieldFilter };
|
|
63
|
+
|
|
64
|
+
//# sourceMappingURL=template_list.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"template_list.js","names":[],"sources":["../../src/middle_layer/template_list.ts"],"sourcesContent":["import type { PruningFunction } from \"@milaboratories/pl-tree\";\nimport { SynchronizedTreeState } from \"@milaboratories/pl-tree\";\nimport type {\n Filter,\n PlClient,\n PlTransaction,\n ResourceType,\n SignedResourceId,\n} from \"@milaboratories/pl-client\";\nimport {\n field,\n isNullSignedResourceId,\n resourceIdToString,\n resourceTypesEqual,\n treeFilter,\n} from \"@milaboratories/pl-client\";\nimport type { TreeAndComputableU } from \"./types\";\nimport { Computable } from \"@milaboratories/computable\";\nimport type { MiddleLayerEnvironment } from \"./middle_layer\";\nimport { notEmpty } from \"@milaboratories/ts-helpers\";\nimport type { Branded, ProjectTemplateV1 } from \"@milaboratories/pl-model-common\";\nimport type { TemplateExportProblem } from \"../model/template_export\";\nimport type { TemplateShareProblem } from \"../model/template_share\";\nimport type { ShareId } from \"../model/sharing_model\";\nimport type { AppliedEntry, TemplateApplyProblem } from \"../model/template_apply\";\nimport type { ProjectId } from \"../model/project_model\";\n\nexport const TemplatesField = \"templates\";\nexport const TemplatesResourceType: ResourceType = { name: \"Templates\", version: \"1\" };\nexport const TemplateResourceType: ResourceType = { name: \"UserTemplate\", version: \"1\" };\n\n/** Mutable: the only part of a stored template a rename may touch. */\nexport const TemplateLabelKey = \"TemplateLabel\";\nexport const TemplateCreatedTimestamp = \"TemplateCreated\";\n\n/**\n * Unique template identifier in middle layer, the stringified signed resource id of the\n * `UserTemplate`. Branded so it cannot be confused with a {@link ProjectId} — both are\n * stringified resource ids and every template method takes one of them.\n */\nexport type TemplateId = Branded<string, \"TemplateId\">;\n\n/**\n * Immutable `data` on a UserTemplate: the document plus what was true when it was taken.\n *\n * The document lives here and never in KV: KV is listed and fully re-read on every poll,\n * while resource data syncs incrementally, and a template document is a multi-kilobyte\n * value that never changes.\n */\nexport interface StoredTemplateData {\n schemaVersion: 1;\n document: ProjectTemplateV1;\n /** Provenance, display only; absent for a template that arrived as a share. */\n sourceProjectLabel?: string;\n /** Login of the sender, when it arrived as a share. */\n sender?: string;\n}\n\n/** Decodes the immutable `data` blob of a `UserTemplate` read through a transaction. The\n * single raw-decode site; the tree side reads the same JSON with `getDataAsJson`. */\nexport function decodeStoredTemplateData(data: Uint8Array): StoredTemplateData {\n return JSON.parse(Buffer.from(data).toString(\"utf-8\")) as StoredTemplateData;\n}\n\n/** One template as the template list surfaces it. */\nexport interface TemplateListEntry {\n /** Unique template identifier in middle layer. Use to operate with the given template. */\n id: TemplateId;\n /** The mutable label, the only part a rename changes. */\n label: string;\n created: Date;\n /** Number of blocks the stored document lists — derived, not stored. */\n blockCount: number;\n sourceProjectLabel?: string;\n sender?: string;\n}\n\n/** What saving a project as a template yields: the stored template, or every block in the way. */\nexport type SaveProjectAsTemplateOutcome =\n | { readonly ok: true; readonly templateId: TemplateId }\n | { readonly ok: false; readonly problems: readonly TemplateExportProblem[] };\n\n/** What sharing a stored template yields: the share's logical id, or every entry in the way. */\nexport type ShareTemplateOutcome =\n | { readonly ok: true; readonly shareId: ShareId }\n | { readonly ok: false; readonly problems: readonly TemplateShareProblem[] };\n\n/**\n * What applying a stored template yields.\n *\n * `ok: false` carries no project id because no project was created: nothing is written\n * until every entry has an installable block.\n */\nexport type CreateProjectFromTemplateOutcome =\n | {\n readonly ok: true;\n readonly projectId: ProjectId;\n readonly added: readonly AppliedEntry[];\n }\n | { readonly ok: false; readonly problems: readonly TemplateApplyProblem[] };\n\n/**\n * Resolves the templates-list resource on the transaction's client root, lazily creating (and\n * locking) an empty one when the {@link TemplatesField} is not yet populated. Returns its signed\n * id. Used when writing into a root that may have no templates list yet, e.g. a template landing\n * in a recipient's root.\n */\nexport async function ensureTemplateListRid(tx: PlTransaction): Promise<SignedResourceId> {\n const templatesField = field(tx.clientRoot, TemplatesField);\n tx.createField(templatesField, \"Dynamic\");\n const fData = await tx.getField(templatesField);\n if (isNullSignedResourceId(fData.value)) {\n const ref = tx.createEphemeral(TemplatesResourceType);\n tx.lock(ref);\n tx.setField(templatesField, ref);\n return await ref.globalId;\n }\n return fData.value;\n}\n\nexport const TemplatesListTreePruningFunction: PruningFunction = (resource) => {\n if (!resourceTypesEqual(resource.type, TemplatesResourceType)) return [];\n return resource.fields;\n};\n\nexport const templatesListFieldFilter: Filter = treeFilter.resourceTypeEq(\n TemplatesResourceType.name,\n);\n\nexport async function createTemplateList(\n pl: PlClient,\n rid: SignedResourceId,\n env: MiddleLayerEnvironment,\n): Promise<TreeAndComputableU<TemplateListEntry[]>> {\n const tree = await SynchronizedTreeState.init(\n pl,\n rid,\n {\n ...env.ops.defaultTreeOptions,\n pruning: TemplatesListTreePruningFunction,\n fieldFilter: templatesListFieldFilter,\n },\n env.logger,\n );\n\n const c = Computable.make((ctx) => {\n const node = ctx.accessor(tree.entry()).node();\n if (node === undefined) return undefined;\n const result: TemplateListEntry[] = [];\n\n // Templates list resource keeps templates assigned to fields. Each field name is a UUID\n for (const field of node.listDynamicFields()) {\n const tpl = node.traverse(field);\n if (tpl === undefined) continue;\n const data = tpl.getDataAsJson<StoredTemplateData>();\n // A template whose data has not synced yet is not an entry with unknown content —\n // it is an entry we cannot describe at all, so it stays out of the list until it has.\n if (data === undefined) continue;\n const label = notEmpty(tpl.getKeyValueAsJson<string>(TemplateLabelKey));\n const created = notEmpty(tpl.getKeyValueAsJson<number>(TemplateCreatedTimestamp));\n result.push({\n id: resourceIdToString(tpl.id) as TemplateId,\n label,\n created: new Date(created),\n blockCount: data.document.blocks.length,\n ...(data.sourceProjectLabel !== undefined\n ? { sourceProjectLabel: data.sourceProjectLabel }\n : {}),\n ...(data.sender !== undefined ? { sender: data.sender } : {}),\n });\n }\n result.sort((a, b) => b.created.valueOf() - a.created.valueOf());\n return result;\n }).withStableType();\n\n return { computable: c, tree };\n}\n"],"mappings":";;;;;AA2BA,MAAa,iBAAiB;AAC9B,MAAa,wBAAsC;CAAE,MAAM;CAAa,SAAS;AAAI;AACrF,MAAa,uBAAqC;CAAE,MAAM;CAAgB,SAAS;AAAI;;AAGvF,MAAa,mBAAmB;AAChC,MAAa,2BAA2B;;;AA2BxC,SAAgB,yBAAyB,MAAsC;CAC7E,OAAO,KAAK,MAAM,OAAO,KAAK,IAAI,CAAC,CAAC,SAAS,OAAO,CAAC;AACvD;AA0DA,MAAa,oCAAqD,aAAa;CAC7E,IAAI,CAAC,mBAAmB,SAAS,MAAM,qBAAqB,GAAG,OAAO,CAAC;CACvE,OAAO,SAAS;AAClB;AAEA,MAAa,2BAAmC,WAAW,eACzD,sBAAsB,IACxB;AAEA,eAAsB,mBACpB,IACA,KACA,KACkD;CAClD,MAAM,OAAO,MAAM,sBAAsB,KACvC,IACA,KACA;EACE,GAAG,IAAI,IAAI;EACX,SAAS;EACT,aAAa;CACf,GACA,IAAI,MACN;CAgCA,OAAO;EAAE,YA9BC,WAAW,MAAM,QAAQ;GACjC,MAAM,OAAO,IAAI,SAAS,KAAK,MAAM,CAAC,CAAC,CAAC,KAAK;GAC7C,IAAI,SAAS,KAAA,GAAW,OAAO,KAAA;GAC/B,MAAM,SAA8B,CAAC;GAGrC,KAAK,MAAM,SAAS,KAAK,kBAAkB,GAAG;IAC5C,MAAM,MAAM,KAAK,SAAS,KAAK;IAC/B,IAAI,QAAQ,KAAA,GAAW;IACvB,MAAM,OAAO,IAAI,cAAkC;IAGnD,IAAI,SAAS,KAAA,GAAW;IACxB,MAAM,QAAQ,SAAS,IAAI,kBAA0B,gBAAgB,CAAC;IACtE,MAAM,UAAU,SAAS,IAAI,kBAA0B,wBAAwB,CAAC;IAChF,OAAO,KAAK;KACV,IAAI,mBAAmB,IAAI,EAAE;KAC7B;KACA,SAAS,IAAI,KAAK,OAAO;KACzB,YAAY,KAAK,SAAS,OAAO;KACjC,GAAI,KAAK,uBAAuB,KAAA,IAC5B,EAAE,oBAAoB,KAAK,mBAAmB,IAC9C,CAAC;KACL,GAAI,KAAK,WAAW,KAAA,IAAY,EAAE,QAAQ,KAAK,OAAO,IAAI,CAAC;IAC7D,CAAC;GACH;GACA,OAAO,MAAM,GAAG,MAAM,EAAE,QAAQ,QAAQ,IAAI,EAAE,QAAQ,QAAQ,CAAC;GAC/D,OAAO;EACT,CAAC,CAAC,CAAC,eAEkB;EAAG;CAAK;AAC/B"}
|
package/dist/model/index.cjs
CHANGED
|
@@ -4,8 +4,11 @@ const require_sharing_model = require("./sharing_model.cjs");
|
|
|
4
4
|
const require_template_parser = require("./template_parser.cjs");
|
|
5
5
|
const require_template_resolve = require("./template_resolve.cjs");
|
|
6
6
|
const require_template_apply = require("./template_apply.cjs");
|
|
7
|
+
const require_template_serializer = require("./template_serializer.cjs");
|
|
8
|
+
const require_template_share = require("./template_share.cjs");
|
|
7
9
|
exports.AcceptanceFieldPrefix = require_sharing_model.AcceptanceFieldPrefix;
|
|
8
10
|
exports.BlockArgsAuthorKeyPrefix = require_project_model.BlockArgsAuthorKeyPrefix;
|
|
11
|
+
exports.EnvelopeSchemaVersionCurrent = require_sharing_model.EnvelopeSchemaVersionCurrent;
|
|
9
12
|
exports.FrontendFromFolderResourceType = require_block_pack_spec.FrontendFromFolderResourceType;
|
|
10
13
|
exports.FrontendFromLocalTgzResourceType = require_block_pack_spec.FrontendFromLocalTgzResourceType;
|
|
11
14
|
exports.FrontendFromUrlResourceType = require_block_pack_spec.FrontendFromUrlResourceType;
|
|
@@ -30,8 +33,13 @@ exports.canGrantToEveryone = require_sharing_model.canGrantToEveryone;
|
|
|
30
33
|
exports.canImpersonate = require_sharing_model.canImpersonate;
|
|
31
34
|
exports.decisionField = require_sharing_model.decisionField;
|
|
32
35
|
exports.decodeEnvelopeData = require_sharing_model.decodeEnvelopeData;
|
|
36
|
+
exports.envelopeProjectMap = require_sharing_model.envelopeProjectMap;
|
|
33
37
|
exports.isAcceptanceField = require_sharing_model.isAcceptanceField;
|
|
38
|
+
exports.locationOf = require_template_serializer.locationOf;
|
|
34
39
|
exports.newShareId = require_sharing_model.newShareId;
|
|
40
|
+
exports.normalizeEnvelopeData = require_sharing_model.normalizeEnvelopeData;
|
|
35
41
|
exports.parseBlockPackName = require_template_resolve.parseBlockPackName;
|
|
36
42
|
exports.parseProjectTemplateV1Yaml = require_template_parser.parseProjectTemplateV1Yaml;
|
|
37
43
|
exports.resolveTemplateEntries = require_template_resolve.resolveTemplateEntries;
|
|
44
|
+
exports.stringifyProjectTemplateV1 = require_template_serializer.stringifyProjectTemplateV1;
|
|
45
|
+
exports.unshareableTemplateEntries = require_template_share.unshareableTemplateEntries;
|