@jd-data-limited/easy-fm 4.1.15 → 5.0.0-beta.1

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 (43) hide show
  1. package/README.md +60 -197
  2. package/dist/bin/generateTypes.js +1 -1
  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 +24 -8
  8. package/dist/connection/FMHost.js +28 -8
  9. package/dist/connection/HostBase.d.ts +4 -2
  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 +18 -1
  20. package/dist/index.js +12 -0
  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/layoutRecordManager.d.ts +6 -15
  25. package/dist/layouts/layoutRecordManager.js +6 -15
  26. package/dist/models/apiResults.d.ts +208 -93
  27. package/dist/models/apiResults.js +95 -25
  28. package/dist/records/field.d.ts +17 -2
  29. package/dist/records/field.js +161 -237
  30. package/dist/records/getOperations/recordGetOperation.d.ts +7 -4
  31. package/dist/records/getOperations/recordGetOperation.js +15 -18
  32. package/dist/records/layoutRecord.d.ts +24 -7
  33. package/dist/records/layoutRecord.js +68 -73
  34. package/dist/records/portal.d.ts +0 -4
  35. package/dist/records/portal.js +0 -4
  36. package/dist/records/recordBase.d.ts +3 -2
  37. package/dist/types.d.ts +41 -17
  38. package/dist/types.js +12 -0
  39. package/dist/utils/addHeaders.d.ts +5 -0
  40. package/dist/utils/addHeaders.js +24 -0
  41. package/dist/utils/query.d.ts +7 -0
  42. package/dist/utils/query.js +5 -0
  43. package/package.json +29 -26
@@ -5,7 +5,9 @@ import { RecordBase } from './recordBase.js';
5
5
  import { PortalRecord } from './portalRecord.js';
6
6
  import { Portal } from './portal.js';
7
7
  import { FMError } from '../FMError.js';
8
+ import { ApiRecordResponseObj } from '../models/apiResults.js';
8
9
  import moment from 'moment';
10
+ import z from 'zod';
9
11
  export class LayoutRecord extends RecordBase {
10
12
  portals = {};
11
13
  portalsToInclude;
@@ -16,11 +18,12 @@ export class LayoutRecord extends RecordBase {
16
18
  this.processPortalData(portalData);
17
19
  }
18
20
  }
21
+ /** Returns all loaded portals on this record as an array. */
19
22
  get portalsArray() {
20
23
  return Object.values(this.portals);
21
24
  }
