@servicenow/sdk-build-core 4.5.0 → 4.6.1
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.
- package/dist/compression.d.ts +2 -1
- package/dist/compression.js +5 -2
- package/dist/compression.js.map +1 -1
- package/dist/now-config.d.ts +22 -6
- package/dist/now-config.js +41 -2
- package/dist/now-config.js.map +1 -1
- package/dist/plugins/file.d.ts +1 -0
- package/dist/plugins/plugin.d.ts +3 -6
- package/dist/plugins/plugin.js +50 -26
- package/dist/plugins/plugin.js.map +1 -1
- package/dist/plugins/post-install.d.ts +7 -0
- package/dist/plugins/shape.js +4 -1
- package/dist/plugins/shape.js.map +1 -1
- package/dist/xml.d.ts +2 -2
- package/dist/xml.js +2 -2
- package/dist/xml.js.map +1 -1
- package/now.config.schema.json +43 -1
- package/package.json +2 -2
- package/src/compression.ts +7 -2
- package/src/now-config.ts +53 -2
- package/src/plugins/file.ts +1 -0
- package/src/plugins/plugin.ts +70 -41
- package/src/plugins/post-install.ts +8 -0
- package/src/plugins/shape.ts +4 -1
- package/src/xml.ts +11 -2
package/src/plugins/plugin.ts
CHANGED
|
@@ -12,7 +12,7 @@ import { Database, DiffDatabase } from './database'
|
|
|
12
12
|
import type { Context as BaseContext } from './context'
|
|
13
13
|
import { getFileType, isSNScope } from '../util'
|
|
14
14
|
import { Cache } from './cache'
|
|
15
|
-
import { NOW_FILE_EXTENSION } from '..'
|
|
15
|
+
import { NOW_FILE_EXTENSION, NowConfig } from '..'
|
|
16
16
|
import { path } from '@servicenow/sdk-build-core'
|
|
17
17
|
|
|
18
18
|
type Context = Omit<BaseContext, 'keys'>
|
|
@@ -66,17 +66,8 @@ export type Relationship = {
|
|
|
66
66
|
* properties, where each is a tuple: [columnName, regexPattern]. The regex extracts
|
|
67
67
|
* values from the respective columns using capturing groups, and records are related
|
|
68
68
|
* if all captured groups match between parent and child
|
|
69
|
-
* - Function: Takes the parent and child records as arguments and returns a boolean to
|
|
70
|
-
* indicate whether the records are related or not (WARNING: Do not use this
|
|
71
|
-
* unless absolutely necessary as it can negatively impact performance and
|
|
72
|
-
* cannot be serialized by the relationship resolver, requiring changes to
|
|
73
|
-
* the REST API it invokes)
|
|
74
69
|
*/
|
|
75
|
-
via:
|
|
76
|
-
| string
|
|
77
|
-
| { [column: string]: string }
|
|
78
|
-
| ((parent: Record, child: Record) => boolean)
|
|
79
|
-
| { parent: [string, string]; child: [string, string] }[]
|
|
70
|
+
via: string | { [column: string]: string } | { parent: [string, string]; child: [string, string] }[]
|
|
80
71
|
|
|
81
72
|
/**
|
|
82
73
|
* When true, the relationship direction is reversed - the parent references the child
|
|
@@ -657,16 +648,22 @@ export class Plugin {
|
|
|
657
648
|
}
|
|
658
649
|
|
|
659
650
|
getDescendants(parent: Record, database: Database): Record[] {
|
|
651
|
+
return this.traverseDescendants(parent, database)
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
private traverseDescendants(parent: Record, database: Database, visited: Set<string> = new Set()): Record[] {
|
|
655
|
+
visited.add(parent.getId().getValue())
|
|
656
|
+
|
|
660
657
|
const descendantRelationships = Object.entries(this.relationships[parent.getTable()] ?? {}).filter(
|
|
661
658
|
([, { descendant }]) => descendant
|
|
662
659
|
)
|
|
663
|
-
|
|
664
660
|
const descendants: Record[] = []
|
|
665
661
|
const push = (children: Record | Record[] | undefined) => {
|
|
666
662
|
for (const child of [children].flat()) {
|
|
667
|
-
if (child &&
|
|
663
|
+
if (child && !visited.has(child.getId().getValue())) {
|
|
664
|
+
visited.add(child.getId().getValue())
|
|
668
665
|
descendants.push(child)
|
|
669
|
-
descendants.push(...this.
|
|
666
|
+
descendants.push(...this.traverseDescendants(child, database, visited))
|
|
670
667
|
}
|
|
671
668
|
}
|
|
672
669
|
}
|
|
@@ -682,8 +679,6 @@ export class Plugin {
|
|
|
682
679
|
} else {
|
|
683
680
|
push(database.query(childTable, { [via]: parent.toString().getValue() }))
|
|
684
681
|
}
|
|
685
|
-
} else if (typeof via === 'function') {
|
|
686
|
-
push(database.query(childTable).filter((child) => via(parent, child)))
|
|
687
682
|
} else if (Array.isArray(via)) {
|
|
688
683
|
push(
|
|
689
684
|
database.query(childTable).filter((child) => {
|
|
@@ -692,16 +687,26 @@ export class Plugin {
|
|
|
692
687
|
parent: [pCol, pReg],
|
|
693
688
|
child: [cCol, cReg],
|
|
694
689
|
} = entry
|
|
695
|
-
const parentValue =
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
690
|
+
const parentValue =
|
|
691
|
+
pCol === 'sys_id'
|
|
692
|
+
? parent.getId().getValue()
|
|
693
|
+
: parent
|
|
694
|
+
.get(pCol)
|
|
695
|
+
.ifString()
|
|
696
|
+
?.asString(
|
|
697
|
+
`Expected string value for column ${pCol} in regex-based via relationship`
|
|
698
|
+
)
|
|
699
|
+
.getValue()
|
|
700
|
+
const childValue =
|
|
701
|
+
cCol === 'sys_id'
|
|
702
|
+
? child.getId().getValue()
|
|
703
|
+
: child
|
|
704
|
+
.get(cCol)
|
|
705
|
+
.ifString()
|
|
706
|
+
?.asString(
|
|
707
|
+
`Expected string value for column ${cCol} in regex-based via relationship`
|
|
708
|
+
)
|
|
709
|
+
.getValue()
|
|
705
710
|
if (!parentValue || !childValue) {
|
|
706
711
|
return false
|
|
707
712
|
}
|
|
@@ -952,6 +957,8 @@ export class Plugin {
|
|
|
952
957
|
}): Promise<Result<Shape[]>> {
|
|
953
958
|
let success = false
|
|
954
959
|
const shapes: Shape[] = []
|
|
960
|
+
const unhandledRecords: Record[] = []
|
|
961
|
+
const unhandledRecordIds: Set<string> = new Set()
|
|
955
962
|
|
|
956
963
|
for (const [table, { toShape }] of Object.entries(this.config.records ?? {})) {
|
|
957
964
|
if (!toShape) {
|
|
@@ -1006,14 +1013,10 @@ export class Plugin {
|
|
|
1006
1013
|
descendants: nonDeletedDescendants,
|
|
1007
1014
|
})
|
|
1008
1015
|
|
|
1009
|
-
// Track unhandled records to avoid marking them as handled
|
|
1010
|
-
const unhandledRecords: Record[] = []
|
|
1011
|
-
const unhandledRecordIds: string[] = []
|
|
1012
|
-
|
|
1013
1016
|
if (result.success === 'partial') {
|
|
1014
1017
|
for (const unhandledRecord of result.unhandledRecords) {
|
|
1015
1018
|
unhandledRecords.push(unhandledRecord)
|
|
1016
|
-
unhandledRecordIds.
|
|
1019
|
+
unhandledRecordIds.add(unhandledRecord.getId().getValue())
|
|
1017
1020
|
}
|
|
1018
1021
|
}
|
|
1019
1022
|
|
|
@@ -1030,7 +1033,7 @@ export class Plugin {
|
|
|
1030
1033
|
|
|
1031
1034
|
// Mark all descendants as handled
|
|
1032
1035
|
for (const descendant of [...deletedDescendants, ...nonDeletedDescendants]) {
|
|
1033
|
-
if (!unhandledRecordIds.
|
|
1036
|
+
if (!unhandledRecordIds.has(descendant.getId().getValue())) {
|
|
1034
1037
|
// This descendent was known to the plugin because it is not deleted, and handling was deferred
|
|
1035
1038
|
// delete_multiple should also be marked 'handled'
|
|
1036
1039
|
if (descendant.getAction() !== 'DELETE') {
|
|
@@ -1045,17 +1048,17 @@ export class Plugin {
|
|
|
1045
1048
|
}
|
|
1046
1049
|
}
|
|
1047
1050
|
}
|
|
1048
|
-
|
|
1049
|
-
if (unhandledRecords.length > 0) {
|
|
1050
|
-
context.logger.debug(
|
|
1051
|
-
`Plugin ${this.getName()} deferred handling of records: ${unhandledRecords.map((r) => r.getTable() + '_' + r.getId().getValue()).join(', ')}`
|
|
1052
|
-
)
|
|
1053
|
-
return { success: 'partial', value: shapes, unhandledRecords }
|
|
1054
|
-
}
|
|
1055
1051
|
}
|
|
1056
1052
|
}
|
|
1057
1053
|
}
|
|
1058
1054
|
|
|
1055
|
+
if (unhandledRecords.length > 0) {
|
|
1056
|
+
context.logger.debug(
|
|
1057
|
+
`Plugin ${this.getName()} deferred handling of records: ${unhandledRecords.map((r) => r.getTable() + '_' + r.getId().getValue()).join(', ')}`
|
|
1058
|
+
)
|
|
1059
|
+
return { success: 'partial', value: shapes, unhandledRecords }
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1059
1062
|
return { success, value: shapes }
|
|
1060
1063
|
}
|
|
1061
1064
|
|
|
@@ -1132,12 +1135,31 @@ export class Plugin {
|
|
|
1132
1135
|
const filePath = path.normalize(record.getOriginalFilePath())
|
|
1133
1136
|
if (isSNScope(context.config.scope) && filePath.endsWith(NOW_FILE_EXTENSION)) {
|
|
1134
1137
|
const relativePath = path.relative(path.join(context.config.fluentDir, 'if'), filePath)
|
|
1135
|
-
|
|
1138
|
+
const relativePathGenerated = path.relative(
|
|
1139
|
+
path.join(context.config.generatedDir, 'if'),
|
|
1140
|
+
filePath
|
|
1141
|
+
)
|
|
1142
|
+
if (
|
|
1143
|
+
!relativePath.startsWith('..') ||
|
|
1144
|
+
(!relativePathGenerated.startsWith('..') && path.dirname(relativePath) !== '.')
|
|
1145
|
+
) {
|
|
1146
|
+
const subPath = relativePath.startsWith('..') ? relativePathGenerated : relativePath
|
|
1136
1147
|
return {
|
|
1137
1148
|
success: true,
|
|
1138
1149
|
value: outputFiles.map((f) => ({
|
|
1139
1150
|
...f,
|
|
1140
|
-
ifDirectoryPackage: path.join('if', path.
|
|
1151
|
+
ifDirectoryPackage: path.join('if', subPath.split(path.sep)[0]!),
|
|
1152
|
+
})),
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
const hosted = NowConfig.getHostedDirectory(context.config, filePath)
|
|
1157
|
+
if (hosted) {
|
|
1158
|
+
return {
|
|
1159
|
+
success: true,
|
|
1160
|
+
value: outputFiles.map((f) => ({
|
|
1161
|
+
...f,
|
|
1162
|
+
hostedPluginDir: hosted,
|
|
1141
1163
|
})),
|
|
1142
1164
|
}
|
|
1143
1165
|
}
|
|
@@ -1294,6 +1316,13 @@ export class Plugins {
|
|
|
1294
1316
|
return this.plugins
|
|
1295
1317
|
}
|
|
1296
1318
|
|
|
1319
|
+
remove(name: string): void {
|
|
1320
|
+
const index = this.plugins.findIndex((plugin) => plugin.getName() === name)
|
|
1321
|
+
if (index !== -1) {
|
|
1322
|
+
this.plugins.splice(index, 1)
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1297
1326
|
prepend(...plugins: Plugin[]): void {
|
|
1298
1327
|
this.plugins.unshift(...plugins)
|
|
1299
1328
|
}
|
|
@@ -10,6 +10,14 @@ export type PostInstallTask = {
|
|
|
10
10
|
/** Human-readable description for the --skip flag help text */
|
|
11
11
|
skipFlagDescription: string
|
|
12
12
|
|
|
13
|
+
/**
|
|
14
|
+
* Controls which project type triggers this task.
|
|
15
|
+
* - `'app'` — only runs after installing a scoped app.
|
|
16
|
+
* - `'configuration'` — only runs after installing a configuration project.
|
|
17
|
+
* - `'both'` — runs after either install path.
|
|
18
|
+
*/
|
|
19
|
+
runFor: 'app' | 'configuration' | 'both'
|
|
20
|
+
|
|
13
21
|
/** Execute the post-install task */
|
|
14
22
|
run(context: PostInstallContext): Promise<void>
|
|
15
23
|
}
|
package/src/plugins/shape.ts
CHANGED
|
@@ -1170,7 +1170,10 @@ export class ObjectShape extends Shape<globalThis.Record<string, unknown>> {
|
|
|
1170
1170
|
}
|
|
1171
1171
|
|
|
1172
1172
|
static quotePropertyNameIfNeeded(name: string): string {
|
|
1173
|
-
|
|
1173
|
+
// Valid JS identifier (e.g. short_description, $id) — no quotes needed
|
|
1174
|
+
// Pure numeric literal without leading zeros (e.g. 0, 100, 1.5) — valid as object key without quotes
|
|
1175
|
+
// Everything else (empty string, hyphens, digit-leading non-numeric chars, octal-like 007) — must be quoted
|
|
1176
|
+
return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name) || /^(0|[1-9]\d*)(\.\d+)?$/.test(name) ? name : `'${name}'`
|
|
1174
1177
|
}
|
|
1175
1178
|
}
|
|
1176
1179
|
|
package/src/xml.ts
CHANGED
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
import { Record, RecordId, ResolvableShape, Shape } from './plugins'
|
|
2
2
|
import { js2xml, type Element } from 'xml-js'
|
|
3
3
|
|
|
4
|
-
export function unloadBuilder({
|
|
4
|
+
export function unloadBuilder({
|
|
5
|
+
scope,
|
|
6
|
+
scopeId,
|
|
7
|
+
table,
|
|
8
|
+
}: {
|
|
9
|
+
scope?: string | undefined
|
|
10
|
+
scopeId: string
|
|
11
|
+
table?: string
|
|
12
|
+
}) {
|
|
5
13
|
const recordUpdateElements = []
|
|
6
14
|
const xmlJsObj = {
|
|
7
15
|
declaration: {
|
|
@@ -41,7 +49,8 @@ export function unloadBuilder({ scope, scopeId, table }: { scope: string; scopeI
|
|
|
41
49
|
const rec = recordXml(recordUpdateElements, table, record.getId().getValue(), {
|
|
42
50
|
attr: { action: record.getAction(), ...extraAttributes },
|
|
43
51
|
})
|
|
44
|
-
|
|
52
|
+
|
|
53
|
+
scope ? rec.field('sys_scope', scopeId, { display_value: scope }) : rec.field('sys_scope', scopeId)
|
|
45
54
|
rec.field('sys_update_name', updateName)
|
|
46
55
|
return rec
|
|
47
56
|
},
|