@thatopen/services 0.22.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 +225 -1
- 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 +58 -0
- 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 +869 -666
- 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);
|
|
@@ -6850,6 +6965,7 @@ class ProjectHistory {
|
|
|
6850
6965
|
docs.push({
|
|
6851
6966
|
source: name.slice(0, dash),
|
|
6852
6967
|
docId: name.slice(dash + 1),
|
|
6968
|
+
folderId: idOf(folder),
|
|
6853
6969
|
modelName: model,
|
|
6854
6970
|
...hasAny ? {} : { empty: true }
|
|
6855
6971
|
});
|
|
@@ -6933,6 +7049,105 @@ class FlowDoc {
|
|
|
6933
7049
|
const response = await this.client.downloadFile(file.id);
|
|
6934
7050
|
return await response.blob();
|
|
6935
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
|
+
}
|
|
6936
7151
|
// ─── Writing versions ─────────────────────────────────────────────────────
|
|
6937
7152
|
/**
|
|
6938
7153
|
* Appends one version: uploads the geometry it carries, then the updated
|
|
@@ -7014,6 +7229,7 @@ class FlowDoc {
|
|
|
7014
7229
|
const indexFile = await this.#file(OPS_FILE);
|
|
7015
7230
|
const suggestion = {
|
|
7016
7231
|
opId: input.opId ?? mintGuid(),
|
|
7232
|
+
to: input.to ?? this.source,
|
|
7017
7233
|
title: input.title,
|
|
7018
7234
|
verb: input.verb,
|
|
7019
7235
|
author: input.author,
|
|
@@ -8356,7 +8572,15 @@ return main;`);
|
|
|
8356
8572
|
console.log(e);
|
|
8357
8573
|
});
|
|
8358
8574
|
}
|
|
8359
|
-
// ─── 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
|
+
}
|
|
8360
8584
|
/**
|
|
8361
8585
|
* A project's history of connected documents (Revit models, AutoCAD
|
|
8362
8586
|
* drawings…): their version timelines and their change requests. See
|