22
25
  /**
23
- * Asynchronously commits the changes made to the current record.
26
+ * Saves this record back to FileMaker.
24
27
  * @param {extraBodyOptions} extraBody - The options for the extra body elements.
25
28
  * @returns {Promise<this>} - A promise that resolves with the modified object.
26
29
  * @throws {FMError} - If an error occurs during the commit process.
@@ -50,44 +53,42 @@ export class LayoutRecord extends RecordBase {
50
53
  data.fieldData.deleteRelated = extraBody.deleteRelatedRecords.map(i => `${i.table}.${i.recordId}`);
51
54
  if (this.recordId === -1) {
52
55
  // This is a new LayoutRecord
53
- const res = await this.layout.database._apiRequestJSON(`${this.layout.endpoint}/records`, {
56
+ const res = await this.layout.database.fetchJSON(`${this.layout.endpoint}/records`, {
54
57
  method: 'POST',
55
- body: JSON.stringify(data)
58
+ body: JSON.stringify(data),
59
+ type: z.object({
60
+ recordId: z.string(),
61
+ modId: z.string(),
62
+ scriptError: z.string().optional()
63
+ })
56
64
  });
57
- if (!res.response) {
58
- throw new FMError(res.messages[0].code, res.httpStatus, res);
59
- }
60
- else if (typeof res.response.scriptError !== 'undefined' && res.response.scriptError !== '0') {
61
- throw new FMError(res.response.scriptError, res.httpStatus, res);
62
- }
63
- else if (res.messages[0].code === '0') {
64
- this.recordId = parseInt(res.response.recordId);
65
- this.modId = parseInt(res.response.modId);
66
- return this;
67
- }
68
- else {
69
- throw new FMError(res.messages[0].code, res.httpStatus, res);
65
+ if (typeof res.scriptError !== 'undefined' && res.scriptError !== '0') {
66
+ throw new FMError(res.scriptError, res.httpStatus, res);
70
67
  }
68
+ this.recordId = parseInt(res.recordId);
69
+ this.modId = parseInt(res.modId);
70
+ return this;
71
71
  }
72
72
  // for (let item of Object.keys(data)) extraBody[item] = data[item]
73
- const res = await this.layout.database._apiRequestJSON(this.endpoint, {
73
+ const res = await this.layout.database.fetchJSON(this.endpoint, {
74
74
  method: 'PATCH',
75
- body: JSON.stringify(data)
75
+ body: JSON.stringify(data),
76
+ type: z.object({
77
+ modId: z.string(),
78
+ newPortalRecordInfo: z.array(z.object({
79
+ tableName: z.string(),
80
+ recordId: z.string(),
81
+ modId: z.string()
82
+ })).optional(),
83
+ scriptError: z.string().optional()
84
+ })
76
85
  });
77
- if (!res.response) {
78
- throw new FMError(res.messages[0].code, res.httpStatus, res);
79
- }
80
- else if (typeof res.response.scriptError !== 'undefined' && res.response.scriptError !== '0') {
81
- throw new FMError(res.response.scriptError, res.httpStatus, res);
82
- }
83
- else if (res.messages[0].code === '0') {
84
- this.modId = +res.response.modId;
85
- this._onSave();
86
- return this;
87
- }
88
- else {
89
- throw new FMError(res.messages[0].code, res.httpStatus, res);
86
+ if (typeof res.scriptError !== 'undefined' && res.scriptError !== '0') {
87
+ throw new FMError(res.scriptError, res.httpStatus, res);
90
88
  }
89
+ this.modId = +res.modId;
90
+ this._onSave();
91
+ return this;
91
92
  }
92
93
  processPortalData(portalData) {
93
94
  for (const portalName of Object.keys(portalData)) {
@@ -103,8 +104,8 @@ export class LayoutRecord extends RecordBase {
103
104
  }
104
105
  }
105
106
  /**
106
- * Re-fetches the current record from the database server.
107
- * Throws an error if commit() has not been called.
107
+ * Reloads this record from FileMaker.
108
+ * Throws if this record has not been saved yet.
108
109
  *
109
110
  * @return {Promise<this>} A Promise that resolves to this RecordBase instance if the record is successfully retrieved.
110
111
  * @throws {Error} If commit() has not been called.
@@ -116,57 +117,50 @@ export class LayoutRecord extends RecordBase {
116
117
  }
117
118
  if (!this.layout.metadata)
118
119
  await this.layout.getLayoutMeta();
119
- const res = await this.layout.database._apiRequestJSON(this.endpoint, {
120
- method: 'GET'
120
+ const res = await this.layout.database.fetchJSON(this.endpoint, {
121
+ type: ApiRecordResponseObj
121
122
  });
122
- if (res.response && res.messages[0].code === '0') {
123
- // console.log(res, res.response.data)
124
- this.modId = +res.response.data[0].modId;
125
- this.processFieldData(res.response.data[0].fieldData);
126
- this.portalData = [];
127
- if (res.response.data[0].portalData)
128
- this.processPortalData(res.response.data[0].portalData);
129
- return this;
130
- }
131
- else {
132
- throw new FMError(res.messages[0].code, res.httpStatus, res);
133
- }
123
+ // console.log(res, res.response.data)
124
+ this.modId = +res.data[0].modId;
125
+ this.processFieldData(res.data[0].fieldData);
126
+ this.portalData = [];
127
+ if (res.data[0].portalData)
128
+ this.processPortalData(res.data[0].portalData);
129
+ return this;
134
130
  }
131
+ /** Creates a duplicate of this record in FileMaker. */
135
132
  async duplicate() {
136
133
  const trace = new Error();
137
- const res = await this.layout.database._apiRequestJSON(this.endpoint, {
138
- method: 'POST'
134
+ const res = await this.layout.database.fetchJSON(this.endpoint, {
135
+ method: 'POST',
136
+ type: z.object({
137
+ recordId: z.string(),
138
+ modId: z.string(),
139
+ scriptError: z.string().optional()
140
+ })
139
141
  });
140
- if (!res.response) {
141
- throw new FMError(res.messages[0].code, res.httpStatus, res, trace);
142
- }
143
- else if (typeof res.response.scriptError !== 'undefined' && res.response.scriptError !== '0') {
144
- throw new FMError(res.response.scriptError, res.httpStatus, res, trace);
145
- }
146
- else if (res.messages[0].code === '0') {
147
- const data = this.toObject((a) => true, (a) => true, (a) => false, (a) => false);
148
- const _res = new LayoutRecord(this.layout, res.response.recordId, res.response.modId, data.fieldData, data.portalData);
149
- this.emit('duplicated');
150
- return _res;
151
- }
152
- else {
153
- throw new FMError(res.messages[0].code, res.httpStatus, res, trace);
142
+ if (typeof res.scriptError !== 'undefined' && res.scriptError !== '0') {
143
+ throw new FMError(res.scriptError, res.httpStatus, res, trace);
154
144
  }
145
+ const data = this.toObject((a) => true, (a) => true, (a) => false, (a) => false);
146
+ const _res = new LayoutRecord(this.layout, res.recordId, res.modId, data.fieldData, data.portalData);
147
+ this.emit('duplicated');
148
+ return _res;
155
149
  }
150
+ /** Deletes this record from FileMaker. */
156
151
  async delete() {
157
- const res = await this.layout.database._apiRequestJSON(this.endpoint, {
158
- method: 'DELETE'
152
+ const res = await this.layout.database.fetchJSON(this.endpoint, {
153
+ method: 'DELETE',
154
+ type: z.object({
155
+ scriptError: z.string().optional()
156
+ }).optional()
159
157
  });
160
- if (typeof res.response?.scriptError !== 'undefined' && res.response?.scriptError !== '0') {
161
- throw new FMError(res.response.scriptError, res.httpStatus, res);
162
- }
163
- else if (res.messages[0].code === '0') {
164
- this.emit('deleted');
165
- }
166
- else {
167
- throw new FMError(res.messages[0].code, res.httpStatus, res);
158
+ if (typeof res.scriptError !== 'undefined' && res.scriptError !== '0') {
159
+ throw new FMError(res.scriptError, res.httpStatus, res);
168
160
  }
161
+ this.emit('deleted');
169
162
  }
163
+ /** Returns this record's edited fields as a FileMaker API payload. */
170
164
  fieldsToObject(filter = (a) => a.edited) {
171
165
  const fieldsProcessed = {};
172
166
  let field;
@@ -196,6 +190,7 @@ export class LayoutRecord extends RecordBase {
196
190
  };
197
191
  return obj;
198
192
  }
193
+ /** Returns this record and its portal edits as a FileMaker API payload. */
199
194
  toObject(filter = (a) => a.edited, portalFilter = (a) => a.records.find(record => record.edited), portalRowFilter = (a) => a.edited, portalFieldFilter = (a) => a.edited) {
200
195
  const obj = {
201
196
  ...this.fieldsToObject(filter),
@@ -6,8 +6,6 @@ import { type LayoutRecord } from './layoutRecord.js';
6
6
  * Represents a portal.
7
7
  *
8
8
  * @template T - The type of RecordFieldsMap.
9
- * @class
10
- * @implements {PortalBase<T>}
11
9
  */
12
10
  export declare class Portal<T extends RecordFieldsMap> implements PortalBase<T> {
13
11
  readonly record: LayoutRecord<any>;
@@ -16,8 +14,6 @@ export declare class Portal<T extends RecordFieldsMap> implements PortalBase<T>
16
14
  constructor(record: LayoutRecord<any>, name: string);
17
15
  /**
18
16
  * Add a new record to the portal
19
- * @function create
20
- * @async
21
17
  * @summary Creates a new record.
22
18
  * @returns {Promise<PortalRecord<T>>} A Promise that resolves to the newly created record.
23
19
  */
@@ -6,8 +6,6 @@ import { PortalRecord } from './portalRecord.js';
6
6
  * Represents a portal.
7
7
  *
8
8
  * @template T - The type of RecordFieldsMap.
9
- * @class
10
- * @implements {PortalBase<T>}
11
9
  */
12
10
  export class Portal {
13
11
  record;
@@ -19,8 +17,6 @@ export class Portal {
19
17
  }
20
18
  /**
21
19
  * Add a new record to the portal
22
- * @function create
23
- * @async
24
20
  * @summary Creates a new record.
25
21
  * @returns {Promise<PortalRecord<T>>} A Promise that resolves to the newly created record.
26
22
  */
@@ -4,6 +4,7 @@ import { type RecordFieldsMap } from '../layouts/recordFieldsMap.js';
4
4
  import { type LayoutBase } from '../layouts/layoutBase.js';
5
5
  import { type ApiFieldData } from '../models/apiResults.js';
6
6
  import { Field, type FieldValue } from './field.js';
7
+ import { type z } from 'zod';
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
@@ -25,6 +26,6 @@ export declare abstract class RecordBase<T extends RecordFieldsMap> extends Even
25
26
  */
26
27
  get edited(): boolean;
27
28
  get fieldsArray(): Array<Field<FieldValue>>;
28
- protected processFieldData(fieldData: ApiFieldData): T;
29
+ protected processFieldData(fieldData: z.infer<typeof ApiFieldData>): T;
29
30
  _onSave(): void;
30
31
  }
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
+ import z from "zod";
4
5
  export type PickPortals<LAYOUT extends LayoutInterface, PORTALS extends string | number | symbol = ''> = 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,23 @@
1
1
  import { type Moment } from 'moment';
2
2
  export declare const FindRequestSymbol: unique symbol;
3
+ /** Represents a date, time, or timestamp value passed into `query(...)`. */
3
4
  export interface TimestampType {
4
5
  type: 'date' | 'time' | 'timestamp';
5
6
  moment: Moment;
6
7
  }
7
8
  type QueryParameter = string | number | TimestampType;
9
+ /** Represents a FileMaker find value produced by `query(...)`. */
8
10
  export interface Query {
9
11
  [FindRequestSymbol]: Array<string | TimestampType>;
10
12
  }
13
+ /** Escapes special characters in a raw FileMaker find string. */
11
14
  export declare function queryEscape(str: string): string;
15
+ /** Builds a FileMaker find value using template-string syntax. */
12
16
  export declare function query(strings: TemplateStringsArray, ...args: QueryParameter[]): Query;
17
+ /** Marks a value as a FileMaker date for use inside `query(...)`. */
13
18
  export declare function asDate(date: Date | Moment): TimestampType;
19
+ /** Marks a value as a FileMaker time for use inside `query(...)`. */
14
20
  export declare function asTime(date: Date | Moment): TimestampType;
21
+ /** Marks a value as a FileMaker timestamp for use inside `query(...)`. */
15
22
  export declare function asTimestamp(date: Date | Moment): TimestampType;
16
23
  export {};
@@ -4,12 +4,14 @@
4
4
  import moment from 'moment';
5
5
  export const FindRequestSymbol = Symbol('easyfm-findrequest');
6
6
  const SPECIAL_CHARACTERS = ['\\', '=', '<', '≤', '≥', '>', '…', '...', '//', '@', '#', '*', '"', '~'];
7
+ /** Escapes special characters in a raw FileMaker find string. */
7
8
  export function queryEscape(str) {
8
9
  for (const char of SPECIAL_CHARACTERS) {
9
10
  str = str.replace(char, `\\${char}`);
10
11
  }
11
12
  return str;
12
13
  }
14
+ /** Builds a FileMaker find value using template-string syntax. */
13
15
  export function query(strings, ...args) {
14
16
  const argStrings = args.map(item => {
15
17
  if (typeof item === 'number') {
@@ -26,18 +28,21 @@ export function query(strings, ...args) {
26
28
  }).flat(1);
27
29
  return { [FindRequestSymbol]: query };
28
30
  }
31
+ /** Marks a value as a FileMaker date for use inside `query(...)`. */
29
32
  export function asDate(date) {
30
33
  return {
31
34
  type: 'date',
32
35
  moment: moment(date)
33
36
  };
34
37
  }
38
+ /** Marks a value as a FileMaker time for use inside `query(...)`. */
35
39
  export function asTime(date) {
36
40
  return {
37
41
  type: 'time',
38
42
  moment: moment(date)
39
43
  };
40
44
  }
45
+ /** Marks a value as a FileMaker timestamp for use inside `query(...)`. */
41
46
  export function asTimestamp(date) {
42
47
  return {
43
48
  type: 'timestamp',
package/package.json CHANGED
@@ -1,14 +1,10 @@
1
1
  {
2
2
  "name": "@jd-data-limited/easy-fm",
3
- "version": "4.1.15",
3
+ "version": "5.0.0-beta.1",
4
4
  "dependencies": {
5
- "@types/follow-redirects": "^1.14.4",
6
- "@types/node": "^22.7.7",
7
- "commander": "^14.0.0",
8
- "follow-redirects": "^1.15.5",
9
- "moment": "^2.29.4",
10
- "node-fetch": "^3.3.2",
11
- "node-fetch-cookies": "^2.1.1"
5
+ "commander": "^14.0.3",
6
+ "moment": "^2.30.1",
7
+ "zod": "^4.4.3"
12
8
  },
13
9
  "type": "module",
14
10
  "main": "dist/index.js",
@@ -41,37 +37,44 @@
41
37
  "url": "https://github.com/JD-Data-Limited/easy-fm/issues"
42
38
  },
43
39
  "engines": {
44
- "node": ">=20.9.0"
40
+ "node": ">=22.22.3"
45
41
  },
46
42
  "devDependencies": {
47
- "@babel/cli": "^7.25.7",
48
- "@babel/core": "^7.25.8",
49
- "@babel/preset-env": "^7.25.8",
50
- "@babel/preset-typescript": "^7.25.7",
51
- "@types/btoa": "^1.2.3",
52
- "@types/events": "^3.0.0",
53
- "@types/inquirer": "^9.0.7",
54
- "@types/jest": "^29.5.13",
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",
47
+ "@types/btoa": "^1.2.5",
48
+ "@types/events": "^3.0.3",
49
+ "@types/inquirer": "^9.0.10",
50
+ "@types/jest": "^29.5.14",
51
+ "@types/node": "^22.19.21",
55
52
  "@typescript-eslint/eslint-plugin": "^6.21.0",
56
53
  "babel-jest": "^29.7.0",
57
- "dotenv": "^16.3.1",
58
- "eslint": "^8.57.0",
54
+ "dotenv": "^16.6.1",
55
+ "eslint": "^8.57.1",
59
56
  "eslint-config-standard-with-typescript": "^43.0.1",
60
- "eslint-plugin-import": "^2.29.1",
57
+ "eslint-plugin-import": "^2.32.0",
61
58
  "eslint-plugin-n": "^16.6.2",
62
- "eslint-plugin-promise": "^6.1.1",
63
- "inquirer": "^9.2.13",
59
+ "eslint-plugin-promise": "^6.6.0",
60
+ "inquirer": "^9.3.8",
64
61
  "jest": "^29.7.0",
65
- "ts-jest": "^29.2.5",
62
+ "ts-jest": "^29.4.11",
66
63
  "ts-morph": "^21.0.1",
67
- "tsx": "^4.19.4",
68
- "typescript": "^5.6.3"
64
+ "tsx": "^4.22.4",
65
+ "typedoc": "^0.28.19",
66
+ "typescript": "^5.9.3"
69
67
  },
70
68
  "bin": {
71
69
  "easyfm": "dist/bin/cli.js"
72
70
  },
73
71
  "scripts": {
74
72
  "build": "tsc",
75
- "test": "npm run build && jest"
73
+ "test": "npm run build && jest",
74
+ "test:coverage": "jest --coverage --testPathIgnorePatterns='/__tests__/database.test.ts$'",
75
+ "test:integration": "npm run build && jest __tests__/database.test.ts --runInBand",
76
+ "test:unit": "jest __tests__/query.test.ts __tests__/CookieJar.test.ts --runInBand",
77
+ "stress:search": "tsx src/bin/stressSearch.ts",
78
+ "docs:build": "typedoc --options ./docs/theme.json"
76
79
  }
77
80
  }