@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 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
- ## Usage
19
+ ## Quick Start
20
+
21
+ Here's a minimal example to get you started:
12
22
 
13
23
  ```typescript
14
- import { createODataClient } from "@proofkit/fmodata";
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
- // Usage example coming soon
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
- ## Development
79
+ ## Core Concepts
20
80
 
21
- ```bash
22
- # Install dependencies
23
- pnpm install
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
- # Build the package
26
- pnpm build
251
+ #### Operator Syntax
27
252
 
28
- # Run tests
29
- pnpm test
253
+ You can use filters in three ways:
30
254
 
31
- # Watch mode for development
32
- pnpm dev
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
- ## License
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
- MIT
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 FileMakerOData implements ExecutionContext {
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 FileMakerOData {
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
- FileMakerOData
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 FileMakerOData 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,eAA2C;AAAA,EAItD,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;"}
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;"}
@@ -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 { FileMakerOData } from "./client/filemaker-odata.js";
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@proofkit/fmodata",
3
- "version": "0.1.0-alpha.0",
3
+ "version": "0.1.0-alpha.2",
4
4
  "description": "FileMaker OData API client",
5
5
  "repository": "git@github.com:proofgeist/proofkit.git",
6
6
  "author": "Eric <37158449+eluce2@users.noreply.github.com>",
@@ -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 FileMakerOData implements ExecutionContext {
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,