@servicenow/sdk-build-core 3.0.3 → 4.0.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 (252) hide show
  1. package/dist/app.d.ts +25 -0
  2. package/dist/app.js +8 -0
  3. package/dist/app.js.map +1 -0
  4. package/dist/compiler.d.ts +60 -0
  5. package/dist/compiler.js +320 -0
  6. package/dist/compiler.js.map +1 -0
  7. package/dist/compression.d.ts +7 -0
  8. package/dist/compression.js +79 -0
  9. package/dist/compression.js.map +1 -0
  10. package/dist/crypto.d.ts +1 -0
  11. package/dist/crypto.js +9 -0
  12. package/dist/crypto.js.map +1 -0
  13. package/dist/diagnostic.d.ts +41 -0
  14. package/dist/diagnostic.js +130 -0
  15. package/dist/diagnostic.js.map +1 -0
  16. package/dist/{plugins/Diagnostic.d.ts → fluent-diagnostic.d.ts} +3 -2
  17. package/dist/fluent-diagnostic.js +23 -0
  18. package/dist/fluent-diagnostic.js.map +1 -0
  19. package/dist/fluent-directive.d.ts +8 -0
  20. package/dist/fluent-directive.js +54 -0
  21. package/dist/fluent-directive.js.map +1 -0
  22. package/dist/fluent-file.d.ts +5 -0
  23. package/dist/fluent-file.js +15 -0
  24. package/dist/fluent-file.js.map +1 -0
  25. package/dist/formatter.d.ts +11 -0
  26. package/dist/formatter.js +77 -0
  27. package/dist/formatter.js.map +1 -0
  28. package/dist/fs.d.ts +174 -0
  29. package/dist/fs.js +313 -0
  30. package/dist/fs.js.map +1 -0
  31. package/dist/guid.d.ts +2 -0
  32. package/dist/{GUID.js → guid.js} +3 -6
  33. package/dist/guid.js.map +1 -0
  34. package/dist/index.d.ts +19 -5
  35. package/dist/index.js +19 -5
  36. package/dist/index.js.map +1 -1
  37. package/dist/json.d.ts +5 -0
  38. package/dist/json.js +43 -0
  39. package/dist/json.js.map +1 -0
  40. package/dist/keys-registry.d.ts +64 -0
  41. package/dist/keys-registry.js +339 -0
  42. package/dist/keys-registry.js.map +1 -0
  43. package/dist/logger.d.ts +8 -0
  44. package/dist/logger.js +17 -0
  45. package/dist/logger.js.map +1 -0
  46. package/dist/now-config.d.ts +348 -0
  47. package/dist/now-config.js +283 -0
  48. package/dist/now-config.js.map +1 -0
  49. package/dist/path.d.ts +3 -0
  50. package/dist/path.js +12 -0
  51. package/dist/path.js.map +1 -0
  52. package/dist/plugins/cache.d.ts +20 -0
  53. package/dist/plugins/cache.js +46 -0
  54. package/dist/plugins/cache.js.map +1 -0
  55. package/dist/plugins/context.d.ts +85 -0
  56. package/dist/plugins/{Context.js → context.js} +1 -1
  57. package/dist/plugins/context.js.map +1 -0
  58. package/dist/plugins/database.d.ts +27 -0
  59. package/dist/plugins/database.js +102 -0
  60. package/dist/plugins/database.js.map +1 -0
  61. package/dist/plugins/file.d.ts +10 -0
  62. package/dist/plugins/{behaviors/Arranger.js → file.js} +1 -1
  63. package/dist/plugins/file.js.map +1 -0
  64. package/dist/plugins/index.d.ts +9 -5
  65. package/dist/plugins/index.js +9 -6
  66. package/dist/plugins/index.js.map +1 -1
  67. package/dist/plugins/plugin.d.ts +478 -0
  68. package/dist/plugins/plugin.js +533 -0
  69. package/dist/plugins/plugin.js.map +1 -0
  70. package/dist/plugins/product.d.ts +15 -0
  71. package/dist/plugins/product.js +38 -0
  72. package/dist/plugins/product.js.map +1 -0
  73. package/dist/plugins/project.d.ts +25 -0
  74. package/dist/plugins/{behaviors/Generator.js → project.js} +1 -1
  75. package/dist/plugins/project.js.map +1 -0
  76. package/dist/plugins/shape.d.ts +424 -0
  77. package/dist/plugins/shape.js +1181 -0
  78. package/dist/plugins/shape.js.map +1 -0
  79. package/dist/plugins/time.d.ts +12 -0
  80. package/dist/plugins/time.js +84 -0
  81. package/dist/plugins/time.js.map +1 -0
  82. package/dist/plugins/usage.d.ts +11 -0
  83. package/dist/plugins/usage.js +26 -0
  84. package/dist/plugins/usage.js.map +1 -0
  85. package/dist/prettier/config-loader.d.ts +13 -0
  86. package/dist/prettier/config-loader.js +105 -0
  87. package/dist/prettier/config-loader.js.map +1 -0
  88. package/dist/telemetry/index.d.ts +25 -0
  89. package/dist/telemetry/index.js +18 -0
  90. package/dist/telemetry/index.js.map +1 -0
  91. package/dist/typescript.d.ts +293 -0
  92. package/dist/typescript.js +454 -0
  93. package/dist/typescript.js.map +1 -0
  94. package/dist/util/get-file-type.d.ts +2 -0
  95. package/dist/util/get-file-type.js +13 -0
  96. package/dist/util/get-file-type.js.map +1 -0
  97. package/dist/util/index.d.ts +2 -6
  98. package/dist/util/index.js +2 -6
  99. package/dist/util/index.js.map +1 -1
  100. package/dist/util/{Scope.js → is-sn-scope.js} +1 -1
  101. package/dist/util/is-sn-scope.js.map +1 -0
  102. package/dist/xml.d.ts +24 -0
  103. package/dist/xml.js +71 -0
  104. package/dist/xml.js.map +1 -0
  105. package/now.config.schema.json +336 -0
  106. package/package.json +22 -12
  107. package/src/app.ts +33 -0
  108. package/src/compiler.ts +384 -0
  109. package/src/compression.ts +93 -0
  110. package/src/crypto.ts +5 -0
  111. package/src/diagnostic.ts +108 -0
  112. package/src/{plugins/Diagnostic.ts → fluent-diagnostic.ts} +3 -10
  113. package/src/fluent-directive.ts +63 -0
  114. package/src/fluent-file.ts +13 -0
  115. package/src/formatter.ts +58 -0
  116. package/src/fs.ts +438 -0
  117. package/src/{GUID.ts → guid.ts} +2 -6
  118. package/src/index.ts +19 -5
  119. package/src/json.ts +20 -0
  120. package/src/keys-registry.ts +384 -0
  121. package/src/logger.ts +20 -0
  122. package/src/now-config.ts +337 -0
  123. package/src/path.ts +9 -0
  124. package/src/plugins/cache.ts +45 -0
  125. package/src/plugins/context.ts +93 -0
  126. package/src/plugins/database.ts +121 -0
  127. package/src/plugins/file.ts +19 -0
  128. package/src/plugins/index.ts +9 -5
  129. package/src/plugins/plugin.ts +995 -0
  130. package/src/plugins/product.ts +44 -0
  131. package/src/plugins/project.ts +39 -0
  132. package/src/plugins/shape.ts +1532 -0
  133. package/src/plugins/time.ts +108 -0
  134. package/src/plugins/usage.ts +26 -0
  135. package/src/prettier/config-loader.ts +130 -0
  136. package/src/telemetry/index.ts +27 -0
  137. package/src/typescript.ts +502 -0
  138. package/src/util/get-file-type.ts +11 -0
  139. package/src/util/index.ts +2 -6
  140. package/src/xml.ts +86 -0
  141. package/dist/GUID.d.ts +0 -2
  142. package/dist/GUID.js.map +0 -1
  143. package/dist/IncludePaths.d.ts +0 -25
  144. package/dist/IncludePaths.js +0 -97
  145. package/dist/IncludePaths.js.map +0 -1
  146. package/dist/Keys.d.ts +0 -32
  147. package/dist/Keys.js +0 -245
  148. package/dist/Keys.js.map +0 -1
  149. package/dist/TypeScript.d.ts +0 -5
  150. package/dist/TypeScript.js +0 -58
  151. package/dist/TypeScript.js.map +0 -1
  152. package/dist/XML.d.ts +0 -32
  153. package/dist/XML.js +0 -83
  154. package/dist/XML.js.map +0 -1
  155. package/dist/plugins/Context.d.ts +0 -190
  156. package/dist/plugins/Context.js.map +0 -1
  157. package/dist/plugins/Diagnostic.js +0 -28
  158. package/dist/plugins/Diagnostic.js.map +0 -1
  159. package/dist/plugins/Plugin.d.ts +0 -175
  160. package/dist/plugins/Plugin.js +0 -15
  161. package/dist/plugins/Plugin.js.map +0 -1
  162. package/dist/plugins/behaviors/Arranger.d.ts +0 -26
  163. package/dist/plugins/behaviors/Arranger.js.map +0 -1
  164. package/dist/plugins/behaviors/Composer.d.ts +0 -102
  165. package/dist/plugins/behaviors/Composer.js +0 -15
  166. package/dist/plugins/behaviors/Composer.js.map +0 -1
  167. package/dist/plugins/behaviors/Diagnostics.d.ts +0 -7
  168. package/dist/plugins/behaviors/Diagnostics.js +0 -3
  169. package/dist/plugins/behaviors/Diagnostics.js.map +0 -1
  170. package/dist/plugins/behaviors/Generator.d.ts +0 -21
  171. package/dist/plugins/behaviors/Generator.js.map +0 -1
  172. package/dist/plugins/behaviors/OwnedTables.d.ts +0 -6
  173. package/dist/plugins/behaviors/OwnedTables.js +0 -3
  174. package/dist/plugins/behaviors/OwnedTables.js.map +0 -1
  175. package/dist/plugins/behaviors/PostProcessor.d.ts +0 -5
  176. package/dist/plugins/behaviors/PostProcessor.js +0 -3
  177. package/dist/plugins/behaviors/PostProcessor.js.map +0 -1
  178. package/dist/plugins/behaviors/Serializer.d.ts +0 -30
  179. package/dist/plugins/behaviors/Serializer.js +0 -3
  180. package/dist/plugins/behaviors/Serializer.js.map +0 -1
  181. package/dist/plugins/behaviors/Transformer.d.ts +0 -23
  182. package/dist/plugins/behaviors/Transformer.js +0 -3
  183. package/dist/plugins/behaviors/Transformer.js.map +0 -1
  184. package/dist/plugins/behaviors/extractors/Data.d.ts +0 -119
  185. package/dist/plugins/behaviors/extractors/Data.js +0 -244
  186. package/dist/plugins/behaviors/extractors/Data.js.map +0 -1
  187. package/dist/plugins/behaviors/extractors/Extractors.d.ts +0 -63
  188. package/dist/plugins/behaviors/extractors/Extractors.js +0 -3
  189. package/dist/plugins/behaviors/extractors/Extractors.js.map +0 -1
  190. package/dist/plugins/behaviors/extractors/index.d.ts +0 -2
  191. package/dist/plugins/behaviors/extractors/index.js +0 -19
  192. package/dist/plugins/behaviors/extractors/index.js.map +0 -1
  193. package/dist/plugins/behaviors/index.d.ts +0 -9
  194. package/dist/plugins/behaviors/index.js +0 -26
  195. package/dist/plugins/behaviors/index.js.map +0 -1
  196. package/dist/plugins/util/CallExpression.d.ts +0 -5
  197. package/dist/plugins/util/CallExpression.js +0 -88
  198. package/dist/plugins/util/CallExpression.js.map +0 -1
  199. package/dist/plugins/util/CodeTransformation.d.ts +0 -95
  200. package/dist/plugins/util/CodeTransformation.js +0 -624
  201. package/dist/plugins/util/CodeTransformation.js.map +0 -1
  202. package/dist/plugins/util/ObjectLiteral.d.ts +0 -9
  203. package/dist/plugins/util/ObjectLiteral.js +0 -37
  204. package/dist/plugins/util/ObjectLiteral.js.map +0 -1
  205. package/dist/plugins/util/index.d.ts +0 -3
  206. package/dist/plugins/util/index.js +0 -20
  207. package/dist/plugins/util/index.js.map +0 -1
  208. package/dist/util/Debug.d.ts +0 -4
  209. package/dist/util/Debug.js +0 -20
  210. package/dist/util/Debug.js.map +0 -1
  211. package/dist/util/Directive.d.ts +0 -16
  212. package/dist/util/Directive.js +0 -107
  213. package/dist/util/Directive.js.map +0 -1
  214. package/dist/util/RuntimeTableSchema.d.ts +0 -5
  215. package/dist/util/RuntimeTableSchema.js +0 -58
  216. package/dist/util/RuntimeTableSchema.js.map +0 -1
  217. package/dist/util/Scope.js.map +0 -1
  218. package/dist/util/Util.d.ts +0 -1
  219. package/dist/util/Util.js +0 -12
  220. package/dist/util/Util.js.map +0 -1
  221. package/dist/util/XMLUploadParser.d.ts +0 -22
  222. package/dist/util/XMLUploadParser.js +0 -67
  223. package/dist/util/XMLUploadParser.js.map +0 -1
  224. package/src/IncludePaths.ts +0 -122
  225. package/src/Keys.ts +0 -274
  226. package/src/TypeScript.ts +0 -65
  227. package/src/XML.ts +0 -98
  228. package/src/plugins/Context.ts +0 -239
  229. package/src/plugins/Plugin.ts +0 -278
  230. package/src/plugins/behaviors/Arranger.ts +0 -42
  231. package/src/plugins/behaviors/Composer.ts +0 -125
  232. package/src/plugins/behaviors/Diagnostics.ts +0 -12
  233. package/src/plugins/behaviors/Generator.ts +0 -31
  234. package/src/plugins/behaviors/OwnedTables.ts +0 -5
  235. package/src/plugins/behaviors/PostProcessor.ts +0 -6
  236. package/src/plugins/behaviors/Serializer.ts +0 -40
  237. package/src/plugins/behaviors/Transformer.ts +0 -32
  238. package/src/plugins/behaviors/extractors/Data.ts +0 -332
  239. package/src/plugins/behaviors/extractors/Extractors.ts +0 -73
  240. package/src/plugins/behaviors/extractors/index.ts +0 -2
  241. package/src/plugins/behaviors/index.ts +0 -9
  242. package/src/plugins/util/CallExpression.ts +0 -110
  243. package/src/plugins/util/CodeTransformation.ts +0 -731
  244. package/src/plugins/util/ObjectLiteral.ts +0 -37
  245. package/src/plugins/util/index.ts +0 -3
  246. package/src/util/Debug.ts +0 -24
  247. package/src/util/Directive.ts +0 -123
  248. package/src/util/RuntimeTableSchema.ts +0 -44
  249. package/src/util/Util.ts +0 -7
  250. package/src/util/XMLUploadParser.ts +0 -90
  251. /package/dist/util/{Scope.d.ts → is-sn-scope.d.ts} +0 -0
  252. /package/src/util/{Scope.ts → is-sn-scope.ts} +0 -0
