@enfocussw/switch-scripting-context 25.11.0-beta.10
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/CHANGELOG.md +81 -0
- package/README.md +151 -0
- package/bin/cli.js +8 -0
- package/dist/init.d.ts +19 -0
- package/dist/init.js +616 -0
- package/docs/switch-api/api-connection.md +63 -0
- package/docs/switch-api/api-debugging.md +51 -0
- package/docs/switch-api/api-document-classes.md +167 -0
- package/docs/switch-api/api-entry-points.md +82 -0
- package/docs/switch-api/api-enums.md +153 -0
- package/docs/switch-api/api-execution-environment.md +70 -0
- package/docs/switch-api/api-flow-element.md +112 -0
- package/docs/switch-api/api-http.md +87 -0
- package/docs/switch-api/api-job-patterns.md +96 -0
- package/docs/switch-api/api-job.md +156 -0
- package/docs/switch-api/api-logging.md +53 -0
- package/docs/switch-api/api-logs-and-dataroot.md +71 -0
- package/docs/switch-api/api-property-editors.md +144 -0
- package/docs/switch-api/api-script-declaration.md +351 -0
- package/docs/switch-api/api-script-structure.md +85 -0
- package/docs/switch-api/api-switch.md +75 -0
- package/docs/switch-api/api-tooling.md +61 -0
- package/docs/switch-api/api-vscode.md +62 -0
- package/docs/switch-scripting.md +40 -0
- package/package.json +60 -0
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# HttpRequest & HttpResponse Classes
|
|
2
|
+
|
|
3
|
+
Used in the `httpRequestTriggeredSync` and `httpRequestTriggeredAsync` entry points. Webhook subscriptions are registered via `s.httpRequestSubscribe()` in `flowStartTriggered`.
|
|
4
|
+
|
|
5
|
+
## HttpRequest
|
|
6
|
+
|
|
7
|
+
Represents an incoming webhook HTTP request.
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
request.method: HttpRequest.Method // "POST" | "PUT" | "DELETE"
|
|
11
|
+
request.path: string // relative URL path
|
|
12
|
+
request.query: { [key: string]: string | string[] } // query string parameters
|
|
13
|
+
request.headers: { [header: string]: string } // request headers
|
|
14
|
+
request.remoteAddress: string // client IP address
|
|
15
|
+
request.body: ArrayBuffer | undefined // raw request body
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
```ts
|
|
19
|
+
request.getBodyAsString(): string
|
|
20
|
+
```
|
|
21
|
+
Returns the raw request body decoded as a UTF-8 string.
|
|
22
|
+
|
|
23
|
+
## HttpResponse
|
|
24
|
+
|
|
25
|
+
Used only in `httpRequestTriggeredSync` to send a response before the function returns.
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
response.setStatusCode(statusCode: number): void
|
|
29
|
+
```
|
|
30
|
+
Set the HTTP status code (e.g. `200`, `400`, `500`).
|
|
31
|
+
|
|
32
|
+
```ts
|
|
33
|
+
response.setHeader(name: string, value: string): void
|
|
34
|
+
```
|
|
35
|
+
Set a response header.
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
response.setBody(data: ArrayBuffer | string): void
|
|
39
|
+
```
|
|
40
|
+
Set the response body.
|
|
41
|
+
|
|
42
|
+
## HttpRequest.Method enum
|
|
43
|
+
|
|
44
|
+
| Value | String |
|
|
45
|
+
|---|---|
|
|
46
|
+
| `HttpRequest.Method.POST` | `"POST"` |
|
|
47
|
+
| `HttpRequest.Method.PUT` | `"PUT"` |
|
|
48
|
+
| `HttpRequest.Method.DELETE` | `"DELETE"` |
|
|
49
|
+
|
|
50
|
+
Available as `EnfocusSwitch.HttpRequest.Method.*` outside `main.ts`.
|
|
51
|
+
|
|
52
|
+
## Webhook pattern
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
// flowStartTriggered — subscribe once when the flow starts
|
|
56
|
+
async function flowStartTriggered(s: Switch, flowElement: FlowElement): Promise<void> {
|
|
57
|
+
await s.httpRequestSubscribe(HttpRequest.Method.POST, '/my-path', []);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// flowStopTriggered — unsubscribe when the flow stops
|
|
61
|
+
async function flowStopTriggered(s: Switch, flowElement: FlowElement): Promise<void> {
|
|
62
|
+
await s.httpRequestUnsubscribe(HttpRequest.Method.POST, '/my-path');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// httpRequestTriggeredSync — handle synchronously, must set response before returning
|
|
66
|
+
async function httpRequestTriggeredSync(request: HttpRequest, args: any[], response: HttpResponse, s: Switch): Promise<void> {
|
|
67
|
+
const body = request.getBodyAsString();
|
|
68
|
+
response.setStatusCode(200);
|
|
69
|
+
response.setHeader('Content-Type', 'application/json');
|
|
70
|
+
response.setBody(JSON.stringify({ received: true }));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// httpRequestTriggeredAsync — handle asynchronously, no response required
|
|
74
|
+
async function httpRequestTriggeredAsync(request: HttpRequest, args: any[], s: Switch, flowElement: FlowElement): Promise<void> {
|
|
75
|
+
const body = request.getBodyAsString();
|
|
76
|
+
// process asynchronously...
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Constraints
|
|
81
|
+
|
|
82
|
+
- Request body limit: 1 MB (server returns HTTP 413 if exceeded)
|
|
83
|
+
- Queue limit: 10,000 pending requests per element (HTTP 429)
|
|
84
|
+
- Sync handler execution timeout: 1 minute (HTTP 524)
|
|
85
|
+
- Default sync response if none set explicitly: HTTP 200 with body `{"status": true}`
|
|
86
|
+
- Async handler only invoked if sync handler is absent or returned a 2xx status
|
|
87
|
+
- Use unpredictable URL paths for security
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# Job Patterns and Behavioral Rules
|
|
2
|
+
|
|
3
|
+
Common behavioral rules for file access, routing, temp file cleanup, and child jobs. These are the most frequent sources of bugs.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## File access semantics
|
|
8
|
+
|
|
9
|
+
`job.get(accessLevel)` and `job.getDataset(name, accessLevel)` behave differently depending on the access level:
|
|
10
|
+
|
|
11
|
+
| Access level | What it returns | Modification |
|
|
12
|
+
|---|---|---|
|
|
13
|
+
| `AccessLevel.ReadOnly` | Path in the element's **input folder** | Throws if the file is modified |
|
|
14
|
+
| `AccessLevel.ReadWrite` | Path in a **temp location** (copy) | Auto-uploaded to the output folder when `sendTo*()` is called |
|
|
15
|
+
|
|
16
|
+
Always use `AccessLevel.ReadOnly` unless you need to modify the file content.
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Temp file cleanup
|
|
21
|
+
|
|
22
|
+
Switch does **not** automatically clean up files you create or pass to job-creation methods. The script is responsible:
|
|
23
|
+
|
|
24
|
+
- Files/folders passed to `flowElement.createJob(path)` — delete after routing.
|
|
25
|
+
- Files/folders passed to `job.createChild(path)` — delete after routing.
|
|
26
|
+
- Files/folders passed to `job.createDataset(name, filePath, model)` — delete after routing.
|
|
27
|
+
|
|
28
|
+
> Files accumulate between executor refreshes if not cleaned up. The executor is refreshed after 5 min idle, 5,000 tasks, 150 MB memory, or 1,024 open file handles — at which point accumulated files are removed automatically, but do not rely on this.
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## Sending jobs
|
|
33
|
+
|
|
34
|
+
After calling any `job.sendTo*()`:
|
|
35
|
+
- Only further `sendTo*()` calls and `job.fail()` are allowed on that job object.
|
|
36
|
+
- An **unmodified** incoming job can be sent to multiple connections (call `sendTo*()` multiple times).
|
|
37
|
+
- A **modified** job (ReadWrite access used) must use `job.createChild()` to produce additional copies for routing to other connections.
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
// Route unmodified job to two connections — OK
|
|
41
|
+
await job.sendToData(Connection.Level.Success);
|
|
42
|
+
await job.sendToData(Connection.Level.Warning); // only if job was not modified
|
|
43
|
+
|
|
44
|
+
// Route modified content to multiple connections — use createChild
|
|
45
|
+
const child = await job.createChild(tempPath);
|
|
46
|
+
await job.sendTo(conn1);
|
|
47
|
+
await child.sendTo(conn2);
|
|
48
|
+
// Clean up tempPath after routing
|
|
49
|
+
await fs.promises.rm(tempPath);
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## Dataset writes must precede child job creation
|
|
55
|
+
|
|
56
|
+
**Known issue — pending a server-side fix; remove this section once fixed.** Applies to all current versions.
|
|
57
|
+
|
|
58
|
+
`job.createChild()` copies the parent's *current* datasets immediately (see [api-job.md](api-job.md#child-jobs)); `job.createDataset()` only registers a pending write, flushed to disk at the next `sendTo*()` call (see [api-job.md](api-job.md#datasets-metadata)). If a child is created *before* a dataset is written, the child inherits the job's existing dataset under that name instead. At flush time, the pending write fans out concurrently to the parent and to every child created so far, all reading from the same source file on disk — targets that already have a dataset by that name get the file **moved** into place, while targets that don't get it **copied**. When a mix of both happens against a single source file, the move wins the race and the copies fail with errors like `Could not place the file with the decoded data '...' into the datasets folder`, deterministically and on every retry (the source file is gone after the first attempt).
|
|
59
|
+
|
|
60
|
+
**Rule:** perform every dataset write on a job before creating any child of it. Where a script's structure naturally interleaves the two, iterate the dataset outputs first, then the child-job outputs, rather than handling each output in one pass.
|
|
61
|
+
|
|
62
|
+
Approaches that do not work:
|
|
63
|
+
- Deferring `createChild()` until after routing — the SDK rejects any of `createChild`, `setPriority`, `log`, `setPrivateData`, `listDatasets`, `removeDataset`, and others once a `sendTo*()` call has been made on the job, throwing `Method is not allowed at this time. Have you called any sendTo method already?`.
|
|
64
|
+
- Creating children first and only deferring *their* routing — a child is enlisted in the dataset flush at `createChild()` time, not when it's routed.
|
|
65
|
+
- Calling `removeDataset()` before `createDataset()` — this only clears the parent's record and cannot reach copies a child already inherited.
|
|
66
|
+
|
|
67
|
+
A dataset name that's genuinely new to the job never triggers this, since every target takes the copy path — the bug requires a pre-existing dataset of that name.
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## processLater
|
|
72
|
+
|
|
73
|
+
`job.processLater()` defers the job for the next `timerFired` invocation.
|
|
74
|
+
|
|
75
|
+
Constraints:
|
|
76
|
+
- Minimum deferral: **10 seconds** from the time `jobArrived` was called.
|
|
77
|
+
- Cannot be called on jobs created with `createJob()` or `createChild()`.
|
|
78
|
+
- Private data changes made before `processLater()` are preserved.
|
|
79
|
+
- New datasets created before `processLater()` are **discarded**.
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## Executor cleanup thresholds
|
|
84
|
+
|
|
85
|
+
The Node.js executor process is recycled when any of the following thresholds is reached:
|
|
86
|
+
|
|
87
|
+
| Threshold | Value |
|
|
88
|
+
|---|---|
|
|
89
|
+
| Idle time | 5 minutes |
|
|
90
|
+
| Tasks processed | 5,000 |
|
|
91
|
+
| Memory consumption | 150 MB |
|
|
92
|
+
| Open file handles | 1,024 |
|
|
93
|
+
|
|
94
|
+
On cleanup, any files/folders created in the temp area are removed. Always close file handles and database connections before the entry point returns.
|
|
95
|
+
|
|
96
|
+
See [api-execution-environment.md](api-execution-environment.md) for how this process model affects state persistence and error handling across job invocations.
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# Job Class
|
|
2
|
+
|
|
3
|
+
A `Job` represents a file or folder moving through the flow. It is passed to `jobArrived` and can be created via `flowElement.createJob()` or `job.createChild()`. Every job must be routed with a `sendTo*()` call or `fail()` before the entry point ends (or at a later time via `timerFired`).
|
|
4
|
+
> Jobs obtained via `flowElement.getJobs()` throw on `getPrivateData()`, `listDatasets()`, and `getDataset()` in the same entry point invocation — private data/metadata cannot be *read* back. `setPrivateData()`/`removePrivateData()` are not restricted this way.
|
|
5
|
+
## Identity
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
job.getName(includeExtension?: boolean): string // default: true
|
|
9
|
+
```
|
|
10
|
+
Returns the filename without the internal ID prefix. Optionally strips the extension.
|
|
11
|
+
|
|
12
|
+
```ts
|
|
13
|
+
job.getId(): string
|
|
14
|
+
```
|
|
15
|
+
Returns the unique job ID (the prefix portion of the filename, without underscores).
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
job.isFile(): boolean
|
|
19
|
+
job.isFolder(): boolean
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Priority
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
job.getPriority(): number
|
|
26
|
+
job.setPriority(priority: number): void
|
|
27
|
+
```
|
|
28
|
+
Use `Priority.*` enum values for standard levels.
|
|
29
|
+
|
|
30
|
+
## File access
|
|
31
|
+
|
|
32
|
+
```ts
|
|
33
|
+
job.get(accessLevel: AccessLevel): Promise<string>
|
|
34
|
+
```
|
|
35
|
+
Returns the local filesystem path to the job. Use `AccessLevel.ReadOnly` to read, `AccessLevel.ReadWrite` if you will modify the file/folder. Modified content is automatically uploaded on the next `sendTo*()` call. Throws if `ReadOnly` is used but the file was modified.
|
|
36
|
+
|
|
37
|
+
## Routing
|
|
38
|
+
|
|
39
|
+
Every job must be routed exactly once.
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
job.sendToNull(): Promise<void>
|
|
43
|
+
```
|
|
44
|
+
Discard the job (mark as completed with no output).
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
job.sendToSingle(newName?: string): Promise<void>
|
|
48
|
+
```
|
|
49
|
+
Send to the single outgoing move connection. Optionally rename.
|
|
50
|
+
|
|
51
|
+
```ts
|
|
52
|
+
job.sendTo(connection: Connection, newName?: string): Promise<void>
|
|
53
|
+
```
|
|
54
|
+
Send to a specific connection (any type). Get connections via `flowElement.getOutConnections()`.
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
job.sendToData(level: Connection.Level, newName?: string): Promise<void>
|
|
58
|
+
```
|
|
59
|
+
Send via a traffic light "data" connection at the specified level. Fails the job if no matching connection exists.
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
job.sendToLog(level: Connection.Level, model: DatasetModel, newName?: string): Promise<void>
|
|
63
|
+
```
|
|
64
|
+
Send via a traffic light "log" connection. For "data with log" connections, attaches the job as a metadata dataset to data jobs routed via `sendToData`. Discards the job if no matching connection exists.
|
|
65
|
+
|
|
66
|
+
```ts
|
|
67
|
+
job.sendToChannel(channelId: string, newName?: string): Promise<void>
|
|
68
|
+
```
|
|
69
|
+
Send to a named channel. Unlike `sendToData`/`sendToLog`, this **throws** synchronously (it does not fail-and-route the job) if the channel has no active subscriber, or if `channelId`/`newName` is empty — catch it explicitly.
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
job.processLater(seconds?: number): Promise<void> // default: 300
|
|
73
|
+
```
|
|
74
|
+
Re-queue the job for `jobArrived` after a minimum delay (re-evaluates dynamic properties). Minimum delay: 10 seconds from `jobArrived` (a warning is logged if less). Cannot be called on newly created or child jobs. Content is not modified; newly created datasets are discarded. Private data changes are preserved.
|
|
75
|
+
|
|
76
|
+
## Failure & logging
|
|
77
|
+
|
|
78
|
+
See [api-logging.md](api-logging.md) for log level semantics and logging practice.
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
job.fail(message: string, messageParams?: (string | number | boolean)[]): void
|
|
82
|
+
```
|
|
83
|
+
Log a fatal error and move the job to Problem Jobs. Newly created jobs are not moved. Use `%1`, `%2`, etc. for substitution.
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
job.log(level: LogLevel, message: string, messageParams?: (string | number | boolean)[]): Promise<void>
|
|
87
|
+
```
|
|
88
|
+
Log a message including job context. To log a literal `%`, pass it as a param: `job.log(LogLevel.Info, '%1', [message])`.
|
|
89
|
+
|
|
90
|
+
## Child jobs
|
|
91
|
+
|
|
92
|
+
```ts
|
|
93
|
+
job.createChild(path: string): Promise<Job>
|
|
94
|
+
```
|
|
95
|
+
Creates a new job inheriting the processing history, metadata, and private data of the parent. Takes effect immediately: the child's datasets are copied from the parent's *current* server-side state, so any pending `createDataset()` write not yet flushed by a `sendTo*()` call is not reflected in it. See [api-job-patterns.md](api-job-patterns.md#dataset-writes-must-precede-child-job-creation) — creating a child before writing a dataset is a common source of bugs. The caller is responsible for cleaning up the source file after routing.
|
|
96
|
+
|
|
97
|
+
## Private data
|
|
98
|
+
|
|
99
|
+
Private data is arbitrary key/value storage attached to a job and passed along with it.
|
|
100
|
+
|
|
101
|
+
```ts
|
|
102
|
+
job.getPrivateData(tag: string | EnfocusSwitchPrivateDataTag): Promise<any>
|
|
103
|
+
job.getPrivateData(tags?: (string | EnfocusSwitchPrivateDataTag)[]): Promise<{ tag: string, value: any }[]>
|
|
104
|
+
```
|
|
105
|
+
Read one tag (returns the value) or multiple tags / all tags (returns an array). Returns an empty string for missing tags.
|
|
106
|
+
|
|
107
|
+
```ts
|
|
108
|
+
job.setPrivateData(tag: string | EnfocusSwitchPrivateDataTag, value: any): Promise<void>
|
|
109
|
+
job.setPrivateData(privateData: { tag: string | EnfocusSwitchPrivateDataTag, value: any }[]): Promise<void>
|
|
110
|
+
```
|
|
111
|
+
Write one or multiple private data entries. Replaces existing values.
|
|
112
|
+
|
|
113
|
+
```ts
|
|
114
|
+
job.removePrivateData(tag: string | EnfocusSwitchPrivateDataTag): Promise<void>
|
|
115
|
+
job.removePrivateData(tags: (string | EnfocusSwitchPrivateDataTag)[]): Promise<void>
|
|
116
|
+
```
|
|
117
|
+
Delete one or multiple private data entries.
|
|
118
|
+
|
|
119
|
+
## Datasets (metadata)
|
|
120
|
+
|
|
121
|
+
Datasets are named files (XML, XMP, JDF, or Opaque) attached to a job.
|
|
122
|
+
|
|
123
|
+
```ts
|
|
124
|
+
job.listDatasets(): Promise<{ name: string, model: DatasetModel, extension: string }[]>
|
|
125
|
+
```
|
|
126
|
+
List all datasets attached to the job. **Immediate**: reads server state at once, so a pending `createDataset()` write not yet flushed by `sendTo*()` will not appear.
|
|
127
|
+
|
|
128
|
+
```ts
|
|
129
|
+
job.createDataset(name: string, filePath: string, model: DatasetModel): Promise<void>
|
|
130
|
+
```
|
|
131
|
+
Attach a new dataset. **Deferred**: registers in-memory only; the file is not uploaded until the next `sendTo*()` call. Caller must clean up the source file.
|
|
132
|
+
|
|
133
|
+
```ts
|
|
134
|
+
job.getDataset(name: string, accessLevel: AccessLevel): Promise<string>
|
|
135
|
+
```
|
|
136
|
+
Returns the local path to the dataset file. Prefer `AccessLevel.ReadOnly`.
|
|
137
|
+
|
|
138
|
+
```ts
|
|
139
|
+
job.removeDataset(name: string): Promise<void>
|
|
140
|
+
```
|
|
141
|
+
Remove a dataset by name. **Immediate**: updates server state at once. Throws if it does not exist.
|
|
142
|
+
|
|
143
|
+
## Variables & structured data
|
|
144
|
+
|
|
145
|
+
```ts
|
|
146
|
+
job.getVariableAsString(variable: string): Promise<string>
|
|
147
|
+
```
|
|
148
|
+
Returns the value of a Switch variable as a string.
|
|
149
|
+
|
|
150
|
+
```ts
|
|
151
|
+
job.getxmlData(path: any, xpathQuery: any): Promise<any>
|
|
152
|
+
job.getxmpData(path: any, xpath: any): Promise<any>
|
|
153
|
+
job.getJdfData(path: any, xpath: any): Promise<any>
|
|
154
|
+
job.getJSONData(metadata: any): Promise<any>
|
|
155
|
+
```
|
|
156
|
+
Convenience helpers to read XML/XMP/JDF/JSON data from a dataset path and return it as a parsed object. On error, these log the failure and resolve to `undefined` rather than throwing or rejecting — check the result rather than wrapping the call in `try`/`catch`.
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Logging Practices
|
|
2
|
+
|
|
3
|
+
Guidance for using `job.log()`, `flowElement.log()`, `job.fail()`, and `flowElement.failProcess()`
|
|
4
|
+
— see [api-job.md](api-job.md#failure--logging) and
|
|
5
|
+
[api-flow-element.md](api-flow-element.md#logging) for their signatures. `LogLevel` values are
|
|
6
|
+
listed in [api-enums.md](api-enums.md). Each log call has a small performance overhead, so where
|
|
7
|
+
and how often to log is a deliberate choice, not a default.
|
|
8
|
+
|
|
9
|
+
## Log levels
|
|
10
|
+
|
|
11
|
+
| Level | Use for |
|
|
12
|
+
|---|---|
|
|
13
|
+
| `Error` | The job/step failed or produced a wrong/unusable result. Often accompanies or precedes a hard stop, but can stand alone if the job continues in a degraded way. |
|
|
14
|
+
| `Warning` | Something unexpected happened but processing continued successfully (a fallback was used, an optional resource was missing). |
|
|
15
|
+
| `Info` | Normal, expected checkpoints worth surfacing to a flow operator without them needing debug mode (e.g. "processed 40 records", "output written to X"). |
|
|
16
|
+
| `Debug` | Verbose detail for diagnosing execution. Many users leave debug logging enabled in production to self-diagnose issues and to help app developers and Enfocus Support — leave meaningful checkpoints in, but trim noisy or no-longer-useful debug logs before shipping. **`Debug` messages only reach the log database if the Switch preference "Log debug messages" is turned on** (off by default) — see [api-logs-and-dataroot.md](api-logs-and-dataroot.md#retention-and-the-debug-level-gate). |
|
|
17
|
+
|
|
18
|
+
## Logging in loops
|
|
19
|
+
|
|
20
|
+
Don't categorically avoid logging inside loops — per-item logs can matter (e.g. a per-job error).
|
|
21
|
+
But where multiple log calls can be combined without losing clarity, prefer a single summary over
|
|
22
|
+
one call per iteration, e.g. `"Found 12 matching jobs"` instead of one log line per job.
|
|
23
|
+
|
|
24
|
+
## `console.log` does not work
|
|
25
|
+
|
|
26
|
+
`console.log` only produces output when a debug session is attached (see
|
|
27
|
+
[api-debugging.md](api-debugging.md)); in a normal run it is not visible anywhere. Never rely on it
|
|
28
|
+
in production scripts — use `job.log()`/`flowElement.log()` for anything that should be visible in
|
|
29
|
+
the log/message pane.
|
|
30
|
+
|
|
31
|
+
## Don't double-log before a failure
|
|
32
|
+
|
|
33
|
+
`job.fail()` and `flowElement.failProcess()` already log their message as a fatal error. Don't call
|
|
34
|
+
`job.log(LogLevel.Error, ...)` / `flowElement.log(LogLevel.Error, ...)` with the same message
|
|
35
|
+
immediately before them — it duplicates the entry in the log. Use a separate `Error` log call only
|
|
36
|
+
when it conveys something distinct from the failure message.
|
|
37
|
+
|
|
38
|
+
## Message formatting and the `%` gotcha
|
|
39
|
+
|
|
40
|
+
Template literals are fine for ordinary log messages. But a string that may contain a literal `%`
|
|
41
|
+
character — commonly external/untrusted values such as cloud storage URLs — will error if
|
|
42
|
+
interpolated directly into the message, because it gets parsed as a `%1`-style substitution token.
|
|
43
|
+
Workaround: pass `'%1'` as the message and the value as the parameter, e.g.:
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
job.log(LogLevel.Info, '%1', [urlThatMightContainPercent]);
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Signature gotcha: array vs. single value
|
|
50
|
+
|
|
51
|
+
`job.fail(message, messageParams?: (string | number | boolean)[])` takes an **array**.
|
|
52
|
+
`flowElement.failProcess(message, messageParam?: string | number | boolean)` takes a **single
|
|
53
|
+
value**, not an array. Passing an array to `failProcess` is a common mistake.
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# Locating and Querying Switch's Log Database
|
|
2
|
+
|
|
3
|
+
For diagnosing or validating a script from the outside — reading what the user themselves would see
|
|
4
|
+
in Switch's Message pane — rather than instrumenting the script itself. See
|
|
5
|
+
[api-logging.md](api-logging.md) for how a script produces these messages in the first place.
|
|
6
|
+
|
|
7
|
+
## Finding the Application Data Root
|
|
8
|
+
|
|
9
|
+
Switch's application data (including logs) lives under a per-installation "Application Data Root"
|
|
10
|
+
folder, which is configurable and does not always sit at its default location. Resolve it in order:
|
|
11
|
+
|
|
12
|
+
1. Read the `SettingsFolder` value for this Switch Server installation:
|
|
13
|
+
- **macOS:** `defaults read "com.enfocus.Switch Server" SettingsFolder`, or read the plist directly
|
|
14
|
+
at `~/Library/Preferences/com.enfocus.Switch Server.plist`.
|
|
15
|
+
- **Windows:** registry key `HKEY_CURRENT_USER\Software\Enfocus\Switch Server`, value
|
|
16
|
+
`SettingsFolder`.
|
|
17
|
+
2. That value is a folder path. Inside it, open `settingsNew.xml`.
|
|
18
|
+
3. In `settingsNew.xml`, find the `<UserPreference QSettingsKey="ApplicationData/ApplicationDataRoot">`
|
|
19
|
+
element — its text content is the actual configured data root path.
|
|
20
|
+
|
|
21
|
+
Default data root if never changed:
|
|
22
|
+
- **macOS:** `~/Library/Application Support/Enfocus/Switch Server`
|
|
23
|
+
- **Windows:** `%APPDATA%\Enfocus\Switch Server`
|
|
24
|
+
|
|
25
|
+
Don't assume the default — always resolve it via `SettingsFolder` → `settingsNew.xml`, since it can
|
|
26
|
+
be moved to an arbitrary location (via the Application Data Root Tool).
|
|
27
|
+
|
|
28
|
+
## The log database
|
|
29
|
+
|
|
30
|
+
`<data root>/logs/ServerLogs.db3` is a SQLite database and is not itself independently configurable
|
|
31
|
+
— it's always at this fixed path under the data root. It can be queried directly and read-only with
|
|
32
|
+
any SQLite client (e.g. `sqlite3 ServerLogs.db3 "SELECT ..."`) while Switch is running.
|
|
33
|
+
|
|
34
|
+
Relevant columns on the `logmessages` table:
|
|
35
|
+
|
|
36
|
+
| Column | Meaning |
|
|
37
|
+
|---|---|
|
|
38
|
+
| `itemtime` | ISO-8601 timestamp of the log entry |
|
|
39
|
+
| `type` | Log level: `debug`, `info`, `warning`, `error`, `assert` |
|
|
40
|
+
| `module` | The flow element's type/display name |
|
|
41
|
+
| `flow` | The flow name |
|
|
42
|
+
| `element` | The flow element instance's name in that flow |
|
|
43
|
+
| `operation` | The message template, with unresolved `%1`–`%9` placeholders |
|
|
44
|
+
| `arg1`–`arg9` | Substitution values for the placeholders in `operation` |
|
|
45
|
+
| `ticket`, `file` | Job identifier / filename, when the message relates to a specific job |
|
|
46
|
+
|
|
47
|
+
The message text is stored **unresolved** — reconstruct the human-readable message yourself by
|
|
48
|
+
replacing each `%N` in `operation` with the corresponding `argN`. To find a specific script's log
|
|
49
|
+
output, filter on `flow` and `element` (matching the script's flow element name in the canvas) and
|
|
50
|
+
order by `itemtime`.
|
|
51
|
+
|
|
52
|
+
Example: find recent messages for a specific flow element, most recent first —
|
|
53
|
+
|
|
54
|
+
```sql
|
|
55
|
+
SELECT itemtime, type, operation, arg1, arg2, arg3
|
|
56
|
+
FROM logmessages
|
|
57
|
+
WHERE flow = 'MyFlow' AND element = 'MyScriptElement'
|
|
58
|
+
ORDER BY itemtime DESC
|
|
59
|
+
LIMIT 50;
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Retention and the debug-level gate
|
|
63
|
+
|
|
64
|
+
- Log messages are pruned after `Logging/KeepLogMessagesForHours` (12 hours by default,
|
|
65
|
+
`settingsNew.xml`) — don't expect old entries to still be present.
|
|
66
|
+
- `LogLevel.Debug` messages from `job.log()`/`flowElement.log()` only reach `ServerLogs.db3` at all
|
|
67
|
+
if the Switch preference `Logging/LogDebugMessages` ("Log debug messages") is enabled — it is
|
|
68
|
+
**off by default** in a typical install and is meant to be turned on when actively debugging. A
|
|
69
|
+
script's `Debug`-level output can be completely absent from the log database even though the
|
|
70
|
+
script correctly calls `job.log(LogLevel.Debug, ...)`; check this preference before concluding a
|
|
71
|
+
script isn't logging.
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# Property Editors
|
|
2
|
+
|
|
3
|
+
Property editors determine how a user enters a value in Switch Designer, and what string is
|
|
4
|
+
returned by `getPropertyStringValue()` at runtime. They're set in the XML declaration via the
|
|
5
|
+
`Editor` and `Type` attributes on a custom property — see
|
|
6
|
+
[api-script-declaration.md](api-script-declaration.md) for the surrounding attribute grammar.
|
|
7
|
+
|
|
8
|
+
All property values are returned as `string` or `string[]`. `getPropertyType()` returns one of a
|
|
9
|
+
fixed set of `PropertyType` values (`literal`, `string`, `number`, `date`, `boolean`, `filepath`,
|
|
10
|
+
`folderpath`, `regex`, `oauthtoken`, etc. — see [api-enums.md](api-enums.md)) describing what kind of
|
|
11
|
+
value is actually present; `literal` is only one of these, not a binary "literal vs. user-entered"
|
|
12
|
+
flag. Use it before interpreting the returned string, e.g. to check for a literal editor's constant
|
|
13
|
+
before comparing it.
|
|
14
|
+
|
|
15
|
+
`Editor` is a `;`-joined list: the inline editor's token (if any) first, then modal editor tokens in
|
|
16
|
+
the order they're offered. `Type` follows from which editor(s) are chosen — see the tables below and
|
|
17
|
+
[the Type reference](api-script-declaration.md#type-reference).
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Inline editors
|
|
22
|
+
|
|
23
|
+
The inline editor slot is always the single token `inline` in `Editor` — what differs is `Type`.
|
|
24
|
+
|
|
25
|
+
| Editor / Type | Resulting `getPropertyStringValue()` value |
|
|
26
|
+
|---|---|
|
|
27
|
+
| `Editor="inline" Type="string"` | Single-line text as entered |
|
|
28
|
+
| `Editor="inline" Type="password"` | Text as entered (displayed masked) |
|
|
29
|
+
| `Editor="inline" Type="number"` | Integer as a string |
|
|
30
|
+
| `Editor="inline" Type="rational"` | Decimal as a string |
|
|
31
|
+
| `Editor="inline" Type="time"` | `"hh:mm"` (zero-padded, e.g. `"09:05"`) |
|
|
32
|
+
| `Editor="inline" Type="date"` | Date as entered |
|
|
33
|
+
| `Editor="inline" Type="datetime"` | Date and time as entered |
|
|
34
|
+
| `Editor="inline" Type="bool"` | `"No"` or `"Yes"` |
|
|
35
|
+
| `Editor="inline" Type="enum:Item1;Item2;..."` | The selected item string (one of the declared values) |
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## Modal editors
|
|
40
|
+
|
|
41
|
+
Additional editor tokens appended after `inline` (or used alone, with no inline component). These
|
|
42
|
+
open a dialog when the user clicks the editor button. All have `Type="string"` unless noted.
|
|
43
|
+
|
|
44
|
+
| `Editor` token | Resulting `getPropertyStringValue()` value |
|
|
45
|
+
|---|---|
|
|
46
|
+
| `choosefile` | Absolute file path as a string |
|
|
47
|
+
| `choosefolder` | Absolute folder path as a string |
|
|
48
|
+
| `regexp` | Regular expression string as entered |
|
|
49
|
+
| `filetype` | Filename pattern(s) for the selected file type |
|
|
50
|
+
| `types` (`Type="filefilter"`) | `string[]` — one pattern string per selected file type |
|
|
51
|
+
| `askplugin` | String value selected from the library dialog (calls `getLibraryForProperty`) |
|
|
52
|
+
| `askplugin2` | `string[]` — one string per selected library item (calls `getLibraryForMultipleProperty`) |
|
|
53
|
+
| `description` | Multi-line text as a single string (may include newlines) |
|
|
54
|
+
| `scriptexp` | Result of script expression evaluated in job context, as a string |
|
|
55
|
+
| `sltextwithvar` | Single-line text with variables substituted, as a string |
|
|
56
|
+
| `mltextwithvar` | Multi-line text with variables substituted, as a string |
|
|
57
|
+
| `conditionwithvar` | `"true"` or `"false"` after evaluating condition with variables |
|
|
58
|
+
| `filepatterns` | `string[]` — one pattern string per entry |
|
|
59
|
+
| `folderpatterns` | `string[]` — one pattern string per entry |
|
|
60
|
+
| `stringlist` (`Type="stringlist"`) | `string[]` — one string per line |
|
|
61
|
+
| `oauth` | OAuth 2.0 access token string, or `""` if not yet authorized — see [ExtraProperties](api-script-declaration.md#extraproperties) |
|
|
62
|
+
| `external`, `external2`, … | File path to a property set edited by an external companion app |
|
|
63
|
+
|
|
64
|
+
> Duplicate tokens in `Editor` are meaningless — each modal editor can only be offered once, except
|
|
65
|
+
> `external` which can appear multiple times with numeric suffixes (`external`, `external2`, `external3`, …).
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## Literal editors
|
|
70
|
+
|
|
71
|
+
A literal editor makes a property return a **fixed string** regardless of user input. Combine it
|
|
72
|
+
with another editor in the `Editor` chain (e.g. `Editor="default;choosefolder"`) to offer the
|
|
73
|
+
constant as one of the choices; all literal tokens have `Type="string"`.
|
|
74
|
+
|
|
75
|
+
Always call `getPropertyType()` first to check whether the value is a literal before comparing the
|
|
76
|
+
string.
|
|
77
|
+
|
|
78
|
+
| `Editor` token | `getPropertyStringValue()` returns |
|
|
79
|
+
|---|---|
|
|
80
|
+
| `default` | `"Default"` |
|
|
81
|
+
| `none` | `""` (empty string) |
|
|
82
|
+
| `automatic` | `"Automatic"` |
|
|
83
|
+
| `nofiles` | `"No Files"` |
|
|
84
|
+
| `allfiles` | `"All Files"` |
|
|
85
|
+
| `allotherfiles` | `"All Other Files"` |
|
|
86
|
+
| `nofolders` | `"No Folders"` |
|
|
87
|
+
| `allfolders` | `"All Folders"` |
|
|
88
|
+
| `allotherfolders` | `"All Other Folders"` |
|
|
89
|
+
| `next` | `"Next"` |
|
|
90
|
+
| `current` | `"Current"` |
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## Extra XML attributes for certain editors
|
|
95
|
+
|
|
96
|
+
**Dropdown (`Type="enum:..."`)** — items are embedded in the `Type` value itself:
|
|
97
|
+
`Type="enum:Option A;Option B;Option C"`.
|
|
98
|
+
|
|
99
|
+
**`askplugin` / `askplugin2`** — add a `SelectFromLibMes` (single) or `SelectManyFromLibMes` (multi)
|
|
100
|
+
attribute to the property element to set a custom dialog message.
|
|
101
|
+
|
|
102
|
+
**`stringlist`** — add a `StringListMes` attribute to show a custom message in the editor dialog.
|
|
103
|
+
|
|
104
|
+
**`choosefile`** — add `Opaque="true"` to include the referenced file as an opaque payload when a
|
|
105
|
+
flow is exported.
|
|
106
|
+
|
|
107
|
+
**`external`** (and `external2`, …) — add matching-suffixed `ExternalEditorName`,
|
|
108
|
+
`ExternalValueOverlay`, `ExternalApplication`, `ExternalArgForNew`, `ExternalArgForEdit`, and
|
|
109
|
+
`ExternalFileFormat` (`Custom` or `Switch`) attributes.
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
## Common practices
|
|
114
|
+
|
|
115
|
+
The following are recommended conventions for pairing property kinds with editor chains, not
|
|
116
|
+
restrictions enforced by Switch — a script writer can combine editors differently if asked to.
|
|
117
|
+
|
|
118
|
+
| Property kind | Recommended `Editor` chain | Why |
|
|
119
|
+
|---|---|---|
|
|
120
|
+
| Secret (password, token, API key) | `password` only | Masks the value and avoids leaking it via logs or variable inspection if a var/expression editor were combined. |
|
|
121
|
+
| Scalar job-specific value (job ID, copy count, company name, date, etc.) | `inline` (`Type` matching the value) + `sltextwithvar` + `scriptexp` | Hard-coded default, plus dynamic substitution and full expression evaluation. |
|
|
122
|
+
| Array/list job-specific value | `stringlist` + `mltextwithvar` + `scriptexp` | One item per line; keeps both the hard-coded and dynamic paths returning a `string[]`. |
|
|
123
|
+
| Structured blob text (XML/JSON/HTML, email body) | `description` + `mltextwithvar` + `scriptexp` | Free multi-line text suits a single blob of content better than a line-per-item list. |
|
|
124
|
+
| Boolean used as a `Dependency` master | `inline` only | Other properties' visibility depends on a value known at design time. |
|
|
125
|
+
| Boolean consumed directly by script logic (not a `Dependency` master) | `inline` + `conditionwithvar` + `scriptexp` | Safe to toggle dynamically since nothing else depends on it for visibility. Also covers booleans that are themselves a `Dependency` *dependent* (not a master). |
|
|
126
|
+
| Enum (`Type="enum:..."`) | `inline` only, always | No editor guarantees a var/expression result matches one of the declared items. |
|
|
127
|
+
| File/folder path that must exist for the app to work | `choosefile`/`choosefolder` only | Switch validates the picked path exists at flow start. |
|
|
128
|
+
| File/folder path that legitimately varies per job | `choosefile`/`choosefolder` + `sltextwithvar` + `scriptexp` | Dynamic values only resolve when read during `jobArrived` — see the "Workaround for deferred processing" note in [api-flow-element.md](api-flow-element.md#properties) if the path is needed later in the flow. |
|
|
129
|
+
|
|
130
|
+
`regexp`, `filetype`/`types`, `askplugin`/`askplugin2`, `oauth`, and `external` can technically be
|
|
131
|
+
combined with `sltextwithvar`/`scriptexp` too, but this is uncommon in practice and not covered by a
|
|
132
|
+
dedicated row above.
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
## Notes
|
|
137
|
+
|
|
138
|
+
- **OAuth 2.0**: endpoint, client ID/secret, scope, and redirect ports are configured per-property
|
|
139
|
+
in a matching `<ExtraProperties>` child element (see
|
|
140
|
+
[api-script-declaration.md § ExtraProperties](api-script-declaration.md#extraproperties)), not in
|
|
141
|
+
`Editor`/`Type`. The script receives a ready-to-use access token string via
|
|
142
|
+
`getPropertyStringValue()`.
|
|
143
|
+
- **External editor**: requires a separate companion application. The script receives a file path to
|
|
144
|
+
the property set file. Entry point `findExternalEditorPath` resolves the editor executable path.
|