@servicenow/sdk-build-core 4.2.0 → 4.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/dist/compiler.d.ts +5 -1
  2. package/dist/compiler.js +104 -7
  3. package/dist/compiler.js.map +1 -1
  4. package/dist/now-config-dependencies.d.ts +2 -1
  5. package/dist/now-config.d.ts +5 -0
  6. package/dist/now-config.js +2 -4
  7. package/dist/now-config.js.map +1 -1
  8. package/dist/plugins/cache.d.ts +8 -13
  9. package/dist/plugins/cache.js +2 -26
  10. package/dist/plugins/cache.js.map +1 -1
  11. package/dist/plugins/data-shape.d.ts +24 -33
  12. package/dist/plugins/data-shape.js +83 -73
  13. package/dist/plugins/data-shape.js.map +1 -1
  14. package/dist/plugins/plugin.d.ts +43 -1
  15. package/dist/plugins/plugin.js +49 -5
  16. package/dist/plugins/plugin.js.map +1 -1
  17. package/dist/plugins/shape.d.ts +19 -11
  18. package/dist/plugins/shape.js +61 -23
  19. package/dist/plugins/shape.js.map +1 -1
  20. package/dist/taxonomy.js +53 -1
  21. package/dist/taxonomy.js.map +1 -1
  22. package/dist/telemetry/clients/abstract-client.d.ts +25 -0
  23. package/dist/telemetry/clients/abstract-client.js +55 -0
  24. package/dist/telemetry/clients/abstract-client.js.map +1 -0
  25. package/dist/telemetry/clients/browser-client.d.ts +20 -0
  26. package/dist/telemetry/clients/browser-client.js +136 -0
  27. package/dist/telemetry/clients/browser-client.js.map +1 -0
  28. package/dist/telemetry/clients/node-client.d.ts +15 -0
  29. package/dist/telemetry/clients/node-client.js +159 -0
  30. package/dist/telemetry/clients/node-client.js.map +1 -0
  31. package/dist/telemetry/clients/noop-client.d.ts +10 -0
  32. package/dist/telemetry/clients/noop-client.js +18 -0
  33. package/dist/telemetry/clients/noop-client.js.map +1 -0
  34. package/dist/telemetry/clients/util.d.ts +11 -0
  35. package/dist/telemetry/clients/util.js +34 -0
  36. package/dist/telemetry/clients/util.js.map +1 -0
  37. package/dist/telemetry/config.d.ts +2 -0
  38. package/dist/telemetry/config.js +28 -0
  39. package/dist/telemetry/config.js.map +1 -0
  40. package/dist/telemetry/factory.d.ts +13 -0
  41. package/dist/telemetry/factory.js +29 -0
  42. package/dist/telemetry/factory.js.map +1 -0
  43. package/dist/telemetry/index.d.ts +2 -25
  44. package/dist/telemetry/index.js +3 -15
  45. package/dist/telemetry/index.js.map +1 -1
  46. package/dist/telemetry/types.d.ts +55 -0
  47. package/dist/telemetry/types.js +12 -0
  48. package/dist/telemetry/types.js.map +1 -0
  49. package/dist/typescript.d.ts +10 -0
  50. package/dist/typescript.js +18 -0
  51. package/dist/typescript.js.map +1 -1
  52. package/dist/util/delete-multiple.d.ts +5 -0
  53. package/dist/util/delete-multiple.js +30 -0
  54. package/dist/util/delete-multiple.js.map +1 -0
  55. package/dist/util/index.d.ts +1 -0
  56. package/dist/util/index.js +1 -0
  57. package/dist/util/index.js.map +1 -1
  58. package/now.config.schema.json +38 -0
  59. package/package.json +13 -9
  60. package/src/compiler.ts +121 -7
  61. package/src/now-config-dependencies.ts +2 -1
  62. package/src/now-config.ts +3 -4
  63. package/src/plugins/cache.ts +5 -27
  64. package/src/plugins/data-shape.ts +95 -84
  65. package/src/plugins/plugin.ts +116 -9
  66. package/src/plugins/shape.ts +64 -30
  67. package/src/taxonomy.ts +53 -1
  68. package/src/telemetry/clients/abstract-client.ts +63 -0
  69. package/src/telemetry/clients/browser-client.ts +160 -0
  70. package/src/telemetry/clients/node-client.ts +151 -0
  71. package/src/telemetry/clients/noop-client.ts +15 -0
  72. package/src/telemetry/clients/util.ts +33 -0
  73. package/src/telemetry/config.ts +12 -0
  74. package/src/telemetry/factory.ts +34 -0
  75. package/src/telemetry/index.ts +2 -27
  76. package/src/telemetry/types.ts +61 -0
  77. package/src/typescript.ts +17 -0
  78. package/src/util/delete-multiple.ts +35 -0
  79. package/src/util/index.ts +1 -0
