@jd-data-limited/easy-fm 4.1.15 → 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.
Files changed (58) hide show
  1. package/README.md +60 -196
  2. package/dist/bin/generateTypes.js +3 -3
  3. package/dist/bin/stressSearch.d.ts +2 -0
  4. package/dist/bin/stressSearch.js +243 -0
  5. package/dist/connection/CookieJar.d.ts +19 -0
  6. package/dist/connection/CookieJar.js +99 -0
  7. package/dist/connection/FMHost.d.ts +25 -11
  8. package/dist/connection/FMHost.js +29 -11
  9. package/dist/connection/HostBase.d.ts +4 -4
  10. package/dist/connection/Session.d.ts +34 -0
  11. package/dist/connection/Session.js +109 -0
  12. package/dist/connection/database.d.ts +34 -31
  13. package/dist/connection/database.js +72 -133
  14. package/dist/connection/databaseBase.d.ts +22 -10
  15. package/dist/connection/databaseConstantSession.d.ts +15 -0
  16. package/dist/connection/databaseConstantSession.js +70 -0
  17. package/dist/connection/databaseSessionPool.d.ts +14 -0
  18. package/dist/connection/databaseSessionPool.js +148 -0
  19. package/dist/index.d.ts +24 -4
  20. package/dist/index.js +15 -1
  21. package/dist/layouts/layout.d.ts +5 -3
  22. package/dist/layouts/layout.js +11 -15
  23. package/dist/layouts/layoutBase.d.ts +3 -2
  24. package/dist/layouts/layoutInterface.d.ts +2 -1
  25. package/dist/layouts/layoutRecordManager.d.ts +6 -15
  26. package/dist/layouts/layoutRecordManager.js +6 -15
  27. package/dist/models/apiResults.d.ts +209 -93
  28. package/dist/models/apiResults.js +95 -25
  29. package/dist/records/field.d.ts +17 -2
  30. package/dist/records/field.js +161 -237
  31. package/dist/records/fields/baseField.d.ts +57 -0
  32. package/dist/records/fields/baseField.js +59 -0
  33. package/dist/records/fields/containerField.d.ts +35 -0
  34. package/dist/records/fields/containerField.js +78 -0
  35. package/dist/records/fields/field.d.ts +3 -0
  36. package/dist/records/fields/field.js +1 -0
  37. package/dist/records/fields/valueField.d.ts +41 -0
  38. package/dist/records/fields/valueField.js +108 -0
  39. package/dist/records/getOperations/recordGetOperation.d.ts +7 -4
  40. package/dist/records/getOperations/recordGetOperation.js +22 -26
  41. package/dist/records/layoutRecord.d.ts +27 -9
  42. package/dist/records/layoutRecord.js +84 -97
  43. package/dist/records/portal.d.ts +1 -5
  44. package/dist/records/portal.js +0 -4
  45. package/dist/records/portalBase.d.ts +1 -2
  46. package/dist/records/portalRecord.d.ts +7 -4
  47. package/dist/records/portalRecord.js +12 -0
  48. package/dist/records/recordBase.d.ts +8 -6
  49. package/dist/records/recordBase.js +32 -33
  50. package/dist/types.d.ts +42 -18
  51. package/dist/types.js +12 -0
  52. package/dist/utils/addHeaders.d.ts +5 -0
  53. package/dist/utils/addHeaders.js +24 -0
  54. package/dist/utils/query.d.ts +24 -8
  55. package/dist/utils/query.js +60 -20
  56. package/dist/utils/temporal.d.ts +9 -0
  57. package/dist/utils/temporal.js +110 -0
  58. package/package.json +35 -31
@@ -1,7 +1,6 @@
1
- import { type RecordFieldsMap } from '../layouts/recordFieldsMap.js';
2
1
  import { type RecordBase } from './recordBase.js';
3
2
  import { type LayoutRecord } from './layoutRecord.js';
