@stndrds/schema 0.1.0-alpha.39 → 0.1.0-alpha.41

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.
@@ -482,7 +482,7 @@ var TenantContextError = class _TenantContextError extends Error {
482
482
  }
483
483
  };
484
484
 
485
- // src/runtime/context/tenant-context.ts
485
+ // src/runtime/context/schema-context.ts
486
486
  var browserStub = {
487
487
  getStore: () => void 0,
488
488
  run: (_store, callback) => callback()
@@ -520,8 +520,102 @@ function getStorage() {
520
520
  storageInstance = browserStub;
521
521
  return storageInstance;
522
522
  }
523
- function getContext() {
523
+ function getSchemaFromContext(objectId) {
524
+ const ctx = getStorage().getStore();
525
+ return ctx?.objectsById.get(objectId);
526
+ }
527
+ function getSchemaByNameFromContext(objectName) {
528
+ const ctx = getStorage().getStore();
529
+ return ctx?.objectsByName.get(objectName);
530
+ }
531
+ function hasSchemaContext() {
532
+ return getStorage().getStore() !== void 0;
533
+ }
534
+ function getSchemaContext() {
535
+ return getStorage().getStore();
536
+ }
537
+ function addSchemaToContext(schema) {
524
538
  const ctx = getStorage().getStore();
539
+ if (!ctx) {
540
+ return;
541
+ }
542
+ if (schema.id) {
543
+ ctx.objectsById.set(schema.id, schema);
544
+ }
545
+ ctx.objectsByName.set(schema.name, schema);
546
+ }
547
+ function buildSchemaContext(schemas) {
548
+ const objectsById = /* @__PURE__ */ new Map();
549
+ const objectsByName = /* @__PURE__ */ new Map();
550
+ for (const schema of schemas) {
551
+ if (schema.id) {
552
+ objectsById.set(schema.id, schema);
553
+ }
554
+ objectsByName.set(schema.name, schema);
555
+ }
556
+ return { objectsById, objectsByName };
557
+ }
558
+ function runWithSchemaContext(schemas, fn) {
559
+ const context = buildSchemaContext(schemas);
560
+ return getStorage().run(context, fn);
561
+ }
562
+ function runWithMergedSchemaContext(schemas, fn) {
563
+ const existing = getStorage().getStore();
564
+ const objectsById = new Map(existing?.objectsById);
565
+ const objectsByName = new Map(existing?.objectsByName);
566
+ for (const schema of schemas) {
567
+ if (schema.id) {
568
+ objectsById.set(schema.id, schema);
569
+ }
570
+ objectsByName.set(schema.name, schema);
571
+ }
572
+ const context = {
573
+ objectsById,
574
+ objectsByName
575
+ };
576
+ return getStorage().run(context, fn);
577
+ }
578
+
579
+ // src/runtime/context/tenant-context.ts
580
+ var browserStub2 = {
581
+ getStore: () => void 0,
582
+ run: (_store, callback) => callback()
583
+ };
584
+ var AsyncLocalStorageClass2 = null;
585
+ if (typeof process !== "undefined" && process.versions?.node) {
586
+ try {
587
+ if (typeof __require !== "undefined") {
588
+ const asyncHooks = __require("async_hooks");
589
+ AsyncLocalStorageClass2 = asyncHooks.AsyncLocalStorage;
590
+ }
591
+ } catch {
592
+ try {
593
+ const dynamicRequire = new Function(
594
+ "m",
595
+ 'return typeof require!=="undefined"?require(m):null'
596
+ );
597
+ const asyncHooks = dynamicRequire("node:async_hooks");
598
+ if (asyncHooks) {
599
+ AsyncLocalStorageClass2 = asyncHooks.AsyncLocalStorage;
600
+ }
601
+ } catch {
602
+ }
603
+ }
604
+ }
605
+ var storageInstance2 = null;
606
+ function getStorage2() {
607
+ if (storageInstance2 !== null) {
608
+ return storageInstance2;
609
+ }
610
+ if (AsyncLocalStorageClass2) {
611
+ storageInstance2 = new AsyncLocalStorageClass2();
612
+ return storageInstance2;
613
+ }
614
+ storageInstance2 = browserStub2;
615
+ return storageInstance2;
616
+ }
617
+ function getContext() {
618
+ const ctx = getStorage2().getStore();
525
619
  if (!ctx) {
526
620
  throw new TenantContextError();
527
621
  }
@@ -534,11 +628,11 @@ function getUserId() {
534
628
  return getContext().userId;
535
629
  }
536
630
  function hasContext() {
537
- return getStorage().getStore() !== void 0;
631
+ return getStorage2().getStore() !== void 0;
538
632
  }
539
633
  function runWithContext(context, fn) {
540
634
  const frozenContext = Object.freeze({ ...context });
541
- return getStorage().run(frozenContext, fn);
635
+ return getStorage2().run(frozenContext, fn);
542
636
  }
543
637
  function withTenantContext(tenantId, fn, userId) {
544
638
  return runWithContext({ tenantId, userId }, fn);
@@ -2315,8 +2409,14 @@ function formatCheckbox(value) {
2315
2409
  function formatNumber(value, attribute) {
2316
2410
  if (typeof value !== "number") return String(value);
2317
2411
  const decimals = attribute.decimals;
2318
- if (attribute.unit === "percentage") {
2412
+ if (attribute.unit === "integer") {
2319
2413
  return value.toLocaleString(void 0, {
2414
+ minimumFractionDigits: 0,
2415
+ maximumFractionDigits: 0
2416
+ });
2417
+ }
2418
+ if (attribute.unit === "percentage") {
2419
+ return (value / 100).toLocaleString(void 0, {
2320
2420
  style: "percent",
2321
2421
  minimumFractionDigits: decimals,
2322
2422
  maximumFractionDigits: decimals
@@ -2462,7 +2562,7 @@ function formatAttributeValue(value, attribute) {
2462
2562
  }
2463
2563
 
2464
2564
  // src/runtime/template.ts
2465
- var pipes = {
2565
+ var simplePipes = {
2466
2566
  /** Convert to uppercase */
2467
2567
  UPPER: (v) => String(v).toUpperCase(),
2468
2568
  /** Convert to lowercase */
@@ -2472,6 +2572,16 @@ var pipes = {
2472
2572
  /** Trim whitespace from both ends */
2473
2573
  trim: (v) => String(v).trim()
2474
2574
  };
2575
+ var pipesWithArgs = {
2576
+ /** Add prefix only if value is non-empty */
2577
+ prefix: (v, pre = "") => v ? `${pre}${v}` : "",
2578
+ /** Add suffix only if value is non-empty */
2579
+ suffix: (v, suf = "") => v ? `${v}${suf}` : "",
2580
+ /** Wrap value with prefix and suffix only if non-empty */
2581
+ wrap: (v, pre = "", suf = "") => v ? `${pre}${v}${suf}` : "",
2582
+ /** Show default value if empty */
2583
+ default: (v, def = "") => v || def
2584
+ };
2475
2585
  function getValue(obj, path) {
2476
2586
  return path.split(".").reduce((acc, key) => {
2477
2587
  if (acc == null || typeof acc !== "object") return void 0;
@@ -2479,20 +2589,42 @@ function getValue(obj, path) {
2479
2589
  }, obj);
2480
2590
  }
2481
2591
  var DEFAULT_LABEL_FALLBACK = "(Untitled)";
2592
+ function parsePipeExpression(pipeExpr) {
2593
+ const match = pipeExpr.match(/^(\w+)(?::(.*))?$/);
2594
+ if (!match) return { name: pipeExpr, args: [] };
2595
+ const name = match[1];
2596
+ const argsStr = match[2];
2597
+ if (!argsStr) return { name, args: [] };
2598
+ const args = [];
2599
+ const argRegex = /["']([^"']*?)["']/g;
2600
+ let argMatch;
2601
+ while ((argMatch = argRegex.exec(argsStr)) !== null) {
2602
+ args.push(argMatch[1]);
2603
+ }
2604
+ return { name, args };
2605
+ }
2482
2606
  function renderLabelExpression(template, values, fallback = DEFAULT_LABEL_FALLBACK) {
2483
2607
  const result = template.replace(/\{\{\s*([^}]+)\s*\}\}/g, (_, expr) => {
2484
2608
  const parts = expr.split("|").map((s) => s.trim());
2485
2609
  const path = parts[0];
2486
2610
  let value = getValue(values, path);
2487
- if (value == null || value === "") return "";
2611
+ const isEmpty3 = value == null || value === "";
2612
+ if (isEmpty3 && parts.length === 1) return "";
2488
2613
  for (let i = 1; i < parts.length; i++) {
2489
- const pipeName = parts[i].trim();
2490
- const fn = pipes[pipeName];
2491
- if (fn) {
2492
- value = fn(String(value));
2614
+ const { name: pipeName, args } = parsePipeExpression(parts[i]);
2615
+ const simpleFn = simplePipes[pipeName];
2616
+ if (simpleFn) {
2617
+ if (value != null && value !== "") {
2618
+ value = simpleFn(String(value));
2619
+ }
2620
+ } else {
2621
+ const argFn = pipesWithArgs[pipeName];
2622
+ if (argFn) {
2623
+ value = argFn(String(value ?? ""), ...args);
2624
+ }
2493
2625
  }
2494
2626
  }
2495
- return String(value);
2627
+ return String(value ?? "");
2496
2628
  }).trim();
2497
2629
  return result || fallback;
2498
2630
  }
@@ -2515,13 +2647,25 @@ function extractAttributeNames(template) {
2515
2647
  function hasOptions(attr) {
2516
2648
  return "options" in attr && Array.isArray(attr.options) && attr.options.length > 0;
2517
2649
  }
2518
- function enrichValuesWithSelectLabels(values, attributes) {
2650
+ var FORMATTABLE_TYPES = /* @__PURE__ */ new Set([
2651
+ "currency",
2652
+ "location",
2653
+ "phone",
2654
+ "date",
2655
+ "rating",
2656
+ "select",
2657
+ "status",
2658
+ "multiselect",
2659
+ "number"
2660
+ ]);
2661
+ function enrichValuesForDisplay(values, attributes) {
2519
2662
  const enriched = { ...values };
2520
2663
  for (const attr of attributes) {
2521
2664
  const value = values[attr.name];
2522
2665
  if (value == null) continue;
2666
+ if (!FORMATTABLE_TYPES.has(attr.type)) continue;
2523
2667
  const isSelectLike = attr.type === "select" || attr.type === "status" || attr.type === "multiselect";
2524
- if (!(isSelectLike && hasOptions(attr))) continue;
2668
+ if (isSelectLike && !hasOptions(attr)) continue;
2525
2669
  if (attr.type === "multiselect" && Array.isArray(value) && value.length === 0) continue;
2526
2670
  const formatted = formatAttributeValue(value, attr);
2527
2671
  if (formatted && formatted !== EMPTY_VALUE_PLACEHOLDER) {
@@ -2530,13 +2674,14 @@ function enrichValuesWithSelectLabels(values, attributes) {
2530
2674
  }
2531
2675
  return enriched;
2532
2676
  }
2677
+ var enrichValuesWithSelectLabels = enrichValuesForDisplay;
2533
2678
  function extractRelationIds(val) {
2534
2679
  if (typeof val === "string") return [val];
2535
2680
  if (Array.isArray(val) && typeof val[0] === "string") return [val[0]];
2536
2681
  return [];
2537
2682
  }
2538
2683
  async function computeLabelWithRelations(template, values, attributes, resolveRelationIds) {
2539
- let enrichedValues = enrichValuesWithSelectLabels(values, attributes);
2684
+ let enrichedValues = enrichValuesForDisplay(values, attributes);
2540
2685
  const attrNames = extractAttributeNames(template);
2541
2686
  const relationAttrs = attributes.filter(
2542
2687
  (attr) => attr.type === "relation" && attrNames.includes(attr.name)
@@ -3089,7 +3234,7 @@ function createMockObjectRecordsRepository(stores) {
3089
3234
  label: a.config.label ?? a.name,
3090
3235
  required: a.config.required ?? false
3091
3236
  }));
3092
- const enrichedValues = enrichValuesWithSelectLabels(r.values, attrs);
3237
+ const enrichedValues = enrichValuesForDisplay(r.values, attrs);
3093
3238
  return {
3094
3239
  objectId: r.objectId,
3095
3240
  objectName: obj?.name ?? "unknown",
@@ -4077,6 +4222,20 @@ var TenantAwareRepository = class {
4077
4222
  return getUserId();
4078
4223
  }
4079
4224
  };
4225
+ var SchemaContextAwareRepository = class extends TenantAwareRepository {
4226
+ /**
4227
+ * Get an ObjectDefinition from context by its ID.
4228
+ */
4229
+ getSchemaFromContext(objectId) {
4230
+ return getSchemaFromContext(objectId);
4231
+ }
4232
+ /**
4233
+ * Get an ObjectDefinition from context by its name.
4234
+ */
4235
+ getSchemaByNameFromContext(objectName) {
4236
+ return getSchemaByNameFromContext(objectName);
4237
+ }
4238
+ };
4080
4239
 
4081
4240
  // src/runtime/services/audit.service.ts
4082
4241
  var SENSITIVE_PATTERNS = [
@@ -7659,21 +7818,32 @@ function createAttributeValidator(attr, messages = DEFAULT_VALIDATION_MESSAGES)
7659
7818
  return z5.unknown();
7660
7819
  }
7661
7820
  }
7821
+ function isEmptyValue(value) {
7822
+ if (value === null || value === void 0) return true;
7823
+ if (typeof value === "string" && value.trim() === "") return true;
7824
+ if (value instanceof Date) return false;
7825
+ if (typeof value === "object" && !Array.isArray(value)) {
7826
+ return Object.values(value).every(
7827
+ (v) => v === null || v === void 0 || typeof v === "string" && v.trim() === ""
7828
+ );
7829
+ }
7830
+ return false;
7831
+ }
7832
+ function withEmptyToNull(validator) {
7833
+ return z5.preprocess((val) => isEmptyValue(val) ? null : val, validator.nullish());
7834
+ }
7662
7835
  function createFormAttributeValidator(attr, messages = DEFAULT_VALIDATION_MESSAGES) {
7663
7836
  const validator = createAttributeValidator(attr, messages);
7664
7837
  if (!attr.required) {
7665
- return validator.nullish();
7838
+ return withEmptyToNull(validator);
7666
7839
  }
7667
7840
  return validator;
7668
7841
  }
7669
7842
  function createObjectValidator(objectDef) {
7670
7843
  const shape = {};
7671
7844
  for (const attr of objectDef.attributes) {
7672
- let validator = createAttributeValidator(attr);
7673
- if (!attr.required) {
7674
- validator = validator.optional();
7675
- }
7676
- shape[attr.name] = validator;
7845
+ const validator = createAttributeValidator(attr);
7846
+ shape[attr.name] = attr.required ? validator : withEmptyToNull(validator);
7677
7847
  }
7678
7848
  return z5.object(shape).strict();
7679
7849
  }
@@ -7726,8 +7896,8 @@ ${errorMessages}`);
7726
7896
  function createDraftValidator(objectDef) {
7727
7897
  const shape = {};
7728
7898
  for (const attr of objectDef.attributes) {
7729
- const validator = createAttributeValidator(attr).nullish();
7730
- shape[attr.name] = validator;
7899
+ const validator = createAttributeValidator(attr);
7900
+ shape[attr.name] = withEmptyToNull(validator);
7731
7901
  }
7732
7902
  return z5.object(shape).strict();
7733
7903
  }
@@ -9293,6 +9463,8 @@ var RelationService = class extends TenantAwareService {
9293
9463
  }
9294
9464
  /**
9295
9465
  * Validate a single relation attribute value
9466
+ *
9467
+ * Uses batch fetching (findByIds) to avoid N+1 query pattern.
9296
9468
  */
9297
9469
  async validateRelationAttribute(attr, value) {
9298
9470
  const errors = [];
@@ -9309,9 +9481,11 @@ var RelationService = class extends TenantAwareService {
9309
9481
  });
9310
9482
  return errors;
9311
9483
  }
9484
+ const records = await this.adapter.objectRecords.findByIds(ids);
9485
+ const recordMap = new Map(records.map((r) => [r.id, r]));
9312
9486
  const invalidIds = [];
9313
9487
  for (const id of ids) {
9314
- const record = await this.adapter.objectRecords.findById(id);
9488
+ const record = recordMap.get(id);
9315
9489
  if (!record) {
9316
9490
  invalidIds.push(id);
9317
9491
  continue;
@@ -9625,22 +9799,37 @@ var RollupService = class {
9625
9799
  return { value: null, recordCount: 0 };
9626
9800
  }
9627
9801
  const sourceObjectName = rollupAttr.relationAttribute;
9628
- const sourceObject = await this.adapter.objects.findByName(sourceObjectName);
9629
- if (!sourceObject) {
9630
- return { value: null, recordCount: 0 };
9802
+ const sourceSchema = getSchemaByNameFromContext(sourceObjectName);
9803
+ let sourceObjectId;
9804
+ let reverseRelationAttrName;
9805
+ if (sourceSchema?.id) {
9806
+ sourceObjectId = sourceSchema.id;
9807
+ const reverseRelationAttr = sourceSchema.attributes.find((attr) => {
9808
+ if (attr.type !== "relation") return false;
9809
+ const relationConfig = attr;
9810
+ return relationConfig?.targets?.some((t) => t.object === schema.name);
9811
+ });
9812
+ reverseRelationAttrName = reverseRelationAttr?.name;
9813
+ } else {
9814
+ const sourceObject = await this.adapter.objects.findByName(sourceObjectName);
9815
+ if (!sourceObject) {
9816
+ return { value: null, recordCount: 0 };
9817
+ }
9818
+ sourceObjectId = sourceObject.id;
9819
+ const sourceAttributes = await this.adapter.attributes.findByObjectId(sourceObject.id);
9820
+ const reverseRelationAttr = sourceAttributes.find((attr) => {
9821
+ if (attr.type !== "relation") return false;
9822
+ const relationConfig = attr.config;
9823
+ return relationConfig?.targets?.some((t) => t.object === schema.name);
9824
+ });
9825
+ reverseRelationAttrName = reverseRelationAttr?.name;
9631
9826
  }
9632
- const sourceAttributes = await this.adapter.attributes.findByObjectId(sourceObject.id);
9633
- const reverseRelationAttr = sourceAttributes.find((attr) => {
9634
- if (attr.type !== "relation") return false;
9635
- const relationConfig = attr.config;
9636
- return relationConfig?.targets?.some((t) => t.object === schema.name);
9637
- });
9638
- if (!reverseRelationAttr) {
9827
+ if (!reverseRelationAttrName) {
9639
9828
  return { value: null, recordCount: 0 };
9640
9829
  }
9641
9830
  const relatedRecords = await this.adapter.objectRecords.findByRelation(
9642
- sourceObject.id,
9643
- reverseRelationAttr.name,
9831
+ sourceObjectId,
9832
+ reverseRelationAttrName,
9644
9833
  recordId
9645
9834
  );
9646
9835
  if (relatedRecords.length === 0) {
@@ -10179,7 +10368,7 @@ var RecordService = class extends TenantAwareService {
10179
10368
  */
10180
10369
  async computeLabel(schema, values) {
10181
10370
  const attrNames = extractAttributeNames(schema.labelExpression);
10182
- let enrichedValues = enrichValuesWithSelectLabels(values, schema.attributes);
10371
+ let enrichedValues = enrichValuesForDisplay(values, schema.attributes);
10183
10372
  const relationAttrs = schema.attributes.filter(
10184
10373
  (attr) => attr.type === "relation" && attrNames.includes(attr.name)
10185
10374
  );
@@ -10767,7 +10956,10 @@ var RecordService = class extends TenantAwareService {
10767
10956
  if (policy?.applyListFilter) {
10768
10957
  effectiveOptions = policy.applyListFilter(this.buildPolicyContext(schema.name), options);
10769
10958
  }
10770
- const result = await this.adapter.objectRecords.list(objectId, effectiveOptions);
10959
+ const result = await runWithSchemaContext(
10960
+ [schema],
10961
+ () => this.adapter.objectRecords.list(objectId, effectiveOptions)
10962
+ );
10771
10963
  let filteredRecords = result.records;
10772
10964
  let effectiveTotal = result.total;
10773
10965
  if (policy?.canAccessRecord) {
@@ -10799,7 +10991,10 @@ var RecordService = class extends TenantAwareService {
10799
10991
  if (this.permissionService && this.userId) {
10800
10992
  await this.checkPermission(schema.name, "read");
10801
10993
  }
10802
- const result = await this.adapter.objectRecords.search(objectId, query, options);
10994
+ const result = await runWithSchemaContext(
10995
+ [schema],
10996
+ () => this.adapter.objectRecords.search(objectId, query, options)
10997
+ );
10803
10998
  if (!options?.skipFormulas) {
10804
10999
  return {
10805
11000
  records: this.enrichRecordsWithFormulas(result.records, schema),
@@ -13306,6 +13501,13 @@ export {
13306
13501
  QueryNoResultError,
13307
13502
  QueryMultipleResultsError,
13308
13503
  TenantContextError,
13504
+ getSchemaFromContext,
13505
+ getSchemaByNameFromContext,
13506
+ hasSchemaContext,
13507
+ getSchemaContext,
13508
+ addSchemaToContext,
13509
+ runWithSchemaContext,
13510
+ runWithMergedSchemaContext,
13309
13511
  getContext,
13310
13512
  getTenantId,
13311
13513
  getUserId,
@@ -13356,6 +13558,7 @@ export {
13356
13558
  renderLabelExpression,
13357
13559
  isLabelExpression,
13358
13560
  extractAttributeNames,
13561
+ enrichValuesForDisplay,
13359
13562
  enrichValuesWithSelectLabels,
13360
13563
  extractRelationIds,
13361
13564
  computeLabelWithRelations,
@@ -13366,6 +13569,7 @@ export {
13366
13569
  buildAuditChanges,
13367
13570
  TenantAwareService,
13368
13571
  TenantAwareRepository,
13572
+ SchemaContextAwareRepository,
13369
13573
  AuditService,
13370
13574
  FileService,
13371
13575
  GeocodingService,