@milaboratories/pl-middle-layer 1.73.10 → 1.74.1
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/middle_layer/build_stamp.cjs +1 -1
- package/dist/middle_layer/build_stamp.js +1 -1
- package/dist/middle_layer/middle_layer.cjs +43 -0
- package/dist/middle_layer/middle_layer.cjs.map +1 -1
- package/dist/middle_layer/middle_layer.d.ts +22 -0
- package/dist/middle_layer/middle_layer.d.ts.map +1 -1
- package/dist/middle_layer/middle_layer.js +44 -1
- package/dist/middle_layer/middle_layer.js.map +1 -1
- package/package.json +19 -19
- package/src/middle_layer/middle_layer.ts +63 -1
- package/src/middle_layer/templates.test.ts +48 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@milaboratories/pl-middle-layer",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.74.1",
|
|
4
4
|
"description": "Pl Middle Layer",
|
|
5
5
|
"keywords": [],
|
|
6
6
|
"license": "UNLICENSED",
|
|
@@ -30,25 +30,25 @@
|
|
|
30
30
|
"utility-types": "^3.11.0",
|
|
31
31
|
"yaml": "^2.8.0",
|
|
32
32
|
"zod": "~3.25.76",
|
|
33
|
-
"@milaboratories/
|
|
34
|
-
"@milaboratories/
|
|
35
|
-
"@milaboratories/helpers": "1.14.5",
|
|
36
|
-
"@milaboratories/pf-spec-driver": "1.5.7",
|
|
37
|
-
"@milaboratories/pl-crash-recorder": "0.3.4",
|
|
33
|
+
"@milaboratories/pf-driver": "1.9.8",
|
|
34
|
+
"@milaboratories/columns-collection-driver": "0.2.9",
|
|
38
35
|
"@milaboratories/computable": "2.9.8",
|
|
39
|
-
"@milaboratories/pl-
|
|
40
|
-
"@milaboratories/
|
|
41
|
-
"@milaboratories/pl-deployments": "3.0.
|
|
42
|
-
"@milaboratories/pl-
|
|
43
|
-
"@milaboratories/
|
|
36
|
+
"@milaboratories/pl-crash-recorder": "0.3.5",
|
|
37
|
+
"@milaboratories/helpers": "1.14.5",
|
|
38
|
+
"@milaboratories/pl-deployments": "3.0.21",
|
|
39
|
+
"@milaboratories/pl-client": "3.18.2",
|
|
40
|
+
"@milaboratories/pf-spec-driver": "1.5.8",
|
|
41
|
+
"@milaboratories/pl-drivers": "1.16.33",
|
|
42
|
+
"@milaboratories/pl-errors": "1.4.51",
|
|
43
|
+
"@milaboratories/pl-model-backend": "1.4.36",
|
|
44
44
|
"@milaboratories/pl-http": "1.2.4",
|
|
45
|
-
"@milaboratories/pl-model-common": "1.
|
|
46
|
-
"@milaboratories/pl-model-middle-layer": "1.34.
|
|
47
|
-
"@milaboratories/pl-tree": "1.16.
|
|
48
|
-
"@milaboratories/ts-helpers": "1.8.6",
|
|
45
|
+
"@milaboratories/pl-model-common": "1.53.0",
|
|
46
|
+
"@milaboratories/pl-model-middle-layer": "1.34.2",
|
|
47
|
+
"@milaboratories/pl-tree": "1.16.3",
|
|
49
48
|
"@milaboratories/resolve-helper": "1.1.3",
|
|
50
|
-
"@
|
|
51
|
-
"@platforma-sdk/
|
|
49
|
+
"@milaboratories/ts-helpers": "1.8.6",
|
|
50
|
+
"@platforma-sdk/block-tools": "2.16.13",
|
|
51
|
+
"@platforma-sdk/model": "1.84.17",
|
|
52
52
|
"@platforma-sdk/workflow-tengo": "6.15.0"
|
|
53
53
|
},
|
|
54
54
|
"devDependencies": {
|
|
@@ -57,9 +57,9 @@
|
|
|
57
57
|
"@vitest/coverage-istanbul": "^4.1.3",
|
|
58
58
|
"typescript": "7.0.2",
|
|
59
59
|
"vitest": "^4.1.3",
|
|
60
|
+
"@milaboratories/build-configs": "2.0.2",
|
|
60
61
|
"@milaboratories/ts-builder": "1.7.2",
|
|
61
|
-
"@milaboratories/ts-configs": "1.4.0"
|
|
62
|
-
"@milaboratories/build-configs": "2.0.2"
|
|
62
|
+
"@milaboratories/ts-configs": "1.4.0"
|
|
63
63
|
},
|
|
64
64
|
"engines": {
|
|
65
65
|
"node": ">=22.19.0"
|
|
@@ -76,7 +76,7 @@ import {
|
|
|
76
76
|
} from "../mutator/project";
|
|
77
77
|
import type { ProjectTemplateExportOutcome } from "../model/template_serializer";
|
|
78
78
|
import type { ProjectTemplateV1 } from "@milaboratories/pl-model-common";
|
|
79
|
-
import { asProjectId, asTemplateId } from "@milaboratories/pl-model-common";
|
|
79
|
+
import { asProjectId, asTemplateId, withTemplateMeta } from "@milaboratories/pl-model-common";
|
|
80
80
|
import { extractConfig, ensureError } from "@platforma-sdk/model";
|
|
81
81
|
import type { TemplateApplyProblem, TemplateApplyReport } from "../model/template_apply";
|
|
82
82
|
import { TemplateEntryRejected, kindMismatch } from "../model/template_apply";
|
|
@@ -943,6 +943,68 @@ export class MiddleLayer {
|
|
|
943
943
|
return { ok: true, templateId };
|
|
944
944
|
}
|
|
945
945
|
|
|
946
|
+
/**
|
|
947
|
+
* Stores a template brought in from outside — a template file the user picked — so it can be
|
|
948
|
+
* seen, applied and shared like one saved from a project.
|
|
949
|
+
*
|
|
950
|
+
* The document's description becomes the template's own, which stays editable; the stored
|
|
951
|
+
* document keeps only the blocks, without the label and description it was written with, so
|
|
952
|
+
* there is one name and one description to edit and nothing to drift.
|
|
953
|
+
*
|
|
954
|
+
* The template lands in `folder`, or at the top level, under `label` made free among the
|
|
955
|
+
* templates already there. A folder deleted meanwhile costs the template its placement, not its
|
|
956
|
+
* existence.
|
|
957
|
+
*
|
|
958
|
+
* @param document the parsed template to store
|
|
959
|
+
* @param options.label the name to store it under — usually the document's own label, which
|
|
960
|
+
* the caller weighs against whatever else it knows, such as the file's name; suffixed when a
|
|
961
|
+
* template there carries it
|
|
962
|
+
* @param options.folder where it lands; the top level when absent
|
|
963
|
+
*/
|
|
964
|
+
public async importTemplate(
|
|
965
|
+
document: ProjectTemplateV1,
|
|
966
|
+
options: { label: string; folder?: FolderId },
|
|
967
|
+
): Promise<TemplateId> {
|
|
968
|
+
const { folder } = options;
|
|
969
|
+
const signedRid = await this.pl.withWriteTx("MLImportTemplate", async (tx) => {
|
|
970
|
+
const tree = await openFoldersTx(tx, this.foldersRids);
|
|
971
|
+
// The name is made free where the template actually lands: the top level when the folder
|
|
972
|
+
// asked for is gone.
|
|
973
|
+
const destination =
|
|
974
|
+
folder !== undefined && tree.view.folders.some((candidate) => candidate.id === folder)
|
|
975
|
+
? folder
|
|
976
|
+
: undefined;
|
|
977
|
+
const name = foldersUniqueName(
|
|
978
|
+
options.label.trim(),
|
|
979
|
+
tree.namesTakenIn(destination, "template"),
|
|
980
|
+
);
|
|
981
|
+
|
|
982
|
+
const tpl = createTemplate(
|
|
983
|
+
tx,
|
|
984
|
+
this.templateListResourceId,
|
|
985
|
+
{ label: name, description: document.description },
|
|
986
|
+
{ schemaVersion: 1, document: withTemplateMeta(document, {}) },
|
|
987
|
+
);
|
|
988
|
+
|
|
989
|
+
const created = await tpl.globalId;
|
|
990
|
+
if (destination !== undefined)
|
|
991
|
+
tree.place(
|
|
992
|
+
[{ kind: "template", id: asTemplateId(resourceIdToString(created)) }],
|
|
993
|
+
destination,
|
|
994
|
+
);
|
|
995
|
+
await tx.commit();
|
|
996
|
+
return created;
|
|
997
|
+
});
|
|
998
|
+
await Promise.all([
|
|
999
|
+
this.templateListTree.refreshState(),
|
|
1000
|
+
...(folder === undefined ? [] : [this.foldersTree.refreshState()]),
|
|
1001
|
+
]);
|
|
1002
|
+
|
|
1003
|
+
const templateId = asTemplateId(resourceIdToString(signedRid));
|
|
1004
|
+
this.templateIdCache.set(templateId, signedRid);
|
|
1005
|
+
return templateId;
|
|
1006
|
+
}
|
|
1007
|
+
|
|
946
1008
|
/**
|
|
947
1009
|
* Changes a template's label. The stored document is immutable and stays untouched —
|
|
948
1010
|
* improving a template means saving a new one.
|
|
@@ -7,7 +7,7 @@ import type {
|
|
|
7
7
|
ProjectTemplateV1,
|
|
8
8
|
ProjectTemplateV1Entry,
|
|
9
9
|
} from "@milaboratories/pl-model-common";
|
|
10
|
-
import { PROJECT_TEMPLATE_SCHEMA_V1 } from "@milaboratories/pl-model-common";
|
|
10
|
+
import { PROJECT_TEMPLATE_SCHEMA_V1, withTemplateMeta } from "@milaboratories/pl-model-common";
|
|
11
11
|
import type { BlockPackSpec } from "@milaboratories/pl-model-middle-layer";
|
|
12
12
|
import type { BlockPackProvider } from "../model/template_resolve";
|
|
13
13
|
import type { ShareId } from "../model/sharing_model";
|
|
@@ -75,6 +75,50 @@ test("a rename changes the label and leaves the stored document byte-identical",
|
|
|
75
75
|
});
|
|
76
76
|
});
|
|
77
77
|
|
|
78
|
+
test("an imported template takes the file's description as its own, under a free name", async () => {
|
|
79
|
+
await withMl(async (ml) => {
|
|
80
|
+
const document = withTemplateMeta(documentOf(entry("a")), {
|
|
81
|
+
label: "Written in the file",
|
|
82
|
+
description: "What it builds",
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const first = await ml.importTemplate(document, { label: " Imported " });
|
|
86
|
+
const second = await ml.importTemplate(document, { label: "Imported" });
|
|
87
|
+
|
|
88
|
+
// The description moves to the template's own, editable one, and the label is the one asked
|
|
89
|
+
// for: the stored document keeps only the blocks, so there is one name and one description,
|
|
90
|
+
// and nothing for an edit to leave behind.
|
|
91
|
+
expect((await ml.getTemplateData(first)).document).toStrictEqual(documentOf(entry("a")));
|
|
92
|
+
|
|
93
|
+
const list = await awaitTemplateList(ml, (l) => l.length === 2);
|
|
94
|
+
expect(list.find((t) => t.id === first)).toMatchObject({
|
|
95
|
+
label: "Imported",
|
|
96
|
+
description: "What it builds",
|
|
97
|
+
blockCount: 1,
|
|
98
|
+
});
|
|
99
|
+
expect(list.find((t) => t.id === second)?.label).not.toBe("Imported");
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("an import into a folder deleted meanwhile lands at the top level, under a name free there", async () => {
|
|
104
|
+
await withMl(async (ml) => {
|
|
105
|
+
const document = documentOf(entry("a"));
|
|
106
|
+
const topLevel = await ml.importTemplate(document, { label: "Imported" });
|
|
107
|
+
|
|
108
|
+
const folder = await ml.createFolder("Gone");
|
|
109
|
+
const planned = await ml.previewFolderDeletion(folder);
|
|
110
|
+
expect(planned.ok).toBe(true);
|
|
111
|
+
if (!planned.ok) return;
|
|
112
|
+
expect((await ml.deleteFolder(folder, planned.removal)).ok).toBe(true);
|
|
113
|
+
|
|
114
|
+
const orphan = await ml.importTemplate(document, { label: "Imported", folder });
|
|
115
|
+
|
|
116
|
+
const list = await awaitTemplateList(ml, (l) => l.length === 2);
|
|
117
|
+
expect(list.find((t) => t.id === topLevel)?.label).toBe("Imported");
|
|
118
|
+
expect(list.find((t) => t.id === orphan)?.label).not.toBe("Imported");
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
78
122
|
test("an entry nothing can resolve creates no project, and every entry is reported", async () => {
|
|
79
123
|
await withMl(async (ml) => {
|
|
80
124
|
const stored = await storeTemplate(ml, "Nothing implements these", {
|
|
@@ -252,9 +296,9 @@ type StoredTemplate = { id: TemplateId; rid: SignedResourceId };
|
|
|
252
296
|
/**
|
|
253
297
|
* Stores one template through the mutator, on the same templates list the middle layer reads.
|
|
254
298
|
*
|
|
255
|
-
* The middle layer
|
|
256
|
-
*
|
|
257
|
-
* `copyShare` do.
|
|
299
|
+
* The middle layer's own import stores a document with no provenance, so a test that needs a
|
|
300
|
+
* specific stored template — a source project label, a sender — writes it here, exactly as
|
|
301
|
+
* `saveProjectAsTemplate` and `copyShare` do.
|
|
258
302
|
*/
|
|
259
303
|
async function storeTemplate(
|
|
260
304
|
ml: MiddleLayer,
|