@@ -1,4 +1,4 @@
1
- import { ObjectShape, ArrayShape, Shape, type StringShape } from './shape'
1
+ import { CallExpressionShape, ObjectShape, Shape, StringShape } from './shape'
2
2
  import type { Source } from './product'
3
3
  import {
4
4
  durationFieldToXML,
@@ -20,10 +20,11 @@ export const DATA_HELPER_NAMES = {
20
20
  } as const
21
21
 
22
22
  /**
23
- * Base class for serializable data shapes that extend ObjectShape.
23
+ * Base class for data helper shapes that extend CallExpressionShape.
24
+ * These shapes represent call expressions like Duration(), Time(), etc.
24
25
  * Provides common equals implementation that compares via toString().
25
26
  */
26
- abstract class SerializableObjectShape extends ObjectShape {
27
+ abstract class DataHelperShape extends CallExpressionShape {
27
28
  abstract override toString(): StringShape
28
29
 
29
30
  override equals(other: unknown): boolean {
@@ -38,50 +39,30 @@ abstract class SerializableObjectShape extends ObjectShape {
38
39
  }
39
40
 
40
41
  /**
41
- * Base class for serializable data shapes that extend ArrayShape.
42
- * Provides common equals implementation that compares via toString().
42
+ * Shape representing a duration value.
43
+ * Used with Duration() helper - represents Duration({ days, hours, minutes, seconds }) call expression.
43
44
  */
44
- abstract class SerializableArrayShape extends ArrayShape {
45
- abstract override toString(): StringShape
46
-
47
- override equals(other: unknown): boolean {
48
- if (typeof other === 'string') {
49
- return this.toString().getValue() === other
50
- } else if (other instanceof Shape) {
51
- return other.equals(this.toString().getValue())
45
+ export class DurationShape extends DataHelperShape {
46
+ constructor({ source, value }: { source: Source; value: Duration | ObjectShape }) {
47
+ let durationValue: Duration
48
+ if (value instanceof ObjectShape) {
49
+ const picked = value.pick(['days', 'hours', 'minutes', 'seconds'])
50
+ durationValue = Object.fromEntries(
51
+ Object.entries(picked).map(([k, v]) => [
52
+ k,
53
+ Shape.from(source, v).asNumber(
54
+ `fields in Duration object can only contain numbers, got ${typeof v}`
55
+ ),
56
+ ])
57
+ ) as Duration
52
58
  } else {
53
- return super.equals(other)
59
+ durationValue = value
54
60
  }
55
- }
56
- }
57
-
58
- /**
59
- * Shape representing a duration value.
60
- * Used with Duration() helper - stores object with days, hours, minutes, seconds.
61
- */
62
- export class DurationShape extends SerializableObjectShape {
63
- constructor({ source, value }: { source: Source; value: Duration }) {
64
- super({ source, properties: value })
61
+ super({ source, callee: DATA_HELPER_NAMES.DURATION, args: [durationValue] })
65
62
  }
66
63
 
67
64
  getDuration(): Duration {
68
- return super.getValue() as Duration
69
- }
70
-
71
- getDays(): number {
72
- return this.getDuration().days ?? 0
73
- }
74
-
75
- getHours(): number {
76
- return this.getDuration().hours ?? 0
77
- }
78
-
79
- getMinutes(): number {
80
- return this.getDuration().minutes ?? 0
81
- }
82
-
83
- getSeconds(): number {
84
- return this.getDuration().seconds ?? 0
65
+ return this.getArgument(0).asObject().getValue() as Duration
85
66
  }
86
67
 
87
68
  /**
@@ -94,51 +75,77 @@ export class DurationShape extends SerializableObjectShape {
94
75
  return Shape.from(this.getSource(), formatted).asString()
95
76
  }
96
77
 
97
- static override from(source: Source, text: StringShape): DurationShape {
98
- const duration = parseGlideDuration(text.getValue())
99
- if (!duration) {
100
- throw new Error(`Invalid duration value: ${text.getValue()}`)
78
+ static override from(source: Source, value: StringShape | ObjectShape): DurationShape {
79
+ if (value instanceof StringShape) {
80
+ const parsedValue = parseGlideDuration(value.getValue())
81
+ if (!parsedValue) {
82
+ throw new Error(`Invalid duration value: ${value.getValue()}`)
83
+ }
84
+ return new DurationShape({
85
+ source,
86
+ value: parsedValue,
87
+ })
101
88
  }
102
89
 
103
90
  return new DurationShape({
104
91
  source,
105
- value: duration,
92
+ value: value,
93
+ })
94
+ }
95
+
96
+ /**
97
+ * Creates a DurationShape representing zero duration.
98
+ * Use this when the platform returns an empty or default duration value.
99
+ * @example DurationShape.zero(source) // Returns Duration({ seconds: 0 })
100
+ */
101
+ static zero(source: Source): DurationShape {
102
+ return new DurationShape({
103
+ source,
104
+ value: { seconds: 0 },
106
105
  })
107
106
  }
108
107
  }
109
108
 
110
109
  /**
111
110
  * Shape representing a time of day value.
112
- * Used with Time() helper - stores object with hours, minutes, seconds.
111
+ * Used with Time() helper - represents Time({ hours, minutes, seconds }, timezone?) call expression.
113
112
  */
114
- export class TimeShape extends SerializableObjectShape {
113
+ export class TimeShape extends DataHelperShape {
115
114
  private readonly timeZone: string | undefined
116
115
 
117
- constructor({ source, value, timeZone }: { source: Source; value: TimeOfDay; timeZone?: string | undefined }) {
118
- super({ source, properties: value })
119
- this.timeZone = timeZone ?? undefined
116
+ constructor({
117
+ source,
118
+ value,
119
+ timeZone,
120
+ }: { source: Source; value: TimeOfDay | ObjectShape; timeZone?: string | undefined }) {
121
+ let timeValue: TimeOfDay
122
+ if (value instanceof ObjectShape) {
123
+ const picked = value.pick(['hours', 'minutes', 'seconds'])
124
+ timeValue = Object.fromEntries(
125
+ Object.entries(picked).map(([k, v]) => [
126
+ k,
127
+ Shape.from(source, v).asNumber(`fields in Time object can only contain numbers, got ${typeof v}`),
128
+ ])
129
+ ) as TimeOfDay
130
+ } else {
131
+ timeValue = value
132
+ }
133
+ const args: unknown[] = [timeValue]
134
+ if (timeZone) {
135
+ args.push(timeZone)
136
+ }
137
+ super({ source, callee: DATA_HELPER_NAMES.TIME, args })
138
+ this.timeZone = timeZone
120
139
  }
121
140
 
122
141
  getTimeData(): TimeOfDay {
123
- return super.getValue() as TimeOfDay
142
+ return this.getArgument(0).asObject().getValue() as TimeOfDay
124
143
  }
125
144
 
126
145
  getTimeZone(): string | undefined {
127
146
  return this.timeZone
128
147
  }
129
148
 
130
- getHours(): number {
131
- return this.getTimeData().hours ?? 0
132
- }
133
-
134
- getMinutes(): number {
135
- return this.getTimeData().minutes ?? 0
136
- }
137
-
138
- getSeconds(): number {
139
- return this.getTimeData().seconds ?? 0
140
- }
141
-
142
149
  /**
143
150
  * Returns a StringShape with the ServiceNow serialized format.
144
151
  * @example '1970-01-01 14:30:00' for { hours: 14, minutes: 30 }
@@ -149,14 +156,22 @@ export class TimeShape extends SerializableObjectShape {
149
156
  return Shape.from(this.getSource(), formatted).toString()
150
157
  }
151
158
 
152
- static override from(source: Source, text: StringShape, timeZone?: string): TimeShape {
153
- const time = parseGlideTime(text.getValue(), timeZone)
154
- if (!time) {
155
- throw new Error(`Invalid time value: ${text.getValue()}`)
159
+ static override from(source: Source, value: StringShape | ObjectShape, timeZone?: string): TimeShape {
160
+ if (value instanceof StringShape) {
161
+ const parsedValue = parseGlideTime(value.getValue(), timeZone)
162
+ if (!parsedValue) {
163
+ throw new Error(`Invalid time value: ${value.getValue()}`)
164
+ }
165
+ return new TimeShape({
166
+ source,
167
+ value: parsedValue,
168
+ timeZone,
169
+ })
156
170
  }
171
+
157
172
  return new TimeShape({
158
173
  source,
159
- value: time,
174
+ value: value,
160
175
  timeZone,
161
176
  })
162
177
  }
@@ -164,15 +179,15 @@ export class TimeShape extends SerializableObjectShape {
164
179
 
165
180
  /**
166
181
  * Shape representing a field list.
167
- * Used with FieldList() helper - stores array of field names.
182
+ * Used with FieldList() helper - represents FieldList(["field1", "field2"]) call expression.
168
183
  */
169
- export class FieldListShape extends SerializableArrayShape {
184
+ export class FieldListShape extends DataHelperShape {
170
185
  constructor({ source, fields }: { source: Source; fields: Shape[] }) {
171
- super({ source, elements: fields })
186
+ super({ source, callee: DATA_HELPER_NAMES.FIELD_LIST, args: [fields] })
172
187
  }
173
188
 
174
189
  getFields(): Shape[] {
175
- return super.getElements()
190
+ return this.getArgument(0).asArray().getElements()
176
191
  }
177
192
 
178
193
  /**
@@ -198,19 +213,15 @@ export class FieldListShape extends SerializableArrayShape {
198
213
 
199
214
  /**
200
215
  * Shape representing a template value (ServiceNow encoded query format).
201
- * Used with TemplateValue() helper - stores object with field-value pairs.
216
+ * Used with TemplateValue() helper - represents TemplateValue({ field: value }) call expression.
202
217
  */
203
- export class TemplateValueShape extends SerializableObjectShape {
218
+ export class TemplateValueShape extends DataHelperShape {
204
219
  constructor({ source, value }: { source: Source; value: globalThis.Record<string, unknown> }) {
205
- super({ source, properties: value })
206
- }
207
-
208
- getFields(): string[] {
209
- return Object.keys(this.getValue())
220
+ super({ source, callee: DATA_HELPER_NAMES.TEMPLATE_VALUE, args: [value] })
210
221
  }
211
222
 
212
- getFieldValue(fieldName: string): unknown {
213
- return this.getValue()[fieldName]
223
+ getTemplateValue(): ObjectShape {
224
+ return this.getArgument(0).asObject()
214
225
  }
215
226
 
216
227
  /**
@@ -220,8 +231,8 @@ export class TemplateValueShape extends SerializableObjectShape {
220
231
  * @example 'name=value^^with^^caret^EQ' for { name: 'value^with^caret' }
221
232
  */
222
233
  override toString(): StringShape {
223
- const entries = Object.entries(this.getValue())
224
- const encoded = entries
234
+ const templateValue = this.getTemplateValue().getValue()
235
+ const encoded = Object.entries(templateValue)
225
236
  .map(([key, value]) => {
226
237
  // Escape single ^ as ^^ in values
227
238
  const escapedValue = String(value).replace(/\^/g, '^^')
@@ -25,6 +25,11 @@ export type Result<Value = unknown> =
25
25
  success: true
26
26
  value: Value
27
27
  }
28
+ | {
29
+ success: 'partial'
30
+ value: Value
31
+ unhandledRecords: Record[]
32
+ }
28
33
 
29
34
  export type CommitResult = { success: boolean }
30
35
 
@@ -92,6 +97,36 @@ export type CoalesceStrategy =
92
97
 
93
98
  export type FileType = 'fluent' | 'module' | 'json' | 'unknown'
94
99
 
100
+ export type PluginApiDoc = {
101
+ /**
102
+ * The name of the API (e.g., 'BusinessRule', 'Acl', 'Table')
103
+ * This is the callee name used in fluent code.
104
+ */
105
+ apiName: string
106
+
107
+ /**
108
+ * Uses module-specifier path to the markdown documentation file for this API relative to the SDK root.
109
+ * For example, @servicenow/sdk/docs/BusinessRule/BusinessRule.md
110
+ */
111
+ docPath: string
112
+
113
+ /**
114
+ * Tags for categorizing and organizing the API documentation
115
+ */
116
+ tags: string[]
117
+ }
118
+
119
+ /**
120
+ * Manifest structure for API documentation.
121
+ * This is the opinionated format returned by getDocsMetadata().
122
+ */
123
+ export type DocsManifest = {
124
+ [apiName: string]: {
125
+ docPath: string
126
+ tags: string[]
127
+ }
128
+ }
129
+
95
130
  export type PluginConfig<
96
131
  Nodes extends SupportedKindName[] = SupportedKindName[],
97
132
  Shapes extends ShapeClass[] = ShapeClass[],
@@ -103,6 +138,12 @@ export type PluginConfig<
103
138
  */
104
139
  name: `${string}Plugin`
105
140
 
141
+ /**
142
+ * Documentation for the APIs provided by this plugin. This is used to automatically
143
+ * generate documentation that can be retrieved via the getDocsMetadata() method.
144
+ */
145
+ docs?: PluginApiDoc[]
146
+
106
147
  /**
107
148
  * The TypeScript AST nodes this plugin handles. Plugins that do not introduce new
108
149
  * syntax should not need to define any handlers here.
@@ -380,7 +421,12 @@ export type PluginConfig<
380
421
  * @returns A result indicating whether diffing was successful and, if so, a database
381
422
  * containing only the changed records
382
423
  */
383
- diff?: (existing: Database, incoming: Database, context: Context) => Promise<Result<Database>>
424
+ diff?: (
425
+ existing: Database,
426
+ incoming: Database,
427
+ descendants: Database,
428
+ context: Context
429
+ ) => Promise<Result<Database>>
384
430
  }
385
431
  }
386
432
 
@@ -595,6 +641,28 @@ export class Plugin {
595
641
  return this.config.name
596
642
  }
597
643
 
644
+ /**
645
+ * Get documentation metadata for APIs provided by this plugin.
646
+ * Returns a manifest object with raw (unresolved) documentation paths.
647
+ * Path resolution is handled by PluginRegistry.getDocsMetadata() using the isomorphic resolver.
648
+ */
649
+ getDocsMetadata(): DocsManifest | undefined {
650
+ if (!this.config.docs) {
651
+ return undefined
652
+ }
653
+
654
+ const manifest: DocsManifest = {}
655
+
656
+ for (const doc of this.config.docs) {
657
+ manifest[doc.apiName] = {
658
+ docPath: doc.docPath,
659
+ tags: doc.tags,
660
+ }
661
+ }
662
+
663
+ return manifest
664
+ }
665
+
598
666
  getDescendants(parent: Record, database: Database): Record[] {
599
667
  const descendantRelationships = Object.entries(this.relationships[parent.getTable()] ?? {}).filter(
600
668
  ([, { descendant }]) => descendant
@@ -720,7 +788,7 @@ export class Plugin {
720
788
  async nodeToShape(node: SupportedNode, context: Omit<Context, 'self'>): Promise<Result<Shape>> {
721
789
  const entry = this.nodeToShapeCache.get(node.compilerNode)
722
790
  if (entry) {
723
- return entry.isError() ? { success: false } : entry.unwrap()
791
+ return entry.success ? entry.value : { success: false }
724
792
  }
725
793
 
726
794
  try {
@@ -773,7 +841,7 @@ export class Plugin {
773
841
  async shapeToSubclass<const S extends Shape>(shape: S, context: Omit<Context, 'self'>): Promise<Result<S>> {
774
842
  const entry = this.shapeToSubclassCache.get(shape)
775
843
  if (entry) {
776
- return entry.isError() ? { success: false } : (entry.unwrap() as Result<S>)
844
+ return entry.success ? (entry.value as Result<S>) : { success: false }
777
845
  }
778
846
 
779
847
  try {
@@ -813,7 +881,7 @@ export class Plugin {
813
881
  async shapeToRecord(shape: Shape, context: Omit<Context, 'self'>): Promise<Result<Record>> {
814
882
  const entry = this.shapeToRecordCache.get(shape)
815
883
  if (entry) {
816
- return entry.isError() ? { success: false } : entry.unwrap()
884
+ return entry.success ? entry.value : { success: false }
817
885
  }
818
886
 
819
887
  try {
@@ -910,6 +978,8 @@ export class Plugin {
910
978
  continue
911
979
  }
912
980
 
981
+ context.logger.debug(`Database contains ${database.query().length} records for transformation.`)
982
+
913
983
  context.logger.debug(
914
984
  `Transforming record into shape: ${record.getTable()}.${record.getId().getValue()}`
915
985
  )
@@ -943,12 +1013,34 @@ export class Plugin {
943
1013
  descendants: nonDeletedDescendants,
944
1014
  })
945
1015
 
1016
+ // Track unhandled records to avoid marking them as handled
1017
+ const unhandledRecords: Record[] = []
1018
+ const unhandledRecordIds: string[] = []
1019
+
1020
+ if (result.success === 'partial') {
1021
+ for (const unhandledRecord of result.unhandledRecords) {
1022
+ unhandledRecords.push(unhandledRecord)
1023
+ unhandledRecordIds.push(unhandledRecord.getId().getValue())
1024
+ }
1025
+ }
1026
+
946
1027
  if (result.success) {
947
1028
  success = true
948
1029
  handledRecords.insert(record)
949
1030
  shapes.push(...[result.value].flat())
1031
+
1032
+ // Mark all descendants as handled
950
1033
  for (const descendant of [...deletedDescendants, ...nonDeletedDescendants]) {
951
- handledRecords.insert(descendant)
1034
+ if (!unhandledRecordIds.includes(descendant.getId().getValue())) {
1035
+ handledRecords.insert(descendant)
1036
+ }
1037
+ }
1038
+ context.logger.debug(
1039
+ `Plugin ${this.getName()} deferred handling of records: ${unhandledRecords.map((r) => r.getTable() + '_' + r.getId().getValue()).join(', ')}`
1040
+ )
1041
+
1042
+ if (unhandledRecords.length > 0) {
1043
+ return { success: 'partial', value: shapes, unhandledRecords }
952
1044
  }
953
1045
  }
954
1046
  }
@@ -998,10 +1090,15 @@ export class Plugin {
998
1090
  ...[incoming.resolve(record.getId())].filter((r) => r !== undefined),
999
1091
  ...incoming.query().filter((r) => descendantsDatabase.resolve(r.getId()) !== undefined),
1000
1092
  ]
1001
- const result = await diff.bind(this)(new Database(existingTree), new Database(incomingTree), {
1002
- ...context,
1003
- self: this,
1004
- })
1093
+ const result = await diff.bind(this)(
1094
+ new Database(existingTree),
1095
+ new Database(incomingTree),
1096
+ new Database(descendants),
1097
+ {
1098
+ ...context,
1099
+ self: this,
1100
+ }
1101
+ )
1005
1102
 
1006
1103
  if (result.success) {
1007
1104
  changedRecords.push(...result.value.query())
@@ -1245,4 +1342,14 @@ export class Plugins {
1245
1342
 
1246
1343
  return false
1247
1344
  }
1345
+
1346
+ getDocsMetadata(): DocsManifest {
1347
+ return this.plugins.reduce((aggregated, plugin) => {
1348
+ const pluginManifest = plugin.getDocsMetadata()
1349
+ if (pluginManifest) {
1350
+ Object.assign(aggregated, pluginManifest)
1351
+ }
1352
+ return aggregated
1353
+ }, {} as DocsManifest)
1354
+ }
1248
1355
  }
@@ -366,6 +366,10 @@ export class UndefinedShape extends Shape<undefined> {
366
366
  override toString(): StringShape {
367
367
  return Shape.from(this, '').asString()
368
368
  }
369
+
370
+ override getCode(): string {
371
+ return 'undefined'
372
+ }
369
373
  }
370
374
 
371
375
  export class DeletedShape extends UndefinedShape {}
@@ -473,6 +477,25 @@ export class PropertyAccessShape extends ResolvableShape {
473
477
  override getCode(): string {
474
478
  return this.elements.map((e) => e.getCode()).join('.')
475
479
  }
480
+
481
+ /**
482
+ * Override equals to compare property access paths instead of resolved values.
483
+ * This is important for datapill references where we need to compare the actual
484
+ * property paths (e.g., params.trigger.table_name vs params.trigger.current.user_name)
485
+ * rather than their resolved values (which would both be UnresolvedShape).
486
+ */
487
+ override equals(other: unknown): boolean {
488
+ if (other instanceof PropertyAccessShape) {
489
+ // Compare the property access paths element by element
490
+ const otherElements = other.getElements()
491
+ if (this.elements.length !== otherElements.length) {
492
+ return false
493
+ }
494
+ return this.elements.every((el, i) => el.getName() === otherElements[i]?.getName())
495
+ }
496
+ // Fall back to default behavior for non-PropertyAccessShape comparisons
497
+ return super.equals(other)
498
+ }
476
499
  }
477
500
 
478
501
  type StringContentType = 'plain' | 'cdata'
@@ -800,7 +823,7 @@ const coerceTo = (coerceToType: 'string' | 'number' | 'boolean' | 'cdata', value
800
823
  }
801
824
  }
802
825
 
803
- class Transform {
826
+ export class ShapeTransform {
804
827
  constructor(
805
828
  private readonly shape: ObjectShape,
806
829
  private readonly resolve = true,
@@ -844,35 +867,35 @@ class Transform {
844
867
  }
845
868
 
846
869
  from(...properties: [string, ...string[]]) {
847
- return new Transform(this.shape, this.resolve, properties, this._map, this._def, this._coerce)
870
+ return new ShapeTransform(this.shape, this.resolve, properties, this._map, this._def, this._coerce)
848
871
  }
849
872
 
850
873
  map(mapFunction: (...from: Shape[]) => unknown) {
851
- return new Transform(this.shape, this.resolve, this._from, mapFunction, this._def, this._coerce)
874
+ return new ShapeTransform(this.shape, this.resolve, this._from, mapFunction, this._def, this._coerce)
852
875
  }
853
876
 
854
877
  val(value: unknown) {
855
- return new Transform(this.shape, this.resolve, this._from, () => value, this._def, this._coerce)
878
+ return new ShapeTransform(this.shape, this.resolve, this._from, () => value, this._def, this._coerce)
856
879
  }
857
880
 
858
881
  def(value: unknown) {
859
- return new Transform(this.shape, this.resolve, this._from, this._map, value, this._coerce)
882
+ return new ShapeTransform(this.shape, this.resolve, this._from, this._map, value, this._coerce)
860
883
  }
861
884
 
862
885
  toCdata() {
863
- return new Transform(this.shape, this.resolve, this._from, this._map, this._def, 'cdata')
886
+ return new ShapeTransform(this.shape, this.resolve, this._from, this._map, this._def, 'cdata')
864
887
  }
865
888
 
866
889
  toString() {
867
- return new Transform(this.shape, this.resolve, this._from, this._map, this._def, 'string')
890
+ return new ShapeTransform(this.shape, this.resolve, this._from, this._map, this._def, 'string')
868
891
  }
869
892
 
870
893
  toNumber() {
871
- return new Transform(this.shape, this.resolve, this._from, this._map, this._def, 'number')
894
+ return new ShapeTransform(this.shape, this.resolve, this._from, this._map, this._def, 'number')
872
895
  }
873
896
 
874
897
  toBoolean() {
875
- return new Transform(this.shape, this.resolve, this._from, this._map, this._def, 'boolean')
898
+ return new ShapeTransform(this.shape, this.resolve, this._from, this._map, this._def, 'boolean')
876
899
  }
877
900
  }
878
901
 
@@ -936,26 +959,37 @@ export class ObjectShape extends Shape<globalThis.Record<string, unknown>> {
936
959
  nonDefaultsOnly = false,
937
960
  resolve = true,
938
961
  }: ObjectPropertyOptions = {}): globalThis.Record<string, Shape> {
962
+ const result: globalThis.Record<string, Shape> = {}
963
+
939
964
  if (nonDefaultsOnly) {
940
- const entries = Object.entries(this.setProperties).filter(
941
- ([k, v]) => v.isDefined() && !v.equals(this.getDefault(k))
942
- )
965
+ // Only include properties that differ from defaults
966
+ for (const k in this.setProperties) {
967
+ const v = this.setProperties[k]
968
+ if (v && v.isDefined() && !v.equals(this.getDefault(k))) {
969
+ result[k] = resolve && v.isResolvable() ? v.resolve() : v
970
+ }
971
+ }
972
+ return result
973
+ }
943
974
 
944
- return Object.fromEntries(
945
- resolve ? entries.map(([k, v]) => [k, v.isResolvable() ? v.resolve() : v]) : entries
946
- )
975
+ // Iterate set properties first to preserve ordering
976
+ for (const k in this.setProperties) {
977
+ const v = this.setProperties[k]
978
+ if (v) {
979
+ result[k] = resolve && v.isResolvable() ? v.resolve() : v
980
+ }
947
981
  }
948
982
 
949
- // This logic may seem odd, but it's written this way to preserve the ordering of
950
- // the set properties instead of using the ordering of the defaults.
951
- const entries = [
952
- ...Object.entries(this.setProperties),
953
- ...Object.entries(this.defaultProperties).filter(
954
- ([k]) => !this.setProperties[k] || this.setProperties[k].isUndefined()
955
- ),
956
- ]
983
+ for (const k in this.defaultProperties) {
984
+ if (!this.setProperties[k] || this.setProperties[k].isUndefined()) {
985
+ const v = this.defaultProperties[k]
986
+ if (v) {
987
+ result[k] = resolve && v.isResolvable() ? v.resolve() : v
988
+ }
989
+ }
990
+ }
957
991
 
958
- return Object.fromEntries(resolve ? entries.map(([k, v]) => [k, v.isResolvable() ? v.resolve() : v]) : entries)
992
+ return result
959
993
  }
960
994
 
961
995
  override getValue(nonDefaultsOnly = false): globalThis.Record<string, unknown> {
@@ -966,8 +1000,8 @@ export class ObjectShape extends Shape<globalThis.Record<string, unknown>> {
966
1000
  const [property, next, ...rest] = typeof propertyOrPath === 'string' ? [propertyOrPath] : propertyOrPath
967
1001
  const propertyAliases = [property, ...this.getAliases(property)]
968
1002
  const properties = this.properties({ resolve })
969
- const match = propertyAliases.find((p) => properties[p])
970
- const value = match ? properties[match] : undefined
1003
+ const match = propertyAliases.find((p) => p in properties)
1004
+ const value = match !== undefined ? properties[match] : undefined
971
1005
  return (
972
1006
  (next ? value?.ifObject()?.get([next, ...rest], resolve) : value) ??
973
1007
  new UndefinedShape({ source: this.getSource() })
@@ -1035,15 +1069,15 @@ export class ObjectShape extends Shape<globalThis.Record<string, unknown>> {
1035
1069
  }
1036
1070
 
1037
1071
  transform(
1038
- schema: (utils: { $: Transform; merge: typeof MERGE }) => {
1039
- [P in string | typeof MERGE]?: Transform
1072
+ schema: (utils: { $: ShapeTransform; merge: typeof MERGE }) => {
1073
+ [P in string | typeof MERGE]?: ShapeTransform
1040
1074
  },
1041
1075
  resolve = true
1042
1076
  ) {
1043
1077
  const properties = {}
1044
1078
  const defaults = {}
1045
1079
 
1046
- const definedSchema = schema({ merge: MERGE, $: new Transform(this, resolve) })
1080
+ const definedSchema = schema({ merge: MERGE, $: new ShapeTransform(this, resolve) })
1047
1081
  for (const to of [...Object.keys(definedSchema), MERGE] as const) {
1048
1082
  if (definedSchema[to]) {
1049
1083
  const result = definedSchema[to]._(to)
@@ -1099,7 +1133,7 @@ export class ObjectShape extends Shape<globalThis.Record<string, unknown>> {
1099
1133
  } else if (a[key]?.equals(value)) {
1100
1134
  // Don't overwrite properties that are equal
1101
1135
  } else if (a[key]?.isObject() && value.isObject()) {
1102
- a[key].merge(value) // Merge objects recursively
1136
+ a[key] = a[key].merge(value) // Merge objects recursively
1103
1137
  } else {
1104
1138
  a[key] = value
1105
1139
  }