@milaboratories/pl-middle-layer 1.48.28 → 1.49.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.
@@ -38,6 +38,7 @@ import {
38
38
  ProjectMetaKey,
39
39
  InitialBlockMeta,
40
40
  blockArgsAuthorKey,
41
+ BlockArgsAuthorKeyPrefix,
41
42
  ProjectLastModifiedTimestamp,
42
43
  ProjectCreatedTimestamp,
43
44
  ProjectStructureAuthorKey,
@@ -1822,6 +1823,83 @@ export async function createProject(
1822
1823
  return prj;
1823
1824
  }
1824
1825
 
1826
+ /**
1827
+ * Duplicates an existing project resource within a transaction.
1828
+ * Creates a new project resource with all dynamic fields (block data) copied by reference
1829
+ * and all KV metadata copied with appropriate adjustments.
1830
+ *
1831
+ * The returned resource is NOT attached to any project list — the caller is responsible
1832
+ * for placing it where needed. This enables cross-user duplication.
1833
+ *
1834
+ * @param tx - active write transaction
1835
+ * @param sourceRid - resource id of the project to duplicate
1836
+ * @param options.label - optional label override for the new project; if omitted, copies the source label
1837
+ */
1838
+ export async function duplicateProject(
1839
+ tx: PlTransaction,
1840
+ sourceRid: ResourceId,
1841
+ options?: { label?: string },
1842
+ ): Promise<AnyResourceRef> {
1843
+ // Read source resource data (with fields) and all KV pairs
1844
+ const sourceDataP = tx.getResourceData(sourceRid, true);
1845
+ const sourceKVsP = tx.listKeyValuesString(sourceRid);
1846
+
1847
+ const sourceData = await sourceDataP;
1848
+ const sourceKVs = await sourceKVsP;
1849
+
1850
+ // Validate schema version
1851
+ const schemaKV = sourceKVs.find((kv) => kv.key === SchemaVersionKey);
1852
+ const schema = schemaKV ? JSON.parse(schemaKV.value) : undefined;
1853
+ if (schema !== SchemaVersionCurrent) {
1854
+ throw new UiError(
1855
+ `Cannot duplicate project with schema version ${schema ?? "unknown"}. ` +
1856
+ `Only schema version ${SchemaVersionCurrent} is supported. ` +
1857
+ `Try opening the project first to trigger migration.`,
1858
+ );
1859
+ }
1860
+
1861
+ // Create new project resource
1862
+ const newPrj = tx.createEphemeral(ProjectResourceType);
1863
+ tx.lock(newPrj);
1864
+
1865
+ // Copy KV pairs with adjustments
1866
+ const ts = String(Date.now());
1867
+ const kvSkipPrefixes = [BlockArgsAuthorKeyPrefix];
1868
+ const kvSkipKeys = new Set([
1869
+ ProjectCreatedTimestamp,
1870
+ ProjectLastModifiedTimestamp,
1871
+ ProjectStructureAuthorKey,
1872
+ ]);
1873
+
1874
+ for (const { key, value } of sourceKVs) {
1875
+ // Skip author markers
1876
+ if (kvSkipKeys.has(key)) continue;
1877
+ if (kvSkipPrefixes.some((prefix) => key.startsWith(prefix))) continue;
1878
+
1879
+ if (key === ProjectMetaKey && options?.label !== undefined) {
1880
+ // Override label
1881
+ const meta: ProjectMeta = JSON.parse(value);
1882
+ tx.setKValue(newPrj, key, JSON.stringify({ ...meta, label: options.label }));
1883
+ } else {
1884
+ // Copy as-is
1885
+ tx.setKValue(newPrj, key, value);
1886
+ }
1887
+ }
1888
+
1889
+ // Set fresh timestamps
1890
+ tx.setKValue(newPrj, ProjectCreatedTimestamp, ts);
1891
+ tx.setKValue(newPrj, ProjectLastModifiedTimestamp, ts);
1892
+
1893
+ // Copy all dynamic fields by sharing references
1894
+ for (const f of sourceData.fields) {
1895
+ if (isNotNullResourceId(f.value)) {
1896
+ tx.createField(field(newPrj, f.name), "Dynamic", f.value);
1897
+ }
1898
+ }
1899
+
1900
+ return newPrj;
1901
+ }
1902
+
1825
1903
  export async function withProject<T>(
1826
1904
  projectHelper: ProjectHelper,
1827
1905
  txOrPl: PlTransaction | PlClient,