@qaecy/cue-cli 0.0.48 → 0.0.49

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.
Files changed (3) hide show
  1. package/main.js +886 -598
  2. package/package.json +1 -1
  3. package/readme.md +1 -0
package/main.js CHANGED
@@ -5141,6 +5141,7 @@ var ENDPOINT_COMMANDS_PROFILE_API_KEYS = "/commands/admin/profile/api-keys";
5141
5141
  var ENDPOINT_COMMANDS_PROFILE_TERMS = "/commands/admin/profile/terms";
5142
5142
  var ENDPOINT_ORG_MEMBERS = (orgId) => `/data-views/admin/organizations/${orgId}/members`;
5143
5143
  var ENDPOINT_CREATE_PROJECT = "/commands/admin/project";
5144
+ var ENDPOINT_DELETE_PROJECT = (projectId) => `/commands/admin/project/${projectId}`;
5144
5145
  var ENDPOINT_SEARCH = "/assistant/search";
5145
5146
  var ENDPOINT_FUSEKI_QUERY = "/triplestore/query";
5146
5147
  var ENDPOINT_FUSEKI_UPDATE = "/triplestore/update";
@@ -5268,6 +5269,11 @@ var CueAuth = class {
5268
5269
  const result = await (0, import_auth3.signInWithCustomToken)(this._auth, token);
5269
5270
  return result.user;
5270
5271
  }
5272
+ /** Sign in with a Firebase custom token (e.g. minted server-side for MCP/OAuth flows). */
5273
+ async signInWithCustomToken(token) {
5274
+ const result = await (0, import_auth3.signInWithCustomToken)(this._auth, token);
5275
+ return result.user;
5276
+ }
5271
5277
  /** Sign out the current user */
5272
5278
  async signOut() {
5273
5279
  await (0, import_auth3.signOut)(this._auth);
@@ -5422,6 +5428,55 @@ var CueTables = class {
5422
5428
  }
5423
5429
  };
5424
5430
 
5431
+ // libs/js/cue-sdk/src/lib/extraction.ts
5432
+ var ENDPOINT_EXTRACTION = "/semantic-extraction/extract";
5433
+ var CueExtraction = class {
5434
+ constructor(_auth, _gatewayUrl) {
5435
+ this._auth = _auth;
5436
+ this._gatewayUrl = _gatewayUrl;
5437
+ }
5438
+ /**
5439
+ * Run semantic extraction on a document page image.
5440
+ *
5441
+ * Sends a multipart/form-data POST to `/semantic-extraction/extract`.
5442
+ * Always requests JSON-LD so the result can be displayed directly in
5443
+ * `cue-rdf-graph` without any further parsing.
5444
+ */
5445
+ async extract(request) {
5446
+ const fmt = request.rdfFormat ?? "json-ld";
5447
+ const body = new FormData();
5448
+ body.append("file", request.image, "page.png");
5449
+ body.append("template", JSON.stringify(request.template));
5450
+ body.append("space_id", request.projectId);
5451
+ body.append("rdf_format", fmt);
5452
+ if (request.category)
5453
+ body.append("category", request.category);
5454
+ if (request.text)
5455
+ body.append("text", request.text);
5456
+ const response = await this._auth.authenticatedFetch(
5457
+ `${this._gatewayUrl}${ENDPOINT_EXTRACTION}`,
5458
+ {
5459
+ method: "POST",
5460
+ // Do NOT set Content-Type; browser sets it with the correct boundary.
5461
+ headers: {
5462
+ "Accept": "application/ld+json",
5463
+ "x-project-id": request.projectId,
5464
+ "cue-project-id": request.projectId
5465
+ },
5466
+ body
5467
+ }
5468
+ );
5469
+ if (!response.ok) {
5470
+ const msg = await response.text().catch(() => "");
5471
+ throw new Error(
5472
+ `Extraction failed: ${response.status} ${response.statusText}${msg ? " \u2014 " + msg.slice(0, 300) : ""}`
5473
+ );
5474
+ }
5475
+ const jsonld = await response.json();
5476
+ return { jsonld };
5477
+ }
5478
+ };
5479
+
5425
5480
  // libs/js/cue-sdk/src/lib/api.ts
5426
5481
  var CueApi = class {
5427
5482
  constructor(_auth, _gatewayUrl, projects, sync) {
@@ -5430,8 +5485,11 @@ var CueApi = class {
5430
5485
  this.projects = projects;
5431
5486
  this.sync = sync;
5432
5487
  this.tables = new CueTables(_auth, _gatewayUrl);
5488
+ this.extraction = new CueExtraction(_auth, _gatewayUrl);
5433
5489
  }
5434
5490
  tables;
5491
+ /** Semantic extraction client — call document pages against a SemanticTemplate. */
5492
+ extraction;
5435
5493
  /** Active language used for language-sensitive SPARQL queries across all project classes. */
5436
5494
  language = "en";
5437
5495
  /** Updates the active language. All project classes (`CueProjectSchema`, `CueProjectDocuments`, `CueProjectEntities`) read this at query time. */
@@ -7394,6 +7452,19 @@ var CueProjects = class {
7394
7452
  const fn = (0, import_functions2.httpsCallable)(this._functions, "removeUserFromProject");
7395
7453
  await fn({ uid, spaceId: projectId });
7396
7454
  }
7455
+ /**
7456
+ * Delete a project by ID. Requires superadmin privileges on the server.
7457
+ */
7458
+ async deleteProject(projectId) {
7459
+ const response = await this._auth.authenticatedFetch(
7460
+ `${this._gatewayUrl}${ENDPOINT_DELETE_PROJECT(projectId)}`,
7461
+ { method: "DELETE" }
7462
+ );
7463
+ if (!response.ok) {
7464
+ const body = await response.text().catch(() => "");
7465
+ throw new Error(`Failed to delete project: ${response.status} ${response.statusText}${body ? ` \u2014 ${body}` : ""}`);
7466
+ }
7467
+ }
7397
7468
  };
7398
7469
 
7399
7470
  // libs/js/cue-sdk/src/lib/profile.ts
@@ -7567,6 +7638,7 @@ var REQUIRED_ROLES = {
7567
7638
  createProvider: "superadmin",
7568
7639
  changeContentCategories: "syncer",
7569
7640
  deleteDocuments: "superadmin",
7641
+ deleteProject: "admin",
7570
7642
  deleteUserFromProject: "admin",
7571
7643
  downloadDocuments: "member",
7572
7644
  editContentCategories: "syncer",
@@ -7581,8 +7653,10 @@ function defaultPrivileges() {
7581
7653
  return {
7582
7654
  changeContentCategories: false,
7583
7655
  createEntities: false,
7656
+ createProject: false,
7584
7657
  createProvider: false,
7585
7658
  deleteDocuments: false,
7659
+ deleteProject: false,
7586
7660
  deleteUserFromProject: false,
7587
7661
  downloadDocuments: false,
7588
7662
  editContentCategories: false,
@@ -7598,18 +7672,31 @@ var CuePrivileges = class {
7598
7672
  constructor(_isSuperAdmin) {
7599
7673
  this._isSuperAdmin = _isSuperAdmin;
7600
7674
  this._projectRoles = new CueSignal([]);
7675
+ this._orgRole = new CueSignal(null);
7601
7676
  this.privileges = cueComputed(
7602
- [this._projectRoles, _isSuperAdmin],
7677
+ [this._projectRoles, this._orgRole, _isSuperAdmin],
7603
7678
  () => this._compute()
7604
7679
  );
7605
7680
  }
7606
7681
  _projectRoles;
7682
+ _orgRole;
7607
7683
  /**
7608
7684
  * Reactive signal — current user's privileges for the selected project.
7609
7685
  * Recomputes automatically when `setProjectRoles()` is called or when
7610
7686
  * the `isSuperAdmin` signal changes.
7611
7687
  */
7612
7688
  privileges;
7689
+ /**
7690
+ * Set the user's role within the organisation that owns the selected project.
7691
+ * Pass `null` to clear the organisation role (e.g. when deselecting a project).
7692
+ *
7693
+ * Only organisation admins (and superadmins) may create projects.
7694
+ */
7695
+ setOrgRole(role) {
7696
+ if (this._orgRole.get() !== role) {
7697
+ this._orgRole.set(role);
7698
+ }
7699
+ }
7613
7700
  /**
7614
7701
  * Set the user's roles for the currently selected project.
7615
7702
  *
@@ -7636,10 +7723,12 @@ var CuePrivileges = class {
7636
7723
  }
7637
7724
  _compute() {
7638
7725
  const roles = this._projectRoles.get();
7726
+ const isSuperAdmin = this._isSuperAdmin.get();
7639
7727
  const result = defaultPrivileges();
7640
7728
  for (const key of Object.keys(REQUIRED_ROLES)) {
7641
7729
  result[key] = roles.includes(REQUIRED_ROLES[key]);
7642
7730
  }
7731
+ result.createProject = isSuperAdmin || this._orgRole.get() === "admin";
7643
7732
  return result;
7644
7733
  }
7645
7734
  };
@@ -7892,11 +7981,12 @@ GROUP BY ?iri ?parent`;
7892
7981
  var OSM_ENDPOINT = "https://qlever.dev/api/osm-planet";
7893
7982
  var EXCLUDE_OSM_RELATION = true;
7894
7983
  var CueProjectEntities = class {
7895
- constructor(_api, _projectId, rdfBase = RESOURCE_BASE, _queryCache, _graphType) {
7984
+ constructor(_api, _projectId, rdfBase = RESOURCE_BASE, _queryCache, _graphType, _verbose = false) {
7896
7985
  this._api = _api;
7897
7986
  this._projectId = _projectId;
7898
7987
  this._queryCache = _queryCache;
7899
7988
  this._graphType = _graphType;
7989
+ this._verbose = _verbose;
7900
7990
  this.baseURL = `${rdfBase}${_projectId}/`;
7901
7991
  this.entityInfoMap = this._entityInfoMapComputed;
7902
7992
  this.entityGraph = this._entityGraph.asReadonly();
@@ -7916,6 +8006,8 @@ var CueProjectEntities = class {
7916
8006
  _entityOSMMap = new CueSignal({});
7917
8007
  _osmWKTMap = new CueSignal({});
7918
8008
  _fetchingOSMIds = /* @__PURE__ */ new Set();
8009
+ /** Cumulative unique entity UUIDs ever passed to request methods (survives cache hits). */
8010
+ _seenIds = /* @__PURE__ */ new Set();
7919
8011
  _entityGraph = new CueSignal(void 0);
7920
8012
  // ── Derived signals ────────────────────────────────────────────────────────
7921
8013
  _entityInfoMapComputed = cueComputed(
@@ -7940,6 +8032,15 @@ var CueProjectEntities = class {
7940
8032
  entityIri(uuid) {
7941
8033
  return `${this.baseURL}${uuid}`;
7942
8034
  }
8035
+ /** @internal Builds a full resource IRI from a UUID without a SPARQL round-trip. */
8036
+ _resourceIri(uuid) {
8037
+ return `${this.baseURL}${uuid}`;
8038
+ }
8039
+ _log(message) {
8040
+ if (this._verbose) {
8041
+ console.debug(`[CueProjectEntities pid=${this._projectId}] ${message}`);
8042
+ }
8043
+ }
7943
8044
  /**
7944
8045
  * Resets all entity state and re-fetches the entity graph.
7945
8046
  * Call when the active project changes.
@@ -7952,6 +8053,7 @@ var CueProjectEntities = class {
7952
8053
  this._osmWKTMap.set({});
7953
8054
  this._entityGraph.set(void 0);
7954
8055
  this._fetchingOSMIds.clear();
8056
+ this._seenIds.clear();
7955
8057
  this._fetchEntityGraph().catch(
7956
8058
  (err) => console.error(
7957
8059
  "[CueProjectEntities] Entity graph fetch failed after reset:",
@@ -7967,11 +8069,14 @@ var CueProjectEntities = class {
7967
8069
  * Data is merged into `entityInfoMap` once the SPARQL response arrives.
7968
8070
  */
7969
8071
  requestEntityData(uuids, includeMentionCount = false) {
8072
+ for (const id of uuids)
8073
+ this._seenIds.add(id);
7970
8074
  const newUUIDs = uuids.filter(
7971
8075
  (id) => this._entityDetails.get()[id] === void 0
7972
8076
  );
7973
8077
  if (newUUIDs.length === 0)
7974
8078
  return;
8079
+ this._log(`requestEntityData: ${newUUIDs.length} new / ${uuids.length} requested | cumulative: ${this._seenIds.size} seen`);
7975
8080
  const values = newUUIDs.map((id) => `r:${id}`).join(" ");
7976
8081
  const q = `PREFIX qcy: <${qaecyPrefixes["qcy"]}>
7977
8082
  PREFIX r: <${this.baseURL}>
@@ -8001,6 +8106,7 @@ GROUP BY ?id ?mentionCount`;
8001
8106
  };
8002
8107
  });
8003
8108
  this._entityDetails.set(updates);
8109
+ this._log(`entityDetails: ${Object.keys(updates).length} with metadata / ${this._seenIds.size} seen (cumulative)`);
8004
8110
  }).catch(
8005
8111
  (err) => console.error("[CueProjectEntities] requestEntityData failed:", err)
8006
8112
  );
@@ -8018,6 +8124,9 @@ GROUP BY ?id ?mentionCount`;
8018
8124
  );
8019
8125
  if (newUUIDs.length === 0)
8020
8126
  return;
8127
+ for (const id of uuids)
8128
+ this._seenIds.add(id);
8129
+ this._log(`requestEntityLocations: ${newUUIDs.length} new / ${uuids.length} requested | cumulative: ${this._seenIds.size} seen`);
8021
8130
  const osmInit = { ...this._entityOSMMap.get() };
8022
8131
  for (const id of newUUIDs)
8023
8132
  osmInit[id] = { direct: [], indirect: [] };
@@ -8074,6 +8183,7 @@ WHERE {
8074
8183
  update[id] = entry;
8075
8184
  });
8076
8185
  this._entityOSMMap.set(update);
8186
+ this._log(`entityOSMMap: ${Object.keys(update).length} with OSM / ${this._seenIds.size} seen (cumulative)`);
8077
8187
  }
8078
8188
  /**
8079
8189
  * Fetches incoming and outgoing relationships for a single entity IRI.
@@ -8164,7 +8274,51 @@ ORDER BY ${orderByOccurences ? "DESC(?count)" : "ASC(?label)"}`;
8164
8274
  };
8165
8275
  });
