@thatopen/services 0.21.0 → 0.23.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/cli.js +268 -8
- package/dist/cli.js.map +1 -1
- package/dist/core/client.d.ts +7 -0
- package/dist/core/documents.d.ts +59 -0
- package/dist/core/history.d.ts +65 -0
- package/dist/core/recipes.d.ts +39 -5
- package/dist/index.cjs.js +2 -2
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.es.js +897 -662
- package/dist/index.es.js.map +1 -1
- package/docs/ai-quickstart.md +45 -5
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -6795,6 +6795,120 @@ Object.assign(lookup, {
|
|
|
6795
6795
|
io: lookup,
|
|
6796
6796
|
connect: lookup
|
|
6797
6797
|
});
|
|
6798
|
+
const DOCUMENT_SCHEMA = "thatopen/document@1";
|
|
6799
|
+
const DOCUMENT_FILE = "document.json";
|
|
6800
|
+
const versionTag$1 = () => `v-${Date.now()}`;
|
|
6801
|
+
function idOf$1(item) {
|
|
6802
|
+
return String(item._id ?? item.id ?? "");
|
|
6803
|
+
}
|
|
6804
|
+
class ProjectDocuments {
|
|
6805
|
+
constructor(client, projectId) {
|
|
6806
|
+
this.client = client;
|
|
6807
|
+
this.projectId = projectId;
|
|
6808
|
+
}
|
|
6809
|
+
/**
|
|
6810
|
+
* The document's descriptor, read and validated, or null when the folder
|
|
6811
|
+
* has none yet (an old folder, or a plain folder that is not a document).
|
|
6812
|
+
*/
|
|
6813
|
+
async descriptor(folderId) {
|
|
6814
|
+
const file = await this.#descriptorFile(folderId);
|
|
6815
|
+
if (!file) return null;
|
|
6816
|
+
const response = await this.client.downloadFile(file.id);
|
|
6817
|
+
const text2 = await response.text();
|
|
6818
|
+
if (!text2.trim()) return null;
|
|
6819
|
+
let parsed;
|
|
6820
|
+
try {
|
|
6821
|
+
parsed = JSON.parse(text2);
|
|
6822
|
+
} catch {
|
|
6823
|
+
return null;
|
|
6824
|
+
}
|
|
6825
|
+
return normalize$1(parsed);
|
|
6826
|
+
}
|
|
6827
|
+
/**
|
|
6828
|
+
* The first view whose format appears in `formats` — searched in the ORDER
|
|
6829
|
+
* OF THE LIST, so the caller's preference wins (ask `['frag', 'ifc']` and a
|
|
6830
|
+
* document carrying both answers the frag). Null when none matches: that is
|
|
6831
|
+
* where an interface offers "Process".
|
|
6832
|
+
*/
|
|
6833
|
+
async view(folderId, formats2) {
|
|
6834
|
+
const descriptor = await this.descriptor(folderId);
|
|
6835
|
+
if (!descriptor) return null;
|
|
6836
|
+
for (const format2 of formats2) {
|
|
6837
|
+
const match = descriptor.views.find((v) => v.format === format2);
|
|
6838
|
+
if (match) return match;
|
|
6839
|
+
}
|
|
6840
|
+
return null;
|
|
6841
|
+
}
|
|
6842
|
+
/**
|
|
6843
|
+
* Registers a renderable representation — a processor's last act. One view
|
|
6844
|
+
* per format: an existing view of the same format is replaced (a re-run
|
|
6845
|
+
* processor updates, it does not accumulate). The descriptor must exist;
|
|
6846
|
+
* create it first with {@link setDescriptor}.
|
|
6847
|
+
*/
|
|
6848
|
+
async addView(folderId, view) {
|
|
6849
|
+
const descriptor = await this.descriptor(folderId);
|
|
6850
|
+
if (!descriptor) {
|
|
6851
|
+
throw new Error(
|
|
6852
|
+
"This folder has no document descriptor yet: call setDescriptor first."
|
|
6853
|
+
);
|
|
6854
|
+
}
|
|
6855
|
+
descriptor.views = [
|
|
6856
|
+
...descriptor.views.filter((v) => v.format !== view.format),
|
|
6857
|
+
view
|
|
6858
|
+
];
|
|
6859
|
+
await this.setDescriptor(folderId, descriptor);
|
|
6860
|
+
}
|
|
6861
|
+
/** Writes the descriptor, creating or replacing `document.json`. */
|
|
6862
|
+
async setDescriptor(folderId, descriptor) {
|
|
6863
|
+
const value2 = {
|
|
6864
|
+
...descriptor,
|
|
6865
|
+
schema: DOCUMENT_SCHEMA,
|
|
6866
|
+
views: descriptor.views ?? []
|
|
6867
|
+
};
|
|
6868
|
+
const blob = new Blob([JSON.stringify(value2)], {
|
|
6869
|
+
type: "application/json"
|
|
6870
|
+
});
|
|
6871
|
+
const existing = await this.#descriptorFile(folderId);
|
|
6872
|
+
if (existing) {
|
|
6873
|
+
await this.client.updateFile(existing.id, {
|
|
6874
|
+
file: blob,
|
|
6875
|
+
versionTag: versionTag$1()
|
|
6876
|
+
});
|
|
6877
|
+
return;
|
|
6878
|
+
}
|
|
6879
|
+
await this.client.createFile({
|
|
6880
|
+
file: blob,
|
|
6881
|
+
name: DOCUMENT_FILE,
|
|
6882
|
+
versionTag: versionTag$1(),
|
|
6883
|
+
parentFolderId: folderId,
|
|
6884
|
+
projectId: this.projectId
|
|
6885
|
+
});
|
|
6886
|
+
}
|
|
6887
|
+
async #descriptorFile(folderId) {
|
|
6888
|
+
const items2 = await this.client.listFiles({ folderId });
|
|
6889
|
+
const found = items2.find(
|
|
6890
|
+
(item) => String(item.name ?? "") === DOCUMENT_FILE
|
|
6891
|
+
);
|
|
6892
|
+
return found ? { id: idOf$1(found) } : void 0;
|
|
6893
|
+
}
|
|
6894
|
+
}
|
|
6895
|
+
function normalize$1(parsed) {
|
|
6896
|
+
if (!parsed || typeof parsed !== "object") return null;
|
|
6897
|
+
const raw = parsed;
|
|
6898
|
+
if (raw.schema !== DOCUMENT_SCHEMA) return null;
|
|
6899
|
+
if (typeof raw.name !== "string") return null;
|
|
6900
|
+
const views = Array.isArray(raw.views) ? raw.views.filter(
|
|
6901
|
+
(v) => !!v && typeof v === "object" && typeof v.fileId === "string" && typeof v.format === "string"
|
|
6902
|
+
) : [];
|
|
6903
|
+
const native = raw.native;
|
|
6904
|
+
const hasNative = !!native && typeof native === "object" && typeof native.fileId === "string" && typeof native.format === "string";
|
|
6905
|
+
return {
|
|
6906
|
+
schema: DOCUMENT_SCHEMA,
|
|
6907
|
+
name: raw.name,
|
|
6908
|
+
...hasNative ? { native: { fileId: native.fileId, format: native.format } } : {},
|
|
6909
|
+
views
|
|
6910
|
+
};
|
|
6911
|
+
}
|
|
6798
6912
|
class StaleHistoryError extends Error {
|
|
6799
6913
|
constructor(expectedGuid, actualGuid) {
|
|
6800
6914
|
super(
|
|
@@ -6811,6 +6925,7 @@ const OPS_FILE = "revitflow_ops.json";
|
|
|
6811
6925
|
const OPS_SCHEMA = "revitflow/ops@1";
|
|
6812
6926
|
const LEGACY_MODEL_FILE = "revitflow_model.frag";
|
|
6813
6927
|
const INTERNAL_PREFIX = "revitflow_";
|
|
6928
|
+
const LOCKS_ANCHOR = "bimterop-locks-anchor";
|
|
6814
6929
|
const versionTag = () => `v-${Date.now()}`;
|
|
6815
6930
|
function childrenOf(folders, parentId) {
|
|
6816
6931
|
return folders.filter((f) => String(f.parentId ?? "") === parentId);
|
|
@@ -6837,10 +6952,22 @@ class ProjectHistory {
|
|
|
6837
6952
|
const name = String(folder.name ?? "");
|
|
6838
6953
|
const dash = name.indexOf("-");
|
|
6839
6954
|
if (dash <= 0) continue;
|
|
6955
|
+
const files = await this.client.listFiles({
|
|
6956
|
+
folderId: idOf(folder)
|
|
6957
|
+
});
|
|
6958
|
+
const names2 = files.map(
|
|
6959
|
+
(f) => String(f.name ?? "")
|
|
6960
|
+
);
|
|
6961
|
+
const model = names2.find((n) => n === LEGACY_MODEL_FILE) ?? names2.find(
|
|
6962
|
+
(n) => n.endsWith(".frag") && !n.startsWith(INTERNAL_PREFIX)
|
|
6963
|
+
);
|
|
6964
|
+
const hasAny = names2.includes(HISTORY_FILE) || names2.includes(OPS_FILE) || !!model;
|
|
6840
6965
|
docs.push({
|
|
6841
6966
|
source: name.slice(0, dash),
|
|
6842
6967
|
docId: name.slice(dash + 1),
|
|
6843
|
-
|
|
6968
|
+
folderId: idOf(folder),
|
|
6969
|
+
modelName: model,
|
|
6970
|
+
...hasAny ? {} : { empty: true }
|
|
6844
6971
|
});
|
|
6845
6972
|
}
|
|
6846
6973
|
return docs;
|
|
@@ -6922,6 +7049,105 @@ class FlowDoc {
|
|
|
6922
7049
|
const response = await this.client.downloadFile(file.id);
|
|
6923
7050
|
return await response.blob();
|
|
6924
7051
|
}
|
|
7052
|
+
/** The document folder's item id — the id the CDE knows this document by. */
|
|
7053
|
+
async folderId() {
|
|
7054
|
+
return await this.#folder(false);
|
|
7055
|
+
}
|
|
7056
|
+
// ─── The document descriptor ───────────────────────────────────────────────
|
|
7057
|
+
/**
|
|
7058
|
+
* The document descriptor (`document.json`), or null when the folder does
|
|
7059
|
+
* not carry one yet — an old folder from before descriptors existed.
|
|
7060
|
+
*/
|
|
7061
|
+
async descriptor() {
|
|
7062
|
+
return await this.#documents.descriptor(await this.#folder(false));
|
|
7063
|
+
}
|
|
7064
|
+
/**
|
|
7065
|
+
* The first view whose format appears in `formats`, searched in the order
|
|
7066
|
+
* of the list (the caller's preference wins). Falls back to the visible
|
|
7067
|
+
* model .frag on a descriptor-less folder, so old documents keep viewing.
|
|
7068
|
+
*/
|
|
7069
|
+
async view(formats2) {
|
|
7070
|
+
const folderId = await this.#folder(false);
|
|
7071
|
+
const descriptor = await this.#documents.descriptor(folderId);
|
|
7072
|
+
if (descriptor) {
|
|
7073
|
+
for (const format2 of formats2) {
|
|
7074
|
+
const match = descriptor.views.find((v) => v.format === format2);
|
|
7075
|
+
if (match) return match;
|
|
7076
|
+
}
|
|
7077
|
+
return null;
|
|
7078
|
+
}
|
|
7079
|
+
if (!formats2.includes("frag")) return null;
|
|
7080
|
+
const model = await this.#modelFile();
|
|
7081
|
+
return model ? { fileId: model.id, format: "frag" } : null;
|
|
7082
|
+
}
|
|
7083
|
+
/**
|
|
7084
|
+
* Registers a renderable representation, creating the descriptor when the
|
|
7085
|
+
* folder predates them (named after the model file, or the folder itself).
|
|
7086
|
+
* One view per format: a same-format view is replaced, not accumulated.
|
|
7087
|
+
*/
|
|
7088
|
+
async addView(view) {
|
|
7089
|
+
const folderId = await this.#folder(true);
|
|
7090
|
+
const descriptor = await this.#documents.descriptor(folderId);
|
|
7091
|
+
if (descriptor) {
|
|
7092
|
+
await this.#documents.addView(folderId, view);
|
|
7093
|
+
return;
|
|
7094
|
+
}
|
|
7095
|
+
const model = await this.#modelFile();
|
|
7096
|
+
await this.#documents.setDescriptor(folderId, {
|
|
7097
|
+
name: model?.name.replace(/\.frag$/, "") ?? this.#folderName,
|
|
7098
|
+
views: [view]
|
|
7099
|
+
});
|
|
7100
|
+
}
|
|
7101
|
+
// ─── Ownership (element locks) ─────────────────────────────────────────────
|
|
7102
|
+
/**
|
|
7103
|
+
* Who currently holds which elements, read from the lock registry the
|
|
7104
|
+
* native plugins arbitrate through. Read-only: claiming and releasing stay
|
|
7105
|
+
* with the plugins. Empty when nobody holds anything (or the document has
|
|
7106
|
+
* no lock anchor yet). Arbitration is applied: a key contested by two
|
|
7107
|
+
* claims counts only for the earliest one, matching the plugins' rule.
|
|
7108
|
+
*/
|
|
7109
|
+
async ownership() {
|
|
7110
|
+
const anchor = await this.#file(LOCKS_ANCHOR);
|
|
7111
|
+
if (!anchor) return [];
|
|
7112
|
+
const metas = await this.client.getHiddenFilesByParent(anchor.id);
|
|
7113
|
+
const claims = await Promise.all(
|
|
7114
|
+
metas.map(async (meta) => {
|
|
7115
|
+
try {
|
|
7116
|
+
const id2 = idOf(meta);
|
|
7117
|
+
const response = await this.client.downloadHiddenFile(id2);
|
|
7118
|
+
const parsed = JSON.parse(await response.text());
|
|
7119
|
+
if (!parsed || typeof parsed !== "object") return null;
|
|
7120
|
+
if (!("revitflowlock" in parsed)) return null;
|
|
7121
|
+
if (!Array.isArray(parsed.keys)) return null;
|
|
7122
|
+
return {
|
|
7123
|
+
owner: String(parsed.user ?? ""),
|
|
7124
|
+
keys: parsed.keys.filter((k) => typeof k === "string")
|
|
7125
|
+
};
|
|
7126
|
+
} catch {
|
|
7127
|
+
return null;
|
|
7128
|
+
}
|
|
7129
|
+
})
|
|
7130
|
+
);
|
|
7131
|
+
const taken = /* @__PURE__ */ new Set();
|
|
7132
|
+
const byOwner = /* @__PURE__ */ new Map();
|
|
7133
|
+
for (const claim of claims) {
|
|
7134
|
+
if (!claim) continue;
|
|
7135
|
+
for (const key of claim.keys) {
|
|
7136
|
+
if (!key || taken.has(key)) continue;
|
|
7137
|
+
taken.add(key);
|
|
7138
|
+
const list = byOwner.get(claim.owner) ?? [];
|
|
7139
|
+
list.push(key);
|
|
7140
|
+
byOwner.set(claim.owner, list);
|
|
7141
|
+
}
|
|
7142
|
+
}
|
|
7143
|
+
return [...byOwner.entries()].map(([owner, elements]) => ({
|
|
7144
|
+
owner,
|
|
7145
|
+
elements
|
|
7146
|
+
}));
|
|
7147
|
+
}
|
|
7148
|
+
get #documents() {
|
|
7149
|
+
return new ProjectDocuments(this.client, this.project.projectId);
|
|
7150
|
+
}
|
|
6925
7151
|
// ─── Writing versions ─────────────────────────────────────────────────────
|
|
6926
7152
|
/**
|
|
6927
7153
|
* Appends one version: uploads the geometry it carries, then the updated
|
|
@@ -7003,6 +7229,7 @@ class FlowDoc {
|
|
|
7003
7229
|
const indexFile = await this.#file(OPS_FILE);
|
|
7004
7230
|
const suggestion = {
|
|
7005
7231
|
opId: input.opId ?? mintGuid(),
|
|
7232
|
+
to: input.to ?? this.source,
|
|
7006
7233
|
title: input.title,
|
|
7007
7234
|
verb: input.verb,
|
|
7008
7235
|
author: input.author,
|
|
@@ -7260,14 +7487,39 @@ class Recipes {
|
|
|
7260
7487
|
return this.#registry.get(name);
|
|
7261
7488
|
}
|
|
7262
7489
|
/**
|
|
7263
|
-
* Runs a recipe WITHOUT queueing anything: the dry pass. Returns
|
|
7264
|
-
*
|
|
7265
|
-
*
|
|
7266
|
-
*
|
|
7490
|
+
* Runs a recipe WITHOUT queueing anything: the dry pass. Returns a
|
|
7491
|
+
* {@link RecipePlan} — the result to show (title, tallies, targets) plus
|
|
7492
|
+
* everything {@link queue} needs to queue EXACTLY this, payload and opId
|
|
7493
|
+
* included, with no second run. Discard it and the plan never happened.
|
|
7267
7494
|
*/
|
|
7268
7495
|
async plan(name, options) {
|
|
7269
|
-
const { result } = await this.#execute(name, options);
|
|
7270
|
-
return
|
|
7496
|
+
const { base, opId, result } = await this.#execute(name, options);
|
|
7497
|
+
return {
|
|
7498
|
+
opId,
|
|
7499
|
+
projectId: options.projectId,
|
|
7500
|
+
target: options.target,
|
|
7501
|
+
...base?.guid ? { baseCommitGuid: base.guid } : {},
|
|
7502
|
+
result
|
|
7503
|
+
};
|
|
7504
|
+
}
|
|
7505
|
+
/**
|
|
7506
|
+
* Queues exactly what {@link plan} planned: same opId, same payload, no
|
|
7507
|
+
* second execution. The pair plan → confirm → queue is the UI flow; `run`
|
|
7508
|
+
* is the one-shot shortcut.
|
|
7509
|
+
*/
|
|
7510
|
+
async queue(plan, options) {
|
|
7511
|
+
const targetDoc = this.client.history(plan.projectId).doc(plan.target.source, plan.target.docId);
|
|
7512
|
+
const suggestion = await targetDoc.suggest({
|
|
7513
|
+
opId: plan.opId,
|
|
7514
|
+
title: plan.result.title,
|
|
7515
|
+
verb: plan.result.verb,
|
|
7516
|
+
author: options.author,
|
|
7517
|
+
payload: plan.result.payload,
|
|
7518
|
+
targets: plan.result.targets,
|
|
7519
|
+
userId: options.userId,
|
|
7520
|
+
baseCommitGuid: plan.baseCommitGuid
|
|
7521
|
+
});
|
|
7522
|
+
return { suggestion, result: plan.result };
|
|
7271
7523
|
}
|
|
7272
7524
|
/**
|
|
7273
7525
|
* Runs a recipe and queues its suggestion on the target document. Safe to
|
|
@@ -8320,7 +8572,15 @@ return main;`);
|
|
|
8320
8572
|
console.log(e);
|
|
8321
8573
|
});
|
|
8322
8574
|
}
|
|
8323
|
-
// ─── History & Recipes
|
|
8575
|
+
// ─── Documents, History & Recipes ────────────────────────────────
|
|
8576
|
+
/**
|
|
8577
|
+
* A project's documents. Every document is a FOLDER holding its files plus
|
|
8578
|
+
* a `document.json` descriptor naming the document, its native form and its
|
|
8579
|
+
* renderable views. See `src/core/documents.ts` for the model and schema.
|
|
8580
|
+
*/
|
|
8581
|
+
documents(projectId) {
|
|
8582
|
+
return new ProjectDocuments(this, projectId);
|
|
8583
|
+
}
|
|
8324
8584
|
/**
|
|
8325
8585
|
* A project's history of connected documents (Revit models, AutoCAD
|
|
8326
8586
|
* drawings…): their version timelines and their change requests. See
|