@jd-data-limited/easy-fm 5.0.0-beta.1 → 5.0.0-beta.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
@@ -23,7 +23,7 @@ npm install @jd-data-limited/easy-fm
23
23
  ## Quick Start
24
24
 
25
25
  ```ts
26
- import FMHost from "@jd-data-limited/easy-fm"
26
+ import {FMHost} from "@jd-data-limited/easy-fm"
27
27
 
28
28
  const host = new FMHost("https://example.com")
29
29
 
@@ -57,6 +57,8 @@ for (const record of records) {
57
57
 
58
58
  ## Documentation
59
59
 
60
+ > Updating from v4 to v5? Check the guide here; [`docs/upgrade-to-v5.md`](./docs/upgrade-to-v5.md)
61
+
60
62
  Start here:
61
63
 
62
64
  - [`docs/getting-started.md`](./docs/getting-started.md): first connection, first find, closing sessions
@@ -67,7 +69,6 @@ Task guides:
67
69
  - [`docs/working-with-records.md`](./docs/working-with-records.md): list, create, update, duplicate, delete, portal rows
68
70
  - [`docs/query-recipes.md`](./docs/query-recipes.md): find requests, sorting, paging, script hooks
69
71
  - [`docs/authentication-and-sessions.md`](./docs/authentication-and-sessions.md): auth modes, pooling, lifecycle
70
- - [`docs/timezones.md`](./docs/timezones.md): date, time, and timestamp behavior
71
72
  - [`docs/typescript-layouts.md`](./docs/typescript-layouts.md): typed layouts and stronger autocomplete
72
73
  - [`docs/testGuide.md`](./docs/testGuide.md): local test commands
73
74
 
@@ -78,7 +78,7 @@ export async function generateTypesCLI() {
78
78
  default: true,
79
79
  message: "Verify your server's identity",
80
80
  when(previousAnswers) {
81
- return previousAnswers.hostname.startsWith('https://');
81
+ return previousAnswers.hostname?.startsWith('https://');
82
82
  }
83
83
  },
84
84
  {
@@ -98,7 +98,7 @@ export async function generateTypesCLI() {
98
98
  mask: '*'
99
99
  }
100
100
  ]);
101
- const HOST = new FMHost(data.hostname, () => data.timezoneOffset, data.verify);
101
+ const HOST = new FMHost(data.hostname, data.verify);
102
102
  const DATABASE = HOST.database({
103
103
  database: data.database,
104
104
  credentials: {
@@ -175,7 +175,7 @@ async function main() {
175
175
  warmup,
176
176
  concurrencyLevels
177
177
  });
178
- const fmHost = new FMHost(host, (moment) => 0 - moment.toDate().getTimezoneOffset(), false);
178
+ const fmHost = new FMHost(host, false);
179
179
  const rows = [];
180
180
  const connection = fmHost.database({
181
181
  database,
@@ -2,7 +2,6 @@ import { type Database } from './database.js';
2
2
  import { type HostBase } from './HostBase.js';
3
3
  import { type databaseOptionsWithExternalSources, FMHostMetadata, type loginOptionsClaris, type loginOptionsFileMaker, type loginOptionsOAuth } from '../types.js';
4
4
  import { type DatabaseStructure } from '../databaseStructure.js';
5
- import { type Moment } from 'moment';
6
5
  import z from 'zod';
7
6
  import { type DatabaseProtocol } from './Session.js';
8
7
  /**
@@ -10,11 +9,10 @@ import { type DatabaseProtocol } from './Session.js';
10
9
  */
11
10
  export default class FMHost implements HostBase {
12
11
  readonly hostname: string;
13
- readonly timezoneOffsetFunc: (moment: Moment) => number;
14
12
  readonly verify: boolean;
15
13
  readonly protocol: DatabaseProtocol;
16
14
  _metadata: z.infer<typeof FMHostMetadata> | null;
17
- constructor(_hostname: string, timezoneOffset?: (moment: Moment) => number, verify?: boolean);
15
+ constructor(_hostname: string, verify?: boolean);
18
16
  get metadata(): z.infer<typeof FMHostMetadata>;
19
17
  /** FileMaker host date format converted to Moment-compatible tokens. */
20
18
  get dateFormat(): string;
@@ -13,17 +13,15 @@ import { DatabaseConstantSession } from './databaseConstantSession.js';
13
13
  */
14
14
  export default class FMHost {
15
15
  hostname;
16
- timezoneOffsetFunc;
17
16
  verify;
18
17
  protocol;
19
18
  _metadata = null;
20
- constructor(_hostname, timezoneOffset = (moment) => 0 - (new Date()).getTimezoneOffset(), verify = true) {
19
+ constructor(_hostname, verify = true) {
21
20
  if (!(/^https?:\/\//).test(_hostname))
22
21
  throw new Error('hostname MUST begin with either http:// or https://');
23
22
  const url = new URL(_hostname);
24
23
  this.protocol = url.protocol;
25
24
  this.hostname = url.hostname;
26
- this.timezoneOffsetFunc = timezoneOffset;
27
25
  this.verify = verify;
28
26
  }
29
27
  get metadata() {
@@ -1,11 +1,9 @@
1
1
  import { type FMHostMetadata } from '../types.js';
2
- import { type Moment } from 'moment';
3
2
  import { type z } from 'zod';
4
3
  import { type DatabaseProtocol } from './Session.js';
5
4
  export interface HostBase {
6
5
  readonly hostname: string;
7
6
  readonly protocol: DatabaseProtocol;
8
- readonly timezoneOffsetFunc: (moment: Moment) => number;
9
7
  readonly verify: boolean;
10
8
  metadata: z.infer<typeof FMHostMetadata>;
11
9
  getMetadata: () => PromiseLike<any>;
package/dist/index.d.ts CHANGED
@@ -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,12 +22,15 @@ 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';
33
+ export { type BaseField } from './records/fields/baseField.js';
31
34
  /** Default export. Represents FileMaker host/server. */
32
35
  export default FMHost;
33
36
  export {
package/dist/index.js CHANGED
@@ -22,7 +22,9 @@ export { RecordGetOperation } from './records/getOperations/recordGetOperation.j
22
22
  export { LayoutRecordManager } from './layouts/layoutRecordManager.js';
23
23
  /** Helpers for safe FileMaker find query construction and date/time formatting. */
24
24
  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';
25
+ /** Conversion helpers for FileMaker-formatted Temporal values. */
26
+ export * from './records/fields/valueField.js';
27
+ export * from './records/fields/containerField.js';
28
+ export { stringToTemporal, temporalToString } from './utils/temporal.js';
27
29
  /** Default export. Represents FileMaker host/server. */
28
30
  export default FMHost;
@@ -1,7 +1,8 @@
1
1
  import { type Portal } from '../records/portal.js';
2
- import { type RecordFieldsMap } from './recordFieldsMap.js';
2
+ import { type Field } from '../records/fields/field.js';
3
3
  export interface LayoutInterface {
4
4
  fields: RecordFieldsMap;
5
5
  portals: PortalInterface;
6
6
  }
7
+ export type RecordFieldsMap = Record<string, Field>;
7
8
  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,57 @@
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
+ export type ContainerValue = null;
6
+ /**
7
+ * RawValueData refers to the raw shapes of data we receive from or sent to FileMaker.
8
+ */
9
+ export type RawValueData = string | number | null;
10
+ export interface Parentable {
11
+ layout: LayoutBase;
12
+ type: RecordTypes;
13
+ endpoint: string;
14
+ portal?: {
15
+ name: string;
16
+ };
17
+ getFieldMetadata(fieldId: string): z.infer<typeof ApiFieldMetadata>;
18
+ }
19
+ /**
20
+ * A class representing a field in a record.
21
+ * API_FIELD_RESULT defines the value of field.metadata.result for the abstracted field type
22
+ *
23
+ * @template T - The type of the field value.
24
+ */
25
+ export declare abstract class BaseField<T extends unknown, API_FIELD_RESULT extends FieldResultTypes> {
26
+ parent: Parentable;
27
+ id: string;
28
+ protected _value: T;
29
+ /**
30
+ * @internal
31
+ * Use `layout.records.create()` instead to create a new LayoutRecord, as this may be safer.
32
+ * @param record
33
+ * @param id
34
+ * @param value
35
+ * @protected
36
+ */
37
+ protected constructor(record: Parentable, id: string, value: RawValueData);
38
+ /**
39
+ * Takes in data in the format that FileMaker gives us and converts it to a more JavaScript-friendly format.
40
+ * This method is intended to be overriden.
41
+ * @param value
42
+ */
43
+ parseRawValue(value: RawValueData): T;
44
+ /**
45
+ * Converts data back into a format that can be sent to FileMaker. Intended to be overriden if conversion is non-standard.
46
+ */
47
+ serializeRawValue(): RawValueData;
48
+ updateFromRawValue(value: RawValueData): void;
49
+ /** Returns the FileMaker metadata for this field. */
50
+ get metadata(): z.infer<typeof ApiFieldMetadata> & {
51
+ result: API_FIELD_RESULT;
52
+ };
53
+ /** Gets or sets the current field value. */
54
+ get value(): T;
55
+ isCalculationField(): boolean;
56
+ isSummaryField(): boolean;
57
+ }
@@ -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,35 @@
1
+ import { BaseField, Parentable, RawValueData } from "./baseField.js";
2
+ import { Readable } from 'node:stream';
3
+ interface StreamOptions {
4
+ abortSignal?: AbortSignal;
5
+ }
6
+ export declare class ContainerField extends BaseField<string, 'container'> {
7
+ #private;
8
+ static firstContainerDownload: Promise<any> | null;
9
+ 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
+ upload(file: File): Promise<void>;
21
+ stream(): Promise<{
22
+ data: Readable;
23
+ mime: string;
24
+ }>;
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<{
31
+ data: ArrayBuffer;
32
+ mime: string;
33
+ }>;
34
+ }
35
+ export {};
@@ -0,0 +1,78 @@
1
+ import { BaseField } from "./baseField.js";
2
+ import { FMError } from '../../FMError.js';
3
+ import { Readable } from 'node:stream';
4
+ import { HttpError } from '../../connection/Session.js';
5
+ import { ApiResults } from "../../models/apiResults.js";
6
+ 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
+ */
21
+ 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
+ }
40
+ }
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})`);
48
+ }
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);
58
+ }
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
+ }
65
+ /**
66
+ * Returns the container download as a Web `Response`.
67
+ */
68
+ async webStream(options) {
69
+ return await this.#streamAsync(options);
70
+ }
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') ?? '' };
77
+ }
78
+ }
@@ -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 {};
@@ -0,0 +1,41 @@
1
+ import { BaseField, Parentable, RawValueData } from "./baseField.js";
2
+ import { Temporal } from "temporal-polyfill";
3
+ import { FieldResultTypes } from "../../models/apiResults.js";
4
+ export declare abstract class ValueFieldBase<T, API_FIELD_RESULT extends FieldResultTypes> extends BaseField<T, API_FIELD_RESULT> {
5
+ #private;
6
+ /**
7
+ * @internal
8
+ * Use `layout.records.create()` instead to create a new LayoutRecord, as this may be safer.
9
+ * @param record
10
+ * @param id
11
+ * @param value
12
+ * @protected
13
+ */
14
+ constructor(record: Parentable, id: string, value: RawValueData);
15
+ set(content: T): void;
16
+ /** `true` if this field has changed since it was last loaded or saved. */
17
+ get edited(): boolean;
18
+ /**
19
+ * Resets the 'edited' state of this field, without changing its value
20
+ */
21
+ updateOriginalContents(): void;
22
+ set value(value: T);
23
+ get value(): T;
24
+ }
25
+ export declare class TextField<T extends string = string> extends ValueFieldBase<T, 'text'> {
26
+ }
27
+ export declare class NumberField<T extends number | null = number | null> extends ValueFieldBase<T, 'number'> {
28
+ }
29
+ export declare class TimeStampField extends ValueFieldBase<Temporal.PlainDateTime | null, 'timeStamp'> {
30
+ parseRawValue(value: RawValueData): Temporal.PlainDateTime | null;
31
+ serializeRawValue(): RawValueData;
32
+ }
33
+ export declare class DateField extends ValueFieldBase<Temporal.PlainDate | null, 'date'> {
34
+ parseRawValue(value: RawValueData): Temporal.PlainDate | null;
35
+ serializeRawValue(): RawValueData;
36
+ }
37
+ export declare class TimeField extends ValueFieldBase<Temporal.PlainTime | null, 'time'> {
38
+ parseRawValue(value: RawValueData): Temporal.PlainTime | null;
39
+ serializeRawValue(): RawValueData;
40
+ }
41
+ export type ValueField = TextField | NumberField | TimeStampField | DateField | TimeField;
@@ -0,0 +1,108 @@
1
+ import { BaseField } from "./baseField.js";
2
+ import { Temporal } from "temporal-polyfill";
3
+ import { stringToTemporal, temporalToString } from "../../utils/temporal.js";
4
+ export class ValueFieldBase extends BaseField {
5
+ #originalContents;
6
+ /**
7
+ * @internal
8
+ * Use `layout.records.create()` instead to create a new LayoutRecord, as this may be safer.
9
+ * @param record
10
+ * @param id
11
+ * @param value
12
+ * @protected
13
+ */
14
+ constructor(record, id, value) {
15
+ super(record, id, value);
16
+ this.#originalContents = value;
17
+ }
18
+ set(content) {
19
+ /**
20
+ * Sets the value of the field.
21
+ *
22
+ * @param {T | null} content - The new content value to be set. Can be either the type T or null.
23
+ * @throws {Error} Cannot set container value using set(). Use upload() instead, if the result is a 'container'.
24
+ */
25
+ if (this.isCalculationField() || this.isSummaryField()) {
26
+ throw new Error('Cannot modify the value of an immutable calculation or sumamry field.');
27
+ }
28
+ else
29
+ this._value = content;
30
+ }
31
+ /** `true` if this field has changed since it was last loaded or saved. */
32
+ get edited() {
33
+ return this.#originalContents !== this.serializeRawValue();
34
+ }
35
+ /**
36
+ * Resets the 'edited' state of this field, without changing its value
37
+ */
38
+ updateOriginalContents() {
39
+ this.#originalContents = this.serializeRawValue();
40
+ }
41
+ set value(value) {
42
+ this.set(value);
43
+ }
44
+ get value() {
45
+ return super.value;
46
+ }
47
+ }
48
+ export class TextField extends ValueFieldBase {
49
+ }
50
+ export class NumberField extends ValueFieldBase {
51
+ }
52
+ export class TimeStampField extends ValueFieldBase {
53
+ parseRawValue(value) {
54
+ if (value === '' || value === null)
55
+ return null;
56
+ if (typeof value !== 'string')
57
+ throw new Error("Received an invalid value for a temporal field.");
58
+ try {
59
+ return stringToTemporal(value, 'timestamp', this.parent.layout.database.host.timeStampFormat);
60
+ }
61
+ catch {
62
+ return Temporal.PlainDateTime.from(value);
63
+ }
64
+ }
65
+ serializeRawValue() {
66
+ if (this._value === null)
67
+ return '';
68
+ return temporalToString(this._value, this.parent.layout.database.host.timeStampFormat);
69
+ }
70
+ }
71
+ export class DateField extends ValueFieldBase {
72
+ parseRawValue(value) {
73
+ if (value === '' || value === null)
74
+ return null;
75
+ if (typeof value !== 'string')
76
+ throw new Error("Received an invalid value for a temporal field.");
77
+ try {
78
+ return stringToTemporal(value, 'date', this.parent.layout.database.host.dateFormat);
79
+ }
80
+ catch {
81
+ return Temporal.PlainDate.from(value);
82
+ }
83
+ }
84
+ serializeRawValue() {
85
+ if (this._value === null)
86
+ return '';
87
+ return temporalToString(this._value, this.parent.layout.database.host.dateFormat);
88
+ }
89
+ }
90
+ export class TimeField extends ValueFieldBase {
91
+ parseRawValue(value) {
92
+ if (value === '' || value === null)
93
+ return null;
94
+ if (typeof value !== 'string')
95
+ throw new Error("Received an invalid value for a temporal field.");
96
+ try {
97
+ return stringToTemporal(value, 'time', this.parent.layout.database.host.timeFormat);
98
+ }
99
+ catch {
100
+ return Temporal.PlainTime.from(value);
101
+ }
102
+ }
103
+ serializeRawValue() {
104
+ if (this._value === null)
105
+ return '';
106
+ return temporalToString(this._value, this.parent.layout.database.host.timeFormat);
107
+ }
108
+ }
@@ -5,6 +5,8 @@ import { LayoutRecord } from '../layoutRecord.js';
5
5
  import { ApiRecordResponseObj } from '../../models/apiResults.js';
