@jd-data-limited/easy-fm 5.0.0-beta.1 → 5.0.0-beta.4

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.
Files changed (75) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +93 -90
  3. package/dist/bin/cli.js +34 -5
  4. package/dist/bin/generateClient.d.ts +1 -0
  5. package/dist/bin/generateClient.js +48 -0
  6. package/dist/bin/generateTypes.js +20 -19
  7. package/dist/bin/init.d.ts +6 -0
  8. package/dist/bin/init.js +46 -0
  9. package/dist/bin/stressSearch.js +12 -12
  10. package/dist/connection/DataApiProvider.d.ts +15 -0
  11. package/dist/connection/DataApiProvider.js +246 -0
  12. package/dist/connection/FMHost.d.ts +1 -3
  13. package/dist/connection/FMHost.js +1 -3
  14. package/dist/connection/HostBase.d.ts +0 -2
  15. package/dist/connection/dataApiDatabaseSession.d.ts +11 -0
  16. package/dist/connection/dataApiDatabaseSession.js +139 -0
  17. package/dist/connection/database.d.ts +22 -56
  18. package/dist/connection/database.js +175 -111
  19. package/dist/connection/databaseBase.d.ts +9 -22
  20. package/dist/connection/databaseBase.js +0 -3
  21. package/dist/connection/databaseProvider.d.ts +196 -0
  22. package/dist/connection/databaseProvider.js +6 -0
  23. package/dist/connection/databaseSessionPool.d.ts +8 -7
  24. package/dist/connection/databaseSessionPool.js +110 -106
  25. package/dist/index.d.ts +11 -7
  26. package/dist/index.js +7 -5
  27. package/dist/layouts/layout.d.ts +0 -2
  28. package/dist/layouts/layout.js +6 -16
  29. package/dist/layouts/layoutBase.d.ts +0 -1
  30. package/dist/layouts/layoutInterface.d.ts +4 -1
  31. package/dist/models/apiResults.d.ts +7 -6
  32. package/dist/models/apiResults.js +1 -1
  33. package/dist/records/fields/baseField.d.ts +60 -0
  34. package/dist/records/fields/baseField.js +59 -0
  35. package/dist/records/fields/containerField.d.ts +18 -0
  36. package/dist/records/fields/containerField.js +44 -0
  37. package/dist/records/fields/field.d.ts +3 -0
  38. package/dist/records/fields/field.js +1 -0
  39. package/dist/records/fields/valueField.d.ts +41 -0
  40. package/dist/records/fields/valueField.js +108 -0
  41. package/dist/records/getOperations/recordGetOperation.js +24 -24
  42. package/dist/records/layoutRecord.d.ts +15 -8
  43. package/dist/records/layoutRecord.js +61 -98
  44. package/dist/records/portal.d.ts +1 -1
  45. package/dist/records/portalBase.d.ts +1 -2
  46. package/dist/records/portalRecord.d.ts +11 -5
  47. package/dist/records/portalRecord.js +24 -2
  48. package/dist/records/recordBase.d.ts +11 -7
  49. package/dist/records/recordBase.js +38 -38
  50. package/dist/schema.d.ts +54 -0
  51. package/dist/schema.js +87 -0
  52. package/dist/typegen/dataApiSchemaSource.d.ts +3 -0
  53. package/dist/typegen/dataApiSchemaSource.js +32 -0
  54. package/dist/typegen/dependencies.d.ts +11 -0
  55. package/dist/typegen/dependencies.js +67 -0
  56. package/dist/typegen/generate.d.ts +5 -0
  57. package/dist/typegen/generate.js +85 -0
  58. package/dist/typegen/index.d.ts +6 -0
  59. package/dist/typegen/index.js +6 -0
  60. package/dist/typegen/init.d.ts +13 -0
  61. package/dist/typegen/init.js +113 -0
  62. package/dist/typegen/types.d.ts +40 -0
  63. package/dist/typegen/types.js +1 -0
  64. package/dist/typegen/zod.d.ts +3 -0
  65. package/dist/typegen/zod.js +32 -0
  66. package/dist/types.d.ts +1 -1
  67. package/dist/utils/query.d.ts +21 -12
  68. package/dist/utils/query.js +58 -23
  69. package/dist/utils/temporal.d.ts +9 -0
  70. package/dist/utils/temporal.js +110 -0
  71. package/package.json +47 -24
  72. package/dist/layouts/recordFieldsMap.d.ts +0 -2
  73. package/dist/layouts/recordFieldsMap.js +0 -4
  74. package/dist/records/field.d.ts +0 -77
  75. package/dist/records/field.js +0 -192
