@milaboratories/pl-middle-layer 1.71.16 → 1.72.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 -5
- package/dist/index.d.ts +5 -4
- package/dist/index.js +3 -2
- package/dist/middle_layer/build_stamp.cjs +1 -1
- package/dist/middle_layer/build_stamp.js +1 -1
- package/dist/middle_layer/folders.cjs +631 -0
- package/dist/middle_layer/folders.cjs.map +1 -0
- package/dist/middle_layer/folders.d.ts +39 -0
- package/dist/middle_layer/folders.d.ts.map +1 -0
- package/dist/middle_layer/folders.js +613 -0
- package/dist/middle_layer/folders.js.map +1 -0
- package/dist/middle_layer/index.cjs +3 -0
- package/dist/middle_layer/index.d.ts +4 -3
- package/dist/middle_layer/index.js +2 -1
- package/dist/middle_layer/middle_layer.cjs +585 -370
- package/dist/middle_layer/middle_layer.cjs.map +1 -1
- package/dist/middle_layer/middle_layer.d.ts +215 -122
- package/dist/middle_layer/middle_layer.d.ts.map +1 -1
- package/dist/middle_layer/middle_layer.js +593 -378
- package/dist/middle_layer/middle_layer.js.map +1 -1
- package/dist/middle_layer/project_list.cjs +36 -19
- package/dist/middle_layer/project_list.cjs.map +1 -1
- package/dist/middle_layer/project_list.d.ts +1 -1
- package/dist/middle_layer/project_list.d.ts.map +1 -1
- package/dist/middle_layer/project_list.js +37 -21
- package/dist/middle_layer/project_list.js.map +1 -1
- package/dist/middle_layer/sharing_list.cjs +123 -93
- package/dist/middle_layer/sharing_list.cjs.map +1 -1
- package/dist/middle_layer/sharing_list.d.ts +43 -19
- package/dist/middle_layer/sharing_list.d.ts.map +1 -1
- package/dist/middle_layer/sharing_list.js +123 -93
- package/dist/middle_layer/sharing_list.js.map +1 -1
- package/dist/middle_layer/template_list.cjs +45 -20
- package/dist/middle_layer/template_list.cjs.map +1 -1
- package/dist/middle_layer/template_list.d.ts +7 -15
- package/dist/middle_layer/template_list.d.ts.map +1 -1
- package/dist/middle_layer/template_list.js +44 -21
- package/dist/middle_layer/template_list.js.map +1 -1
- package/dist/model/index.cjs +7 -5
- package/dist/model/index.d.ts +2 -2
- package/dist/model/index.js +2 -2
- package/dist/model/sharing_model.cjs +46 -20
- package/dist/model/sharing_model.cjs.map +1 -1
- package/dist/model/sharing_model.d.ts +125 -55
- package/dist/model/sharing_model.d.ts.map +1 -1
- package/dist/model/sharing_model.js +40 -16
- package/dist/model/sharing_model.js.map +1 -1
- package/dist/mutator/list.cjs +30 -0
- package/dist/mutator/list.cjs.map +1 -0
- package/dist/mutator/list.js +29 -0
- package/dist/mutator/list.js.map +1 -0
- package/dist/mutator/project.cjs +14 -1
- package/dist/mutator/project.cjs.map +1 -1
- package/dist/mutator/project.d.ts.map +1 -1
- package/dist/mutator/project.js +15 -2
- package/dist/mutator/project.js.map +1 -1
- package/dist/mutator/sharing.cjs +130 -76
- package/dist/mutator/sharing.cjs.map +1 -1
- package/dist/mutator/sharing.js +130 -76
- package/dist/mutator/sharing.js.map +1 -1
- package/dist/mutator/template.cjs +24 -17
- package/dist/mutator/template.cjs.map +1 -1
- package/dist/mutator/template.js +26 -20
- package/dist/mutator/template.js.map +1 -1
- package/package.json +19 -19
- package/src/middle_layer/folders.test.ts +1068 -0
- package/src/middle_layer/folders.ts +1059 -0
- package/src/middle_layer/folders_read.test.ts +359 -0
- package/src/middle_layer/index.ts +3 -2
- package/src/middle_layer/middle_layer.ts +813 -547
- package/src/middle_layer/project_list.test.ts +183 -0
- package/src/middle_layer/project_list.ts +58 -21
- package/src/middle_layer/sharing.test.ts +183 -0
- package/src/middle_layer/sharing_list.ts +208 -152
- package/src/middle_layer/template_list.test.ts +148 -0
- package/src/middle_layer/template_list.ts +72 -32
- package/src/middle_layer/templates.test.ts +76 -45
- package/src/model/sharing_model.test.ts +69 -2
- package/src/model/sharing_model.ts +161 -73
- package/src/mutator/list.ts +36 -0
- package/src/mutator/project-v3.test.ts +3 -1
- package/src/mutator/project.ts +13 -2
- package/src/mutator/sharing.ts +193 -120
- package/src/mutator/template.ts +37 -29
- package/src/test/with_ml.ts +73 -16
|
@@ -1 +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 { 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. */\nexport type ShareTemplateOutcome = { readonly shareId: ShareId };\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":";;;;;AA0BA,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;AAwDA,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"}
|
|
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 type { ProjectTemplateV1, TemplateId } from \"@milaboratories/pl-model-common\";\nimport { asTemplateId } from \"@milaboratories/pl-model-common\";\nimport { normalizeDescription } from \"@milaboratories/pl-model-middle-layer\";\nimport type { TemplateExportProblem } from \"../model/template_export\";\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\";\n/** Mutable: free text about the template. Absent on a template nobody described. */\nexport const TemplateDescriptionKey = \"TemplateDescription\";\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 *\n * Declared in the model package because the folder document places templates as well as\n * projects, and that document is written by code that cannot see this file.\n */\nexport type { 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 /** Free text the user wrote about the template. Absent when there is none. */\n description?: 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/**\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 return templateListEntries(node);\n }).withStableType();\n\n return { computable: c, tree };\n}\n\n/** The part of a tree node this reader needs from a single entry of the templates list. */\ninterface TemplateListEntryNode {\n readonly id: SignedResourceId;\n readonly resourceType: ResourceType;\n getDataAsJson<T>(): T | undefined;\n getKeyValueAsJson<T>(key: string): T | undefined;\n}\n\n/** The part of a tree node this reader needs from the templates-list resource itself. */\ninterface TemplatesListNode {\n listDynamicFields(): string[];\n traverse(fieldName: string): TemplateListEntryNode | undefined;\n}\n\n/**\n * Every stored template of one root, most recently created first.\n *\n * A function rather than a closure inside {@link createTemplateList} because the folder listing\n * reads the same tree in its own computable: templates and projects share one folder tree, and\n * joining it against two independently published lists would let a template show up in a folder\n * the tree has not heard of.\n *\n * Skips, rather than throws on, anything that is not a fully synced template, by the same rule\n * the project list follows: the folder listing is built on this, so one template whose data or\n * label has not arrived must cost that entry and nothing else. The resource type is matched by\n * name only, to exclude foreign resources without hiding a template stored under an earlier\n * resource-type version.\n */\nexport function templateListEntries(node: TemplatesListNode): TemplateListEntry[] {\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 if (tpl.resourceType.name !== TemplateResourceType.name) continue;\n\n const data = tpl.getDataAsJson<StoredTemplateData>();\n const label = tpl.getKeyValueAsJson<string>(TemplateLabelKey);\n const created = tpl.getKeyValueAsJson<number>(TemplateCreatedTimestamp);\n if (data === undefined || label === undefined || created === undefined) continue;\n\n // Written only once something is said about the template, so its absence is the ordinary case.\n const description = normalizeDescription(tpl.getKeyValueAsJson<string>(TemplateDescriptionKey));\n result.push({\n id: asTemplateId(resourceIdToString(tpl.id)),\n label,\n ...(description === undefined ? {} : { description }),\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}\n"],"mappings":";;;;;;AA0BA,MAAa,iBAAiB;AAC9B,MAAa,wBAAsC;CAAE,MAAM;CAAa,SAAS;AAAI;AACrF,MAAa,uBAAqC;CAAE,MAAM;CAAgB,SAAS;AAAI;;AAGvF,MAAa,mBAAmB;;AAEhC,MAAa,yBAAyB;AACtC,MAAa,2BAA2B;;;AA8BxC,SAAgB,yBAAyB,MAAsC;CAC7E,OAAO,KAAK,MAAM,OAAO,KAAK,IAAI,CAAC,CAAC,SAAS,OAAO,CAAC;AACvD;AAuDA,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;CAQA,OAAO;EAAE,YANCC,2BAAAA,WAAW,MAAM,QAAQ;GACjC,MAAM,OAAO,IAAI,SAAS,KAAK,MAAM,CAAC,CAAC,CAAC,KAAK;GAC7C,IAAI,SAAS,KAAA,GAAW,OAAO,KAAA;GAC/B,OAAO,oBAAoB,IAAI;EACjC,CAAC,CAAC,CAAC,eAEkB;EAAG;CAAK;AAC/B;;;;;;;;;;;;;;;AA8BA,SAAgB,oBAAoB,MAA8C;CAChF,MAAM,SAA8B,CAAC;CAGrC,KAAK,MAAM,SAAS,KAAK,kBAAkB,GAAG;EAC5C,MAAM,MAAM,KAAK,SAAS,KAAK;EAC/B,IAAI,QAAQ,KAAA,GAAW;EACvB,IAAI,IAAI,aAAa,SAAS,qBAAqB,MAAM;EAEzD,MAAM,OAAO,IAAI,cAAkC;EACnD,MAAM,QAAQ,IAAI,kBAA0B,gBAAgB;EAC5D,MAAM,UAAU,IAAI,kBAA0B,wBAAwB;EACtE,IAAI,SAAS,KAAA,KAAa,UAAU,KAAA,KAAa,YAAY,KAAA,GAAW;EAGxE,MAAM,eAAA,GAAA,sCAAA,qBAAA,CAAmC,IAAI,kBAA0B,sBAAsB,CAAC;EAC9F,OAAO,KAAK;GACV,KAAA,GAAA,gCAAA,aAAA,EAAA,GAAA,0BAAA,mBAAA,CAAoC,IAAI,EAAE,CAAC;GAC3C;GACA,GAAI,gBAAgB,KAAA,IAAY,CAAC,IAAI,EAAE,YAAY;GACnD,SAAS,IAAI,KAAK,OAAO;GACzB,YAAY,KAAK,SAAS,OAAO;GACjC,GAAI,KAAK,uBAAuB,KAAA,IAC5B,EAAE,oBAAoB,KAAK,mBAAmB,IAC9C,CAAC;GACL,GAAI,KAAK,WAAW,KAAA,IAAY,EAAE,QAAQ,KAAK,OAAO,IAAI,CAAC;EAC7D,CAAC;CACH;CACA,OAAO,MAAM,GAAG,MAAM,EAAE,QAAQ,QAAQ,IAAI,EAAE,QAAQ,QAAQ,CAAC;CAC/D,OAAO;AACT"}
|
|
@@ -1,20 +1,13 @@
|
|
|
1
1
|
import { AppliedEntry, TemplateApplyProblem } from "../model/template_apply.js";
|
|
2
|
-
import "./types.js";
|
|
3
2
|
import { ProjectId as ProjectId$1 } from "../model/project_model.js";
|
|
3
|
+
import "./types.js";
|
|
4
4
|
import { TemplateExportProblem } from "../model/template_export.js";
|
|
5
|
-
import { ShareId } from "../model/sharing_model.js";
|
|
6
5
|
import "./middle_layer.js";
|
|
7
|
-
import {
|
|
6
|
+
import { ProjectTemplateV1, TemplateId as TemplateId$1 } from "@milaboratories/pl-model-common";
|
|
8
7
|
import { PlClient, PlTransaction, ResourceType, SignedResourceId } from "@milaboratories/pl-client";
|
|
9
8
|
import "@milaboratories/pl-tree";
|
|
10
9
|
//#region src/middle_layer/template_list.d.ts
|
|
11
10
|
export declare const TemplatesField = "templates";
|
|
12
|
-
/**
|
|
13
|
-
* Unique template identifier in middle layer, the stringified signed resource id of the
|
|
14
|
-
* `UserTemplate`. Branded so it cannot be confused with a {@link ProjectId} — both are
|
|
15
|
-
* stringified resource ids and every template method takes one of them.
|
|
16
|
-
*/
|
|
17
|
-
export type TemplateId = Branded<string, "TemplateId">;
|
|
18
11
|
/**
|
|
19
12
|
* Immutable `data` on a UserTemplate: the document plus what was true when it was taken.
|
|
20
13
|
*
|
|
@@ -33,9 +26,11 @@ export interface StoredTemplateData {
|
|
|
33
26
|
/** One template as the template list surfaces it. */
|
|
34
27
|
export interface TemplateListEntry {
|
|
35
28
|
/** Unique template identifier in middle layer. Use to operate with the given template. */
|
|
36
|
-
id: TemplateId;
|
|
29
|
+
id: TemplateId$1;
|
|
37
30
|
/** The mutable label, the only part a rename changes. */
|
|
38
31
|
label: string;
|
|
32
|
+
/** Free text the user wrote about the template. Absent when there is none. */
|
|
33
|
+
description?: string;
|
|
39
34
|
created: Date;
|
|
40
35
|
/** Number of blocks the stored document lists — derived, not stored. */
|
|
41
36
|
blockCount: number;
|
|
@@ -45,15 +40,11 @@ export interface TemplateListEntry {
|
|
|
45
40
|
/** What saving a project as a template yields: the stored template, or every block in the way. */
|
|
46
41
|
export type SaveProjectAsTemplateOutcome = {
|
|
47
42
|
readonly ok: true;
|
|
48
|
-
readonly templateId: TemplateId;
|
|
43
|
+
readonly templateId: TemplateId$1;
|
|
49
44
|
} | {
|
|
50
45
|
readonly ok: false;
|
|
51
46
|
readonly problems: readonly TemplateExportProblem[];
|
|
52
47
|
};
|
|
53
|
-
/** What sharing a stored template yields: the share's logical id. */
|
|
54
|
-
export type ShareTemplateOutcome = {
|
|
55
|
-
readonly shareId: ShareId;
|
|
56
|
-
};
|
|
57
48
|
/**
|
|
58
49
|
* What applying a stored template yields.
|
|
59
50
|
*
|
|
@@ -69,4 +60,5 @@ export type CreateProjectFromTemplateOutcome = {
|
|
|
69
60
|
readonly problems: readonly TemplateApplyProblem[];
|
|
70
61
|
};
|
|
71
62
|
//#endregion
|
|
63
|
+
export type { TemplateId$1 as TemplateId };
|
|
72
64
|
//# sourceMappingURL=template_list.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"template_list.d.ts","names":[],"sources":["../../src/middle_layer/template_list.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"template_list.d.ts","names":[],"sources":["../../src/middle_layer/template_list.ts"],"mappings":";;;;;;;;;qBA0Ba;;;;;;;;iBA2BI;EACf;EACA,UAAU;;EAEV;;EAEA;;;iBAUe;;EAEf,IAAI;;EAEJ;;EAEA;EACA,SAAS;;EAET;EACA;EACA;;;YAIU;WACG;WAAmB,YAAY;;WAC/B;WAAoB,mBAAmB;;;;;;;;YAQ1C;WAEG;WACA,WAAW;WACX,gBAAgB;;WAEhB;WAAoB,mBAAmB"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { normalizeDescription } from "@milaboratories/pl-model-middle-layer";
|
|
2
|
+
import { asTemplateId } from "@milaboratories/pl-model-common";
|
|
2
3
|
import { resourceIdToString, resourceTypesEqual, treeFilter } from "@milaboratories/pl-client";
|
|
3
4
|
import { SynchronizedTreeState } from "@milaboratories/pl-tree";
|
|
4
5
|
import { Computable } from "@milaboratories/computable";
|
|
@@ -14,6 +15,8 @@ const TemplateResourceType = {
|
|
|
14
15
|
};
|
|
15
16
|
/** Mutable: the only part of a stored template a rename may touch. */
|
|
16
17
|
const TemplateLabelKey = "TemplateLabel";
|
|
18
|
+
/** Mutable: free text about the template. Absent on a template nobody described. */
|
|
19
|
+
const TemplateDescriptionKey = "TemplateDescription";
|
|
17
20
|
const TemplateCreatedTimestamp = "TemplateCreated";
|
|
18
21
|
/** Decodes the immutable `data` blob of a `UserTemplate` read through a transaction. The
|
|
19
22
|
* single raw-decode site; the tree side reads the same JSON with `getDataAsJson`. */
|
|
@@ -35,30 +38,50 @@ async function createTemplateList(pl, rid, env) {
|
|
|
35
38
|
computable: Computable.make((ctx) => {
|
|
36
39
|
const node = ctx.accessor(tree.entry()).node();
|
|
37
40
|
if (node === void 0) return void 0;
|
|
38
|
-
|
|
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;
|
|
41
|
+
return templateListEntries(node);
|
|
57
42
|
}).withStableType(),
|
|
58
43
|
tree
|
|
59
44
|
};
|
|
60
45
|
}
|
|
46
|
+
/**
|
|
47
|
+
* Every stored template of one root, most recently created first.
|
|
48
|
+
*
|
|
49
|
+
* A function rather than a closure inside {@link createTemplateList} because the folder listing
|
|
50
|
+
* reads the same tree in its own computable: templates and projects share one folder tree, and
|
|
51
|
+
* joining it against two independently published lists would let a template show up in a folder
|
|
52
|
+
* the tree has not heard of.
|
|
53
|
+
*
|
|
54
|
+
* Skips, rather than throws on, anything that is not a fully synced template, by the same rule
|
|
55
|
+
* the project list follows: the folder listing is built on this, so one template whose data or
|
|
56
|
+
* label has not arrived must cost that entry and nothing else. The resource type is matched by
|
|
57
|
+
* name only, to exclude foreign resources without hiding a template stored under an earlier
|
|
58
|
+
* resource-type version.
|
|
59
|
+
*/
|
|
60
|
+
function templateListEntries(node) {
|
|
61
|
+
const result = [];
|
|
62
|
+
for (const field of node.listDynamicFields()) {
|
|
63
|
+
const tpl = node.traverse(field);
|
|
64
|
+
if (tpl === void 0) continue;
|
|
65
|
+
if (tpl.resourceType.name !== TemplateResourceType.name) continue;
|
|
66
|
+
const data = tpl.getDataAsJson();
|
|
67
|
+
const label = tpl.getKeyValueAsJson(TemplateLabelKey);
|
|
68
|
+
const created = tpl.getKeyValueAsJson(TemplateCreatedTimestamp);
|
|
69
|
+
if (data === void 0 || label === void 0 || created === void 0) continue;
|
|
70
|
+
const description = normalizeDescription(tpl.getKeyValueAsJson(TemplateDescriptionKey));
|
|
71
|
+
result.push({
|
|
72
|
+
id: asTemplateId(resourceIdToString(tpl.id)),
|
|
73
|
+
label,
|
|
74
|
+
...description === void 0 ? {} : { description },
|
|
75
|
+
created: new Date(created),
|
|
76
|
+
blockCount: data.document.blocks.length,
|
|
77
|
+
...data.sourceProjectLabel !== void 0 ? { sourceProjectLabel: data.sourceProjectLabel } : {},
|
|
78
|
+
...data.sender !== void 0 ? { sender: data.sender } : {}
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
result.sort((a, b) => b.created.valueOf() - a.created.valueOf());
|
|
82
|
+
return result;
|
|
83
|
+
}
|
|
61
84
|
//#endregion
|
|
62
|
-
export { TemplateCreatedTimestamp, TemplateLabelKey, TemplateResourceType, TemplatesField, TemplatesListTreePruningFunction, TemplatesResourceType, createTemplateList, decodeStoredTemplateData, templatesListFieldFilter };
|
|
85
|
+
export { TemplateCreatedTimestamp, TemplateDescriptionKey, TemplateLabelKey, TemplateResourceType, TemplatesField, TemplatesListTreePruningFunction, TemplatesResourceType, createTemplateList, decodeStoredTemplateData, templateListEntries, templatesListFieldFilter };
|
|
63
86
|
|
|
64
87
|
//# sourceMappingURL=template_list.js.map
|
|
@@ -1 +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 { 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. */\nexport type ShareTemplateOutcome = { readonly shareId: ShareId };\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":";;;;;AA0BA,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;AAwDA,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"}
|
|
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 type { ProjectTemplateV1, TemplateId } from \"@milaboratories/pl-model-common\";\nimport { asTemplateId } from \"@milaboratories/pl-model-common\";\nimport { normalizeDescription } from \"@milaboratories/pl-model-middle-layer\";\nimport type { TemplateExportProblem } from \"../model/template_export\";\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\";\n/** Mutable: free text about the template. Absent on a template nobody described. */\nexport const TemplateDescriptionKey = \"TemplateDescription\";\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 *\n * Declared in the model package because the folder document places templates as well as\n * projects, and that document is written by code that cannot see this file.\n */\nexport type { 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 /** Free text the user wrote about the template. Absent when there is none. */\n description?: 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/**\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 return templateListEntries(node);\n }).withStableType();\n\n return { computable: c, tree };\n}\n\n/** The part of a tree node this reader needs from a single entry of the templates list. */\ninterface TemplateListEntryNode {\n readonly id: SignedResourceId;\n readonly resourceType: ResourceType;\n getDataAsJson<T>(): T | undefined;\n getKeyValueAsJson<T>(key: string): T | undefined;\n}\n\n/** The part of a tree node this reader needs from the templates-list resource itself. */\ninterface TemplatesListNode {\n listDynamicFields(): string[];\n traverse(fieldName: string): TemplateListEntryNode | undefined;\n}\n\n/**\n * Every stored template of one root, most recently created first.\n *\n * A function rather than a closure inside {@link createTemplateList} because the folder listing\n * reads the same tree in its own computable: templates and projects share one folder tree, and\n * joining it against two independently published lists would let a template show up in a folder\n * the tree has not heard of.\n *\n * Skips, rather than throws on, anything that is not a fully synced template, by the same rule\n * the project list follows: the folder listing is built on this, so one template whose data or\n * label has not arrived must cost that entry and nothing else. The resource type is matched by\n * name only, to exclude foreign resources without hiding a template stored under an earlier\n * resource-type version.\n */\nexport function templateListEntries(node: TemplatesListNode): TemplateListEntry[] {\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 if (tpl.resourceType.name !== TemplateResourceType.name) continue;\n\n const data = tpl.getDataAsJson<StoredTemplateData>();\n const label = tpl.getKeyValueAsJson<string>(TemplateLabelKey);\n const created = tpl.getKeyValueAsJson<number>(TemplateCreatedTimestamp);\n if (data === undefined || label === undefined || created === undefined) continue;\n\n // Written only once something is said about the template, so its absence is the ordinary case.\n const description = normalizeDescription(tpl.getKeyValueAsJson<string>(TemplateDescriptionKey));\n result.push({\n id: asTemplateId(resourceIdToString(tpl.id)),\n label,\n ...(description === undefined ? {} : { description }),\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}\n"],"mappings":";;;;;;AA0BA,MAAa,iBAAiB;AAC9B,MAAa,wBAAsC;CAAE,MAAM;CAAa,SAAS;AAAI;AACrF,MAAa,uBAAqC;CAAE,MAAM;CAAgB,SAAS;AAAI;;AAGvF,MAAa,mBAAmB;;AAEhC,MAAa,yBAAyB;AACtC,MAAa,2BAA2B;;;AA8BxC,SAAgB,yBAAyB,MAAsC;CAC7E,OAAO,KAAK,MAAM,OAAO,KAAK,IAAI,CAAC,CAAC,SAAS,OAAO,CAAC;AACvD;AAuDA,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;CAQA,OAAO;EAAE,YANC,WAAW,MAAM,QAAQ;GACjC,MAAM,OAAO,IAAI,SAAS,KAAK,MAAM,CAAC,CAAC,CAAC,KAAK;GAC7C,IAAI,SAAS,KAAA,GAAW,OAAO,KAAA;GAC/B,OAAO,oBAAoB,IAAI;EACjC,CAAC,CAAC,CAAC,eAEkB;EAAG;CAAK;AAC/B;;;;;;;;;;;;;;;AA8BA,SAAgB,oBAAoB,MAA8C;CAChF,MAAM,SAA8B,CAAC;CAGrC,KAAK,MAAM,SAAS,KAAK,kBAAkB,GAAG;EAC5C,MAAM,MAAM,KAAK,SAAS,KAAK;EAC/B,IAAI,QAAQ,KAAA,GAAW;EACvB,IAAI,IAAI,aAAa,SAAS,qBAAqB,MAAM;EAEzD,MAAM,OAAO,IAAI,cAAkC;EACnD,MAAM,QAAQ,IAAI,kBAA0B,gBAAgB;EAC5D,MAAM,UAAU,IAAI,kBAA0B,wBAAwB;EACtE,IAAI,SAAS,KAAA,KAAa,UAAU,KAAA,KAAa,YAAY,KAAA,GAAW;EAGxE,MAAM,cAAc,qBAAqB,IAAI,kBAA0B,sBAAsB,CAAC;EAC9F,OAAO,KAAK;GACV,IAAI,aAAa,mBAAmB,IAAI,EAAE,CAAC;GAC3C;GACA,GAAI,gBAAgB,KAAA,IAAY,CAAC,IAAI,EAAE,YAAY;GACnD,SAAS,IAAI,KAAK,OAAO;GACzB,YAAY,KAAK,SAAS,OAAO;GACjC,GAAI,KAAK,uBAAuB,KAAA,IAC5B,EAAE,oBAAoB,KAAK,mBAAmB,IAC9C,CAAC;GACL,GAAI,KAAK,WAAW,KAAA,IAAY,EAAE,QAAQ,KAAK,OAAO,IAAI,CAAC;EAC7D,CAAC;CACH;CACA,OAAO,MAAM,GAAG,MAAM,EAAE,QAAQ,QAAQ,IAAI,EAAE,QAAQ,QAAQ,CAAC;CAC/D,OAAO;AACT"}
|
package/dist/model/index.cjs
CHANGED
|
@@ -5,12 +5,12 @@ 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
7
|
const require_template_serializer = require("./template_serializer.cjs");
|
|
8
|
-
exports.AcceptanceFieldPrefix = require_sharing_model.AcceptanceFieldPrefix;
|
|
9
8
|
exports.BlockArgsAuthorKeyPrefix = require_project_model.BlockArgsAuthorKeyPrefix;
|
|
10
9
|
exports.EnvelopeSchemaVersionCurrent = require_sharing_model.EnvelopeSchemaVersionCurrent;
|
|
11
10
|
exports.FrontendFromFolderResourceType = require_block_pack_spec.FrontendFromFolderResourceType;
|
|
12
11
|
exports.FrontendFromLocalTgzResourceType = require_block_pack_spec.FrontendFromLocalTgzResourceType;
|
|
13
12
|
exports.FrontendFromUrlResourceType = require_block_pack_spec.FrontendFromUrlResourceType;
|
|
13
|
+
exports.HiddenFieldPrefix = require_sharing_model.HiddenFieldPrefix;
|
|
14
14
|
exports.ProjectCreatedTimestamp = require_project_model.ProjectCreatedTimestamp;
|
|
15
15
|
exports.ProjectLastModifiedTimestamp = require_project_model.ProjectLastModifiedTimestamp;
|
|
16
16
|
exports.ProjectMetaKey = require_project_model.ProjectMetaKey;
|
|
@@ -25,16 +25,18 @@ exports.SharingOutboxResourceType = require_sharing_model.SharingOutboxResourceT
|
|
|
25
25
|
exports.SharingStateField = require_sharing_model.SharingStateField;
|
|
26
26
|
exports.SharingStateResourceType = require_sharing_model.SharingStateResourceType;
|
|
27
27
|
exports.TemplateEntryRejected = require_template_apply.TemplateEntryRejected;
|
|
28
|
-
exports.acceptanceField = require_sharing_model.acceptanceField;
|
|
29
|
-
exports.acceptanceFieldLogin = require_sharing_model.acceptanceFieldLogin;
|
|
30
28
|
exports.asShareId = require_sharing_model.asShareId;
|
|
31
29
|
exports.canGrantToEveryone = require_sharing_model.canGrantToEveryone;
|
|
32
30
|
exports.canImpersonate = require_sharing_model.canImpersonate;
|
|
33
|
-
exports.decisionField = require_sharing_model.decisionField;
|
|
34
31
|
exports.decodeEnvelopeData = require_sharing_model.decodeEnvelopeData;
|
|
32
|
+
exports.envelopeFolderRoot = require_sharing_model.envelopeFolderRoot;
|
|
35
33
|
exports.envelopeProjectMap = require_sharing_model.envelopeProjectMap;
|
|
36
|
-
exports.
|
|
34
|
+
exports.hiddenField = require_sharing_model.hiddenField;
|
|
35
|
+
exports.hiddenFieldShareId = require_sharing_model.hiddenFieldShareId;
|
|
36
|
+
exports.isHiddenField = require_sharing_model.isHiddenField;
|
|
37
37
|
exports.locationOf = require_template_serializer.locationOf;
|
|
38
|
+
exports.newEnvelopeFolderId = require_sharing_model.newEnvelopeFolderId;
|
|
39
|
+
exports.newProjectFieldUuid = require_sharing_model.newProjectFieldUuid;
|
|
38
40
|
exports.newShareId = require_sharing_model.newShareId;
|
|
39
41
|
exports.normalizeEnvelopeData = require_sharing_model.normalizeEnvelopeData;
|
|
40
42
|
exports.parseBlockPackName = require_template_resolve.parseBlockPackName;
|
package/dist/model/index.d.ts
CHANGED
|
@@ -2,8 +2,8 @@ import { AppliedEntry, TemplateApplyProblem, TemplateApplyReport, TemplateEntryR
|
|
|
2
2
|
import { BlockPackProvider, ExactResolution, KindResolution, ResolvedEntry, TemplateResolveOutcome, parseBlockPackName, resolveTemplateEntries } from "./template_resolve.js";
|
|
3
3
|
import { BlockArgsAuthorKeyPrefix, ProjectCreatedTimestamp, ProjectField, ProjectId, ProjectLastModifiedTimestamp, ProjectListEntry, ProjectMetaKey, ProjectResourceType, ProjectStructureAuthorKey, ProjectStructureKey, SchemaVersionCurrent, SchemaVersionKey } from "./project_model.js";
|
|
4
4
|
import { TemplateExportProblem } from "./template_export.js";
|
|
5
|
-
import { AcceptanceFieldPrefix, EnvelopeAcceptance, EnvelopeData, EnvelopeMode, EnvelopePayload, EnvelopePayloadKind, EnvelopeProject, EnvelopeSchemaVersion, EnvelopeSchemaVersionCurrent, ProjectChangeAction, ProjectFieldUuid, ShareId, ShareProjectsOptions, ShareTemplateOptions, SharedEnvelopeResourceType, SharingDecision, SharingOutboxField, SharingOutboxResourceType, SharingStateField, SharingStateResourceType, acceptanceField, acceptanceFieldLogin, asShareId, canGrantToEveryone, canImpersonate, decisionField, decodeEnvelopeData, envelopeProjectMap, isAcceptanceField, newShareId, normalizeEnvelopeData } from "./sharing_model.js";
|
|
6
5
|
import { ProjectTemplateExportOutcome, locationOf, stringifyProjectTemplateV1 } from "./template_serializer.js";
|
|
6
|
+
import { EnvelopeData, EnvelopeFolder, EnvelopeFolderId, EnvelopeFolderProject, EnvelopeFolderTemplate, EnvelopeMode, EnvelopePayload, EnvelopePayloadKind, EnvelopeProject, EnvelopeSchemaVersion, EnvelopeSchemaVersionCurrent, HiddenFieldPrefix, ProjectFieldUuid, ShareAudience, ShareFolderOptions, ShareHidden, ShareId, ShareOptions, ShareOutcome, ShareProjectsOptions, ShareReplaceOption, ShareTemplateOptions, SharedEnvelopeResourceType, SharingOutboxField, SharingOutboxResourceType, SharingStateField, SharingStateResourceType, asShareId, canGrantToEveryone, canImpersonate, decodeEnvelopeData, envelopeFolderRoot, envelopeProjectMap, hiddenField, hiddenFieldShareId, isHiddenField, newEnvelopeFolderId, newProjectFieldUuid, newShareId, normalizeEnvelopeData } from "./sharing_model.js";
|
|
7
7
|
import { BlockPackExplicit, BlockPackSpecAny, BlockPackSpecPrepared, FrontendFromFolder, FrontendFromFolderData, FrontendFromFolderResourceType, FrontendFromLocalTgz, FrontendFromLocalTgzData, FrontendFromLocalTgzResourceType, FrontendFromUrl, FrontendFromUrlData, FrontendFromUrlResourceType, FrontendSpec } from "./block_pack_spec.js";
|
|
8
8
|
import { TemplateParseOutcome, parseProjectTemplateV1Yaml } from "./template_parser.js";
|
|
9
|
-
export {
|
|
9
|
+
export { type AppliedEntry, BlockArgsAuthorKeyPrefix, BlockPackExplicit, type BlockPackProvider, BlockPackSpecAny, BlockPackSpecPrepared, EnvelopeData, EnvelopeFolder, EnvelopeFolderId, EnvelopeFolderProject, EnvelopeFolderTemplate, EnvelopeMode, EnvelopePayload, EnvelopePayloadKind, EnvelopeProject, EnvelopeSchemaVersion, EnvelopeSchemaVersionCurrent, type ExactResolution, FrontendFromFolder, FrontendFromFolderData, FrontendFromFolderResourceType, FrontendFromLocalTgz, FrontendFromLocalTgzData, FrontendFromLocalTgzResourceType, FrontendFromUrl, FrontendFromUrlData, FrontendFromUrlResourceType, FrontendSpec, HiddenFieldPrefix, type KindResolution, ProjectCreatedTimestamp, type ProjectField, ProjectFieldUuid, ProjectLastModifiedTimestamp, type ProjectListEntry, ProjectMetaKey, ProjectResourceType, ProjectStructureAuthorKey, ProjectStructureKey, type ProjectTemplateExportOutcome, type ResolvedEntry, SchemaVersionCurrent, SchemaVersionKey, ShareAudience, ShareFolderOptions, ShareHidden, ShareId, ShareOptions, ShareOutcome, ShareProjectsOptions, ShareReplaceOption, ShareTemplateOptions, SharedEnvelopeResourceType, SharingOutboxField, SharingOutboxResourceType, SharingStateField, SharingStateResourceType, type TemplateApplyProblem, type TemplateApplyReport, TemplateEntryRejected, type TemplateExportProblem, type TemplateParseOutcome, type TemplateResolveOutcome, asShareId, canGrantToEveryone, canImpersonate, decodeEnvelopeData, envelopeFolderRoot, envelopeProjectMap, hiddenField, hiddenFieldShareId, isHiddenField, locationOf, newEnvelopeFolderId, newProjectFieldUuid, newShareId, normalizeEnvelopeData, parseBlockPackName, parseProjectTemplateV1Yaml, resolveTemplateEntries, stringifyProjectTemplateV1 };
|
package/dist/model/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { BlockArgsAuthorKeyPrefix, ProjectCreatedTimestamp, ProjectLastModifiedTimestamp, ProjectMetaKey, ProjectResourceType, ProjectStructureAuthorKey, ProjectStructureKey, SchemaVersionCurrent, SchemaVersionKey } from "./project_model.js";
|
|
2
2
|
import { FrontendFromFolderResourceType, FrontendFromLocalTgzResourceType, FrontendFromUrlResourceType } from "./block_pack_spec.js";
|
|
3
|
-
import {
|
|
3
|
+
import { EnvelopeSchemaVersionCurrent, HiddenFieldPrefix, SharedEnvelopeResourceType, SharingOutboxField, SharingOutboxResourceType, SharingStateField, SharingStateResourceType, asShareId, canGrantToEveryone, canImpersonate, decodeEnvelopeData, envelopeFolderRoot, envelopeProjectMap, hiddenField, hiddenFieldShareId, isHiddenField, newEnvelopeFolderId, newProjectFieldUuid, newShareId, normalizeEnvelopeData } from "./sharing_model.js";
|
|
4
4
|
import { parseProjectTemplateV1Yaml } from "./template_parser.js";
|
|
5
5
|
import { parseBlockPackName, resolveTemplateEntries } from "./template_resolve.js";
|
|
6
6
|
import { TemplateEntryRejected } from "./template_apply.js";
|
|
7
7
|
import { locationOf, stringifyProjectTemplateV1 } from "./template_serializer.js";
|
|
8
|
-
export {
|
|
8
|
+
export { BlockArgsAuthorKeyPrefix, EnvelopeSchemaVersionCurrent, FrontendFromFolderResourceType, FrontendFromLocalTgzResourceType, FrontendFromUrlResourceType, HiddenFieldPrefix, ProjectCreatedTimestamp, ProjectLastModifiedTimestamp, ProjectMetaKey, ProjectResourceType, ProjectStructureAuthorKey, ProjectStructureKey, SchemaVersionCurrent, SchemaVersionKey, SharedEnvelopeResourceType, SharingOutboxField, SharingOutboxResourceType, SharingStateField, SharingStateResourceType, TemplateEntryRejected, asShareId, canGrantToEveryone, canImpersonate, decodeEnvelopeData, envelopeFolderRoot, envelopeProjectMap, hiddenField, hiddenFieldShareId, isHiddenField, locationOf, newEnvelopeFolderId, newProjectFieldUuid, newShareId, normalizeEnvelopeData, parseBlockPackName, parseProjectTemplateV1Yaml, resolveTemplateEntries, stringifyProjectTemplateV1 };
|
|
@@ -12,7 +12,7 @@ function asShareId(id) {
|
|
|
12
12
|
}
|
|
13
13
|
/** Field on the donor's clientRoot holding the {@link SharingOutboxResourceType} resource. */
|
|
14
14
|
const SharingOutboxField = "sharingOutbox";
|
|
15
|
-
/** Field on the
|
|
15
|
+
/** Field on the recipient's clientRoot holding the {@link SharingStateResourceType} resource. */
|
|
16
16
|
const SharingStateField = "sharingState";
|
|
17
17
|
const SharingOutboxResourceType = {
|
|
18
18
|
name: "SharingOutbox",
|
|
@@ -26,6 +26,10 @@ const SharingStateResourceType = {
|
|
|
26
26
|
name: "SharingState",
|
|
27
27
|
version: "1"
|
|
28
28
|
};
|
|
29
|
+
/** Mints a fresh {@link ProjectFieldUuid} for one snapshot. */
|
|
30
|
+
function newProjectFieldUuid() {
|
|
31
|
+
return (0, node_crypto.randomUUID)();
|
|
32
|
+
}
|
|
29
33
|
/**
|
|
30
34
|
* Whether a role may make a resource public (grant to everyone): true for controller,
|
|
31
35
|
* admin; false for workflow and unspecified. The middle layer carries no policy
|
|
@@ -55,6 +59,10 @@ function canImpersonate(role) {
|
|
|
55
59
|
default: return false;
|
|
56
60
|
}
|
|
57
61
|
}
|
|
62
|
+
/** Mints a fresh {@link EnvelopeFolderId} for one folder of the envelope being built. */
|
|
63
|
+
function newEnvelopeFolderId() {
|
|
64
|
+
return (0, node_crypto.randomUUID)();
|
|
65
|
+
}
|
|
58
66
|
/** Version written into every new envelope. Bumped from 1 when the payload became discriminated. */
|
|
59
67
|
const EnvelopeSchemaVersionCurrent = 2;
|
|
60
68
|
/** The project map of a projects-payload envelope, or `{}` for any other payload — the one
|
|
@@ -62,19 +70,34 @@ const EnvelopeSchemaVersionCurrent = 2;
|
|
|
62
70
|
function envelopeProjectMap(data) {
|
|
63
71
|
return data.payload.kind === "projects" ? data.payload.projects : {};
|
|
64
72
|
}
|
|
65
|
-
/**
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
73
|
+
/**
|
|
74
|
+
* The shared folder itself: the one folder of the subtree that has no parent.
|
|
75
|
+
*
|
|
76
|
+
* Derived rather than stored, so it cannot disagree with the folders beside it. `undefined` for
|
|
77
|
+
* a subtree with no root or more than one, which is an envelope nothing can be reconstructed
|
|
78
|
+
* from — the copy reports it rather than guessing which folder was meant.
|
|
79
|
+
*/
|
|
80
|
+
function envelopeFolderRoot(folders) {
|
|
81
|
+
const roots = Object.keys(folders).filter((id) => folders[id].parent === void 0);
|
|
82
|
+
return roots.length === 1 ? roots[0] : void 0;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Dynamic field on SharingState, one per share this user has hidden, keyed by shareId.
|
|
86
|
+
*
|
|
87
|
+
* Hiding is private to the recipient and reversible: the field is written to put a share out of
|
|
88
|
+
* sight and removed to bring it back. It says nothing to the donor and nothing about whether
|
|
89
|
+
* anything was ever copied out of the share — a share can be copied from any number of times,
|
|
90
|
+
* before or after being hidden.
|
|
91
|
+
*
|
|
92
|
+
* The field name keeps its original `decision/` prefix. Records written before hiding replaced
|
|
93
|
+
* accept/reject carry a different value under the same key, and every reader treats the presence
|
|
94
|
+
* of the field as the whole answer: someone who accepted or rejected a share back then does not
|
|
95
|
+
* want to see it, which is exactly what hidden means.
|
|
96
|
+
*/
|
|
97
|
+
const HiddenFieldPrefix = "decision/";
|
|
98
|
+
const hiddenField = (shareId) => `${HiddenFieldPrefix}${shareId}`;
|
|
99
|
+
const isHiddenField = (name) => name.startsWith(HiddenFieldPrefix);
|
|
100
|
+
const hiddenFieldShareId = (name) => asShareId(name.slice(9));
|
|
78
101
|
/**
|
|
79
102
|
* Single owner of the raw-data → {@link EnvelopeData} decode. The envelope's immutable `data`
|
|
80
103
|
* blob is UTF-8 JSON set once at createEphemeral; every site that reads it from a raw resource
|
|
@@ -122,7 +145,8 @@ function normalizeEnvelopeData(raw) {
|
|
|
122
145
|
* teaching the decoder about it is a compile error, not a share that silently disappears. */
|
|
123
146
|
const KnownPayloadKinds = {
|
|
124
147
|
projects: true,
|
|
125
|
-
template: true
|
|
148
|
+
template: true,
|
|
149
|
+
folder: true
|
|
126
150
|
};
|
|
127
151
|
/** Every schema version {@link normalizeEnvelopeData} accepts. Keyed by
|
|
128
152
|
* {@link EnvelopeSchemaVersion}, so widening that union without deciding how the new shape
|
|
@@ -132,22 +156,24 @@ const ReadableSchemaVersions = {
|
|
|
132
156
|
2: true
|
|
133
157
|
};
|
|
134
158
|
//#endregion
|
|
135
|
-
exports.AcceptanceFieldPrefix = AcceptanceFieldPrefix;
|
|
136
159
|
exports.EnvelopeSchemaVersionCurrent = EnvelopeSchemaVersionCurrent;
|
|
160
|
+
exports.HiddenFieldPrefix = HiddenFieldPrefix;
|
|
137
161
|
exports.SharedEnvelopeResourceType = SharedEnvelopeResourceType;
|
|
138
162
|
exports.SharingOutboxField = SharingOutboxField;
|
|
139
163
|
exports.SharingOutboxResourceType = SharingOutboxResourceType;
|
|
140
164
|
exports.SharingStateField = SharingStateField;
|
|
141
165
|
exports.SharingStateResourceType = SharingStateResourceType;
|
|
142
|
-
exports.acceptanceField = acceptanceField;
|
|
143
|
-
exports.acceptanceFieldLogin = acceptanceFieldLogin;
|
|
144
166
|
exports.asShareId = asShareId;
|
|
145
167
|
exports.canGrantToEveryone = canGrantToEveryone;
|
|
146
168
|
exports.canImpersonate = canImpersonate;
|
|
147
|
-
exports.decisionField = decisionField;
|
|
148
169
|
exports.decodeEnvelopeData = decodeEnvelopeData;
|
|
170
|
+
exports.envelopeFolderRoot = envelopeFolderRoot;
|
|
149
171
|
exports.envelopeProjectMap = envelopeProjectMap;
|
|
150
|
-
exports.
|
|
172
|
+
exports.hiddenField = hiddenField;
|
|
173
|
+
exports.hiddenFieldShareId = hiddenFieldShareId;
|
|
174
|
+
exports.isHiddenField = isHiddenField;
|
|
175
|
+
exports.newEnvelopeFolderId = newEnvelopeFolderId;
|
|
176
|
+
exports.newProjectFieldUuid = newProjectFieldUuid;
|
|
151
177
|
exports.newShareId = newShareId;
|
|
152
178
|
exports.normalizeEnvelopeData = normalizeEnvelopeData;
|
|
153
179
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sharing_model.cjs","names":["RoleEnum"],"sources":["../../src/model/sharing_model.ts"],"sourcesContent":["import type { ResourceType, Role } from \"@milaboratories/pl-client\";\nimport { Role as RoleEnum } from \"@milaboratories/pl-client\";\nimport type { Branded, ProjectId, ProjectTemplateV1 } from \"@milaboratories/pl-model-common\";\nimport { randomUUID } from \"node:crypto\";\n\n/**\n * Logical identity of a share, stable across replaces. A donor-generated UUID string,\n * branded so it cannot be silently confused with a project id, a login, or a raw field\n * name. Minted once with {@link newShareId}; every other site receives it (from decoded\n * {@link EnvelopeData} or by parsing a `decision/{shareId}` field name) and threads it\n * through unchanged.\n */\nexport type ShareId = Branded<string, \"ShareId\">;\n\n/** Mints a fresh {@link ShareId}. The single place a share's logical identity is created. */\nexport function newShareId(): ShareId {\n return randomUUID() as ShareId;\n}\n\n/** Brands a string already known to be a share id (e.g. parsed from a `decision/{shareId}`\n * field name) as a {@link ShareId}, without minting a new one. */\nexport function asShareId(id: string): ShareId {\n return id as ShareId;\n}\n\n//\n// Pl Model — Project Sharing\n//\n// All sharing structures are defined and managed by the middle layer; the\n// backend knows nothing about envelopes.\n//\n\n/** Field on the donor's clientRoot holding the {@link SharingOutboxResourceType} resource. */\nexport const SharingOutboxField = \"sharingOutbox\";\n/** Field on the acceptor's clientRoot holding the {@link SharingStateResourceType} resource. */\nexport const SharingStateField = \"sharingState\";\n\nexport const SharingOutboxResourceType: ResourceType = { name: \"SharingOutbox\", version: \"1\" };\nexport const SharedEnvelopeResourceType: ResourceType = { name: \"SharedEnvelope\", version: \"1\" };\nexport const SharingStateResourceType: ResourceType = { name: \"SharingState\", version: \"1\" };\n\nexport type EnvelopeMode = \"copy\" | \"read-only\" | \"collaboration\";\n\n/** Per-project decision on change, matching the UI labels: re-snapshot the live source (\"update\"),\n * carry the existing snapshot (\"keep\"), or drop the project from the pack (\"remove\"). */\nexport type ProjectChangeAction = \"keep\" | \"update\" | \"remove\";\n\n/** Key of the per-project envelope maps: a uuid minted per snapshot to name the `project/{uuid}`\n * field. Distinct from {@link ProjectId} — re-snapshotting one source yields a new uuid each time. */\nexport type ProjectFieldUuid = Branded<string, \"ProjectFieldUuid\">;\n\n/**\n * Whether a role may make a resource public (grant to everyone): true for controller,\n * admin; false for workflow and unspecified. The middle layer carries no policy\n * of its own here — a crafted call still hits the backend's role + permission-ceiling gate.\n * `null` (no-auth mode) returns false.\n */\nexport function canGrantToEveryone(role: Role | null): boolean {\n switch (role) {\n case RoleEnum.CONTROLLER:\n case RoleEnum.ADMIN:\n return true;\n default:\n return false;\n }\n}\n\n/**\n * Whether a role may impersonate another user: open/create another user's root and list\n * the resources that user can access. Mirrors the backend's authorization rule\n * `util/misecurity/role.go` `CanImpersonate` — true for controller and admin only. This is\n * the admin gate for the \"open another user's root\" feature and is intentionally stricter\n * than {@link canGrantToEveryone}, which also returns true for a regular user (a normal user\n * may share their own projects, but must never be offered impersonation). `null` (no-auth\n * mode) returns false.\n */\nexport function canImpersonate(role: Role | null): boolean {\n switch (role) {\n case RoleEnum.CONTROLLER:\n case RoleEnum.ADMIN:\n return true;\n default:\n return false;\n }\n}\n\n/** One project's snapshot inside an envelope, keyed by {@link ProjectFieldUuid} in a\n * `projects` {@link EnvelopePayload}. */\nexport interface EnvelopeProject {\n label: string; // carried so the pending-share UI renders without traversing into the project\n source: ProjectId; // donor's source projectId; supersedes a prior share and matches the snapshot to its live source on change\n updatedAt: number; // ms epoch of the last (re)snapshot\n}\n\n/**\n * What a share carries. The discriminant is what a reader checks before anything else: a\n * client that does not know a kind hides the share instead of offering something it cannot\n * act on.\n *\n * `projects` snapshots ride as `project/{uuid}` fields on the envelope and this map only\n * describes them; a `template` payload has no fields at all — the document is right here.\n */\nexport type EnvelopePayload =\n | { kind: \"projects\"; projects: Record<ProjectFieldUuid, EnvelopeProject> }\n | {\n kind: \"template\";\n document: ProjectTemplateV1;\n /** Label to give the template on the recipient's own shelf. */\n label: string;\n /** Donor login, kept on the accepted template as its provenance. */\n from: string;\n };\n\nexport type EnvelopePayloadKind = EnvelopePayload[\"kind\"];\n\n/** Every envelope schema version this build can read. Adding a version here is what makes\n * {@link normalizeEnvelopeData} accept it; bumping {@link EnvelopeSchemaVersionCurrent} to a\n * version missing from this union is a compile error. */\nexport type EnvelopeSchemaVersion = 1 | 2;\n\n/** Version written into every new envelope. Bumped from 1 when the payload became discriminated. */\nexport const EnvelopeSchemaVersionCurrent = 2 satisfies EnvelopeSchemaVersion;\n\n/**\n * Immutable `data` on a SharedEnvelope, set at createEphemeral, never mutated.\n *\n * Always the current version in memory: a v1 envelope (project map at the top level, no\n * `payload` field) is upcast on read by {@link normalizeEnvelopeData}, so no reader past the\n * decode has to know that two shapes ever existed.\n */\nexport interface EnvelopeData {\n schemaVersion: typeof EnvelopeSchemaVersionCurrent;\n shareId: ShareId; // donor-generated UUID; logical share identity, stable across changes\n sharedAt: number; // ms epoch; this instance's creation time — distinguishes instances of one shareId\n expiresAt: number | null; // ms epoch; sharedAt + ttl (default 14 days) for a targeted share; null for share-with-everybody (never expires)\n mode: EnvelopeMode; // what the acceptor's app should do with the contents\n sender: string; // donor login (informational; backend granted_by is authoritative)\n title: string; // display name shown to recipients; defaults to the first project's name\n payload: EnvelopePayload; // what the share carries\n}\n\n/** The project map of a projects-payload envelope, or `{}` for any other payload — the one\n * place a project-shaped reader turns a payload into the map it expects. */\nexport function envelopeProjectMap(data: EnvelopeData): Record<ProjectFieldUuid, EnvelopeProject> {\n return data.payload.kind === \"projects\" ? data.payload.projects : {};\n}\n\n/** Dynamic field on SharingState, one per handled share, keyed by shareId. */\nexport const decisionField = (shareId: ShareId) => `decision/${shareId}`;\n\nexport interface SharingDecision {\n decision: \"accepted\" | \"rejected\";\n timestamp: number; // ms epoch — when the acceptor acted\n envelopeSharedAt: number; // the acted-on envelope instance's sharedAt — pins which instance was handled (paired with the shareId key; the resource id is never stored)\n acceptedProjects: string[]; // ids of the projects created in the acceptor's list ([] for a rejected share, and for a template share, which creates none)\n}\n\n/** Dynamic field on SharedEnvelope, one per recipient who accepted or rejected, keyed\n * by recipient login. Written by the acceptor in read-write shares only (Copy & Share,\n * Live collaboration) — the acceptor's writable envelope grant is what permits the\n * write; read-only shares omit it. The donor reads these from its own outbox to see\n * who responded and when. Informational, not authoritative (a writable grant holder\n * could write under another login — same trust assumption as the sender field).\n * Copied forward when a share is changed. */\nexport const AcceptanceFieldPrefix = \"acceptance/\";\nexport const acceptanceField = (login: string) => `${AcceptanceFieldPrefix}${login}`;\nexport const isAcceptanceField = (name: string) => name.startsWith(AcceptanceFieldPrefix);\nexport const acceptanceFieldLogin = (name: string) => name.slice(AcceptanceFieldPrefix.length);\n\nexport interface EnvelopeAcceptance {\n action: \"accepted\" | \"rejected\";\n timestamp: number; // ms since epoch\n}\n\n/**\n * Single owner of the raw-data → {@link EnvelopeData} decode. The envelope's immutable `data`\n * blob is UTF-8 JSON set once at createEphemeral; every site that reads it from a raw resource\n * `data` byte buffer (the basic-resource read path) goes through here. The reactive tree-node\n * path decodes the same JSON with `getDataAsJson` and normalizes it with\n * {@link normalizeEnvelopeData} — both paths must, so neither sees the raw v1 shape.\n *\n * `undefined` for an envelope this build cannot act on; see {@link normalizeEnvelopeData}.\n */\nexport function decodeEnvelopeData(data: Uint8Array): EnvelopeData | undefined {\n return normalizeEnvelopeData(JSON.parse(Buffer.from(data).toString(\"utf-8\")));\n}\n\n/**\n * Brings a decoded envelope blob to the current shape, or reports that this build cannot act\n * on it by returning `undefined` — an unknown `schemaVersion` or an unknown payload kind. A\n * caller hides such a share rather than offering the recipient something it cannot handle.\n *\n * A v1 envelope carried its project map at the top level and had no `payload` field; it reads\n * here as a `projects` payload, so envelopes written before the discriminant existed keep\n * working unchanged.\n */\nexport function normalizeEnvelopeData(raw: unknown): EnvelopeData | undefined {\n if (typeof raw !== \"object\" || raw === null) return undefined;\n const e = raw as RawEnvelopeData;\n if (!Object.hasOwn(ReadableSchemaVersions, e.schemaVersion)) return undefined;\n\n const payload =\n e.payload ??\n (e.projects !== undefined ? ({ kind: \"projects\", projects: e.projects } as const) : undefined);\n if (payload === undefined) return undefined;\n if (!Object.hasOwn(KnownPayloadKinds, payload.kind)) return undefined;\n\n return {\n schemaVersion: EnvelopeSchemaVersionCurrent,\n shareId: e.shareId,\n sharedAt: e.sharedAt,\n expiresAt: e.expiresAt,\n mode: e.mode,\n sender: e.sender,\n title: e.title,\n payload,\n };\n}\n\n/**\n * Options for {@link MiddleLayer.shareProjects}.\n *\n * Recipients XOR everyone — two clean variants, not one struct with mutually exclusive\n * optional fields. The everyone variant issues a single make-public grant (the envelope's\n * `expiresAt` is set to `null`, so it never expires); the recipients variant grants each\n * named recipient and the envelope expires after the default TTL.\n */\nexport type ShareProjectsOptions =\n | {\n recipients: string[]; // recipient logins\n title: string; // display name shown to recipients; defaults to the first project's name\n mode: EnvelopeMode; // v1 UI always sends \"copy\"\n }\n | {\n everyone: true; // share with all users on the server\n /**\n * When true and an everyone-share of the same project already exists, refresh it under its\n * stable shareId (recipients who already accepted or rejected are not re-prompted) instead of\n * minting a new share. No-op when no prior everyone-share of the project exists. Callers that\n * don't care pass `false`.\n */\n replace: boolean;\n title: string;\n mode: EnvelopeMode;\n };\n\n/**\n * Options for {@link MiddleLayer.shareTemplate}.\n *\n * Recipients XOR everyone, exactly as {@link ShareProjectsOptions}, minus the mode: a template\n * share is always granted read-only, because the recipient copies no resource out of the\n * envelope — the document is in the envelope's own data.\n */\nexport type ShareTemplateOptions =\n | {\n recipients: string[]; // recipient logins\n title: string; // display name shown to recipients; defaults to the template's label\n }\n | {\n everyone: true; // share with all users on the server\n title: string;\n };\n\n//\n// Internals\n//\n\n/** Every payload kind this build can act on; anything else is hidden rather than offered.\n * Keyed by {@link EnvelopePayloadKind}, so adding a kind to {@link EnvelopePayload} without\n * teaching the decoder about it is a compile error, not a share that silently disappears. */\nconst KnownPayloadKinds: Record<EnvelopePayloadKind, true> = {\n projects: true,\n template: true,\n};\n\n/** Every schema version {@link normalizeEnvelopeData} accepts. Keyed by\n * {@link EnvelopeSchemaVersion}, so widening that union without deciding how the new shape\n * is upcast is a compile error. */\nconst ReadableSchemaVersions: Record<EnvelopeSchemaVersion, true> = {\n 1: true,\n 2: true,\n};\n\n/**\n * The envelope blob as it comes off the wire, before {@link normalizeEnvelopeData} decides\n * whether this build can act on it: the version is any number, the payload may be missing,\n * and `projects` is the v1 top-level project map.\n */\ntype RawEnvelopeData = Omit<EnvelopeData, \"schemaVersion\" | \"payload\"> & {\n schemaVersion: number;\n payload?: EnvelopePayload;\n projects?: Record<ProjectFieldUuid, EnvelopeProject>;\n};\n"],"mappings":";;;;AAeA,SAAgB,aAAsB;CACpC,QAAA,GAAA,YAAA,WAAA,CAAkB;AACpB;;;AAIA,SAAgB,UAAU,IAAqB;CAC7C,OAAO;AACT;;AAUA,MAAa,qBAAqB;;AAElC,MAAa,oBAAoB;AAEjC,MAAa,4BAA0C;CAAE,MAAM;CAAiB,SAAS;AAAI;AAC7F,MAAa,6BAA2C;CAAE,MAAM;CAAkB,SAAS;AAAI;AAC/F,MAAa,2BAAyC;CAAE,MAAM;CAAgB,SAAS;AAAI;;;;;;;AAkB3F,SAAgB,mBAAmB,MAA4B;CAC7D,QAAQ,MAAR;EACE,KAAKA,0BAAAA,KAAS;EACd,KAAKA,0BAAAA,KAAS,OACZ,OAAO;EACT,SACE,OAAO;CACX;AACF;;;;;;;;;;AAWA,SAAgB,eAAe,MAA4B;CACzD,QAAQ,MAAR;EACE,KAAKA,0BAAAA,KAAS;EACd,KAAKA,0BAAAA,KAAS,OACZ,OAAO;EACT,SACE,OAAO;CACX;AACF;;AAqCA,MAAa,+BAA+B;;;AAsB5C,SAAgB,mBAAmB,MAA+D;CAChG,OAAO,KAAK,QAAQ,SAAS,aAAa,KAAK,QAAQ,WAAW,CAAC;AACrE;;AAGA,MAAa,iBAAiB,YAAqB,YAAY;;;;;;;;AAgB/D,MAAa,wBAAwB;AACrC,MAAa,mBAAmB,UAAkB,GAAG,wBAAwB;AAC7E,MAAa,qBAAqB,SAAiB,KAAK,WAAW,qBAAqB;AACxF,MAAa,wBAAwB,SAAiB,KAAK,MAAM,EAA4B;;;;;;;;;;AAgB7F,SAAgB,mBAAmB,MAA4C;CAC7E,OAAO,sBAAsB,KAAK,MAAM,OAAO,KAAK,IAAI,CAAC,CAAC,SAAS,OAAO,CAAC,CAAC;AAC9E;;;;;;;;;;AAWA,SAAgB,sBAAsB,KAAwC;CAC5E,IAAI,OAAO,QAAQ,YAAY,QAAQ,MAAM,OAAO,KAAA;CACpD,MAAM,IAAI;CACV,IAAI,CAAC,OAAO,OAAO,wBAAwB,EAAE,aAAa,GAAG,OAAO,KAAA;CAEpE,MAAM,UACJ,EAAE,YACD,EAAE,aAAa,KAAA,IAAa;EAAE,MAAM;EAAY,UAAU,EAAE;CAAS,IAAc,KAAA;CACtF,IAAI,YAAY,KAAA,GAAW,OAAO,KAAA;CAClC,IAAI,CAAC,OAAO,OAAO,mBAAmB,QAAQ,IAAI,GAAG,OAAO,KAAA;CAE5D,OAAO;EACL,eAAA;EACA,SAAS,EAAE;EACX,UAAU,EAAE;EACZ,WAAW,EAAE;EACb,MAAM,EAAE;EACR,QAAQ,EAAE;EACV,OAAO,EAAE;EACT;CACF;AACF;;;;AAqDA,MAAM,oBAAuD;CAC3D,UAAU;CACV,UAAU;AACZ;;;;AAKA,MAAM,yBAA8D;CAClE,GAAG;CACH,GAAG;AACL"}
|
|
1
|
+
{"version":3,"file":"sharing_model.cjs","names":["RoleEnum"],"sources":["../../src/model/sharing_model.ts"],"sourcesContent":["import type { ResourceType, Role } from \"@milaboratories/pl-client\";\nimport { Role as RoleEnum } from \"@milaboratories/pl-client\";\nimport type {\n Branded,\n ProjectId,\n ProjectTemplateV1,\n TemplateId,\n} from \"@milaboratories/pl-model-common\";\nimport type { FolderId } from \"@milaboratories/pl-model-middle-layer\";\nimport { randomUUID } from \"node:crypto\";\n\n/**\n * Identity of one share. A donor-generated UUID string,\n * branded so it cannot be silently confused with a project id, a login, or a raw field\n * name. Minted once with {@link newShareId}; every other site receives it (from decoded\n * {@link EnvelopeData} or by parsing a `decision/{shareId}` field name) and threads it\n * through unchanged.\n */\nexport type ShareId = Branded<string, \"ShareId\">;\n\n/** Mints a fresh {@link ShareId}. The single place a share's logical identity is created. */\nexport function newShareId(): ShareId {\n return randomUUID() as ShareId;\n}\n\n/** Brands a string already known to be a share id (e.g. parsed from a `decision/{shareId}`\n * field name) as a {@link ShareId}, without minting a new one. */\nexport function asShareId(id: string): ShareId {\n return id as ShareId;\n}\n\n//\n// Pl Model — Project Sharing\n//\n// All sharing structures are defined and managed by the middle layer; the\n// backend knows nothing about envelopes.\n//\n\n/** Field on the donor's clientRoot holding the {@link SharingOutboxResourceType} resource. */\nexport const SharingOutboxField = \"sharingOutbox\";\n/** Field on the recipient's clientRoot holding the {@link SharingStateResourceType} resource. */\nexport const SharingStateField = \"sharingState\";\n\nexport const SharingOutboxResourceType: ResourceType = { name: \"SharingOutbox\", version: \"1\" };\nexport const SharedEnvelopeResourceType: ResourceType = { name: \"SharedEnvelope\", version: \"1\" };\nexport const SharingStateResourceType: ResourceType = { name: \"SharingState\", version: \"1\" };\n\nexport type EnvelopeMode = \"copy\" | \"read-only\" | \"collaboration\";\n\n/** Key of the per-project envelope maps: a uuid minted per snapshot to name the `project/{uuid}`\n * field. Distinct from {@link ProjectId} — re-snapshotting one source yields a new uuid each time. */\nexport type ProjectFieldUuid = Branded<string, \"ProjectFieldUuid\">;\n\n/** Mints a fresh {@link ProjectFieldUuid} for one snapshot. */\nexport function newProjectFieldUuid(): ProjectFieldUuid {\n return randomUUID() as ProjectFieldUuid;\n}\n\n/**\n * Whether a role may make a resource public (grant to everyone): true for controller,\n * admin; false for workflow and unspecified. The middle layer carries no policy\n * of its own here — a crafted call still hits the backend's role + permission-ceiling gate.\n * `null` (no-auth mode) returns false.\n */\nexport function canGrantToEveryone(role: Role | null): boolean {\n switch (role) {\n case RoleEnum.CONTROLLER:\n case RoleEnum.ADMIN:\n return true;\n default:\n return false;\n }\n}\n\n/**\n * Whether a role may impersonate another user: open/create another user's root and list\n * the resources that user can access. Mirrors the backend's authorization rule\n * `util/misecurity/role.go` `CanImpersonate` — true for controller and admin only. This is\n * the admin gate for the \"open another user's root\" feature and is intentionally stricter\n * than {@link canGrantToEveryone}, which also returns true for a regular user (a normal user\n * may share their own projects, but must never be offered impersonation). `null` (no-auth\n * mode) returns false.\n */\nexport function canImpersonate(role: Role | null): boolean {\n switch (role) {\n case RoleEnum.CONTROLLER:\n case RoleEnum.ADMIN:\n return true;\n default:\n return false;\n }\n}\n\n/** One project's snapshot inside an envelope, keyed by {@link ProjectFieldUuid} in a\n * `projects` {@link EnvelopePayload}. */\nexport interface EnvelopeProject {\n label: string; // carried so the share lists render without traversing into the project\n source: ProjectId; // donor's source projectId; what a prior share of the same project is matched on\n updatedAt: number; // ms epoch of the last (re)snapshot\n /** What the project said about itself when it was snapshotted, carried for the same reason as\n * `label`. Absent when it had none, and on envelopes written before it was carried. */\n description?: string;\n}\n\n/**\n * Identifier of a folder inside one envelope.\n *\n * Local to the envelope, because the recipient's own folder document mints its own ids and the\n * donor's mean nothing there. What travels is the shape of the subtree, not its identity.\n */\nexport type EnvelopeFolderId = Branded<string, \"EnvelopeFolderId\">;\n\n/** Mints a fresh {@link EnvelopeFolderId} for one folder of the envelope being built. */\nexport function newEnvelopeFolderId(): EnvelopeFolderId {\n return randomUUID() as EnvelopeFolderId;\n}\n\n/** One folder of a shared subtree. */\nexport interface EnvelopeFolder {\n name: string;\n /** Absent for the subtree's root — the folder that was shared. */\n parent?: EnvelopeFolderId;\n /** What the donor wrote about the folder. Absent when there is none. */\n description?: string;\n}\n\n/** A project of a shared subtree: an {@link EnvelopeProject} placed in the subtree. */\nexport interface EnvelopeFolderProject extends EnvelopeProject {\n folder: EnvelopeFolderId;\n}\n\n/** A template of a shared subtree. The document rides here whole, exactly as a `template`\n * payload carries it — a template is never snapshotted. */\nexport interface EnvelopeFolderTemplate {\n document: ProjectTemplateV1;\n /** Label to give the template among the recipient's templates. */\n label: string;\n /** What the template says about itself. Absent when the donor described it with nothing. */\n description?: string;\n folder: EnvelopeFolderId;\n}\n\n/**\n * What a share carries. The discriminant is what a reader checks before anything else: a\n * client that does not know a kind hides the share instead of offering something it cannot\n * act on.\n *\n * `projects` snapshots ride as `project/{uuid}` fields on the envelope and this map only\n * describes them; a `template` payload has no fields at all — the document is right here.\n */\nexport type EnvelopePayload =\n | { kind: \"projects\"; projects: Record<ProjectFieldUuid, EnvelopeProject> }\n | {\n kind: \"template\";\n document: ProjectTemplateV1;\n /** Donor's own id of the shared template; what a prior share of the same template is\n * matched on. It names nothing in the recipient's tree, and envelopes written before it\n * existed carry none — those match no later share of anything. */\n source?: TemplateId;\n /** Label to give the template among the recipient's templates. */\n label: string;\n /** What the template says about itself, carried to the recipient's copy. Absent when the\n * donor described it with nothing. */\n description?: string;\n /** Donor login, kept on each copy of the template as its provenance. */\n from: string;\n }\n | {\n kind: \"folder\";\n /** Donor's own id of the shared folder; what a prior share of the same folder is matched\n * on. It names nothing in the recipient's tree. */\n source: FolderId;\n /** The shared subtree. Exactly one folder has no parent, and that one is its root. */\n folders: Record<EnvelopeFolderId, EnvelopeFolder>;\n /** Project snapshots, each tagged with the folder of the subtree holding it. Their\n * `project/{uuid}` fields are the same ones a `projects` payload describes. */\n projects: Record<ProjectFieldUuid, EnvelopeFolderProject>;\n /** Templates of the subtree, documents and all. Nothing of a template is snapshotted. */\n templates: EnvelopeFolderTemplate[];\n /** Donor login, kept on each copied template as its provenance. */\n from: string;\n };\n\nexport type EnvelopePayloadKind = EnvelopePayload[\"kind\"];\n\n/** Every envelope schema version this build can read. Adding a version here is what makes\n * {@link normalizeEnvelopeData} accept it; bumping {@link EnvelopeSchemaVersionCurrent} to a\n * version missing from this union is a compile error. */\nexport type EnvelopeSchemaVersion = 1 | 2;\n\n/** Version written into every new envelope. Bumped from 1 when the payload became discriminated. */\nexport const EnvelopeSchemaVersionCurrent = 2 satisfies EnvelopeSchemaVersion;\n\n/**\n * Immutable `data` on a SharedEnvelope, set at createEphemeral, never mutated.\n *\n * Always the current version in memory: a v1 envelope (project map at the top level, no\n * `payload` field) is upcast on read by {@link normalizeEnvelopeData}, so no reader past the\n * decode has to know that two shapes ever existed.\n */\nexport interface EnvelopeData {\n schemaVersion: typeof EnvelopeSchemaVersionCurrent;\n shareId: ShareId; // donor-generated UUID; identity of this share alone\n sharedAt: number; // ms epoch; when the share was created\n expiresAt: number | null; // ms epoch; sharedAt + ttl (default 14 days) for a targeted share; null for share-with-everybody (never expires)\n mode: EnvelopeMode; // what the recipient's app should do with the contents\n sender: string; // donor login (informational; backend granted_by is authoritative)\n title: string; // display name shown to recipients; defaults to the first project's name\n payload: EnvelopePayload; // what the share carries\n}\n\n/** The project map of a projects-payload envelope, or `{}` for any other payload — the one\n * place a project-shaped reader turns a payload into the map it expects. */\nexport function envelopeProjectMap(data: EnvelopeData): Record<ProjectFieldUuid, EnvelopeProject> {\n return data.payload.kind === \"projects\" ? data.payload.projects : {};\n}\n\n/**\n * The shared folder itself: the one folder of the subtree that has no parent.\n *\n * Derived rather than stored, so it cannot disagree with the folders beside it. `undefined` for\n * a subtree with no root or more than one, which is an envelope nothing can be reconstructed\n * from — the copy reports it rather than guessing which folder was meant.\n */\nexport function envelopeFolderRoot(\n folders: Record<EnvelopeFolderId, EnvelopeFolder>,\n): EnvelopeFolderId | undefined {\n const roots = (Object.keys(folders) as EnvelopeFolderId[]).filter(\n (id) => folders[id].parent === undefined,\n );\n return roots.length === 1 ? roots[0] : undefined;\n}\n\n/**\n * Dynamic field on SharingState, one per share this user has hidden, keyed by shareId.\n *\n * Hiding is private to the recipient and reversible: the field is written to put a share out of\n * sight and removed to bring it back. It says nothing to the donor and nothing about whether\n * anything was ever copied out of the share — a share can be copied from any number of times,\n * before or after being hidden.\n *\n * The field name keeps its original `decision/` prefix. Records written before hiding replaced\n * accept/reject carry a different value under the same key, and every reader treats the presence\n * of the field as the whole answer: someone who accepted or rejected a share back then does not\n * want to see it, which is exactly what hidden means.\n */\nexport const HiddenFieldPrefix = \"decision/\";\nexport const hiddenField = (shareId: ShareId) => `${HiddenFieldPrefix}${shareId}`;\nexport const isHiddenField = (name: string) => name.startsWith(HiddenFieldPrefix);\nexport const hiddenFieldShareId = (name: string): ShareId =>\n asShareId(name.slice(HiddenFieldPrefix.length));\n\nexport interface ShareHidden {\n hidden: true;\n timestamp: number; // ms epoch — when the recipient hid it\n}\n\n/**\n * Single owner of the raw-data → {@link EnvelopeData} decode. The envelope's immutable `data`\n * blob is UTF-8 JSON set once at createEphemeral; every site that reads it from a raw resource\n * `data` byte buffer (the basic-resource read path) goes through here. The reactive tree-node\n * path decodes the same JSON with `getDataAsJson` and normalizes it with\n * {@link normalizeEnvelopeData} — both paths must, so neither sees the raw v1 shape.\n *\n * `undefined` for an envelope this build cannot act on; see {@link normalizeEnvelopeData}.\n */\nexport function decodeEnvelopeData(data: Uint8Array): EnvelopeData | undefined {\n return normalizeEnvelopeData(JSON.parse(Buffer.from(data).toString(\"utf-8\")));\n}\n\n/**\n * Brings a decoded envelope blob to the current shape, or reports that this build cannot act\n * on it by returning `undefined` — an unknown `schemaVersion` or an unknown payload kind. A\n * caller hides such a share rather than offering the recipient something it cannot handle.\n *\n * A v1 envelope carried its project map at the top level and had no `payload` field; it reads\n * here as a `projects` payload, so envelopes written before the discriminant existed keep\n * working unchanged.\n */\nexport function normalizeEnvelopeData(raw: unknown): EnvelopeData | undefined {\n if (typeof raw !== \"object\" || raw === null) return undefined;\n const e = raw as RawEnvelopeData;\n if (!Object.hasOwn(ReadableSchemaVersions, e.schemaVersion)) return undefined;\n\n const payload =\n e.payload ??\n (e.projects !== undefined ? ({ kind: \"projects\", projects: e.projects } as const) : undefined);\n if (payload === undefined) return undefined;\n if (!Object.hasOwn(KnownPayloadKinds, payload.kind)) return undefined;\n\n return {\n schemaVersion: EnvelopeSchemaVersionCurrent,\n shareId: e.shareId,\n sharedAt: e.sharedAt,\n expiresAt: e.expiresAt,\n mode: e.mode,\n sender: e.sender,\n title: e.title,\n payload,\n };\n}\n\n/**\n * Who a share is granted to: named recipients XOR everyone — two clean variants, not one struct\n * with mutually exclusive optional fields.\n *\n * The everyone variant issues a single make-public grant, and the envelope's `expiresAt` is `null`,\n * so it never expires. The recipients variant grants each named login, and the envelope expires\n * after the default TTL.\n */\nexport type ShareAudience =\n | { recipients: string[] } // recipient logins\n | { everyone: true }; // every user on the server\n\n/**\n * Options every share takes: the audience, the title recipients see it under, and the prior\n * shares it replaces.\n */\nexport type ShareOptions = ShareAudience & {\n title: string;\n replace?: ShareReplaceOption;\n};\n\n/**\n * Prior shares the new one supersedes: each is deleted in the same transaction that creates the\n * replacement, so the outbox never holds both.\n *\n * The set is the caller's, never inferred here. The author is shown the shares that will go and\n * agrees to that list, so what was shown has to be what is deleted — an overlap rule computed on\n * this side would diverge from it. Ids that no longer resolve are skipped: a share revoked between\n * the dialog opening and the confirm is nothing to undo.\n *\n * The replacement is a new {@link ShareId}: a recipient who had hidden the old share sees the new\n * one, and copies already taken from the old share are untouched.\n */\nexport type ShareReplaceOption = ShareId[];\n\n/** Options for {@link MiddleLayer.shareProjects}: the common ones, plus what the recipient's app\n * does with the projects. */\nexport type ShareProjectsOptions = ShareOptions & { mode: EnvelopeMode };\n\n/** Options for {@link MiddleLayer.shareTemplate}. */\nexport type ShareTemplateOptions = ShareOptions;\n\n/** Options for {@link MiddleLayer.shareFolder}. */\nexport type ShareFolderOptions = ShareOptions;\n\n/** What creating a share hands back: the id of the share just created. */\nexport type ShareOutcome = { readonly shareId: ShareId };\n\n//\n// Internals\n//\n\n/** Every payload kind this build can act on; anything else is hidden rather than offered.\n * Keyed by {@link EnvelopePayloadKind}, so adding a kind to {@link EnvelopePayload} without\n * teaching the decoder about it is a compile error, not a share that silently disappears. */\nconst KnownPayloadKinds: Record<EnvelopePayloadKind, true> = {\n projects: true,\n template: true,\n folder: true,\n};\n\n/** Every schema version {@link normalizeEnvelopeData} accepts. Keyed by\n * {@link EnvelopeSchemaVersion}, so widening that union without deciding how the new shape\n * is upcast is a compile error. */\nconst ReadableSchemaVersions: Record<EnvelopeSchemaVersion, true> = {\n 1: true,\n 2: true,\n};\n\n/**\n * The envelope blob as it comes off the wire, before {@link normalizeEnvelopeData} decides\n * whether this build can act on it: the version is any number, the payload may be missing,\n * and `projects` is the v1 top-level project map.\n */\ntype RawEnvelopeData = Omit<EnvelopeData, \"schemaVersion\" | \"payload\"> & {\n schemaVersion: number;\n payload?: EnvelopePayload;\n projects?: Record<ProjectFieldUuid, EnvelopeProject>;\n};\n"],"mappings":";;;;AAqBA,SAAgB,aAAsB;CACpC,QAAA,GAAA,YAAA,WAAA,CAAkB;AACpB;;;AAIA,SAAgB,UAAU,IAAqB;CAC7C,OAAO;AACT;;AAUA,MAAa,qBAAqB;;AAElC,MAAa,oBAAoB;AAEjC,MAAa,4BAA0C;CAAE,MAAM;CAAiB,SAAS;AAAI;AAC7F,MAAa,6BAA2C;CAAE,MAAM;CAAkB,SAAS;AAAI;AAC/F,MAAa,2BAAyC;CAAE,MAAM;CAAgB,SAAS;AAAI;;AAS3F,SAAgB,sBAAwC;CACtD,QAAA,GAAA,YAAA,WAAA,CAAkB;AACpB;;;;;;;AAQA,SAAgB,mBAAmB,MAA4B;CAC7D,QAAQ,MAAR;EACE,KAAKA,0BAAAA,KAAS;EACd,KAAKA,0BAAAA,KAAS,OACZ,OAAO;EACT,SACE,OAAO;CACX;AACF;;;;;;;;;;AAWA,SAAgB,eAAe,MAA4B;CACzD,QAAQ,MAAR;EACE,KAAKA,0BAAAA,KAAS;EACd,KAAKA,0BAAAA,KAAS,OACZ,OAAO;EACT,SACE,OAAO;CACX;AACF;;AAsBA,SAAgB,sBAAwC;CACtD,QAAA,GAAA,YAAA,WAAA,CAAkB;AACpB;;AA4EA,MAAa,+BAA+B;;;AAsB5C,SAAgB,mBAAmB,MAA+D;CAChG,OAAO,KAAK,QAAQ,SAAS,aAAa,KAAK,QAAQ,WAAW,CAAC;AACrE;;;;;;;;AASA,SAAgB,mBACd,SAC8B;CAC9B,MAAM,QAAS,OAAO,KAAK,OAAO,CAAC,CAAwB,QACxD,OAAO,QAAQ,GAAG,CAAC,WAAW,KAAA,CACjC;CACA,OAAO,MAAM,WAAW,IAAI,MAAM,KAAK,KAAA;AACzC;;;;;;;;;;;;;;AAeA,MAAa,oBAAoB;AACjC,MAAa,eAAe,YAAqB,GAAG,oBAAoB;AACxE,MAAa,iBAAiB,SAAiB,KAAK,WAAW,iBAAiB;AAChF,MAAa,sBAAsB,SACjC,UAAU,KAAK,MAAM,CAAwB,CAAC;;;;;;;;;;AAgBhD,SAAgB,mBAAmB,MAA4C;CAC7E,OAAO,sBAAsB,KAAK,MAAM,OAAO,KAAK,IAAI,CAAC,CAAC,SAAS,OAAO,CAAC,CAAC;AAC9E;;;;;;;;;;AAWA,SAAgB,sBAAsB,KAAwC;CAC5E,IAAI,OAAO,QAAQ,YAAY,QAAQ,MAAM,OAAO,KAAA;CACpD,MAAM,IAAI;CACV,IAAI,CAAC,OAAO,OAAO,wBAAwB,EAAE,aAAa,GAAG,OAAO,KAAA;CAEpE,MAAM,UACJ,EAAE,YACD,EAAE,aAAa,KAAA,IAAa;EAAE,MAAM;EAAY,UAAU,EAAE;CAAS,IAAc,KAAA;CACtF,IAAI,YAAY,KAAA,GAAW,OAAO,KAAA;CAClC,IAAI,CAAC,OAAO,OAAO,mBAAmB,QAAQ,IAAI,GAAG,OAAO,KAAA;CAE5D,OAAO;EACL,eAAA;EACA,SAAS,EAAE;EACX,UAAU,EAAE;EACZ,WAAW,EAAE;EACb,MAAM,EAAE;EACR,QAAQ,EAAE;EACV,OAAO,EAAE;EACT;CACF;AACF;;;;AAyDA,MAAM,oBAAuD;CAC3D,UAAU;CACV,UAAU;CACV,QAAQ;AACV;;;;AAKA,MAAM,yBAA8D;CAClE,GAAG;CACH,GAAG;AACL"}
|