@stndrds/schema 0.1.0-alpha.56 → 0.1.0-alpha.57

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.
@@ -543,7 +543,7 @@ var TenantContextError = class _TenantContextError extends Error {
543
543
  }
544
544
  };
545
545
 
546
- // src/runtime/context/schema-context.ts
546
+ // src/runtime/context/feature-flags-context.ts
547
547
  var browserStub = {
548
548
  getStore: () => void 0,
549
549
  run: (_store, callback) => callback()
@@ -581,22 +581,106 @@ function getStorage() {
581
581
  storageInstance = browserStub;
582
582
  return storageInstance;
583
583
  }
584
- function getSchemaFromContext(objectId) {
584
+ var FeatureFlagsContextError = class _FeatureFlagsContextError extends Error {
585
+ constructor(message) {
586
+ super(
587
+ message ?? "No feature flags context found. This usually means:\n - HTTP request: Missing FeatureFlagsInterceptor\n - Background job: Wrap with runWithFeatureFlags()\n - Test: Wrap test body with runWithFeatureFlags()\n\nTip: FeatureFlagsInterceptor must run AFTER TenantContextInterceptor."
588
+ );
589
+ this.name = "FeatureFlagsContextError";
590
+ if ("captureStackTrace" in Error) {
591
+ Error.captureStackTrace(
592
+ this,
593
+ _FeatureFlagsContextError
594
+ );
595
+ }
596
+ }
597
+ };
598
+ function getContext() {
599
+ const ctx = getStorage().getStore();
600
+ if (!ctx) {
601
+ throw new FeatureFlagsContextError();
602
+ }
603
+ return ctx;
604
+ }
605
+ function isFeatureEnabled(flagName) {
606
+ return getContext().flags.get(flagName) === true;
607
+ }
608
+ function getFeatureValue(flagName, defaultValue) {
609
+ const value = getContext().flags.get(flagName);
610
+ return value !== void 0 ? value : defaultValue;
611
+ }
612
+ function getFeatureFlags() {
613
+ return getContext().flags;
614
+ }
615
+ function tryGetFeatureValue(flagName) {
585
616
  const ctx = getStorage().getStore();
617
+ return ctx?.flags.get(flagName);
618
+ }
619
+ function hasFeatureFlagsContext() {
620
+ return getStorage().getStore() !== void 0;
621
+ }
622
+ function runWithFeatureFlags(flags, fn) {
623
+ const frozenContext = Object.freeze({ flags });
624
+ return getStorage().run(frozenContext, fn);
625
+ }
626
+ function withFeatureFlags(resolvedFlags, fn) {
627
+ return runWithFeatureFlags(resolvedFlags, fn);
628
+ }
629
+
630
+ // src/runtime/context/schema-context.ts
631
+ var browserStub2 = {
632
+ getStore: () => void 0,
633
+ run: (_store, callback) => callback()
634
+ };
635
+ var AsyncLocalStorageClass2 = null;
636
+ if (typeof process !== "undefined" && process.versions?.node) {
637
+ try {
638
+ if (typeof __require !== "undefined") {
639
+ const asyncHooks = __require("async_hooks");
640
+ AsyncLocalStorageClass2 = asyncHooks.AsyncLocalStorage;
641
+ }
642
+ } catch {
643
+ try {
644
+ const dynamicRequire = new Function(
645
+ "m",
646
+ 'return typeof require!=="undefined"?require(m):null'
647
+ );
648
+ const asyncHooks = dynamicRequire("node:async_hooks");
649
+ if (asyncHooks) {
650
+ AsyncLocalStorageClass2 = asyncHooks.AsyncLocalStorage;
651
+ }
652
+ } catch {
653
+ }
654
+ }
655
+ }
656
+ var storageInstance2 = null;
657
+ function getStorage2() {
658
+ if (storageInstance2 !== null) {
659
+ return storageInstance2;
660
+ }
661
+ if (AsyncLocalStorageClass2) {
662
+ storageInstance2 = new AsyncLocalStorageClass2();
663
+ return storageInstance2;
664
+ }
665
+ storageInstance2 = browserStub2;
666
+ return storageInstance2;
667
+ }
668
+ function getSchemaFromContext(objectId) {
669
+ const ctx = getStorage2().getStore();
586
670
  return ctx?.objectsById.get(objectId);
587
671
  }
588
672
  function getSchemaByNameFromContext(objectName) {
589
- const ctx = getStorage().getStore();
673
+ const ctx = getStorage2().getStore();
590
674
  return ctx?.objectsByName.get(objectName);
591
675
  }
592
676
  function hasSchemaContext() {
593
- return getStorage().getStore() !== void 0;
677
+ return getStorage2().getStore() !== void 0;
594
678
  }
595
679
  function getSchemaContext() {
596
- return getStorage().getStore();
680
+ return getStorage2().getStore();
597
681
  }
598
682
  function addSchemaToContext(schema) {
599
- const ctx = getStorage().getStore();
683
+ const ctx = getStorage2().getStore();
600
684
  if (!ctx) {
601
685
  return;
602
686
  }
@@ -618,10 +702,10 @@ function buildSchemaContext(schemas) {
618
702
  }
619
703
  function runWithSchemaContext(schemas, fn) {
620
704
  const context = buildSchemaContext(schemas);
621
- return getStorage().run(context, fn);
705
+ return getStorage2().run(context, fn);
622
706
  }
623
707
  function runWithMergedSchemaContext(schemas, fn) {
624
- const existing = getStorage().getStore();
708
+ const existing = getStorage2().getStore();
625
709
  const objectsById = new Map(existing?.objectsById);
626
710
  const objectsByName = new Map(existing?.objectsByName);
627
711
  for (const schema of schemas) {
@@ -634,20 +718,20 @@ function runWithMergedSchemaContext(schemas, fn) {
634
718
  objectsById,
635
719
  objectsByName
636
720
  };
637
- return getStorage().run(context, fn);
721
+ return getStorage2().run(context, fn);
638
722
  }
639
723
 
640
724
  // src/runtime/context/tenant-context.ts
641
- var browserStub2 = {
725
+ var browserStub3 = {
642
726
  getStore: () => void 0,
643
727
  run: (_store, callback) => callback()
644
728
  };
645
- var AsyncLocalStorageClass2 = null;
729
+ var AsyncLocalStorageClass3 = null;
646
730
  if (typeof process !== "undefined" && process.versions?.node) {
647
731
  try {
648
732
  if (typeof __require !== "undefined") {
649
733
  const asyncHooks = __require("async_hooks");
650
- AsyncLocalStorageClass2 = asyncHooks.AsyncLocalStorage;
734
+ AsyncLocalStorageClass3 = asyncHooks.AsyncLocalStorage;
651
735
  }
652
736
  } catch {
653
737
  try {
@@ -657,43 +741,43 @@ if (typeof process !== "undefined" && process.versions?.node) {
657
741
  );
658
742
  const asyncHooks = dynamicRequire("node:async_hooks");
659
743
  if (asyncHooks) {
660
- AsyncLocalStorageClass2 = asyncHooks.AsyncLocalStorage;
744
+ AsyncLocalStorageClass3 = asyncHooks.AsyncLocalStorage;
661
745
  }
662
746
  } catch {
663
747
  }
664
748
  }
665
749
  }
666
- var storageInstance2 = null;
667
- function getStorage2() {
668
- if (storageInstance2 !== null) {
669
- return storageInstance2;
750
+ var storageInstance3 = null;
751
+ function getStorage3() {
752
+ if (storageInstance3 !== null) {
753
+ return storageInstance3;
670
754
  }
671
- if (AsyncLocalStorageClass2) {
672
- storageInstance2 = new AsyncLocalStorageClass2();
673
- return storageInstance2;
755
+ if (AsyncLocalStorageClass3) {
756
+ storageInstance3 = new AsyncLocalStorageClass3();
757
+ return storageInstance3;
674
758
  }
675
- storageInstance2 = browserStub2;
676
- return storageInstance2;
759
+ storageInstance3 = browserStub3;
760
+ return storageInstance3;
677
761
  }
678
- function getContext() {
679
- const ctx = getStorage2().getStore();
762
+ function getContext2() {
763
+ const ctx = getStorage3().getStore();
680
764
  if (!ctx) {
681
765
  throw new TenantContextError();
682
766
  }
683
767
  return ctx;
684
768
  }
685
769
  function getTenantId() {
686
- return getContext().tenantId;
770
+ return getContext2().tenantId;
687
771
  }
688
772
  function getUserId() {
689
- return getContext().userId;
773
+ return getContext2().userId;
690
774
  }
691
775
  function hasContext() {
692
- return getStorage2().getStore() !== void 0;
776
+ return getStorage3().getStore() !== void 0;
693
777
  }
694
778
  function runWithContext(context, fn) {
695
779
  const frozenContext = Object.freeze({ ...context });
696
- return getStorage2().run(frozenContext, fn);
780
+ return getStorage3().run(frozenContext, fn);
697
781
  }
698
782
  function withTenantContext(tenantId, fn, userId) {
699
783
  return runWithContext({ tenantId, userId }, fn);
@@ -4000,6 +4084,7 @@ function createEmptyStores() {
4000
4084
  files: /* @__PURE__ */ new Map(),
4001
4085
  objectRecords: /* @__PURE__ */ new Map(),
4002
4086
  views: /* @__PURE__ */ new Map(),
4087
+ viewOverlays: /* @__PURE__ */ new Map(),
4003
4088
  roles: /* @__PURE__ */ new Map(),
4004
4089
  permissions: /* @__PURE__ */ new Map(),
4005
4090
  userRoles: /* @__PURE__ */ new Map(),
@@ -4297,32 +4382,38 @@ function createMockViewsRepository(stores) {
4297
4382
  ) ?? null
4298
4383
  );
4299
4384
  },
4300
- findByObjectName(objectName) {
4385
+ findByNameAndType(objectName, viewName, type) {
4386
+ const tenantId = getTenantId();
4387
+ return Promise.resolve(
4388
+ Array.from(stores.views.values()).find(
4389
+ (v) => v.tenantId === tenantId && v.objectName === objectName && v.name === viewName && v.type === type
4390
+ ) ?? null
4391
+ );
4392
+ },
4393
+ findByObjectName(objectName, type) {
4301
4394
  const tenantId = getTenantId();
4302
4395
  return Promise.resolve(
4303
4396
  Array.from(stores.views.values()).filter(
4304
- (v) => v.tenantId === tenantId && v.objectName === objectName
4397
+ (v) => v.tenantId === tenantId && v.objectName === objectName && (type === void 0 || v.type === type)
4305
4398
  )
4306
4399
  );
4307
4400
  },
4308
- findAllForTenant() {
4401
+ findAllForTenant(type) {
4309
4402
  const tenantId = getTenantId();
4310
4403
  return Promise.resolve(
4311
- Array.from(stores.views.values()).filter((v) => v.tenantId === tenantId)
4404
+ Array.from(stores.views.values()).filter(
4405
+ (v) => v.tenantId === tenantId && (type === void 0 || v.type === type)
4406
+ )
4312
4407
  );
4313
4408
  },
4314
- findSystemByName(objectName, viewName) {
4409
+ findDefault(objectName, type) {
4410
+ const tenantId = getTenantId();
4315
4411
  return Promise.resolve(
4316
4412
  Array.from(stores.views.values()).find(
4317
- (v) => v.objectName === objectName && v.name === viewName && v.system
4413
+ (v) => v.tenantId === tenantId && v.objectName === objectName && v.type === type && v.default === true
4318
4414
  ) ?? null
4319
4415
  );
4320
4416
  },
4321
- findSystemByObjectName(objectName) {
4322
- return Promise.resolve(
4323
- Array.from(stores.views.values()).filter((v) => v.objectName === objectName && v.system)
4324
- );
4325
- },
4326
4417
  create(data) {
4327
4418
  const tenantId = getTenantId();
4328
4419
  const id = generateId();
@@ -4331,13 +4422,13 @@ function createMockViewsRepository(stores) {
4331
4422
  id,
4332
4423
  tenantId,
4333
4424
  objectName: data.objectName,
4425
+ type: data.type,
4334
4426
  name: data.name,
4335
4427
  label: data.label,
4336
4428
  description: data.description,
4337
4429
  icon: data.icon,
4338
- tabs: data.tabs,
4430
+ config: data.config,
4339
4431
  default: data.default ?? false,
4340
- system: data.system ?? false,
4341
4432
  metadata: data.metadata,
4342
4433
  createdAt: now,
4343
4434
  updatedAt: now
@@ -4362,10 +4453,11 @@ function createMockViewsRepository(stores) {
4362
4453
  stores.views.delete(id);
4363
4454
  return Promise.resolve();
4364
4455
  },
4365
- deleteNotIn(objectName, keepViewNames) {
4456
+ deleteNotIn(objectName, type, keepViewNames) {
4457
+ const tenantId = getTenantId();
4366
4458
  let deleted = 0;
4367
4459
  for (const [id, view2] of stores.views.entries()) {
4368
- if (view2.objectName === objectName && view2.system && !keepViewNames.includes(view2.name)) {
4460
+ if (view2.tenantId === tenantId && view2.objectName === objectName && view2.type === type && !keepViewNames.includes(view2.name)) {
4369
4461
  stores.views.delete(id);
4370
4462
  deleted++;
4371
4463
  }
@@ -4373,18 +4465,151 @@ function createMockViewsRepository(stores) {
4373
4465
  return Promise.resolve(deleted);
4374
4466
  },
4375
4467
  async upsert(data) {
4376
- const existing = data.system ? await this.findSystemByName(data.objectName, data.name) : await this.findByName(data.objectName, data.name);
4468
+ const existing = await this.findByNameAndType(data.objectName, data.name, data.type);
4377
4469
  if (existing) {
4378
4470
  return this.update(existing.id, {
4379
4471
  label: data.label,
4380
4472
  description: data.description,
4381
4473
  icon: data.icon,
4382
- tabs: data.tabs,
4474
+ config: data.config,
4383
4475
  default: data.default,
4384
4476
  metadata: data.metadata
4385
4477
  });
4386
4478
  }
4387
4479
  return this.create(data);
4480
+ },
4481
+ async exists(objectName, viewName, type) {
4482
+ const existing = await this.findByNameAndType(objectName, viewName, type);
4483
+ return existing !== null;
4484
+ }
4485
+ };
4486
+ }
4487
+ function createMockViewOverlaysRepository(stores) {
4488
+ return {
4489
+ findById(id) {
4490
+ return Promise.resolve(stores.viewOverlays.get(id) ?? null);
4491
+ },
4492
+ findByViewAndUser(viewId, userId) {
4493
+ const tenantId = getTenantId();
4494
+ return Promise.resolve(
4495
+ Array.from(stores.viewOverlays.values()).find(
4496
+ (o) => o.tenantId === tenantId && o.viewId === viewId && o.userId === userId
4497
+ ) ?? null
4498
+ );
4499
+ },
4500
+ findByUser(userId) {
4501
+ const tenantId = getTenantId();
4502
+ return Promise.resolve(
4503
+ Array.from(stores.viewOverlays.values()).filter(
4504
+ (o) => o.tenantId === tenantId && o.userId === userId
4505
+ )
4506
+ );
4507
+ },
4508
+ findByView(viewId) {
4509
+ const tenantId = getTenantId();
4510
+ return Promise.resolve(
4511
+ Array.from(stores.viewOverlays.values()).filter(
4512
+ (o) => o.tenantId === tenantId && o.viewId === viewId
4513
+ )
4514
+ );
4515
+ },
4516
+ async findUserDefault(userId, objectName, type) {
4517
+ const tenantId = getTenantId();
4518
+ const views = Array.from(stores.views.values()).filter(
4519
+ (v) => v.tenantId === tenantId && v.objectName === objectName && v.type === type
4520
+ );
4521
+ const viewIds = new Set(views.map((v) => v.id));
4522
+ return Promise.resolve(
4523
+ Array.from(stores.viewOverlays.values()).find(
4524
+ (o) => o.tenantId === tenantId && o.userId === userId && o.isUserDefault === true && viewIds.has(o.viewId)
4525
+ ) ?? null
4526
+ );
4527
+ },
4528
+ create(data) {
4529
+ const tenantId = getTenantId();
4530
+ const id = generateId();
4531
+ const now = /* @__PURE__ */ new Date();
4532
+ const overlay = {
4533
+ id,
4534
+ tenantId,
4535
+ viewId: data.viewId,
4536
+ userId: data.userId,
4537
+ configOverrides: data.configOverrides,
4538
+ isUserDefault: data.isUserDefault,
4539
+ createdAt: now,
4540
+ updatedAt: now
4541
+ };
4542
+ stores.viewOverlays.set(id, overlay);
4543
+ return Promise.resolve(overlay);
4544
+ },
4545
+ update(id, data) {
4546
+ const overlay = stores.viewOverlays.get(id);
4547
+ if (!overlay) {
4548
+ return Promise.reject(new Error(`ViewOverlay not found: ${id}`));
4549
+ }
4550
+ const updated = {
4551
+ ...overlay,
4552
+ ...data,
4553
+ updatedAt: /* @__PURE__ */ new Date()
4554
+ };
4555
+ stores.viewOverlays.set(id, updated);
4556
+ return Promise.resolve(updated);
4557
+ },
4558
+ delete(id) {
4559
+ stores.viewOverlays.delete(id);
4560
+ return Promise.resolve();
4561
+ },
4562
+ async deleteByViewAndUser(viewId, userId) {
4563
+ const overlay = await this.findByViewAndUser(viewId, userId);
4564
+ if (overlay) {
4565
+ stores.viewOverlays.delete(overlay.id);
4566
+ }
4567
+ },
4568
+ deleteByView(viewId) {
4569
+ const tenantId = getTenantId();
4570
+ let deleted = 0;
4571
+ for (const [id, overlay] of stores.viewOverlays.entries()) {
4572
+ if (overlay.tenantId === tenantId && overlay.viewId === viewId) {
4573
+ stores.viewOverlays.delete(id);
4574
+ deleted++;
4575
+ }
4576
+ }
4577
+ return Promise.resolve(deleted);
4578
+ },
4579
+ migrateViewId(fromViewId, toViewId) {
4580
+ const tenantId = getTenantId();
4581
+ let migrated = 0;
4582
+ for (const overlay of stores.viewOverlays.values()) {
4583
+ if (overlay.tenantId === tenantId && overlay.viewId === fromViewId) {
4584
+ overlay.viewId = toViewId;
4585
+ overlay.updatedAt = /* @__PURE__ */ new Date();
4586
+ migrated++;
4587
+ }
4588
+ }
4589
+ return Promise.resolve(migrated);
4590
+ },
4591
+ async upsert(data) {
4592
+ const existing = await this.findByViewAndUser(data.viewId, data.userId);
4593
+ if (existing) {
4594
+ return this.update(existing.id, {
4595
+ configOverrides: data.configOverrides,
4596
+ isUserDefault: data.isUserDefault
4597
+ });
4598
+ }
4599
+ return this.create(data);
4600
+ },
4601
+ async clearUserDefault(userId, objectName, type) {
4602
+ const tenantId = getTenantId();
4603
+ const views = Array.from(stores.views.values()).filter(
4604
+ (v) => v.tenantId === tenantId && v.objectName === objectName && v.type === type
4605
+ );
4606
+ const viewIds = new Set(views.map((v) => v.id));
4607
+ for (const overlay of stores.viewOverlays.values()) {
4608
+ if (overlay.tenantId === tenantId && overlay.userId === userId && overlay.isUserDefault === true && viewIds.has(overlay.viewId)) {
4609
+ overlay.isUserDefault = false;
4610
+ overlay.updatedAt = /* @__PURE__ */ new Date();
4611
+ }
4612
+ }
4388
4613
  }
4389
4614
  };
4390
4615
  }
@@ -4774,6 +4999,7 @@ function createMockAdapter() {
4774
4999
  files: createMockFilesRepository(stores),
4775
5000
  objectRecords: createMockObjectRecordsRepository(stores),
4776
5001
  views: createMockViewsRepository(stores),
5002
+ viewOverlays: createMockViewOverlaysRepository(stores),
4777
5003
  permissions: createMockPermissionsRepository(stores),
4778
5004
  workflows: createMockWorkflowsRepository(stores),
4779
5005
  workflowInstances: createMockWorkflowInstancesRepository(stores),
@@ -4796,6 +5022,7 @@ function createMockAdapter() {
4796
5022
  stores.files.clear();
4797
5023
  stores.objectRecords.clear();
4798
5024
  stores.views.clear();
5025
+ stores.viewOverlays.clear();
4799
5026
  stores.roles.clear();
4800
5027
  stores.permissions.clear();
4801
5028
  stores.userRoles.clear();
@@ -5339,6 +5566,32 @@ var BaseAttributeBuilder = class {
5339
5566
  this.attr.metadata = value;
5340
5567
  return this;
5341
5568
  }
5569
+ /**
5570
+ * Add a feature gate to conditionally show/hide/disable this attribute.
5571
+ *
5572
+ * @param flagName - Name of the feature flag to check
5573
+ * @param options - Optional configuration for expected value and fallback behavior
5574
+ *
5575
+ * @example Hide attribute when flag is disabled
5576
+ * ```typescript
5577
+ * text({ name: "aiSummary", label: "AI Summary" })
5578
+ * .featureGate("ai-features")
5579
+ * ```
5580
+ *
5581
+ * @example Disable attribute when tier is not enterprise
5582
+ * ```typescript
5583
+ * text({ name: "advancedField", label: "Advanced Field" })
5584
+ * .featureGate("tier", { expectedValue: "enterprise", fallback: "disable" })
5585
+ * ```
5586
+ */
5587
+ featureGate(flagName, options) {
5588
+ this.attr.featureGate = {
5589
+ flag: flagName,
5590
+ expectedValue: options?.expectedValue,
5591
+ fallback: options?.fallback
5592
+ };
5593
+ return this;
5594
+ }
5342
5595
  build() {
5343
5596
  return this.attr;
5344
5597
  }
@@ -6261,6 +6514,7 @@ function object(config) {
6261
6514
  }
6262
6515
 
6263
6516
  // src/builders/view-builder.ts
6517
+ import { randomUUID } from "crypto";
6264
6518
  import { z as z3 } from "zod";
6265
6519
  var GroupBuilder = class {
6266
6520
  constructor(id, label) {
@@ -6411,7 +6665,7 @@ var BaseTableTabConfig = class {
6411
6665
  return new TabBuilder(this.view, name, label);
6412
6666
  }
6413
6667
  /**
6414
- * Finish this tab and return to ViewBuilder
6668
+ * Finish this tab and return to DetailViewBuilder
6415
6669
  */
6416
6670
  done() {
6417
6671
  this.finalize();
@@ -6477,7 +6731,7 @@ var CustomTabConfig = class {
6477
6731
  return new TabBuilder(this.view, name, label);
6478
6732
  }
6479
6733
  /**
6480
- * Finish this tab and return to ViewBuilder
6734
+ * Finish this tab and return to DetailViewBuilder
6481
6735
  */
6482
6736
  done() {
6483
6737
  this.view._addTab(this.tabData);
@@ -6519,7 +6773,7 @@ var NotesTabConfig = class {
6519
6773
  return new TabBuilder(this.view, name, label);
6520
6774
  }
6521
6775
  /**
6522
- * Finish this tab and return to ViewBuilder
6776
+ * Finish this tab and return to DetailViewBuilder
6523
6777
  */
6524
6778
  done() {
6525
6779
  this.view._addTab(this.tabData);
@@ -6554,7 +6808,7 @@ var ActivityTabConfig = class {
6554
6808
  return new TabBuilder(this.view, name, label);
6555
6809
  }
6556
6810
  /**
6557
- * Finish this tab and return to ViewBuilder
6811
+ * Finish this tab and return to DetailViewBuilder
6558
6812
  */
6559
6813
  done() {
6560
6814
  this.view._addTab(this.tabData);
@@ -6617,7 +6871,7 @@ var FlowsTabConfig = class {
6617
6871
  return new TabBuilder(this.view, name, label);
6618
6872
  }
6619
6873
  /**
6620
- * Finish this tab and return to ViewBuilder
6874
+ * Finish this tab and return to DetailViewBuilder
6621
6875
  */
6622
6876
  done() {
6623
6877
  this.view._addTab(this.tabData);
@@ -6688,7 +6942,7 @@ var DocumentsTabConfig = class {
6688
6942
  return new TabBuilder(this.view, name, label);
6689
6943
  }
6690
6944
  /**
6691
- * Finish this tab and return to ViewBuilder
6945
+ * Finish this tab and return to DetailViewBuilder
6692
6946
  */
6693
6947
  done() {
6694
6948
  this.view._addTab(this.tabData);
@@ -6726,13 +6980,6 @@ var TabBuilder = class {
6726
6980
  this.base.order = value;
6727
6981
  return this;
6728
6982
  }
6729
- /**
6730
- * Mark as system tab (protected)
6731
- */
6732
- system() {
6733
- this.base.system = true;
6734
- return this;
6735
- }
6736
6983
  // ─────────────────────────────────────────────────────────────────────────
6737
6984
  // TYPE DISCRIMINATORS
6738
6985
  // ─────────────────────────────────────────────────────────────────────────
@@ -6812,13 +7059,15 @@ var TabBuilder = class {
6812
7059
  return new DocumentsTabConfig(this.view, this.base);
6813
7060
  }
6814
7061
  };
6815
- var ViewBuilder = class {
7062
+ var DetailViewBuilder = class {
6816
7063
  constructor(name, label) {
6817
- this.data = { tabs: [] };
6818
- this.validated = false;
6819
7064
  this.validateName(name);
6820
- this.data.name = name;
6821
- this.data.label = label;
7065
+ this.data = {
7066
+ name,
7067
+ label,
7068
+ layout: "page",
7069
+ tabs: []
7070
+ };
6822
7071
  }
6823
7072
  /**
6824
7073
  * Set view description
@@ -6843,19 +7092,12 @@ var ViewBuilder = class {
6843
7092
  return this;
6844
7093
  }
6845
7094
  /**
6846
- * Mark as default view for the object (within its layout)
7095
+ * Mark as default view for the object
6847
7096
  */
6848
7097
  default() {
6849
7098
  this.data.default = true;
6850
7099
  return this;
6851
7100
  }
6852
- /**
6853
- * Mark as system view (protected, defined by developer)
6854
- */
6855
- system() {
6856
- this.data.system = true;
6857
- return this;
6858
- }
6859
7101
  /**
6860
7102
  * Set the layout mode for the view
6861
7103
  * @param value - "page" for full tabs, "modal" for single form
@@ -6890,14 +7132,14 @@ var ViewBuilder = class {
6890
7132
  * Add a pre-built tab
6891
7133
  */
6892
7134
  addTab(tab) {
6893
- this.data.tabs?.push(tab);
7135
+ this.data.tabs.push(tab);
6894
7136
  return this;
6895
7137
  }
6896
7138
  /**
6897
7139
  * @internal Used by TabBuilder to add tabs
6898
7140
  */
6899
7141
  _addTab(tab) {
6900
- this.data.tabs?.push(tab);
7142
+ this.data.tabs.push(tab);
6901
7143
  return this;
6902
7144
  }
6903
7145
  /**
@@ -6905,48 +7147,332 @@ var ViewBuilder = class {
6905
7147
  */
6906
7148
  build() {
6907
7149
  if (!this.data.object) {
6908
- throw new Error("[ViewBuilder] for() is required - specify the target object");
7150
+ throw new Error("[DetailViewBuilder] for() is required - specify the target object");
6909
7151
  }
6910
- if (!this.data.tabs || this.data.tabs.length === 0) {
6911
- throw new Error("[ViewBuilder] At least one tab is required");
7152
+ if (this.data.tabs.length === 0) {
7153
+ throw new Error("[DetailViewBuilder] At least one tab is required");
6912
7154
  }
6913
7155
  const tabNames = /* @__PURE__ */ new Set();
6914
7156
  for (const tab of this.data.tabs) {
6915
7157
  if (tabNames.has(tab.name)) {
6916
- throw new Error(`[ViewBuilder] Duplicate tab name "${tab.name}"`);
7158
+ throw new Error(`[DetailViewBuilder] Duplicate tab name "${tab.name}"`);
6917
7159
  }
6918
7160
  tabNames.add(tab.name);
6919
7161
  }
6920
7162
  if (this.data.layout === "modal") {
6921
7163
  if (this.data.tabs.length !== 1) {
6922
- throw new Error("[ViewBuilder] Modal views must have exactly one tab");
7164
+ throw new Error("[DetailViewBuilder] Modal views must have exactly one tab");
6923
7165
  }
6924
7166
  if (this.data.tabs[0].type !== "form") {
6925
- throw new Error("[ViewBuilder] Modal views must have a form tab");
7167
+ throw new Error("[DetailViewBuilder] Modal views must have a form tab");
6926
7168
  }
6927
7169
  }
6928
- this.validated = true;
6929
- return this.data;
7170
+ const config = {
7171
+ layout: this.data.layout,
7172
+ tabs: this.data.tabs
7173
+ };
7174
+ return {
7175
+ name: this.data.name,
7176
+ label: this.data.label,
7177
+ description: this.data.description,
7178
+ icon: this.data.icon,
7179
+ object: this.data.object,
7180
+ type: "detail",
7181
+ config,
7182
+ default: this.data.default,
7183
+ metadata: this.data.metadata
7184
+ };
7185
+ }
7186
+ /**
7187
+ * Validate view name format (kebab-case)
7188
+ */
7189
+ validateName(name) {
7190
+ const viewNameSchema = z3.string().min(1, "View name cannot be empty").max(63, "View name is too long (max 63 characters)").regex(/^[a-z][a-z0-9-]*$/, {
7191
+ message: "Invalid view name format.\nName must be in kebab-case:\n \u2705 Valid: 'detail', 'list-view', 'company-detail'\n \u274C Invalid: 'Detail', 'listView', 'list_view'"
7192
+ });
7193
+ try {
7194
+ viewNameSchema.parse(name);
7195
+ } catch (error2) {
7196
+ if (error2 instanceof z3.ZodError) {
7197
+ throw new Error(`[DetailViewBuilder] ${error2.issues[0].message}`);
7198
+ }
7199
+ throw error2;
7200
+ }
7201
+ }
7202
+ };
7203
+ var ViewBuilder = DetailViewBuilder;
7204
+ function detailView(name, label) {
7205
+ return new DetailViewBuilder(name, label);
7206
+ }
7207
+ function view(name, label) {
7208
+ return new DetailViewBuilder(name, label);
7209
+ }
7210
+ var ListViewBuilder = class {
7211
+ constructor(name, label) {
7212
+ this.validateName(name);
7213
+ this.data = {
7214
+ name,
7215
+ label,
7216
+ layout: "table",
7217
+ columns: [],
7218
+ tabs: []
7219
+ };
7220
+ }
7221
+ /**
7222
+ * Set view description
7223
+ */
7224
+ description(value) {
7225
+ this.data.description = value;
7226
+ return this;
7227
+ }
7228
+ /**
7229
+ * Set view icon
7230
+ */
7231
+ icon(value) {
7232
+ this.data.icon = value;
7233
+ return this;
7234
+ }
7235
+ /**
7236
+ * Associate view with an object
7237
+ * @param objectName - Object name (kebab-case)
7238
+ */
7239
+ for(objectName) {
7240
+ this.data.object = objectName;
7241
+ return this;
7242
+ }
7243
+ /**
7244
+ * Mark as default view for the object
7245
+ */
7246
+ default() {
7247
+ this.data.default = true;
7248
+ return this;
7249
+ }
7250
+ /**
7251
+ * Set metadata
7252
+ */
7253
+ metadata(value) {
7254
+ this.data.metadata = value;
7255
+ return this;
7256
+ }
7257
+ /**
7258
+ * Set the layout to table (default)
7259
+ */
7260
+ table() {
7261
+ this.data.layout = "table";
7262
+ return this;
7263
+ }
7264
+ /**
7265
+ * Set the layout to kanban and specify the grouping attribute
7266
+ * @param groupByAttribute - Attribute to group by (must be a select/status type)
7267
+ */
7268
+ kanban(groupByAttribute) {
7269
+ this.data.layout = "kanban";
7270
+ this.data.groupByAttribute = groupByAttribute;
7271
+ return this;
7272
+ }
7273
+ /**
7274
+ * Set columns to display
7275
+ * @example .columns("name", "email", "status", "createdAt")
7276
+ */
7277
+ columns(...names) {
7278
+ this.data.columns = names;
7279
+ return this;
7280
+ }
7281
+ /**
7282
+ * Set width for a specific column in pixels
7283
+ * @example .columnWidth("email", 200)
7284
+ */
7285
+ columnWidth(columnName, width) {
7286
+ if (!this.data.columnSizing) {
7287
+ this.data.columnSizing = {};
7288
+ }
7289
+ this.data.columnSizing[columnName] = width;
7290
+ return this;
7291
+ }
7292
+ /**
7293
+ * Set widths for multiple columns
7294
+ * @example .columnWidths({ email: 200, name: 300, status: 100 })
7295
+ */
7296
+ columnWidths(widths) {
7297
+ this.data.columnSizing = { ...this.data.columnSizing, ...widths };
7298
+ return this;
7299
+ }
7300
+ /**
7301
+ * Set default filters
7302
+ * @example .filter({ combinator: "and", rules: [{ attribute: "status", operator: "is", value: "active" }] })
7303
+ */
7304
+ filter(filters) {
7305
+ this.data.defaultFilters = filters;
7306
+ return this;
7307
+ }
7308
+ /**
7309
+ * Add a single sort rule
7310
+ * @example .sort("lastName", "asc")
7311
+ */
7312
+ sort(attribute, direction = "asc") {
7313
+ if (!this.data.defaultSorts) {
7314
+ this.data.defaultSorts = [];
7315
+ }
7316
+ this.data.defaultSorts.push({ attribute, direction });
7317
+ return this;
7318
+ }
7319
+ /**
7320
+ * Set multiple sort rules
7321
+ * @example .sorts([{ attribute: "lastName", direction: "asc" }, { attribute: "firstName", direction: "asc" }])
7322
+ */
7323
+ sorts(rules) {
7324
+ this.data.defaultSorts = rules;
7325
+ return this;
7326
+ }
7327
+ /**
7328
+ * Add an internal tab (filter preset)
7329
+ * Returns a TabConfigBuilder for chaining tab configuration
7330
+ * @example
7331
+ * ```typescript
7332
+ * .tab("all", "All Contacts").icon("users").default()
7333
+ * .tab("active", "Active").icon("check").filter({ ... })
7334
+ * ```
7335
+ */
7336
+ tab(id, label) {
7337
+ return new ListViewTabConfigBuilder(this, id, label);
7338
+ }
7339
+ /**
7340
+ * @internal Used by ListViewTabConfigBuilder to add tabs
7341
+ */
7342
+ _addTab(tab) {
7343
+ this.data.tabs.push(tab);
7344
+ return this;
7345
+ }
7346
+ /**
7347
+ * Build the final list view definition
7348
+ */
7349
+ build() {
7350
+ if (!this.data.object) {
7351
+ throw new Error("[ListViewBuilder] for() is required - specify the target object");
7352
+ }
7353
+ if (this.data.columns.length === 0) {
7354
+ throw new Error("[ListViewBuilder] columns() is required - specify at least one column");
7355
+ }
7356
+ if (this.data.layout === "kanban" && !this.data.groupByAttribute) {
7357
+ throw new Error(
7358
+ "[ListViewBuilder] kanban() requires a groupByAttribute - specify the attribute to group by"
7359
+ );
7360
+ }
7361
+ const tabIds = /* @__PURE__ */ new Set();
7362
+ for (const tab of this.data.tabs) {
7363
+ if (tabIds.has(tab.id)) {
7364
+ throw new Error(`[ListViewBuilder] Duplicate tab id "${tab.id}"`);
7365
+ }
7366
+ tabIds.add(tab.id);
7367
+ }
7368
+ const defaultTabs = this.data.tabs.filter((t) => t.default);
7369
+ if (defaultTabs.length > 1) {
7370
+ throw new Error("[ListViewBuilder] Only one tab can be marked as default");
7371
+ }
7372
+ const config = {
7373
+ layout: this.data.layout,
7374
+ columns: this.data.columns,
7375
+ columnSizing: this.data.columnSizing,
7376
+ defaultFilters: this.data.defaultFilters ? {
7377
+ id: randomUUID(),
7378
+ combinator: this.data.defaultFilters.combinator,
7379
+ rules: this.data.defaultFilters.rules
7380
+ } : void 0,
7381
+ defaultSorts: this.data.defaultSorts,
7382
+ groupByAttribute: this.data.groupByAttribute,
7383
+ tabs: this.data.tabs.length > 0 ? this.data.tabs.map((tab) => ({
7384
+ id: tab.id,
7385
+ label: tab.label,
7386
+ icon: tab.icon,
7387
+ filters: tab.filters ? {
7388
+ id: randomUUID(),
7389
+ combinator: tab.filters.combinator,
7390
+ rules: tab.filters.rules
7391
+ } : void 0,
7392
+ default: tab.default
7393
+ })) : void 0
7394
+ };
7395
+ return {
7396
+ name: this.data.name,
7397
+ label: this.data.label,
7398
+ description: this.data.description,
7399
+ icon: this.data.icon,
7400
+ object: this.data.object,
7401
+ type: "list",
7402
+ config,
7403
+ default: this.data.default,
7404
+ metadata: this.data.metadata
7405
+ };
7406
+ }
7407
+ /**
7408
+ * Validate view name format (kebab-case)
7409
+ */
7410
+ validateName(name) {
7411
+ const viewNameSchema = z3.string().min(1, "View name cannot be empty").max(63, "View name is too long (max 63 characters)").regex(/^[a-z][a-z0-9-]*$/, {
7412
+ message: "Invalid view name format.\nName must be in kebab-case:\n \u2705 Valid: 'default', 'list-view', 'active-contacts'\n \u274C Invalid: 'Default', 'listView', 'list_view'"
7413
+ });
7414
+ try {
7415
+ viewNameSchema.parse(name);
7416
+ } catch (error2) {
7417
+ if (error2 instanceof z3.ZodError) {
7418
+ throw new Error(`[ListViewBuilder] ${error2.issues[0].message}`);
7419
+ }
7420
+ throw error2;
7421
+ }
7422
+ }
7423
+ };
7424
+ var ListViewTabConfigBuilder = class _ListViewTabConfigBuilder {
7425
+ /** @internal */
7426
+ constructor(view2, id, label) {
7427
+ this.view = view2;
7428
+ this.tabData = { id, label };
7429
+ }
7430
+ /**
7431
+ * Set tab icon
7432
+ */
7433
+ icon(value) {
7434
+ this.tabData.icon = value;
7435
+ return this;
7436
+ }
7437
+ /**
7438
+ * Set filters for this tab (filter preset)
7439
+ * @example .filter({ combinator: "and", rules: [{ attribute: "status", operator: "is", value: "active" }] })
7440
+ */
7441
+ filter(filters) {
7442
+ this.tabData.filters = filters;
7443
+ return this;
7444
+ }
7445
+ /**
7446
+ * Mark this tab as the default tab
7447
+ */
7448
+ default() {
7449
+ this.tabData.default = true;
7450
+ return this;
7451
+ }
7452
+ /**
7453
+ * Continue building with a new tab
7454
+ */
7455
+ tab(id, label) {
7456
+ this.view._addTab(this.tabData);
7457
+ return new _ListViewTabConfigBuilder(this.view, id, label);
7458
+ }
7459
+ /**
7460
+ * Finish this tab and return to ListViewBuilder
7461
+ */
7462
+ done() {
7463
+ this.view._addTab(this.tabData);
7464
+ return this.view;
6930
7465
  }
6931
7466
  /**
6932
- * Validate view name format (kebab-case)
7467
+ * Build the final view definition
6933
7468
  */
6934
- validateName(name) {
6935
- const viewNameSchema = z3.string().min(1, "View name cannot be empty").max(63, "View name is too long (max 63 characters)").regex(/^[a-z][a-z0-9-]*$/, {
6936
- message: "Invalid view name format.\nName must be in kebab-case:\n \u2705 Valid: 'detail', 'list-view', 'company-detail'\n \u274C Invalid: 'Detail', 'listView', 'list_view'"
6937
- });
6938
- try {
6939
- viewNameSchema.parse(name);
6940
- } catch (error2) {
6941
- if (error2 instanceof z3.ZodError) {
6942
- throw new Error(`[ViewBuilder] ${error2.issues[0].message}`);
6943
- }
6944
- throw error2;
6945
- }
7469
+ build() {
7470
+ this.view._addTab(this.tabData);
7471
+ return this.view.build();
6946
7472
  }
6947
7473
  };
6948
- function view(name, label) {
6949
- return new ViewBuilder(name, label);
7474
+ function listView(name, label) {
7475
+ return new ListViewBuilder(name, label);
6950
7476
  }
6951
7477
  function group(id, label) {
6952
7478
  return new GroupBuilder(id, label);
@@ -7492,6 +8018,46 @@ function workflow(name, label) {
7492
8018
  return new WorkflowBuilder(name, label);
7493
8019
  }
7494
8020
 
8021
+ // src/types/attribute-protection.ts
8022
+ var IDENTITY_PROPERTIES = ["name", "type"];
8023
+ var BEHAVIOR_PROPERTIES = [
8024
+ "required",
8025
+ "disabled",
8026
+ "hidden",
8027
+ "archived",
8028
+ "deprecated",
8029
+ "defaultValue",
8030
+ "config",
8031
+ "order",
8032
+ "unique"
8033
+ ];
8034
+ var PRESENTATION_PROPERTIES = ["label", "description", "placeholder", "icon"];
8035
+ function isIdentityProperty(property) {
8036
+ return IDENTITY_PROPERTIES.includes(property);
8037
+ }
8038
+ function isBehaviorProperty(property) {
8039
+ return BEHAVIOR_PROPERTIES.includes(property);
8040
+ }
8041
+ function isPresentationProperty(property) {
8042
+ return PRESENTATION_PROPERTIES.includes(property);
8043
+ }
8044
+ function getPropertyProtectionLevel(property) {
8045
+ if (isIdentityProperty(property)) return "identity";
8046
+ if (isBehaviorProperty(property)) return "behavior";
8047
+ if (isPresentationProperty(property)) return "presentation";
8048
+ return "unknown";
8049
+ }
8050
+ function filterPropertiesByCategory(properties, category) {
8051
+ switch (category) {
8052
+ case "identity":
8053
+ return properties.filter(isIdentityProperty);
8054
+ case "behavior":
8055
+ return properties.filter(isBehaviorProperty);
8056
+ case "presentation":
8057
+ return properties.filter(isPresentationProperty);
8058
+ }
8059
+ }
8060
+
7495
8061
  // src/types/errors.ts
7496
8062
  var RecordReferencedError = class extends Error {
7497
8063
  constructor(recordId, references) {
@@ -8321,7 +8887,8 @@ var ObjectSchemaService = class extends BaseService {
8321
8887
  }
8322
8888
  /**
8323
8889
  * Update an attribute.
8324
- * Can only update custom attributes (system=false).
8890
+ * Custom attributes can be fully updated.
8891
+ * System attributes can only have presentation properties modified (label, description, placeholder, icon).
8325
8892
  * Automatically uses tenant context from AsyncLocalStorage.
8326
8893
  *
8327
8894
  * @param attributeId - Attribute UUID
@@ -8333,20 +8900,34 @@ var ObjectSchemaService = class extends BaseService {
8333
8900
  if (!dbAttr) {
8334
8901
  throw new Error(`Attribute with id "${attributeId}" not found`);
8335
8902
  }
8336
- if (dbAttr.system) {
8337
- throw new Error(
8338
- `Cannot modify system attribute "${dbAttr.name}". System attributes are protected.`
8339
- );
8340
- }
8341
- if (updates.name && updates.name !== dbAttr.name) {
8342
- throw new Error(
8343
- "Attribute name cannot be changed after creation. Create a new attribute instead."
8344
- );
8903
+ const requestedKeys = Object.keys(updates);
8904
+ const identityChanges = requestedKeys.filter(
8905
+ (k) => IDENTITY_PROPERTIES.includes(k)
8906
+ );
8907
+ if (identityChanges.length > 0) {
8908
+ const actualChanges = identityChanges.filter((k) => {
8909
+ const key = k;
8910
+ return updates[key] !== void 0 && updates[key] !== dbAttr[key];
8911
+ });
8912
+ if (actualChanges.length > 0) {
8913
+ throw new Error(
8914
+ `Cannot modify identity properties: ${actualChanges.join(", ")}. These properties cannot be changed after creation.`
8915
+ );
8916
+ }
8345
8917
  }
8346
- if (updates.type && updates.type !== dbAttr.type) {
8347
- throw new Error(
8348
- "Attribute type cannot be changed after creation. Create a new attribute instead."
8918
+ if (dbAttr.system) {
8919
+ const behaviorChanges = requestedKeys.filter(
8920
+ (k) => BEHAVIOR_PROPERTIES.includes(k)
8349
8921
  );
8922
+ const actualBehaviorChanges = behaviorChanges.filter((k) => {
8923
+ const key = k;
8924
+ return updates[key] !== void 0;
8925
+ });
8926
+ if (actualBehaviorChanges.length > 0) {
8927
+ throw new Error(
8928
+ `Cannot modify behavior properties on system attribute "${dbAttr.name}": ${actualBehaviorChanges.join(", ")}. Only presentation properties (${PRESENTATION_PROPERTIES.join(", ")}) are editable.`
8929
+ );
8930
+ }
8350
8931
  }
8351
8932
  const mergedInput = {
8352
8933
  name: dbAttr.name,
@@ -12231,7 +12812,6 @@ var WorkflowAccessGrantService = class extends BaseService {
12231
12812
  };
12232
12813
 
12233
12814
  // src/runtime/services/workflow/instance.service.ts
12234
- import { randomUUID } from "crypto";
12235
12815
  var WorkflowInstanceService = class extends BaseService {
12236
12816
  constructor(adapter, workflowService, options) {
12237
12817
  super(adapter);
@@ -12497,7 +13077,7 @@ var WorkflowInstanceService = class extends BaseService {
12497
13077
  updatedAt: /* @__PURE__ */ new Date()
12498
13078
  };
12499
13079
  }
12500
- const executionId = randomUUID();
13080
+ const executionId = generateId();
12501
13081
  let current = {
12502
13082
  ...instance,
12503
13083
  context: {
@@ -16172,124 +16752,340 @@ var PermissionService = class extends BaseService {
16172
16752
 
16173
16753
  // src/runtime/services/view.service.ts
16174
16754
  var ViewService = class extends BaseService {
16175
- constructor(adapter, nativeViews) {
16755
+ constructor(adapter) {
16176
16756
  super(adapter);
16177
- this.nativeViews = nativeViews;
16178
16757
  }
16179
16758
  // ============================================================================
16180
16759
  // CACHE MANAGEMENT
16181
16760
  // ============================================================================
16182
- /**
16183
- * Invalidate cached views for an object.
16184
- * Called automatically after view mutations.
16185
- */
16186
16761
  async invalidateViewCache(objectName) {
16187
16762
  await this.invalidateCache(cacheKeys.viewsByObject(this.tenantId, objectName));
16188
16763
  }
16764
+ // ============================================================================
16765
+ // READ METHODS
16766
+ // ============================================================================
16189
16767
  /**
16190
- * Get all views for an object (native + custom).
16191
- * Automatically uses tenant context from AsyncLocalStorage.
16768
+ * Get all views for the current tenant.
16769
+ * Optionally filter by view type.
16192
16770
  *
16193
- * Results are cached if a CacheAdapter is configured.
16771
+ * @param type - Optional view type filter
16772
+ * @returns All views for the tenant
16773
+ */
16774
+ async getAllViews(type) {
16775
+ const dbViews = await this.adapter.views.findAllForTenant(type);
16776
+ return dbViews.map((v) => this.convertDBViewToDefinition(v));
16777
+ }
16778
+ /**
16779
+ * Get a specific view by its ID.
16780
+ * Returns null if not found.
16194
16781
  *
16195
- * @param objectName - Object name
16196
- * @returns All views for the object
16782
+ * @param viewId - View ID (UUID)
16783
+ * @returns View definition or null
16197
16784
  */
16198
- async getViewsForObject(objectName) {
16199
- return this.cachedBy("viewsByObject", objectName, () => this.fetchViewsForObject(objectName));
16785
+ async getViewById(viewId) {
16786
+ const dbView = await this.adapter.views.findById(viewId);
16787
+ if (!dbView) {
16788
+ return null;
16789
+ }
16790
+ return this.convertDBViewToDefinition(dbView);
16200
16791
  }
16201
16792
  /**
16202
- * Internal method to fetch views for an object (no caching)
16793
+ * Get all views for an object from the database.
16794
+ * Optionally filter by type and merge with user overlays.
16795
+ *
16796
+ * @param objectName - Object name
16797
+ * @param options - Filter and overlay options
16798
+ * @returns Views from database
16203
16799
  */
16204
- async fetchViewsForObject(objectName) {
16205
- const nativeViewDefs = this.nativeViews.getByObjectName(objectName);
16206
- const dbViews = await this.adapter.views.findByObjectName(objectName);
16207
- const customViews = dbViews.filter((v) => !v.system).map(this.convertDBViewToDefinition);
16208
- return [...nativeViewDefs, ...customViews];
16800
+ async getViews(objectName, options = {}) {
16801
+ const { type, userId } = options;
16802
+ const dbViews = await this.adapter.views.findByObjectName(objectName, type);
16803
+ const views = dbViews.map((v) => this.convertDBViewToDefinition(v));
16804
+ if (!userId) {
16805
+ return views;
16806
+ }
16807
+ const userOverlays = await this.adapter.viewOverlays.findByUser(userId);
16808
+ return views.map((view2) => {
16809
+ const overlay = userOverlays.find((o) => o.viewId === view2.id);
16810
+ return overlay ? this.applyOverlay(view2, overlay) : view2;
16811
+ });
16209
16812
  }
16210
16813
  /**
16211
16814
  * Get a specific view by name.
16212
- * Automatically uses tenant context from AsyncLocalStorage.
16815
+ * Returns null if not found (use getDefaultView for fallback behavior).
16213
16816
  *
16214
16817
  * @param objectName - Object name
16215
16818
  * @param viewName - View name
16216
- * @returns View definition or null
16217
- */
16218
- async getView(objectName, viewName) {
16219
- const nativeView = this.nativeViews.get(objectName, viewName);
16220
- if (nativeView) {
16221
- return nativeView;
16819
+ * @param options - Type filter and overlay options
16820
+ * @returns View or null
16821
+ */
16822
+ async getView(objectName, viewName, options = {}) {
16823
+ const { type, userId } = options;
16824
+ let dbView;
16825
+ if (type) {
16826
+ dbView = await this.adapter.views.findByNameAndType(objectName, viewName, type);
16827
+ } else {
16828
+ dbView = await this.adapter.views.findByName(objectName, viewName);
16222
16829
  }
16223
- const dbView = await this.adapter.views.findByName(objectName, viewName);
16224
- if (dbView) {
16225
- return this.convertDBViewToDefinition(dbView);
16830
+ if (!dbView) {
16831
+ return null;
16226
16832
  }
16227
- return null;
16833
+ const view2 = this.convertDBViewToDefinition(dbView);
16834
+ if (!userId) {
16835
+ return view2;
16836
+ }
16837
+ const overlay = await this.adapter.viewOverlays.findByViewAndUser(dbView.id, userId);
16838
+ return overlay ? this.applyOverlay(view2, overlay) : view2;
16228
16839
  }
16229
16840
  /**
16230
- * Get the default view for an object
16841
+ * Get the default view for an object and type.
16842
+ * If no view exists in DB, auto-creates and persists a default view.
16231
16843
  *
16232
16844
  * Priority:
16233
- * 1. Custom view marked as default (for the specified layout)
16234
- * 2. Native view marked as default (for the specified layout)
16235
- * 3. First available view (for the specified layout)
16845
+ * 1. User's preferred view (from overlay with isUserDefault=true)
16846
+ * 2. View marked as default in DB (with matching layout if specified)
16847
+ * 3. First available view (with matching layout if specified)
16848
+ * 4. Auto-created default view (persisted to DB)
16236
16849
  *
16237
16850
  * @param objectName - Object name
16238
- * @param layout - Optional layout filter ("page" or "modal")
16239
- * @returns Default view or null
16851
+ * @param type - View type
16852
+ * @param objectDefinition - Object definition (for default generation)
16853
+ * @param options - Optional filters (userId, layout for detail views)
16854
+ * @returns View definition (existing or auto-created)
16855
+ */
16856
+ async getDefaultView(objectName, type, objectDefinition, options) {
16857
+ const { userId, layout } = options ?? {};
16858
+ const matchesLayout = (view2) => {
16859
+ if (!layout || type !== "detail") return true;
16860
+ const detailView2 = view2;
16861
+ return detailView2.config.layout === layout;
16862
+ };
16863
+ const applyUserOverlay = async (view2, viewId) => {
16864
+ if (!userId) return view2;
16865
+ const overlay = await this.adapter.viewOverlays.findByViewAndUser(viewId, userId);
16866
+ return overlay ? this.applyOverlay(view2, overlay) : view2;
16867
+ };
16868
+ if (userId) {
16869
+ const userDefaultOverlay = await this.adapter.viewOverlays.findUserDefault(
16870
+ userId,
16871
+ objectName,
16872
+ type
16873
+ );
16874
+ if (userDefaultOverlay) {
16875
+ const dbView = await this.adapter.views.findById(userDefaultOverlay.viewId);
16876
+ if (dbView && dbView.type === type) {
16877
+ const view2 = this.convertDBViewToDefinition(dbView);
16878
+ if (matchesLayout(view2)) {
16879
+ return this.applyOverlay(view2, userDefaultOverlay);
16880
+ }
16881
+ }
16882
+ }
16883
+ }
16884
+ const defaultDbView = await this.adapter.views.findDefault(objectName, type);
16885
+ if (defaultDbView) {
16886
+ const view2 = this.convertDBViewToDefinition(defaultDbView);
16887
+ if (matchesLayout(view2)) {
16888
+ return await applyUserOverlay(view2, defaultDbView.id);
16889
+ }
16890
+ }
16891
+ const dbViews = await this.adapter.views.findByObjectName(objectName, type);
16892
+ for (const dbView of dbViews) {
16893
+ const view2 = this.convertDBViewToDefinition(dbView);
16894
+ if (matchesLayout(view2)) {
16895
+ return await applyUserOverlay(view2, dbView.id);
16896
+ }
16897
+ }
16898
+ if (dbViews.length > 0) {
16899
+ const firstView = dbViews[0];
16900
+ const view2 = this.convertDBViewToDefinition(firstView);
16901
+ return await applyUserOverlay(view2, firstView.id);
16902
+ }
16903
+ const created = await this.ensureDefaultView(objectName, type, objectDefinition, layout);
16904
+ if (userId) {
16905
+ if (!created.id) {
16906
+ throw new Error("Created view missing ID - this should never happen");
16907
+ }
16908
+ const overlay = await this.adapter.viewOverlays.findByViewAndUser(created.id, userId);
16909
+ return overlay ? this.applyOverlay(created, overlay) : created;
16910
+ }
16911
+ return created;
16912
+ }
16913
+ // ============================================================================
16914
+ // DEFAULT VIEW GENERATION
16915
+ // ============================================================================
16916
+ /**
16917
+ * Ensure a default view exists in DB for the given object and type.
16918
+ * If no view exists, generates and persists one.
16919
+ * Idempotent — safe to call concurrently (uses upsert).
16240
16920
  */
16241
- async getDefaultView(objectName, layout) {
16242
- const views = await this.getViewsForObject(objectName);
16243
- const filtered = layout ? views.filter((v) => (v.layout ?? "page") === layout) : views;
16244
- const customDefault = filtered.find((v) => v.default && !v.system);
16245
- if (customDefault) return customDefault;
16246
- const nativeDefault = filtered.find((v) => v.default && v.system);
16247
- if (nativeDefault) return nativeDefault;
16248
- return filtered[0] ?? null;
16921
+ async ensureDefaultView(objectName, type, objectDefinition, layout) {
16922
+ const generated = this.generateDefaultViewConfig(objectName, type, objectDefinition, layout);
16923
+ const viewName = type === "detail" && layout === "modal" ? "default-modal" : "default";
16924
+ const dbView = await this.adapter.views.upsert({
16925
+ objectName,
16926
+ type,
16927
+ name: viewName,
16928
+ label: generated.label,
16929
+ config: generated.config,
16930
+ default: true
16931
+ });
16932
+ const legacyFallbackId = `fallback:${objectName}:${type}`;
16933
+ await this.adapter.viewOverlays.migrateViewId(legacyFallbackId, dbView.id);
16934
+ await this.invalidateViewCache(objectName);
16935
+ return this.convertDBViewToDefinition(dbView);
16249
16936
  }
16250
16937
  /**
16251
- * Create a custom view.
16252
- * Automatically uses tenant context from AsyncLocalStorage.
16938
+ * Generate default view config for an object.
16939
+ * Used by ensureDefaultView() and resetViewToDefault().
16940
+ */
16941
+ generateDefaultViewConfig(objectName, type, objectDefinition, layout) {
16942
+ switch (type) {
16943
+ case "detail":
16944
+ return this.generateDefaultDetailConfig(objectName, objectDefinition, layout);
16945
+ case "list":
16946
+ return this.generateDefaultListConfig(objectName, objectDefinition);
16947
+ default:
16948
+ throw new SchemaError(
16949
+ `Default view generation not supported for type: ${type}`,
16950
+ SchemaErrorCode.VALIDATION_FAILED,
16951
+ { type }
16952
+ );
16953
+ }
16954
+ }
16955
+ generateDefaultDetailConfig(objectName, object2, layout = "page") {
16956
+ const visibleAttrs = object2.attributes.filter(
16957
+ (attr) => !(attr.hidden || attr.archived || attr.system)
16958
+ );
16959
+ const fields = visibleAttrs.sort((a, b) => (a.order ?? 999) - (b.order ?? 999)).map((attr) => ({
16960
+ attribute: attr.name,
16961
+ span: this.getDefaultFieldSpan(attr.type)
16962
+ }));
16963
+ const formTab = {
16964
+ id: "form",
16965
+ name: "form",
16966
+ label: "Details",
16967
+ type: "form",
16968
+ groups: [
16969
+ {
16970
+ id: "general",
16971
+ label: "General",
16972
+ fields,
16973
+ order: 0
16974
+ }
16975
+ ],
16976
+ order: 0
16977
+ };
16978
+ const tabs = [formTab];
16979
+ tabs.push({
16980
+ id: "activity",
16981
+ name: "activity",
16982
+ label: "Activity",
16983
+ type: "activity",
16984
+ order: 1
16985
+ });
16986
+ tabs.push({
16987
+ id: "notes",
16988
+ name: "notes",
16989
+ label: "Notes",
16990
+ type: "notes",
16991
+ order: 2,
16992
+ allowCreate: true
16993
+ });
16994
+ const hasDocuments = object2.attributes.some((attr) => attr.type === "document");
16995
+ if (hasDocuments) {
16996
+ tabs.push({
16997
+ id: "documents",
16998
+ name: "documents",
16999
+ label: "Documents",
17000
+ type: "documents",
17001
+ order: 3,
17002
+ allowUpload: true,
17003
+ allowRemove: true,
17004
+ showProcessing: true
17005
+ });
17006
+ }
17007
+ const config = {
17008
+ layout,
17009
+ tabs
17010
+ };
17011
+ return {
17012
+ name: "default",
17013
+ label: "Default View",
17014
+ object: objectName,
17015
+ type: "detail",
17016
+ config,
17017
+ default: true
17018
+ };
17019
+ }
17020
+ generateDefaultListConfig(objectName, object2) {
17021
+ const visibleAttrs = object2.attributes.filter(
17022
+ (attr) => !(attr.hidden || attr.archived || attr.system)
17023
+ );
17024
+ const columns = visibleAttrs.sort((a, b) => (a.order ?? 999) - (b.order ?? 999)).slice(0, 5).map((attr) => attr.name);
17025
+ const config = {
17026
+ layout: "table",
17027
+ columns
17028
+ };
17029
+ return {
17030
+ name: "default",
17031
+ label: "All Records",
17032
+ object: objectName,
17033
+ type: "list",
17034
+ config,
17035
+ default: true
17036
+ };
17037
+ }
17038
+ getDefaultFieldSpan(type) {
17039
+ switch (type) {
17040
+ case "textarea":
17041
+ case "richtext":
17042
+ case "location":
17043
+ return 12;
17044
+ case "checkbox":
17045
+ return 4;
17046
+ default:
17047
+ return 6;
17048
+ }
17049
+ }
17050
+ // ============================================================================
17051
+ // WRITE METHODS (Architect Mode)
17052
+ // ============================================================================
17053
+ /**
17054
+ * Create a new view (Architect Mode).
16253
17055
  *
16254
17056
  * @param input - View definition
16255
17057
  * @returns Created view
16256
17058
  */
16257
17059
  async createView(input) {
16258
17060
  this.validateViewName(input.name);
16259
- this.validateModalLayout(input.tabs, input.layout);
16260
- const existing = await this.adapter.views.findByName(input.objectName, input.name);
17061
+ const existing = await this.adapter.views.findByNameAndType(
17062
+ input.objectName,
17063
+ input.name,
17064
+ input.type
17065
+ );
16261
17066
  if (existing) {
16262
17067
  throw new SchemaError(
16263
- `View "${input.name}" already exists for object "${input.objectName}"`,
16264
- SchemaErrorCode.DUPLICATE_ATTRIBUTE,
16265
- { viewName: input.name, objectName: input.objectName }
16266
- );
16267
- }
16268
- if (this.nativeViews.has(input.objectName, input.name)) {
16269
- throw new SchemaError(
16270
- `Cannot create view "${input.name}": a system view with this name already exists`,
17068
+ `View "${input.name}" of type "${input.type}" already exists for object "${input.objectName}"`,
16271
17069
  SchemaErrorCode.DUPLICATE_ATTRIBUTE,
16272
- { viewName: input.name, objectName: input.objectName, system: true }
17070
+ { viewName: input.name, type: input.type, objectName: input.objectName }
16273
17071
  );
16274
17072
  }
16275
17073
  const dbView = await this.adapter.views.create({
16276
17074
  objectName: input.objectName,
17075
+ type: input.type,
16277
17076
  name: input.name,
16278
17077
  label: input.label,
16279
17078
  description: input.description,
16280
17079
  icon: input.icon,
16281
- layout: input.layout,
16282
- tabs: input.tabs ?? [],
16283
- default: input.default ?? false,
16284
- system: false,
16285
- // Custom views are never system
17080
+ config: input.config,
17081
+ default: input.default,
16286
17082
  metadata: input.metadata
16287
17083
  });
16288
17084
  await this.invalidateViewCache(input.objectName);
16289
17085
  return this.convertDBViewToDefinition(dbView);
16290
17086
  }
16291
17087
  /**
16292
- * Update a custom view
17088
+ * Update an existing view.
16293
17089
  *
16294
17090
  * @param viewId - View ID
16295
17091
  * @param input - Update data
@@ -16300,18 +17096,11 @@ var ViewService = class extends BaseService {
16300
17096
  if (!dbView) {
16301
17097
  throw new NotFoundError("View", viewId);
16302
17098
  }
16303
- if (dbView.system) {
16304
- throw new ProtectedResourceError("view", dbView.name, "modify");
16305
- }
16306
- const effectiveLayout = input.layout ?? dbView.layout;
16307
- const effectiveTabs = input.tabs ?? dbView.tabs;
16308
- this.validateModalLayout(effectiveTabs, effectiveLayout);
16309
17099
  const updated = await this.adapter.views.update(viewId, {
16310
17100
  label: input.label,
16311
17101
  description: input.description,
16312
17102
  icon: input.icon,
16313
- layout: input.layout,
16314
- tabs: input.tabs,
17103
+ config: input.config,
16315
17104
  default: input.default,
16316
17105
  metadata: input.metadata
16317
17106
  });
@@ -16319,7 +17108,8 @@ var ViewService = class extends BaseService {
16319
17108
  return this.convertDBViewToDefinition(updated);
16320
17109
  }
16321
17110
  /**
16322
- * Delete a custom view
17111
+ * Delete a view.
17112
+ * Overlays are automatically deleted (cascade).
16323
17113
  *
16324
17114
  * @param viewId - View ID
16325
17115
  */
@@ -16328,16 +17118,12 @@ var ViewService = class extends BaseService {
16328
17118
  if (!dbView) {
16329
17119
  throw new NotFoundError("View", viewId);
16330
17120
  }
16331
- if (dbView.system) {
16332
- throw new ProtectedResourceError("view", dbView.name, "delete");
16333
- }
17121
+ await this.adapter.viewOverlays.deleteByView(viewId);
16334
17122
  await this.adapter.views.delete(viewId);
16335
17123
  await this.invalidateViewCache(dbView.objectName);
16336
17124
  }
16337
17125
  /**
16338
- * Set a view as default for its object and layout.
16339
- * Only unsets other defaults for the same layout.
16340
- * Automatically uses tenant context from AsyncLocalStorage.
17126
+ * Set a view as default for its object and type.
16341
17127
  *
16342
17128
  * @param viewId - View ID
16343
17129
  * @returns Updated view
@@ -16347,11 +17133,9 @@ var ViewService = class extends BaseService {
16347
17133
  if (!dbView) {
16348
17134
  throw new NotFoundError("View", viewId);
16349
17135
  }
16350
- const viewLayout = dbView.layout ?? "page";
16351
- const currentViews = await this.adapter.views.findByObjectName(dbView.objectName);
17136
+ const currentViews = await this.adapter.views.findByObjectName(dbView.objectName, dbView.type);
16352
17137
  for (const v of currentViews) {
16353
- const vLayout = v.layout ?? "page";
16354
- if (v.default && v.id !== viewId && !v.system && vLayout === viewLayout) {
17138
+ if (v.default && v.id !== viewId) {
16355
17139
  await this.adapter.views.update(v.id, { default: false });
16356
17140
  }
16357
17141
  }
@@ -16359,12 +17143,140 @@ var ViewService = class extends BaseService {
16359
17143
  await this.invalidateViewCache(dbView.objectName);
16360
17144
  return this.convertDBViewToDefinition(updated);
16361
17145
  }
17146
+ /**
17147
+ * Reset a view to its default (auto-generated) state.
17148
+ * Regenerates the view config based on the object definition.
17149
+ *
17150
+ * @param viewId - View ID
17151
+ * @param objectDefinition - Object definition for regeneration
17152
+ * @returns Updated view
17153
+ */
17154
+ async resetViewToDefault(viewId, objectDefinition) {
17155
+ const dbView = await this.adapter.views.findById(viewId);
17156
+ if (!dbView) {
17157
+ throw new NotFoundError("View", viewId);
17158
+ }
17159
+ const generated = this.generateDefaultViewConfig(
17160
+ dbView.objectName,
17161
+ dbView.type,
17162
+ objectDefinition,
17163
+ dbView.type === "detail" ? dbView.config?.layout ?? "page" : void 0
17164
+ );
17165
+ const newConfig = generated.config;
17166
+ const updated = await this.adapter.views.update(viewId, { config: newConfig });
17167
+ await this.invalidateViewCache(dbView.objectName);
17168
+ return this.convertDBViewToDefinition(updated);
17169
+ }
16362
17170
  // ============================================================================
16363
- // PRIVATE HELPERS
17171
+ // USER CUSTOMIZATION METHODS
16364
17172
  // ============================================================================
16365
17173
  /**
16366
- * Validate view name format (kebab-case)
17174
+ * Reset user customizations for a view.
17175
+ * Deletes the overlay, returning to source/fallback view.
17176
+ *
17177
+ * @param viewId - View ID (can be UUID or fallback ID)
17178
+ * @param userId - User ID
17179
+ */
17180
+ async resetUserCustomizations(viewId, userId) {
17181
+ await this.adapter.viewOverlays.deleteByViewAndUser(viewId, userId);
17182
+ }
17183
+ /**
17184
+ * Set a view as the user's default for an object and type.
17185
+ *
17186
+ * @param viewId - View ID (can be UUID or fallback ID)
17187
+ * @param userId - User ID
17188
+ * @param objectName - Object name
17189
+ * @param type - View type
17190
+ */
17191
+ async setUserDefaultView(viewId, userId, objectName, type) {
17192
+ await this.adapter.viewOverlays.clearUserDefault(userId, objectName, type);
17193
+ const existingOverlay = await this.adapter.viewOverlays.findByViewAndUser(viewId, userId);
17194
+ if (existingOverlay) {
17195
+ await this.adapter.viewOverlays.update(existingOverlay.id, { isUserDefault: true });
17196
+ } else {
17197
+ await this.adapter.viewOverlays.create({
17198
+ viewId,
17199
+ userId,
17200
+ configOverrides: {},
17201
+ isUserDefault: true
17202
+ });
17203
+ }
17204
+ }
17205
+ /**
17206
+ * Check if a user has customized a view.
17207
+ *
17208
+ * @param viewId - View ID
17209
+ * @param userId - User ID
17210
+ * @returns True if overlay exists
17211
+ */
17212
+ async hasUserCustomizations(viewId, userId) {
17213
+ const overlay = await this.adapter.viewOverlays.findByViewAndUser(viewId, userId);
17214
+ return overlay !== null && Object.keys(overlay.configOverrides).length > 0;
17215
+ }
17216
+ // ============================================================================
17217
+ // OVERLAY MERGE LOGIC
17218
+ // ============================================================================
17219
+ /**
17220
+ * Apply an overlay to a view definition.
17221
+ * Implements merge semantics defined in the plan.
17222
+ *
17223
+ * @param view - Source view definition
17224
+ * @param overlay - User overlay
17225
+ * @returns Merged view definition
17226
+ */
17227
+ applyOverlay(view2, overlay) {
17228
+ const overrides = overlay.configOverrides;
17229
+ switch (view2.type) {
17230
+ case "list":
17231
+ return this.applyListViewOverlay(view2, overrides);
17232
+ case "detail":
17233
+ return this.applyDetailViewOverlay(view2, overrides);
17234
+ default:
17235
+ return view2;
17236
+ }
17237
+ }
17238
+ applyListViewOverlay(view2, overrides) {
17239
+ const mergedConfig = {
17240
+ ...view2.config,
17241
+ tabs: this.mergeViewTabs(view2.config.tabs, overrides.tabs, overrides.hiddenTabIds)
17242
+ };
17243
+ return { ...view2, config: mergedConfig };
17244
+ }
17245
+ applyDetailViewOverlay(view2, overrides) {
17246
+ const mergedConfig = {
17247
+ ...view2.config,
17248
+ // Append for tabs (detail tabs)
17249
+ tabs: this.mergeDetailTabs(
17250
+ view2.config.tabs,
17251
+ overrides.detailTabs,
17252
+ overrides.hiddenDetailTabIds
17253
+ )
17254
+ };
17255
+ return {
17256
+ ...view2,
17257
+ config: mergedConfig
17258
+ };
17259
+ }
17260
+ /**
17261
+ * Merge source tabs with overlay tabs.
17262
+ * - Source tabs are visible to all
17263
+ * - Overlay tabs are appended (user-private)
17264
+ * - hiddenTabIds allows hiding source tabs
17265
+ */
17266
+ mergeViewTabs(sourceTabs = [], overlayTabs = [], hiddenTabIds = []) {
17267
+ const visibleSourceTabs = sourceTabs.filter((t) => !hiddenTabIds.includes(t.id));
17268
+ return [...visibleSourceTabs, ...overlayTabs];
17269
+ }
17270
+ /**
17271
+ * Merge detail tabs (form, activity, etc.)
16367
17272
  */
17273
+ mergeDetailTabs(sourceTabs = [], overlayTabs = [], hiddenTabIds = []) {
17274
+ const visibleSourceTabs = sourceTabs.filter((t) => !hiddenTabIds.includes(t.id));
17275
+ return [...visibleSourceTabs, ...overlayTabs];
17276
+ }
17277
+ // ============================================================================
17278
+ // PRIVATE HELPERS
17279
+ // ============================================================================
16368
17280
  validateViewName(name) {
16369
17281
  if (!name || name.length === 0) {
16370
17282
  throw new ValidationError("View name cannot be empty", [
@@ -16386,66 +17298,72 @@ var ViewService = class extends BaseService {
16386
17298
  ]);
16387
17299
  }
16388
17300
  }
16389
- /**
16390
- * Validate modal layout constraints.
16391
- * Modal views must have exactly one form tab.
16392
- */
16393
- validateModalLayout(tabs, layout) {
16394
- if (layout !== "modal") return;
16395
- if (!tabs || tabs.length === 0) {
16396
- throw new ValidationError("Modal views must have exactly one form tab", [
16397
- { path: ["tabs"], message: "Modal views must have exactly one form tab" }
16398
- ]);
16399
- }
16400
- if (tabs.length > 1) {
16401
- throw new ValidationError("Modal views can only have one tab", [
16402
- { path: ["tabs"], message: "Modal views can only have one tab" }
16403
- ]);
16404
- }
16405
- if (tabs[0].type !== "form") {
16406
- throw new ValidationError("Modal views must have a form tab", [
16407
- { path: ["tabs"], message: `Modal views must have a form tab, not a ${tabs[0].type}` }
16408
- ]);
16409
- }
16410
- }
16411
17301
  /**
16412
17302
  * Convert database view to ViewDefinition
16413
17303
  */
16414
17304
  convertDBViewToDefinition(dbView) {
16415
- return {
17305
+ const base = {
16416
17306
  id: dbView.id,
16417
17307
  name: dbView.name,
16418
17308
  label: dbView.label,
16419
17309
  description: dbView.description,
16420
17310
  icon: dbView.icon,
16421
17311
  object: dbView.objectName,
16422
- layout: dbView.layout,
16423
- tabs: dbView.tabs,
16424
17312
  default: dbView.default,
16425
- system: dbView.system,
16426
17313
  metadata: dbView.metadata
16427
17314
  };
17315
+ switch (dbView.type) {
17316
+ case "detail": {
17317
+ const detailView2 = {
17318
+ ...base,
17319
+ type: "detail",
17320
+ config: dbView.config
17321
+ };
17322
+ return detailView2;
17323
+ }
17324
+ case "list": {
17325
+ const listView2 = {
17326
+ ...base,
17327
+ type: "list",
17328
+ config: dbView.config
17329
+ };
17330
+ return listView2;
17331
+ }
17332
+ // TODO: Add proper config types for calendar, timeline, gallery when implemented
17333
+ case "calendar":
17334
+ case "timeline":
17335
+ case "gallery":
17336
+ return { ...base, type: dbView.type, config: dbView.config };
17337
+ default:
17338
+ throw new SchemaError(
17339
+ `Unknown view type: ${dbView.type}`,
17340
+ SchemaErrorCode.VALIDATION_FAILED,
17341
+ { type: dbView.type }
17342
+ );
17343
+ }
16428
17344
  }
16429
17345
  };
16430
17346
 
16431
17347
  // src/runtime/view-sync.ts
16432
- async function syncNativeViews(adapter, nativeViewRegistry, options = {}) {
17348
+ async function seedRegistryViews(adapter, registry2, options = {}) {
16433
17349
  const result = {
16434
17350
  success: true,
16435
17351
  viewsSynced: 0,
16436
17352
  viewsCreated: 0,
16437
- viewsUpdated: 0,
17353
+ viewsSkipped: 0,
16438
17354
  viewsDeleted: 0,
16439
17355
  errors: []
16440
17356
  };
16441
- const nativeViews = nativeViewRegistry.getAll();
17357
+ const registryViews = registry2.getAll();
16442
17358
  if (options.verbose && options.logger) {
16443
- options.logger.info(`[ViewSync] Starting sync for ${nativeViews.length} native views...`);
17359
+ options.logger.info(`[ViewSync] Starting seed for ${registryViews.length} registry views...`);
16444
17360
  }
16445
17361
  try {
16446
17362
  await adapter.transaction(async (tx) => {
16447
- const viewsByObject = await syncAllViews(tx, nativeViews, result, options);
16448
- await cleanupRemovedViews(tx, viewsByObject, result, options);
17363
+ const viewsByObjectAndType = await seedAllViews(tx, registryViews, result, options);
17364
+ if (options.deleteOrphans) {
17365
+ await cleanupOrphanViews(tx, viewsByObjectAndType, result, options);
17366
+ }
16449
17367
  });
16450
17368
  } catch (error2) {
16451
17369
  handleTransactionError(result, error2);
@@ -16453,38 +17371,78 @@ async function syncNativeViews(adapter, nativeViewRegistry, options = {}) {
16453
17371
  logSyncComplete(result, options);
16454
17372
  return result;
16455
17373
  }
16456
- async function syncAllViews(tx, nativeViews, result, options) {
16457
- const viewsByObject = /* @__PURE__ */ new Map();
16458
- for (const nativeView of nativeViews) {
17374
+ var syncNativeViews = seedRegistryViews;
17375
+ async function seedAllViews(tx, registryViews, result, options) {
17376
+ const viewsByObjectAndType = /* @__PURE__ */ new Map();
17377
+ for (const view2 of registryViews) {
16459
17378
  try {
16460
- await syncSingleView(tx, nativeView, result, options);
16461
- trackViewByObject(viewsByObject, nativeView);
17379
+ await seedSingleView(tx, view2, result, options);
17380
+ trackViewByObjectAndType(viewsByObjectAndType, view2);
16462
17381
  } catch (error2) {
16463
- handleViewSyncError(result, nativeView, error2, options);
17382
+ handleViewSyncError(result, view2, error2);
17383
+ }
17384
+ }
17385
+ return viewsByObjectAndType;
17386
+ }
17387
+ async function seedSingleView(adapter, view2, result, options) {
17388
+ const exists = await adapter.views.exists(view2.object, view2.name, view2.type);
17389
+ if (exists) {
17390
+ result.viewsSkipped++;
17391
+ result.viewsSynced++;
17392
+ if (options.verbose && options.logger) {
17393
+ options.logger.info(`[ViewSync] Skipped (exists): ${view2.object}:${view2.name}:${view2.type}`);
17394
+ }
17395
+ return;
17396
+ }
17397
+ if (options.dryRun) {
17398
+ result.viewsCreated++;
17399
+ result.viewsSynced++;
17400
+ if (options.verbose && options.logger) {
17401
+ options.logger.info(`[ViewSync] Would create: ${view2.object}:${view2.name}:${view2.type}`);
16464
17402
  }
17403
+ return;
17404
+ }
17405
+ await adapter.views.upsert({
17406
+ objectName: view2.object,
17407
+ type: view2.type,
17408
+ name: view2.name,
17409
+ label: view2.label,
17410
+ description: view2.description,
17411
+ icon: view2.icon,
17412
+ config: view2.config,
17413
+ default: view2.default ?? false,
17414
+ metadata: view2.metadata
17415
+ });
17416
+ result.viewsCreated++;
17417
+ result.viewsSynced++;
17418
+ if (options.verbose && options.logger) {
17419
+ options.logger.info(`[ViewSync] \u2713 Created: ${view2.object}:${view2.name}:${view2.type}`);
16465
17420
  }
16466
- return viewsByObject;
16467
17421
  }
16468
- function trackViewByObject(viewsByObject, nativeView) {
16469
- const objectViews = viewsByObject.get(nativeView.object) ?? [];
16470
- objectViews.push(nativeView.name);
16471
- viewsByObject.set(nativeView.object, objectViews);
17422
+ function trackViewByObjectAndType(viewsByObjectAndType, view2) {
17423
+ const key = `${view2.object}:${view2.type}`;
17424
+ const viewNames = viewsByObjectAndType.get(key) ?? [];
17425
+ viewNames.push(view2.name);
17426
+ viewsByObjectAndType.set(key, viewNames);
16472
17427
  }
16473
- function handleViewSyncError(result, nativeView, error2, _options) {
17428
+ function handleViewSyncError(result, view2, error2) {
16474
17429
  result.success = false;
16475
17430
  result.errors.push({
16476
- viewName: nativeView.name,
16477
- objectName: nativeView.object,
17431
+ viewName: view2.name,
17432
+ objectName: view2.object,
16478
17433
  error: error2 instanceof Error ? error2.message : String(error2)
16479
17434
  });
16480
17435
  }
16481
- async function cleanupRemovedViews(tx, viewsByObject, result, options) {
17436
+ async function cleanupOrphanViews(tx, viewsByObjectAndType, result, options) {
16482
17437
  if (options.dryRun) return;
16483
- for (const [objectName, viewNames] of viewsByObject) {
16484
- const deletedCount = await tx.views.deleteNotIn(objectName, viewNames);
17438
+ for (const [key, viewNames] of viewsByObjectAndType) {
17439
+ const [objectName, type] = key.split(":");
17440
+ const deletedCount = await tx.views.deleteNotIn(objectName, type, viewNames);
16485
17441
  result.viewsDeleted += deletedCount;
16486
17442
  if (options.verbose && deletedCount > 0 && options.logger) {
16487
- options.logger.info(`[ViewSync] Deleted ${deletedCount} obsolete views for ${objectName}`);
17443
+ options.logger.info(
17444
+ `[ViewSync] Deleted ${deletedCount} orphan ${type} views for ${objectName}`
17445
+ );
16488
17446
  }
16489
17447
  }
16490
17448
  }
@@ -16499,63 +17457,27 @@ function handleTransactionError(result, error2) {
16499
17457
  function logSyncComplete(result, options) {
16500
17458
  if (options.verbose && options.logger) {
16501
17459
  options.logger.info(
16502
- `[ViewSync] ${result.success ? "\u2713" : "\u2717"} Sync complete:
16503
- Views: ${result.viewsCreated} created, ${result.viewsUpdated} updated, ${result.viewsDeleted} deleted
17460
+ `[ViewSync] ${result.success ? "\u2713" : "\u2717"} Seed complete:
17461
+ Views: ${result.viewsCreated} created, ${result.viewsSkipped} skipped, ${result.viewsDeleted} deleted
16504
17462
  Errors: ${result.errors.length}`
16505
17463
  );
16506
17464
  }
16507
17465
  }
16508
- async function syncSingleView(adapter, nativeView, result, options) {
16509
- if (!nativeView.system) {
16510
- throw new Error(`View ${nativeView.name} is not marked as system`);
16511
- }
16512
- const existingView = await adapter.views.findSystemByName(nativeView.object, nativeView.name);
16513
- const isNew = !existingView;
16514
- if (isNew) {
16515
- result.viewsCreated++;
16516
- } else {
16517
- result.viewsUpdated++;
16518
- }
16519
- result.viewsSynced++;
16520
- if (options.dryRun) {
16521
- if (options.verbose && options.logger) {
16522
- options.logger.info(
16523
- `[ViewSync] Would ${isNew ? "create" : "update"} view: ${nativeView.object}:${nativeView.name}`
16524
- );
16525
- }
16526
- return;
16527
- }
16528
- await adapter.views.upsert({
16529
- objectName: nativeView.object,
16530
- name: nativeView.name,
16531
- label: nativeView.label,
16532
- description: nativeView.description,
16533
- icon: nativeView.icon,
16534
- tabs: nativeView.tabs,
16535
- default: nativeView.default ?? false,
16536
- system: true,
16537
- metadata: nativeView.metadata
16538
- });
16539
- if (options.verbose && options.logger) {
16540
- options.logger.info(
16541
- `[ViewSync] \u2713 Synced ${nativeView.object}:${nativeView.name}: ${nativeView.tabs.length} tabs`
16542
- );
16543
- }
16544
- }
16545
- async function verifyNativeViewsSync(adapter, nativeViewRegistry) {
16546
- const nativeViews = nativeViewRegistry.getAll();
16547
- for (const view2 of nativeViews) {
16548
- if (!view2.system) continue;
16549
- const existing = await adapter.views.findSystemByName(view2.object, view2.name);
16550
- if (!existing) {
17466
+ async function verifyRegistryViewsSeeded(adapter, registry2) {
17467
+ const registryViews = registry2.getAll();
17468
+ for (const view2 of registryViews) {
17469
+ const exists = await adapter.views.exists(view2.object, view2.name, view2.type);
17470
+ if (!exists) {
16551
17471
  return false;
16552
17472
  }
16553
17473
  }
16554
17474
  return true;
16555
17475
  }
16556
- async function getViewSyncPreview(adapter, nativeViewRegistry) {
16557
- return await syncNativeViews(adapter, nativeViewRegistry, { dryRun: true });
17476
+ var verifyNativeViewsSync = verifyRegistryViewsSeeded;
17477
+ async function getViewSeedPreview(adapter, registry2) {
17478
+ return await seedRegistryViews(adapter, registry2, { dryRun: true });
16558
17479
  }
17480
+ var getViewSyncPreview = getViewSeedPreview;
16559
17481
 
16560
17482
  // src/runtime/sync.ts
16561
17483
  var BASE_ATTRIBUTE_KEYS = /* @__PURE__ */ new Set([
@@ -16755,7 +17677,7 @@ async function syncAll(adapter, objectRegistry, nativeViewRegistry, options = {}
16755
17677
  console.info(
16756
17678
  `[SyncAll] ${result.success ? "\u2713" : "\u2717"} Full sync complete:
16757
17679
  Objects: ${objectsResult.objectsSynced} synced (${objectsResult.objectsCreated} created, ${objectsResult.objectsUpdated} updated)
16758
- Views: ${viewsResult.viewsSynced} synced (${viewsResult.viewsCreated} created, ${viewsResult.viewsUpdated} updated)
17680
+ Views: ${viewsResult.viewsSynced} synced (${viewsResult.viewsCreated} created, ${viewsResult.viewsSkipped} skipped)
16759
17681
  Errors: ${objectsResult.errors.length + viewsResult.errors.length}`
16760
17682
  );
16761
17683
  }
@@ -16776,6 +17698,14 @@ var NoopGeocodingAdapter = class {
16776
17698
  };
16777
17699
 
16778
17700
  export {
17701
+ IDENTITY_PROPERTIES,
17702
+ BEHAVIOR_PROPERTIES,
17703
+ PRESENTATION_PROPERTIES,
17704
+ isIdentityProperty,
17705
+ isBehaviorProperty,
17706
+ isPresentationProperty,
17707
+ getPropertyProtectionLevel,
17708
+ filterPropertiesByCategory,
16779
17709
  RELATION_TARGET_ANY,
16780
17710
  isUniversalRelation,
16781
17711
  RecordReferencedError,
@@ -16919,8 +17849,13 @@ export {
16919
17849
  FlowsTabConfig,
16920
17850
  DocumentsTabConfig,
16921
17851
  TabBuilder,
17852
+ DetailViewBuilder,
16922
17853
  ViewBuilder,
17854
+ detailView,
16923
17855
  view,
17856
+ ListViewBuilder,
17857
+ ListViewTabConfigBuilder,
17858
+ listView,
16924
17859
  group,
16925
17860
  WorkflowFormRowBuilder,
16926
17861
  WorkflowFormBuilder,
@@ -17011,6 +17946,14 @@ export {
17011
17946
  QueryNoResultError,
17012
17947
  QueryMultipleResultsError,
17013
17948
  TenantContextError,
17949
+ FeatureFlagsContextError,
17950
+ isFeatureEnabled,
17951
+ getFeatureValue,
17952
+ getFeatureFlags,
17953
+ tryGetFeatureValue,
17954
+ hasFeatureFlagsContext,
17955
+ runWithFeatureFlags,
17956
+ withFeatureFlags,
17014
17957
  getSchemaFromContext,
17015
17958
  getSchemaByNameFromContext,
17016
17959
  hasSchemaContext,
@@ -17018,7 +17961,7 @@ export {
17018
17961
  addSchemaToContext,
17019
17962
  runWithSchemaContext,
17020
17963
  runWithMergedSchemaContext,
17021
- getContext,
17964
+ getContext2 as getContext,
17022
17965
  getTenantId,
17023
17966
  getUserId,
17024
17967
  hasContext,
@@ -17136,8 +18079,11 @@ export {
17136
18079
  GlobalSearchService,
17137
18080
  PermissionService,
17138
18081
  ViewService,
18082
+ seedRegistryViews,
17139
18083
  syncNativeViews,
18084
+ verifyRegistryViewsSeeded,
17140
18085
  verifyNativeViewsSync,
18086
+ getViewSeedPreview,
17141
18087
  getViewSyncPreview,
17142
18088
  syncNativeObjects,
17143
18089
  verifyNativeObjectsSync,