@jd-data-limited/easy-fm 5.0.0-beta.2 → 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 (59) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +93 -91
  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 +19 -18
  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/dataApiDatabaseSession.d.ts +11 -0
  13. package/dist/connection/dataApiDatabaseSession.js +139 -0
  14. package/dist/connection/database.d.ts +22 -56
  15. package/dist/connection/database.js +175 -111
  16. package/dist/connection/databaseBase.d.ts +9 -22
  17. package/dist/connection/databaseBase.js +0 -3
  18. package/dist/connection/databaseProvider.d.ts +196 -0
  19. package/dist/connection/databaseProvider.js +6 -0
  20. package/dist/connection/databaseSessionPool.d.ts +8 -7
  21. package/dist/connection/databaseSessionPool.js +110 -106
  22. package/dist/index.d.ts +4 -3
  23. package/dist/index.js +3 -3
  24. package/dist/layouts/layout.d.ts +0 -2
  25. package/dist/layouts/layout.js +6 -16
  26. package/dist/layouts/layoutBase.d.ts +0 -1
  27. package/dist/layouts/layoutInterface.d.ts +4 -2
  28. package/dist/records/fields/baseField.d.ts +4 -1
  29. package/dist/records/fields/containerField.d.ts +6 -23
  30. package/dist/records/fields/containerField.js +34 -68
  31. package/dist/records/fields/valueField.js +6 -6
  32. package/dist/records/getOperations/recordGetOperation.js +19 -18
  33. package/dist/records/layoutRecord.d.ts +8 -2
  34. package/dist/records/layoutRecord.js +45 -74
  35. package/dist/records/portalRecord.d.ts +4 -1
  36. package/dist/records/portalRecord.js +12 -2
  37. package/dist/records/recordBase.d.ts +6 -3
  38. package/dist/records/recordBase.js +6 -5
  39. package/dist/schema.d.ts +54 -0
  40. package/dist/schema.js +87 -0
  41. package/dist/typegen/dataApiSchemaSource.d.ts +3 -0
  42. package/dist/typegen/dataApiSchemaSource.js +32 -0
  43. package/dist/typegen/dependencies.d.ts +11 -0
  44. package/dist/typegen/dependencies.js +67 -0
  45. package/dist/typegen/generate.d.ts +5 -0
  46. package/dist/typegen/generate.js +85 -0
  47. package/dist/typegen/index.d.ts +6 -0
  48. package/dist/typegen/index.js +6 -0
  49. package/dist/typegen/init.d.ts +13 -0
  50. package/dist/typegen/init.js +113 -0
  51. package/dist/typegen/types.d.ts +40 -0
  52. package/dist/typegen/types.js +1 -0
  53. package/dist/typegen/zod.d.ts +3 -0
  54. package/dist/typegen/zod.js +32 -0
  55. package/package.json +26 -4
  56. package/dist/layouts/recordFieldsMap.d.ts +0 -2
  57. package/dist/layouts/recordFieldsMap.js +0 -4
  58. package/dist/records/field.d.ts +0 -77
  59. 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. */
@@ -31,8 +31,9 @@ export { type Field } from './records/fields/field.js';
31
31
  export { stringToTemporal, temporalToString, type TemporalValue, type TemporalValueType } from './utils/temporal.js';
32
32
  /** Field wrapper used for reading, editing, and container access. */
33
33
  export { type BaseField } from './records/fields/baseField.js';