8166
8276
  }
8167
- async buildSummaryGraph(format) {
8277
+ async entitiesByCategory(categoryIris, includeMetadata = false) {
8278
+ if (categoryIris.length === 0)
8279
+ return [];
8280
+ const ce = CompactExpand.getInstance();
8281
+ const normalise = (iri) => iri.startsWith("http://") || iri.startsWith("https://") ? iri : ce.expandIRI(iri);
8282
+ const values = categoryIris.map((c) => `<${normalise(c)}>`).join(" ");
8283
+ const q = includeMetadata ? `PREFIX qcy: <${qaecyPrefixes["qcy"]}>
8284
+ SELECT ?id (SAMPLE(?val) AS ?value) (GROUP_CONCAT(DISTINCT STR(?allCat); SEPARATOR=";") AS ?categories)
8285
+ WHERE {
8286
+ VALUES ?filterCat { ${values} }
8287
+ ?iri a qcy:CanonicalEntity ;
8288
+ qcy:hasEntityCategory ?filterCat ;
8289
+ qcy:value ?val ;
8290
+ qcy:hasEntityCategory ?allCat .
8291
+ BIND(REPLACE(STR(?iri), "^.*/", "") AS ?id)
8292
+ }
8293
+ GROUP BY ?id` : `PREFIX qcy: <${qaecyPrefixes["qcy"]}>
8294
+ SELECT DISTINCT ?id
8295
+ WHERE {
8296
+ VALUES ?filterCat { ${values} }
8297
+ ?iri a qcy:CanonicalEntity ;
8298
+ qcy:hasEntityCategory ?filterCat .
8299
+ BIND(REPLACE(STR(?iri), "^.*/", "") AS ?id)
8300
+ }`;
8301
+ const data = await this._api.sparql(
8302
+ q,
8303
+ this._projectId,
8304
+ this._graphType
8305
+ );
8306
+ return data.results.bindings.filter((b) => b["id"]).map((b) => {
8307
+ const uuid = b["id"].value;
8308
+ const base = { iri: this._resourceIri(uuid), uuid };
8309
+ if (!includeMetadata)
8310
+ return base;
8311
+ return {
8312
+ ...base,
8313
+ value: b["value"]?.value ?? "",
8314
+ categories: b["categories"]?.value?.split(";").filter(Boolean) ?? []
8315
+ };
8316
+ });
8317
+ }
8318
+ async buildSummaryGraph(format, entityIRI) {
8319
+ const ce = CompactExpand.getInstance();
8320
+ const expandedIRI = entityIRI ? entityIRI.includes("://") ? entityIRI : ce.expandIRI(entityIRI) : void 0;
8321
+ const neighbourFilter = expandedIRI ? ` FILTER(?sourceCat = <${expandedIRI}> || ?targetCat = <${expandedIRI}>)` : "";
8168
8322
  const q = `PREFIX qcy: <${qaecyPrefixes["qcy"]}>
8169
8323
  SELECT
8170
8324
  ?sourceCat
@@ -8179,6 +8333,7 @@ WHERE {
8179
8333
  ?s ?predicate ?o .
8180
8334
  FILTER(isIRI(?s) && isIRI(?o))
8181
8335
  FILTER(?predicate != qcy:relatedEntity)
8336
+ ${neighbourFilter}
8182
8337
  }
8183
8338
  GROUP BY ?sourceCat ?predicate ?targetCat
8184
8339
  ORDER BY DESC(?weight)`;
@@ -8208,7 +8363,6 @@ ORDER BY DESC(?weight)`;
8208
8363
  };
8209
8364
  }
8210
8365
  if (format === "md") {
8211
- const ce = CompactExpand.getInstance();
8212
8366
  const rows = bindings.map((b) => ({
8213
8367
  src: ce.compactIRI(b["sourceCat"].value),
8214
8368
  pred: ce.compactIRI(b["predicate"].value),
@@ -8453,557 +8607,6 @@ SELECT * WHERE {
8453
8607
  }
8454
8608
  };
8455
8609
 
8456
- // libs/js/cue-sdk/src/lib/documents.ts
8457
- var CueProjectDocuments = class {
8458
- constructor(_api, _projectId, language, rdfBase = RESOURCE_BASE, _queryCache, _graphType) {
8459
- this._api = _api;
8460
- this._projectId = _projectId;
8461
- this._queryCache = _queryCache;
8462
- this._graphType = _graphType;
8463
- this.baseURL = `${rdfBase}${_projectId}/`;
8464
- this._currentLang = language ?? this._api.language;
8465
- this.documentInfoMap = this._documentInfoMap.asReadonly();
8466
- this.projectDocumentsData = this._projectDocumentsData.asReadonly();
8467
- }
8468
- /** Full RDF base URL for this project, e.g. `https://cue.qaecy.com/r/{pid}/` */
8469
- baseURL;
8470
- /** Tracks the language for which `_documentInfoMap` is currently populated. */
8471
- _currentLang;
8472
- _documentInfoMap = new CueSignal({});
8473
- _projectDocumentsData = new CueSignal({
8474
- duplicateCount: 0,
8475
- documentsBySuffix: {},
8476
- documentsByContentCategory: {}
8477
- });
8478
- /** Lazily populated per-document detail map. */
8479
- documentInfoMap;
8480
- /** Project-level document overview (grouped counts + sizes). */
8481
- projectDocumentsData;
8482
- // ── Lifecycle ──────────────────────────────────────────────────────────────
8483
- /**
8484
- * Resets all document state. Call when the active project changes.
8485
- * Follow with `fetchOverview()` once the triplestore is ready.
8486
- */
8487
- reset() {
8488
- this._documentInfoMap.set({});
8489
- this._projectDocumentsData.set({
8490
- duplicateCount: 0,
8491
- documentsBySuffix: {},
8492
- documentsByContentCategory: {}
8493
- });
8494
- }
8495
- /**
8496
- * Updates the active language and clears the document info map so that
8497
- * language-sensitive fields (subject, summary) are re-fetched on the next
8498
- * `requestDocumentData()` call.
8499
- */
8500
- setLanguage(lang) {
8501
- if (this._currentLang === lang)
8502
- return;
8503
- this._currentLang = lang;
8504
- this._api.setLanguage(lang);
8505
- this._documentInfoMap.set({});
8506
- }
8507
- // ── Public API ─────────────────────────────────────────────────────────────
8508
- /**
8509
- * Fetches the three-part project overview (by suffix, by content category,
8510
- * duplicate count) in parallel and writes them as a single atomic update to
8511
- * `projectDocumentsData`. Safe to call again to refresh.
8512
- */
8513
- async fetchOverview() {
8514
- this._projectDocumentsData.set({
8515
- duplicateCount: 0,
8516
- documentsBySuffix: {},
8517
- documentsByContentCategory: {}
8518
- });
8519
- const qSuffix = this._buildDocumentsBySuffixQuery();
8520
- const qCategory = this._buildDocumentsByContentCategoryQuery();
8521
- const qDuplicates = this._buildDuplicateCountQuery();
8522
- await staleWhileRevalidate(
8523
- qSuffix + qCategory + qDuplicates,
8524
- async () => {
8525
- const [bySuffix, byCategory, duplicateCount] = await Promise.all([
8526
- this._runDocumentsBySuffixQuery(qSuffix),
8527
- this._runDocumentsByContentCategoryQuery(qCategory),
8528
- this._runDuplicateCountQuery(qDuplicates)
8529
- ]);
8530
- return { duplicateCount, documentsBySuffix: bySuffix, documentsByContentCategory: byCategory };
8531
- },
8532
- (overview) => this._projectDocumentsData.set(overview),
8533
- this._queryCache
8534
- );
8535
- }
8536
- /**
8537
- * Lazily batch-fetches core metadata for the given document UUIDs.
8538
- * Already-cached UUIDs are skipped. Data is merged into `documentInfoMap`
8539
- * once the SPARQL response arrives.
8540
- */
8541
- requestDocumentData(uuids) {
8542
- const newUUIDs = uuids.filter((id) => this._documentInfoMap.get()[id] === void 0);
8543
- if (newUUIDs.length === 0)
8544
- return;
8545
- this._fetchDocumentInfoBatch(newUUIDs).catch(
8546
- (err) => console.error("[CueProjectDocuments] requestDocumentData failed:", err)
8547
- );
8548
- }
8549
- /**
8550
- * Promise-based alternative to {@link requestDocumentData} for non-reactive contexts.
8551
- *
8552
- * Resolves with the `DocumentInfo` entries for every requested UUID once the
8553
- * SPARQL response arrives. UUIDs already present in the cache are returned
8554
- * immediately without a network request. The result is also written into
8555
- * `documentInfoMap` so reactive consumers stay in sync.
8556
- *
8557
- * UUIDs not found in the triplestore are omitted from the returned map.
8558
- *
8559
- * @example
8560
- * ```ts
8561
- * const docs = await cueProjectDocs.fetchDocumentData(['uuid1', 'uuid2']);
8562
- * console.log(docs['uuid1'].subject);
8563
- * ```
8564
- */
8565
- async fetchDocumentData(uuids) {
8566
- const current = this._documentInfoMap.get();
8567
- const newUUIDs = uuids.filter((id) => current[id] === void 0);
8568
- if (newUUIDs.length > 0) {
8569
- await this._fetchDocumentInfoBatch(newUUIDs);
8570
- }
8571
- const updated = this._documentInfoMap.get();
8572
- return Object.fromEntries(
8573
- uuids.filter((id) => updated[id] !== void 0).map((id) => [id, updated[id]])
8574
- );
8575
- }
8576
- /**
8577
- * Fetches a lightweight document metadata shape (id/path/suffix/size) for
8578
- * the given UUIDs and merges the results into `documentInfoMap`.
8579
- *
8580
- * This is useful for list/table contexts that do not need language-tagged
8581
- * fields (`subject`, `summary`) or category/tag enrichment.
8582
- *
8583
- * UUIDs already present in `documentInfoMap` are skipped.
8584
- */
8585
- async fetchDocumentDataSimple(uuids) {
8586
- const current = this._documentInfoMap.get();
8587
- const newUUIDs = uuids.filter((id) => current[id] === void 0);
8588
- if (newUUIDs.length > 0) {
8589
- await this._fetchSimpleDocumentInfoBatch(newUUIDs);
8590
- }
8591
- const updated = this._documentInfoMap.get();
8592
- return Object.fromEntries(
8593
- uuids.filter((id) => updated[id] !== void 0).map((id) => [id, updated[id]])
8594
- );
8595
- }
8596
- /**
8597
- * Returns the alternative representations of the given document UUID.
8598
- *
8599
- * Alternative representations are derived artefacts stored under
8600
- * `qcy:alternativeRepresentation` in the triplestore — for example a
8601
- * `.fragments` BIM tile derived from an `.ifc` source file.
8602
- *
8603
- * The returned `DocumentInfo` entries are also merged into
8604
- * `documentInfoMap` so reactive consumers stay in sync.
8605
- *
8606
- * @example
8607
- * ```ts
8608
- * const alts = await docs.fetchAlternativeRepresentations('abc-123');
8609
- * // alts[0].suffix => '.fragments'
8610
- * ```
8611
- */
8612
- async fetchAlternativeRepresentations(uuid) {
8613
- const q = `PREFIX qcy: <${qaecyPrefixes["qcy"]}>
8614
- PREFIX r: <${this.baseURL}>
8615
- SELECT ?altId ?contentIRI ?suffix ?rrp
8616
- WHERE {
8617
- r:${uuid} qcy:alternativeRepresentation ?contentIRI .
8618
- BIND(REPLACE(STR(?contentIRI), "^.*/([^/]*)$", "$1") AS ?altId)
8619
- ?contentIRI qcy:hasFileLocation ?loc .
8620
- ?loc qcy:suffix ?suffix .
8621
- OPTIONAL { ?loc qcy:remoteRelativePath ?rrp }
8622
- }`;
8623
- const data = await this._api.sparql(q, this._projectId, this._graphType);
8624
- const updates = { ...this._documentInfoMap.get() };
8625
- const alts = [];
8626
- data.results.bindings.forEach((b) => {
8627
- if (!b["altId"] || !b["contentIRI"])
8628
- return;
8629
- const id = b["altId"].value;
8630
- const info = {
8631
- id,
8632
- contentIRI: b["contentIRI"].value,
8633
- path: "",
8634
- suffix: b["suffix"]?.value ?? "",
8635
- size: 0,
8636
- tags: [],
8637
- categories: [],
8638
- remoteRelativePath: b["rrp"]?.value
8639
- };
8640
- updates[id] = info;
8641
- alts.push(info);
8642
- });
8643
- this._documentInfoMap.set(updates);
8644
- return alts;
8645
- }
8646
- /**
8647
- * Returns a single arbitrary file path from the project's triplestore.
8648
- * Useful for pre-filling path-based query inputs with a realistic example.
8649
- */
8650
- async randomFilePath() {
8651
- const q = `PREFIX qcy: <${qaecyPrefixes["qcy"]}>
8652
- SELECT ?path
8653
- WHERE {
8654
- ?fl a qcy:FileLocation ;
8655
- qcy:filePath ?path .
8656
- }
8657
- LIMIT 1`;
8658
- const data = await this._api.sparql(q, this._projectId, this._graphType);
8659
- return data.results.bindings[0]?.["path"]?.value ?? null;
8660
- }
8661
- // ── Private helpers ────────────────────────────────────────────────────────
8662
- /** Executes the document-info SPARQL query for the given UUIDs, merges results
8663
- * into `documentInfoMap`, and returns the newly fetched entries. */
8664
- async _fetchDocumentInfoBatch(uuids) {
8665
- const values = uuids.map((id) => `r:${id}`).join(" ");
8666
- const lang = this._api.language;
8667
- const q = `PREFIX qcy: <${qaecyPrefixes["qcy"]}>
8668
- PREFIX r: <${this.baseURL}>
8669
- SELECT ?id ?contentIRI ?suffix ?size ?subject ?summary
8670
- (SAMPLE(?fp) AS ?path)
8671
- (GROUP_CONCAT(DISTINCT ?tag; SEPARATOR=";") AS ?tags)
8672
- (GROUP_CONCAT(DISTINCT STR(?cat); SEPARATOR=";") AS ?categories)
8673
- WHERE {
8674
- VALUES ?contentIRI { ${values} }
8675
- ?contentIRI qcy:sizeBytes ?size ;
8676
- qcy:hasFileLocation ?loc .
8677
- ?loc qcy:filePath ?fp ;
8678
- qcy:suffix ?suffix .
8679
- OPTIONAL { ?contentIRI qcy:hasContentCategory ?cat }
8680
- OPTIONAL { ?contentIRI qcy:tag ?tag }
8681
- OPTIONAL { ?loc qcy:remoteProviderId ?pid }
8682
-
8683
- OPTIONAL { ?contentIRI qcy:subject ?lang_subj FILTER(LANG(?lang_subj) = "${lang}") }
8684
- OPTIONAL { ?contentIRI qcy:subject ?no_lang_subj }
8685
- BIND(COALESCE(?lang_subj, ?no_lang_subj) AS ?subject)
8686
-
8687
- OPTIONAL { ?contentIRI qcy:textSummary ?lang_summary FILTER(LANG(?lang_summary) = "${lang}") }
8688
- OPTIONAL { ?contentIRI qcy:textSummary ?no_lang_summary }
8689
- BIND(COALESCE(?lang_summary, ?no_lang_summary) AS ?summary)
8690
-
8691
- BIND(REPLACE(STR(?contentIRI), "^.*/([^/]*)$", "$1") AS ?id)
8692
- }
8693
- GROUP BY ?id ?contentIRI ?suffix ?size ?subject ?summary`;
8694
- const result = await this._api.sparql(q, this._projectId);
8695
- const updates = { ...this._documentInfoMap.get() };
8696
- const fetched = {};
8697
- result.results.bindings.forEach((b) => {
8698
- if (!b["id"] || !b["contentIRI"])
8699
- return;
8700
- const id = b["id"].value;
8701
- const info = {
8702
- id,
8703
- contentIRI: b["contentIRI"].value,
8704
- path: b["path"]?.value ?? "",
8705
- suffix: b["suffix"]?.value ?? "",
8706
- size: b["size"] ? parseInt(b["size"].value, 10) : 0,
8707
- tags: b["tags"]?.value?.split(";").filter(Boolean) ?? [],
8708
- categories: b["categories"]?.value?.split(";").filter(Boolean) ?? [],
8709
- subject: b["subject"]?.value,
8710
- summary: b["summary"]?.value,
8711
- providerId: b["pid"]?.value
8712
- };
8713
- updates[id] = info;
8714
- fetched[id] = info;
8715
- });
8716
- this._documentInfoMap.set(updates);
8717
- return fetched;
8718
- }
8719
- /** Executes a reduced document-info query (id/path/suffix/size only), merges
8720
- * into `documentInfoMap`, and returns newly fetched entries. */
8721
- async _fetchSimpleDocumentInfoBatch(uuids) {
8722
- const values = uuids.map((id) => `r:${id}`).join(" ");
8723
- const q = `PREFIX qcy: <${qaecyPrefixes["qcy"]}>
8724
- PREFIX r: <${this.baseURL}>
8725
- SELECT ?id ?contentIRI ?suffix ?size (SAMPLE(?fp) AS ?path)
8726
- WHERE {
8727
- VALUES ?contentIRI { ${values} }
8728
- ?contentIRI qcy:sizeBytes ?size ;
8729
- qcy:hasFileLocation ?loc .
8730
- ?loc qcy:filePath ?fp ;
8731
- qcy:suffix ?suffix .
8732
- BIND(REPLACE(STR(?contentIRI), "^.*/([^/]*)$", "$1") AS ?id)
8733
- }
8734
- GROUP BY ?id ?contentIRI ?suffix ?size`;
8735
- const result = await this._api.sparql(q, this._projectId, this._graphType);
8736
- const updates = { ...this._documentInfoMap.get() };
8737
- const fetched = {};
8738
- result.results.bindings.forEach((b) => {
8739
- if (!b["id"])
8740
- return;
8741
- const id = b["id"].value;
8742
- const existing = updates[id];
8743
- const info = {
8744
- id,
8745
- contentIRI: b["contentIRI"]?.value ?? existing?.contentIRI ?? id,
8746
- path: b["path"]?.value ?? existing?.path ?? id,
8747
- suffix: b["suffix"]?.value ?? existing?.suffix ?? "",
8748
- size: b["size"] ? parseInt(b["size"].value, 10) : existing?.size ?? 0,
8749
- tags: existing?.tags ?? [],
8750
- categories: existing?.categories ?? [],
8751
- subject: existing?.subject,
8752
- summary: existing?.summary,
8753
- providerId: existing?.providerId
8754
- };
8755
- updates[id] = info;
8756
- fetched[id] = info;
8757
- });
8758
- this._documentInfoMap.set(updates);
8759
- return fetched;
8760
- }
8761
- async _fetchDocumentsBySuffix() {
8762
- return this._runDocumentsBySuffixQuery(this._buildDocumentsBySuffixQuery());
8763
- }
8764
- _buildDocumentsBySuffixQuery() {
8765
- return `PREFIX qcy: <${qaecyPrefixes["qcy"]}>
8766
- SELECT ?ext (SUM(?size) AS ?totalSize) (COUNT(*) AS ?docCount)
8767
- WHERE {
8768
- {
8769
- SELECT DISTINCT ?fc ?ext ?size
8770
- WHERE {
8771
- ?fc a qcy:FileContent ;
8772
- qcy:sizeBytes ?size ;
8773
- qcy:hasFileLocation/qcy:suffix ?ext .
8774
- FILTER NOT EXISTS { ?x qcy:alternativeRepresentation ?fc }
8775
- }
8776
- }
8777
- }
8778
- GROUP BY ?ext
8779
- ORDER BY DESC(?docCount)`;
8780
- }
8781
- async _runDocumentsBySuffixQuery(q) {
8782
- const data = await this._api.sparql(q, this._projectId, this._graphType);
8783
- const result = {};
8784
- data.results.bindings.forEach((b) => {
8785
- if (!b["ext"])
8786
- return;
8787
- result[b["ext"].value] = {
8788
- size: b["totalSize"] ? parseInt(b["totalSize"].value, 10) : 0,
8789
- count: b["docCount"] ? parseInt(b["docCount"].value, 10) : 0
8790
- };
8791
- });
8792
- return result;
8793
- }
8794
- async _fetchDocumentsByContentCategory() {
8795
- return this._runDocumentsByContentCategoryQuery(this._buildDocumentsByContentCategoryQuery());
8796
- }
8797
- _buildDocumentsByContentCategoryQuery() {
8798
- return `PREFIX qcy: <${qaecyPrefixes["qcy"]}>
8799
- SELECT ?cat (SUM(?size) AS ?totalSize) (COUNT(*) AS ?docCount)
8800
- WHERE {
8801
- {
8802
- SELECT DISTINCT ?fc ?cat ?size
8803
- WHERE {
8804
- ?fc a qcy:FileContent ;
8805
- qcy:hasContentCategory ?cat ;
8806
- qcy:sizeBytes ?size .
8807
- FILTER NOT EXISTS { ?x qcy:alternativeRepresentation ?fc }
8808
- }
8809
- }
8810
- }
8811
- GROUP BY ?cat
8812
- ORDER BY DESC(?docCount)`;
8813
- }
8814
- async _runDocumentsByContentCategoryQuery(q) {
8815
- const data = await this._api.sparql(q, this._projectId, this._graphType);
8816
- const result = {};
8817
- data.results.bindings.forEach((b) => {
8818
- if (!b["cat"])
8819
- return;
8820
- result[b["cat"].value] = {
8821
- size: b["totalSize"] ? parseInt(b["totalSize"].value, 10) : 0,
8822
- count: b["docCount"] ? parseInt(b["docCount"].value, 10) : 0
8823
- };
8824
- });
8825
- return result;
8826
- }
8827
- async _fetchDuplicateCount() {
8828
- return this._runDuplicateCountQuery(this._buildDuplicateCountQuery());
8829
- }
8830
- _buildDuplicateCountQuery() {
8831
- return `PREFIX qcy: <${qaecyPrefixes["qcy"]}>
8832
- SELECT (COUNT(*) AS ?count)
8833
- WHERE {
8834
- SELECT ?fc
8835
- WHERE {
8836
- ?fc a qcy:FileContent ;
8837
- qcy:hasFileLocation ?fl .
8838
- FILTER NOT EXISTS { ?x qcy:alternativeRepresentation ?fc }
8839
- }
8840
- GROUP BY ?fc
8841
- HAVING (COUNT(?fl) > 1)
8842
- }`;
8843
- }
8844
- async _runDuplicateCountQuery(q) {
8845
- const data = await this._api.sparql(q, this._projectId, this._graphType);
8846
- const first = data.results.bindings[0];
8847
- return first?.["count"] ? parseInt(first["count"].value, 10) : 0;
8848
- }
8849
- };
8850
-
8851
- // libs/js/cue-sdk/src/lib/project-view.ts
8852
- var CueProjectView = class {
8853
- constructor(_api, _projectId, { language, queryCache, rdfBase = RESOURCE_BASE, graphType }) {
8854
- this._api = _api;
8855
- this._projectId = _projectId;
8856
- this.schema = new CueProjectSchema(_api, _projectId, language, queryCache, graphType);
8857
- this.entities = new CueProjectEntities(_api, _projectId, rdfBase, queryCache, graphType);
8858
- this.documents = new CueProjectDocuments(_api, _projectId, language, rdfBase, queryCache, graphType);
8859
- this.availableContentCategories = this.schema.availableContentCategories;
8860
- this.availableEntityCategories = this.schema.availableEntityCategories;
8861
- this.availableEntityRelationships = this.schema.availableEntityRelationships;
8862
- this.schemaReady = this.schema.ready;
8863
- this.entityInfoMap = this.entities.entityInfoMap;
8864
- this.entityGraph = this.entities.entityGraph;
8865
- this.documentInfoMap = this.documents.documentInfoMap;
8866
- this.projectDocumentsData = this.documents.projectDocumentsData;
8867
- this.searchResults = this._searchResults.asReadonly();
8868
- this.documents.fetchOverview().catch((err) => console.error("[CueProjectView] fetchOverview failed:", err));
8869
- }
8870
- /** Direct access to the schema data class (available categories / relationships). */
8871
- schema;
8872
- /** Direct access to the entity data class. */
8873
- entities;
8874
- /** Direct access to the document data class. */
8875
- documents;
8876
- // ── Proxied signals ────────────────────────────────────────────────────────
8877
- /** Available content category definitions for this project. Auto-fetched on init. */
8878
- availableContentCategories;
8879
- /** Available entity category definitions for this project. Auto-fetched on init. */
8880
- availableEntityCategories;
8881
- /** Available entity relationship types. Auto-fetched on init. */
8882
- availableEntityRelationships;
8883
- /**
8884
- * Resolves when the initial schema load has completed. Await before reading
8885
- * schema signal values imperatively.
8886
- */
8887
- schemaReady;
8888
- /** Merged per-entity detail map. Populated lazily via `requestEntityData()` etc. */
8889
- entityInfoMap;
8890
- /** Project-level entity co-occurrence graph. Fetched once on init. */
8891
- entityGraph;
8892
- /** Per-document info map. Populated lazily via `requestDocumentData()`. */
8893
- documentInfoMap;
8894
- /** Project document overview (counts by suffix and category). Fetched on init. */
8895
- projectDocumentsData;
8896
- // ── Search state ───────────────────────────────────────────────────────────
8897
- _searchResults = new CueSignal(void 0);
8898
- /** The result of the most recent `search()` call. `undefined` before first search. */
8899
- searchResults;
8900
- _destroyed = false;
8901
- // ── Entity methods ─────────────────────────────────────────────────────────
8902
- /**
8903
- * Lazily batch-fetch core data (label + categories) for the given entity UUIDs.
8904
- * Already-fetched UUIDs are skipped. Populates `entityInfoMap`.
8905
- */
8906
- requestEntityData(uuids, includeMentionCount = false) {
8907
- if (this._destroyed)
8908
- return;
8909
- this.entities.requestEntityData(uuids, includeMentionCount);
8910
- }
8911
- /**
8912
- * Lazily fetch OSM location data for the given entity UUIDs.
8913
- * Already-fetched UUIDs are skipped. Populates `entityInfoMap` geometry fields.
8914
- */
8915
- async requestEntityLocations(uuids) {
8916
- if (this._destroyed)
8917
- return;
8918
- return this.entities.requestEntityLocations(uuids);
8919
- }
8920
- /**
8921
- * Fetch incoming and outgoing relationships for a single entity IRI.
8922
- * Result is stored in `entityInfoMap[uuid].relationshipData`.
8923
- */
8924
- async fetchEntityRelationships(iri) {
8925
- if (this._destroyed)
8926
- throw new Error("CueProjectView is destroyed");
8927
- return this.entities.fetchEntityRelationships(iri);
8928
- }
8929
- /**
8930
- * Fetch UUIDs of documents that reference the given entity IRI.
8931
- * Result is stored in `entityInfoMap[uuid].documentRefs`.
8932
- */
8933
- async fetchEntityDocuments(iri) {
8934
- if (this._destroyed)
8935
- throw new Error("CueProjectView is destroyed");
8936
- return this.entities.fetchEntityDocuments(iri);
8937
- }
8938
- /** Constructs the full RDF IRI for an entity UUID. */
8939
- entityIri(uuid) {
8940
- return this.entities.entityIri(uuid);
8941
- }
8942
- // ── Document methods ───────────────────────────────────────────────────────
8943
- /**
8944
- * Lazily batch-fetch document info for the given UUIDs.
8945
- * Already-fetched UUIDs are skipped. Populates `documentInfoMap`.
8946
- */
8947
- requestDocumentData(uuids) {
8948
- if (this._destroyed)
8949
- return;
8950
- this.documents.requestDocumentData(uuids);
8951
- }
8952
- // ── Search ─────────────────────────────────────────────────────────────────
8953
- /**
8954
- * Run a natural-language search against the project.
8955
- * The result is stored in `searchResults` and replaces any previous result.
8956
- */
8957
- async search(term, options) {
8958
- if (this._destroyed)
8959
- return;
8960
- const result = await this._api.search({
8961
- term,
8962
- projectId: this._projectId,
8963
- categories: options?.categories
8964
- });
8965
- if (!this._destroyed) {
8966
- this._searchResults.set(result);
8967
- }
8968
- }
8969
- // ── Lifecycle ──────────────────────────────────────────────────────────────
8970
- /**
8971
- * Switch the active language for schema labels and document text fields.
8972
- * Schema responses are cached per language (instant if previously loaded).
8973
- * The document info map is cleared and lazily re-populated on next access.
8974
- */
8975
- setLanguage(lang) {
8976
- if (this._destroyed)
8977
- return;
8978
- this.schema.setLanguage(lang);
8979
- this.documents.setLanguage(lang);
8980
- }
8981
- /**
8982
- * Reset all entity and document state and re-fetch the project overview.
8983
- * Prefer creating a fresh `CueProjectView` when switching projects.
8984
- * Use `reset()` only when the same project's data needs to be invalidated.
8985
- */
8986
- reset() {
8987
- if (this._destroyed)
8988
- return;
8989
- this.entities.reset();
8990
- this.documents.reset();
8991
- this._searchResults.set(void 0);
8992
- this.documents.fetchOverview().catch((err) => console.error("[CueProjectView] fetchOverview failed after reset:", err));
8993
- }
8994
- /**
8995
- * Tear down this view instance. Clears all reactive state and blocks further
8996
- * updates. Call from the Angular adapter's `ngOnDestroy` or equivalent.
8997
- */
8998
- destroy() {
8999
- this._destroyed = true;
9000
- this._searchResults.set(void 0);
9001
- }
9002
- };
9003
-
9004
- // libs/js/file-metadata-helpers/src/lib/js-file-metadata-helpers.ts
9005
- init_src();
9006
-
9007
8610
  // libs/js/models/src/lib/file-extensions.ts
9008
8611
  var fileExtensionsInfo = {
9009
8612
  [".aac" /* AAC */]: {
@@ -9866,41 +9469,706 @@ var fileExtensionsInfo = {
9866
9469
  }
9867
9470
  };
9868
9471
 
9869
- // libs/js/models/src/lib/file-mime-categories.ts
9870
- var fileTypeToMimeCategory = {
9871
- ["audio" /* AUDIO */]: "AudioFileContent" /* AUDIO */,
9872
- ["video" /* VIDEO */]: "VideoFileContent" /* VIDEO */,
9873
- ["image" /* IMAGE */]: "ImageFileContent" /* IMAGE */,
9874
- ["text" /* TEXT */]: "TextFileContent" /* TEXT */,
9875
- ["markup" /* MARKUP */]: "MarkupFileContent" /* MARKUP */,
9876
- ["script" /* SCRIPT */]: "ScriptFileContent" /* SCRIPT */,
9877
- ["data" /* DATA */]: "DataFileContent" /* DATA */,
9878
- ["archive" /* ARCHIVE */]: "ArchiveFileContent" /* ARCHIVE */,
9879
- ["installer" /* INSTALLER */]: "InstallerFileContent" /* INSTALLER */,
9880
- ["binary" /* BINARY */]: "BinaryFileContent" /* BINARY */,
9881
- ["backup" /* BACKUP */]: "BackupFileContent" /* BACKUP */,
9882
- ["automation" /* AUTOMATION */]: "AutomationFileContent" /* AUTOMATION */,
9883
- ["presentation" /* PRESENTATION */]: "PresentationFileContent" /* PRESENTATION */,
9884
- ["spreadsheet" /* SPREADSHEET */]: "SpreadsheetFileContent" /* SPREADSHEET */,
9885
- ["font" /* FONT */]: "FontFileContent" /* FONT */,
9886
- ["geospatial" /* GEOSPATIAL */]: "GeospatialFileContent" /* GEOSPATIAL */,
9887
- ["3d" /* THREE_D */]: "ThreeDFileContent" /* THREE_D */,
9888
- ["cad" /* CAD */]: "CADFileContent" /* CAD */,
9889
- ["bim" /* BIM */]: "BIMFileContent" /* BIM */,
9890
- ["planning" /* PLANNING */]: "PlanningFileContent" /* PLANNING */,
9891
- ["email" /* EMAIL */]: "EmailFileContent" /* EMAIL */,
9892
- ["multimedia" /* MULTIMEDIA */]: "MultimediaFileContent" /* MULTIMEDIA */,
9893
- ["unknown" /* UNKNOWN */]: "UnknownFileContent" /* UNKNOWN */
9472
+ // libs/js/models/src/lib/file-mime-categories.ts
9473
+ var fileTypeToMimeCategory = {
9474
+ ["audio" /* AUDIO */]: "AudioFileContent" /* AUDIO */,
9475
+ ["video" /* VIDEO */]: "VideoFileContent" /* VIDEO */,
9476
+ ["image" /* IMAGE */]: "ImageFileContent" /* IMAGE */,
9477
+ ["text" /* TEXT */]: "TextFileContent" /* TEXT */,
9478
+ ["markup" /* MARKUP */]: "MarkupFileContent" /* MARKUP */,
9479
+ ["script" /* SCRIPT */]: "ScriptFileContent" /* SCRIPT */,
9480
+ ["data" /* DATA */]: "DataFileContent" /* DATA */,
9481
+ ["archive" /* ARCHIVE */]: "ArchiveFileContent" /* ARCHIVE */,
9482
+ ["installer" /* INSTALLER */]: "InstallerFileContent" /* INSTALLER */,
9483
+ ["binary" /* BINARY */]: "BinaryFileContent" /* BINARY */,
9484
+ ["backup" /* BACKUP */]: "BackupFileContent" /* BACKUP */,
9485
+ ["automation" /* AUTOMATION */]: "AutomationFileContent" /* AUTOMATION */,
9486
+ ["presentation" /* PRESENTATION */]: "PresentationFileContent" /* PRESENTATION */,
9487
+ ["spreadsheet" /* SPREADSHEET */]: "SpreadsheetFileContent" /* SPREADSHEET */,
9488
+ ["font" /* FONT */]: "FontFileContent" /* FONT */,
9489
+ ["geospatial" /* GEOSPATIAL */]: "GeospatialFileContent" /* GEOSPATIAL */,
9490
+ ["3d" /* THREE_D */]: "ThreeDFileContent" /* THREE_D */,
9491
+ ["cad" /* CAD */]: "CADFileContent" /* CAD */,
9492
+ ["bim" /* BIM */]: "BIMFileContent" /* BIM */,
9493
+ ["planning" /* PLANNING */]: "PlanningFileContent" /* PLANNING */,
9494
+ ["email" /* EMAIL */]: "EmailFileContent" /* EMAIL */,
9495
+ ["multimedia" /* MULTIMEDIA */]: "MultimediaFileContent" /* MULTIMEDIA */,
9496
+ ["unknown" /* UNKNOWN */]: "UnknownFileContent" /* UNKNOWN */
9497
+ };
9498
+
9499
+ // libs/js/models/src/lib/project.ts
9500
+ var import_uuid3 = require("uuid");
9501
+
9502
+ // libs/js/models/src/lib/search-log-entry.ts
9503
+ var import_uuid4 = require("uuid");
9504
+
9505
+ // libs/js/models/src/lib/search-response.ts
9506
+ var import_uuid5 = require("uuid");
9507
+
9508
+ // libs/js/cue-sdk/src/lib/documents.ts
9509
+ var CueProjectDocuments = class {
9510
+ constructor(_api, _projectId, language, rdfBase = RESOURCE_BASE, _queryCache, _graphType, _verbose = false) {
9511
+ this._api = _api;
9512
+ this._projectId = _projectId;
9513
+ this._queryCache = _queryCache;
9514
+ this._graphType = _graphType;
9515
+ this._verbose = _verbose;
9516
+ this.baseURL = `${rdfBase}${_projectId}/`;
9517
+ this._currentLang = language ?? this._api.language;
9518
+ this.documentInfoMap = this._documentInfoMap.asReadonly();
9519
+ this.projectDocumentsData = this._projectDocumentsData.asReadonly();
9520
+ }
9521
+ /** Full RDF base URL for this project, e.g. `https://cue.qaecy.com/r/{pid}/` */
9522
+ baseURL;
9523
+ /** Tracks the language for which `_documentInfoMap` is currently populated. */
9524
+ _currentLang;
9525
+ _documentInfoMap = new CueSignal({});
9526
+ /** Cumulative unique document UUIDs ever passed to request methods (survives cache hits). */
9527
+ _seenIds = /* @__PURE__ */ new Set();
9528
+ _projectDocumentsData = new CueSignal({
9529
+ duplicateCount: 0,
9530
+ documentsBySuffix: {},
9531
+ documentsByContentCategory: {}
9532
+ });
9533
+ /** Lazily populated per-document detail map. */
9534
+ documentInfoMap;
9535
+ /** Project-level document overview (grouped counts + sizes). */
9536
+ projectDocumentsData;
9537
+ // ── Lifecycle ──────────────────────────────────────────────────────────────
9538
+ /**
9539
+ * Resets all document state. Call when the active project changes.
9540
+ * Follow with `fetchOverview()` once the triplestore is ready.
9541
+ */
9542
+ reset() {
9543
+ this._documentInfoMap.set({});
9544
+ this._seenIds.clear();
9545
+ this._projectDocumentsData.set({
9546
+ duplicateCount: 0,
9547
+ documentsBySuffix: {},
9548
+ documentsByContentCategory: {}
9549
+ });
9550
+ }
9551
+ /**
9552
+ * Updates the active language and clears the document info map so that
9553
+ * language-sensitive fields (subject, summary) are re-fetched on the next
9554
+ * `requestDocumentData()` call.
9555
+ */
9556
+ setLanguage(lang) {
9557
+ if (this._currentLang === lang)
9558
+ return;
9559
+ this._currentLang = lang;
9560
+ this._api.setLanguage(lang);
9561
+ this._documentInfoMap.set({});
9562
+ }
9563
+ // ── Public API ─────────────────────────────────────────────────────────────
9564
+ /**
9565
+ * Fetches the three-part project overview (by suffix, by content category,
9566
+ * duplicate count) in parallel and writes them as a single atomic update to
9567
+ * `projectDocumentsData`. Safe to call again to refresh.
9568
+ */
9569
+ async fetchOverview() {
9570
+ this._projectDocumentsData.set({
9571
+ duplicateCount: 0,
9572
+ documentsBySuffix: {},
9573
+ documentsByContentCategory: {}
9574
+ });
9575
+ const qSuffix = this._buildDocumentsBySuffixQuery();
9576
+ const qCategory = this._buildDocumentsByContentCategoryQuery();
9577
+ const qDuplicates = this._buildDuplicateCountQuery();
9578
+ await staleWhileRevalidate(
9579
+ qSuffix + qCategory + qDuplicates,
9580
+ async () => {
9581
+ const [bySuffix, byCategory, duplicateCount] = await Promise.all([
9582
+ this._runDocumentsBySuffixQuery(qSuffix),
9583
+ this._runDocumentsByContentCategoryQuery(qCategory),
9584
+ this._runDuplicateCountQuery(qDuplicates)
9585
+ ]);
9586
+ return { duplicateCount, documentsBySuffix: bySuffix, documentsByContentCategory: byCategory };
9587
+ },
9588
+ (overview) => this._projectDocumentsData.set(overview),
9589
+ this._queryCache
9590
+ );
9591
+ }
9592
+ /**
9593
+ * Lazily batch-fetches core metadata for the given document UUIDs.
9594
+ * Already-cached UUIDs are skipped. Data is merged into `documentInfoMap`
9595
+ * once the SPARQL response arrives.
9596
+ */
9597
+ requestDocumentData(uuids) {
9598
+ for (const id of uuids)
9599
+ this._seenIds.add(id);
9600
+ const newUUIDs = uuids.filter((id) => this._documentInfoMap.get()[id] === void 0);
9601
+ if (newUUIDs.length === 0)
9602
+ return;
9603
+ this._log(`requestDocumentData: ${newUUIDs.length} new / ${uuids.length} requested | cumulative: ${this._seenIds.size} seen`);
9604
+ this._fetchDocumentInfoBatch(newUUIDs).catch(
9605
+ (err) => console.error("[CueProjectDocuments] requestDocumentData failed:", err)
9606
+ );
9607
+ }
9608
+ /**
9609
+ * Promise-based alternative to {@link requestDocumentData} for non-reactive contexts.
9610
+ *
9611
+ * Resolves with the `DocumentInfo` entries for every requested UUID once the
9612
+ * SPARQL response arrives. UUIDs already present in the cache are returned
9613
+ * immediately without a network request. The result is also written into
9614
+ * `documentInfoMap` so reactive consumers stay in sync.
9615
+ *
9616
+ * UUIDs not found in the triplestore are omitted from the returned map.
9617
+ *
9618
+ * @example
9619
+ * ```ts
9620
+ * const docs = await cueProjectDocs.fetchDocumentData(['uuid1', 'uuid2']);
9621
+ * console.log(docs['uuid1'].subject);
9622
+ * ```
9623
+ */
9624
+ async fetchDocumentData(uuids) {
9625
+ const current = this._documentInfoMap.get();
9626
+ const newUUIDs = uuids.filter((id) => current[id] === void 0);
9627
+ for (const id of uuids)
9628
+ this._seenIds.add(id);
9629
+ if (newUUIDs.length > 0) {
9630
+ await this._fetchDocumentInfoBatch(newUUIDs);
9631
+ }
9632
+ const updated = this._documentInfoMap.get();
9633
+ return Object.fromEntries(
9634
+ uuids.filter((id) => updated[id] !== void 0).map((id) => [id, updated[id]])
9635
+ );
9636
+ }
9637
+ /**
9638
+ * Fetches a lightweight document metadata shape (id/path/suffix/size) for
9639
+ * the given UUIDs and merges the results into `documentInfoMap`.
9640
+ *
9641
+ * This is useful for list/table contexts that do not need language-tagged
9642
+ * fields (`subject`, `summary`) or category/tag enrichment.
9643
+ *
9644
+ * UUIDs already present in `documentInfoMap` are skipped.
9645
+ */
9646
+ async fetchDocumentDataSimple(uuids) {
9647
+ const current = this._documentInfoMap.get();
9648
+ const newUUIDs = uuids.filter((id) => current[id] === void 0);
9649
+ for (const id of uuids)
9650
+ this._seenIds.add(id);
9651
+ if (newUUIDs.length > 0) {
9652
+ await this._fetchSimpleDocumentInfoBatch(newUUIDs);
9653
+ }
9654
+ const updated = this._documentInfoMap.get();
9655
+ return Object.fromEntries(
9656
+ uuids.filter((id) => updated[id] !== void 0).map((id) => [id, updated[id]])
9657
+ );
9658
+ }
9659
+ /**
9660
+ * Returns the alternative representations of the given document UUID.
9661
+ *
9662
+ * Alternative representations are derived artefacts stored under
9663
+ * `qcy:alternativeRepresentation` in the triplestore — for example a
9664
+ * `.fragments` BIM tile derived from an `.ifc` source file.
9665
+ *
9666
+ * The returned `DocumentInfo` entries are also merged into
9667
+ * `documentInfoMap` so reactive consumers stay in sync.
9668
+ *
9669
+ * @example
9670
+ * ```ts
9671
+ * const alts = await docs.fetchAlternativeRepresentations('abc-123');
9672
+ * // alts[0].suffix => '.fragments'
9673
+ * ```
9674
+ */
9675
+ async fetchAlternativeRepresentations(uuid) {
9676
+ const q = `PREFIX qcy: <${qaecyPrefixes["qcy"]}>
9677
+ PREFIX r: <${this.baseURL}>
9678
+ SELECT ?altId ?contentIRI ?suffix ?rrp
9679
+ WHERE {
9680
+ r:${uuid} qcy:alternativeRepresentation ?contentIRI .
9681
+ BIND(REPLACE(STR(?contentIRI), "^.*/([^/]*)$", "$1") AS ?altId)
9682
+ ?contentIRI qcy:hasFileLocation ?loc .
9683
+ ?loc qcy:suffix ?suffix .
9684
+ OPTIONAL { ?loc qcy:remoteRelativePath ?rrp }
9685
+ }`;
9686
+ const data = await this._api.sparql(q, this._projectId, this._graphType);
9687
+ const updates = { ...this._documentInfoMap.get() };
9688
+ const alts = [];
9689
+ data.results.bindings.forEach((b) => {
9690
+ if (!b["altId"] || !b["contentIRI"])
9691
+ return;
9692
+ const id = b["altId"].value;
9693
+ const info = {
9694
+ id,
9695
+ contentIRI: b["contentIRI"].value,
9696
+ path: "",
9697
+ suffix: b["suffix"]?.value ?? "",
9698
+ size: 0,
9699
+ tags: [],
9700
+ categories: [],
9701
+ remoteRelativePath: b["rrp"]?.value
9702
+ };
9703
+ updates[id] = info;
9704
+ alts.push(info);
9705
+ });
9706
+ this._documentInfoMap.set(updates);
9707
+ return alts;
9708
+ }
9709
+ /**
9710
+ * Returns a single arbitrary file path from the project's triplestore.
9711
+ * Useful for pre-filling path-based query inputs with a realistic example.
9712
+ */
9713
+ async randomFilePath() {
9714
+ const q = `PREFIX qcy: <${qaecyPrefixes["qcy"]}>
9715
+ SELECT ?path
9716
+ WHERE {
9717
+ ?fl a qcy:FileLocation ;
9718
+ qcy:filePath ?path .
9719
+ }
9720
+ LIMIT 1`;
9721
+ const data = await this._api.sparql(q, this._projectId, this._graphType);
9722
+ return data.results.bindings[0]?.["path"]?.value ?? null;
9723
+ }
9724
+ async documentsBySuffix(suffixes, includeMetadata = false) {
9725
+ if (suffixes.length === 0)
9726
+ return [];
9727
+ const normalised = suffixes.map(
9728
+ (s) => (s.startsWith(".") ? s : `.${s}`).toLowerCase()
9729
+ );
9730
+ const values = normalised.map((s) => `"${s}"`).join(" ");
9731
+ const q = includeMetadata ? `PREFIX qcy: <${qaecyPrefixes["qcy"]}>
9732
+ SELECT ?id (SAMPLE(?fp) AS ?path) ?suffix (MAX(?sz) AS ?size)
9733
+ WHERE {
9734
+ VALUES ?suffix { ${values} }
9735
+ ?iri a qcy:FileContent ;
9736
+ qcy:sizeBytes ?sz ;
9737
+ qcy:hasFileLocation ?loc .
9738
+ ?loc qcy:suffix ?suffix ;
9739
+ qcy:filePath ?fp .
9740
+ FILTER NOT EXISTS { ?x qcy:alternativeRepresentation ?iri }
9741
+ BIND(REPLACE(STR(?iri), "^.*/", "") AS ?id)
9742
+ }
9743
+ GROUP BY ?id ?suffix` : `PREFIX qcy: <${qaecyPrefixes["qcy"]}>
9744
+ SELECT DISTINCT ?id
9745
+ WHERE {
9746
+ VALUES ?suffix { ${values} }
9747
+ ?iri a qcy:FileContent ;
9748
+ qcy:hasFileLocation/qcy:suffix ?suffix .
9749
+ FILTER NOT EXISTS { ?x qcy:alternativeRepresentation ?iri }
9750
+ BIND(REPLACE(STR(?iri), "^.*/", "") AS ?id)
9751
+ }`;
9752
+ const data = await this._api.sparql(q, this._projectId, this._graphType);
9753
+ return data.results.bindings.filter((b) => b["id"]).map((b) => {
9754
+ const uuid = b["id"].value;
9755
+ const base = { iri: this._resourceIri(uuid), uuid };
9756
+ if (!includeMetadata)
9757
+ return base;
9758
+ return {
9759
+ ...base,
9760
+ path: b["path"]?.value ?? "",
9761
+ suffix: b["suffix"]?.value ?? "",
9762
+ size: b["size"] ? parseInt(b["size"].value, 10) : 0
9763
+ };
9764
+ });
9765
+ }
9766
+ documentsByFileType(fileTypes, includeMetadata = false) {
9767
+ const typeSet = new Set(fileTypes);
9768
+ const suffixes = Object.values(fileExtensionsInfo).filter((info) => typeSet.has(info.type)).map((info) => info.suffix);
9769
+ return this.documentsBySuffix(suffixes, includeMetadata);
9770
+ }
9771
+ async documentsByContentCategory(categoryIRIs, includeMetadata = false) {
9772
+ if (categoryIRIs.length === 0)
9773
+ return [];
9774
+ const values = categoryIRIs.map((iri) => `<${iri}>`).join(" ");
9775
+ const q = includeMetadata ? `PREFIX qcy: <${qaecyPrefixes["qcy"]}>
9776
+ SELECT ?id (SAMPLE(?fp) AS ?path) ?suffix (MAX(?sz) AS ?size)
9777
+ WHERE {
9778
+ VALUES ?cat { ${values} }
9779
+ ?iri a qcy:FileContent ;
9780
+ qcy:hasContentCategory ?cat ;
9781
+ qcy:sizeBytes ?sz ;
9782
+ qcy:hasFileLocation ?loc .
9783
+ ?loc qcy:suffix ?suffix ;
9784
+ qcy:filePath ?fp .
9785
+ FILTER NOT EXISTS { ?x qcy:alternativeRepresentation ?iri }
9786
+ BIND(REPLACE(STR(?iri), "^.*/", "") AS ?id)
9787
+ }
9788
+ GROUP BY ?id ?suffix` : `PREFIX qcy: <${qaecyPrefixes["qcy"]}>
9789
+ SELECT DISTINCT ?id
9790
+ WHERE {
9791
+ VALUES ?cat { ${values} }
9792
+ ?iri a qcy:FileContent ;
9793
+ qcy:hasContentCategory ?cat .
9794
+ FILTER NOT EXISTS { ?x qcy:alternativeRepresentation ?iri }
9795
+ BIND(REPLACE(STR(?iri), "^.*/", "") AS ?id)
9796
+ }`;
9797
+ const data = await this._api.sparql(q, this._projectId, this._graphType);
9798
+ return data.results.bindings.filter((b) => b["id"]).map((b) => {
9799
+ const uuid = b["id"].value;
9800
+ const base = { iri: this._resourceIri(uuid), uuid };
9801
+ if (!includeMetadata)
9802
+ return base;
9803
+ return {
9804
+ ...base,
9805
+ path: b["path"]?.value ?? "",
9806
+ suffix: b["suffix"]?.value ?? "",
9807
+ size: b["size"] ? parseInt(b["size"].value, 10) : 0
9808
+ };
9809
+ });
9810
+ }
9811
+ documentsByMime(mimeTypes, includeMetadata = false) {
9812
+ const mimeSet = new Set(mimeTypes);
9813
+ const suffixes = Object.values(fileExtensionsInfo).filter((info) => mimeSet.has(info.mime)).map((info) => info.suffix);
9814
+ return this.documentsBySuffix(suffixes, includeMetadata);
9815
+ }
9816
+ // ── Private helpers ────────────────────────────────────────────────────────
9817
+ /** Builds a full resource IRI from a UUID without a SPARQL round-trip. */
9818
+ _resourceIri(uuid) {
9819
+ return `${this.baseURL}${uuid}`;
9820
+ }
9821
+ _log(message) {
9822
+ if (this._verbose) {
9823
+ console.debug(`[CueProjectDocuments pid=${this._projectId}] ${message}`);
9824
+ }
9825
+ }
9826
+ /** Executes the document-info SPARQL query for the given UUIDs, merges results
9827
+ * into `documentInfoMap`, and returns the newly fetched entries. */
9828
+ async _fetchDocumentInfoBatch(uuids) {
9829
+ const values = uuids.map((id) => `r:${id}`).join(" ");
9830
+ const lang = this._api.language;
9831
+ const q = `PREFIX qcy: <${qaecyPrefixes["qcy"]}>
9832
+ PREFIX r: <${this.baseURL}>
9833
+ SELECT ?id ?suffix ?size ?subject ?summary
9834
+ (SAMPLE(?fp) AS ?path)
9835
+ (GROUP_CONCAT(DISTINCT ?tag; SEPARATOR=";") AS ?tags)
9836
+ (GROUP_CONCAT(DISTINCT STR(?cat); SEPARATOR=";") AS ?categories)
9837
+ WHERE {
9838
+ VALUES ?contentIRI { ${values} }
9839
+ ?contentIRI qcy:sizeBytes ?size ;
9840
+ qcy:hasFileLocation ?loc .
9841
+ ?loc qcy:filePath ?fp ;
9842
+ qcy:suffix ?suffix .
9843
+ OPTIONAL { ?contentIRI qcy:hasContentCategory ?cat }
9844
+ OPTIONAL { ?contentIRI qcy:tag ?tag }
9845
+ OPTIONAL { ?loc qcy:remoteProviderId ?pid }
9846
+
9847
+ OPTIONAL { ?contentIRI qcy:subject ?lang_subj FILTER(LANG(?lang_subj) = "${lang}") }
9848
+ OPTIONAL { ?contentIRI qcy:subject ?no_lang_subj }
9849
+ BIND(COALESCE(?lang_subj, ?no_lang_subj) AS ?subject)
9850
+
9851
+ OPTIONAL { ?contentIRI qcy:textSummary ?lang_summary FILTER(LANG(?lang_summary) = "${lang}") }
9852
+ OPTIONAL { ?contentIRI qcy:textSummary ?no_lang_summary }
9853
+ BIND(COALESCE(?lang_summary, ?no_lang_summary) AS ?summary)
9854
+
9855
+ BIND(REPLACE(STR(?contentIRI), "^.*/", "") AS ?id)
9856
+ }
9857
+ GROUP BY ?id ?suffix ?size ?subject ?summary`;
9858
+ const result = await this._api.sparql(q, this._projectId);
9859
+ const updates = { ...this._documentInfoMap.get() };
9860
+ const fetched = {};
9861
+ result.results.bindings.forEach((b) => {
9862
+ if (!b["id"])
9863
+ return;
9864
+ const id = b["id"].value;
9865
+ const info = {
9866
+ id,
9867
+ contentIRI: this._resourceIri(id),
9868
+ path: b["path"]?.value ?? "",
9869
+ suffix: b["suffix"]?.value ?? "",
9870
+ size: b["size"] ? parseInt(b["size"].value, 10) : 0,
9871
+ tags: b["tags"]?.value?.split(";").filter(Boolean) ?? [],
9872
+ categories: b["categories"]?.value?.split(";").filter(Boolean) ?? [],
9873
+ subject: b["subject"]?.value,
9874
+ summary: b["summary"]?.value,
9875
+ providerId: b["pid"]?.value
9876
+ };
9877
+ updates[id] = info;
9878
+ fetched[id] = info;
9879
+ });
9880
+ this._documentInfoMap.set(updates);
9881
+ this._log(`fetchDocumentInfoBatch: ${Object.keys(fetched).length} fetched | cumulative: ${Object.keys(updates).length} with metadata / ${this._seenIds.size} seen`);
9882
+ return fetched;
9883
+ }
9884
+ /** Executes a reduced document-info query (id/path/suffix/size only), merges
9885
+ * into `documentInfoMap`, and returns newly fetched entries. */
9886
+ async _fetchSimpleDocumentInfoBatch(uuids) {
9887
+ const values = uuids.map((id) => `r:${id}`).join(" ");
9888
+ const q = `PREFIX qcy: <${qaecyPrefixes["qcy"]}>
9889
+ PREFIX r: <${this.baseURL}>
9890
+ SELECT ?id ?suffix ?size (SAMPLE(?fp) AS ?path)
9891
+ WHERE {
9892
+ VALUES ?contentIRI { ${values} }
9893
+ ?contentIRI qcy:sizeBytes ?size ;
9894
+ qcy:hasFileLocation ?loc .
9895
+ ?loc qcy:filePath ?fp ;
9896
+ qcy:suffix ?suffix .
9897
+ BIND(REPLACE(STR(?contentIRI), "^.*/", "") AS ?id)
9898
+ }
9899
+ GROUP BY ?id ?suffix ?size`;
9900
+ const result = await this._api.sparql(q, this._projectId, this._graphType);
9901
+ const updates = { ...this._documentInfoMap.get() };
9902
+ const fetched = {};
9903
+ result.results.bindings.forEach((b) => {
9904
+ if (!b["id"])
9905
+ return;
9906
+ const id = b["id"].value;
9907
+ const existing = updates[id];
9908
+ const info = {
9909
+ id,
9910
+ contentIRI: this._resourceIri(id),
9911
+ path: b["path"]?.value ?? existing?.path ?? id,
9912
+ suffix: b["suffix"]?.value ?? existing?.suffix ?? "",
9913
+ size: b["size"] ? parseInt(b["size"].value, 10) : existing?.size ?? 0,
9914
+ tags: existing?.tags ?? [],
9915
+ categories: existing?.categories ?? [],
9916
+ subject: existing?.subject,
9917
+ summary: existing?.summary,
9918
+ providerId: existing?.providerId
9919
+ };
9920
+ updates[id] = info;
9921
+ fetched[id] = info;
9922
+ });
9923
+ this._documentInfoMap.set(updates);
9924
+ this._log(`fetchSimpleDocumentInfoBatch: ${Object.keys(fetched).length} fetched | cumulative: ${Object.keys(updates).length} with metadata / ${this._seenIds.size} seen`);
9925
+ return fetched;
9926
+ }
9927
+ async _fetchDocumentsBySuffix() {
9928
+ return this._runDocumentsBySuffixQuery(this._buildDocumentsBySuffixQuery());
9929
+ }
9930
+ _buildDocumentsBySuffixQuery() {
9931
+ return `PREFIX qcy: <${qaecyPrefixes["qcy"]}>
9932
+ SELECT ?ext (SUM(?size) AS ?totalSize) (COUNT(*) AS ?docCount)
9933
+ WHERE {
9934
+ {
9935
+ SELECT DISTINCT ?fc ?ext ?size
9936
+ WHERE {
9937
+ ?fc a qcy:FileContent ;
9938
+ qcy:sizeBytes ?size ;
9939
+ qcy:hasFileLocation/qcy:suffix ?ext .
9940
+ FILTER NOT EXISTS { ?x qcy:alternativeRepresentation ?fc }
9941
+ }
9942
+ }
9943
+ }
9944
+ GROUP BY ?ext
9945
+ ORDER BY DESC(?docCount)`;
9946
+ }
9947
+ async _runDocumentsBySuffixQuery(q) {
9948
+ const data = await this._api.sparql(q, this._projectId, this._graphType);
9949
+ const result = {};
9950
+ data.results.bindings.forEach((b) => {
9951
+ if (!b["ext"])
9952
+ return;
9953
+ result[b["ext"].value] = {
9954
+ size: b["totalSize"] ? parseInt(b["totalSize"].value, 10) : 0,
9955
+ count: b["docCount"] ? parseInt(b["docCount"].value, 10) : 0
9956
+ };
9957
+ });
9958
+ return result;
9959
+ }
9960
+ async _fetchDocumentsByContentCategory() {
9961
+ return this._runDocumentsByContentCategoryQuery(this._buildDocumentsByContentCategoryQuery());
9962
+ }
9963
+ _buildDocumentsByContentCategoryQuery() {
9964
+ return `PREFIX qcy: <${qaecyPrefixes["qcy"]}>
9965
+ SELECT ?cat (SUM(?size) AS ?totalSize) (COUNT(*) AS ?docCount)
9966
+ WHERE {
9967
+ {
9968
+ SELECT DISTINCT ?fc ?cat ?size
9969
+ WHERE {
9970
+ ?fc a qcy:FileContent ;
9971
+ qcy:hasContentCategory ?cat ;
9972
+ qcy:sizeBytes ?size .
9973
+ FILTER NOT EXISTS { ?x qcy:alternativeRepresentation ?fc }
9974
+ }
9975
+ }
9976
+ }
9977
+ GROUP BY ?cat
9978
+ ORDER BY DESC(?docCount)`;
9979
+ }
9980
+ async _runDocumentsByContentCategoryQuery(q) {
9981
+ const data = await this._api.sparql(q, this._projectId, this._graphType);
9982
+ const result = {};
9983
+ data.results.bindings.forEach((b) => {
9984
+ if (!b["cat"])
9985
+ return;
9986
+ result[b["cat"].value] = {
9987
+ size: b["totalSize"] ? parseInt(b["totalSize"].value, 10) : 0,
9988
+ count: b["docCount"] ? parseInt(b["docCount"].value, 10) : 0
9989
+ };
9990
+ });
9991
+ return result;
9992
+ }
9993
+ async _fetchDuplicateCount() {
9994
+ return this._runDuplicateCountQuery(this._buildDuplicateCountQuery());
9995
+ }
9996
+ _buildDuplicateCountQuery() {
9997
+ return `PREFIX qcy: <${qaecyPrefixes["qcy"]}>
9998
+ SELECT (COUNT(*) AS ?count)
9999
+ WHERE {
10000
+ SELECT ?fc
10001
+ WHERE {
10002
+ ?fc a qcy:FileContent ;
10003
+ qcy:hasFileLocation ?fl .
10004
+ FILTER NOT EXISTS { ?x qcy:alternativeRepresentation ?fc }
10005
+ }
10006
+ GROUP BY ?fc
10007
+ HAVING (COUNT(?fl) > 1)
10008
+ }`;
10009
+ }
10010
+ async _runDuplicateCountQuery(q) {
10011
+ const data = await this._api.sparql(q, this._projectId, this._graphType);
10012
+ const first = data.results.bindings[0];
10013
+ return first?.["count"] ? parseInt(first["count"].value, 10) : 0;
10014
+ }
10015
+ };
10016
+
10017
+ // libs/js/cue-sdk/src/lib/project-view.ts
10018
+ var CueProjectView = class {
10019
+ constructor(_api, _projectId, { language, queryCache, rdfBase = RESOURCE_BASE, graphType, verbose }) {
10020
+ this._api = _api;
10021
+ this._projectId = _projectId;
10022
+ this.schema = new CueProjectSchema(_api, _projectId, language, queryCache, graphType);
10023
+ this.entities = new CueProjectEntities(_api, _projectId, rdfBase, queryCache, graphType, verbose);
10024
+ this.documents = new CueProjectDocuments(_api, _projectId, language, rdfBase, queryCache, graphType, verbose);
10025
+ this.availableContentCategories = this.schema.availableContentCategories;
10026
+ this.availableEntityCategories = this.schema.availableEntityCategories;
10027
+ this.availableEntityRelationships = this.schema.availableEntityRelationships;
10028
+ this.schemaReady = this.schema.ready;
10029
+ this.entityInfoMap = this.entities.entityInfoMap;
10030
+ this.entityGraph = this.entities.entityGraph;
10031
+ this.documentInfoMap = this.documents.documentInfoMap;
10032
+ this.projectDocumentsData = this.documents.projectDocumentsData;
10033
+ this.searchResults = this._searchResults.asReadonly();
10034
+ this.documents.fetchOverview().catch((err) => console.error("[CueProjectView] fetchOverview failed:", err));
10035
+ }
10036
+ /** Direct access to the schema data class (available categories / relationships). */
10037
+ schema;
10038
+ /** Direct access to the entity data class. */
10039
+ entities;
10040
+ /** Direct access to the document data class. */
10041
+ documents;
10042
+ // ── Proxied signals ────────────────────────────────────────────────────────
10043
+ /** Available content category definitions for this project. Auto-fetched on init. */
10044
+ availableContentCategories;
10045
+ /** Available entity category definitions for this project. Auto-fetched on init. */
10046
+ availableEntityCategories;
10047
+ /** Available entity relationship types. Auto-fetched on init. */
10048
+ availableEntityRelationships;
10049
+ /**
10050
+ * Resolves when the initial schema load has completed. Await before reading
10051
+ * schema signal values imperatively.
10052
+ */
10053
+ schemaReady;
10054
+ /** Merged per-entity detail map. Populated lazily via `requestEntityData()` etc. */
10055
+ entityInfoMap;
10056
+ /** Project-level entity co-occurrence graph. Fetched once on init. */
10057
+ entityGraph;
10058
+ /** Per-document info map. Populated lazily via `requestDocumentData()`. */
10059
+ documentInfoMap;
10060
+ /** Project document overview (counts by suffix and category). Fetched on init. */
10061
+ projectDocumentsData;
10062
+ // ── Search state ───────────────────────────────────────────────────────────
10063
+ _searchResults = new CueSignal(void 0);
10064
+ /** The result of the most recent `search()` call. `undefined` before first search. */
10065
+ searchResults;
10066
+ _destroyed = false;
10067
+ // ── Entity methods ─────────────────────────────────────────────────────────
10068
+ /**
10069
+ * Lazily batch-fetch core data (label + categories) for the given entity UUIDs.
10070
+ * Already-fetched UUIDs are skipped. Populates `entityInfoMap`.
10071
+ */
10072
+ requestEntityData(uuids, includeMentionCount = false) {
10073
+ if (this._destroyed)
10074
+ return;
10075
+ this.entities.requestEntityData(uuids, includeMentionCount);
10076
+ }
10077
+ /**
10078
+ * Lazily fetch OSM location data for the given entity UUIDs.
10079
+ * Already-fetched UUIDs are skipped. Populates `entityInfoMap` geometry fields.
10080
+ */
10081
+ async requestEntityLocations(uuids) {
10082
+ if (this._destroyed)
10083
+ return;
10084
+ return this.entities.requestEntityLocations(uuids);
10085
+ }
10086
+ /**
10087
+ * Fetch incoming and outgoing relationships for a single entity IRI.
10088
+ * Result is stored in `entityInfoMap[uuid].relationshipData`.
10089
+ */
10090
+ async fetchEntityRelationships(iri) {
10091
+ if (this._destroyed)
10092
+ throw new Error("CueProjectView is destroyed");
10093
+ return this.entities.fetchEntityRelationships(iri);
10094
+ }
10095
+ /**
10096
+ * Fetch UUIDs of documents that reference the given entity IRI.
10097
+ * Result is stored in `entityInfoMap[uuid].documentRefs`.
10098
+ */
10099
+ async fetchEntityDocuments(iri) {
10100
+ if (this._destroyed)
10101
+ throw new Error("CueProjectView is destroyed");
10102
+ return this.entities.fetchEntityDocuments(iri);
10103
+ }
10104
+ /** Constructs the full RDF IRI for an entity UUID. */
10105
+ entityIri(uuid) {
10106
+ return this.entities.entityIri(uuid);
10107
+ }
10108
+ // ── Document methods ───────────────────────────────────────────────────────
10109
+ /**
10110
+ * Lazily batch-fetch document info for the given UUIDs.
10111
+ * Already-fetched UUIDs are skipped. Populates `documentInfoMap`.
10112
+ */
10113
+ requestDocumentData(uuids) {
10114
+ if (this._destroyed)
10115
+ return;
10116
+ this.documents.requestDocumentData(uuids);
10117
+ }
10118
+ // ── Search ─────────────────────────────────────────────────────────────────
10119
+ /**
10120
+ * Run a natural-language search against the project.
10121
+ * The result is stored in `searchResults` and replaces any previous result.
10122
+ */
10123
+ async search(term, options) {
10124
+ if (this._destroyed)
10125
+ return;
10126
+ const result = await this._api.search({
10127
+ term,
10128
+ projectId: this._projectId,
10129
+ categories: options?.categories
10130
+ });
10131
+ if (!this._destroyed) {
10132
+ this._searchResults.set(result);
10133
+ }
10134
+ }
10135
+ // ── Lifecycle ──────────────────────────────────────────────────────────────
10136
+ /**
10137
+ * Switch the active language for schema labels and document text fields.
10138
+ * Schema responses are cached per language (instant if previously loaded).
10139
+ * The document info map is cleared and lazily re-populated on next access.
10140
+ */
10141
+ setLanguage(lang) {
10142
+ if (this._destroyed)
10143
+ return;
10144
+ this.schema.setLanguage(lang);
10145
+ this.documents.setLanguage(lang);
10146
+ }
10147
+ /**
10148
+ * Reset all entity and document state and re-fetch the project overview.
10149
+ * Prefer creating a fresh `CueProjectView` when switching projects.
10150
+ * Use `reset()` only when the same project's data needs to be invalidated.
10151
+ */
10152
+ reset() {
10153
+ if (this._destroyed)
10154
+ return;
10155
+ this.entities.reset();
10156
+ this.documents.reset();
10157
+ this._searchResults.set(void 0);
10158
+ this.documents.fetchOverview().catch((err) => console.error("[CueProjectView] fetchOverview failed after reset:", err));
10159
+ }
10160
+ /**
10161
+ * Tear down this view instance. Clears all reactive state and blocks further
10162
+ * updates. Call from the Angular adapter's `ngOnDestroy` or equivalent.
10163
+ */
10164
+ destroy() {
10165
+ this._destroyed = true;
10166
+ this._searchResults.set(void 0);
10167
+ }
9894
10168
  };
9895
10169
 
9896
- // libs/js/models/src/lib/project.ts
9897
- var import_uuid3 = require("uuid");
9898
-
9899
- // libs/js/models/src/lib/search-log-entry.ts
9900
- var import_uuid4 = require("uuid");
9901
-
9902
- // libs/js/models/src/lib/search-response.ts
9903
- var import_uuid5 = require("uuid");
10170
+ // libs/js/file-metadata-helpers/src/lib/js-file-metadata-helpers.ts
10171
+ init_src();
9904
10172
 
9905
10173
  // libs/js/rdf-document-writers/src/lib/alternative-representation.ts
9906
10174
  var import_n33 = require("n3");
@@ -10700,6 +10968,7 @@ var Cue = class _Cue {
10700
10968
  _storageProcessed;
10701
10969
  _gis = null;
10702
10970
  _projectDocuments = /* @__PURE__ */ new Map();
10971
+ _verbose;
10703
10972
  /**
10704
10973
  * Reactive GIS service. Lazily constructed on first access.
10705
10974
  *
@@ -10733,6 +11002,7 @@ var Cue = class _Cue {
10733
11002
  const env = config.environment ?? "production";
10734
11003
  this._endpoints = { ...ENDPOINTS[env], ...config.endpoints };
10735
11004
  this._isEmulator = env === "emulator";
11005
+ this._verbose = config.verbose ?? false;
10736
11006
  this._app = (0, import_app2.getApps)().find((a5) => a5.name === "[DEFAULT]") ?? (0, import_app2.initializeApp)({
10737
11007
  apiKey,
10738
11008
  appId,
@@ -10861,7 +11131,7 @@ var Cue = class _Cue {
10861
11131
  get: (key) => this.cache.getQueryCache(projectId, key).then((entry) => entry?.results),
10862
11132
  set: (key, data) => this.cache.setQueryCache(projectId, key, { query: key, results: data })
10863
11133
  };
10864
- return new CueProjectView(this.api, projectId, { ...opts, queryCache });
11134
+ return new CueProjectView(this.api, projectId, { verbose: this._verbose, ...opts, queryCache });
10865
11135
  }
10866
11136
  /**
10867
11137
  * Creates a `CueProjectEntities` instance for the given project, with the
@@ -10890,7 +11160,8 @@ var Cue = class _Cue {
10890
11160
  projectId,
10891
11161
  opts?.rdfBase,
10892
11162
  queryCache,
10893
- opts?.graphType
11163
+ opts?.graphType,
11164
+ opts?.verbose ?? this._verbose
10894
11165
  );
10895
11166
  }
10896
11167
  /**
@@ -10925,7 +11196,8 @@ var Cue = class _Cue {
10925
11196
  this.api.language,
10926
11197
  void 0,
10927
11198
  queryCache2,
10928
- void 0
11199
+ void 0,
11200
+ opts?.verbose ?? this._verbose
10929
11201
  );
10930
11202
  this._projectDocuments.set(projectId, docs);
10931
11203
  return docs;
@@ -10940,7 +11212,8 @@ var Cue = class _Cue {
10940
11212
  opts?.language ?? this.api.language,
10941
11213
  opts?.rdfBase,
10942
11214
  queryCache,
10943
- opts?.graphType
11215
+ opts?.graphType,
11216
+ opts?.verbose ?? this._verbose
10944
11217
  );
10945
11218
  }
10946
11219
  };
@@ -12301,6 +12574,10 @@ async function buildCueClient(options) {
12301
12574
  console.error("API key is required. Provide it via --key or CUE_API_KEY env variable.");
12302
12575
  process.exit(1);
12303
12576
  }
12577
+ if (options.verbose) {
12578
+ const source = options.key ? "--key flag" : "CUE_API_KEY env variable";
12579
+ console.error(`Authenticating with API key from ${source}...`);
12580
+ }
12304
12581
  const cue = new CueNode({
12305
12582
  apiKey: FIREBASE_CONFIG().apiKey,
12306
12583
  appId: FIREBASE_CONFIG().appId,
@@ -12336,21 +12613,32 @@ async function entitySummaryGraphHandler(options) {
12336
12613
  try {
12337
12614
  const cue = await buildCueClient(options);
12338
12615
  if (options.verbose)
12339
- console.error(`Fetching entity summary graph for project ${options.space}...`);
12616
+ console.error(`Fetching entity summary graph for project ${options.space}${options.entity ? ` (neighbourhood of ${options.entity})` : ""}...`);
12340
12617
  const entities = new CueProjectEntities(cue.api, options.space);
12341
12618
  if (options.format === "graph") {
12342
- const graph = await entities.buildSummaryGraph("graph");
12619
+ const graph = await entities.buildSummaryGraph("graph", options.entity);
12343
12620
  console.log(JSON.stringify(graph, null, 2));
12344
12621
  } else if (options.format === "md") {
12345
- const md = await entities.buildSummaryGraph("md");
12622
+ const md = await entities.buildSummaryGraph("md", options.entity);
12346
12623
  console.log(md);
12347
12624
  } else {
12348
- const raw = await entities.buildSummaryGraph();
12625
+ const raw = await entities.buildSummaryGraph(void 0, options.entity);
12349
12626
  console.log(JSON.stringify(raw, null, 2));
12350
12627
  }
12351
12628
  process.exit(0);
12352
12629
  } catch (err) {
12353
- console.error("Error:", err instanceof Error ? err.message : String(err));
12630
+ const msg = err instanceof Error ? err.message : String(err);
12631
+ if (msg.includes("403")) {
12632
+ console.error(`Error: ${msg}`);
12633
+ console.error(
12634
+ `Hint: 403 Forbidden usually means the API key does not have access to project "${options.space}".
12635
+ - If targeting a local environment, add the -e / --emulators flag.
12636
+ - If using a production project, verify the API key has access.
12637
+ - API key source: ${options.key ? "--key flag" : "CUE_API_KEY env variable"}`
12638
+ );
12639
+ } else {
12640
+ console.error("Error:", msg);
12641
+ }
12354
12642
  process.exit(1);
12355
12643
  }
12356
12644
  }
@@ -12384,7 +12672,7 @@ async function sparqlHandler(options) {
12384
12672
  }
12385
12673
  var appBuilderToolsCommand = new import_commander.Command("app-builder-tools").description("Tools for agent-assisted Cue app development. Outputs JSON for machine consumption.");
12386
12674
  appBuilderToolsCommand.command("list-projects").description("List all projects accessible to the authenticated user").option("-k, --key <api-key>", "Specify the API key (or set CUE_API_KEY env variable)").option("-e, --emulators", "Use emulators", false).option("-v, --verbose", "Enable verbose output", false).action(listProjectsHandler);
12387
- appBuilderToolsCommand.command("entity-summary-graph").description("Fetch the entity category relationship summary graph for a project").requiredOption("-s, --space <id>", "Project ID (required)").option("-k, --key <api-key>", "Specify the API key (or set CUE_API_KEY env variable)").option("-e, --emulators", "Use emulators", false).option("-f, --format <format>", "Output format: md (aligned text), graph (JSON nodes+edges), json (raw SPARQL)", "md").option("-v, --verbose", "Enable verbose output", false).action(entitySummaryGraphHandler);
12675
+ appBuilderToolsCommand.command("entity-summary-graph").description("Fetch the entity category relationship summary graph for a project").requiredOption("-s, --space <id>", "Project ID (required)").option("-k, --key <api-key>", "Specify the API key (or set CUE_API_KEY env variable)").option("-e, --emulators", "Use emulators", false).option("-f, --format <format>", "Output format: md (aligned text), graph (JSON nodes+edges), json (raw SPARQL)", "md").option("--entity <iri>", "Filter to the one-hop neighbourhood of this category IRI (prefixed or full, e.g. qcy:Building)").option("-v, --verbose", "Enable verbose output", false).action(entitySummaryGraphHandler);
12388
12676
  appBuilderToolsCommand.command("sparql").description("Execute a SPARQL SELECT query against a project triplestore. Query via --query or stdin.").requiredOption("-s, --space <id>", "Project ID (required)").option("-q, --query <sparql>", "SPARQL query string (or pipe query via stdin)").option("-k, --key <api-key>", "Specify the API key (or set CUE_API_KEY env variable)").option("-e, --emulators", "Use emulators", false).option("-v, --verbose", "Enable verbose output", false).action(sparqlHandler);
12389
12677
 
12390
12678
  // apps/desktop/cue-cli/src/main.ts