@proofkit/fmodata 0.1.0-alpha.0 → 0.1.0-alpha.2
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/README.md +953 -18
- package/dist/esm/client/filemaker-odata.d.ts +1 -1
- package/dist/esm/client/filemaker-odata.js +2 -2
- package/dist/esm/client/filemaker-odata.js.map +1 -1
- package/dist/esm/index.d.ts +1 -1
- package/dist/esm/index.js +2 -2
- package/package.json +1 -1
- package/src/client/filemaker-odata.ts +1 -1
- package/src/index.ts +1 -1
package/README.md
CHANGED
|
@@ -1,37 +1,972 @@
|
|
|
1
|
-
# @proofkit/fmodata
|
|
1
|
+
# @proofkit/fmodata Documentation
|
|
2
2
|
|
|
3
|
-
FileMaker OData API client
|
|
3
|
+
A strongly-typed FileMaker OData API client.
|
|
4
|
+
|
|
5
|
+
⚠️ WARNING: This library is in "alpha" status. The API is subject to change. Feedback is welcome on the [community forum](https://community.ottomatic.cloud/c/proofkit/13) or on [GitHub](https://github.com/proofgeist/proofkit/issues).
|
|
6
|
+
|
|
7
|
+
Roadmap:
|
|
8
|
+
|
|
9
|
+
- [ ] Batch operations
|
|
10
|
+
- [ ] Proper docs at proofkit.dev
|
|
11
|
+
- [ ] @proofkit/typegen integration
|
|
4
12
|
|
|
5
13
|
## Installation
|
|
6
14
|
|
|
7
15
|
```bash
|
|
8
|
-
pnpm add @proofkit/fmodata
|
|
16
|
+
pnpm add @proofkit/fmodata@alpha
|
|
9
17
|
```
|
|
10
18
|
|
|
11
|
-
##
|
|
19
|
+
## Quick Start
|
|
20
|
+
|
|
21
|
+
Here's a minimal example to get you started:
|
|
12
22
|
|
|
13
23
|
```typescript
|
|
14
|
-
import {
|
|
24
|
+
import {
|
|
25
|
+
FMServerConnection,
|
|
26
|
+
BaseTable,
|
|
27
|
+
TableOccurrence,
|
|
28
|
+
} from "@proofkit/fmodata";
|
|
29
|
+
import { z } from "zod/v4";
|
|
30
|
+
|
|
31
|
+
// 1. Create a connection to the server
|
|
32
|
+
const connection = new FMServerConnection({
|
|
33
|
+
serverUrl: "https://your-server.com",
|
|
34
|
+
auth: {
|
|
35
|
+
// OttoFMS API key
|
|
36
|
+
apiKey: "your-api-key",
|
|
37
|
+
|
|
38
|
+
// or username and password
|
|
39
|
+
// username: "admin",
|
|
40
|
+
// password: "password",
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// 2. Define your table schema
|
|
45
|
+
const usersBase = new BaseTable({
|
|
46
|
+
schema: {
|
|
47
|
+
id: z.string(),
|
|
48
|
+
username: z.string(),
|
|
49
|
+
email: z.string(),
|
|
50
|
+
active: z.boolean(),
|
|
51
|
+
},
|
|
52
|
+
idField: "id",
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// 3. Create a table occurrence
|
|
56
|
+
const usersTO = new TableOccurrence({
|
|
57
|
+
name: "users",
|
|
58
|
+
baseTable: usersBase,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// 4. Create a database instance
|
|
62
|
+
const db = connection.database("MyDatabase.fmp12", {
|
|
63
|
+
occurrences: [usersTO],
|
|
64
|
+
});
|
|
15
65
|
|
|
16
|
-
//
|
|
66
|
+
// 5. Query your data
|
|
67
|
+
const { data, error } = await db.from("users").list().execute();
|
|
68
|
+
|
|
69
|
+
if (error) {
|
|
70
|
+
console.error(error);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (data) {
|
|
75
|
+
console.log(data); // Array of users, properly typed
|
|
76
|
+
}
|
|
17
77
|
```
|
|
18
78
|
|
|
19
|
-
##
|
|
79
|
+
## Core Concepts
|
|
20
80
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
81
|
+
This library relies heavily on the builder pattern for defining your queries and operations. Most operations require a final call to `execute()` to send the request to the server. The builder pattern is designed to support batch operations in the future, allowing you to execute multiple operations in a single request as supported by the FileMaker OData API. **Note:** Batch operations are not yet supported but are planned before the production release. It's also helpful for testing the library, as you can call `getQueryString()` to get the OData query string without executing the request.
|
|
82
|
+
|
|
83
|
+
As such, there are layers to the library to help you build your queries and operations.
|
|
84
|
+
|
|
85
|
+
- `FMServerConnection` - hold server connection details and authentication
|
|
86
|
+
- `BaseTable` - defines the fields and validators for a base table
|
|
87
|
+
- `TableOccurrence` - references a base table, and other table occurrences for navigation
|
|
88
|
+
- `Database` - connects the table occurrences to the server connection
|
|
89
|
+
|
|
90
|
+
### FileMaker Server prerequisites
|
|
91
|
+
|
|
92
|
+
To use this library you need:
|
|
93
|
+
|
|
94
|
+
- OData service enabled on your FileMaker server
|
|
95
|
+
- A FileMaker account with `fmodata` privilege enabled
|
|
96
|
+
- (if using OttoFMS) a Data API key setup for your FileMaker account with OData enabled
|
|
97
|
+
|
|
98
|
+
A note on best practices:
|
|
99
|
+
|
|
100
|
+
OData relies entirely on the table occurances in the relationship graph for data access. Relationships between table occurrences are also used, but maybe not as you expect (in short, only the simplest relationships are supported). Given these constraints, it may be best for you to have a seperate FileMaker file for your OData connection, using external data sources to link to your actual data. We've found this especially helpful for larger projects that have very large graphs with lots of duplicated table occurances compared to actual base tables.
|
|
101
|
+
|
|
102
|
+
### Server Connection
|
|
103
|
+
|
|
104
|
+
The client can authenticate using username/password or API key:
|
|
105
|
+
|
|
106
|
+
```typescript
|
|
107
|
+
// Username and password authentication
|
|
108
|
+
const connection = new FMServerConnection({
|
|
109
|
+
serverUrl: "https://api.example.com",
|
|
110
|
+
auth: {
|
|
111
|
+
username: "test",
|
|
112
|
+
password: "test",
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// API key authentication
|
|
117
|
+
const connection = new FMServerConnection({
|
|
118
|
+
serverUrl: "https://api.example.com",
|
|
119
|
+
auth: {
|
|
120
|
+
apiKey: "your-api-key",
|
|
121
|
+
},
|
|
122
|
+
});
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### Schema Definitions
|
|
126
|
+
|
|
127
|
+
This library relies on a schema-first approach for good type-safety and optional runtime validation. These are absracted into BaseTable and TableOccurrence classes to match FileMaker concepts.
|
|
128
|
+
|
|
129
|
+
A `BaseTable` defines the schema for your FileMaker table using Standard Schema. These examples show zod, but you can use any other validation library that supports Standard Schema.
|
|
130
|
+
|
|
131
|
+
```typescript
|
|
132
|
+
import { z } from "zod/v4";
|
|
133
|
+
import { BaseTable } from "@proofkit/fmodata";
|
|
134
|
+
|
|
135
|
+
const contactsBase = new BaseTable({
|
|
136
|
+
schema: {
|
|
137
|
+
id: z.string(),
|
|
138
|
+
name: z.string(),
|
|
139
|
+
email: z.string(),
|
|
140
|
+
phone: z.string().optional(),
|
|
141
|
+
createdAt: z.string(),
|
|
142
|
+
},
|
|
143
|
+
idField: "id", // The primary key field
|
|
144
|
+
insertRequired: ["name", "email"], // optional: fields that are required on insert
|
|
145
|
+
updateRequired: ["email"], // optional: fields that are required on update
|
|
146
|
+
});
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
A `TableOccurrence` is the actual entry point for the OData service on the FileMaker server. It's where you can define the relations between tables and also allows you to reference the same base table multiple times with different names.
|
|
150
|
+
|
|
151
|
+
```typescript
|
|
152
|
+
import { TableOccurrence } from "@proofkit/fmodata";
|
|
153
|
+
|
|
154
|
+
const contactsTO = new TableOccurrence({
|
|
155
|
+
name: "contacts", // The table occurrence name in FileMaker
|
|
156
|
+
baseTable: contactsBase,
|
|
157
|
+
});
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
#### Default Field Selection
|
|
161
|
+
|
|
162
|
+
FileMaker will automatically return all non-container fields from a schema if you don't specify a $select parameter in your query. This library forces you to be a bit more explicit about what fields you want to return so that the types will more accurately reflect the full data you will get back. To modify this behavior, change the `defaultSelect` option when creating the `TableOccurrence`.
|
|
163
|
+
|
|
164
|
+
```typescript
|
|
165
|
+
// Option 1 (default): "schema" - Select all fields from the schema (same as "all" but more explicit)
|
|
166
|
+
const usersTO = new TableOccurrence({
|
|
167
|
+
name: "users",
|
|
168
|
+
baseTable: usersBase,
|
|
169
|
+
defaultSelect: "schema", // a $select parameter will be always be added to the query for only the fields you've defined in the BaseTable schema
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
// Option 2: "all" - Select all fields (default behavior)
|
|
173
|
+
const usersTO = new TableOccurrence({
|
|
174
|
+
name: "users",
|
|
175
|
+
baseTable: usersBase,
|
|
176
|
+
defaultSelect: "all", // Don't always a $select parameter to the query; FileMaker will return all non-container fields from the table
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
// Option 3: Array of field names - Select only specific fields by default
|
|
180
|
+
const usersTO = new TableOccurrence({
|
|
181
|
+
name: "users",
|
|
182
|
+
baseTable: usersBase,
|
|
183
|
+
defaultSelect: ["username", "email"], // Only select these fields by default
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
// When you call list(), the defaultSelect is applied automatically
|
|
187
|
+
const result = await db.from("users").list().execute();
|
|
188
|
+
// If defaultSelect is ["username", "email"], result.data will only contain those fields
|
|
189
|
+
|
|
190
|
+
// You can still override with explicit select()
|
|
191
|
+
const result = await db
|
|
192
|
+
.from("users")
|
|
193
|
+
.list()
|
|
194
|
+
.select("username", "email", "age") // Always overrides at the per-request level
|
|
195
|
+
.execute();
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
Lastly, you can combine all table occurrences into a database instance for the full type-safe experience. This is a method on the main `FMServerConnection` client class.
|
|
199
|
+
|
|
200
|
+
```typescript
|
|
201
|
+
const db = connection.database("MyDatabase.fmp12", {
|
|
202
|
+
occurrences: [contactsTO, usersTO], // Register your table occurrences
|
|
203
|
+
});
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
## Querying Data
|
|
207
|
+
|
|
208
|
+
### Basic Queries
|
|
209
|
+
|
|
210
|
+
Use `list()` to retrieve multiple records:
|
|
211
|
+
|
|
212
|
+
```typescript
|
|
213
|
+
// Get all users
|
|
214
|
+
const result = await db.from("users").list().execute();
|
|
215
|
+
|
|
216
|
+
if (result.data) {
|
|
217
|
+
result.data.forEach((user) => {
|
|
218
|
+
console.log(user.username);
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
Get a specific record by ID:
|
|
224
|
+
|
|
225
|
+
```typescript
|
|
226
|
+
const result = await db.from("users").get("user-123").execute();
|
|
227
|
+
|
|
228
|
+
if (result.data) {
|
|
229
|
+
console.log(result.data.username);
|
|
230
|
+
}
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
Get a single field value:
|
|
234
|
+
|
|
235
|
+
```typescript
|
|
236
|
+
const result = await db
|
|
237
|
+
.from("users")
|
|
238
|
+
.get("user-123")
|
|
239
|
+
.getSingleField("email")
|
|
240
|
+
.execute();
|
|
241
|
+
|
|
242
|
+
if (result.data) {
|
|
243
|
+
console.log(result.data); // "user@example.com"
|
|
244
|
+
}
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
### Filtering
|
|
248
|
+
|
|
249
|
+
fmodata provides type-safe filter operations that prevent common errors at compile time. The filter system supports three syntaxes: shorthand, single operator objects, and arrays for multiple operators.
|
|
24
250
|
|
|
25
|
-
|
|
26
|
-
pnpm build
|
|
251
|
+
#### Operator Syntax
|
|
27
252
|
|
|
28
|
-
|
|
29
|
-
pnpm test
|
|
253
|
+
You can use filters in three ways:
|
|
30
254
|
|
|
31
|
-
|
|
32
|
-
|
|
255
|
+
**1. Shorthand (direct value):**
|
|
256
|
+
|
|
257
|
+
```typescript
|
|
258
|
+
.filter({ name: "John" })
|
|
259
|
+
// Equivalent to: { name: [{ eq: "John" }] }
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
**2. Single operator object:**
|
|
263
|
+
|
|
264
|
+
```typescript
|
|
265
|
+
.filter({ age: { gt: 18 } })
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
**3. Array of operators (for multiple operators on same field):**
|
|
269
|
+
|
|
270
|
+
```typescript
|
|
271
|
+
.filter({ age: [{ gt: 18 }, { lt: 65 }] })
|
|
272
|
+
// Result: age gt 18 and age lt 65
|
|
33
273
|
```
|
|
34
274
|
|
|
35
|
-
|
|
275
|
+
The array pattern prevents duplicate operators on the same field and allows multiple conditions with implicit AND.
|
|
276
|
+
|
|
277
|
+
#### Available Operators
|
|
278
|
+
|
|
279
|
+
**String fields:**
|
|
280
|
+
|
|
281
|
+
- `eq`, `ne` - equality/inequality
|
|
282
|
+
- `contains`, `startswith`, `endswith` - string functions
|
|
283
|
+
- `gt`, `ge`, `lt`, `le` - comparison
|
|
284
|
+
- `in` - match any value in array
|
|
285
|
+
|
|
286
|
+
**Number fields:**
|
|
287
|
+
|
|
288
|
+
- `eq`, `ne`, `gt`, `ge`, `lt`, `le` - comparisons
|
|
289
|
+
- `in` - match any value in array
|
|
290
|
+
|
|
291
|
+
**Boolean fields:**
|
|
292
|
+
|
|
293
|
+
- `eq`, `ne` - equality only
|
|
294
|
+
|
|
295
|
+
**Date fields:**
|
|
296
|
+
|
|
297
|
+
- `eq`, `ne`, `gt`, `ge`, `lt`, `le` - date comparisons
|
|
298
|
+
- `in` - match any date in array
|
|
299
|
+
|
|
300
|
+
#### Shorthand Syntax
|
|
301
|
+
|
|
302
|
+
For simple equality checks, use the shorthand:
|
|
303
|
+
|
|
304
|
+
```typescript
|
|
305
|
+
const result = await db.from("users").list().filter({ name: "John" }).execute();
|
|
306
|
+
// Equivalent to: { name: [{ eq: "John" }] }
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
#### Examples
|
|
310
|
+
|
|
311
|
+
```typescript
|
|
312
|
+
// Equality filter (single operator)
|
|
313
|
+
const activeUsers = await db
|
|
314
|
+
.from("users")
|
|
315
|
+
.list()
|
|
316
|
+
.filter({ active: { eq: true } })
|
|
317
|
+
.execute();
|
|
318
|
+
|
|
319
|
+
// Comparison operators (single operator)
|
|
320
|
+
const adultUsers = await db
|
|
321
|
+
.from("users")
|
|
322
|
+
.list()
|
|
323
|
+
.filter({ age: { gt: 18 } })
|
|
324
|
+
.execute();
|
|
325
|
+
|
|
326
|
+
// String operators (single operator)
|
|
327
|
+
const johns = await db
|
|
328
|
+
.from("users")
|
|
329
|
+
.list()
|
|
330
|
+
.filter({ name: { contains: "John" } })
|
|
331
|
+
.execute();
|
|
332
|
+
|
|
333
|
+
// Multiple operators on same field (array syntax, implicit AND)
|
|
334
|
+
const rangeQuery = await db
|
|
335
|
+
.from("users")
|
|
336
|
+
.list()
|
|
337
|
+
.filter({ age: [{ gt: 18 }, { lt: 65 }] })
|
|
338
|
+
.execute();
|
|
339
|
+
|
|
340
|
+
// Combine filters with AND
|
|
341
|
+
const result = await db
|
|
342
|
+
.from("users")
|
|
343
|
+
.list()
|
|
344
|
+
.filter({
|
|
345
|
+
and: [{ active: [{ eq: true }] }, { age: [{ gt: 18 }] }],
|
|
346
|
+
})
|
|
347
|
+
.execute();
|
|
348
|
+
|
|
349
|
+
// Combine filters with OR
|
|
350
|
+
const result = await db
|
|
351
|
+
.from("users")
|
|
352
|
+
.list()
|
|
353
|
+
.filter({
|
|
354
|
+
or: [{ name: [{ eq: "John" }] }, { name: [{ eq: "Jane" }] }],
|
|
355
|
+
})
|
|
356
|
+
.execute();
|
|
357
|
+
|
|
358
|
+
// IN operator
|
|
359
|
+
const result = await db
|
|
360
|
+
.from("users")
|
|
361
|
+
.list()
|
|
362
|
+
.filter({ age: [{ in: [18, 21, 25] }] })
|
|
363
|
+
.execute();
|
|
364
|
+
|
|
365
|
+
// Null checks
|
|
366
|
+
const result = await db
|
|
367
|
+
.from("users")
|
|
368
|
+
.list()
|
|
369
|
+
.filter({ deletedAt: [{ eq: null }] })
|
|
370
|
+
.execute();
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
#### Logical Operators
|
|
374
|
+
|
|
375
|
+
Combine multiple conditions with `and`, `or`, `not`:
|
|
376
|
+
|
|
377
|
+
```typescript
|
|
378
|
+
const result = await db
|
|
379
|
+
.from("users")
|
|
380
|
+
.list()
|
|
381
|
+
.filter({
|
|
382
|
+
and: [{ name: [{ contains: "John" }] }, { age: [{ gt: 18 }] }],
|
|
383
|
+
})
|
|
384
|
+
.execute();
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
#### Escape Hatch
|
|
388
|
+
|
|
389
|
+
For unsupported edge cases, pass a raw OData filter string:
|
|
390
|
+
|
|
391
|
+
```typescript
|
|
392
|
+
const result = await db
|
|
393
|
+
.from("users")
|
|
394
|
+
.list()
|
|
395
|
+
.filter("substringof('John', name)")
|
|
396
|
+
.execute();
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
### Sorting
|
|
400
|
+
|
|
401
|
+
Sort results using `orderBy()`:
|
|
402
|
+
|
|
403
|
+
```typescript
|
|
404
|
+
// Sort ascending
|
|
405
|
+
const result = await db.from("users").list().orderBy("name").execute();
|
|
406
|
+
|
|
407
|
+
// Sort descending
|
|
408
|
+
const result = await db.from("users").list().orderBy("name desc").execute();
|
|
409
|
+
|
|
410
|
+
// Multiple sort fields
|
|
411
|
+
const result = await db
|
|
412
|
+
.from("users")
|
|
413
|
+
.list()
|
|
414
|
+
.orderBy("lastName, firstName desc")
|
|
415
|
+
.execute();
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
### Pagination
|
|
419
|
+
|
|
420
|
+
Control the number of records returned and pagination:
|
|
421
|
+
|
|
422
|
+
```typescript
|
|
423
|
+
// Limit results
|
|
424
|
+
const result = await db.from("users").list().top(10).execute();
|
|
425
|
+
|
|
426
|
+
// Skip records (pagination)
|
|
427
|
+
const result = await db.from("users").list().top(10).skip(20).execute();
|
|
428
|
+
|
|
429
|
+
// Count total records
|
|
430
|
+
const result = await db.from("users").list().count().execute();
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
### Selecting Fields
|
|
434
|
+
|
|
435
|
+
Select specific fields to return:
|
|
436
|
+
|
|
437
|
+
```typescript
|
|
438
|
+
const result = await db
|
|
439
|
+
.from("users")
|
|
440
|
+
.list()
|
|
441
|
+
.select("username", "email")
|
|
442
|
+
.execute();
|
|
443
|
+
|
|
444
|
+
// result.data[0] will only have username and email fields
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
### Single Records
|
|
448
|
+
|
|
449
|
+
Use `single()` to ensure exactly one record is returned (returns an error if zero or multiple records are found):
|
|
450
|
+
|
|
451
|
+
```typescript
|
|
452
|
+
const result = await db
|
|
453
|
+
.from("users")
|
|
454
|
+
.list()
|
|
455
|
+
.filter({ email: { eq: "user@example.com" } })
|
|
456
|
+
.single()
|
|
457
|
+
.execute();
|
|
36
458
|
|
|
37
|
-
|
|
459
|
+
if (result.data) {
|
|
460
|
+
// result.data is a single record, not an array
|
|
461
|
+
console.log(result.data.username);
|
|
462
|
+
}
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
Use `maybeSingle()` when you want at most one record (returns `null` if no record is found, returns an error if multiple records are found):
|
|
466
|
+
|
|
467
|
+
```typescript
|
|
468
|
+
const result = await db
|
|
469
|
+
.from("users")
|
|
470
|
+
.list()
|
|
471
|
+
.filter({ email: { eq: "user@example.com" } })
|
|
472
|
+
.maybeSingle()
|
|
473
|
+
.execute();
|
|
474
|
+
|
|
475
|
+
if (result.data) {
|
|
476
|
+
// result.data is a single record or null
|
|
477
|
+
console.log(result.data?.username);
|
|
478
|
+
} else {
|
|
479
|
+
// No record found - result.data would be null
|
|
480
|
+
console.log("User not found");
|
|
481
|
+
}
|
|
482
|
+
```
|
|
483
|
+
|
|
484
|
+
**Difference between `single()` and `maybeSingle()`:**
|
|
485
|
+
|
|
486
|
+
- `single()` - Requires exactly one record. Returns an error if zero or multiple records are found.
|
|
487
|
+
- `maybeSingle()` - Allows zero or one record. Returns `null` if no record is found, returns an error only if multiple records are found.
|
|
488
|
+
|
|
489
|
+
### Chaining Methods
|
|
490
|
+
|
|
491
|
+
All query methods can be chained together:
|
|
492
|
+
|
|
493
|
+
```typescript
|
|
494
|
+
const result = await db
|
|
495
|
+
.from("users")
|
|
496
|
+
.list()
|
|
497
|
+
.select("username", "email", "age")
|
|
498
|
+
.filter({ age: { gt: 18 } })
|
|
499
|
+
.orderBy("username")
|
|
500
|
+
.top(10)
|
|
501
|
+
.skip(0)
|
|
502
|
+
.execute();
|
|
503
|
+
```
|
|
504
|
+
|
|
505
|
+
## CRUD Operations
|
|
506
|
+
|
|
507
|
+
### Insert
|
|
508
|
+
|
|
509
|
+
Insert new records with type-safe data:
|
|
510
|
+
|
|
511
|
+
```typescript
|
|
512
|
+
// Insert a new user
|
|
513
|
+
const result = await db
|
|
514
|
+
.from("users")
|
|
515
|
+
.insert({
|
|
516
|
+
username: "johndoe",
|
|
517
|
+
email: "john@example.com",
|
|
518
|
+
active: true,
|
|
519
|
+
})
|
|
520
|
+
.execute();
|
|
521
|
+
|
|
522
|
+
if (result.data) {
|
|
523
|
+
console.log("Created user:", result.data);
|
|
524
|
+
}
|
|
525
|
+
```
|
|
526
|
+
|
|
527
|
+
If you specify `insertRequired` fields in your base table, those fields become required:
|
|
528
|
+
|
|
529
|
+
```typescript
|
|
530
|
+
const usersBase = new BaseTable({
|
|
531
|
+
schema: {
|
|
532
|
+
id: z.string(),
|
|
533
|
+
username: z.string(),
|
|
534
|
+
email: z.string(),
|
|
535
|
+
createdAt: z.string().optional(),
|
|
536
|
+
},
|
|
537
|
+
idField: "id",
|
|
538
|
+
insertRequired: ["username", "email"], // These fields are required on insert
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
// TypeScript will enforce that username and email are provided
|
|
542
|
+
const result = await db
|
|
543
|
+
.from("users")
|
|
544
|
+
.insert({
|
|
545
|
+
username: "johndoe",
|
|
546
|
+
email: "john@example.com",
|
|
547
|
+
// createdAt is optional
|
|
548
|
+
})
|
|
549
|
+
.execute();
|
|
550
|
+
```
|
|
551
|
+
|
|
552
|
+
### Update
|
|
553
|
+
|
|
554
|
+
Update records by ID or filter:
|
|
555
|
+
|
|
556
|
+
```typescript
|
|
557
|
+
// Update by ID
|
|
558
|
+
const result = await db
|
|
559
|
+
.from("users")
|
|
560
|
+
.update({ username: "newname" })
|
|
561
|
+
.byId("user-123")
|
|
562
|
+
.execute();
|
|
563
|
+
|
|
564
|
+
if (result.data) {
|
|
565
|
+
console.log(`Updated ${result.data.updatedCount} record(s)`);
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// Update by filter
|
|
569
|
+
const result = await db
|
|
570
|
+
.from("users")
|
|
571
|
+
.update({ active: false })
|
|
572
|
+
.where((q) => q.filter({ lastLogin: { lt: "2023-01-01" } }))
|
|
573
|
+
.execute();
|
|
574
|
+
|
|
575
|
+
// Complex filter example
|
|
576
|
+
const result = await db
|
|
577
|
+
.from("users")
|
|
578
|
+
.update({ active: false })
|
|
579
|
+
.where((q) =>
|
|
580
|
+
q.filter({
|
|
581
|
+
and: [{ active: true }, { count: { lt: 5 } }],
|
|
582
|
+
}),
|
|
583
|
+
)
|
|
584
|
+
.execute();
|
|
585
|
+
|
|
586
|
+
// Update with additional query options
|
|
587
|
+
const result = await db
|
|
588
|
+
.from("users")
|
|
589
|
+
.update({ active: false })
|
|
590
|
+
.where((q) => q.filter({ active: true }).top(10))
|
|
591
|
+
.execute();
|
|
592
|
+
```
|
|
593
|
+
|
|
594
|
+
### Delete
|
|
595
|
+
|
|
596
|
+
Delete records by ID or filter:
|
|
597
|
+
|
|
598
|
+
```typescript
|
|
599
|
+
// Delete by ID
|
|
600
|
+
const result = await db.from("users").delete().byId("user-123").execute();
|
|
601
|
+
|
|
602
|
+
if (result.data) {
|
|
603
|
+
console.log(`Deleted ${result.data.deletedCount} record(s)`);
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
// Delete by filter
|
|
607
|
+
const result = await db
|
|
608
|
+
.from("users")
|
|
609
|
+
.delete()
|
|
610
|
+
.where((q) => q.filter({ active: false }))
|
|
611
|
+
.execute();
|
|
612
|
+
|
|
613
|
+
// Delete with complex filters
|
|
614
|
+
const result = await db
|
|
615
|
+
.from("users")
|
|
616
|
+
.delete()
|
|
617
|
+
.where((q) =>
|
|
618
|
+
q.filter({
|
|
619
|
+
and: [{ active: false }, { lastLogin: { lt: "2023-01-01" } }],
|
|
620
|
+
}),
|
|
621
|
+
)
|
|
622
|
+
.execute();
|
|
623
|
+
```
|
|
624
|
+
|
|
625
|
+
## Navigation & Relationships
|
|
626
|
+
|
|
627
|
+
### Defining Navigation
|
|
628
|
+
|
|
629
|
+
Define relationships between tables using the `navigation` option:
|
|
630
|
+
|
|
631
|
+
```typescript
|
|
632
|
+
const contactsBase = new BaseTable({
|
|
633
|
+
schema: {
|
|
634
|
+
id: z.string(),
|
|
635
|
+
name: z.string(),
|
|
636
|
+
userId: z.string(),
|
|
637
|
+
},
|
|
638
|
+
idField: "id",
|
|
639
|
+
});
|
|
640
|
+
|
|
641
|
+
const usersBase = new BaseTable({
|
|
642
|
+
schema: {
|
|
643
|
+
id: z.string(),
|
|
644
|
+
username: z.string(),
|
|
645
|
+
email: z.string(),
|
|
646
|
+
},
|
|
647
|
+
idField: "id",
|
|
648
|
+
});
|
|
649
|
+
|
|
650
|
+
// Define navigation using functions to handle circular dependencies
|
|
651
|
+
const contactsTO = new TableOccurrence({
|
|
652
|
+
name: "contacts",
|
|
653
|
+
baseTable: contactsBase,
|
|
654
|
+
navigation: {
|
|
655
|
+
users: () => usersTO, // Relationship to users table
|
|
656
|
+
},
|
|
657
|
+
});
|
|
658
|
+
|
|
659
|
+
const usersTO = new TableOccurrence({
|
|
660
|
+
name: "users",
|
|
661
|
+
baseTable: usersBase,
|
|
662
|
+
navigation: {
|
|
663
|
+
contacts: () => contactsTO, // Relationship to contacts table
|
|
664
|
+
},
|
|
665
|
+
});
|
|
666
|
+
|
|
667
|
+
// You can also add navigation after creation
|
|
668
|
+
const updatedUsersTO = usersTO.addNavigation({
|
|
669
|
+
profile: () => profileTO,
|
|
670
|
+
});
|
|
671
|
+
```
|
|
672
|
+
|
|
673
|
+
### Navigating Between Tables
|
|
674
|
+
|
|
675
|
+
Navigate to related records:
|
|
676
|
+
|
|
677
|
+
```typescript
|
|
678
|
+
// Navigate from a specific record
|
|
679
|
+
const result = await db
|
|
680
|
+
.from("contacts")
|
|
681
|
+
.get("contact-123")
|
|
682
|
+
.navigate("users")
|
|
683
|
+
.select("username", "email")
|
|
684
|
+
.execute();
|
|
685
|
+
|
|
686
|
+
// Navigate without specifying a record first
|
|
687
|
+
const result = await db.from("contacts").navigate("users").list().execute();
|
|
688
|
+
|
|
689
|
+
// You can navigate to arbitrary tables not in your schema
|
|
690
|
+
const result = await db
|
|
691
|
+
.from("contacts")
|
|
692
|
+
.navigate("some_other_table")
|
|
693
|
+
.list()
|
|
694
|
+
.execute();
|
|
695
|
+
```
|
|
696
|
+
|
|
697
|
+
### Expanding Related Records
|
|
698
|
+
|
|
699
|
+
Use `expand()` to include related records in your query results:
|
|
700
|
+
|
|
701
|
+
```typescript
|
|
702
|
+
// Simple expand
|
|
703
|
+
const result = await db.from("contacts").list().expand("users").execute();
|
|
704
|
+
|
|
705
|
+
// Expand with field selection
|
|
706
|
+
const result = await db
|
|
707
|
+
.from("contacts")
|
|
708
|
+
.list()
|
|
709
|
+
.expand("users", (b) => b.select("username", "email"))
|
|
710
|
+
.execute();
|
|
711
|
+
|
|
712
|
+
// Expand with filtering
|
|
713
|
+
const result = await db
|
|
714
|
+
.from("contacts")
|
|
715
|
+
.list()
|
|
716
|
+
.expand("users", (b) => b.filter({ active: true }))
|
|
717
|
+
.execute();
|
|
718
|
+
|
|
719
|
+
// Multiple expands
|
|
720
|
+
const result = await db
|
|
721
|
+
.from("contacts")
|
|
722
|
+
.list()
|
|
723
|
+
.expand("users", (b) => b.select("username"))
|
|
724
|
+
.expand("orders", (b) => b.select("total").top(5))
|
|
725
|
+
.execute();
|
|
726
|
+
|
|
727
|
+
// Nested expands
|
|
728
|
+
const result = await db
|
|
729
|
+
.from("contacts")
|
|
730
|
+
.list()
|
|
731
|
+
.expand("users", (usersBuilder) =>
|
|
732
|
+
usersBuilder
|
|
733
|
+
.select("username", "email")
|
|
734
|
+
.expand("customer", (customerBuilder) =>
|
|
735
|
+
customerBuilder.select("name", "tier"),
|
|
736
|
+
),
|
|
737
|
+
)
|
|
738
|
+
.execute();
|
|
739
|
+
|
|
740
|
+
// Complex expand with multiple options
|
|
741
|
+
const result = await db
|
|
742
|
+
.from("contacts")
|
|
743
|
+
.list()
|
|
744
|
+
.expand("users", (b) =>
|
|
745
|
+
b
|
|
746
|
+
.select("username", "email")
|
|
747
|
+
.filter({ active: true })
|
|
748
|
+
.orderBy("username")
|
|
749
|
+
.top(10)
|
|
750
|
+
.expand("customer", (nested) => nested.select("name")),
|
|
751
|
+
)
|
|
752
|
+
.execute();
|
|
753
|
+
```
|
|
754
|
+
|
|
755
|
+
## Running Scripts
|
|
756
|
+
|
|
757
|
+
Execute FileMaker scripts via OData:
|
|
758
|
+
|
|
759
|
+
```typescript
|
|
760
|
+
// Simple script execution
|
|
761
|
+
const result = await db.runScript("MyScriptName");
|
|
762
|
+
|
|
763
|
+
console.log(result.resultCode); // Script result code
|
|
764
|
+
console.log(result.result); // Optional script result string
|
|
765
|
+
|
|
766
|
+
// Pass parameters to script
|
|
767
|
+
const result = await db.runScript("MyScriptName", {
|
|
768
|
+
scriptParam: "some value",
|
|
769
|
+
});
|
|
770
|
+
|
|
771
|
+
// Script parameters can be strings, numbers, or objects
|
|
772
|
+
const result = await db.runScript("ProcessOrder", {
|
|
773
|
+
scriptParam: {
|
|
774
|
+
orderId: "12345",
|
|
775
|
+
action: "approve",
|
|
776
|
+
}, // Will be JSON stringified
|
|
777
|
+
});
|
|
778
|
+
|
|
779
|
+
// Validate script result with Zod schema
|
|
780
|
+
// NOTE: Your validator must be able to parse a string.
|
|
781
|
+
// See Zod codecs for how to build a jsonCodec function that does this
|
|
782
|
+
// https://zod.dev/codecs?id=jsonschema
|
|
783
|
+
|
|
784
|
+
const schema = jsonCodec(
|
|
785
|
+
z.object({
|
|
786
|
+
success: z.boolean(),
|
|
787
|
+
message: z.string(),
|
|
788
|
+
recordId: z.string(),
|
|
789
|
+
}),
|
|
790
|
+
);
|
|
791
|
+
|
|
792
|
+
const result = await db.runScript("CreateRecord", {
|
|
793
|
+
resultSchema: schema,
|
|
794
|
+
});
|
|
795
|
+
|
|
796
|
+
// result.result is now typed based on your schema
|
|
797
|
+
console.log(result.result.recordId);
|
|
798
|
+
```
|
|
799
|
+
|
|
800
|
+
**Note:** OData doesn't support script names with special characters (e.g., `@`, `&`, `/`) or script names beginning with a number. TypeScript will catch these at compile time.
|
|
801
|
+
|
|
802
|
+
## Advanced Features
|
|
803
|
+
|
|
804
|
+
### Type Safety
|
|
805
|
+
|
|
806
|
+
The library provides full TypeScript type inference:
|
|
807
|
+
|
|
808
|
+
```typescript
|
|
809
|
+
const usersBase = new BaseTable({
|
|
810
|
+
schema: {
|
|
811
|
+
id: z.string(),
|
|
812
|
+
username: z.string(),
|
|
813
|
+
email: z.string(),
|
|
814
|
+
},
|
|
815
|
+
idField: "id",
|
|
816
|
+
});
|
|
817
|
+
|
|
818
|
+
const usersTO = new TableOccurrence({
|
|
819
|
+
name: "users",
|
|
820
|
+
baseTable: usersBase,
|
|
821
|
+
});
|
|
822
|
+
|
|
823
|
+
const db = connection.database("MyDB", {
|
|
824
|
+
occurrences: [usersTO],
|
|
825
|
+
});
|
|
826
|
+
|
|
827
|
+
// TypeScript knows these are valid field names
|
|
828
|
+
db.from("users").list().select("username", "email");
|
|
829
|
+
|
|
830
|
+
// TypeScript error: "invalid" is not a field name
|
|
831
|
+
db.from("users").list().select("invalid"); // TS Error
|
|
832
|
+
|
|
833
|
+
// Type-safe filters
|
|
834
|
+
db.from("users")
|
|
835
|
+
.list()
|
|
836
|
+
.filter({ username: { eq: "john" } }); // ✓
|
|
837
|
+
db.from("users")
|
|
838
|
+
.list()
|
|
839
|
+
.filter({ invalid: { eq: "john" } }); // TS Error
|
|
840
|
+
```
|
|
841
|
+
|
|
842
|
+
### Required Fields
|
|
843
|
+
|
|
844
|
+
Control which fields are required for insert and update operations:
|
|
845
|
+
|
|
846
|
+
```typescript
|
|
847
|
+
const usersBase = new BaseTable({
|
|
848
|
+
schema: {
|
|
849
|
+
id: z.string(),
|
|
850
|
+
username: z.string(),
|
|
851
|
+
email: z.string(),
|
|
852
|
+
status: z.string(),
|
|
853
|
+
updatedAt: z.string().optional(),
|
|
854
|
+
},
|
|
855
|
+
idField: "id",
|
|
856
|
+
insertRequired: ["username", "email"], // Required on insert
|
|
857
|
+
updateRequired: ["status"], // Required on update
|
|
858
|
+
});
|
|
859
|
+
|
|
860
|
+
// Insert requires username and email
|
|
861
|
+
db.from("users").insert({
|
|
862
|
+
username: "john",
|
|
863
|
+
email: "john@example.com",
|
|
864
|
+
// updatedAt is optional
|
|
865
|
+
});
|
|
866
|
+
|
|
867
|
+
// Update requires status
|
|
868
|
+
db.from("users")
|
|
869
|
+
.update({
|
|
870
|
+
status: "active",
|
|
871
|
+
// other fields are optional
|
|
872
|
+
})
|
|
873
|
+
.byId("user-123");
|
|
874
|
+
```
|
|
875
|
+
|
|
876
|
+
### Error Handling
|
|
877
|
+
|
|
878
|
+
All operations return a `Result` type with either `data` or `error`:
|
|
879
|
+
|
|
880
|
+
```typescript
|
|
881
|
+
const result = await db.from("users").list().execute();
|
|
882
|
+
|
|
883
|
+
if (result.error) {
|
|
884
|
+
console.error("Query failed:", result.error.message);
|
|
885
|
+
return;
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
if (result.data) {
|
|
889
|
+
console.log("Query succeeded:", result.data);
|
|
890
|
+
}
|
|
891
|
+
```
|
|
892
|
+
|
|
893
|
+
### OData Annotations and Validation
|
|
894
|
+
|
|
895
|
+
By default, the library automatically strips OData annotations fields (`@id` and `@editLink`) from responses. If you need these fields, you can include them by passing `includeODataAnnotations: true`:
|
|
896
|
+
|
|
897
|
+
```typescript
|
|
898
|
+
const result = await db.from("users").list().execute({
|
|
899
|
+
includeODataAnnotations: true,
|
|
900
|
+
});
|
|
901
|
+
```
|
|
902
|
+
|
|
903
|
+
You can also skip runtime validation by passing `skipValidation: true`.
|
|
904
|
+
|
|
905
|
+
```typescript
|
|
906
|
+
const result = await db.from("users").list().execute({
|
|
907
|
+
skipValidation: true,
|
|
908
|
+
});
|
|
909
|
+
|
|
910
|
+
// Response is returned without schema validation
|
|
911
|
+
```
|
|
912
|
+
|
|
913
|
+
**Note:** Skipping validation means the response won't be validated OR transformed against your schema, so you lose runtime type safety guarantees. Use with caution.
|
|
914
|
+
|
|
915
|
+
### Custom Fetch Handlers
|
|
916
|
+
|
|
917
|
+
You can provide custom fetch handlers for testing or custom networking:
|
|
918
|
+
|
|
919
|
+
```typescript
|
|
920
|
+
const customFetch = async (url, options) => {
|
|
921
|
+
console.log("Fetching:", url);
|
|
922
|
+
return fetch(url, options);
|
|
923
|
+
};
|
|
924
|
+
|
|
925
|
+
const result = await db.from("users").list().execute({
|
|
926
|
+
fetchHandler: customFetch,
|
|
927
|
+
});
|
|
928
|
+
```
|
|
929
|
+
|
|
930
|
+
## Testing
|
|
931
|
+
|
|
932
|
+
The library supports testing with custom fetch handlers. You can create mock fetch functions to return test data:
|
|
933
|
+
|
|
934
|
+
```typescript
|
|
935
|
+
const mockResponse = {
|
|
936
|
+
"@odata.context": "...",
|
|
937
|
+
value: [
|
|
938
|
+
{ id: "1", username: "john", email: "john@example.com" },
|
|
939
|
+
{ id: "2", username: "jane", email: "jane@example.com" },
|
|
940
|
+
],
|
|
941
|
+
};
|
|
942
|
+
|
|
943
|
+
const mockFetch = async () => {
|
|
944
|
+
return new Response(JSON.stringify(mockResponse), {
|
|
945
|
+
status: 200,
|
|
946
|
+
headers: { "content-type": "application/json" },
|
|
947
|
+
});
|
|
948
|
+
};
|
|
949
|
+
|
|
950
|
+
const result = await db.from("users").list().execute({
|
|
951
|
+
fetchHandler: mockFetch,
|
|
952
|
+
});
|
|
953
|
+
|
|
954
|
+
expect(result.data).toHaveLength(2);
|
|
955
|
+
expect(result.data[0].username).toBe("john");
|
|
956
|
+
```
|
|
957
|
+
|
|
958
|
+
You can also inspect query strings without executing:
|
|
959
|
+
|
|
960
|
+
```typescript
|
|
961
|
+
const queryString = db
|
|
962
|
+
.from("users")
|
|
963
|
+
.list()
|
|
964
|
+
.select("username", "email")
|
|
965
|
+
.filter({ active: true })
|
|
966
|
+
.orderBy("username")
|
|
967
|
+
.top(10)
|
|
968
|
+
.getQueryString();
|
|
969
|
+
|
|
970
|
+
console.log(queryString);
|
|
971
|
+
// Output: "/users?$select=username,email&$filter=active eq true&$orderby=username&$top=10"
|
|
972
|
+
```
|
|
@@ -2,7 +2,7 @@ import { FFetchOptions } from '@fetchkit/ffetch';
|
|
|
2
2
|
import { Auth, ExecutionContext } from '../types.js';
|
|
3
3
|
import { Database } from './database.js';
|
|
4
4
|
import { TableOccurrence } from './table-occurrence.js';
|
|
5
|
-
export declare class
|
|
5
|
+
export declare class FMServerConnection implements ExecutionContext {
|
|
6
6
|
private fetchClient;
|
|
7
7
|
private serverUrl;
|
|
8
8
|
private auth;
|
|
@@ -3,7 +3,7 @@ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { en
|
|
|
3
3
|
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
4
4
|
import createClient from "@fetchkit/ffetch";
|
|
5
5
|
import { Database } from "./database.js";
|
|
6
|
-
class
|
|
6
|
+
class FMServerConnection {
|
|
7
7
|
constructor(config) {
|
|
8
8
|
__publicField(this, "fetchClient");
|
|
9
9
|
__publicField(this, "serverUrl");
|
|
@@ -80,6 +80,6 @@ class FileMakerOData {
|
|
|
80
80
|
}
|
|
81
81
|
}
|
|
82
82
|
export {
|
|
83
|
-
|
|
83
|
+
FMServerConnection
|
|
84
84
|
};
|
|
85
85
|
//# sourceMappingURL=filemaker-odata.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"filemaker-odata.js","sources":["../../../src/client/filemaker-odata.ts"],"sourcesContent":["import createClient, { FFetchOptions } from \"@fetchkit/ffetch\";\nimport type { Auth, ExecutionContext } from \"../types\";\nimport { Database } from \"./database\";\nimport { TableOccurrence } from \"./table-occurrence\";\n\nexport class
|
|
1
|
+
{"version":3,"file":"filemaker-odata.js","sources":["../../../src/client/filemaker-odata.ts"],"sourcesContent":["import createClient, { FFetchOptions } from \"@fetchkit/ffetch\";\nimport type { Auth, ExecutionContext } from \"../types\";\nimport { Database } from \"./database\";\nimport { TableOccurrence } from \"./table-occurrence\";\n\nexport class FMServerConnection implements ExecutionContext {\n private fetchClient: ReturnType<typeof createClient>;\n private serverUrl: string;\n private auth: Auth;\n constructor(config: {\n serverUrl: string;\n auth: Auth;\n fetchClientOptions?: FFetchOptions;\n }) {\n this.fetchClient = createClient({\n retries: 0,\n ...config.fetchClientOptions,\n });\n // Ensure the URL uses https://, is valid, and has no trailing slash\n const url = new URL(config.serverUrl);\n if (url.protocol !== \"https:\") {\n url.protocol = \"https:\";\n }\n // Remove any trailing slash from pathname\n url.pathname = url.pathname.replace(/\\/+$/, \"\");\n this.serverUrl = url.toString().replace(/\\/+$/, \"\");\n this.auth = config.auth;\n }\n\n /**\n * @internal\n */\n async _makeRequest<T>(\n url: string,\n options?: RequestInit & FFetchOptions,\n ): Promise<T> {\n const baseUrl = `${this.serverUrl}${\"apiKey\" in this.auth ? `/otto` : \"\"}/fmi/odata/v4`;\n\n const headers = {\n Authorization:\n \"apiKey\" in this.auth\n ? `Bearer ${this.auth.apiKey}`\n : `Basic ${btoa(`${this.auth.username}:${this.auth.password}`)}`,\n \"Content-Type\": \"application/json\",\n Accept: \"application/json\",\n ...(options?.headers || {}),\n };\n\n // TEMPORARY WORKAROUND: Hopefully this feature will be fixed in the ffetch library\n // Extract fetchHandler and headers separately, only for tests where we're overriding the fetch handler per-request\n const fetchHandler = options?.fetchHandler;\n const {\n headers: _headers,\n fetchHandler: _fetchHandler,\n ...restOptions\n } = options || {};\n\n // If fetchHandler is provided, create a temporary client with it\n // Otherwise use the existing client\n const clientToUse = fetchHandler\n ? createClient({ retries: 0, fetchHandler })\n : this.fetchClient;\n\n const resp = await clientToUse(baseUrl + url, {\n ...restOptions,\n headers,\n });\n\n if (!resp.ok) {\n throw new Error(\n `Failed to make request to ${baseUrl + url}: ${resp.statusText}`,\n );\n }\n\n // Check for affected rows header (for DELETE and bulk PATCH operations)\n // FileMaker may return this with 204 No Content or 200 OK\n const affectedRows = resp.headers.get(\"fmodata.affected_rows\");\n if (affectedRows !== null) {\n return parseInt(affectedRows, 10) as T;\n }\n\n // Handle 204 No Content with no body\n if (resp.status === 204) {\n return 0 as T;\n }\n\n if (resp.headers.get(\"content-type\")?.includes(\"application/json\")) {\n let data = await resp.json();\n if (data.error) {\n throw new Error(data.error);\n }\n return data as T;\n }\n return (await resp.text()) as T;\n }\n\n database<\n const Occurrences extends readonly TableOccurrence<any, any, any, any>[],\n >(\n name: string,\n config?: { occurrences?: Occurrences },\n ): Database<Occurrences> {\n return new Database(name, this, config);\n }\n\n /**\n * Lists all available databases from the FileMaker OData service.\n * @returns Promise resolving to an array of database names\n */\n async listDatabaseNames(): Promise<string[]> {\n const response = (await this._makeRequest(\"/\")) as {\n value?: Array<{ name: string }>;\n };\n if (response.value && Array.isArray(response.value)) {\n return response.value.map((item) => item.name);\n }\n return [];\n }\n}\n"],"names":[],"mappings":";;;;;AAKO,MAAM,mBAA+C;AAAA,EAI1D,YAAY,QAIT;AAPK;AACA;AACA;AAMN,SAAK,cAAc,aAAa;AAAA,MAC9B,SAAS;AAAA,MACT,GAAG,OAAO;AAAA,IAAA,CACX;AAED,UAAM,MAAM,IAAI,IAAI,OAAO,SAAS;AAChC,QAAA,IAAI,aAAa,UAAU;AAC7B,UAAI,WAAW;AAAA,IAAA;AAGjB,QAAI,WAAW,IAAI,SAAS,QAAQ,QAAQ,EAAE;AAC9C,SAAK,YAAY,IAAI,SAAW,EAAA,QAAQ,QAAQ,EAAE;AAClD,SAAK,OAAO,OAAO;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA,EAMrB,MAAM,aACJ,KACA,SACY;;AACN,UAAA,UAAU,GAAG,KAAK,SAAS,GAAG,YAAY,KAAK,OAAO,UAAU,EAAE;AAExE,UAAM,UAAU;AAAA,MACd,eACE,YAAY,KAAK,OACb,UAAU,KAAK,KAAK,MAAM,KAC1B,SAAS,KAAK,GAAG,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,QAAQ,EAAE,CAAC;AAAA,MAClE,gBAAgB;AAAA,MAChB,QAAQ;AAAA,MACR,IAAI,mCAAS,YAAW,CAAA;AAAA,IAC1B;AAIA,UAAM,eAAe,mCAAS;AACxB,UAAA;AAAA,MACJ,SAAS;AAAA,MACT,cAAc;AAAA,MACd,GAAG;AAAA,IACL,IAAI,WAAW,CAAC;AAIV,UAAA,cAAc,eAChB,aAAa,EAAE,SAAS,GAAG,aAAA,CAAc,IACzC,KAAK;AAET,UAAM,OAAO,MAAM,YAAY,UAAU,KAAK;AAAA,MAC5C,GAAG;AAAA,MACH;AAAA,IAAA,CACD;AAEG,QAAA,CAAC,KAAK,IAAI;AACZ,YAAM,IAAI;AAAA,QACR,6BAA6B,UAAU,GAAG,KAAK,KAAK,UAAU;AAAA,MAChE;AAAA,IAAA;AAKF,UAAM,eAAe,KAAK,QAAQ,IAAI,uBAAuB;AAC7D,QAAI,iBAAiB,MAAM;AAClB,aAAA,SAAS,cAAc,EAAE;AAAA,IAAA;AAI9B,QAAA,KAAK,WAAW,KAAK;AAChB,aAAA;AAAA,IAAA;AAGT,SAAI,UAAK,QAAQ,IAAI,cAAc,MAA/B,mBAAkC,SAAS,qBAAqB;AAC9D,UAAA,OAAO,MAAM,KAAK,KAAK;AAC3B,UAAI,KAAK,OAAO;AACR,cAAA,IAAI,MAAM,KAAK,KAAK;AAAA,MAAA;AAErB,aAAA;AAAA,IAAA;AAED,WAAA,MAAM,KAAK,KAAK;AAAA,EAAA;AAAA,EAG1B,SAGE,MACA,QACuB;AACvB,WAAO,IAAI,SAAS,MAAM,MAAM,MAAM;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOxC,MAAM,oBAAuC;AAC3C,UAAM,WAAY,MAAM,KAAK,aAAa,GAAG;AAG7C,QAAI,SAAS,SAAS,MAAM,QAAQ,SAAS,KAAK,GAAG;AACnD,aAAO,SAAS,MAAM,IAAI,CAAC,SAAS,KAAK,IAAI;AAAA,IAAA;AAE/C,WAAO,CAAC;AAAA,EAAA;AAEZ;"}
|
package/dist/esm/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export { BaseTable } from './client/base-table.js';
|
|
2
2
|
export { TableOccurrence, createTableOccurrence, } from './client/table-occurrence.js';
|
|
3
|
-
export { FileMakerOData } from './client/filemaker-odata.js';
|
|
3
|
+
export { FMServerConnection as FileMakerOData } from './client/filemaker-odata.js';
|
|
4
4
|
export type { Filter, TypedFilter, FieldFilter, StringOperators, NumberOperators, BooleanOperators, DateOperators, LogicalFilter, } from './filter-types.js';
|
package/dist/esm/index.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { BaseTable } from "./client/base-table.js";
|
|
2
2
|
import { TableOccurrence, createTableOccurrence } from "./client/table-occurrence.js";
|
|
3
|
-
import {
|
|
3
|
+
import { FMServerConnection } from "./client/filemaker-odata.js";
|
|
4
4
|
export {
|
|
5
5
|
BaseTable,
|
|
6
|
-
FileMakerOData,
|
|
6
|
+
FMServerConnection as FileMakerOData,
|
|
7
7
|
TableOccurrence,
|
|
8
8
|
createTableOccurrence
|
|
9
9
|
};
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@ import type { Auth, ExecutionContext } from "../types";
|
|
|
3
3
|
import { Database } from "./database";
|
|
4
4
|
import { TableOccurrence } from "./table-occurrence";
|
|
5
5
|
|
|
6
|
-
export class
|
|
6
|
+
export class FMServerConnection implements ExecutionContext {
|
|
7
7
|
private fetchClient: ReturnType<typeof createClient>;
|
|
8
8
|
private serverUrl: string;
|
|
9
9
|
private auth: Auth;
|
package/src/index.ts
CHANGED
|
@@ -4,7 +4,7 @@ export {
|
|
|
4
4
|
TableOccurrence,
|
|
5
5
|
createTableOccurrence,
|
|
6
6
|
} from "./client/table-occurrence";
|
|
7
|
-
export { FileMakerOData } from "./client/filemaker-odata";
|
|
7
|
+
export { FMServerConnection as FileMakerOData } from "./client/filemaker-odata";
|
|
8
8
|
export type {
|
|
9
9
|
Filter,
|
|
10
10
|
TypedFilter,
|