6
6
  import { FMError } from '../../FMError.js';
7
7
  import { FindRequestSymbol } from '../../utils/query.js';
8
+ import { Temporal } from "temporal-polyfill";
9
+ import { temporalToString } from "../../utils/temporal.js";
8
10
  /** Builder/executor for list and find operations against one layout. */
9
11
  export class RecordGetOperation {
10
12
  layout;
@@ -131,15 +133,12 @@ export class RecordGetOperation {
131
133
  if (typeof item === 'string')
132
134
  return item;
133
135
  // Re-write date into correct format
134
- return item
135
- .moment
136
- .clone()
137
- .utcOffset(this.layout.database.host.timezoneOffsetFunc(item.moment))
138
- .format(item.type === 'date'
139
- ? this.layout.database.host.dateFormat
140
- : item.type === 'time'
141
- ? this.layout.database.host.timeFormat
142
- : this.layout.database.host.timeStampFormat);
136
+ if (item instanceof Temporal.PlainDateTime)
137
+ return temporalToString(item, this.layout.database.host.timeStampFormat);
138
+ if (item instanceof Temporal.PlainDate)
139
+ return temporalToString(item, this.layout.database.host.dateFormat);
140
+ if (item instanceof Temporal.PlainTime)
141
+ return temporalToString(item, this.layout.database.host.timeFormat);
143
142
  }).join('');
144
143
  }
145
144
  return out;