@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.
- package/README.md +60 -196
- package/dist/bin/generateTypes.js +3 -3
- package/dist/bin/stressSearch.d.ts +2 -0
- package/dist/bin/stressSearch.js +243 -0
- package/dist/connection/CookieJar.d.ts +19 -0
- package/dist/connection/CookieJar.js +99 -0
- package/dist/connection/FMHost.d.ts +25 -11
- package/dist/connection/FMHost.js +29 -11
- package/dist/connection/HostBase.d.ts +4 -4
- package/dist/connection/Session.d.ts +34 -0
- package/dist/connection/Session.js +109 -0
- package/dist/connection/database.d.ts +34 -31
- package/dist/connection/database.js +72 -133
- package/dist/connection/databaseBase.d.ts +22 -10
- package/dist/connection/databaseConstantSession.d.ts +15 -0
- package/dist/connection/databaseConstantSession.js +70 -0
- package/dist/connection/databaseSessionPool.d.ts +14 -0
- package/dist/connection/databaseSessionPool.js +148 -0
- package/dist/index.d.ts +24 -4
- package/dist/index.js +15 -1
- package/dist/layouts/layout.d.ts +5 -3
- package/dist/layouts/layout.js +11 -15
- package/dist/layouts/layoutBase.d.ts +3 -2
- package/dist/layouts/layoutInterface.d.ts +2 -1
- package/dist/layouts/layoutRecordManager.d.ts +6 -15
- package/dist/layouts/layoutRecordManager.js +6 -15
- package/dist/models/apiResults.d.ts +209 -93
- package/dist/models/apiResults.js +95 -25
- package/dist/records/field.d.ts +17 -2
- package/dist/records/field.js +161 -237
- package/dist/records/fields/baseField.d.ts +57 -0
- package/dist/records/fields/baseField.js +59 -0
- package/dist/records/fields/containerField.d.ts +35 -0
- package/dist/records/fields/containerField.js +78 -0
- package/dist/records/fields/field.d.ts +3 -0
- package/dist/records/fields/field.js +1 -0
- package/dist/records/fields/valueField.d.ts +41 -0
- package/dist/records/fields/valueField.js +108 -0
- package/dist/records/getOperations/recordGetOperation.d.ts +7 -4
- package/dist/records/getOperations/recordGetOperation.js +22 -26
- package/dist/records/layoutRecord.d.ts +27 -9
- package/dist/records/layoutRecord.js +84 -97
- package/dist/records/portal.d.ts +1 -5
- package/dist/records/portal.js +0 -4
- package/dist/records/portalBase.d.ts +1 -2
- package/dist/records/portalRecord.d.ts +7 -4
- package/dist/records/portalRecord.js +12 -0
- package/dist/records/recordBase.d.ts +8 -6
- package/dist/records/recordBase.js +32 -33
- package/dist/types.d.ts +42 -18
- package/dist/types.js +12 -0
- package/dist/utils/addHeaders.d.ts +5 -0
- package/dist/utils/addHeaders.js +24 -0
- package/dist/utils/query.d.ts +24 -8
- package/dist/utils/query.js +60 -20
- package/dist/utils/temporal.d.ts +9 -0
- package/dist/utils/temporal.js +110 -0
- 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
|
|
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("../
|
|
22
|
-
|
|
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 {
|
|
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
|
|
28
|
-
|
|
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 {
|
|
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.
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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] =
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
8
|
+
/** Shared database connection options. */
|
|
9
|
+
export interface databaseOptionsBase<CREDENTAILS = loginOptionsOAuth | loginOptionsFileMaker | loginOptionsClaris | loginOptionsToken> {
|
|
8
10
|
database: string;
|
|
9
11
|
debug?: boolean;
|
|
10
|
-
credentials:
|
|
12
|
+
credentials: CREDENTAILS;
|
|
11
13
|
}
|
|
12
|
-
|
|
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:
|
|
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
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,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
|
+
}
|
package/dist/utils/query.d.ts
CHANGED
|
@@ -1,16 +1,32 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Temporal } from 'temporal-polyfill';
|
|
2
2
|
export declare const FindRequestSymbol: unique symbol;
|
|
3
|
-
export
|
|
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 {};
|
package/dist/utils/query.js
CHANGED
|
@@ -1,15 +1,74 @@
|
|
|
1
1
|
/*
|
|
2
2
|
* Copyright (c) 2024. See LICENSE file for more information
|
|
3
3
|
*/
|
|
4
|
-
import
|
|
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
|
+
}
|