34
- /** Default export. Represents FileMaker host/server. */
35
- export default FMHost;
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';
36
37
  export {
37
38
  /** Namespace re-export of library public types. */
38
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. */
@@ -26,5 +25,6 @@ export { asDate, asTime, asTimestamp, query, queryEscape } from './utils/query.j
26
25
  export * from './records/fields/valueField.js';
27
26
  export * from './records/fields/containerField.js';
28
27
  export { stringToTemporal, temporalToString } from './utils/temporal.js';
29
- /** Default export. Represents FileMaker host/server. */
30
- export default FMHost;
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,8 +1,10 @@
1
1
  import { type Portal } from '../records/portal.js';
2
- import { type Field } from '../records/fields/field.js';
3
2
  export interface LayoutInterface {
4
3
  fields: RecordFieldsMap;
5
4
  portals: PortalInterface;
6
5
  }
7
- export type RecordFieldsMap = Record<string, Field>;
6
+ /** Public field views, including generated read-only views, satisfy this shape. */
7
+ export type RecordFieldsMap = Record<string, {
8
+ readonly value: unknown;
9
+ }>;
8
10
  export type PortalInterface = Record<string | number | symbol, Portal<RecordFieldsMap>>;
@@ -2,6 +2,7 @@ import { RecordTypes } from '../../types.js';
2
2
  import { type ApiFieldMetadata, FieldResultTypes } from '../../models/apiResults.js';
3
3
  import { type LayoutBase } from '../../layouts/layoutBase.js';
4
4
  import { type z } from 'zod';
5
+ import { type SessionBinding } from '../../connection/databaseProvider.js';
5
6
  export type ContainerValue = null;
6
7
  /**
7
8
  * RawValueData refers to the raw shapes of data we receive from or sent to FileMaker.
@@ -10,11 +11,13 @@ export type RawValueData = string | number | null;
10
11
  export interface Parentable {
11
12
  layout: LayoutBase;
12
13
  type: RecordTypes;
13
- endpoint: string;
14
+ recordId: number;
15
+ sessionBinding?: SessionBinding;
14
16
  portal?: {
15
17
  name: string;
16
18
  };
17
19
  getFieldMetadata(fieldId: string): z.infer<typeof ApiFieldMetadata>;
20
+ refreshContainer(fieldId: string): Promise<import('./containerField.js').ContainerField>;
18
21
  }
19
22
  /**
20
23
  * A class representing a field in a record.
@@ -1,35 +1,18 @@
1
- import { BaseField, Parentable, RawValueData } from "./baseField.js";
2
1
  import { Readable } from 'node:stream';
3
- interface StreamOptions {
4
- abortSignal?: AbortSignal;
5
- }
2
+ import { BaseField, type Parentable, type RawValueData } from './baseField.js';
3
+ import { type ContainerDownloadOptions } from '../../connection/databaseProvider.js';
6
4
  export declare class ContainerField extends BaseField<string, 'container'> {
7
5
  #private;
8
- static firstContainerDownload: Promise<any> | null;
9
6
  constructor(record: Parentable, id: string, value: RawValueData);
10
- /**
11
- * Uploads a file to the container field.
12
- *
13
- * @param {Buffer} file - The file content as a buffer.
14
- *
15
- * @throws {Error} - Cannot upload a file to the field if it's not a container field.
16
- * @throws {Error} - Upload failed with HTTP error.
17
- *
18
- * @returns {Promise<void>} - A promise that resolves when the file is successfully uploaded.
19
- */
20
7
  upload(file: File): Promise<void>;
21
- stream(): Promise<{
8
+ /** @deprecated Use webStream instead. */
9
+ stream(options?: ContainerDownloadOptions): Promise<{
22
10
  data: Readable;
23
11
  mime: string;
24
12
  }>;
25
- /**
26
- * Returns the container download as a Web `Response`.
27
- */
28
- webStream(options: StreamOptions): Promise<Response>;
29
- /** Downloads the full container contents into memory. */
30
- arrayBuffer(): Promise<{
13
+ webStream(options?: ContainerDownloadOptions): Promise<Response>;
14
+ arrayBuffer(options?: ContainerDownloadOptions): Promise<{
31
15
  data: ArrayBuffer;
32
16
  mime: string;
33
17
  }>;
34
18
  }
35
- export {};
@@ -1,78 +1,44 @@
1
- import { BaseField } from "./baseField.js";
2
- import { FMError } from '../../FMError.js';
3
1
  import { Readable } from 'node:stream';
4
- import { HttpError } from '../../connection/Session.js';
5
- import { ApiResults } from "../../models/apiResults.js";
2
+ import { BaseField } from './baseField.js';
3
+ import { ContainerSessionAffinityError } from '../../connection/databaseProvider.js';
6
4
  export class ContainerField extends BaseField {
7
- static firstContainerDownload = null;
8
- constructor(record, id, value) {
9
- super(record, id, value);
10
- }
11
- /**
12
- * Uploads a file to the container field.
13
- *
14
- * @param {Buffer} file - The file content as a buffer.
15
- *
16
- * @throws {Error} - Cannot upload a file to the field if it's not a container field.
17
- * @throws {Error} - Upload failed with HTTP error.
18
- *
19
- * @returns {Promise<void>} - A promise that resolves when the file is successfully uploaded.
20
- */
5
+ constructor(record, id, value) { super(record, id, value); }
21
6
  async upload(file) {
22
- if (this.metadata.result !== 'container') {
23
- throw new Error('Cannot upload a file to the field; ' + this.id + ' (not a container field)');
24
- }
25
- const form = new FormData();
26
- form.append('upload', file);
27
- const res = await this.parent.layout.database.fetch(`${this.parent.endpoint}/containers/${this.id}/1`, {
28
- method: 'POST',
29
- body: form
30
- });
31
- if (!res.ok) {
32
- throw await HttpError.new(res);
33
- }
34
- const data = ApiResults.parse(await res.json());
35
- if (data.messages[0].code === 0)
36
- return;
37
- else {
38
- throw new FMError(data.messages[0].code, res.status, res);
39
- }
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 });
40
12
  }
41
- async #streamAsync(options = {}) {
42
- const req = await this.parent.layout.database.fetch(this.value, {
43
- signal: options.abortSignal ?? null
44
- });
45
- // const req = await this.parent.layout.database._apiRequestRaw(this.string, {useCookieJar: true})
46
- if (!req.ok || !req.body) {
47
- throw new Error(`HTTP Error: ${req.status} (${req.statusText})`);
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;
48
24
  }
49
- return req;
50
- }
51
- /**
52
- * @deprecated use webStream instead.
53
- */
54
- async stream() {
55
- const stream = await this.#streamAsync();
56
- if (!stream.body) {
57
- throw await HttpError.new(stream);
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 });
58
32
  }
