@objectstack/objectql 9.2.0 → 9.3.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/index.js CHANGED
@@ -950,6 +950,22 @@ function applySystemFields(schema, opts) {
950
950
  fields: { ...additions, ...schema.fields ?? {} }
951
951
  };
952
952
  }
953
+ var SYS_METADATA_OWNER = "sys_metadata";
954
+ function isRealPackage(pkg) {
955
+ return typeof pkg === "string" && pkg.length > 0 && pkg !== SYS_METADATA_OWNER;
956
+ }
957
+ var MetadataCollisionError = class extends Error {
958
+ constructor(type, name, existingPackageId, incomingPackageId) {
959
+ super(
960
+ `Cross-package metadata collision: ${type}/${name} is registered by package "${existingPackageId}" and package "${incomingPackageId}". Bare-named ${type} metadata has no package coordinate in the registry, so the second registration would silently shadow the first (last-write-wins at read time). Rename one of them (a namespace prefix such as "<namespace>_${name}" is recommended), or, if this is a deliberate migration, set OS_METADATA_COLLISION=warn to downgrade to a warning. See ADR-0048.`
961
+ );
962
+ this.name = "MetadataCollisionError";
963
+ this.type = type;
964
+ this.name_ = name;
965
+ this.existingPackageId = existingPackageId;
966
+ this.incomingPackageId = incomingPackageId;
967
+ }
968
+ };
953
969
  var SchemaRegistry = class {
954
970
  constructor(options = {}) {
955
971
  // ==========================================
@@ -993,6 +1009,7 @@ var SchemaRegistry = class {
993
1009
  } else {
994
1010
  this.multiTenant = String((0, import_types.readEnvWithDeprecation)("OS_MULTI_ORG_ENABLED", "OS_MULTI_TENANT") ?? "false").toLowerCase() !== "false";
995
1011
  }
1012
+ this.collisionPolicy = options.collisionPolicy ?? ((process.env.OS_METADATA_COLLISION ?? "").toLowerCase() === "warn" ? "warn" : "error");
996
1013
  }
997
1014
  get logLevel() {
998
1015
  return this._logLevel;
@@ -1300,6 +1317,17 @@ var SchemaRegistry = class {
1300
1317
  if (collection.has(storageKey)) {
1301
1318
  this.log(`[Registry] Overwriting ${type}: ${storageKey}`);
1302
1319
  }
1320
+ if (isRealPackage(packageId)) {
1321
+ const conflictOwner = this.findOtherPackageOwner(collection, baseName, packageId);
1322
+ if (conflictOwner) {
1323
+ const err = new MetadataCollisionError(type, baseName, conflictOwner, packageId);
1324
+ if (this.collisionPolicy === "warn") {
1325
+ console.warn(`[Registry] ${err.message}`);
1326
+ } else {
1327
+ throw err;
1328
+ }
1329
+ }
1330
+ }
1303
1331
  if (packageId && collection.has(baseName)) {
1304
1332
  const dbOnly = collection.get(baseName);
1305
1333
  if (dbOnly && !dbOnly._packageId) {
@@ -1311,6 +1339,22 @@ var SchemaRegistry = class {
1311
1339
  collection.set(storageKey, item);
1312
1340
  this.log(`[Registry] Registered ${type}: ${storageKey}`);
1313
1341
  }
1342
+ /**
1343
+ * Find a code package OTHER than `incoming` that already owns `baseName` in
1344
+ * `collection` (ADR-0048 cross-package collision detection). Scans the live
1345
+ * collection — like {@link getItem} / {@link unregisterItem} — so it always
1346
+ * reflects current state with no parallel index to drift across
1347
+ * reset/unregister. Returns the conflicting owner's package id, or undefined
1348
+ * when the name is free or only held by the same package / a runtime overlay.
1349
+ */
1350
+ findOtherPackageOwner(collection, baseName, incoming) {
1351
+ for (const [key, item] of collection) {
1352
+ if (key !== baseName && !key.endsWith(`:${baseName}`)) continue;
1353
+ const owner = item?._packageId;
1354
+ if (isRealPackage(owner) && owner !== incoming) return owner;
1355
+ }
1356
+ return void 0;
1357
+ }
1314
1358
  /**
1315
1359
  * Validate Metadata against Spec Zod Schemas
1316
1360
  */
@@ -1370,6 +1414,70 @@ var SchemaRegistry = class {
1370
1414
  }
1371
1415
  return void 0;
1372
1416
  }
1417
+ /**
1418
+ * Artifact-only lookup (ADR-0010 §3.3). Unlike {@link getItem} — which
1419
+ * returns the plain-key entry first, so a runtime/DB-rehydrated row
1420
+ * registered under the bare name SHADOWS the packaged artifact — this
1421
+ * scans the composite (`<packageId>:<name>`) entries first and only
1422
+ * returns an item whose `_packageId` marks a genuine code package
1423
+ * (truthy and not the `'sys_metadata'` rehydration sentinel).
1424
+ *
1425
+ * This is what the protocol's lock/provenance resolution must use:
1426
+ * the artifact's `_lock` envelope always wins over an overlay, and an
1427
+ * overlay row hydrated into the plain key must never be able to mask
1428
+ * it (that masking is exactly the "registry pollution" bug where a
1429
+ * locked app's `_lock` read back as undefined after a PUT+GET).
1430
+ */
1431
+ getArtifactItem(type, name) {
1432
+ if (type === "object" || type === "objects") {
1433
+ const obj = this.getObject(name);
1434
+ return obj && obj._packageId && obj._packageId !== "sys_metadata" ? obj : void 0;
1435
+ }
1436
+ const collection = this.metadata.get(type);
1437
+ if (!collection) return void 0;
1438
+ for (const [key, item] of collection) {
1439
+ if (key !== name && key.endsWith(`:${name}`)) {
1440
+ const it = item;
1441
+ if (it && it._packageId && it._packageId !== "sys_metadata") return item;
1442
+ }
1443
+ }
1444
+ const direct = collection.get(name);
1445
+ if (direct && direct._packageId && direct._packageId !== "sys_metadata") {
1446
+ return direct;
1447
+ }
1448
+ return void 0;
1449
+ }
1450
+ /**
1451
+ * Remove a plain-key runtime shadow so the packaged artifact registered
1452
+ * under a composite key becomes the visible value again. Used by the
1453
+ * metadata reset path (`deleteMetaItem`): deleting the `sys_metadata`
1454
+ * overlay row must also heal the in-memory registry, otherwise the
1455
+ * stale overlay copy keeps shadowing the artifact until restart.
1456
+ *
1457
+ * Deliberately conservative: the plain-key entry is only deleted when a
1458
+ * packaged artifact still exists under a composite key, so the name
1459
+ * stays resolvable afterwards. A runtime-only item (no artifact
1460
+ * backing) is left untouched. Note the plain entry's own `_packageId`
1461
+ * is NOT consulted — the hydration path grafts the artifact envelope
1462
+ * onto the shadow (ADR-0010 §3.3), so a stamped `_packageId` does not
1463
+ * mean the plain entry IS the artifact registration; artifact loaders
1464
+ * always register under a composite key.
1465
+ */
1466
+ removeRuntimeShadow(type, name) {
1467
+ const collection = this.metadata.get(type);
1468
+ if (!collection || !collection.has(name)) return false;
1469
+ for (const [key, item] of collection) {
1470
+ if (key !== name && key.endsWith(`:${name}`)) {
1471
+ const it = item;
1472
+ if (it && it._packageId && it._packageId !== "sys_metadata") {
1473
+ collection.delete(name);
1474
+ this.log(`[Registry] Removed runtime shadow ${type}: ${name} (artifact ${it._packageId} restored)`);
1475
+ return true;
1476
+ }
1477
+ }
1478
+ }
1479
+ return false;
1480
+ }
1373
1481
  /**
1374
1482
  * Universal List Method
1375
1483
  */
@@ -2394,6 +2502,48 @@ function decorateMetadataItems(type, items) {
2394
2502
  if (!Array.isArray(items)) return items;
2395
2503
  return items.map((item) => decorateMetadataItem(type, item));
2396
2504
  }
2505
+ function fieldMap(objectDef) {
2506
+ const map = /* @__PURE__ */ new Map();
2507
+ const fields = objectDef?.fields;
2508
+ if (Array.isArray(fields)) {
2509
+ for (const f of fields) if (f?.name) map.set(f.name, f);
2510
+ } else if (fields && typeof fields === "object") {
2511
+ for (const [name, f] of Object.entries(fields)) map.set(name, f ?? {});
2512
+ }
2513
+ return map;
2514
+ }
2515
+ function computeViewReferenceDiagnostics(view, objectDef) {
2516
+ const fields = fieldMap(objectDef);
2517
+ const errors = [];
2518
+ const requireField = (name, path) => {
2519
+ if (typeof name !== "string" || !name) return;
2520
+ if (!fields.has(name)) {
2521
+ errors.push({
2522
+ path,
2523
+ message: `Field "${name}" does not exist on the source object`,
2524
+ code: "reference_not_found"
2525
+ });
2526
+ }
2527
+ };
2528
+ const userFilters = view?.userFilters;
2529
+ userFilters?.fields?.forEach((f, i) => requireField(f?.field, `userFilters.fields.${i}.field`));
2530
+ userFilters?.tabs?.forEach((t, i) => t?.filter?.forEach((r, j) => requireField(r?.field, `userFilters.tabs.${i}.filter.${j}.field`)));
2531
+ view?.tabs?.forEach((t, i) => t?.filter?.forEach((r, j) => requireField(r?.field, `tabs.${i}.filter.${j}.field`)));
2532
+ view?.filterableFields?.forEach((f, i) => requireField(f, `filterableFields.${i}`));
2533
+ const kanban = view?.kanban;
2534
+ if (kanban?.groupByField) {
2535
+ requireField(kanban.groupByField, "kanban.groupByField");
2536
+ const def = fields.get(kanban.groupByField);
2537
+ if (def && def.type && !["select", "multi-select", "boolean", "lookup", "master_detail"].includes(def.type)) {
2538
+ errors.push({
2539
+ path: "kanban.groupByField",
2540
+ message: `Field "${kanban.groupByField}" (type "${def.type}") cannot group a kanban \u2014 use a select-like field`,
2541
+ code: "invalid_binding"
2542
+ });
2543
+ }
2544
+ }
2545
+ return errors.length ? { valid: false, errors } : { valid: true };
2546
+ }
2397
2547
 
2398
2548
  // src/protocol.ts
2399
2549
  var TYPE_TO_FORM = import_system.METADATA_FORM_REGISTRY;
@@ -3216,8 +3366,13 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
3216
3366
  }
3217
3367
  byName.set(data.name, data);
3218
3368
  }
3219
- if (this.environmentId === void 0) {
3220
- this.engine.registry.registerItem(request.type, data, "name");
3369
+ if (this.environmentId === void 0 && data && typeof data === "object") {
3370
+ const artifact = this.lookupArtifactItem(request.type, data.name);
3371
+ this.engine.registry.registerItem(
3372
+ request.type,
3373
+ mergeArtifactProtection(data, artifact),
3374
+ "name"
3375
+ );
3221
3376
  }
3222
3377
  }
3223
3378
  items = Array.from(byName.values());
@@ -3423,10 +3578,34 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
3423
3578
  item = this.engine.registry.applyNavContributions(item);
3424
3579
  }
3425
3580
  const artifactItem = this.lookupArtifactItem(request.type, request.name);
3426
- const decorated = decorateMetadataItem(
3581
+ let decorated = decorateMetadataItem(
3427
3582
  request.type,
3428
3583
  mergeArtifactProtection(item, artifactItem)
3429
3584
  );
3585
+ if ((request.type === "view" || request.type === "views") && decorated && typeof decorated === "object") {
3586
+ try {
3587
+ const viewDoc = decorated;
3588
+ const sourceObject = viewDoc?.object ?? viewDoc?.data?.object ?? viewDoc?.objectName ?? viewDoc?.list?.data?.object;
3589
+ const objectDef = typeof sourceObject === "string" ? this.engine.registry.getObject(sourceObject) : void 0;
3590
+ if (objectDef) {
3591
+ const refs = computeViewReferenceDiagnostics(viewDoc, objectDef);
3592
+ if (!refs.valid) {
3593
+ const prior = viewDoc._diagnostics;
3594
+ decorated = {
3595
+ ...viewDoc,
3596
+ _diagnostics: {
3597
+ valid: false,
3598
+ errors: [
3599
+ ...prior && prior.valid === false && Array.isArray(prior.errors) ? prior.errors : [],
3600
+ ...refs.errors ?? []
3601
+ ]
3602
+ }
3603
+ };
3604
+ }
3605
+ }
3606
+ } catch {
3607
+ }
3608
+ }
3430
3609
  const artifactBacked = this.isArtifactBacked(request.type, request.name);
3431
3610
  const lockState = (0, import_kernel5.resolveLockState)(decorated, artifactBacked);
3432
3611
  return {
@@ -3476,7 +3655,7 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
3476
3655
  } catch {
3477
3656
  }
3478
3657
  if (code === null) {
3479
- let regItem = this.engine.registry.getItem(request.type, request.name);
3658
+ let regItem = this.lookupArtifactItem(request.type, request.name) ?? this.engine.registry.getItem(request.type, request.name);
3480
3659
  if (regItem === void 0) {
3481
3660
  const alt = import_shared4.PLURAL_TO_SINGULAR[request.type] ?? import_shared4.SINGULAR_TO_PLURAL[request.type];
3482
3661
  if (alt) regItem = this.engine.registry.getItem(alt, request.name);
@@ -4522,14 +4701,7 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
4522
4701
  * "authoring a DB-only item" (requires only `allowRuntimeCreate`).
4523
4702
  */
4524
4703
  isArtifactBacked(type, name) {
4525
- const registry = this.engine?.registry;
4526
- if (!registry || typeof registry.getItem !== "function") {
4527
- return false;
4528
- }
4529
- const singular = import_shared4.PLURAL_TO_SINGULAR[type] ?? type;
4530
- const item = registry.getItem(singular, name) ?? registry.getItem(type, name);
4531
- if (!item || !item._packageId) return false;
4532
- return item._packageId !== "sys_metadata";
4704
+ return this.lookupArtifactItem(type, name) !== void 0;
4533
4705
  }
4534
4706
  // ───────────────────────────────────────────────────────────────────
4535
4707
  // ADR-0010 — metadata protection (Phase 1: L3 item-level lock)
@@ -4541,9 +4713,17 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
4541
4713
  */
4542
4714
  lookupArtifactItem(type, name) {
4543
4715
  const registry = this.engine?.registry;
4544
- if (!registry || typeof registry.getItem !== "function") return void 0;
4716
+ if (!registry) return void 0;
4545
4717
  const singular = import_shared4.PLURAL_TO_SINGULAR[type] ?? type;
4546
- return registry.getItem(singular, name) ?? registry.getItem(type, name);
4718
+ if (typeof registry.getArtifactItem === "function") {
4719
+ return registry.getArtifactItem(singular, name) ?? registry.getArtifactItem(type, name);
4720
+ }
4721
+ if (typeof registry.getItem !== "function") return void 0;
4722
+ const item = registry.getItem(singular, name) ?? registry.getItem(type, name);
4723
+ if (!item || !item._packageId || item._packageId === "sys_metadata") {
4724
+ return void 0;
4725
+ }
4726
+ return item;
4547
4727
  }
4548
4728
  /**
4549
4729
  * Resolve the effective `_lock` for an item by consulting the
@@ -4557,13 +4737,8 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
4557
4737
  * scope and the caller is expected to also gate on `environmentId`.
4558
4738
  */
4559
4739
  async getEffectiveLock(type, name, organizationId) {
4560
- const registry = this.engine?.registry;
4561
- const singular = import_shared4.PLURAL_TO_SINGULAR[type] ?? type;
4562
- let artifactItem;
4563
- if (registry && typeof registry.getItem === "function") {
4564
- artifactItem = registry.getItem(singular, name) ?? registry.getItem(type, name);
4565
- }
4566
- if (artifactItem && artifactItem._packageId && artifactItem._packageId !== "sys_metadata") {
4740
+ const artifactItem = this.lookupArtifactItem(type, name);
4741
+ if (artifactItem) {
4567
4742
  const p = (0, import_kernel5.extractProtection)(artifactItem);
4568
4743
  if (p.lock !== "none") {
4569
4744
  return { lock: p.lock, lockReason: p.lockReason, lockSource: "artifact" };
@@ -4704,6 +4879,49 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
4704
4879
  );
4705
4880
  }
4706
4881
  }
4882
+ /**
4883
+ * Heal the in-memory registry after a metadata reset (overlay-row
4884
+ * delete) on control-plane kernels. Two layers:
4885
+ *
4886
+ * 1. Drop the plain-key runtime shadow so the packaged artifact
4887
+ * (registered under `<packageId>:<name>`) becomes the visible
4888
+ * value again. The shadow is written by the overlay-hydration
4889
+ * paths (`getMetaItems` / `loadMetaFromDb`) and — pre-fix —
4890
+ * survived the reset until restart, leaving stale overlay
4891
+ * content (and a stripped `_lock` envelope) in every
4892
+ * registry-direct read (ADR-0010 §3.3).
4893
+ * 2. When no composite-key artifact exists, fall back to the
4894
+ * MetadataService baseline (FilesystemLoader-sourced types) and
4895
+ * re-register it, preserving the historical refresh behaviour
4896
+ * for items the SchemaRegistry never held as artifacts.
4897
+ *
4898
+ * Best-effort: a failure must never block the delete that already
4899
+ * succeeded; the next full reload fixes the registry anyway.
4900
+ */
4901
+ async restoreArtifactRegistryView(type, name) {
4902
+ try {
4903
+ const registry = this.engine.registry;
4904
+ let healed = false;
4905
+ if (typeof registry.removeRuntimeShadow === "function") {
4906
+ const singular = import_shared4.PLURAL_TO_SINGULAR[type] ?? type;
4907
+ healed = registry.removeRuntimeShadow(singular, name);
4908
+ if (type !== singular) {
4909
+ healed = registry.removeRuntimeShadow(type, name) || healed;
4910
+ }
4911
+ }
4912
+ if (healed) return;
4913
+ if (this.environmentId !== void 0) return;
4914
+ const services = this.getServicesRegistry?.();
4915
+ const metadataService = services?.get("metadata");
4916
+ if (metadataService && typeof metadataService.get === "function") {
4917
+ const artifactItem = await metadataService.get(type, name);
4918
+ if (artifactItem !== void 0) {
4919
+ this.engine.registry.registerItem(type, artifactItem, "name");
4920
+ }
4921
+ }
4922
+ } catch {
4923
+ }
4924
+ }
4707
4925
  /**
4708
4926
  * Ensure a just-PUBLISHED object's physical table exists so it is usable
4709
4927
  * for data CRUD immediately — without a server restart. Registering the
@@ -5525,6 +5743,9 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
5525
5743
  const targetState = request.state === "draft" ? "draft" : "active";
5526
5744
  const current = await repo.get(ref, { state: targetState });
5527
5745
  if (!current) {
5746
+ if (targetState === "active") {
5747
+ await this.restoreArtifactRegistryView(request.type, request.name);
5748
+ }
5528
5749
  return {
5529
5750
  success: true,
5530
5751
  reset: false,
@@ -5539,18 +5760,8 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
5539
5760
  intent: this.isArtifactBacked(singularTypeForRepo, request.name) ? "override-artifact" : "runtime-only",
5540
5761
  state: targetState
5541
5762
  });
5542
- if (this.environmentId === void 0) {
5543
- try {
5544
- const services = this.getServicesRegistry?.();
5545
- const metadataService = services?.get("metadata");
5546
- if (metadataService && typeof metadataService.get === "function") {
5547
- const artifactItem = await metadataService.get(request.type, request.name);
5548
- if (artifactItem !== void 0) {
5549
- this.engine.registry.registerItem(request.type, artifactItem, "name");
5550
- }
5551
- }
5552
- } catch {
5553
- }
5763
+ if (targetState === "active") {
5764
+ await this.restoreArtifactRegistryView(request.type, request.name);
5554
5765
  }
5555
5766
  if (this.shouldDropStorage(request.type, request.name, request.dropStorage, targetState)) {
5556
5767
  await this.dropObjectStorage(singularTypeForRepo, request.name);
@@ -5609,18 +5820,8 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
5609
5820
  await this.dropObjectStorage(import_shared4.PLURAL_TO_SINGULAR[request.type] ?? request.type, request.name);
5610
5821
  }
5611
5822
  }
5612
- if (this.environmentId === void 0) {
5613
- try {
5614
- const services = this.getServicesRegistry?.();
5615
- const metadataService = services?.get("metadata");
5616
- if (metadataService && typeof metadataService.get === "function") {
5617
- const artifactItem = await metadataService.get(request.type, request.name);
5618
- if (artifactItem !== void 0) {
5619
- this.engine.registry.registerItem(request.type, artifactItem, "name");
5620
- }
5621
- }
5622
- } catch {
5623
- }
5823
+ if (request.state !== "draft") {
5824
+ await this.restoreArtifactRegistryView(request.type, request.name);
5624
5825
  }
5625
5826
  return {
5626
5827
  success: true,
@@ -5658,7 +5859,12 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
5658
5859
  if (normalizedType === "object") {
5659
5860
  this.engine.registry.registerObject(data, record.packageId || "sys_metadata");
5660
5861
  } else {
5661
- this.engine.registry.registerItem(normalizedType, data, "name");
5862
+ const artifact = this.lookupArtifactItem(normalizedType, data?.name);
5863
+ this.engine.registry.registerItem(
5864
+ normalizedType,
5865
+ mergeArtifactProtection(data, artifact),
5866
+ "name"
5867
+ );
5662
5868
  }
5663
5869
  loaded++;
5664
5870
  } catch (e) {
@@ -7625,7 +7831,9 @@ var _ObjectQL = class _ObjectQL {
7625
7831
  "mappings",
7626
7832
  "analyticsCubes",
7627
7833
  // Integration Protocol
7628
- "connectors"
7834
+ "connectors",
7835
+ // System Protocol — package documentation (ADR-0046); inert data
7836
+ "docs"
7629
7837
  ];
7630
7838
  for (const key of metadataArrayKeys) {
7631
7839
  const items = manifest[key];
@@ -7761,7 +7969,8 @@ var _ObjectQL = class _ObjectQL {
7761
7969
  "hooks",
7762
7970
  "mappings",
7763
7971
  "analyticsCubes",
7764
- "connectors"
7972
+ "connectors",
7973
+ "docs"
7765
7974
  ];
7766
7975
  for (const key of metadataArrayKeys) {
7767
7976
  const items = plugin[key];
@@ -9290,10 +9499,11 @@ var MetadataFacade = class {
9290
9499
  */
9291
9500
  async register(type, name, data) {
9292
9501
  const definition = typeof data === "object" && data !== null ? { ...data, name: data.name ?? name } : data;
9502
+ const packageId = definition?._packageId;
9293
9503
  if (type === "object") {
9294
- this.registry.registerItem(type, definition, "name");
9504
+ this.registry.registerItem(type, definition, "name", packageId);
9295
9505
  } else {
9296
- this.registry.registerItem(type, definition, definition.id ? "id" : "name");
9506
+ this.registry.registerItem(type, definition, definition.id ? "id" : "name", packageId);
9297
9507
  }
9298
9508
  }
9299
9509
  /**
@@ -9962,7 +10172,7 @@ var ObjectQLPlugin = class {
9962
10172
  return;
9963
10173
  }
9964
10174
  if (this.ql?.registry?.registerItem) {
9965
- this.ql.registry.registerItem(type, item, keyField);
10175
+ this.ql.registry.registerItem(type, item, keyField, item._packageId);
9966
10176
  }
9967
10177
  });
9968
10178
  if (type === "hook" && this.ql && typeof this.ql.bindHooks === "function") {