@thatopen/services 0.22.0 → 0.24.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 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