4
- import { type LayoutInterface } from '../layouts/layoutInterface.js';
3
+ import { type LayoutInterface, RecordFieldsMap } from '../layouts/layoutInterface.js';
5
4
  export interface PortalBase<T extends RecordFieldsMap> {
6
5
  readonly record: LayoutRecord<LayoutInterface>;
7
6
  readonly name: string;
@@ -1,8 +1,10 @@
1
1
  import { RecordBase } from './recordBase.js';
2
2
  import { type extraBodyOptions, RecordTypes } from '../types.js';
3
- import { type RecordFieldsMap } from '../layouts/recordFieldsMap.js';
4
3
  import { type PortalBase } from './portalBase.js';
5
- import { type Field, type FieldValue } from './field.js';
4
+ import z from "zod";
5
+ import { ApiFieldMetadata } from "../models/apiResults.js";
6
+ import { RecordFieldsMap } from "../layouts/layoutInterface.js";
7
+ import { Field } from "./fields/field.js";
6
8
  /**
7
9
  * Represents a PortalRecord, which is a record in a portal within a parent record.
8
10
  * @template T - The type of the record's field map.
@@ -18,8 +20,9 @@ export declare class PortalRecord<T extends RecordFieldsMap> extends RecordBase<
18
20
  * @param {extraBodyOptions} [extraBody={}] - The optional extra body options.
19
21
  * @returns {Promise} - A promise that resolves when the record is committed.
20
22
  */
21
- commit(extraBody?: extraBodyOptions): Promise<import("./layoutRecord.js").LayoutRecord<import("../index.js").LayoutInterface>>;
22
- toObject(fieldFilter: (a: Field<FieldValue>) => boolean): {
23
+ commit(extraBody?: extraBodyOptions): Promise<import("./layoutRecord.js").LayoutRecord<import("../layouts/layoutInterface.js").LayoutInterface>>;
24
+ getFieldMetadata(fieldId: string): z.infer<typeof ApiFieldMetadata>;
25
+ toObject(fieldFilter: (a: Field) => boolean): {
23
26
  modId?: string;
24
27
  recordId?: string;
25
28
  } & Record<string, string>;
@@ -27,6 +27,18 @@ export class PortalRecord extends RecordBase {
27
27
  async commit(extraBody = {}) {
28
28
  return await this.portal.record.commit(extraBody);
29
29
  }
30
+ getFieldMetadata(fieldId) {
31
+ if (!this.layout.metadata) {
32
+ throw new Error("Field metadata not found. Ensure you run layout.getLayoutMeta() first.");
33
+ }
34
+ const portalMetadata = this.portal
35
+ ? this.layout.metadata.portalMetaData[this.portal.name] ?? []
36
+ : Object.values(this.layout.metadata.portalMetaData).flat();
37
+ const result = portalMetadata.find(i => i.name === fieldId);
38
+ if (!result)
39
+ throw new Error("Field metadata not found. Ensure you run layout.getLayoutMeta() first.");
40
+ return result;
41
+ }
30
42
  toObject(fieldFilter) {
31
43
  const res = {
32
44
  recordId: this.recordId === -1 ? undefined : this.recordId.toString(),
@@ -1,9 +1,10 @@
1
1
  import { EventEmitter } from 'events';
2
2
  import { RecordTypes } from '../types.js';
3
- import { type RecordFieldsMap } from '../layouts/recordFieldsMap.js';
4
3
  import { type LayoutBase } from '../layouts/layoutBase.js';
5
- import { type ApiFieldData } from '../models/apiResults.js';
6
- import { Field, type FieldValue } from './field.js';
4
+ import { type ApiFieldData, type ApiFieldMetadata } from '../models/apiResults.js';
5
+ import { type z } from 'zod';
6
+ import { RecordFieldsMap } from "../layouts/layoutInterface.js";
7
+ import { Field } from "./fields/field.js";
7
8
  export declare abstract class RecordBase<T extends RecordFieldsMap> extends EventEmitter {
8
9
  readonly layout: LayoutBase;
9
10
  readonly type: RecordTypes;
@@ -16,7 +17,7 @@ export declare abstract class RecordBase<T extends RecordFieldsMap> extends Even
16
17
  */
17
18
  fields: T;
18
19
  protected portalData: any[];
19
- protected constructor(layout: LayoutBase, recordId: number, modId: number | undefined, fieldData: ApiFieldData);
20
+ protected constructor(layout: LayoutBase, recordId: number, modId: number | undefined, fieldData: z.infer<typeof ApiFieldData>);
20
21
  get endpoint(): string;
21
22
  /**
22
23
  * A boolean indicating whether this record has been modified and should be committed
@@ -24,7 +25,8 @@ export declare abstract class RecordBase<T extends RecordFieldsMap> extends Even
24
25
  * @returns {boolean} A boolean value indicating whether any of the fields have been edited.
25
26
  */
26
27
  get edited(): boolean;
27
- get fieldsArray(): Array<Field<FieldValue>>;
28
- protected processFieldData(fieldData: ApiFieldData): T;
28
+ get fieldsArray(): Array<Field>;
29
+ abstract getFieldMetadata(fieldId: string): z.infer<typeof ApiFieldMetadata>;
30
+ protected processFieldData(fieldData: z.infer<typeof ApiFieldData>): T;
29
31
  _onSave(): void;
30
32
  }
@@ -2,9 +2,9 @@
2
2
  * Copyright (c) 2023-2024. See LICENSE file for more information
3
3
  */
4
4
  import { EventEmitter } from 'events';
5
- import * as moment from 'moment';
6
5
  import { RecordTypes } from '../types.js';
7
- import { Field } from './field.js';
6
+ import { DateField, NumberField, TextField, TimeField, TimeStampField, ValueFieldBase } from "./fields/valueField.js";
7
+ import { ContainerField } from "./fields/containerField.js";
8
8
  export class RecordBase extends EventEmitter {
9
9
  layout;
10
10
  type = RecordTypes.UNKNOWN;
@@ -33,49 +33,48 @@ export class RecordBase extends EventEmitter {
33
33
  * @returns {boolean} A boolean value indicating whether any of the fields have been edited.
34
34
  */
35
35
  get edited() {
36
- return !!this.fieldsArray.find(i => i.edited);
36
+ return !!this.fieldsArray.find(i => i instanceof ValueFieldBase && i.edited);
37
37
  }
38
38
  get fieldsArray() {
39
39
  return Object.values(this.fields);
40
40
  }
41
41
  processFieldData(fieldData) {
42
42
  const fields = {};
43
- for (const key of Object.keys(fieldData)) {
44
- const _field = new Field(this, key, fieldData[key]);
45
- if (fieldData[key]) {
46
- if (_field.metadata.result === 'timeStamp') {
47
- let date = moment.default(fieldData[key]);
48
- date = date
49
- .utcOffset(this.layout.database.host.timezoneOffsetFunc(date), true)
50
- .local();
51
- _field.set(date);
52
- _field.updateOriginalContents();
53
- }
54
- else if (_field.metadata.result === 'time') {
55
- let date = moment.default(fieldData[key]);
56
- date = date
57
- .utcOffset(this.layout.database.host.timezoneOffsetFunc(date), true)
58
- .local();
59
- _field.set(date);
60
- _field.updateOriginalContents();
61
- }
62
- else if (_field.metadata.result === 'date') {
63
- let date = moment.default(fieldData[key]);
64
- date = date
65
- .utcOffset(this.layout.database.host.timezoneOffsetFunc(date), true)
66
- .local();
67
- _field.set(date);
68
- _field.updateOriginalContents();
69
- }
43
+ for (const [key, value] of Object.entries(fieldData)) {
44
+ const fieldMeta = this.getFieldMetadata(key);
45
+ let field;
46
+ switch (fieldMeta.result) {
47
+ case "text":
48
+ field = new TextField(this, key, value);
49
+ break;
50
+ case "number":
51
+ field = new NumberField(this, key, value);
52
+ break;
53
+ case "container":
54
+ field = new ContainerField(this, key, value);
55
+ break;
56
+ case "timeStamp":
57
+ field = new TimeStampField(this, key, value);
58
+ break;
59
+ case "time":
60
+ field = new TimeField(this, key, value);
61
+ break;
62
+ case "date":
63
+ field = new DateField(this, key, value);
64
+ break;
65
+ default:
66
+ throw new Error(`Attempted to parse unknown field type: ${fieldMeta.result}`);
70
67
  }
71
- fields[key] = _field;
68
+ fields[key] = field;
72
69
  }
73
70
  this.fields = fields;
74
71
  return fields;
75
72
  }
76
73
  _onSave() {
77
74
  this.emit('saved');
78
- for (const field of this.fieldsArray)
79
- field.updateOriginalContents();
75
+ for (const field of this.fieldsArray) {
76
+ if (field instanceof ValueFieldBase)
77
+ field.updateOriginalContents();
78
+ }
80
79
  }
81
80
  }
package/dist/types.d.ts CHANGED
@@ -1,24 +1,29 @@
1
1
  import type * as http from 'http';
2
2
  import { type FMError } from './FMError.js';
3
3
  import { type LayoutInterface } from './layouts/layoutInterface.js';
4
- export type PickPortals<LAYOUT extends LayoutInterface, PORTALS extends string | number | symbol = ''> = Omit<LAYOUT, 'portals'> & {
4
+ import z from "zod";
5
+ export type PickPortals<LAYOUT extends LayoutInterface, PORTALS extends keyof LAYOUT['portals'] = never> = Omit<LAYOUT, 'portals'> & {
5
6
  portals: Pick<LAYOUT['portals'], PORTALS>;
6
7
  };
7
- export interface databaseOptionsBase {
8
+ /** Shared database connection options. */
9
+ export interface databaseOptionsBase<CREDENTAILS = loginOptionsOAuth | loginOptionsFileMaker | loginOptionsClaris | loginOptionsToken> {
8
10
  database: string;
9
11
  debug?: boolean;
10
- credentials: loginOptionsOAuth | loginOptionsFileMaker | loginOptionsClaris | loginOptionsToken;
12
+ credentials: CREDENTAILS;
11
13
  }
12
- export interface databaseOptionsWithExternalSources extends databaseOptionsBase {
14
+ /** Database options including any external FileMaker sources. */
15
+ export interface databaseOptionsWithExternalSources<CREDENTIALS = loginOptionsOAuth | loginOptionsFileMaker | loginOptionsClaris | loginOptionsToken> extends databaseOptionsBase<CREDENTIALS> {
13
16
  database: string;
14
17
  debug?: boolean;
15
- credentials: loginOptionsOAuth | loginOptionsFileMaker | loginOptionsClaris | loginOptionsToken;
16
- externalSources: databaseOptionsBase[];
18
+ credentials: CREDENTIALS;
19
+ externalSources: Array<databaseOptionsBase<loginOptionsFileMaker>>;
17
20
  }
21
+ /** Use an existing FileMaker Data API token. */
18
22
  export interface loginOptionsToken {
19
23
  method: 'token';
20
24
  token: string;
21
25
  }
26
+ /** Use FileMaker Data API OAuth headers to create session. */
22
27
  export interface loginOptionsOAuth {
23
28
  method: 'oauth';
24
29
  oauth: {
@@ -26,27 +31,36 @@ export interface loginOptionsOAuth {
26
31
  requestIdentifier: string;
27
32
  };
28
33
  }
34
+ /** Use standard FileMaker username/password login. */
29
35
  export interface loginOptionsFileMaker {
30
36
  method: 'filemaker';
31
37
  username: string;
32
38
  password: string;
39
+ /**
40
+ * sessionPoolSize - The maximum number of sessions to keep open in the pool. Default is 4.
41
+ */
42
+ sessionPoolSize?: number;
33
43
  }
44
+ /** Use Claris authentication for session creation. */
34
45
  export interface loginOptionsClaris {
35
46
  method: 'claris';
36
47
  claris: {
37
48
  fmid: string;
38
49
  };
39
50
  }
51
+ /** Portal fetch paging options for one portal. */
40
52
  export interface portalFetchData<T = string> {
41
53
  portalName: T;
42
54
  offset: number;
43
55
  limit: number;
44
56
  }
57
+ /** FileMaker scripts to run before, during, or after request processing. */
45
58
  export interface ScriptRequestData {
46
59
  prerequest?: Script;
47
60
  presort?: Script;
48
61
  after?: Script;
49
62
  }
63
+ /** Additional body options accepted by write operations. */
50
64
  export interface extraBodyOptions {
51
65
  scripts?: ScriptRequestData;
52
66
  options?: {
@@ -68,10 +82,12 @@ export interface extraBodyOptions {
68
82
  recordId: number;
69
83
  }>;
70
84
  }
85
+ /** Simple script reference with optional parameter. */
71
86
  export interface Script {
72
87
  name: string;
73
88
  parameter: string;
74
89
  }
90
+ /** Raw record payload shape returned by FileMaker Data API. */
75
91
  export interface recordObject {
76
92
  recordId: number;
77
93
  modId: number;
@@ -79,6 +95,7 @@ export interface recordObject {
79
95
  portalData?: portalDataObject;
80
96
  }
81
97
  export type portalDataObject = Record<string, recordObject>;
98
+ /** Field metadata returned by layout metadata endpoint. */
82
99
  export interface FieldMetaData {
83
100
  name: string;
84
101
  type: string;
@@ -95,38 +112,45 @@ export interface FieldMetaData {
95
112
  repetitionStart: number;
96
113
  repetitionEnd: number;
97
114
  }
115
+ /** Layout metadata for top-level fields and portals. */
98
116
  export interface LayoutMetaData {
99
117
  fieldMetaData: FieldMetaData[];
100
118
  portalMetaData?: Record<string, FieldMetaData[]>;
101
119
  }
102
- export interface FMHostMetadata {
103
- productInfo: {
104
- buildDate: Date;
105
- name: string;
106
- version: string;
107
- dateFormat: string;
108
- timeFormat: string;
109
- timeStampFormat: string;
110
- };
111
- }
120
+ /** FileMaker host metadata returned by `/productInfo`. */
121
+ export declare const FMHostMetadata: z.ZodObject<{
122
+ productInfo: z.ZodObject<{
123
+ buildDate: z.ZodOptional<z.z.ZodCoercedDate<unknown>>;
124
+ name: z.ZodString;
125
+ version: z.ZodOptional<z.ZodString>;
126
+ dateFormat: z.ZodString;
127
+ timeFormat: z.ZodString;
128
+ timeStampFormat: z.ZodString;
129
+ }, z.z.core.$strip>;
130
+ }, z.z.core.$strip>;
131
+ /** Result returned when a script executes through API call. */
112
132
  export interface ScriptResult {
113
133
  scriptError?: FMError;
114
134
  scriptResult?: string;
115
135
  }
136
+ /** Container download result with payload and response metadata. */
116
137
  export interface ContainerBufferResult {
117
138
  buffer: Buffer;
118
139
  mime?: string;
119
140
  request: http.IncomingMessage;
120
141
  }
121
- export interface AuthorizationHeaders {
142
+ /** Standard JSON request headers with bearer authorization. */
143
+ export interface AuthorizationHeaders extends Record<string, string> {
122
144
  'Content-Type': 'application/json';
123
145
  Authorization: string;
124
146
  }
125
- export interface AuthorizationHeadersOAuth {
147
+ /** OAuth request headers required by FileMaker Data API. */
148
+ export interface AuthorizationHeadersOAuth extends Record<string, string> {
126
149
  'Content-Type': 'application/json';
127
150
  'X-FM-Data-OAuth-RequestId': string;
128
151
  'X-FM-Data-OAuth-Identifier': string;
129
152
  }
153
+ /** Options used when creating empty record shells. */
130
154
  export interface RecordFetchOptions {
131
155
  readonly portals: readonly string[];
132
156
  }
package/dist/types.js CHANGED
@@ -1,6 +1,18 @@
1
1
  /*
2
2
  * Copyright (c) 2022-2024. See LICENSE file for more information
3
3
  */
4
+ import z from "zod";
5
+ /** FileMaker host metadata returned by `/productInfo`. */
6
+ export const FMHostMetadata = z.object({
7
+ productInfo: z.object({
8
+ buildDate: z.coerce.date().optional(),
9
+ name: z.string(),
10
+ version: z.string().optional(),
11
+ dateFormat: z.string(),
12
+ timeFormat: z.string(),
13
+ timeStampFormat: z.string()
14
+ })
15
+ });
4
16
  export var RecordTypes;
5
17
  (function (RecordTypes) {
6
18
  RecordTypes[RecordTypes["UNKNOWN"] = 0] = "UNKNOWN";
@@ -0,0 +1,5 @@
1
+ /**
2
+ * addHeaders adds headers to the inputted request header object
3
+ * @param headers
4
+ */
5
+ export declare function addHeaders(req: RequestInit, headers: Record<string, string>): void;
@@ -0,0 +1,24 @@
1
+ /**
2
+ * addHeaders adds headers to the inputted request header object
3
+ * @param headers
4
+ */
5
+ export function addHeaders(req, headers) {
6
+ if (!req.headers) {
7
+ req.headers = headers;
8
+ }
9
+ else if (req.headers instanceof Headers) {
10
+ for (const key of Object.keys(headers)) {
11
+ req.headers.set(key, headers[key]);
12
+ }
13
+ }
14
+ else if (Array.isArray(req.headers)) {
15
+ for (const [key, value] of Object.entries(headers)) {
16
+ req.headers.push([key, value]);
17
+ }
18
+ }
19
+ else {
20
+ for (const key of Object.keys(headers)) {
21
+ req.headers[key] = headers[key];
22
+ }
23
+ }
24
+ }
@@ -1,16 +1,32 @@
1
- import { type Moment } from 'moment';
1
+ import { Temporal } from 'temporal-polyfill';
2
2
  export declare const FindRequestSymbol: unique symbol;
3
- export interface TimestampType {
4
- type: 'date' | 'time' | 'timestamp';
5
- moment: Moment;
6
- }
3
+ export type TimestampType = Temporal.PlainDate | Temporal.PlainTime | Temporal.PlainDateTime;
7
4
  type QueryParameter = string | number | TimestampType;
5
+ /** Represents a FileMaker find value produced by `query(...)`. */
8
6
  export interface Query {
9
7
  [FindRequestSymbol]: Array<string | TimestampType>;
10
8
  }
9
+ type LegacyDateLike = {
10
+ toDate: () => Date;
11
+ };
12
+ type DateInput = Date | LegacyDateLike;
13
+ /**
14
+ * Converts a JavaScript Date or legacy Moment-like value to a Temporal date.
15
+ * @deprecated Construct a `Temporal.PlainDate` directly instead.
16
+ */
17
+ export declare function asDate(value: DateInput | Temporal.PlainDate | Temporal.PlainDateTime): Temporal.PlainDate;
18
+ /**
19
+ * Converts a JavaScript Date or legacy Moment-like value to a Temporal time.
20
+ * @deprecated Construct a `Temporal.PlainTime` directly instead.
21
+ */
22
+ export declare function asTime(value: DateInput | Temporal.PlainTime | Temporal.PlainDateTime): Temporal.PlainTime;
23
+ /**
24
+ * Converts a JavaScript Date or legacy Moment-like value to a Temporal timestamp.
25
+ * @deprecated Construct a `Temporal.PlainDateTime` directly instead.
26
+ */
27
+ export declare function asTimestamp(value: DateInput | Temporal.PlainDateTime): Temporal.PlainDateTime;
28
+ /** Escapes special characters in a raw FileMaker find string. */
11
29
  export declare function queryEscape(str: string): string;
30
+ /** Builds a FileMaker find value using template-string syntax. */
12
31
  export declare function query(strings: TemplateStringsArray, ...args: QueryParameter[]): Query;
13
- export declare function asDate(date: Date | Moment): TimestampType;
14
- export declare function asTime(date: Date | Moment): TimestampType;
15
- export declare function asTimestamp(date: Date | Moment): TimestampType;
16
32
  export {};
@@ -1,15 +1,74 @@
1
1
  /*
2
2
  * Copyright (c) 2024. See LICENSE file for more information
3
3
  */
4
- import moment from 'moment';
4
+ import { Temporal } from 'temporal-polyfill';
5
5
  export const FindRequestSymbol = Symbol('easyfm-findrequest');
6
6
  const SPECIAL_CHARACTERS = ['\\', '=', '<', '≤', '≥', '>', '…', '...', '//', '@', '#', '*', '"', '~'];
7
+ function toDate(value) {
8
+ const date = value instanceof Date ? value : value.toDate();
9
+ if (Number.isNaN(date.getTime()))
10
+ throw new RangeError('Invalid date value');
11
+ return date;
12
+ }
13
+ /**
14
+ * Converts a JavaScript Date or legacy Moment-like value to a Temporal date.
15
+ * @deprecated Construct a `Temporal.PlainDate` directly instead.
16
+ */
17
+ export function asDate(value) {
18
+ if (value instanceof Temporal.PlainDate)
19
+ return value;
20
+ if (value instanceof Temporal.PlainDateTime)
21
+ return value.toPlainDate();
22
+ const date = toDate(value);
23
+ return Temporal.PlainDate.from({
24
+ year: date.getFullYear(),
25
+ month: date.getMonth() + 1,
26
+ day: date.getDate()
27
+ });
28
+ }
29
+ /**
30
+ * Converts a JavaScript Date or legacy Moment-like value to a Temporal time.
31
+ * @deprecated Construct a `Temporal.PlainTime` directly instead.
32
+ */
33
+ export function asTime(value) {
34
+ if (value instanceof Temporal.PlainTime)
35
+ return value;
36
+ if (value instanceof Temporal.PlainDateTime)
37
+ return value.toPlainTime();
38
+ const date = toDate(value);
39
+ return Temporal.PlainTime.from({
40
+ hour: date.getHours(),
41
+ minute: date.getMinutes(),
42
+ second: date.getSeconds(),
43
+ millisecond: date.getMilliseconds()
44
+ });
45
+ }
46
+ /**
47
+ * Converts a JavaScript Date or legacy Moment-like value to a Temporal timestamp.
48
+ * @deprecated Construct a `Temporal.PlainDateTime` directly instead.
49
+ */
50
+ export function asTimestamp(value) {
51
+ if (value instanceof Temporal.PlainDateTime)
52
+ return value;
53
+ const date = toDate(value);
54
+ return Temporal.PlainDateTime.from({
55
+ year: date.getFullYear(),
56
+ month: date.getMonth() + 1,
57
+ day: date.getDate(),
58
+ hour: date.getHours(),
59
+ minute: date.getMinutes(),
60
+ second: date.getSeconds(),
61
+ millisecond: date.getMilliseconds()
62
+ });
63
+ }
64
+ /** Escapes special characters in a raw FileMaker find string. */
7
65
  export function queryEscape(str) {
8
66
  for (const char of SPECIAL_CHARACTERS) {
9
67
  str = str.replace(char, `\\${char}`);
10
68
  }
11
69
  return str;
12
70
  }
71
+ /** Builds a FileMaker find value using template-string syntax. */
13
72
  export function query(strings, ...args) {
14
73
  const argStrings = args.map(item => {
15
74
  if (typeof item === 'number') {
@@ -26,22 +85,3 @@ export function query(strings, ...args) {
26
85
  }).flat(1);
27
86
  return { [FindRequestSymbol]: query };
28
87
  }
29
- export function asDate(date) {
30
- return {
31
- type: 'date',
32
- moment: moment(date)
33
- };
34
- }
35
- export function asTime(date) {
36
- return {
37
- type: 'time',
38
- moment: moment(date)
39
- };
40
- }
41
- export function asTimestamp(date) {
42
- return {
43
- type: 'timestamp',
44
- moment: moment(date)
45
- };
46
- }
47
- query;
@@ -0,0 +1,9 @@
1
+ import { Temporal } from 'temporal-polyfill';
2
+ export type TemporalValue = Temporal.PlainDate | Temporal.PlainTime | Temporal.PlainDateTime;
3
+ export type TemporalValueType = 'date' | 'time' | 'timestamp';
4
+ /** Formats a Temporal value using the supplied token-based format. */
5
+ export declare function temporalToString(value: TemporalValue, format: string): string;
6
+ export declare function stringToTemporal(value: string, type: 'date', format: string): Temporal.PlainDate;
7
+ export declare function stringToTemporal(value: string, type: 'time', format: string): Temporal.PlainTime;
8
+ export declare function stringToTemporal(value: string, type: 'timestamp', format: string): Temporal.PlainDateTime;
9
+ export declare function stringToTemporal(value: string, type: TemporalValueType, format: string): TemporalValue;
@@ -0,0 +1,110 @@
1
+ /*
2
+ * Copyright (c) 2023-2024. See LICENSE file for more information
3
+ */
4
+ import { Temporal } from 'temporal-polyfill';
5
+ const FORMAT_TOKEN_PATTERN = /yyyy|MM|dd|HH|mm|ss/g;
6
+ const TOKEN_PATTERNS = {
7
+ yyyy: '\\d{4}',
8
+ MM: '\\d{2}',
9
+ dd: '\\d{2}',
10
+ HH: '\\d{2}',
11
+ mm: '\\d{2}',
12
+ ss: '\\d{2}'
13
+ };
14
+ function normalizeFormat(format) {
15
+ return format.replace(/YYYY/g, 'yyyy').replace(/DD/g, 'dd');
16
+ }
17
+ function pad(value, length = 2) {
18
+ return value.toString().padStart(length, '0');
19
+ }
20
+ function invalidTemporalString(value, format) {
21
+ throw new RangeError(`Invalid Temporal value ${JSON.stringify(value)}; expected ${format}`);
22
+ }
23
+ function escapeRegExp(value) {
24
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
25
+ }
26
+ function tokensIn(format) {
27
+ const tokens = format.match(FORMAT_TOKEN_PATTERN);
28
+ if (!tokens)
29
+ throw new RangeError(`Format ${JSON.stringify(format)} contains no supported tokens`);
30
+ if (new Set(tokens).size !== tokens.length) {
31
+ throw new RangeError(`Format ${JSON.stringify(format)} contains duplicate tokens`);
32
+ }
33
+ return tokens;
34
+ }
35
+ function parserFor(format) {
36
+ const tokens = tokensIn(format);
37
+ let source = '^';
38
+ let lastIndex = 0;
39
+ for (const match of format.matchAll(FORMAT_TOKEN_PATTERN)) {
40
+ source += escapeRegExp(format.slice(lastIndex, match.index));
41
+ source += `(${TOKEN_PATTERNS[match[0]]})`;
42
+ lastIndex = match.index + match[0].length;
43
+ }
44
+ source += `${escapeRegExp(format.slice(lastIndex))}$`;
45
+ return { pattern: new RegExp(source), tokens };
46
+ }
47
+ /** Formats a Temporal value using the supplied token-based format. */
48
+ export function temporalToString(value, format) {
49
+ format = normalizeFormat(format);
50
+ const values = {};
51
+ if (value instanceof Temporal.PlainDateTime) {
52
+ Object.assign(values, {
53
+ yyyy: pad(value.year, 4), MM: pad(value.month), dd: pad(value.day),
54
+ HH: pad(value.hour), mm: pad(value.minute), ss: pad(value.second)
55
+ });
56
+ }
57
+ else if (value instanceof Temporal.PlainDate) {
58
+ Object.assign(values, { yyyy: pad(value.year, 4), MM: pad(value.month), dd: pad(value.day) });
59
+ }
60
+ else if (value instanceof Temporal.PlainTime) {
61
+ Object.assign(values, { HH: pad(value.hour), mm: pad(value.minute), ss: pad(value.second) });
62
+ }
63
+ else {
64
+ throw new TypeError('Expected a Temporal.PlainDate, Temporal.PlainTime, or Temporal.PlainDateTime');
65
+ }
66
+ tokensIn(format).forEach(token => {
67
+ if (values[token] === undefined)
68
+ throw new RangeError(`Token ${token} is not available for this Temporal type`);
69
+ });
70
+ return format.replace(FORMAT_TOKEN_PATTERN, token => values[token]);
71
+ }
72
+ /** Parses a Temporal value using the supplied token-based format. */
73
+ export function stringToTemporal(value, type, format) {
74
+ format = normalizeFormat(format);
75
+ const { pattern, tokens } = parserFor(format);
76
+ const match = pattern.exec(value);
77
+ if (!match)
78
+ return invalidTemporalString(value, format);
79
+ const parts = Object.fromEntries(tokens.map((token, index) => [token, Number(match[index + 1])]));
80
+ const required = type === 'date'
81
+ ? ['yyyy', 'MM', 'dd']
82
+ : type === 'time'
83
+ ? ['HH', 'mm', 'ss']
84
+ : ['yyyy', 'MM', 'dd', 'HH', 'mm', 'ss'];
85
+ if (tokens.length !== required.length || required.some(token => parts[token] === undefined)) {
86
+ throw new RangeError(`Format ${JSON.stringify(format)} does not match Temporal type ${type}`);
87
+ }
88
+ if (type === 'date') {
89
+ return Temporal.PlainDate.from({
90
+ year: parts.yyyy,
91
+ month: parts.MM,
92
+ day: parts.dd
93
+ }, { overflow: 'reject' });
94
+ }
95
+ else if (type === 'time') {
96
+ return Temporal.PlainTime.from({
97
+ hour: parts.HH,
98
+ minute: parts.mm,
99
+ second: parts.ss
100
+ }, { overflow: 'reject' });
101
+ }
102
+ return Temporal.PlainDateTime.from({
103
+ year: parts.yyyy,
104
+ month: parts.MM,
105
+ day: parts.dd,
106
+ hour: parts.HH,
107
+ minute: parts.mm,
108
+ second: parts.ss
109
+ }, { overflow: 'reject' });
110
+ }