@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.
@@ -4,14 +4,14 @@ import { PortalRecord } from './portalRecord.js';
4
4
  import { Portal } from './portal.js';
5
5
  import { type LayoutInterface } from '../layouts/layoutInterface.js';
6
6
  import { type LayoutRecordBase } from './layoutRecordBase.js';
7
- import { type ApiPortalData, type ApiRowDataDef } from '../models/apiResults.js';
7
+ import { type ApiFieldData, ApiFieldMetadata, type ApiPortalData, type ApiRowDataDef } from '../models/apiResults.js';
8
8
  import { type LayoutBase } from '../layouts/layoutBase.js';
9
- import { type Field, type FieldValue } from './field.js';
10
9
  import z from 'zod';
10
+ import { Field } from "./fields/field.js";
11
11
  export declare class LayoutRecord<LAYOUT extends LayoutInterface> extends RecordBase<LAYOUT['fields']> implements LayoutRecordBase {
12
12
  portals: LAYOUT['portals'];
13
13
  private readonly portalsToInclude;
14
- constructor(layout: LayoutBase, recordId: number | string, modId?: string | number, fieldData?: Record<string, string | number>, portalData?: z.infer<typeof ApiPortalData> | null, portalsToInclude?: Array<keyof LAYOUT['portals']>);
14
+ constructor(layout: LayoutBase, recordId: number | string, modId?: string | number, fieldData?: z.infer<typeof ApiFieldData>, portalData?: z.infer<typeof ApiPortalData> | null, portalsToInclude?: Array<keyof LAYOUT['portals']>);
15
15
  /** Returns all loaded portals on this record as an array. */
16
16
  get portalsArray(): Array<Portal<any>>;
17
17
  /**
@@ -21,6 +21,7 @@ export declare class LayoutRecord<LAYOUT extends LayoutInterface> extends Record
21
21
  * @throws {FMError} - If an error occurs during the commit process.
22
22
  */
23
23
  commit(extraBody?: extraBodyOptions): Promise<this>;
24
+ getFieldMetadata(fieldId: string): z.infer<typeof ApiFieldMetadata>;
24
25
  protected processPortalData(portalData: z.infer<typeof ApiPortalData>): void;
25
26
  /**
26
27
  * Reloads this record from FileMaker.
@@ -36,11 +37,11 @@ export declare class LayoutRecord<LAYOUT extends LayoutInterface> extends Record
36
37
  /** Deletes this record from FileMaker. */
37
38
  delete(): Promise<void>;
38
39
  /** Returns this record's edited fields as a FileMaker API payload. */
39
- fieldsToObject(filter?: (a: Field<FieldValue>) => boolean): Omit<z.infer<typeof ApiRowDataDef>, 'portalData'>;
40
+ fieldsToObject(filter?: (a: Field) => boolean): Omit<z.infer<typeof ApiRowDataDef>, 'portalData'>;
40
41
  /** Returns this record and its portal edits as a FileMaker API payload. */
41
- toObject(filter?: (a: Field<FieldValue>) => any, portalFilter?: (a: Portal<any>) => any, portalRowFilter?: (a: PortalRecord<any>) => any, portalFieldFilter?: (a: Field<FieldValue>) => any): {
42
- fieldData: Record<string, string | number>;
43
- portalData: Record<string, Record<string, string | number>[]>;
42
+ toObject(filter?: (a: Field) => any, portalFilter?: (a: Portal<any>) => any, portalRowFilter?: (a: PortalRecord<any>) => any, portalFieldFilter?: (a: Field) => any): {
43
+ fieldData: Record<string, string | number | null>;
44
+ portalData: Record<string, Record<string, string | number | null>[]>;
44
45
  modId: string;
45
46
  recordId: string;
46
47
  portalDataInfo?: {
@@ -6,8 +6,8 @@ import { PortalRecord } from './portalRecord.js';
6
6
  import { Portal } from './portal.js';
7
7
  import { FMError } from '../FMError.js';
8
8
  import { ApiRecordResponseObj } from '../models/apiResults.js';
9
- import moment from 'moment';
10
9
  import z from 'zod';
10
+ import { ValueFieldBase } from "./fields/valueField.js";
11
11
  export class LayoutRecord extends RecordBase {
12
12
  portals = {};
13
13
  portalsToInclude;
@@ -67,6 +67,7 @@ export class LayoutRecord extends RecordBase {
67
67
  }
68
68
  this.recordId = parseInt(res.recordId);
69
69
  this.modId = parseInt(res.modId);
70
+ this._onSave();
70
71
  return this;
71
72
  }
72
73
  // for (let item of Object.keys(data)) extraBody[item] = data[item]
@@ -90,6 +91,15 @@ export class LayoutRecord extends RecordBase {
90
91
  this._onSave();
91
92
  return this;
92
93
  }
94
+ getFieldMetadata(fieldId) {
95
+ if (!this.layout.metadata) {
96
+ throw new Error("Field metadata not found. Ensure you run layout.getLayoutMeta() first.");
97
+ }
98
+ let result = this.layout.metadata.fieldMetaData.find(i => i.name === fieldId);
99
+ if (!result)
100
+ throw new Error("Field metadata not found. Ensure you run layout.getLayoutMeta() first.");
101
+ return result;
102
+ }
93
103
  processPortalData(portalData) {
94
104
  for (const portalName of Object.keys(portalData)) {
95
105
  const _portal = new Portal(this, portalName);
@@ -161,37 +171,19 @@ export class LayoutRecord extends RecordBase {
161
171
  this.emit('deleted');
162
172
  }
163
173
  /** Returns this record's edited fields as a FileMaker API payload. */
164
- fieldsToObject(filter = (a) => a.edited) {
174
+ fieldsToObject(filter = (a) => a instanceof ValueFieldBase && a.edited) {
165
175
  const fieldsProcessed = {};
166
- let field;
167
- for (field of this.fieldsArray.filter(field => filter(field))) {
168
- let value = field.value;
169
- if (value instanceof Date) {
170
- let _value = moment(value);
171
- _value = _value
172
- .utcOffset(this.layout.database.host.timezoneOffsetFunc(_value));
173
- switch (field.metadata.result) {
174
- case 'time':
175
- value = _value.format(this.layout.database.host.timeFormat);
176
- break;
177
- case 'date':
178
- value = _value.format(this.layout.database.host.dateFormat);
179
- break;
180
- default:
181
- value = _value.format(this.layout.database.host.timeStampFormat);
182
- }
183
- }
184
- fieldsProcessed[field.id] = value;
176
+ for (const field of this.fieldsArray.filter(field => filter(field))) {
177
+ fieldsProcessed[field.id] = field.serializeRawValue();
185
178
  }
186
- const obj = {
179
+ return {
187
180
  recordId: this.recordId.toString(),
188
181
  modId: this.modId.toString(),
189
182
  fieldData: fieldsProcessed
190
183
  };
191
- return obj;
192
184
  }
193
185
  /** Returns this record and its portal edits as a FileMaker API payload. */
194
- toObject(filter = (a) => a.edited, portalFilter = (a) => a.records.find(record => record.edited), portalRowFilter = (a) => a.edited, portalFieldFilter = (a) => a.edited) {
186
+ toObject(filter = (a) => a instanceof ValueFieldBase && a.edited, portalFilter = (a) => a.records.find(record => record.edited), portalRowFilter = (a) => a.edited, portalFieldFilter = (a) => a instanceof ValueFieldBase && a.edited) {
195
187
  const obj = {
196
188
  ...this.fieldsToObject(filter),
197
189
  portalData: {}
@@ -1,7 +1,7 @@
1
1
  import { PortalRecord } from './portalRecord.js';
2
- import { type RecordFieldsMap } from '../layouts/recordFieldsMap.js';
3
2
  import { type PortalBase } from './portalBase.js';
4
3
  import { type LayoutRecord } from './layoutRecord.js';
4
+ import { RecordFieldsMap } from "../layouts/layoutInterface.js";
5
5
  /**
6
6
  * Represents a portal.
7
7
  *
@@ -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,10 +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';
7
5
  import { type z } from 'zod';
6
+ import { RecordFieldsMap } from "../layouts/layoutInterface.js";
7
+ import { Field } from "./fields/field.js";
8
8
  export declare abstract class RecordBase<T extends RecordFieldsMap> extends EventEmitter {
9
9
  readonly layout: LayoutBase;
10
10
  readonly type: RecordTypes;
@@ -25,7 +25,8 @@ export declare abstract class RecordBase<T extends RecordFieldsMap> extends Even
25
25
  * @returns {boolean} A boolean value indicating whether any of the fields have been edited.
26
26
  */
27
27
  get edited(): boolean;
28
- get fieldsArray(): Array<Field<FieldValue>>;
28
+ get fieldsArray(): Array<Field>;
29
+ abstract getFieldMetadata(fieldId: string): z.infer<typeof ApiFieldMetadata>;
29
30
  protected processFieldData(fieldData: z.infer<typeof ApiFieldData>): T;
30
31
  _onSave(): void;
31
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
@@ -2,7 +2,7 @@ import type * as http from 'http';
2
2
  import { type FMError } from './FMError.js';
3
3
  import { type LayoutInterface } from './layouts/layoutInterface.js';
4
4
  import z from "zod";
5
- export type PickPortals<LAYOUT extends LayoutInterface, PORTALS extends string | number | symbol = ''> = Omit<LAYOUT, 'portals'> & {
5
+ export type PickPortals<LAYOUT extends LayoutInterface, PORTALS extends keyof LAYOUT['portals'] = never> = Omit<LAYOUT, 'portals'> & {
6
6
  portals: Pick<LAYOUT['portals'], PORTALS>;
7
7
  };
8
8
  /** Shared database connection options. */
@@ -1,23 +1,32 @@
1
- import { type Moment } from 'moment';
1
+ import { Temporal } from 'temporal-polyfill';
2
2
  export declare const FindRequestSymbol: unique symbol;
3
- /** Represents a date, time, or timestamp value passed into `query(...)`. */
4
- export interface TimestampType {
5
- type: 'date' | 'time' | 'timestamp';
6
- moment: Moment;
7
- }
3
+ export type TimestampType = Temporal.PlainDate | Temporal.PlainTime | Temporal.PlainDateTime;
8
4
  type QueryParameter = string | number | TimestampType;
9
5
  /** Represents a FileMaker find value produced by `query(...)`. */
10
6
  export interface Query {
11
7
  [FindRequestSymbol]: Array<string | TimestampType>;
12
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;
13
28
  /** Escapes special characters in a raw FileMaker find string. */
14
29
  export declare function queryEscape(str: string): string;
15
30
  /** Builds a FileMaker find value using template-string syntax. */
16
31
  export declare function query(strings: TemplateStringsArray, ...args: QueryParameter[]): Query;
17
- /** Marks a value as a FileMaker date for use inside `query(...)`. */
18
- export declare function asDate(date: Date | Moment): TimestampType;
19
- /** Marks a value as a FileMaker time for use inside `query(...)`. */
20
- export declare function asTime(date: Date | Moment): TimestampType;
21
- /** Marks a value as a FileMaker timestamp for use inside `query(...)`. */
22
- export declare function asTimestamp(date: Date | Moment): TimestampType;
23
32
  export {};
@@ -1,9 +1,66 @@
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
+ }
7
64
  /** Escapes special characters in a raw FileMaker find string. */
8
65
  export function queryEscape(str) {
9
66
  for (const char of SPECIAL_CHARACTERS) {
@@ -28,25 +85,3 @@ export function query(strings, ...args) {
28
85
  }).flat(1);
29
86
  return { [FindRequestSymbol]: query };
30
87
  }
31
- /** Marks a value as a FileMaker date for use inside `query(...)`. */
32
- export function asDate(date) {
33
- return {
34
- type: 'date',
35
- moment: moment(date)
36
- };
37
- }
38
- /** Marks a value as a FileMaker time for use inside `query(...)`. */
39
- export function asTime(date) {
40
- return {
41
- type: 'time',
42
- moment: moment(date)
43
- };
44
- }
45
- /** Marks a value as a FileMaker timestamp for use inside `query(...)`. */
46
- export function asTimestamp(date) {
47
- return {
48
- type: 'timestamp',
49
- moment: moment(date)
50
- };
51
- }
52
- 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
+ }
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@jd-data-limited/easy-fm",
3
- "version": "5.0.0-beta.1",
3
+ "version": "5.0.0-beta.2",
4
4
  "dependencies": {
5
- "commander": "^14.0.3",
6
- "moment": "^2.30.1",
7
- "zod": "^4.4.3"
5
+ "commander": "^15.0.0",
6
+ "temporal-polyfill": "^1.0.5",
7
+ "zod": "^4.6.5"
8
8
  },
9
9
  "type": "module",
10
10
  "main": "dist/index.js",
@@ -40,30 +40,30 @@
40
40
  "node": ">=22.22.3"
41
41
  },
42
42
  "devDependencies": {
43
- "@babel/cli": "^7.29.7",
44
- "@babel/core": "^7.29.7",
45
- "@babel/preset-env": "^7.29.7",
46
- "@babel/preset-typescript": "^7.29.7",
43
+ "@babel/cli": "^8.0.6",
44
+ "@babel/core": "^8.0.6",
45
+ "@babel/preset-env": "^8.0.6",
46
+ "@babel/preset-typescript": "^8.0.1",
47
47
  "@types/btoa": "^1.2.5",
48
48
  "@types/events": "^3.0.3",
49
49
  "@types/inquirer": "^9.0.10",
50
- "@types/jest": "^29.5.14",
51
- "@types/node": "^22.19.21",
52
- "@typescript-eslint/eslint-plugin": "^6.21.0",
53
- "babel-jest": "^29.7.0",
54
- "dotenv": "^16.6.1",
55
- "eslint": "^8.57.1",
50
+ "@types/jest": "^30.0.0",
51
+ "@types/node": "^26.6.2",
52
+ "@typescript-eslint/eslint-plugin": "^8.70.1",
53
+ "babel-jest": "^30.5.2",
54
+ "dotenv": "^18.0.2",
55
+ "eslint": "^10.11.0",
56
56
  "eslint-config-standard-with-typescript": "^43.0.1",
57
57
  "eslint-plugin-import": "^2.32.0",
58
- "eslint-plugin-n": "^16.6.2",
59
- "eslint-plugin-promise": "^6.6.0",
60
- "inquirer": "^9.3.8",
61
- "jest": "^29.7.0",
62
- "ts-jest": "^29.4.11",
63
- "ts-morph": "^21.0.1",
64
- "tsx": "^4.22.4",
65
- "typedoc": "^0.28.19",
66
- "typescript": "^5.9.3"
58
+ "eslint-plugin-n": "^18.3.0",
59
+ "eslint-plugin-promise": "^7.3.0",
60
+ "inquirer": "^14.2.2",
61
+ "jest": "^30.5.2",
62
+ "ts-jest": "^29.4.12",
63
+ "ts-morph": "^28.0.0",
64
+ "tsx": "^4.23.15",
65
+ "typedoc": "^0.28.20",
66
+ "typescript": "^6.0.3"
67
67
  },
68
68
  "bin": {
69
69
  "easyfm": "dist/bin/cli.js"
@@ -71,6 +71,7 @@
71
71
  "scripts": {
72
72
  "build": "tsc",
73
73
  "test": "npm run build && jest",
74
+ "test:types": "tsc --project tsconfig.test.json",
74
75
  "test:coverage": "jest --coverage --testPathIgnorePatterns='/__tests__/database.test.ts$'",
75
76
  "test:integration": "npm run build && jest __tests__/database.test.ts --runInBand",
76
77
  "test:unit": "jest __tests__/query.test.ts __tests__/CookieJar.test.ts --runInBand",