@@ -0,0 +1,1532 @@
1
+ import { type Source, Product } from './product'
2
+ import { path } from '../path'
3
+ import { ts } from '../typescript'
4
+ import type { Plugin } from './plugin'
5
+ import type { InstallCategory } from './file'
6
+ import { JSON5 } from '../json'
7
+ import * as _ from 'lodash'
8
+
9
+ // biome-ignore lint/suspicious/noExplicitAny: TypeScript is annoying about function arguments. There's not any better options here.
10
+ export type ShapeClass<P extends Shape = Shape> = (abstract new (...args: any[]) => P) & { prototype: P }
11
+
12
+ export abstract class Shape<T = unknown> extends Product {
13
+ constructor({ source }: { source: Source }) {
14
+ super(source)
15
+ }
16
+
17
+ /**
18
+ * Instantiate an appropriate shape from the given value. If the value is already
19
+ * a shape, it will be returned as-is.
20
+ */
21
+ static from(source: Source, value: unknown): Shape {
22
+ if (value instanceof Shape) {
23
+ return value
24
+ } else if (typeof value === 'string') {
25
+ return new StringLiteralShape({ source, literalText: value })
26
+ } else if (typeof value === 'number') {
27
+ return new NumberShape({ source, value })
28
+ } else if (typeof value === 'boolean') {
29
+ return new BooleanShape({ source, value })
30
+ } else if (value === undefined || value === null) {
31
+ return new UndefinedShape({ source })
32
+ } else if (Array.isArray(value)) {
33
+ return new ArrayShape({ source, elements: value })
34
+ } else if (typeof value === 'object') {
35
+ return new ObjectShape({ source, properties: value })
36
+ } else {
37
+ throw new Error(
38
+ `Tried to create shape from invalid value (type: ${typeof value}): ${JSON5.stringify(value)}`
39
+ )
40
+ }
41
+ }
42
+
43
+ /**
44
+ * Instantiate a NoOpShape which has no value and will not generate or update
45
+ * any code. Useful for shapes that are not supported for bidirectional sync.
46
+ */
47
+ static noOp(source: Source): NoOpShape {
48
+ return new NoOpShape(source)
49
+ }
50
+
51
+ /**
52
+ * Gets the value of the shape.
53
+ */
54
+ getValue(): T {
55
+ return Symbol(this.getKind()) as T
56
+ }
57
+
58
+ equals(value: unknown): boolean {
59
+ value = value instanceof Shape ? value.getValue() : value
60
+ return value === this.getValue()
61
+ }
62
+
63
+ pipe<T>(pipeFn: (shape: this) => T): T {
64
+ return pipeFn(this)
65
+ }
66
+
67
+ isResolvable(): this is ResolvableShape {
68
+ return this.is(ResolvableShape)
69
+ }
70
+
71
+ isIdentifier(): this is IdentifierShape {
72
+ return this.is(IdentifierShape)
73
+ }
74
+
75
+ isString(): this is StringShape {
76
+ return this.is(StringShape)
77
+ }
78
+
79
+ isStringLiteral(): this is StringLiteralShape {
80
+ return this.is(StringLiteralShape)
81
+ }
82
+
83
+ isNumber(): this is NumberShape {
84
+ return this.is(NumberShape)
85
+ }
86
+
87
+ isBoolean(): this is BooleanShape {
88
+ return this.is(BooleanShape)
89
+ }
90
+
91
+ isNoOp(): this is NoOpShape {
92
+ return this.is(NoOpShape)
93
+ }
94
+
95
+ isUnresolved(): this is UnresolvedShape {
96
+ return this.is(UnresolvedShape)
97
+ }
98
+
99
+ isUndefined(): this is UndefinedShape {
100
+ return this.is(UndefinedShape)
101
+ }
102
+
103
+ isDefined(): this is this {
104
+ return !this.isUndefined() && !this.isUnresolved() && !this.isNoOp()
105
+ }
106
+
107
+ isArray(): this is ArrayShape {
108
+ return this.is(ArrayShape)
109
+ }
110
+
111
+ isObject(): this is ObjectShape {
112
+ return this.is(ObjectShape)
113
+ }
114
+
115
+ isRecord(): this is Record {
116
+ return this.is(Record)
117
+ }
118
+
119
+ isRecordId(): this is RecordId {
120
+ return this.is(RecordId)
121
+ }
122
+
123
+ is<const T extends [Shape, Shape, ...Shape[]]>(
124
+ shapeClasses: { [I in keyof T]: ShapeClass<T[I]> },
125
+ failureMessage?: string
126
+ ): this is T[number]
127
+ is<const S extends Shape>(shapeClass: ShapeClass<S>, failureMessage?: string): this is S
128
+ is(shapeClass: ShapeClass | ShapeClass[]): boolean {
129
+ for (const c of Array.isArray(shapeClass) ? shapeClass : [shapeClass]) {
130
+ if (this instanceof c) {
131
+ return true
132
+ }
133
+ }
134
+
135
+ return false
136
+ }
137
+
138
+ ifResolvable(): ResolvableShape | undefined {
139
+ return this.if(ResolvableShape)
140
+ }
141
+
142
+ ifIdentifier(): IdentifierShape | undefined {
143
+ return this.if(IdentifierShape)
144
+ }
145
+
146
+ ifString(): StringShape | undefined {
147
+ return this.if(StringShape)
148
+ }
149
+
150
+ ifStringLiteral(): StringLiteralShape | undefined {
151
+ return this.if(StringLiteralShape)
152
+ }
153
+
154
+ ifNumber(): NumberShape | undefined {
155
+ return this.if(NumberShape)
156
+ }
157
+
158
+ ifBoolean(): BooleanShape | undefined {
159
+ return this.if(BooleanShape)
160
+ }
161
+
162
+ ifUnresolved(): UnresolvedShape | undefined {
163
+ return this.if(UnresolvedShape)
164
+ }
165
+
166
+ ifUndefined(): UndefinedShape | undefined {
167
+ return this.if(UndefinedShape)
168
+ }
169
+
170
+ ifDefined(): this | undefined {
171
+ return this.isDefined() ? this : undefined
172
+ }
173
+
174
+ ifArray(): ArrayShape | undefined {
175
+ return this.if(ArrayShape)
176
+ }
177
+
178
+ ifObject(): ObjectShape | undefined {
179
+ return this.if(ObjectShape)
180
+ }
181
+
182
+ ifRecord(): Record | undefined {
183
+ return this.if(Record)
184
+ }
185
+
186
+ ifRecordId(): RecordId | undefined {
187
+ return this.if(RecordId)
188
+ }
189
+
190
+ if<const T extends [Shape, Shape, ...Shape[]]>(
191
+ shapeClasses: { [I in keyof T]: ShapeClass<T[I]> },
192
+ failureMessage?: string
193
+ ): T[number] | undefined
194
+ if<const S extends Shape>(shapeClass: ShapeClass<S>, failureMessage?: string): S | undefined
195
+ if(shapeClass: ShapeClass | ShapeClass[]): Shape | undefined {
196
+ return this.is(shapeClass as any) ? this : undefined
197
+ }
198
+
199
+ asResolvable(failureMessage?: string): ResolvableShape {
200
+ return this.as(ResolvableShape, failureMessage)
201
+ }
202
+
203
+ asIdentifier(failureMessage?: string): IdentifierShape {
204
+ return this.as(IdentifierShape, failureMessage)
205
+ }
206
+
207
+ asString(failureMessage?: string): StringShape {
208
+ return this.as(StringShape, failureMessage)
209
+ }
210
+
211
+ asStringLiteral(failureMessage?: string): StringLiteralShape {
212
+ return this.as(StringLiteralShape, failureMessage)
213
+ }
214
+
215
+ asNumber(failureMessage?: string): NumberShape {
216
+ return this.as(NumberShape, failureMessage)
217
+ }
218
+
219
+ asBoolean(failureMessage?: string): BooleanShape {
220
+ return this.as(BooleanShape, failureMessage)
221
+ }
222
+
223
+ asUnresolved(failureMessage?: string): UnresolvedShape {
224
+ return this.as(UnresolvedShape, failureMessage)
225
+ }
226
+
227
+ asUndefined(failureMessage?: string): UndefinedShape {
228
+ return this.as(UndefinedShape, failureMessage)
229
+ }
230
+
231
+ asArray(failureMessage?: string): ArrayShape {
232
+ return this.as(ArrayShape, failureMessage)
233
+ }
234
+
235
+ asObject(failureMessage?: string): ObjectShape {
236
+ return this.as(ObjectShape, failureMessage)
237
+ }
238
+
239
+ asRecord(failureMessage?: string): Record {
240
+ return this.as(Record, failureMessage)
241
+ }
242
+
243
+ asRecordId(failureMessage?: string): RecordId {
244
+ return this.as(RecordId, failureMessage)
245
+ }
246
+
247
+ as<const T extends [Shape, Shape, ...Shape[]]>(
248
+ shapeClasses: { [I in keyof T]: ShapeClass<T[I]> },
249
+ failureMessage?: string
250
+ ): T[number]
251
+ as<const S extends Shape>(shapeClass: ShapeClass<S>, failureMessage?: string): S
252
+ as(shapeClass: ShapeClass | ShapeClass[], failureMessage?: string): Shape {
253
+ for (const c of Array.isArray(shapeClass) ? shapeClass : [shapeClass]) {
254
+ if (this.is(c)) {
255
+ return this
256
+ }
257
+ }
258
+
259
+ throw new Error(
260
+ failureMessage ??
261
+ `Failed to cast ${this.getKind()} to ${Array.isArray(shapeClass) ? `any of the requested shapes: ${shapeClass.map((c) => c.name).join(', ')}` : shapeClass.name}`
262
+ )
263
+ }
264
+
265
+ override toString(): StringShape {
266
+ if (this.isString()) {
267
+ return this
268
+ }
269
+
270
+ return Shape.from(this, String(this.getValue())).asString()
271
+ }
272
+
273
+ toNumber(): NumberShape {
274
+ if (this.isNumber()) {
275
+ return this
276
+ }
277
+
278
+ const value = this.getValue()
279
+ const coerced = Number(value)
280
+
281
+ //check for empty string since Number('') returns 0
282
+ if (isNaN(coerced) || (typeof value === 'string' && !value.trim())) {
283
+ throw new Error(`Cannot coerce value to number: ${value}`)
284
+ }
285
+
286
+ return new NumberShape({ source: this.getSource(), value: coerced })
287
+ }
288
+
289
+ toBoolean(): BooleanShape {
290
+ if (this.isBoolean()) {
291
+ return this
292
+ }
293
+
294
+ const value = this.getValue()
295
+ if (typeof value !== 'boolean') {
296
+ throw new Error(`Cannot coerce value to boolean: ${value}`)
297
+ }
298
+
299
+ return new BooleanShape({ source: this, value })
300
+ }
301
+
302
+ toRecordId(): RecordId {
303
+ if (this.isRecordId()) {
304
+ return this
305
+ }
306
+
307
+ throw new Error(`Coercion from ${this.getKind()} to record ID is not supported`)
308
+ }
309
+
310
+ getKind(): string {
311
+ return this.constructor.name
312
+ }
313
+
314
+ getDescription(): string {
315
+ return this.getKind()
316
+ }
317
+
318
+ getCode(): string {
319
+ const value = this.getValue()
320
+ if (typeof value === 'symbol') {
321
+ throw new Error(
322
+ `Shape's value is a symbol which is not supported by the default code generation behavior: ${this.getDescription()}`
323
+ )
324
+ }
325
+
326
+ return JSON5.stringify(this.getValue(), { space: 1, quote: "'" })
327
+ }
328
+ }
329
+
330
+ export class NoOpShape extends Shape<never> {
331
+ constructor(source: Source) {
332
+ super({ source })
333
+ }
334
+
335
+ override getValue(): never {
336
+ throw new Error('Cannot get a value from a no-op shape')
337
+ }
338
+ }
339
+
340
+ export class UnresolvedShape extends Shape<symbol> {
341
+ public static readonly CODE = 'Now.UNRESOLVED'
342
+ public static readonly VALUE: unique symbol = Symbol(UnresolvedShape.CODE)
343
+
344
+ override getValue() {
345
+ return UnresolvedShape.VALUE
346
+ }
347
+
348
+ override getCode(): string {
349
+ return UnresolvedShape.CODE
350
+ }
351
+ }
352
+
353
+ export class UndefinedShape extends Shape<undefined> {
354
+ constructor({ source }: { source: Source }) {
355
+ super({ source })
356
+ }
357
+
358
+ override getValue(): undefined {
359
+ return undefined
360
+ }
361
+
362
+ override equals(other: unknown): boolean {
363
+ return Shape.from(this, other).isUndefined()
364
+ }
365
+
366
+ override toString(): StringShape {
367
+ return Shape.from(this, '').asString()
368
+ }
369
+ }
370
+
371
+ export class DeletedShape extends UndefinedShape {}
372
+
373
+ export abstract class ResolvableShape extends Shape {
374
+ abstract resolve(deep?: boolean): Shape
375
+
376
+ override equals(other: unknown): boolean {
377
+ return this.resolve(true).equals(other)
378
+ }
379
+
380
+ override getValue(): unknown {
381
+ return this.resolve(true).getValue()
382
+ }
383
+
384
+ override toString(): StringShape {
385
+ return this.resolve(true).toString()
386
+ }
387
+
388
+ override toNumber(): NumberShape {
389
+ return this.resolve(true).toNumber()
390
+ }
391
+
392
+ override toBoolean(): BooleanShape {
393
+ return this.resolve(true).toBoolean()
394
+ }
395
+
396
+ override toRecordId(): RecordId {
397
+ return this.resolve(true).toRecordId()
398
+ }
399
+ }
400
+
401
+ export class IdentifierShape extends ResolvableShape {
402
+ private readonly name: string
403
+ private readonly value: Shape
404
+
405
+ constructor({
406
+ source,
407
+ name,
408
+ value = new UnresolvedShape({ source }),
409
+ }: {
410
+ source: Source
411
+ name: string
412
+ value?: unknown
413
+ }) {
414
+ super({ source })
415
+ this.name = name
416
+ this.value = Shape.from(source, value)
417
+ }
418
+
419
+ getName(): string {
420
+ return this.name
421
+ }
422
+
423
+ override resolve(deep = true): Shape {
424
+ return deep && this.value.isResolvable() ? this.value.resolve(true) : this.value
425
+ }
426
+
427
+ override getCode(): string {
428
+ return this.getName()
429
+ }
430
+
431
+ override equals(other: unknown): boolean {
432
+ return other instanceof IdentifierShape ? this.name === other.name && super.equals(other) : super.equals(other)
433
+ }
434
+ }
435
+
436
+ type PropertyAccessElements = readonly [IdentifierShape, IdentifierShape, ...IdentifierShape[]]
437
+
438
+ export class PropertyAccessShape extends ResolvableShape {
439
+ private readonly elements: PropertyAccessElements
440
+
441
+ constructor({ source, elements }: { source: Source; elements: readonly [unknown, unknown, ...unknown[]] }) {
442
+ super({ source })
443
+ this.elements = elements.map((e) =>
444
+ typeof e === 'string' ? new IdentifierShape({ source, name: e }) : Shape.from(source, e).asIdentifier()
445
+ ) as unknown as PropertyAccessElements // Annoying cast needed because `map()` doesn't respect tuples
446
+ }
447
+
448
+ getElements(): PropertyAccessElements {
449
+ return this.elements
450
+ }
451
+
452
+ getElement(index: number): IdentifierShape {
453
+ const element = this.elements[index]
454
+ if (!element) {
455
+ throw new Error(`Property access shape does not have an element at index ${index}: ${this.getCode()}`)
456
+ }
457
+
458
+ return element
459
+ }
460
+
461
+ getFirstElement(): IdentifierShape {
462
+ return this.getElement(0)
463
+ }
464
+
465
+ getLastElement(): IdentifierShape {
466
+ return this.getElement(this.elements.length - 1)
467
+ }
468
+
469
+ override resolve(deep = true) {
470
+ return this.getLastElement().resolve(deep)
471
+ }
472
+
473
+ override getCode(): string {
474
+ return this.elements.map((e) => e.getCode()).join('.')
475
+ }
476
+ }
477
+
478
+ type StringContentType = 'plain' | 'cdata'
479
+
480
+ export abstract class StringShape extends Shape<string> {
481
+ private contentType: StringContentType = 'plain'
482
+
483
+ startsWith(substring: string): boolean {
484
+ return this.getValue().startsWith(substring)
485
+ }
486
+
487
+ endsWith(substring: string): boolean {
488
+ return this.getValue().endsWith(substring)
489
+ }
490
+
491
+ includes(substring: string): boolean {
492
+ return this.getValue().includes(substring)
493
+ }
494
+
495
+ split(separator: string): string[] {
496
+ return this.getValue().split(separator)
497
+ }
498
+
499
+ isEmpty(): boolean {
500
+ return this.getValue().length === 0
501
+ }
502
+
503
+ ifNotEmpty(): StringShape | undefined {
504
+ return this.isEmpty() ? undefined : this
505
+ }
506
+
507
+ replace(regex: string | RegExp, replacement: string): StringShape {
508
+ return Shape.from(this, this.getValue().replace(regex, replacement)).asString()
509
+ }
510
+
511
+ replaceAll(regex: string | RegExp, replacement: string): StringShape {
512
+ return Shape.from(this, this.getValue().replaceAll(regex, replacement)).asString()
513
+ }
514
+
515
+ parseJson(): ObjectShape | ArrayShape {
516
+ const value = this.getValue()
517
+ const result = JSON5.parse<unknown>(value)
518
+ if (!result || (!Array.isArray(result) && typeof result !== 'object')) {
519
+ throw new Error(`Failed to parse string shape as JSON: ${value}`)
520
+ }
521
+
522
+ return Shape.from(this, result).as([
523
+ // I have absolutely no idea why these casts are needed in this file and
524
+ // nowhere else. Seems like a probable TypeScript bug.
525
+ ObjectShape as ShapeClass<ObjectShape>,
526
+ ArrayShape as ShapeClass<ArrayShape>,
527
+ ])
528
+ }
529
+
530
+ withContentType(contentType: StringContentType): this {
531
+ this.contentType = contentType
532
+ return this
533
+ }
534
+
535
+ getContentType(): StringContentType {
536
+ return this.contentType
537
+ }
538
+
539
+ override toBoolean(): BooleanShape {
540
+ const value = this.getValue()
541
+ const trimmed = value.trim().toLowerCase()
542
+
543
+ if (trimmed === '') {
544
+ return new BooleanShape({ source: this, value: false })
545
+ }
546
+
547
+ if (!/^(true|false)$/.test(trimmed)) {
548
+ throw new Error(`Cannot coerce string "${value}" to boolean`)
549
+ }
550
+
551
+ return new BooleanShape({ source: this, value: trimmed === 'true' })
552
+ }
553
+
554
+ static escapeSingleQuotes(str: string): string {
555
+ return str.replaceAll("'", "\\'")
556
+ }
557
+
558
+ static escapeBackticks(str: string): string {
559
+ return str.replace(/[`\\]|\${/g, (char) => {
560
+ switch (char) {
561
+ case '`':
562
+ return '\\`'
563
+ case '\\':
564
+ return '\\' + char
565
+ case '${':
566
+ return '\\${'
567
+ default:
568
+ return char
569
+ }
570
+ })
571
+ }
572
+
573
+ static escapeCdataTags(str: string): string {
574
+ return str.replace(/]]>/g, ']]]]><![CDATA[>')
575
+ }
576
+ }
577
+
578
+ export class StringLiteralShape extends StringShape {
579
+ private readonly literalText: string
580
+
581
+ constructor({ source, literalText }: { source: Source; literalText: string | Shape }) {
582
+ super({ source })
583
+ this.literalText = typeof literalText === 'string' ? literalText : literalText.toString().getValue()
584
+ }
585
+
586
+ override getValue(): string {
587
+ return this.literalText
588
+ }
589
+
590
+ override getCode(): string {
591
+ return this.includes('\n')
592
+ ? `\`${StringShape.escapeBackticks(this.literalText)}\``
593
+ : `'${StringShape.escapeSingleQuotes(this.literalText)}'`
594
+ }
595
+ }
596
+
597
+ type TemplateSubstitutionValue = Shape | string | boolean | number
598
+ export type TemplateSubstitutions =
599
+ | ((expression: Shape) => TemplateSubstitutionValue)
600
+ | globalThis.Record<number, ((expression: Shape) => TemplateSubstitutionValue) | TemplateSubstitutionValue>
601
+
602
+ export class TemplateSpanShape extends StringShape {
603
+ private readonly literalText: string
604
+ private readonly expression: Shape
605
+
606
+ constructor({
607
+ source,
608
+ literalText,
609
+ expression,
610
+ }: {
611
+ source: Source
612
+ literalText: string | Shape
613
+ expression: Shape
614
+ }) {
615
+ super({ source })
616
+ this.literalText = Shape.from(source, literalText).asString().getValue()
617
+ this.expression = expression
618
+ }
619
+
620
+ getLiteralText(): string {
621
+ return this.literalText
622
+ }
623
+
624
+ getExpression(): Shape {
625
+ return this.expression
626
+ }
627
+
628
+ override getValue(
629
+ substitution?: ((expression: Shape) => TemplateSubstitutionValue) | TemplateSubstitutionValue
630
+ ): string {
631
+ return `${substitution instanceof Shape ? substitution.getValue() : typeof substitution === 'function' ? substitution(this.expression) : (substitution ?? this.expression.toString().getValue())}${this.literalText}`
632
+ }
633
+
634
+ override getCode(): string {
635
+ return `$\{${this.expression.getCode()}}${StringShape.escapeBackticks(this.literalText)}`
636
+ }
637
+ }
638
+
639
+ export class TemplateExpressionShape extends StringShape {
640
+ private readonly literalText: string
641
+ private readonly spans: TemplateSpanShape[]
642
+
643
+ constructor({
644
+ source,
645
+ literalText,
646
+ spans,
647
+ }: {
648
+ source: Source
649
+ literalText: string | Shape
650
+ spans?: TemplateSpanShape[]
651
+ }) {
652
+ super({ source })
653
+ this.literalText = Shape.from(source, literalText).asString().getValue()
654
+ this.spans = spans ?? []
655
+ }
656
+
657
+ getLiteralText(): string {
658
+ return this.literalText
659
+ }
660
+
661
+ hasSubstitution(): boolean {
662
+ return this.spans.length > 0
663
+ }
664
+
665
+ getSpans(): TemplateSpanShape[] {
666
+ return this.spans
667
+ }
668
+
669
+ override getValue(substitutions?: TemplateSubstitutions): string {
670
+ return `${this.literalText}${this.spans.map((s, i) => s.getValue(typeof substitutions === 'function' ? substitutions : substitutions?.[i])).join('')}`
671
+ }
672
+
673
+ override getCode(): string {
674
+ return `\`${StringShape.escapeBackticks(this.literalText)}${this.spans.map((s) => s.getCode()).join('')}\``
675
+ }
676
+ }
677
+
678
+ export class TaggedTemplateExpressionShape extends StringShape {
679
+ private readonly tag: string
680
+ private readonly template: TemplateExpressionShape
681
+
682
+ constructor({ source, tag, template }: { source: Source; tag: string | Shape; template: Shape }) {
683
+ super({ source })
684
+ this.tag = Shape.from(source, tag).asString().getValue()
685
+ this.template = template.as(TemplateExpressionShape, 'Template must be a template expression shape')
686
+ }
687
+
688
+ getTag(): string {
689
+ return this.tag
690
+ }
691
+
692
+ getTemplate(): TemplateExpressionShape {
693
+ return this.template
694
+ }
695
+
696
+ override getValue(substitutions?: TemplateSubstitutions): string {
697
+ return this.template.getValue(substitutions)
698
+ }
699
+
700
+ override getCode(): string {
701
+ return `${this.tag || ''}${this.template.getCode()}`
702
+ }
703
+ }
704
+
705
+ export class NumberShape extends Shape<number> {
706
+ private readonly value: number
707
+
708
+ constructor({ source, value }: { source: Source; value: number }) {
709
+ super({ source })
710
+ this.value = value
711
+ }
712
+
713
+ override getValue(): number {
714
+ return this.value
715
+ }
716
+ }
717
+
718
+ export class BooleanShape extends Shape<boolean> {
719
+ private readonly value: boolean
720
+
721
+ constructor({ source, value }: { source: Source; value: boolean }) {
722
+ super({ source })
723
+ this.value = value
724
+ }
725
+
726
+ override getValue(): boolean {
727
+ return this.value
728
+ }
729
+ }
730
+
731
+ export class ArrayShape extends Shape<unknown[]> {
732
+ private readonly elements: Shape[]
733
+
734
+ constructor({ source, elements }: { source: Source; elements: unknown[] }) {
735
+ super({ source })
736
+ this.elements = elements.map((e) => Shape.from(source, e))
737
+ }
738
+
739
+ override getValue(): unknown[] {
740
+ return this.elements.map((e) => e.getValue())
741
+ }
742
+
743
+ getElements(resolve = true): Shape[] {
744
+ return resolve ? this.elements.map((s) => (s.isResolvable() ? s.resolve(true) : s)) : this.elements
745
+ }
746
+
747
+ getElement(index: number, resolve = true): Shape {
748
+ return this.getElements(resolve)[index] ?? new UndefinedShape({ source: this })
749
+ }
750
+
751
+ hasElement(index: number): boolean {
752
+ return index < this.elements.length
753
+ }
754
+
755
+ map<T>(mapFunction: (element: Shape) => T, resolve = true): T[] {
756
+ return this.getElements(resolve).map(mapFunction)
757
+ }
758
+
759
+ override equals(other: unknown): boolean {
760
+ const otherShape = Shape.from(this, other)
761
+ if (!otherShape.isArray() || this.elements.length !== otherShape.elements.length) {
762
+ return false
763
+ }
764
+
765
+ for (const [i, element] of this.elements.entries()) {
766
+ if (!element.equals(otherShape.getElement(i))) {
767
+ return false
768
+ }
769
+ }
770
+
771
+ return true
772
+ }
773
+
774
+ static override from(source: Source, value: unknown): ArrayShape {
775
+ if (value instanceof ArrayShape) {
776
+ return value
777
+ }
778
+
779
+ value = value instanceof Shape ? value.getValue() : value
780
+ const coerced = Array.isArray(value) ? value : [value]
781
+ return new ArrayShape({ source, elements: coerced })
782
+ }
783
+
784
+ override getCode(): string {
785
+ return `[${this.map((e) => e.getCode(), false).join(', ')}]`
786
+ }
787
+ }
788
+
789
+ const MERGE = Symbol('MERGE')
790
+ const coerceTo = (coerceToType: 'string' | 'number' | 'boolean' | 'cdata', value: Shape): unknown => {
791
+ switch (coerceToType) {
792
+ case 'string':
793
+ return value.toString()
794
+ case 'number':
795
+ return value.toNumber()
796
+ case 'cdata':
797
+ return value.toString().withContentType('cdata')
798
+ default:
799
+ return value.toBoolean()
800
+ }
801
+ }
802
+
803
+ class Transform {
804
+ constructor(
805
+ private readonly shape: ObjectShape,
806
+ private readonly resolve = true,
807
+ private readonly _from: [string, ...string[]] | undefined = undefined,
808
+ private readonly _map: ((...from: Shape[]) => unknown) | undefined = undefined,
809
+ private readonly _def: unknown = undefined,
810
+ private readonly _coerce: 'string' | 'number' | 'boolean' | 'cdata' | undefined = undefined
811
+ ) {}
812
+
813
+ _(to: string | typeof MERGE) {
814
+ const args = (this._from ?? (to === MERGE ? ([] as string[]) : [to])).map((p) =>
815
+ this.shape.get(p, this.resolve)
816
+ )
817
+
818
+ const val = (() => {
819
+ if (this._map) {
820
+ return this._map(...args)
821
+ } else if (this._from) {
822
+ return this._from.length === 1
823
+ ? this.shape.get(this._from[0], this.resolve)
824
+ : this.shape.pick(this._from, { resolve: this.resolve })
825
+ } else if (to !== MERGE) {
826
+ return this.shape.get(to, this.resolve)
827
+ } else {
828
+ return this.shape.properties({ resolve: this.resolve })
829
+ }
830
+ })()
831
+
832
+ if (!this._coerce) {
833
+ return {
834
+ def: this._def,
835
+ val,
836
+ }
837
+ } else {
838
+ const valShape = Shape.from(this.shape, val)
839
+ return {
840
+ def: this._def,
841
+ val: valShape.ifUndefined() ?? coerceTo(this._coerce, valShape),
842
+ }
843
+ }
844
+ }
845
+
846
+ from(...properties: [string, ...string[]]) {
847
+ return new Transform(this.shape, this.resolve, properties, this._map, this._def, this._coerce)
848
+ }
849
+
850
+ map(mapFunction: (...from: Shape[]) => unknown) {
851
+ return new Transform(this.shape, this.resolve, this._from, mapFunction, this._def, this._coerce)
852
+ }
853
+
854
+ val(value: unknown) {
855
+ return new Transform(this.shape, this.resolve, this._from, () => value, this._def, this._coerce)
856
+ }
857
+
858
+ def(value: unknown) {
859
+ return new Transform(this.shape, this.resolve, this._from, this._map, value, this._coerce)
860
+ }
861
+
862
+ toCdata() {
863
+ return new Transform(this.shape, this.resolve, this._from, this._map, this._def, 'cdata')
864
+ }
865
+
866
+ toString() {
867
+ return new Transform(this.shape, this.resolve, this._from, this._map, this._def, 'string')
868
+ }
869
+
870
+ toNumber() {
871
+ return new Transform(this.shape, this.resolve, this._from, this._map, this._def, 'number')
872
+ }
873
+
874
+ toBoolean() {
875
+ return new Transform(this.shape, this.resolve, this._from, this._map, this._def, 'boolean')
876
+ }
877
+ }
878
+
879
+ function assignWithoutOverwriting(target: NonNullable<object>, source: unknown) {
880
+ if (!source) {
881
+ return
882
+ }
883
+
884
+ for (const [k, v] of Object.entries(source)) {
885
+ if (!(k in target)) {
886
+ target[k] = v
887
+ }
888
+ }
889
+ }
890
+
891
+ type ObjectPropertyOptions = {
892
+ nonDefaultsOnly?: boolean
893
+ resolve?: boolean
894
+ }
895
+
896
+ export class ObjectShape extends Shape<globalThis.Record<string, unknown>> {
897
+ protected readonly setProperties: globalThis.Record<string, Shape>
898
+ private defaultProperties: globalThis.Record<string, Shape>
899
+ private aliasedKeys: globalThis.Record<string, string[]>
900
+
901
+ constructor({ source, properties }: { source: Source; properties: NonNullable<object> }) {
902
+ super({ source })
903
+ if (properties instanceof ObjectShape) {
904
+ this.defaultProperties = properties.defaultProperties
905
+ this.setProperties = properties.setProperties
906
+ this.aliasedKeys = properties.aliasedKeys
907
+ } else {
908
+ this.defaultProperties = {}
909
+ this.aliasedKeys = {}
910
+ this.setProperties = Object.fromEntries(
911
+ Object.entries(properties).map(([k, v]) => [k, Shape.from(source, v)])
912
+ )
913
+ }
914
+ }
915
+
916
+ withAliasedKeys(aliasedKeys: globalThis.Record<string, string[]>): this {
917
+ this.aliasedKeys = aliasedKeys
918
+ return this
919
+ }
920
+
921
+ getAliasedKeys(): globalThis.Record<string, string[]> {
922
+ return this.aliasedKeys
923
+ }
924
+
925
+ findAliasUsed(key: string): string | undefined {
926
+ const aliases = this.getAliases(key)
927
+ const properties = this.properties({ resolve: false })
928
+ return aliases.find((alias) => properties[alias])
929
+ }
930
+
931
+ getAliases(key: string): string[] {
932
+ return this.aliasedKeys[key] ?? []
933
+ }
934
+
935
+ properties({
936
+ nonDefaultsOnly = false,
937
+ resolve = true,
938
+ }: ObjectPropertyOptions = {}): globalThis.Record<string, Shape> {
939
+ if (nonDefaultsOnly) {
940
+ const entries = Object.entries(this.setProperties).filter(
941
+ ([k, v]) => v.isDefined() && !v.equals(this.getDefault(k))
942
+ )
943
+
944
+ return Object.fromEntries(
945
+ resolve ? entries.map(([k, v]) => [k, v.isResolvable() ? v.resolve() : v]) : entries
946
+ )
947
+ }
948
+
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
+ ]
957
+
958
+ return Object.fromEntries(resolve ? entries.map(([k, v]) => [k, v.isResolvable() ? v.resolve() : v]) : entries)
959
+ }
960
+
961
+ override getValue(nonDefaultsOnly = false): globalThis.Record<string, unknown> {
962
+ return Object.fromEntries(this.entries({ nonDefaultsOnly }).map(([k, v]) => [k, v.getValue()]))
963
+ }
964
+
965
+ get(propertyOrPath: string | [string, ...string[]], resolve = true): Shape {
966
+ const [property, next, ...rest] = typeof propertyOrPath === 'string' ? [propertyOrPath] : propertyOrPath
967
+ const propertyAliases = [property, ...this.getAliases(property)]
968
+ const properties = this.properties({ resolve })
969
+ const match = propertyAliases.find((p) => properties[p])
970
+ const value = match ? properties[match] : undefined
971
+ return (
972
+ (next ? value?.ifObject()?.get([next, ...rest], resolve) : value) ??
973
+ new UndefinedShape({ source: this.getSource() })
974
+ )
975
+ }
976
+
977
+ getDefault(property: string): Shape {
978
+ return this.defaultProperties[property] ?? new UndefinedShape({ source: this.getSource() })
979
+ }
980
+
981
+ keys(nonDefaultsOnly = false): string[] {
982
+ return Object.keys(this.properties({ nonDefaultsOnly }))
983
+ }
984
+
985
+ values(options: ObjectPropertyOptions = {}): Shape[] {
986
+ return Object.values(this.properties(options))
987
+ }
988
+
989
+ entries(options: ObjectPropertyOptions = {}): [string, Shape][] {
990
+ return Object.entries(this.properties(options))
991
+ }
992
+
993
+ has(property: string): boolean {
994
+ return property in this.setProperties
995
+ }
996
+
997
+ omit(
998
+ properties: string | [string, ...string[]],
999
+ options: ObjectPropertyOptions = {}
1000
+ ): globalThis.Record<string, Shape> {
1001
+ properties = typeof properties === 'string' ? [properties] : properties
1002
+ const regex = new RegExp(`^(${properties.map((p) => p.replaceAll('$', '\\$')).join('|')})$`)
1003
+ return Object.fromEntries(
1004
+ this.entries(options)
1005
+ .filter(([k]) => !regex.test(k))
1006
+ .map(([k, v]) => [k, v])
1007
+ )
1008
+ }
1009
+
1010
+ pick(
1011
+ properties: string | [string, ...string[]],
1012
+ options: ObjectPropertyOptions = {}
1013
+ ): globalThis.Record<string, Shape> {
1014
+ properties = typeof properties === 'string' ? [properties] : properties
1015
+ const regex = new RegExp(`^(${properties.join('|')})$`)
1016
+ return Object.fromEntries(
1017
+ this.entries(options)
1018
+ .filter(([k]) => regex.test(k))
1019
+ .map(([k, v]) => [k, v])
1020
+ )
1021
+ }
1022
+
1023
+ private setDefaults(defaults: globalThis.Record<string, unknown>): this {
1024
+ this.defaultProperties = Object.fromEntries(
1025
+ Object.entries(defaults).map(([k, v]) => [k, Shape.from(this.getSource(), v)])
1026
+ )
1027
+
1028
+ for (const [k, v] of Object.entries(this.setProperties)) {
1029
+ if (v.isObject() && this.defaultProperties[k]?.isObject()) {
1030
+ v.setDefaults(this.defaultProperties[k].getValue())
1031
+ }
1032
+ }
1033
+
1034
+ return this
1035
+ }
1036
+
1037
+ transform(
1038
+ schema: (utils: { $: Transform; merge: typeof MERGE }) => {
1039
+ [P in string | typeof MERGE]?: Transform
1040
+ },
1041
+ resolve = true
1042
+ ) {
1043
+ const properties = {}
1044
+ const defaults = {}
1045
+
1046
+ const definedSchema = schema({ merge: MERGE, $: new Transform(this, resolve) })
1047
+ for (const to of [...Object.keys(definedSchema), MERGE] as const) {
1048
+ if (definedSchema[to]) {
1049
+ const result = definedSchema[to]._(to)
1050
+ if (to === MERGE) {
1051
+ const { val, def } = result
1052
+ if (val && (typeof val !== 'object' || Array.isArray(val))) {
1053
+ throw new Error('Cannot merge non-object value')
1054
+ }
1055
+
1056
+ if (def && (typeof def !== 'object' || Array.isArray(def))) {
1057
+ throw new Error('Cannot merge non-object default value')
1058
+ }
1059
+
1060
+ if (val instanceof ObjectShape) {
1061
+ assignWithoutOverwriting(properties, val.setProperties)
1062
+ assignWithoutOverwriting(defaults, val.defaultProperties)
1063
+ } else {
1064
+ assignWithoutOverwriting(properties, val)
1065
+ }
1066
+
1067
+ if (def instanceof ObjectShape) {
1068
+ assignWithoutOverwriting(defaults, def.setProperties)
1069
+ } else if (def) {
1070
+ assignWithoutOverwriting(defaults, def)
1071
+ }
1072
+ } else {
1073
+ properties[to] = result.val
1074
+ if (result.def !== undefined) {
1075
+ defaults[to] = result.def
1076
+ }
1077
+ }
1078
+ }
1079
+ }
1080
+
1081
+ return new ObjectShape({
1082
+ source: this.getSource(),
1083
+ properties,
1084
+ }).setDefaults(defaults)
1085
+ }
1086
+
1087
+ merge(other: NonNullable<object>): ObjectShape {
1088
+ const mergedProperties = { ...this.setProperties }
1089
+ const mergedDefaults = { ...this.defaultProperties }
1090
+
1091
+ for (const [a, b] of [
1092
+ [mergedProperties, other instanceof ObjectShape ? other.setProperties : other],
1093
+ [mergedDefaults, other instanceof ObjectShape ? other.defaultProperties : {}],
1094
+ ] as const) {
1095
+ for (const [key, v] of Object.entries(b)) {
1096
+ const value = Shape.from(this, v)
1097
+ if (!(key in a) && value.isUndefined()) {
1098
+ // Don't add properties that are undefined
1099
+ } else if (a[key]?.equals(value)) {
1100
+ // Don't overwrite properties that are equal
1101
+ } else if (a[key]?.isObject() && value.isObject()) {
1102
+ a[key].merge(value) // Merge objects recursively
1103
+ } else {
1104
+ a[key] = value
1105
+ }
1106
+ }
1107
+ }
1108
+
1109
+ return new ObjectShape({
1110
+ source: this.getSource(),
1111
+ properties: mergedProperties,
1112
+ })
1113
+ .setDefaults(mergedDefaults)
1114
+ .setCreator(this.getCreator())
1115
+ }
1116
+
1117
+ override equals(other: unknown): boolean {
1118
+ const otherShape = Shape.from(this, other)
1119
+ if (!otherShape.isObject() || this.keys().length !== otherShape.keys().length) {
1120
+ return false
1121
+ }
1122
+
1123
+ for (const [k, v] of this.entries()) {
1124
+ if (!v.equals(otherShape.get(k))) {
1125
+ return false
1126
+ }
1127
+ }
1128
+
1129
+ return true
1130
+ }
1131
+
1132
+ override getCode(): string {
1133
+ return `{\n ${this.entries({ nonDefaultsOnly: true, resolve: false })
1134
+ .map(([k, v]) => `${ObjectShape.quotePropertyNameIfNeeded(k)}: ${v.getCode()}`)
1135
+ .join(',\n ')}\n}`
1136
+ }
1137
+
1138
+ static quotePropertyNameIfNeeded(name: string): string {
1139
+ return /[^\w$]/.test(name) ? `'${name}'` : name
1140
+ }
1141
+ }
1142
+
1143
+ export class CallExpressionShape extends Shape {
1144
+ private readonly callee: string
1145
+ private readonly args: ArrayShape
1146
+
1147
+ constructor({ source, callee, args }: { source: Source; callee: string; args: unknown[] }) {
1148
+ super({ source })
1149
+ this.callee = callee
1150
+ this.args = new ArrayShape({ source, elements: args })
1151
+ }
1152
+
1153
+ getCallee(): string {
1154
+ return this.callee
1155
+ }
1156
+
1157
+ getArguments(resolve = true): Shape[] {
1158
+ return this.args.getElements(resolve)
1159
+ }
1160
+
1161
+ getArgument(index: number, resolve = true): Shape {
1162
+ return this.args.getElement(index, resolve)
1163
+ }
1164
+
1165
+ hasArgument(index: number): boolean {
1166
+ return this.args.hasElement(index)
1167
+ }
1168
+
1169
+ override getDescription(): string {
1170
+ return `${this.getKind()} (${this.callee})`
1171
+ }
1172
+
1173
+ override getCode(): string {
1174
+ return `${this.callee}(${this.args.map((a) => a.getCode(), false).join(', ')})`
1175
+ }
1176
+ }
1177
+
1178
+ export class SourceFileShape extends StringShape {
1179
+ private readonly path: string
1180
+ private readonly content: string
1181
+
1182
+ constructor({ file }: { file: ts.SourceFile }) {
1183
+ super({ source: file })
1184
+ this.path = file.getFilePath()
1185
+ this.content = file.getFullText()
1186
+ }
1187
+
1188
+ getPath(): string {
1189
+ return this.path
1190
+ }
1191
+
1192
+ getBaseName(): string {
1193
+ return path.basename(this.path)
1194
+ }
1195
+
1196
+ getExtension(): string {
1197
+ return path.extname(this.path)
1198
+ }
1199
+
1200
+ getContent(): string {
1201
+ return this.content
1202
+ }
1203
+
1204
+ override getSource(): ts.SourceFile {
1205
+ return this.getOriginalNode().asKindOrThrow(ts.SyntaxKind.SourceFile)
1206
+ }
1207
+
1208
+ override getValue(): string {
1209
+ return this.getContent()
1210
+ }
1211
+
1212
+ override getCode(): string {
1213
+ return this.getContent()
1214
+ }
1215
+ }
1216
+
1217
+ export class ElementAccessExpressionShape extends Shape {
1218
+ private readonly callee: string
1219
+ private readonly arg: StringShape | NumberShape
1220
+
1221
+ constructor({ source, callee, arg }: { source: Source; callee: string; arg: unknown }) {
1222
+ super({ source })
1223
+ this.callee = callee
1224
+ this.arg = Shape.from(source, arg)
1225
+ .pipe((e) => (e.isResolvable() ? e.resolve() : e))
1226
+ .as(
1227
+ [
1228
+ // I have absolutely no idea why these casts are needed in this file and
1229
+ // nowhere else. Seems like a probable TypeScript bug.
1230
+ StringShape as ShapeClass<StringShape>,
1231
+ NumberShape as ShapeClass<NumberShape>,
1232
+ ],
1233
+ `Invalid argument to element access expression: ${arg instanceof Shape ? arg.getDescription() : JSON.stringify(arg)}`
1234
+ )
1235
+ }
1236
+
1237
+ getCallee(): string {
1238
+ return this.callee
1239
+ }
1240
+
1241
+ getArg(): StringShape | NumberShape {
1242
+ return this.arg
1243
+ }
1244
+ }
1245
+
1246
+ export class VariableStatementShape extends Shape {
1247
+ private readonly variableName: IdentifierShape
1248
+ private readonly initializer: Shape
1249
+ private readonly _isExported: boolean
1250
+
1251
+ constructor({
1252
+ source,
1253
+ variableName,
1254
+ initializer,
1255
+ isExported = false,
1256
+ }: {
1257
+ source: Source
1258
+ variableName: IdentifierShape | string
1259
+ initializer: unknown
1260
+ isExported?: boolean
1261
+ }) {
1262
+ super({ source })
1263
+ this.variableName =
1264
+ variableName instanceof Shape ? variableName : new IdentifierShape({ source, name: variableName })
1265
+ this.initializer = Shape.from(source, initializer)
1266
+ this._isExported = isExported
1267
+ }
1268
+
1269
+ getVariableName(): IdentifierShape {
1270
+ return this.variableName
1271
+ }
1272
+
1273
+ getInitializer(): Shape {
1274
+ return this.initializer
1275
+ }
1276
+
1277
+ isExported(): boolean {
1278
+ return this._isExported
1279
+ }
1280
+
1281
+ override getCode(): string {
1282
+ return `${this.isExported() ? 'export ' : ''}const ${this.getVariableName().getName()} = ${this.getInitializer().getCode()}`
1283
+ }
1284
+ }
1285
+
1286
+ function getDefaultCategoryFromTable(table: string): InstallCategory {
1287
+ return table === 'sys_app' ? 'scope' : 'update'
1288
+ }
1289
+
1290
+ function getInstallCategoryFromInstallMethod(installMethod: string): InstallCategory {
1291
+ return installMethod === 'demo' ? 'unload.demo' : 'unload'
1292
+ }
1293
+
1294
+ export function getInstallCategory(source: Source, table: string): InstallCategory {
1295
+ let installCategory: InstallCategory = getDefaultCategoryFromTable(table)
1296
+ if (source instanceof CallExpressionShape) {
1297
+ const installMethod = source.getArgument(0).ifObject()?.get(['$meta', 'installMethod']).ifString()?.getValue()
1298
+ if (installMethod) {
1299
+ installCategory = getInstallCategoryFromInstallMethod(installMethod)
1300
+ }
1301
+ }
1302
+ return installCategory
1303
+ }
1304
+
1305
+ export type Action = 'INSERT_OR_UPDATE' | 'UPDATE' | 'DELETE' | 'delete_multiple'
1306
+
1307
+ export class Record extends ObjectShape {
1308
+ private readonly id: RecordId
1309
+ private readonly related: Record[] = []
1310
+ private readonly action: Action
1311
+ private readonly installCategory: InstallCategory
1312
+
1313
+ constructor({
1314
+ source,
1315
+ id,
1316
+ properties,
1317
+ action = 'INSERT_OR_UPDATE',
1318
+ installCategory = getDefaultCategoryFromTable(id.getTable()),
1319
+ }: {
1320
+ source: Source
1321
+ id: RecordId
1322
+ properties: NonNullable<object>
1323
+ action?: Action
1324
+ installCategory?: InstallCategory
1325
+ }) {
1326
+ super({ source, properties })
1327
+ this.action = action
1328
+ this.id = id
1329
+ this.installCategory = installCategory
1330
+ }
1331
+
1332
+ static isDeleteAction(action: Action): boolean {
1333
+ return action === 'DELETE' || action === 'delete_multiple'
1334
+ }
1335
+
1336
+ getId(): RecordId {
1337
+ return this.id
1338
+ }
1339
+
1340
+ getInstallCategory(): InstallCategory {
1341
+ return this.installCategory
1342
+ }
1343
+
1344
+ getTable(): string {
1345
+ return this.id.getTable()
1346
+ }
1347
+
1348
+ getAction(): Action {
1349
+ return this.action
1350
+ }
1351
+
1352
+ isDeleted(): boolean {
1353
+ return Record.isDeleteAction(this.getAction())
1354
+ }
1355
+
1356
+ getUpdateName(): string {
1357
+ return this.id.getUpdateName()
1358
+ }
1359
+
1360
+ with(...related: Record[]): this {
1361
+ this.related.length = 0
1362
+ this.related.push(...related)
1363
+ return this
1364
+ }
1365
+
1366
+ flat(): Record[] {
1367
+ return [this, ...this.related.flatMap((r) => r.flat())]
1368
+ }
1369
+
1370
+ getCount(): number {
1371
+ return 1 + this.related.reduce((count, r) => count + r.getCount(), 0)
1372
+ }
1373
+
1374
+ override merge(other: NonNullable<object>): Record {
1375
+ return new Record({
1376
+ source: this.getSource(),
1377
+ id: this.getId(),
1378
+ action: other instanceof Record ? other.getAction() : this.getAction(),
1379
+ installCategory: this.getInstallCategory(),
1380
+ properties: super.merge(other),
1381
+ }).setCreator(this.getCreator())
1382
+ }
1383
+
1384
+ override equals(other: unknown): boolean {
1385
+ return this.getId().equals(other)
1386
+ }
1387
+
1388
+ /**
1389
+ * Useful when you need to know if this record is equal to another not
1390
+ * just in terms of its identity but also its data.
1391
+ */
1392
+ strictEquals(other: unknown): boolean {
1393
+ if (!this.equals(other)) {
1394
+ return false
1395
+ }
1396
+
1397
+ if (!(other instanceof Record)) {
1398
+ return false
1399
+ }
1400
+
1401
+ return this.getAction() === other.getAction() && super.equals(other)
1402
+ }
1403
+
1404
+ override setCreator(creator: Plugin | undefined, overwrite = false): this {
1405
+ this.related.forEach((r) => r.setCreator(creator, overwrite))
1406
+ return super.setCreator(creator, overwrite)
1407
+ }
1408
+
1409
+ override toRecordId(): RecordId {
1410
+ return this.id
1411
+ }
1412
+
1413
+ override toString(): StringShape {
1414
+ return this.id
1415
+ }
1416
+ }
1417
+
1418
+ export type CoalesceKeys = globalThis.Record<string, string | RecordId>
1419
+ export type ResolvedCoalesceKeys = globalThis.Record<string, string>
1420
+ export class RecordId extends StringShape {
1421
+ private readonly table: string
1422
+ private readonly guid: string | (() => string)
1423
+ private readonly keys: CoalesceKeys | undefined
1424
+ private readonly nowIdKey: string | number | undefined
1425
+
1426
+ constructor({
1427
+ source,
1428
+ table,
1429
+ guid,
1430
+ keys: rawKeys,
1431
+ nowIdKey,
1432
+ }: {
1433
+ source: Source
1434
+ table: string
1435
+ guid: string | (() => string)
1436
+ keys?: CoalesceKeys | ObjectShape | undefined
1437
+ nowIdKey?: string | number | undefined
1438
+ }) {
1439
+ const keys =
1440
+ rawKeys instanceof ObjectShape
1441
+ ? Object.fromEntries(rawKeys.entries().map(([k, v]) => [k, v.ifString()?.getValue() ?? v.toRecordId()]))
1442
+ : rawKeys
1443
+
1444
+ if (keys && Object.keys(keys).length <= 0) {
1445
+ throw new Error('Failed to create RecordId shape. Coalesce keys must contain at least one entry.')
1446
+ }
1447
+
1448
+ super({ source })
1449
+ this.table = table
1450
+ this.guid = guid
1451
+ this.keys = keys
1452
+ this.nowIdKey = nowIdKey
1453
+ }
1454
+
1455
+ getTable(): string {
1456
+ return this.table
1457
+ }
1458
+
1459
+ getGuidProducer(): string | (() => string) {
1460
+ return this.guid
1461
+ }
1462
+
1463
+ getNowIdKey(): string | number | undefined {
1464
+ return this.nowIdKey
1465
+ }
1466
+
1467
+ getKeys(): CoalesceKeys | undefined {
1468
+ return this.keys
1469
+ }
1470
+
1471
+ /**
1472
+ * Similar to `getKeys`, but resolves any nested record IDs to their GUID values
1473
+ */
1474
+ resolveKeys(): ResolvedCoalesceKeys | undefined {
1475
+ if (!this.keys) {
1476
+ return undefined
1477
+ }
1478
+
1479
+ return Object.fromEntries(
1480
+ Object.entries(this.keys).map(([k, v]) => [k, typeof v === 'string' ? v : v.getValue()])
1481
+ )
1482
+ }
1483
+
1484
+ getKey(key: string): string | RecordId | undefined {
1485
+ return this.getKeys()?.[key]
1486
+ }
1487
+
1488
+ getPrimaryKey(): string {
1489
+ const keyValues = Object.values(this.keys ?? {})
1490
+ const [primaryKeyValue] = keyValues
1491
+ if (keyValues.length !== 1 || !primaryKeyValue || typeof primaryKeyValue !== 'string') {
1492
+ throw new Error("Tried to get primary key of record ID that doesn't have one")
1493
+ }
1494
+
1495
+ return primaryKeyValue
1496
+ }
1497
+
1498
+ hasPrimaryKey(): boolean {
1499
+ return !!this.keys && Object.keys(this.keys).length === 1
1500
+ }
1501
+
1502
+ getUpdateName(): string {
1503
+ // TODO: Provide a way to override this for non-standard coalesce strategies, or
1504
+ // just hard-code the very short list of special cases from UpdateName.java
1505
+ return `${this.table}_${this.getValue()}`
1506
+ }
1507
+
1508
+ override getValue(): string {
1509
+ return typeof this.guid === 'string' ? this.guid : this.guid()
1510
+ }
1511
+
1512
+ override equals(other: unknown): boolean {
1513
+ const otherShape = Shape.from(this, other)
1514
+ if (otherShape.isRecord()) {
1515
+ return this.equals(otherShape.getId())
1516
+ }
1517
+
1518
+ if (otherShape.isRecordId()) {
1519
+ return (
1520
+ this.getTable() === otherShape.getTable() &&
1521
+ (this.getValue() === otherShape.getValue() ||
1522
+ (this.keys !== undefined && _.isEqual(this.keys, otherShape.keys)))
1523
+ )
1524
+ }
1525
+
1526
+ return (this.hasPrimaryKey() && otherShape.equals(this.getPrimaryKey())) || super.equals(other)
1527
+ }
1528
+
1529
+ override getCode(): string {
1530
+ return `'${this.hasPrimaryKey() ? this.getPrimaryKey() : this.getValue()}'`
1531
+ }
1532
+ }