@@ -1,40 +1,37 @@
1
- import { Database } from './database.js';
2
- import { HttpError, session } from './Session.js';
3
- import { generateAuthorizationHeaders } from './generateAuthorizationHeaders.js';
4
1
  import { FMError } from '../FMError.js';
5
2
  import { ApiResults } from '../models/apiResults.js';
6
3
  import { CookieJar } from './CookieJar.js';
7
- /**
8
- * DatabaseSessionPool handles database connections that can be pooled.
9
- */
10
- export class DatabaseSessionPool extends Database {
4
+ import { generateAuthorizationHeaders } from './generateAuthorizationHeaders.js';
5
+ import { HttpError, session } from './Session.js';
6
+ import { DataApiDatabaseSession } from './dataApiDatabaseSession.js';
7
+ /** Reserves independent FileMaker Data API sessions from a bounded pool. */
8
+ export class DatabaseSessionPool {
9
+ host;
11
10
  #connectionDetails;
12
11
  #activeSessions = new Set();
13
- #withSessionQueue = [];
12
+ #waiters = [];
13
+ #abortController = new AbortController();
14
+ #openingSessions = 0;
15
+ #closed = false;
14
16
  constructor(host, connectionDetails) {
15
- super(host, connectionDetails);
17
+ this.host = host;
16
18
  this.#connectionDetails = connectionDetails;
17
19
  }
18
- async close() {
19
- if (this.abortController.signal.aborted)
20
- return;
21
- await super.close();
22
- // System might be shutting down, so we want to make sure we're quick
23
- const sessions = [...this.#activeSessions];
24
- this.#activeSessions.clear();
25
- await Promise.allSettled(sessions.map(session => session.session.logout()));
20
+ get formatting() {
21
+ return this.host;
22
+ }
23
+ get endpoint() {
24
+ return `${this.host.protocol}//${this.host.hostname}/fmi/data/v2/databases/${this.#connectionDetails.database}`;
26
25
  }
27
26
  get #maxSessions() {
28
27
  return this.#connectionDetails.credentials.sessionPoolSize ?? 8;
29
28
  }
30
- // Opens a new database session
31
29
  async #openSession() {
32
- if (!this.canOpenNewConnections)
33
- throw new Error('Cannot open new connections');
30
+ if (this.#closed)
31
+ throw new Error('Database provider is closed');
34
32
  await this.host.getMetadata();
35
33
  const url = new URL(`${this.endpoint}/sessions`);
36
- url.hostname = this.host.hostname;
37
- const cookiejar = new CookieJar();
34
+ const cookieJar = new CookieJar();
38
35
  const res = await fetch(url, {
39
36
  method: 'POST',
40
37
  headers: generateAuthorizationHeaders(this.#connectionDetails.credentials),
@@ -44,105 +41,112 @@ export class DatabaseSessionPool extends Database {
44
41
  username: data.credentials.username,
45
42
  password: data.credentials.password
46
43
  }))
47
- })
44
+ }),
45
+ signal: this.#abortController.signal
48
46
  });
49
47
  for (const cookie of res.headers.getSetCookie())
50
- cookiejar.addCookie(url, cookie);
51
- const _res = ApiResults.parse(await res.json());
52
- if (res.status === 200) {
53
- return session({
54
- token: res.headers.get('x-fm-data-access-token') ?? '',
55
- endpoint: this.endpoint,
56
- baseCookieJar: cookiejar,
57
- abortSignal: this.abortController.signal
58
- });
59
- }
60
- else {
61
- throw new FMError(_res.messages[0].code, res.status, res);
62
- }
48
+ cookieJar.addCookie(url, cookie);
49
+ const data = ApiResults.parse(await res.json());
50
+ if (res.status !== 200)
51
+ throw new FMError(data.messages[0].code, res.status, data);
52
+ return session({
53
+ token: res.headers.get('x-fm-data-access-token') ?? '',
54
+ endpoint: this.endpoint,
55
+ baseCookieJar: cookieJar,
56
+ abortSignal: this.#abortController.signal
57
+ });
63
58
  }
