fms-odata-js 0.3.0 → 0.4.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/CHANGELOG.md +96 -1
- package/README.md +102 -28
- package/dist/client.d.ts +46 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/fms-odata.esm.js +469 -37
- package/dist/fms-odata.esm.min.js +4 -4
- package/dist/fms-odata.iife.min.js +3 -3
- package/dist/http.d.ts +3 -3
- package/dist/http.d.ts.map +1 -1
- package/dist/index.d.ts +8 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/query.d.ts +3 -3
- package/dist/schema.d.ts +103 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/scripts.d.ts +10 -6
- package/dist/scripts.d.ts.map +1 -1
- package/dist/webhooks.d.ts +98 -0
- package/dist/webhooks.d.ts.map +1 -0
- package/package.json +2 -2
- package/src/client.ts +118 -1
- package/src/http.ts +6 -6
- package/src/index.ts +18 -1
- package/src/query.ts +4 -4
- package/src/schema.ts +236 -0
- package/src/scripts.ts +28 -11
- package/src/webhooks.ts +203 -0
package/src/schema.ts
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Schema modification (DDL) for FileMaker Server via the OData system tables.
|
|
3
|
+
*
|
|
4
|
+
* FMS exposes DDL through two system tables:
|
|
5
|
+
*
|
|
6
|
+
* FileMaker_Tables — create tables, add fields, delete tables/fields
|
|
7
|
+
* FileMaker_Indexes — create and delete field indexes
|
|
8
|
+
*
|
|
9
|
+
* All operations require a FileMaker account with full access (schema
|
|
10
|
+
* modification) privileges in the target file.
|
|
11
|
+
*
|
|
12
|
+
* Destructive operations (`deleteTable`, `deleteField`) require an explicit
|
|
13
|
+
* `confirm: true` option to prevent accidental data loss, per the spec's
|
|
14
|
+
* security guidance (docs/10-schema-modification.md).
|
|
15
|
+
*
|
|
16
|
+
* @see https://github.com/fsans/fms-odata-spec/blob/main/docs/10-schema-modification.md
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import {
|
|
20
|
+
FIELD_TYPES,
|
|
21
|
+
parseFieldType,
|
|
22
|
+
type CreateTableParams,
|
|
23
|
+
type AddFieldsParams,
|
|
24
|
+
type FMFieldDefinition,
|
|
25
|
+
} from '@fms-odata/spec-ts'
|
|
26
|
+
import type { FMSOData } from './client.js'
|
|
27
|
+
import { executeJson } from './http.js'
|
|
28
|
+
import type { RequestOptions } from './types.js'
|
|
29
|
+
import { encodePathSegment } from './url.js'
|
|
30
|
+
|
|
31
|
+
/** Options accepted by schema operations. */
|
|
32
|
+
export interface SchemaOptions extends RequestOptions {}
|
|
33
|
+
|
|
34
|
+
/** Options for destructive schema operations (deleteTable, deleteField). */
|
|
35
|
+
export interface DeleteSchemaOptions extends SchemaOptions {
|
|
36
|
+
/**
|
|
37
|
+
* Must be `true` to proceed. Destructive DDL operations permanently destroy
|
|
38
|
+
* data with no undo. This guard prevents accidental invocation.
|
|
39
|
+
*/
|
|
40
|
+
confirm: true
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Validate that every field definition uses a supported base type. */
|
|
44
|
+
function validateFieldDefinitions(fields: FMFieldDefinition[]): void {
|
|
45
|
+
for (const f of fields) {
|
|
46
|
+
if (!f.name) throw new TypeError('SchemaEditor: field `name` is required')
|
|
47
|
+
if (!f.type) throw new TypeError('SchemaEditor: field `type` is required')
|
|
48
|
+
const { baseType } = parseFieldType(f.type)
|
|
49
|
+
const upper = baseType.toUpperCase()
|
|
50
|
+
if (!FIELD_TYPES.includes(upper as (typeof FIELD_TYPES)[number])) {
|
|
51
|
+
throw new TypeError(
|
|
52
|
+
`SchemaEditor: unsupported field type "${f.type}" (base: "${baseType}"). ` +
|
|
53
|
+
`Supported: ${FIELD_TYPES.join(', ')}`,
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Schema editor for FileMaker DDL operations.
|
|
61
|
+
*
|
|
62
|
+
* Obtain an instance via `db.schema()` or use the convenience methods on
|
|
63
|
+
* `FMSOData` (`db.createTable`, `db.addFields`, etc.).
|
|
64
|
+
*/
|
|
65
|
+
export class SchemaEditor {
|
|
66
|
+
/** @internal */ readonly _client: FMSOData
|
|
67
|
+
|
|
68
|
+
constructor(client: FMSOData) {
|
|
69
|
+
this._client = client
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Create a new table with the given field definitions.
|
|
74
|
+
*
|
|
75
|
+
* ```ts
|
|
76
|
+
* await db.schema().createTable({
|
|
77
|
+
* tableName: 'Company',
|
|
78
|
+
* fields: [
|
|
79
|
+
* { name: 'Company ID', type: 'int', primary: true },
|
|
80
|
+
* { name: 'Company Name', type: 'varchar(100)', nullable: false },
|
|
81
|
+
* ],
|
|
82
|
+
* })
|
|
83
|
+
* ```
|
|
84
|
+
*/
|
|
85
|
+
async createTable(
|
|
86
|
+
params: CreateTableParams,
|
|
87
|
+
opts: SchemaOptions = {},
|
|
88
|
+
): Promise<unknown> {
|
|
89
|
+
if (!params.tableName) throw new TypeError('SchemaEditor: `tableName` is required')
|
|
90
|
+
if (!params.fields?.length) throw new TypeError('SchemaEditor: `fields` must be a non-empty array')
|
|
91
|
+
validateFieldDefinitions(params.fields)
|
|
92
|
+
|
|
93
|
+
const url = `${this._client.baseUrl}/FileMaker_Tables`
|
|
94
|
+
return executeJson<unknown>(this._client._ctx, url, {
|
|
95
|
+
method: 'POST',
|
|
96
|
+
headers: { 'Content-Type': 'application/json' },
|
|
97
|
+
body: JSON.stringify(params),
|
|
98
|
+
accept: 'json',
|
|
99
|
+
...(opts.signal ? { signal: opts.signal } : {}),
|
|
100
|
+
})
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Add fields to an existing table.
|
|
105
|
+
*
|
|
106
|
+
* ```ts
|
|
107
|
+
* await db.schema().addFields({
|
|
108
|
+
* tableName: 'Company',
|
|
109
|
+
* fields: [{ name: 'Phone', type: 'varchar(30)' }],
|
|
110
|
+
* })
|
|
111
|
+
* ```
|
|
112
|
+
*/
|
|
113
|
+
async addFields(
|
|
114
|
+
params: AddFieldsParams,
|
|
115
|
+
opts: SchemaOptions = {},
|
|
116
|
+
): Promise<unknown> {
|
|
117
|
+
if (!params.tableName) throw new TypeError('SchemaEditor: `tableName` is required')
|
|
118
|
+
if (!params.fields?.length) throw new TypeError('SchemaEditor: `fields` must be a non-empty array')
|
|
119
|
+
validateFieldDefinitions(params.fields)
|
|
120
|
+
|
|
121
|
+
const url = `${this._client.baseUrl}/FileMaker_Tables/${encodePathSegment(params.tableName)}`
|
|
122
|
+
return executeJson<unknown>(this._client._ctx, url, {
|
|
123
|
+
method: 'PATCH',
|
|
124
|
+
headers: { 'Content-Type': 'application/json' },
|
|
125
|
+
body: JSON.stringify({ fields: params.fields }),
|
|
126
|
+
accept: 'json',
|
|
127
|
+
...(opts.signal ? { signal: opts.signal } : {}),
|
|
128
|
+
})
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Delete a table and ALL its records. Irreversible.
|
|
133
|
+
*
|
|
134
|
+
* Requires `opts.confirm === true` as a safety guard.
|
|
135
|
+
*
|
|
136
|
+
* ```ts
|
|
137
|
+
* await db.schema().deleteTable('OldTable', { confirm: true })
|
|
138
|
+
* ```
|
|
139
|
+
*/
|
|
140
|
+
async deleteTable(
|
|
141
|
+
tableName: string,
|
|
142
|
+
opts: DeleteSchemaOptions,
|
|
143
|
+
): Promise<void> {
|
|
144
|
+
if (!tableName) throw new TypeError('SchemaEditor: `tableName` is required')
|
|
145
|
+
if (opts.confirm !== true) {
|
|
146
|
+
throw new TypeError(
|
|
147
|
+
`SchemaEditor: deleteTable("${tableName}") requires { confirm: true } — ` +
|
|
148
|
+
'this operation permanently destroys the table and all its records.',
|
|
149
|
+
)
|
|
150
|
+
}
|
|
151
|
+
const url = `${this._client.baseUrl}/FileMaker_Tables/${encodePathSegment(tableName)}`
|
|
152
|
+
await executeJson<void>(this._client._ctx, url, {
|
|
153
|
+
method: 'DELETE',
|
|
154
|
+
accept: 'json',
|
|
155
|
+
...(opts.signal ? { signal: opts.signal } : {}),
|
|
156
|
+
})
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Delete a field from a table. Irreversible.
|
|
161
|
+
*
|
|
162
|
+
* Requires `opts.confirm === true` as a safety guard.
|
|
163
|
+
*
|
|
164
|
+
* ```ts
|
|
165
|
+
* await db.schema().deleteField('Company', 'OldField', { confirm: true })
|
|
166
|
+
* ```
|
|
167
|
+
*/
|
|
168
|
+
async deleteField(
|
|
169
|
+
tableName: string,
|
|
170
|
+
fieldName: string,
|
|
171
|
+
opts: DeleteSchemaOptions,
|
|
172
|
+
): Promise<void> {
|
|
173
|
+
if (!tableName) throw new TypeError('SchemaEditor: `tableName` is required')
|
|
174
|
+
if (!fieldName) throw new TypeError('SchemaEditor: `fieldName` is required')
|
|
175
|
+
if (opts.confirm !== true) {
|
|
176
|
+
throw new TypeError(
|
|
177
|
+
`SchemaEditor: deleteField("${tableName}", "${fieldName}") requires { confirm: true } — ` +
|
|
178
|
+
'this operation permanently destroys the field and all its data.',
|
|
179
|
+
)
|
|
180
|
+
}
|
|
181
|
+
const url = `${this._client.baseUrl}/FileMaker_Tables/${encodePathSegment(tableName)}/${encodePathSegment(fieldName)}`
|
|
182
|
+
await executeJson<void>(this._client._ctx, url, {
|
|
183
|
+
method: 'DELETE',
|
|
184
|
+
accept: 'json',
|
|
185
|
+
...(opts.signal ? { signal: opts.signal } : {}),
|
|
186
|
+
})
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Create an index on a field.
|
|
191
|
+
*
|
|
192
|
+
* ```ts
|
|
193
|
+
* await db.schema().createIndex('Company', 'Company Name')
|
|
194
|
+
* ```
|
|
195
|
+
*/
|
|
196
|
+
async createIndex(
|
|
197
|
+
tableName: string,
|
|
198
|
+
fieldName: string,
|
|
199
|
+
opts: SchemaOptions = {},
|
|
200
|
+
): Promise<unknown> {
|
|
201
|
+
if (!tableName) throw new TypeError('SchemaEditor: `tableName` is required')
|
|
202
|
+
if (!fieldName) throw new TypeError('SchemaEditor: `fieldName` is required')
|
|
203
|
+
|
|
204
|
+
const url = `${this._client.baseUrl}/FileMaker_Indexes/${encodePathSegment(tableName)}`
|
|
205
|
+
return executeJson<unknown>(this._client._ctx, url, {
|
|
206
|
+
method: 'POST',
|
|
207
|
+
headers: { 'Content-Type': 'application/json' },
|
|
208
|
+
body: JSON.stringify({ indexName: fieldName }),
|
|
209
|
+
accept: 'json',
|
|
210
|
+
...(opts.signal ? { signal: opts.signal } : {}),
|
|
211
|
+
})
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Delete an index from a field.
|
|
216
|
+
*
|
|
217
|
+
* ```ts
|
|
218
|
+
* await db.schema().deleteIndex('Company', 'Company Name')
|
|
219
|
+
* ```
|
|
220
|
+
*/
|
|
221
|
+
async deleteIndex(
|
|
222
|
+
tableName: string,
|
|
223
|
+
fieldName: string,
|
|
224
|
+
opts: SchemaOptions = {},
|
|
225
|
+
): Promise<void> {
|
|
226
|
+
if (!tableName) throw new TypeError('SchemaEditor: `tableName` is required')
|
|
227
|
+
if (!fieldName) throw new TypeError('SchemaEditor: `fieldName` is required')
|
|
228
|
+
|
|
229
|
+
const url = `${this._client.baseUrl}/FileMaker_Indexes/${encodePathSegment(tableName)}/${encodePathSegment(fieldName)}`
|
|
230
|
+
await executeJson<void>(this._client._ctx, url, {
|
|
231
|
+
method: 'DELETE',
|
|
232
|
+
accept: 'json',
|
|
233
|
+
...(opts.signal ? { signal: opts.signal } : {}),
|
|
234
|
+
})
|
|
235
|
+
}
|
|
236
|
+
}
|
package/src/scripts.ts
CHANGED
|
@@ -15,9 +15,12 @@
|
|
|
15
15
|
* POST /<db>/<EntitySet>/Script.FMSID:<id> // entity-set context
|
|
16
16
|
* POST /<db>/<EntitySet>(<key>)/Script.FMSID:<id> // single-record context
|
|
17
17
|
*
|
|
18
|
-
* The optional parameter is sent as `{ "scriptParameterValue":
|
|
19
|
-
* response envelope
|
|
20
|
-
*
|
|
18
|
+
* The optional parameter is sent as `{ "scriptParameterValue": ... }` (string,
|
|
19
|
+
* number or JSON object). The response envelope depends on the FMS version:
|
|
20
|
+
* older servers reply `{ "scriptResult": "...", "scriptError": "0" }`, while
|
|
21
|
+
* FMS v26+ nests the result as
|
|
22
|
+
* `{ "scriptResult": { "code": 0, "resultParameter": "...", "message": "..." } }`.
|
|
23
|
+
* A non-zero error code becomes an `FMScriptError`.
|
|
21
24
|
*
|
|
22
25
|
* @see https://github.com/fsans/fms-odata-spec/blob/main/docs/06-scripts.md
|
|
23
26
|
*/
|
|
@@ -41,10 +44,11 @@ export type ScriptIdentifier =
|
|
|
41
44
|
export interface ScriptOptions extends RequestOptions {
|
|
42
45
|
/**
|
|
43
46
|
* Optional script parameter. Serialized to the FMS `scriptParameterValue`
|
|
44
|
-
* field in the request body.
|
|
45
|
-
*
|
|
47
|
+
* field in the request body. FMS accepts a string, a number or a JSON
|
|
48
|
+
* object. If omitted, the body is empty and the script runs with no
|
|
49
|
+
* parameter (FileMaker's `Get(ScriptParameter)` returns empty).
|
|
46
50
|
*/
|
|
47
|
-
parameter?: string
|
|
51
|
+
parameter?: string | number | object
|
|
48
52
|
}
|
|
49
53
|
|
|
50
54
|
/**
|
|
@@ -183,12 +187,23 @@ export function parseScriptEnvelope(
|
|
|
183
187
|
const rawResult = envelope.scriptResult
|
|
184
188
|
let scriptResult: string | undefined
|
|
185
189
|
let scriptError: string
|
|
190
|
+
let fmsMessage: string | undefined
|
|
186
191
|
|
|
187
|
-
if (
|
|
188
|
-
|
|
189
|
-
|
|
192
|
+
if (
|
|
193
|
+
rawResult !== null &&
|
|
194
|
+
typeof rawResult === 'object' &&
|
|
195
|
+
('code' in rawResult || 'resultParameter' in rawResult)
|
|
196
|
+
) {
|
|
197
|
+
// Nested envelope (FMS v26+): code + resultParameter (+ message) inside scriptResult
|
|
198
|
+
const nested = rawResult as { code?: unknown; resultParameter?: unknown; message?: unknown }
|
|
190
199
|
scriptError = nested.code !== undefined ? String(nested.code) : '0'
|
|
191
|
-
scriptResult =
|
|
200
|
+
scriptResult =
|
|
201
|
+
nested.resultParameter !== undefined
|
|
202
|
+
? typeof nested.resultParameter === 'string'
|
|
203
|
+
? nested.resultParameter
|
|
204
|
+
: JSON.stringify(nested.resultParameter)
|
|
205
|
+
: undefined
|
|
206
|
+
if (typeof nested.message === 'string') fmsMessage = nested.message
|
|
192
207
|
} else {
|
|
193
208
|
// Flat envelope (older FMS): scriptResult is a string, scriptError is a sibling
|
|
194
209
|
scriptError = envelope.scriptError !== undefined ? String(envelope.scriptError) : '0'
|
|
@@ -202,7 +217,9 @@ export function parseScriptEnvelope(
|
|
|
202
217
|
|
|
203
218
|
if (scriptError !== '0') {
|
|
204
219
|
throw new FMScriptError(
|
|
205
|
-
|
|
220
|
+
fmsMessage
|
|
221
|
+
? `FileMaker script error ${scriptError}: ${fmsMessage}`
|
|
222
|
+
: `FileMaker script error ${scriptError}`,
|
|
206
223
|
{
|
|
207
224
|
status: 200,
|
|
208
225
|
scriptError,
|
package/src/webhooks.ts
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Webhook management for FileMaker Server OData.
|
|
3
|
+
*
|
|
4
|
+
* FMS exposes webhook management through five endpoints following the
|
|
5
|
+
* standard OData function-call shape: read endpoints use GET, and action
|
|
6
|
+
* endpoints use POST with the `<id>` passed as a function argument in the
|
|
7
|
+
* URL path.
|
|
8
|
+
*
|
|
9
|
+
* GET /<db>/Webhook.GetAll — list all webhooks
|
|
10
|
+
* GET /<db>/Webhook.Get(<id>) — get one webhook by id
|
|
11
|
+
* POST /<db>/Webhook.Add — create a webhook (config in JSON body)
|
|
12
|
+
* POST /<db>/Webhook.Delete(<id>) — delete a webhook (id in URL path)
|
|
13
|
+
* POST /<db>/Webhook.Invoke(<id>) — manually trigger a webhook (id in URL path)
|
|
14
|
+
*
|
|
15
|
+
* Webhooks require FileMaker Server 2025+ (v22). Use
|
|
16
|
+
* `db.hasFeature('webhooks')` to check before calling.
|
|
17
|
+
*
|
|
18
|
+
* @see https://github.com/fsans/fms-odata-spec/blob/main/docs/09-webhooks.md
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import type { WebhookCreateParams, WebhookData } from '@fms-odata/spec-ts'
|
|
22
|
+
import type { FMSOData } from './client.js'
|
|
23
|
+
import { executeJson } from './http.js'
|
|
24
|
+
import type { RequestOptions } from './types.js'
|
|
25
|
+
import { encodePathSegment } from './url.js'
|
|
26
|
+
|
|
27
|
+
/** Options accepted by webhook operations. */
|
|
28
|
+
export interface WebhookOptions extends RequestOptions {}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Normalize create params: merge legacy `headers` into `endpointHeaders`
|
|
32
|
+
* (spec says `headers` is a legacy alias for `endpointHeaders`).
|
|
33
|
+
*/
|
|
34
|
+
function normalizeCreateParams(params: WebhookCreateParams): Record<string, unknown> {
|
|
35
|
+
const out: Record<string, unknown> = {
|
|
36
|
+
webhook: params.webhook,
|
|
37
|
+
tableName: params.tableName,
|
|
38
|
+
}
|
|
39
|
+
if (params.endpointHeaders) out.endpointHeaders = params.endpointHeaders
|
|
40
|
+
if (params.headers && !params.endpointHeaders) out.endpointHeaders = params.headers
|
|
41
|
+
if (params.queryHeaders) out.queryHeaders = params.queryHeaders
|
|
42
|
+
if (params.notifySchemaChanges !== undefined) out.notifySchemaChanges = params.notifySchemaChanges
|
|
43
|
+
if (params.select) out.select = params.select
|
|
44
|
+
if (params.filter) out.filter = params.filter
|
|
45
|
+
if (params.maxFailedAttempts !== undefined) out.maxFailedAttempts = params.maxFailedAttempts
|
|
46
|
+
return out
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Extract the webhook ID from a `Webhook.Add` response.
|
|
51
|
+
*
|
|
52
|
+
* FMS returns `{ webhookResult: { webhookID: N } }`. Older or alternate
|
|
53
|
+
* shapes (`{ webhookID: N }`, `{ id: N }`) are accepted as fallbacks.
|
|
54
|
+
*/
|
|
55
|
+
function extractWebhookId(data: unknown): string | undefined {
|
|
56
|
+
if (!data || typeof data !== 'object') return undefined
|
|
57
|
+
const obj = data as Record<string, unknown>
|
|
58
|
+
const fromResult = obj.webhookResult as Record<string, unknown> | undefined
|
|
59
|
+
const id = fromResult?.webhookID ?? obj.webhookID ?? obj.id
|
|
60
|
+
return id !== undefined ? String(id) : undefined
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Webhook manager for FileMaker Server webhook CRUD.
|
|
65
|
+
*
|
|
66
|
+
* Obtain an instance via `db.webhooks()` or use the convenience methods on
|
|
67
|
+
* `FMSOData` (`db.createWebhook`, `db.removeWebhook`, etc.).
|
|
68
|
+
*
|
|
69
|
+
* Requires FileMaker Server 2025+ (v22).
|
|
70
|
+
*/
|
|
71
|
+
export class WebhookManager {
|
|
72
|
+
/** @internal */ readonly _client: FMSOData
|
|
73
|
+
|
|
74
|
+
constructor(client: FMSOData) {
|
|
75
|
+
this._client = client
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Build the URL for a webhook operation. */
|
|
79
|
+
private _url(operation: 'Add' | 'Delete' | 'Get' | 'GetAll' | 'Invoke', id?: string | number): string {
|
|
80
|
+
const base = `${this._client.baseUrl}/Webhook.${operation}`
|
|
81
|
+
return id !== undefined ? `${base}(${encodePathSegment(String(id))})` : base
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Create a webhook.
|
|
86
|
+
*
|
|
87
|
+
* ```ts
|
|
88
|
+
* const { id } = await db.webhooks().create({
|
|
89
|
+
* webhook: 'https://my.example.com:8080/webhook',
|
|
90
|
+
* tableName: 'contact',
|
|
91
|
+
* select: 'id,first_name',
|
|
92
|
+
* filter: "status eq 'active'",
|
|
93
|
+
* notifySchemaChanges: true,
|
|
94
|
+
* })
|
|
95
|
+
* ```
|
|
96
|
+
*
|
|
97
|
+
* @returns An object with the server-generated `id` of the new webhook.
|
|
98
|
+
*/
|
|
99
|
+
async create(
|
|
100
|
+
params: WebhookCreateParams,
|
|
101
|
+
opts: WebhookOptions = {},
|
|
102
|
+
): Promise<{ id: string }> {
|
|
103
|
+
if (!params.webhook) throw new TypeError('WebhookManager: `webhook` URL is required')
|
|
104
|
+
if (!params.tableName) throw new TypeError('WebhookManager: `tableName` is required')
|
|
105
|
+
|
|
106
|
+
const body = JSON.stringify(normalizeCreateParams(params))
|
|
107
|
+
const data = await executeJson<unknown>(this._client._ctx, this._url('Add'), {
|
|
108
|
+
method: 'POST',
|
|
109
|
+
headers: { 'Content-Type': 'application/json' },
|
|
110
|
+
body,
|
|
111
|
+
accept: 'json',
|
|
112
|
+
...(opts.signal ? { signal: opts.signal } : {}),
|
|
113
|
+
})
|
|
114
|
+
const id = extractWebhookId(data)
|
|
115
|
+
if (id === undefined) {
|
|
116
|
+
throw new Error('WebhookManager: Webhook.Add did not return a webhook id')
|
|
117
|
+
}
|
|
118
|
+
return { id }
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Delete a webhook by its ID.
|
|
123
|
+
*
|
|
124
|
+
* ```ts
|
|
125
|
+
* await db.webhooks().remove('1')
|
|
126
|
+
* ```
|
|
127
|
+
*/
|
|
128
|
+
async remove(id: string | number, opts: WebhookOptions = {}): Promise<unknown> {
|
|
129
|
+
if (id === '' || id === undefined) throw new TypeError('WebhookManager: webhook `id` is required')
|
|
130
|
+
return executeJson<unknown>(this._client._ctx, this._url('Delete', id), {
|
|
131
|
+
method: 'POST',
|
|
132
|
+
accept: 'json',
|
|
133
|
+
...(opts.signal ? { signal: opts.signal } : {}),
|
|
134
|
+
})
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Alias for {@link remove} (matches the FMS endpoint name `Webhook.Delete`). */
|
|
138
|
+
async delete(id: string | number, opts: WebhookOptions = {}): Promise<unknown> {
|
|
139
|
+
return this.remove(id, opts)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Get a specific webhook's data by ID.
|
|
144
|
+
*
|
|
145
|
+
* ```ts
|
|
146
|
+
* const data = await db.webhooks().get('1')
|
|
147
|
+
* ```
|
|
148
|
+
*/
|
|
149
|
+
async get(id: string | number, opts: WebhookOptions = {}): Promise<WebhookData | unknown> {
|
|
150
|
+
if (id === '' || id === undefined) throw new TypeError('WebhookManager: webhook `id` is required')
|
|
151
|
+
return executeJson<unknown>(this._client._ctx, this._url('Get', id), {
|
|
152
|
+
method: 'GET',
|
|
153
|
+
accept: 'json',
|
|
154
|
+
...(opts.signal ? { signal: opts.signal } : {}),
|
|
155
|
+
})
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* List all webhooks.
|
|
160
|
+
*
|
|
161
|
+
* ```ts
|
|
162
|
+
* const result = await db.webhooks().getAll()
|
|
163
|
+
* ```
|
|
164
|
+
*/
|
|
165
|
+
async getAll(opts: WebhookOptions = {}): Promise<unknown> {
|
|
166
|
+
return executeJson<unknown>(this._client._ctx, this._url('GetAll'), {
|
|
167
|
+
method: 'GET',
|
|
168
|
+
accept: 'json',
|
|
169
|
+
...(opts.signal ? { signal: opts.signal } : {}),
|
|
170
|
+
})
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Manually invoke (trigger) a webhook by ID. Useful for testing.
|
|
175
|
+
*
|
|
176
|
+
* Optionally pass `rowIDs` to target specific records; if omitted, FMS
|
|
177
|
+
* triggers the webhook for all pending records.
|
|
178
|
+
*
|
|
179
|
+
* ```ts
|
|
180
|
+
* await db.webhooks().invoke('1')
|
|
181
|
+
* await db.webhooks().invoke('1', { rowIDs: [10, 20] })
|
|
182
|
+
* ```
|
|
183
|
+
*/
|
|
184
|
+
async invoke(
|
|
185
|
+
id: string | number,
|
|
186
|
+
opts: WebhookOptions & { rowIDs?: ReadonlyArray<string | number> } = {},
|
|
187
|
+
): Promise<unknown> {
|
|
188
|
+
if (id === '' || id === undefined) throw new TypeError('WebhookManager: webhook `id` is required')
|
|
189
|
+
const { rowIDs, signal, ...rest } = opts
|
|
190
|
+
// FMS requires a JSON body for Invoke (it rejects empty bodies with a
|
|
191
|
+
// JSON syntax error). Send { rowIDs: [...] } — an empty array triggers
|
|
192
|
+
// the webhook for all pending records.
|
|
193
|
+
const body = JSON.stringify({ rowIDs: rowIDs ? [...rowIDs] : [] })
|
|
194
|
+
return executeJson<unknown>(this._client._ctx, this._url('Invoke', id), {
|
|
195
|
+
method: 'POST',
|
|
196
|
+
headers: { 'Content-Type': 'application/json' },
|
|
197
|
+
body,
|
|
198
|
+
accept: 'json',
|
|
199
|
+
...(signal ? { signal } : {}),
|
|
200
|
+
...rest,
|
|
201
|
+
})
|
|
202
|
+
}
|
|
203
|
+
}
|