@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.
@@ -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.