@proofkit/fmodata 0.1.0-alpha.0
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 +37 -0
- package/dist/esm/client/base-table.d.ts +13 -0
- package/dist/esm/client/base-table.js +19 -0
- package/dist/esm/client/base-table.js.map +1 -0
- package/dist/esm/client/database.d.ts +49 -0
- package/dist/esm/client/database.js +90 -0
- package/dist/esm/client/database.js.map +1 -0
- package/dist/esm/client/delete-builder.d.ts +61 -0
- package/dist/esm/client/delete-builder.js +121 -0
- package/dist/esm/client/delete-builder.js.map +1 -0
- package/dist/esm/client/entity-set.d.ts +43 -0
- package/dist/esm/client/entity-set.js +120 -0
- package/dist/esm/client/entity-set.js.map +1 -0
- package/dist/esm/client/filemaker-odata.d.ts +26 -0
- package/dist/esm/client/filemaker-odata.js +85 -0
- package/dist/esm/client/filemaker-odata.js.map +1 -0
- package/dist/esm/client/insert-builder.d.ts +23 -0
- package/dist/esm/client/insert-builder.js +69 -0
- package/dist/esm/client/insert-builder.js.map +1 -0
- package/dist/esm/client/query-builder.d.ts +94 -0
- package/dist/esm/client/query-builder.js +649 -0
- package/dist/esm/client/query-builder.js.map +1 -0
- package/dist/esm/client/record-builder.d.ts +43 -0
- package/dist/esm/client/record-builder.js +121 -0
- package/dist/esm/client/record-builder.js.map +1 -0
- package/dist/esm/client/table-occurrence.d.ts +25 -0
- package/dist/esm/client/table-occurrence.js +47 -0
- package/dist/esm/client/table-occurrence.js.map +1 -0
- package/dist/esm/client/update-builder.d.ts +69 -0
- package/dist/esm/client/update-builder.js +134 -0
- package/dist/esm/client/update-builder.js.map +1 -0
- package/dist/esm/filter-types.d.ts +76 -0
- package/dist/esm/index.d.ts +4 -0
- package/dist/esm/index.js +10 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/types.d.ts +67 -0
- package/dist/esm/validation.d.ts +41 -0
- package/dist/esm/validation.js +270 -0
- package/dist/esm/validation.js.map +1 -0
- package/package.json +68 -0
- package/src/client/base-table.ts +25 -0
- package/src/client/database.ts +177 -0
- package/src/client/delete-builder.ts +193 -0
- package/src/client/entity-set.ts +310 -0
- package/src/client/filemaker-odata.ts +119 -0
- package/src/client/insert-builder.ts +93 -0
- package/src/client/query-builder.ts +1076 -0
- package/src/client/record-builder.ts +240 -0
- package/src/client/table-occurrence.ts +100 -0
- package/src/client/update-builder.ts +212 -0
- package/src/filter-types.ts +97 -0
- package/src/index.ts +17 -0
- package/src/types.ts +123 -0
- package/src/validation.ts +397 -0
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import createClient, { FFetchOptions } from "@fetchkit/ffetch";
|
|
2
|
+
import type { Auth, ExecutionContext } from "../types";
|
|
3
|
+
import { Database } from "./database";
|
|
4
|
+
import { TableOccurrence } from "./table-occurrence";
|
|
5
|
+
|
|
6
|
+
export class FileMakerOData implements ExecutionContext {
|
|
7
|
+
private fetchClient: ReturnType<typeof createClient>;
|
|
8
|
+
private serverUrl: string;
|
|
9
|
+
private auth: Auth;
|
|
10
|
+
constructor(config: {
|
|
11
|
+
serverUrl: string;
|
|
12
|
+
auth: Auth;
|
|
13
|
+
fetchClientOptions?: FFetchOptions;
|
|
14
|
+
}) {
|
|
15
|
+
this.fetchClient = createClient({
|
|
16
|
+
retries: 0,
|
|
17
|
+
...config.fetchClientOptions,
|
|
18
|
+
});
|
|
19
|
+
// Ensure the URL uses https://, is valid, and has no trailing slash
|
|
20
|
+
const url = new URL(config.serverUrl);
|
|
21
|
+
if (url.protocol !== "https:") {
|
|
22
|
+
url.protocol = "https:";
|
|
23
|
+
}
|
|
24
|
+
// Remove any trailing slash from pathname
|
|
25
|
+
url.pathname = url.pathname.replace(/\/+$/, "");
|
|
26
|
+
this.serverUrl = url.toString().replace(/\/+$/, "");
|
|
27
|
+
this.auth = config.auth;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* @internal
|
|
32
|
+
*/
|
|
33
|
+
async _makeRequest<T>(
|
|
34
|
+
url: string,
|
|
35
|
+
options?: RequestInit & FFetchOptions,
|
|
36
|
+
): Promise<T> {
|
|
37
|
+
const baseUrl = `${this.serverUrl}${"apiKey" in this.auth ? `/otto` : ""}/fmi/odata/v4`;
|
|
38
|
+
|
|
39
|
+
const headers = {
|
|
40
|
+
Authorization:
|
|
41
|
+
"apiKey" in this.auth
|
|
42
|
+
? `Bearer ${this.auth.apiKey}`
|
|
43
|
+
: `Basic ${btoa(`${this.auth.username}:${this.auth.password}`)}`,
|
|
44
|
+
"Content-Type": "application/json",
|
|
45
|
+
Accept: "application/json",
|
|
46
|
+
...(options?.headers || {}),
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
// TEMPORARY WORKAROUND: Hopefully this feature will be fixed in the ffetch library
|
|
50
|
+
// Extract fetchHandler and headers separately, only for tests where we're overriding the fetch handler per-request
|
|
51
|
+
const fetchHandler = options?.fetchHandler;
|
|
52
|
+
const {
|
|
53
|
+
headers: _headers,
|
|
54
|
+
fetchHandler: _fetchHandler,
|
|
55
|
+
...restOptions
|
|
56
|
+
} = options || {};
|
|
57
|
+
|
|
58
|
+
// If fetchHandler is provided, create a temporary client with it
|
|
59
|
+
// Otherwise use the existing client
|
|
60
|
+
const clientToUse = fetchHandler
|
|
61
|
+
? createClient({ retries: 0, fetchHandler })
|
|
62
|
+
: this.fetchClient;
|
|
63
|
+
|
|
64
|
+
const resp = await clientToUse(baseUrl + url, {
|
|
65
|
+
...restOptions,
|
|
66
|
+
headers,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
if (!resp.ok) {
|
|
70
|
+
throw new Error(
|
|
71
|
+
`Failed to make request to ${baseUrl + url}: ${resp.statusText}`,
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Check for affected rows header (for DELETE and bulk PATCH operations)
|
|
76
|
+
// FileMaker may return this with 204 No Content or 200 OK
|
|
77
|
+
const affectedRows = resp.headers.get("fmodata.affected_rows");
|
|
78
|
+
if (affectedRows !== null) {
|
|
79
|
+
return parseInt(affectedRows, 10) as T;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Handle 204 No Content with no body
|
|
83
|
+
if (resp.status === 204) {
|
|
84
|
+
return 0 as T;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (resp.headers.get("content-type")?.includes("application/json")) {
|
|
88
|
+
let data = await resp.json();
|
|
89
|
+
if (data.error) {
|
|
90
|
+
throw new Error(data.error);
|
|
91
|
+
}
|
|
92
|
+
return data as T;
|
|
93
|
+
}
|
|
94
|
+
return (await resp.text()) as T;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
database<
|
|
98
|
+
const Occurrences extends readonly TableOccurrence<any, any, any, any>[],
|
|
99
|
+
>(
|
|
100
|
+
name: string,
|
|
101
|
+
config?: { occurrences?: Occurrences },
|
|
102
|
+
): Database<Occurrences> {
|
|
103
|
+
return new Database(name, this, config);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Lists all available databases from the FileMaker OData service.
|
|
108
|
+
* @returns Promise resolving to an array of database names
|
|
109
|
+
*/
|
|
110
|
+
async listDatabaseNames(): Promise<string[]> {
|
|
111
|
+
const response = (await this._makeRequest("/")) as {
|
|
112
|
+
value?: Array<{ name: string }>;
|
|
113
|
+
};
|
|
114
|
+
if (response.value && Array.isArray(response.value)) {
|
|
115
|
+
return response.value.map((item) => item.name);
|
|
116
|
+
}
|
|
117
|
+
return [];
|
|
118
|
+
}
|
|
119
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ExecutionContext,
|
|
3
|
+
ExecutableBuilder,
|
|
4
|
+
Result,
|
|
5
|
+
ODataRecordMetadata,
|
|
6
|
+
InferSchemaType,
|
|
7
|
+
} from "../types";
|
|
8
|
+
import type { TableOccurrence } from "./table-occurrence";
|
|
9
|
+
import { validateSingleResponse } from "../validation";
|
|
10
|
+
import { type FFetchOptions } from "@fetchkit/ffetch";
|
|
11
|
+
|
|
12
|
+
export class InsertBuilder<
|
|
13
|
+
T extends Record<string, any>,
|
|
14
|
+
Occ extends TableOccurrence<any, any, any, any> | undefined = undefined,
|
|
15
|
+
> implements ExecutableBuilder<T & ODataRecordMetadata>
|
|
16
|
+
{
|
|
17
|
+
private occurrence?: Occ;
|
|
18
|
+
private tableName: string;
|
|
19
|
+
private databaseName: string;
|
|
20
|
+
private context: ExecutionContext;
|
|
21
|
+
private data: Partial<T>;
|
|
22
|
+
|
|
23
|
+
constructor(config: {
|
|
24
|
+
occurrence?: Occ;
|
|
25
|
+
tableName: string;
|
|
26
|
+
databaseName: string;
|
|
27
|
+
context: ExecutionContext;
|
|
28
|
+
data: Partial<T>;
|
|
29
|
+
}) {
|
|
30
|
+
this.occurrence = config.occurrence;
|
|
31
|
+
this.tableName = config.tableName;
|
|
32
|
+
this.databaseName = config.databaseName;
|
|
33
|
+
this.context = config.context;
|
|
34
|
+
this.data = config.data;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async execute(
|
|
38
|
+
options?: RequestInit & FFetchOptions,
|
|
39
|
+
): Promise<Result<T & ODataRecordMetadata>> {
|
|
40
|
+
try {
|
|
41
|
+
const url = `/${this.databaseName}/${this.tableName}`;
|
|
42
|
+
|
|
43
|
+
// Make POST request with JSON body
|
|
44
|
+
const response = await this.context._makeRequest(url, {
|
|
45
|
+
method: "POST",
|
|
46
|
+
headers: {
|
|
47
|
+
"Content-Type": "application/json",
|
|
48
|
+
},
|
|
49
|
+
body: JSON.stringify(this.data),
|
|
50
|
+
...options,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// Get schema from occurrence if available
|
|
54
|
+
const schema = this.occurrence?.baseTable?.schema;
|
|
55
|
+
|
|
56
|
+
// Validate the response (FileMaker returns the created record)
|
|
57
|
+
const validation = await validateSingleResponse<T>(
|
|
58
|
+
response,
|
|
59
|
+
schema,
|
|
60
|
+
undefined, // No selected fields for insert
|
|
61
|
+
undefined, // No expand configs
|
|
62
|
+
"exact", // Expect exactly one record
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
if (!validation.valid) {
|
|
66
|
+
return { data: undefined, error: validation.error };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Handle null response (shouldn't happen for insert, but handle it)
|
|
70
|
+
if (validation.data === null) {
|
|
71
|
+
return {
|
|
72
|
+
data: undefined,
|
|
73
|
+
error: new Error("Insert operation returned null response"),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return { data: validation.data, error: undefined };
|
|
78
|
+
} catch (error) {
|
|
79
|
+
return {
|
|
80
|
+
data: undefined,
|
|
81
|
+
error: error instanceof Error ? error : new Error(String(error)),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
getRequestConfig(): { method: string; url: string; body?: any } {
|
|
87
|
+
return {
|
|
88
|
+
method: "POST",
|
|
89
|
+
url: `/${this.databaseName}/${this.tableName}`,
|
|
90
|
+
body: JSON.stringify(this.data),
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
}
|