64
- /**
65
- *
66
- * @param session
67
- * @param callback
68
- * @param retry when true, if an HTTP 401 error occurs, we'll re-attempt it with another session.
69
- * @private
70
- */
71
- async #withSessionInternal(session, callback, retry) {
59
+ async #createReservedSession() {
60
+ this.#openingSessions++;
72
61
  try {
73
- session.working = true;
74
- const result = await callback(session.session);
75
- return result;
62
+ const active = { working: true, session: await this.#openSession() };
63
+ this.#activeSessions.add(active);
64
+ return active;
76
65
  }
77
- catch (e) {
78
- if (e instanceof HttpError && e.status === 401) {
79
- // Invalidate this session
80
- this.#activeSessions.delete(session);
81
- if (retry)
82
- return await this.withSession(callback, false);
66
+ finally {
67
+ this.#openingSessions--;
68
+ }
69
+ }
70
+ async #acquire() {
71
+ if (this.#closed)
72
+ throw new Error('Database provider is closed');
73
+ for (const active of this.#activeSessions) {
74
+ if (!active.working) {
75
+ active.working = true;
76
+ return active;
83
77
  }
84
- throw e;
85
78
  }
86
- finally {
87
- if (this.#activeSessions.has(session)) {
88
- session.working = false;
79
+ if (this.#activeSessions.size + this.#openingSessions < this.#maxSessions) {
80
+ try {
81
+ return await this.#createReservedSession();
82
+ }
83
+ catch (error) {
84
+ this.#serveWaitingRequest();
85
+ throw error;
89
86
  }
90
- await this.#scheduleQueuedWork();
91
87
  }
88
+ return await new Promise((resolve, reject) => {
89
+ this.#waiters.push({ resolve, reject });
90
+ });
92
91
  }
93
- async #processWithSessionQueue(session) {
94
- const job = this.#withSessionQueue.shift();
95
- if (!job)
92
+ #release(active) {
93
+ if (!this.#activeSessions.has(active))
94
+ return;
95
+ const waiter = this.#waiters.shift();
96
+ if (waiter) {
97
+ waiter.resolve(active);
96
98
  return;
97
- await this.#withSessionInternal(session, job.callback, job.retry).then(job.resolve).catch(job.reject);
99
+ }
100
+ active.working = false;
98
101
  }
99
- async #scheduleQueuedWork() {
100
- if (this.#withSessionQueue.length === 0)
102
+ async #discard(active) {
103
+ if (!this.#activeSessions.delete(active))
104
+ return;
105
+ await active.session.logout().catch(() => undefined);
106
+ this.#serveWaitingRequest();
107
+ }
108
+ #serveWaitingRequest() {
109
+ const waiter = this.#waiters.shift();
110
+ if (!waiter)
111
+ return;
112
+ if (this.#closed) {
113
+ waiter.reject(new Error('Database provider is closed'));
101
114
  return;
102
- for (const session of this.#activeSessions) {
103
- if (!session.working) {
104
- session.working = true;
105
- void this.#processWithSessionQueue(session);
106
- return;
107
- }
108
115
  }
109
- if (this.#activeSessions.size < this.#maxSessions) {
110
- void this.#openSession().then(async (session) => {
111
- const newSession = {
112
- working: true,
113
- session,
114
- disconnectTimeout: null
115
- };
116
- this.#activeSessions.add(newSession);
117
- await this.#processWithSessionQueue(newSession);
118
- });
116
+ void this.#createReservedSession()
117
+ .then(waiter.resolve)
118
+ .catch(error => {
119
+ waiter.reject(error);
120
+ this.#serveWaitingRequest();
121
+ });
122
+ }
123
+ async withSession(callback) {
124
+ const active = await this.#acquire();
125
+ let discard = false;
126
+ try {
127
+ return await callback(new DataApiDatabaseSession(this.endpoint, active.session, this.#connectionDetails.debug ?? false));
128
+ }
129
+ catch (error) {
130
+ discard = error instanceof HttpError && error.status === 401;
131
+ throw error;
132
+ }
133
+ finally {
134
+ if (discard)
135
+ await this.#discard(active);
136
+ else
137
+ this.#release(active);
119
138
  }
120
139
  }
121
- async withSession(callback, retry = true) {
122
- return await new Promise((resolve, reject) => {
123
- // First, see if there's an available session
124
- for (const session of this.#activeSessions) {
125
- if (!session.working) {
126
- session.working = true;
127
- this.#withSessionInternal(session, callback, retry).then(resolve).catch(reject);
128
- return;
129
- }
130
- }
131
- // If there's no available sessions, next check if we can open a new one
132
- if (this.#activeSessions.size < this.#maxSessions) {
133
- this.#openSession().then(async (session) => {
134
- const newSession = {
135
- working: true,
136
- session,
137
- disconnectTimeout: null
138
- };
139
- this.#activeSessions.add(newSession);
140
- return await this.#withSessionInternal(newSession, callback, retry);
141
- }).then(resolve).catch(reject);
142
- return;
143
- }
144
- // Finally if all else fails, queue the job
145
- this.#withSessionQueue.push({ callback, resolve, reject, retry });
146
- });
140
+ async close() {
141
+ if (this.#closed)
142
+ return;
143
+ this.#closed = true;
144
+ this.#abortController.abort('Closing database provider');
145
+ const error = new Error('Database provider is closed');
146
+ for (const waiter of this.#waiters.splice(0))
147
+ waiter.reject(error);
148
+ const sessions = [...this.#activeSessions];
149
+ this.#activeSessions.clear();
150
+ await Promise.allSettled(sessions.map(active => active.session.logout()));
147
151
  }
148
152
  }
package/dist/index.d.ts CHANGED
@@ -1,9 +1,9 @@
1
- import FMHost from './connection/FMHost.js';
2
1
  import type * as TYPES from './types.js';
3
2
  /** Error type returned for FileMaker Data API failures. */
4
3
  export { FMError } from './FMError.js';
5
4
  /** Utility type for narrowing portal data on typed layouts. */
6
5
  export { type PickPortals } from './types.js';
6
+ export { type DatabaseStructure } from './databaseStructure.js';
7
7
  /** Base database connection abstraction used by all auth modes. */
8
8
  export { Database } from './connection/database.js';
9
9
  /** Layout-scoped API wrapper for metadata, scripts, and record operations. */
@@ -13,7 +13,7 @@ export { RecordBase } from './records/recordBase.js';
13
13
  /** Record returned from a layout. Supports fetch, commit, duplicate, and delete. */
14
14
  export { LayoutRecord } from './records/layoutRecord.js';
15
15
  /** Type helpers for describing typed layouts and portals. */
16
- export { type LayoutInterface, type PortalInterface } from './layouts/layoutInterface.js';
16
+ export { type LayoutInterface, type PortalInterface, type RecordFieldsMap } from './layouts/layoutInterface.js';
17
17
  /** Record returned from a portal row. */
18
18
  export { PortalRecord } from './records/portalRecord.js';
19
19
  /** Wrapper around portal rows included in a layout record. */
@@ -22,14 +22,18 @@ export { Portal } from './records/portal.js';
22
22
  export { type FindRequest, type FindRequestRaw, RecordGetOperation } from './records/getOperations/recordGetOperation.js';
23
23
  /** Entry point for `layout.records.*` operations. */
24
24
  export { LayoutRecordManager } from './layouts/layoutRecordManager.js';
25
- /** Type alias for map of fields on a typed record. */
26
- export { type RecordFieldsMap } from './layouts/recordFieldsMap.js';
27
25
  /** Helpers for safe FileMaker find query construction and date/time formatting. */
28
26
  export { asDate, asTime, asTimestamp, query, queryEscape } from './utils/query.js';
27
+ /** Conversion helpers for FileMaker-formatted Temporal values. */
28
+ export * from './records/fields/valueField.js';
29
+ export * from './records/fields/containerField.js';
30
+ export { type Field } from './records/fields/field.js';
31
+ export { stringToTemporal, temporalToString, type TemporalValue, type TemporalValueType } from './utils/temporal.js';
29
32
  /** Field wrapper used for reading, editing, and container access. */
30
- export { type Container, Field } from './records/field.js';
31
- /** Default export. Represents FileMaker host/server. */
32
- export default FMHost;
33
+ export { type BaseField } from './records/fields/baseField.js';
34
+ export { SchemaDriftError, withSchemaValidation, zodValidator, type DatabaseRuntimeSchema, type LayoutRuntimeSchema, type ReadonlyField, type RuntimeValidator, type ValidationIssue, type ValidationResult } from './schema.js';
35
+ export { DataApiProvider, type DataApiProviderOptions } from './connection/DataApiProvider.js';
36
+ export { type DatabaseProvider, type ProviderConnection, type ProviderSession, type ProviderContext, type DatabaseFormatting, type DatabaseOperation, type DatabaseOperationResult, type DatabaseOperationType, type ContainerDownloadOptions, ProviderSessionExpiredError, ContainerSessionAffinityError } from './connection/databaseProvider.js';
33
37
  export {
34
38
  /** Namespace re-export of library public types. */
35
39
  type TYPES };
package/dist/index.js CHANGED
@@ -1,7 +1,6 @@
1
1
  /*
2
2
  * Copyright (c) 2023-2024. See LICENSE file for more information
3
3
  */
4
- import FMHost from './connection/FMHost.js';
5
4
  /** Error type returned for FileMaker Data API failures. */
6
5
  export { FMError } from './FMError.js';
7
6
  /** Base database connection abstraction used by all auth modes. */
@@ -22,7 +21,10 @@ export { RecordGetOperation } from './records/getOperations/recordGetOperation.j
22
21
  export { LayoutRecordManager } from './layouts/layoutRecordManager.js';
23
22
  /** Helpers for safe FileMaker find query construction and date/time formatting. */
24
23
  export { asDate, asTime, asTimestamp, query, queryEscape } from './utils/query.js';
25
- /** Field wrapper used for reading, editing, and container access. */
26
- export { Field } from './records/field.js';
27
- /** Default export. Represents FileMaker host/server. */
28
- export default FMHost;
24
+ /** Conversion helpers for FileMaker-formatted Temporal values. */
25
+ export * from './records/fields/valueField.js';
26
+ export * from './records/fields/containerField.js';
27
+ export { stringToTemporal, temporalToString } from './utils/temporal.js';
28
+ export { SchemaDriftError, withSchemaValidation, zodValidator } from './schema.js';
29
+ export { DataApiProvider } from './connection/DataApiProvider.js';
30
+ export { ProviderSessionExpiredError, ContainerSessionAffinityError } from './connection/databaseProvider.js';
@@ -11,8 +11,6 @@ export declare class Layout<T extends LayoutInterface> implements LayoutBase {
11
11
  readonly records: LayoutRecordManager<T>;
12
12
  metadata: z.infer<typeof ApiLayoutMetadata> | null;
13
13
  constructor(database: DatabaseBase, name: string);
14
- /** Base endpoint for this layout on FileMaker Data API. */
15
- get endpoint(): string;
16
14
  /**
17
15
  * Executes a FileMaker script on this layout asynchronously and returns the result.
18
16
  * @param {Script} script - The script to be executed.
@@ -3,7 +3,7 @@
3
3
  */
4
4
  import { LayoutRecordManager } from './layoutRecordManager.js';
5
5
  import { FMError } from '../FMError.js';
6
- import { ApiLayoutMetadata, ApiScriptResult } from '../models/apiResults.js';
6
+ import { ApiLayoutMetadata } from '../models/apiResults.js';
7
7
  export class Layout {
8
8
  database;
9
9
  name;
@@ -13,26 +13,16 @@ export class Layout {
13
13
  this.database = database;
14
14
  this.name = name;
15
15
  }
16
- /** Base endpoint for this layout on FileMaker Data API. */
17
- get endpoint() {
18
- return `${this.database.endpoint}/layouts/${this.name}`;
19
- }
20
16
  /**
21
17
  * Executes a FileMaker script on this layout asynchronously and returns the result.
22
18
  * @param {Script} script - The script to be executed.
23
19
  * @returns {Promise<ScriptResult>} - A promise that resolves to the script result or rejects with an error.
24
20
  */
25
21
  async runScript(script) {
26
- let url = `${this.endpoint}/script/${encodeURIComponent(script.name)}`;
27
- if (script.parameter)
28
- url += '?script.param=' + encodeURIComponent(script.parameter);
29
- const res = await this.database.fetchJSON(url, {
30
- type: ApiScriptResult,
31
- method: 'GET'
32
- });
33
- const error = parseInt(res.scriptError);
22
+ const { value: res } = await this.database.execute({ type: 'script.run', layout: this.name, script: script.name, parameter: script.parameter || undefined });
23
+ const error = res.scriptError ?? 0;
34
24
  return {
35
- scriptError: error ? new FMError(error, 200, res) : undefined,
25
+ scriptError: error ? new FMError(error, res.status ?? 200, res) : undefined,
36
26
  scriptResult: res.scriptResult
37
27
  };
38
28
  }
@@ -46,8 +36,8 @@ export class Layout {
46
36
  if (this.metadata) {
47
37
  return this.metadata;
48
38
  }
49
- const res = await this.database.fetchJSON(this.endpoint, { type: ApiLayoutMetadata });
50
- this.metadata = res;
39
+ const { value: res } = await this.database.execute({ type: 'layout.metadata', layout: this.name });
40
+ this.metadata = ApiLayoutMetadata.parse(res);
51
41
  return this.metadata;
52
42
  }
53
43
  }
@@ -5,7 +5,6 @@ import { type z } from 'zod';
5
5
  export interface LayoutBase {
6
6
  readonly name: string;
7
7
  metadata: z.infer<typeof ApiLayoutMetadata> | null;
8
- endpoint: string;
9
8
  runScript: (script: Script) => Promise<ScriptResult>;
10
9
  getLayoutMeta: () => Promise<z.infer<typeof ApiLayoutMetadata>>;
11
10
  database: DatabaseBase;
@@ -1,7 +1,10 @@
1
1
  import { type Portal } from '../records/portal.js';
2
- import { type RecordFieldsMap } from './recordFieldsMap.js';
3
2
  export interface LayoutInterface {
4
3
  fields: RecordFieldsMap;
5
4
  portals: PortalInterface;
6
5
  }
6
+ /** Public field views, including generated read-only views, satisfy this shape. */
7
+ export type RecordFieldsMap = Record<string, {
8
+ readonly value: unknown;
9
+ }>;
7
10
  export type PortalInterface = Record<string | number | symbol, Portal<RecordFieldsMap>>;
@@ -21,6 +21,7 @@ export declare const ApiFieldResultTypes: {
21
21
  readonly TIMESTAMP: "timeStamp";
22
22
  readonly CONTAINER: "container";
23
23
  };
24
+ export type FieldResultTypes = z.infer<typeof ApiFieldResultTypes>;
24
25
  export declare const ApiMessage: z.ZodObject<{
25
26
  code: z.ZodCoercedNumber<unknown>;
26
27
  message: z.ZodString;
@@ -166,7 +167,7 @@ export declare const ApiScriptResult: z.ZodObject<{
166
167
  scriptError: z.ZodString;
167
168
  scriptResult: z.ZodOptional<z.ZodString>;
168
169
  }, z.core.$strip>;
169
- export declare const ApiFieldData: z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>>;
170
+ export declare const ApiFieldData: z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodNull]>>;
170
171
  export declare const ApiResultSetObj: z.ZodObject<{
171
172
  database: z.ZodString;
172
173
  layout: z.ZodString;
@@ -175,10 +176,10 @@ export declare const ApiResultSetObj: z.ZodObject<{
175
176
  foundCount: z.ZodNumber;
176
177
  returnedCount: z.ZodNumber;
177
178
  }, z.core.$strip>;
178
- export declare const ApiPortalData: z.ZodRecord<z.ZodString, z.ZodArray<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>>>>;
179
+ export declare const ApiPortalData: z.ZodRecord<z.ZodString, z.ZodArray<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodNull]>>>>;
179
180
  export declare const ApiRowDataDef: z.ZodObject<{
180
- fieldData: z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>>;
181
- portalData: z.ZodRecord<z.ZodString, z.ZodArray<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>>>>;
181
+ fieldData: z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodNull]>>;
182
+ portalData: z.ZodRecord<z.ZodString, z.ZodArray<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodNull]>>>>;
182
183
  modId: z.ZodString;
183
184
  recordId: z.ZodString;
184
185
  portalDataInfo: z.ZodOptional<z.ZodArray<z.ZodObject<{
@@ -190,8 +191,8 @@ export declare const ApiRowDataDef: z.ZodObject<{
190
191
  }, z.core.$strip>;
191
192
  export declare const ApiRecordResponseObj: z.ZodObject<{
192
193
  data: z.ZodArray<z.ZodObject<{
193
- fieldData: z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>>;
194
- portalData: z.ZodRecord<z.ZodString, z.ZodArray<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>>>>;
194
+ fieldData: z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodNull]>>;
195
+ portalData: z.ZodRecord<z.ZodString, z.ZodArray<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodNull]>>>>;
195
196
  modId: z.ZodString;
196
197
  recordId: z.ZodString;
197
198
  portalDataInfo: z.ZodOptional<z.ZodArray<z.ZodObject<{
@@ -70,7 +70,7 @@ export const ApiScriptResult = z.object({
70
70
  scriptError: z.string(),
71
71
  scriptResult: z.string().optional()
72
72
  });
73
- export const ApiFieldData = z.record(z.string(), z.union([z.string(), z.number()]));
73
+ export const ApiFieldData = z.record(z.string(), z.union([z.string(), z.number(), z.null()]));
74
74
  export const ApiResultSetObj = z.object({
75
75
  database: z.string(),
76
76
  layout: z.string(),
@@ -0,0 +1,60 @@
1
+ import { RecordTypes } from '../../types.js';
2
+ import { type ApiFieldMetadata, FieldResultTypes } from '../../models/apiResults.js';
3
+ import { type LayoutBase } from '../../layouts/layoutBase.js';
4
+ import { type z } from 'zod';
5
+ import { type SessionBinding } from '../../connection/databaseProvider.js';
6
+ export type ContainerValue = null;
7
+ /**
8
+ * RawValueData refers to the raw shapes of data we receive from or sent to FileMaker.
9
+ */
10
+ export type RawValueData = string | number | null;
11
+ export interface Parentable {
12
+ layout: LayoutBase;
13
+ type: RecordTypes;
14
+ recordId: number;
15
+ sessionBinding?: SessionBinding;
16
+ portal?: {
17
+ name: string;
18
+ };
19
+ getFieldMetadata(fieldId: string): z.infer<typeof ApiFieldMetadata>;
20
+ refreshContainer(fieldId: string): Promise<import('./containerField.js').ContainerField>;
21
+ }
22
+ /**
23
+ * A class representing a field in a record.
24
+ * API_FIELD_RESULT defines the value of field.metadata.result for the abstracted field type
25
+ *
26
+ * @template T - The type of the field value.
27
+ */
28
+ export declare abstract class BaseField<T extends unknown, API_FIELD_RESULT extends FieldResultTypes> {
29
+ parent: Parentable;
30
+ id: string;
31
+ protected _value: T;
32
+ /**
33
+ * @internal
34
+ * Use `layout.records.create()` instead to create a new LayoutRecord, as this may be safer.
35
+ * @param record
36
+ * @param id
37
+ * @param value
38
+ * @protected
39
+ */
40
+ protected constructor(record: Parentable, id: string, value: RawValueData);
41
+ /**
42
+ * Takes in data in the format that FileMaker gives us and converts it to a more JavaScript-friendly format.
43
+ * This method is intended to be overriden.
44
+ * @param value
45
+ */
46
+ parseRawValue(value: RawValueData): T;
47
+ /**
48
+ * Converts data back into a format that can be sent to FileMaker. Intended to be overriden if conversion is non-standard.
49
+ */
50
+ serializeRawValue(): RawValueData;
51
+ updateFromRawValue(value: RawValueData): void;
52
+ /** Returns the FileMaker metadata for this field. */
53
+ get metadata(): z.infer<typeof ApiFieldMetadata> & {
54
+ result: API_FIELD_RESULT;
55
+ };
56
+ /** Gets or sets the current field value. */
57
+ get value(): T;
58
+ isCalculationField(): boolean;
59
+ isSummaryField(): boolean;
60
+ }
@@ -0,0 +1,59 @@
1
+ /*
2
+ * Copyright (c) 2023-2024. See LICENSE file for more information
3
+ */
4
+ /**
5
+ * A class representing a field in a record.
6
+ * API_FIELD_RESULT defines the value of field.metadata.result for the abstracted field type
7
+ *
8
+ * @template T - The type of the field value.
9
+ */
10
+ export class BaseField {
11
+ parent;
12
+ id;
13
+ _value;
14
+ /**
15
+ * @internal
16
+ * Use `layout.records.create()` instead to create a new LayoutRecord, as this may be safer.
17
+ * @param record
18
+ * @param id
19
+ * @param value
20
+ * @protected
21
+ */
22
+ constructor(record, id, value) {
23
+ this.parent = record;
24
+ this.id = id;
25
+ this._value = this.parseRawValue(value);
26
+ }
27
+ /**
28
+ * Takes in data in the format that FileMaker gives us and converts it to a more JavaScript-friendly format.
29
+ * This method is intended to be overriden.
30
+ * @param value
31
+ */
32
+ parseRawValue(value) {
33
+ return value;
34
+ }
35
+ /**
36
+ * Converts data back into a format that can be sent to FileMaker. Intended to be overriden if conversion is non-standard.
37
+ */
38
+ serializeRawValue() {
39
+ return this._value;
40
+ }
41
+ updateFromRawValue(value) {
42
+ this._value = this.parseRawValue(value);
43
+ }
44
+ /** Returns the FileMaker metadata for this field. */
45
+ get metadata() {
46
+ return this.parent.getFieldMetadata(this.id);
47
+ }
48
+ /** Gets or sets the current field value. */
49
+ get value() {
50
+ // if (this.metadata.result === "container") throw "Use await field.stream() to get the contents of a container field, instead of field.value"
51
+ return this._value;
52
+ }
53
+ isCalculationField() {
54
+ return this.metadata.type === 'calculation';
55
+ }
56
+ isSummaryField() {
57
+ return this.metadata.type === 'summary';
58
+ }
59
+ }
@@ -0,0 +1,18 @@
1
+ import { Readable } from 'node:stream';
2
+ import { BaseField, type Parentable, type RawValueData } from './baseField.js';
3
+ import { type ContainerDownloadOptions } from '../../connection/databaseProvider.js';
4
+ export declare class ContainerField extends BaseField<string, 'container'> {
5
+ #private;
6
+ constructor(record: Parentable, id: string, value: RawValueData);
7
+ upload(file: File): Promise<void>;
8
+ /** @deprecated Use webStream instead. */
9
+ stream(options?: ContainerDownloadOptions): Promise<{
10
+ data: Readable;
11
+ mime: string;
12
+ }>;
13
+ webStream(options?: ContainerDownloadOptions): Promise<Response>;
14
+ arrayBuffer(options?: ContainerDownloadOptions): Promise<{
15
+ data: ArrayBuffer;
16
+ mime: string;
17
+ }>;
18
+ }
@@ -0,0 +1,44 @@
1
+ import { Readable } from 'node:stream';
2
+ import { BaseField } from './baseField.js';
3
+ import { ContainerSessionAffinityError } from '../../connection/databaseProvider.js';
4
+ export class ContainerField extends BaseField {
5
+ constructor(record, id, value) { super(record, id, value); }
6
+ async upload(file) {
7
+ if (this.metadata.result !== 'container')
8
+ throw new Error(`Cannot upload a file to the field; ${this.id} (not a container field)`);
9
+ if (this.parent.recordId === -1)
10
+ throw new Error('Cannot upload to an unsaved record');
11
+ await this.parent.layout.database.execute({ type: 'container.upload', layout: this.parent.layout.name, recordId: this.parent.recordId, field: this.id, file });
12
+ }
13
+ async #response(options = {}) {
14
+ if (!this.value)
15
+ throw new Error(`Container field ${this.id} is empty`);
16
+ const binding = this.parent.sessionBinding;
17
+ if (!binding)
18
+ throw new ContainerSessionAffinityError('Container has no originating provider session');
19
+ try {
20
+ const response = await this.parent.layout.database.fetchContainer(this.value, binding, options.signal);
21
+ if (!response.ok || !response.body)
22
+ throw new Error(`Container response failed: ${response.status} (${response.statusText})`);
23
+ return response;
24
+ }
25
+ catch (error) {
26
+ if (!(error instanceof ContainerSessionAffinityError) || !options.refreshOnSessionLoss)
27
+ throw error;
28
+ const refreshed = await this.parent.refreshContainer(this.id);
29
+ this.updateFromRawValue(refreshed.value);
30
+ this.parent.sessionBinding = refreshed.parent.sessionBinding;
31
+ return await this.#response({ ...options, refreshOnSessionLoss: false });
32
+ }
33
+ }
34
+ /** @deprecated Use webStream instead. */
35
+ async stream(options = {}) {
36
+ const response = await this.#response(options);
37
+ return { data: Readable.fromWeb(response.body), mime: response.headers.get('Content-Type') ?? '' };
38
+ }
39
+ async webStream(options = {}) { return await this.#response(options); }
40
+ async arrayBuffer(options = {}) {
41
+ const response = await this.#response(options);
42
+ return { data: await response.arrayBuffer(), mime: response.headers.get('Content-Type') ?? '' };
43
+ }
44
+ }
@@ -0,0 +1,3 @@
1
+ import { ValueField } from "./valueField.js";
2
+ import { ContainerField } from "./containerField.js";
3
+ export type Field = ValueField | ContainerField;
@@ -0,0 +1 @@
1
+ export {};