@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,995 @@
1
+ import {
2
+ getKindName,
3
+ type SupportedKindName,
4
+ type SupportedNode,
5
+ type SupportedNodeByKindName,
6
+ ts,
7
+ } from '../typescript'
8
+ import { DeletedShape, type ObjectShape, type Record, type Shape, type ShapeClass, type StringShape } from './shape'
9
+ import type { File, OutputFile } from './file'
10
+ import type { Product } from './product'
11
+ import { Database, DiffDatabase } from './database'
12
+ import type { Context as BaseContext } from './context'
13
+ import { getFileType } from '../util'
14
+ import { Cache } from './cache'
15
+
16
+ type Context = Omit<BaseContext, 'keys'>
17
+
18
+ export type Result<Value = unknown> =
19
+ | {
20
+ success: false
21
+ }
22
+ | {
23
+ success: true
24
+ value: Value
25
+ }
26
+
27
+ export type CommitResult = { success: boolean }
28
+
29
+ export type CommitFunction = (shape: Shape, target: ts.Node, ...plugins: Plugin[]) => Promise<void>
30
+
31
+ export type CommitContext = Context & {
32
+ commit: CommitFunction
33
+ }
34
+
35
+ export type RecordContext = Context & {
36
+ readonly database: Database
37
+ readonly descendants: Database
38
+ }
39
+
40
+ export type Relationships = {
41
+ [table: string]: Relationship
42
+ }
43
+
44
+ export type Relationship = {
45
+ /**
46
+ * Specifies how the tables are related:
47
+ *
48
+ * - String: Name of the reference field on the child table pointing to the parent (or
49
+ * vice versa for inverse relationships)
50
+ * - Object: Key-value pairs where keys are column names on the child table and values
51
+ * are the matching column names on the parent table (or vice versa for inverse
52
+ * relationships)
53
+ * - Function: Takes the parent and child records as arguments and returns a boolean to
54
+ * indicate whether the records are related or not (WARNING: Do not use this
55
+ * unless absolutely necessary as it can negatively impact performance)
56
+ */
57
+ via: string | { [column: string]: string } | ((parent: Record, child: Record) => boolean)
58
+
59
+ /**
60
+ * When true, the relationship direction is reversed - the parent references the child
61
+ * instead of the child referencing the parent
62
+ */
63
+ inverse?: boolean
64
+
65
+ /**
66
+ * When true, the related record is considered part of the same logical entity as its
67
+ * parent. These records should be processed together as a unit.
68
+ */
69
+ descendant?: boolean
70
+
71
+ /**
72
+ * Nested relationships that define how this table relates to other tables
73
+ */
74
+ relationships?: Relationships
75
+ }
76
+
77
+ export type CoalesceStrategy =
78
+ | [string, ...string[]] // Simple column-based strategy
79
+ | ((properties: ObjectShape) => globalThis.Record<string, string | StringShape> | ObjectShape) // dynamic strategy
80
+
81
+ export type FileType = 'fluent' | 'module' | 'json' | 'unknown'
82
+
83
+ export type PluginConfig<
84
+ Nodes extends SupportedKindName[] = SupportedKindName[],
85
+ Shapes extends ShapeClass[] = ShapeClass[],
86
+ Records extends string[] = string[],
87
+ > = {
88
+ /**
89
+ * The name of the plugin. Must end with "Plugin" suffix. This name is used in metrics
90
+ * and error messages.
91
+ */
92
+ name: `${string}Plugin`
93
+
94
+ /**
95
+ * The TypeScript AST nodes this plugin handles. Plugins that do not introduce new
96
+ * syntax should not need to define any handlers here.
97
+ */
98
+ nodes?: {
99
+ [I in keyof Nodes]: {
100
+ /**
101
+ * The TypeScript node kind to handle. Must be one of the hard-coded set
102
+ * of supported kinds in the Fluent DSL.
103
+ */
104
+ node: Nodes[I]
105
+
106
+ /**
107
+ * File types this handler should run on. These are based on the file
108
+ * extension. If not specified, it runs on all file types.
109
+ */
110
+ fileTypes?: FileType[]
111
+
112
+ /**
113
+ * Indicates whether this node should be parsed as an entry point during
114
+ * the top-level AST traversal to produce output. (Default: false)
115
+ */
116
+ entryPoint?: boolean
117
+
118
+ /**
119
+ * Converts a TypeScript node to a shape.
120
+ *
121
+ * This is the initial transformation step when processing source code.
122
+ * The function receives a TypeScript node of the specified kind and
123
+ * returns a shape derived from it. That shape can then be handled later
124
+ * by shape transforms defined in this plugin or some other plugin.
125
+ *
126
+ * @param node - The TypeScript node to transform
127
+ * @param context - Context object with useful services and information
128
+ * @returns A result indicating whether the transformation was successful
129
+ * and, if so, the resulting shape
130
+ */
131
+ toShape?(node: SupportedNodeByKindName<Nodes[I]>, context: Context): Result<Shape> | Promise<Result<Shape>>
132
+ }
133
+ }
134
+
135
+ /**
136
+ * The shapes this plugin handles. Each shape handler can implement the following:
137
+ *
138
+ * - Transform functions that turn shapes into records (toRecord)
139
+ * - Transform functions that turn shapes into more specialized subclasses of those shapes (toSubclass)
140
+ * - Commit functions that mutate TypeScript nodes to match shapes (commit)
141
+ * - Inspect functions that can examine shapes for diagnostic purposes (inspect)
142
+ */
143
+ shapes?: {
144
+ [I in keyof Shapes]: Shapes[I] extends ShapeClass<infer S>
145
+ ? {
146
+ /**
147
+ * The shape class to handle.
148
+ */
149
+ shape: Shapes[I]
150
+
151
+ /**
152
+ * File types this handler should run on. These are based on the file
153
+ * extension. If not specified, it runs on all file types.
154
+ */
155
+ fileTypes?: FileType[]
156
+
157
+ /**
158
+ * Inspects a shape. Useful for validating the details of a shape created
159
+ * by another plugin and producing diagnostics.
160
+ *
161
+ * @param shape - The shape to inspect
162
+ * @param context - Context object with useful services and information
163
+ */
164
+ inspect?(shape: S, context: Context): void
165
+
166
+ /**
167
+ * Transforms a shape into a more specific subclass. Plugins that introduce
168
+ * a shape which extends another shape should implement this. For example,
169
+ * many plugins implement support for entities based on call expressions,
170
+ * and therefore handle CallExpressionShape. Creating a subclass of that
171
+ * shape allows plugins to more easily handle their use case.
172
+ *
173
+ * NOTE: The shape returned by this function MUST be a subclass of the input
174
+ * shape. If it is not, an error will be thrown at runtime.
175
+ *
176
+ * @param shape - The shape to transform
177
+ * @param context - Context object with useful services and information
178
+ * @returns A result indicating whether the transformation was successful
179
+ * and, if so, an instance of the subclass
180
+ */
181
+ toSubclass?(shape: S, context: Context): Result<S>
182
+
183
+ /**
184
+ * Transforms a shape into a record. Almost all plugins should implement
185
+ * this, as it is the fundamental transformation step in the process of
186
+ * turning Fluent code into ServiceNow records. These records will then
187
+ * be serialized into XML as part of a build, or merged with incoming
188
+ * records as part of bidirectional sync.
189
+ *
190
+ * @param shape - The shape to transform
191
+ * @param context - Context object with useful services and information
192
+ * @returns A result indicating whether the transformation was successful
193
+ * and, if so, the resulting record(s)
194
+ */
195
+ toRecord?(shape: S, context: Context): Result<Record> | Promise<Result<Record>>
196
+
197
+ /**
198
+ * Mutates a target TypeScript node to match a given shape. Plugins that
199
+ * do not introduce new syntax should not need to implement this.
200
+ *
201
+ * NOTE: It is CRUCIAL that the commit function does not apply any changes
202
+ * to the target node other than what is absolutely necessary to match
203
+ * the shape. They should be "minimally invasive", preserving as much
204
+ * as possible of the original code, including comments and whitespace.
205
+ * Care must be taken to check if parts of the existing code are
206
+ * functionally equivalent to the incoming shape, even if they are not
207
+ * exactly the same.
208
+ *
209
+ * @param shape - The shape to commit
210
+ * @param target - The target TypeScript node to modify
211
+ * @param context - Context with useful services and information
212
+ * @returns A result indicating whether the commit was successful
213
+ */
214
+ commit?(shape: S, target: ts.Node, context: CommitContext): CommitResult | Promise<CommitResult>
215
+ }
216
+ : never
217
+ }
218
+
219
+ /**
220
+ * The records this plugin handles. The keys of this object are the names of the tables to be
221
+ * handled, and the values are handlers which can implement the following:
222
+ *
223
+ * - Transform functions that turn records into shapes (toShape)
224
+ * - Transform functions that turn records into files (toFile)
225
+ * - Relationship mappings
226
+ * - Coalesce strategies
227
+ */
228
+ records?: {
229
+ [T in Records[number]]: {
230
+ /**
231
+ * Columns that compose the coalesce strategy for this table. If there is a strategy defined in
232
+ * the platform for this table, you must make sure this configuration matches it. If there isn't
233
+ * one defined in the platform, you can decide to either create a suitable one here or leave it
234
+ * undefined. Tables without coalesce strategies will rely on their GUIDs to determine identity.
235
+ *
236
+ * For advanced use cases where simple column-based coalesce strategies cannot be used, you may
237
+ * define a dynamic coalesce strategy by supplying a function that accepts the properties of a
238
+ * record and returns an object with the desired coalesce keys.
239
+ *
240
+ * Refer to CoalesceStrategies.java to find platform-defined coalesce strategies:
241
+ * https://code.devsnc.com/dev/glide/blob/master/glide/src/com/glide/script/coalesce/CoalesceStrategies.java
242
+ */
243
+ coalesce?: CoalesceStrategy
244
+
245
+ /**
246
+ * You can think of the relationship between two tables as a parent-child relationship. Usually,
247
+ * the child references the parent via a foreign key. For example, sys_security_acl_role (child)
248
+ * references sys_security_acl (parent) via a reference column. This is the most basic type of
249
+ * relationship. In this case, the ACL plugin should handle the parent record and the children
250
+ * as part of the same entity. That's because the full definition of an ACL includes both the
251
+ * top-level sys_security_acl record AND the sys_security_acl_role M2M records which link it to
252
+ * its roles. This type of relationship, where a child record is part of a singular entity with
253
+ * its parent, is called a "descendant" relationship.
254
+ *
255
+ * sys_security_acl <- sys_security_acl_role (descendant relationship)
256
+ *
257
+ * Sometimes, relationships are the other way around, where the parent references the child. To
258
+ * continue the ACL example, sys_security_acl_role (parent) references sys_user_role (child) via
259
+ * a reference column. This is called an "inverse" relationship because it goes parent -> child
260
+ * instead of child -> parent. However, this would NOT be considered a "descendant" relationship
261
+ * because the role itself is not part of the ACL definition. The ACL and the role are related,
262
+ * but the role itself is its own separate entity.
263
+ *
264
+ * sys_security_acl_role <- sys_user_role (inverse non-descendant relationship)
265
+ *
266
+ * So far we've only explored relationships based on foreign keys (reference columns). In these
267
+ * cases your "via" would simply be the name of the reference column that connects them, either
268
+ * on the child table (normal relationship) or the parent (inverse relationship). However, it's
269
+ * also possible for two tables to be related by matching the values from one or more columns on
270
+ * both tables. For example, sys_db_object (parent) and sys_dictionary (child) are related by
271
+ * matching the "name" columns from each table. In other words, if a sys_db_object record has a
272
+ * value of "incident" in its "name" column, then any sys_dictionary records that also have the
273
+ * value "incident" in their "name" columns are its children. These children are also considered
274
+ * "descendants" because they are all part of the same table entity. In these cases, your "via"
275
+ * would be represented as key-value pairs instead of a single column name. The keys should be
276
+ * the names of columns on the child table, and the values should be the names of columns that
277
+ * should be matched on the parent table. (For "inverse" relationships, the key-value pairs are
278
+ * flipped.)
279
+ *
280
+ * sys_db_object <- sys_dictionary (descendant relationship by matching "name":"name")
281
+ */
282
+ relationships?: Relationships
283
+
284
+ /**
285
+ * Transforms a record into a shape. Almost all plugins should implement this, as it is
286
+ * the fundamental transformation step in the process of turning ServiceNow records into
287
+ * Fluent code, or bidirectionally syncing changes to existing Fluent code.
288
+ *
289
+ * @param record - The record to transform
290
+ * @param context - Context with useful services and information
291
+ * @returns A result indicating whether the transformation was successful and, if so, the
292
+ * resulting shape
293
+ */
294
+ toShape?(record: Record, context: RecordContext): Result<Shape> | Promise<Result<Shape>>
295
+
296
+ /**
297
+ * Transforms a database record into output file(s). Plugins should only implement this
298
+ * if the records they handle must be serialized to some specialized non-standard format.
299
+ * Records that follow the usual ServiceNow format do NOT need to be handled here. This
300
+ * transformation is the last step in the build process.
301
+ *
302
+ * @param record - The record to transform
303
+ * @param context - Context with useful services and information
304
+ * @returns A result indicating whether the transformation was successful and, if so, the
305
+ * resulting output file(s)
306
+ */
307
+ toFile?(
308
+ record: Record,
309
+ context: RecordContext
310
+ ): Result<OutputFile | OutputFile[]> | Promise<Result<OutputFile | OutputFile[]>>
311
+ }
312
+ }
313
+
314
+ /**
315
+ * The files this plugin handles. Plugins that do not introduce new file types should not
316
+ * need to implement anything here.
317
+ */
318
+ files?: {
319
+ /**
320
+ * A regex pattern or predicate to apply to a file's path to determine if it should be handled by this plugin.
321
+ */
322
+ matcher?: RegExp | ((path: string, context: Context) => boolean | Promise<boolean>)
323
+
324
+ /**
325
+ * Indicates whether this file should be parsed as an entry point when generating output. (Default: false)
326
+ */
327
+ entryPoint?: boolean
328
+
329
+ /**
330
+ * Transforms a file into record(s).
331
+ *
332
+ * @param file - The file to transform
333
+ * @param context - Context object with useful services and information
334
+ * @returns A result indicating whether the transformation was successful and, if so, the
335
+ * resulting record(s)
336
+ */
337
+ toRecord?(file: File, context: Context): Result<Record> | Promise<Result<Record>>
338
+ }[]
339
+ }
340
+
341
+ function setCreator<V extends Product | Product[]>(creator: Plugin, result: Result<V>): Result<V> {
342
+ if (result.success) {
343
+ result.value = Array.isArray(result.value)
344
+ ? (result.value.map((r) => r.setCreator(creator)) as V)
345
+ : (result.value.setCreator(creator) as V)
346
+ }
347
+
348
+ return result
349
+ }
350
+
351
+ type SearchableRelationships = {
352
+ [table: string]: Relationships
353
+ }
354
+
355
+ type SearchableReferences = {
356
+ [table: string]: {
357
+ [column: string]: string
358
+ }
359
+ }
360
+
361
+ export class Plugin {
362
+ private readonly nodeToShapeCache = new Cache<ts.ts.Node, Result<Shape>>()
363
+ private readonly shapeToSubclassCache = new Cache<Shape, Result<Shape>>()
364
+ private readonly shapeToRecordCache = new Cache<Shape, Result<Record>>()
365
+ private readonly relationships: SearchableRelationships = {}
366
+ private readonly references: SearchableReferences = {}
367
+
368
+ private constructor(private readonly config: PluginConfig) {
369
+ for (const [table, config] of Object.entries(this.config.records ?? {})) {
370
+ this.parseRelationships([table, config.relationships])
371
+ }
372
+ }
373
+
374
+ private parseRelationships([table, relationships]: [string, Relationships | undefined]) {
375
+ if (table === '*' || !relationships) {
376
+ return
377
+ }
378
+
379
+ this.relationships[table] = { ...this.relationships[table], ...relationships }
380
+ for (const [subTable, subConfig] of Object.entries(relationships)) {
381
+ if (typeof subConfig.via === 'string') {
382
+ const [parent, child] = subConfig.inverse ? [subTable, table] : [table, subTable]
383
+ this.references[child] = { ...this.references[child], [subConfig.via]: parent }
384
+ }
385
+
386
+ this.parseRelationships([subTable, subConfig.relationships])
387
+ }
388
+ }
389
+
390
+ /**
391
+ * Creates a new Fluent plugin from the provided configuration.
392
+ *
393
+ * Plugins are the primary mechanism for extending the Fluent DSL and providing support for
394
+ * new types of ServiceNow entities. Plugins implement handlers responsible for parsing and
395
+ * transforming Fluent code, ServiceNow records, and any other files that exist within a
396
+ * Fluent project.
397
+ *
398
+ * The build system orchestrates the plugins together to carry a project through all the
399
+ * stages of building and bidirectionally syncing a Fluent project, invoking each plugin's
400
+ * handlers at the appropriate points in the process, and providing the relevant inputs and
401
+ * context.
402
+ *
403
+ * During a regular build, the stages are:
404
+ *
405
+ * 1. Traverse the full AST of all the Fluent code, transforming each node into a shape
406
+ * 2. Transform each shape into a more specific subclass of that shape
407
+ * 3. Transform each subclassed shape into one or more ServiceNow records
408
+ * 4. Serialize each record into an output file
409
+ *
410
+ * During bidirectional sync, the stages are:
411
+ *
412
+ * 1. Perform stage 1 from the regular build process to get the existing shapes
413
+ * 2. Perform stage 2 from the regular build process to subclass those existing shapes
414
+ * 3. Perform stage 3 from the regular build process to get the existing records
415
+ * 4. Parse input files (usually XML from a ServiceNow instance) to get the incoming records
416
+ * 5. Merge the incoming records into the existing records so that all changes are reflected
417
+ * 6. Transform each merged record into a shape
418
+ * 7. Commit each shape to the Fluent AST, generating new nodes as needed
419
+ *
420
+ * @example
421
+ * // Stages of a regular build
422
+ * const CoolPlugin = Plugin.create({
423
+ * name: 'CoolPlugin',
424
+ * nodes: [
425
+ * {
426
+ * node: 'CallExpression',
427
+ * toShape(node) {
428
+ * // BUILD STAGE 1: A CallExpression node is transformed into a CallExpressionShape.
429
+ * }
430
+ * }
431
+ * ],
432
+ * shapes: [
433
+ * {
434
+ * shape: CallExpressionShape,
435
+ * toSubclass(shape) {
436
+ * // BUILD STAGE 2: The CallExpressionShape from stage 1 is transformed into a CoolShape.
437
+ * }
438
+ * },
439
+ * {
440
+ * shape: CoolShape,
441
+ * toRecord(shape) {
442
+ * // BUILD STAGE 3: The CoolShape from stage 2 is transformed into a cool_table record.
443
+ * }
444
+ * }
445
+ * ],
446
+ * records: {
447
+ * cool_table: {
448
+ * toFile(record) {
449
+ * // BUILD STAGE 4: The cool_table record from stage 3 is serialized into an output file.
450
+ * }
451
+ * }
452
+ * },
453
+ * })
454
+ *
455
+ * @example
456
+ * // Stages of bidirectional sync
457
+ * const CoolPlugin = Plugin.create({
458
+ * name: 'CoolPlugin',
459
+ * nodes: [
460
+ * {
461
+ * node: 'CallExpression',
462
+ * toShape(node) {
463
+ * // SYNC STAGE 1: An existing CallExpression node is transformed into a CallExpressionShape.
464
+ * }
465
+ * }
466
+ * ],
467
+ * shapes: [
468
+ * {
469
+ * shape: CallExpressionShape,
470
+ * toSubclass(shape) {
471
+ * // SYNC STAGE 2: The existing CallExpressionShape from stage 1 is transformed into a CoolShape
472
+ * },
473
+ * commit(shape, target) {
474
+ * // SYNC STAGE 7: The new CoolShape from stage 6, which now contains the changes from the
475
+ * // incoming cool_table record, is committed to the original CallExpression node.
476
+ * //
477
+ * // NOTES:
478
+ * // - Since CoolShape extends CallExpressionShape, and there is no commit function
479
+ * // for CoolShape, this commit function is used. If, for some reason, CoolShape
480
+ * // needed to be committed differently than other CallExpressionShapes, then this
481
+ * // plugin would need to implement a commit function for CoolShape.
482
+ * // - If the incoming cool_table record didn't already exist in the code, then the
483
+ * // build system would generate an arbitrary new node before passing it into this
484
+ * // commit function as the target. So the implementation should never assume that
485
+ * // the target is a CallExpression node.
486
+ * }
487
+ * },
488
+ * {
489
+ * shape: CoolShape,
490
+ * toRecord(shape) {
491
+ * // SYNC STAGE 3: The existing CoolShape from stage 2 is transformed into a cool_table record.
492
+ * }
493
+ * }
494
+ * ],
495
+ * files: [
496
+ * {
497
+ * extension: 'xml',
498
+ * toRecord(file) {
499
+ * // SYNC STAGE 4: An incoming XML file with changes is parsed into a cool_table record.
500
+ * }
501
+ * }
502
+ * ],
503
+ * records: {
504
+ * cool_table: {
505
+ * toShape(record) {
506
+ * // SYNC STAGE 6: After the incoming cool_table record from stage 4 is merged into the
507
+ * // existing one from stage 3, the merged record is transformed back into a CoolShape.
508
+ * }
509
+ * }
510
+ * },
511
+ * })
512
+ */
513
+ static create<const N extends SupportedKindName[], const S extends ShapeClass[]>(config: PluginConfig<N, S>) {
514
+ return new Plugin(config)
515
+ }
516
+
517
+ getName(): string {
518
+ return this.config.name
519
+ }
520
+
521
+ getDescendants(parent: Record, database: Database): Record[] {
522
+ const descendantRelationships = Object.entries(this.relationships[parent.getTable()] ?? {}).filter(
523
+ ([, { descendant }]) => descendant
524
+ )
525
+
526
+ const descendants: Record[] = []
527
+ const push = (children: Record | Record[] | undefined) => {
528
+ for (const child of [children].flat()) {
529
+ if (child) {
530
+ descendants.push(child)
531
+ descendants.push(...this.getDescendants(child, database))
532
+ }
533
+ }
534
+ }
535
+
536
+ for (const [childTable, { via, inverse = false }] of descendantRelationships) {
537
+ if (typeof via === 'string') {
538
+ if (inverse) {
539
+ parent
540
+ .get(via)
541
+ .ifDefined()
542
+ ?.toString()
543
+ .pipe((v) => push(database.get(childTable, v.getValue())))
544
+ } else {
545
+ push(database.query(childTable, { [via]: parent.toString().getValue() }))
546
+ }
547
+ } else if (typeof via === 'function') {
548
+ push(database.query(childTable).filter((child) => via(parent, child)))
549
+ } else {
550
+ push(
551
+ database.query(
552
+ childTable,
553
+ Object.fromEntries(
554
+ Object.entries(via).map((entry) => {
555
+ const [parentColumn, childColumn] = inverse ? entry : [entry[1], entry[0]]
556
+ return [childColumn, parent.get(parentColumn).getValue()]
557
+ })
558
+ )
559
+ )
560
+ )
561
+ }
562
+ }
563
+
564
+ return descendants
565
+ }
566
+
567
+ getCoalesceStrategy(table: string): CoalesceStrategy | undefined {
568
+ return (this.config.records ?? {})[table]?.coalesce
569
+ }
570
+
571
+ getCoalesceTables(): string[] {
572
+ return Object.entries(this.config.records ?? {})
573
+ .filter(([_, val]) => val.coalesce)
574
+ .map(([table]) => table)
575
+ }
576
+
577
+ getTables(relationships?: Relationships, tables: string[] = []): Set<string> {
578
+ const tableEntries: any[] = relationships
579
+ ? Object.entries(relationships).filter(([, val]) => val.descendant)
580
+ : Object.entries(this.config.records ?? {}).filter(([, val]) => val.toShape)
581
+
582
+ tables.push(...tableEntries.filter(([table]) => table !== '*').map(([table]) => table))
583
+ tableEntries
584
+ .filter(([_, val]) => val.relationships)
585
+ .forEach(([_, val]) => this.getTables(val.relationships, tables))
586
+ return new Set(tables)
587
+ }
588
+
589
+ getReferenceColumns(table: string): { [column: string]: string } {
590
+ return this.references[table] ?? {}
591
+ }
592
+
593
+ getRelationships(): SearchableRelationships {
594
+ return this.relationships
595
+ }
596
+
597
+ flushCache(): void {
598
+ this.nodeToShapeCache.clear()
599
+ this.shapeToRecordCache.clear()
600
+ }
601
+
602
+ async nodeToShape(node: SupportedNode, context: Omit<Context, 'self'>): Promise<Result<Shape>> {
603
+ const entry = this.nodeToShapeCache.get(node.compilerNode)
604
+ if (entry) {
605
+ return entry.isError() ? { success: false } : entry.unwrap()
606
+ }
607
+
608
+ try {
609
+ for (const config of this.config.nodes ?? []) {
610
+ if (
611
+ config.toShape &&
612
+ getKindName(node) === config.node &&
613
+ (!config.fileTypes || config.fileTypes.includes(getFileType(node.getSourceFile().getFilePath())))
614
+ ) {
615
+ const result = setCreator(this, await config.toShape.bind(this)(node, { ...context, self: this }))
616
+ if (result.success) {
617
+ return this.nodeToShapeCache.put(node.compilerNode, result)
618
+ }
619
+ }
620
+ }
621
+
622
+ return this.nodeToShapeCache.put(node.compilerNode, { success: false })
623
+ } catch (e) {
624
+ throw this.nodeToShapeCache.error(node.compilerNode, e)
625
+ }
626
+ }
627
+
628
+ async commit(shape: Shape, target: ts.Node, context: Omit<CommitContext, 'self'>): Promise<CommitResult> {
629
+ for (const { shape: shapeClass, commit } of this.config.shapes ?? []) {
630
+ if (shape.is(shapeClass) && commit) {
631
+ const result = await commit.bind(this)(shape, target, { ...context, self: this })
632
+ if (result.success) {
633
+ return result
634
+ }
635
+ }
636
+ }
637
+
638
+ return { success: false }
639
+ }
640
+
641
+ shapeToSubclass<const S extends Shape>(shape: S, context: Omit<Context, 'self'>): Result<S> {
642
+ const entry = this.shapeToSubclassCache.get(shape)
643
+ if (entry) {
644
+ return entry.isError() ? { success: false } : (entry.unwrap() as Result<S>)
645
+ }
646
+
647
+ try {
648
+ for (const config of this.config.shapes ?? []) {
649
+ if (
650
+ config.toSubclass &&
651
+ shape.constructor === config.shape &&
652
+ (!config.fileTypes || config.fileTypes.includes(getFileType(shape.getOriginalFilePath())))
653
+ ) {
654
+ const result = setCreator(
655
+ this,
656
+ config.toSubclass.bind(this)(shape, {
657
+ ...context,
658
+ self: this,
659
+ })
660
+ )
661
+
662
+ if (result.success) {
663
+ if (result.value.constructor === config.shape) {
664
+ throw new Error(
665
+ `Result of subclassing "${config.shape.name}" is an instance of the same class. The result MUST be a subclass.`
666
+ )
667
+ }
668
+
669
+ return this.shapeToSubclassCache.put(shape, result as Result<S>)
670
+ }
671
+ }
672
+ }
673
+
674
+ return this.shapeToSubclassCache.put(shape, { success: false })
675
+ } catch (e) {
676
+ throw this.shapeToSubclassCache.error(shape, e)
677
+ }
678
+ }
679
+
680
+ async shapeToRecord(shape: Shape, context: Omit<Context, 'self'>): Promise<Result<Record>> {
681
+ const entry = this.shapeToRecordCache.get(shape)
682
+ if (entry) {
683
+ return entry.isError() ? { success: false } : entry.unwrap()
684
+ }
685
+
686
+ try {
687
+ for (const config of this.config.shapes ?? []) {
688
+ if (
689
+ config.toRecord &&
690
+ shape.is(config.shape) &&
691
+ (!config.fileTypes || config.fileTypes.includes(getFileType(shape.getOriginalFilePath())))
692
+ ) {
693
+ const result = setCreator(this, await config.toRecord.bind(this)(shape, { ...context, self: this }))
694
+ if (result.success) {
695
+ return this.shapeToRecordCache.put(shape, result)
696
+ }
697
+ }
698
+ }
699
+
700
+ return this.shapeToRecordCache.put(shape, { success: false })
701
+ } catch (e) {
702
+ throw this.shapeToRecordCache.error(shape, e)
703
+ }
704
+ }
705
+
706
+ private async recordToShape0(record: Record, context: Omit<RecordContext, 'self'>): Promise<Result<Shape>> {
707
+ if (record.getAction() !== 'INSERT_OR_UPDATE') {
708
+ return { success: true, value: new DeletedShape({ source: record }) }
709
+ }
710
+
711
+ const table = record.getTable()
712
+ for (const [configTable, { toShape }] of Object.entries(this.config.records ?? {})) {
713
+ if ((configTable === '*' || configTable === table) && toShape) {
714
+ const result = setCreator(this, await toShape.bind(this)(record, { ...context, self: this }))
715
+ if (result.success) {
716
+ return result
717
+ }
718
+ }
719
+ }
720
+
721
+ return { success: false }
722
+ }
723
+
724
+ async recordToShape({
725
+ record,
726
+ database,
727
+ context,
728
+ }: {
729
+ record: Record
730
+ database: Database
731
+ context: Omit<Context, 'self'>
732
+ }): Promise<Result<Shape>> {
733
+ context.logger.debug(`Transforming record into shape: ${record.getTable()}.${record.getId().getValue()}`)
734
+
735
+ const descendants = this.getDescendants(record, database)
736
+ return this.recordToShape0(record, {
737
+ ...context,
738
+ database,
739
+ descendants: new Database(descendants.filter((r) => r.getAction() === 'INSERT_OR_UPDATE')),
740
+ })
741
+ }
742
+
743
+ async recordsToShapes({
744
+ database,
745
+ creatorOnly,
746
+ changedOnly = false,
747
+ mode,
748
+ handledRecords,
749
+ context,
750
+ }: {
751
+ database: Database
752
+ creatorOnly: boolean
753
+ changedOnly?: boolean
754
+ mode: 'explicit' | 'catch-all'
755
+ handledRecords: Database
756
+ context: Omit<Context, 'self'>
757
+ }): Promise<Result<Shape[]>> {
758
+ let success = false
759
+ const shapes: Shape[] = []
760
+
761
+ for (const [table, { toShape }] of Object.entries(this.config.records ?? {})) {
762
+ if (!toShape) {
763
+ continue
764
+ } else if (mode === 'explicit' && table === '*') {
765
+ continue
766
+ } else if (mode === 'catch-all' && table !== '*') {
767
+ continue
768
+ }
769
+
770
+ for (const record of table === '*' ? database.query() : database.query(table)) {
771
+ if (handledRecords.resolve(record.getId())) {
772
+ continue
773
+ }
774
+
775
+ if (creatorOnly && record.getCreator() !== this) {
776
+ continue
777
+ }
778
+
779
+ context.logger.debug(
780
+ `Transforming record into shape: ${record.getTable()}.${record.getId().getValue()}`
781
+ )
782
+
783
+ const descendants = this.getDescendants(record, database)
784
+ if (changedOnly) {
785
+ if (!(database instanceof DiffDatabase)) {
786
+ throw new Error(
787
+ `Tried to check for changed records while creating shapes, but the provided database does not support diffing.`
788
+ )
789
+ }
790
+
791
+ if (![record, ...descendants].some((r) => database.isChanged(r))) {
792
+ continue // If neither the record nor its descendants are in the diff, skip it
793
+ }
794
+ }
795
+
796
+ const result = await this.recordToShape0(record, {
797
+ ...context,
798
+ database,
799
+ descendants: new Database(descendants.filter((r) => r.getAction() === 'INSERT_OR_UPDATE')),
800
+ })
801
+
802
+ if (result.success) {
803
+ success = true
804
+ handledRecords.insert(record)
805
+ descendants.forEach((d) => handledRecords.insert(d))
806
+ shapes.push(...[result.value].flat())
807
+ }
808
+ }
809
+ }
810
+
811
+ return { success, value: shapes }
812
+ }
813
+
814
+ async recordToFile(
815
+ record: Record,
816
+ context: Omit<RecordContext, 'self'>
817
+ ): Promise<Result<OutputFile | OutputFile[]>> {
818
+ const table = record.getTable()
819
+ for (const [configTable, { toFile }] of Object.entries(this.config.records ?? {})) {
820
+ if ((configTable === '*' || configTable === table) && toFile) {
821
+ const result = await toFile.bind(this)(record, { ...context, self: this })
822
+ if (result.success) {
823
+ return result
824
+ }
825
+ }
826
+ }
827
+
828
+ return { success: false }
829
+ }
830
+
831
+ async recordsToFiles({
832
+ database,
833
+ mode,
834
+ handledGuids,
835
+ context,
836
+ }: {
837
+ database: Database
838
+ mode: 'explicit' | 'catch-all'
839
+ handledGuids: Set<string>
840
+ context: Omit<Context, 'self'>
841
+ }): Promise<Result<OutputFile[]>> {
842
+ let success = false
843
+ const files: OutputFile[] = []
844
+
845
+ for (const [table, { toFile }] of Object.entries(this.config.records ?? {})) {
846
+ if (!toFile) {
847
+ continue
848
+ } else if (mode === 'explicit' && table === '*') {
849
+ continue
850
+ } else if (mode === 'catch-all' && table !== '*') {
851
+ continue
852
+ }
853
+
854
+ for (const record of table === '*' ? database.query() : database.query(table)) {
855
+ if (handledGuids.has(record.getId().getValue())) {
856
+ continue
857
+ }
858
+
859
+ const descendants = this.getDescendants(record, database)
860
+ const result = await this.recordToFile(record, {
861
+ ...context,
862
+ database,
863
+ descendants: new Database(descendants.filter((r) => r.getAction() === 'INSERT_OR_UPDATE')),
864
+ })
865
+
866
+ if (result.success) {
867
+ success = true
868
+ handledGuids.add(record.getId().getValue())
869
+ descendants.forEach((r) => handledGuids.add(r.getId().getValue()))
870
+ files.push(...[result.value].flat())
871
+ }
872
+ }
873
+ }
874
+
875
+ return { success, value: files }
876
+ }
877
+
878
+ async isEntryPoint(pathOrNode: string | SupportedNode, context: Omit<Context, 'self'>): Promise<boolean> {
879
+ if (ts.Node.isNode(pathOrNode)) {
880
+ for (const config of this.config.nodes ?? []) {
881
+ if (getKindName(pathOrNode) !== config.node) {
882
+ continue
883
+ }
884
+
885
+ const fileType = getFileType(pathOrNode.getSourceFile().getFilePath())
886
+ if (config.fileTypes && !config.fileTypes.includes(fileType)) {
887
+ continue
888
+ }
889
+
890
+ return !!config.entryPoint
891
+ }
892
+ } else {
893
+ for (const config of this.config.files ?? []) {
894
+ if (config.matcher instanceof RegExp && !config.matcher.test(pathOrNode)) {
895
+ continue
896
+ }
897
+
898
+ if (
899
+ typeof config.matcher === 'function' &&
900
+ !(await config.matcher(pathOrNode, { ...context, self: this }))
901
+ ) {
902
+ continue
903
+ }
904
+
905
+ return !!config.entryPoint
906
+ }
907
+ }
908
+
909
+ return false
910
+ }
911
+
912
+ async fileToRecord(file: File, context: Omit<Context, 'self'>): Promise<Result<Record>> {
913
+ for (const config of this.config.files ?? []) {
914
+ if (!config.toRecord) {
915
+ continue
916
+ }
917
+
918
+ if (config.matcher instanceof RegExp && !config.matcher.test(file.path)) {
919
+ continue
920
+ }
921
+
922
+ if (typeof config.matcher === 'function' && !config.matcher(file.path, { ...context, self: this })) {
923
+ continue
924
+ }
925
+
926
+ const result = setCreator(this, await config.toRecord.bind(this)(file, { ...context, self: this }))
927
+ if (result.success) {
928
+ return result
929
+ }
930
+ }
931
+
932
+ return { success: false }
933
+ }
934
+
935
+ inspect(shape: Shape, context: Context): void {
936
+ for (const config of this.config.shapes ?? []) {
937
+ if (shape.is(config.shape) && config.inspect) {
938
+ config.inspect.bind(this)(shape, context)
939
+ }
940
+ }
941
+ }
942
+ }
943
+
944
+ export class Plugins {
945
+ constructor(private readonly plugins: Plugin[]) {}
946
+
947
+ toArray(): Plugin[] {
948
+ return this.plugins
949
+ }
950
+
951
+ add(plugin: Plugin): void {
952
+ this.plugins.push(plugin)
953
+ }
954
+
955
+ getCoalesceStrategy(table: string): CoalesceStrategy | undefined {
956
+ for (const plugin of this.plugins) {
957
+ const coalesceStrategy = plugin.getCoalesceStrategy(table)
958
+ if (coalesceStrategy) {
959
+ return coalesceStrategy
960
+ }
961
+ }
962
+
963
+ return undefined
964
+ }
965
+
966
+ getCoalesceTables(): Set<string> {
967
+ const tables = new Set<string>()
968
+ for (const plugin of this.plugins) {
969
+ plugin.getCoalesceTables().forEach((t) => {
970
+ tables.add(t)
971
+ })
972
+ }
973
+
974
+ return tables
975
+ }
976
+
977
+ getReferenceColumns(table: string): { [column: string]: string } {
978
+ const refColumnEntries: [string, string][] = []
979
+ for (const plugin of this.plugins) {
980
+ refColumnEntries.push(...Object.entries(plugin.getReferenceColumns(table)))
981
+ }
982
+
983
+ return Object.fromEntries(refColumnEntries)
984
+ }
985
+
986
+ async isEntryPoint(pathOrNode: string | SupportedNode, context: Omit<Context, 'self'>): Promise<boolean> {
987
+ for (const plugin of this.plugins) {
988
+ if (await plugin.isEntryPoint(pathOrNode, context)) {
989
+ return true
990
+ }
991
+ }
992
+
993
+ return false
994
+ }
995
+ }