59
- return {
60
- // @ts-expect-error stream types are correct
61
- data: Readable.fromWeb(stream.body),
62
- mime: stream.headers.get('Content-Type') ?? ''
63
- };
64
33
  }
65
- /**
66
- * Returns the container download as a Web `Response`.
67
- */
68
- async webStream(options) {
69
- return await this.#streamAsync(options);
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') ?? '' };
70
38
  }
71
- /** Downloads the full container contents into memory. */
72
- async arrayBuffer() {
73
- const stream = await this.#streamAsync();
74
- if (!stream.ok)
75
- throw await HttpError.new(stream);
76
- return { data: await stream.arrayBuffer(), mime: stream.headers.get('Content-Type') ?? '' };
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') ?? '' };
77
43
  }
78
44
  }
@@ -56,7 +56,7 @@ export class TimeStampField extends ValueFieldBase {
56
56
  if (typeof value !== 'string')
57
57
  throw new Error("Received an invalid value for a temporal field.");
58
58
  try {
59
- return stringToTemporal(value, 'timestamp', this.parent.layout.database.host.timeStampFormat);
59
+ return stringToTemporal(value, 'timestamp', this.parent.layout.database.timeStampFormat);
60
60
  }
61
61
  catch {
62
62
  return Temporal.PlainDateTime.from(value);
@@ -65,7 +65,7 @@ export class TimeStampField extends ValueFieldBase {
65
65
  serializeRawValue() {
66
66
  if (this._value === null)
67
67
  return '';
68
- return temporalToString(this._value, this.parent.layout.database.host.timeStampFormat);
68
+ return temporalToString(this._value, this.parent.layout.database.timeStampFormat);
69
69
  }
70
70
  }
71
71
  export class DateField extends ValueFieldBase {
@@ -75,7 +75,7 @@ export class DateField extends ValueFieldBase {
75
75
  if (typeof value !== 'string')
76
76
  throw new Error("Received an invalid value for a temporal field.");
77
77
  try {
78
- return stringToTemporal(value, 'date', this.parent.layout.database.host.dateFormat);
78
+ return stringToTemporal(value, 'date', this.parent.layout.database.dateFormat);
79
79
  }
80
80
  catch {
81
81
  return Temporal.PlainDate.from(value);
@@ -84,7 +84,7 @@ export class DateField extends ValueFieldBase {
84
84
  serializeRawValue() {
85
85
  if (this._value === null)
86
86
  return '';
87
- return temporalToString(this._value, this.parent.layout.database.host.dateFormat);
87
+ return temporalToString(this._value, this.parent.layout.database.dateFormat);
88
88
  }
89
89
  }
90
90
  export class TimeField extends ValueFieldBase {
@@ -94,7 +94,7 @@ export class TimeField extends ValueFieldBase {
94
94
  if (typeof value !== 'string')
95
95
  throw new Error("Received an invalid value for a temporal field.");
96
96
  try {
97
- return stringToTemporal(value, 'time', this.parent.layout.database.host.timeFormat);
97
+ return stringToTemporal(value, 'time', this.parent.layout.database.timeFormat);
98
98
  }
99
99
  catch {
100
100
  return Temporal.PlainTime.from(value);
@@ -103,6 +103,6 @@ export class TimeField extends ValueFieldBase {
103
103
  serializeRawValue() {
104
104
  if (this._value === null)
105
105
  return '';
106
- return temporalToString(this._value, this.parent.layout.database.host.timeFormat);
106
+ return temporalToString(this._value, this.parent.layout.database.timeFormat);
107
107
  }
108
108
  }