@jd-data-limited/easy-fm 5.0.0-beta.1 → 5.0.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -2
- package/dist/bin/generateTypes.js +2 -2
- package/dist/bin/stressSearch.js +1 -1
- package/dist/connection/FMHost.d.ts +1 -3
- package/dist/connection/FMHost.js +1 -3
- package/dist/connection/HostBase.d.ts +0 -2
- package/dist/index.d.ts +7 -4
- package/dist/index.js +4 -2
- package/dist/layouts/layoutInterface.d.ts +2 -1
- package/dist/models/apiResults.d.ts +7 -6
- package/dist/models/apiResults.js +1 -1
- 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.js +8 -9
- package/dist/records/layoutRecord.d.ts +8 -7
- package/dist/records/layoutRecord.js +16 -24
- package/dist/records/portal.d.ts +1 -1
- 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 +5 -4
- package/dist/records/recordBase.js +32 -33
- package/dist/types.d.ts +1 -1
- package/dist/utils/query.d.ts +21 -12
- package/dist/utils/query.js +58 -23
- package/dist/utils/temporal.d.ts +9 -0
- package/dist/utils/temporal.js +110 -0
- package/package.json +24 -23
package/README.md
CHANGED
|
@@ -23,7 +23,7 @@ npm install @jd-data-limited/easy-fm
|
|
|
23
23
|
## Quick Start
|
|
24
24
|
|
|
25
25
|
```ts
|
|
26
|
-
import FMHost from "@jd-data-limited/easy-fm"
|
|
26
|
+
import {FMHost} from "@jd-data-limited/easy-fm"
|
|
27
27
|
|
|
28
28
|
const host = new FMHost("https://example.com")
|
|
29
29
|
|
|
@@ -57,6 +57,8 @@ for (const record of records) {
|
|
|
57
57
|
|
|
58
58
|
## Documentation
|
|
59
59
|
|
|
60
|
+
> Updating from v4 to v5? Check the guide here; [`docs/upgrade-to-v5.md`](./docs/upgrade-to-v5.md)
|
|
61
|
+
|
|
60
62
|
Start here:
|
|
61
63
|
|
|
62
64
|
- [`docs/getting-started.md`](./docs/getting-started.md): first connection, first find, closing sessions
|
|
@@ -67,7 +69,6 @@ Task guides:
|
|
|
67
69
|
- [`docs/working-with-records.md`](./docs/working-with-records.md): list, create, update, duplicate, delete, portal rows
|
|
68
70
|
- [`docs/query-recipes.md`](./docs/query-recipes.md): find requests, sorting, paging, script hooks
|
|
69
71
|
- [`docs/authentication-and-sessions.md`](./docs/authentication-and-sessions.md): auth modes, pooling, lifecycle
|
|
70
|
-
- [`docs/timezones.md`](./docs/timezones.md): date, time, and timestamp behavior
|
|
71
72
|
- [`docs/typescript-layouts.md`](./docs/typescript-layouts.md): typed layouts and stronger autocomplete
|
|
72
73
|
- [`docs/testGuide.md`](./docs/testGuide.md): local test commands
|
|
73
74
|
|
|
@@ -78,7 +78,7 @@ export async function generateTypesCLI() {
|
|
|
78
78
|
default: true,
|
|
79
79
|
message: "Verify your server's identity",
|
|
80
80
|
when(previousAnswers) {
|
|
81
|
-
return previousAnswers.hostname
|
|
81
|
+
return previousAnswers.hostname?.startsWith('https://');
|
|
82
82
|
}
|
|
83
83
|
},
|
|
84
84
|
{
|
|
@@ -98,7 +98,7 @@ export async function generateTypesCLI() {
|
|
|
98
98
|
mask: '*'
|
|
99
99
|
}
|
|
100
100
|
]);
|
|
101
|
-
const HOST = new FMHost(data.hostname,
|
|
101
|
+
const HOST = new FMHost(data.hostname, data.verify);
|
|
102
102
|
const DATABASE = HOST.database({
|
|
103
103
|
database: data.database,
|
|
104
104
|
credentials: {
|
package/dist/bin/stressSearch.js
CHANGED
|
@@ -175,7 +175,7 @@ async function main() {
|
|
|
175
175
|
warmup,
|
|
176
176
|
concurrencyLevels
|
|
177
177
|
});
|
|
178
|
-
const fmHost = new FMHost(host,
|
|
178
|
+
const fmHost = new FMHost(host, false);
|
|
179
179
|
const rows = [];
|
|
180
180
|
const connection = fmHost.database({
|
|
181
181
|
database,
|
|
@@ -2,7 +2,6 @@ import { type Database } from './database.js';
|
|
|
2
2
|
import { type HostBase } from './HostBase.js';
|
|
3
3
|
import { type databaseOptionsWithExternalSources, FMHostMetadata, type loginOptionsClaris, type loginOptionsFileMaker, type loginOptionsOAuth } from '../types.js';
|
|
4
4
|
import { type DatabaseStructure } from '../databaseStructure.js';
|
|
5
|
-
import { type Moment } from 'moment';
|
|
6
5
|
import z from 'zod';
|
|
7
6
|
import { type DatabaseProtocol } from './Session.js';
|
|
8
7
|
/**
|
|
@@ -10,11 +9,10 @@ import { type DatabaseProtocol } from './Session.js';
|
|
|
10
9
|
*/
|
|
11
10
|
export default class FMHost implements HostBase {
|
|
12
11
|
readonly hostname: string;
|
|
13
|
-
readonly timezoneOffsetFunc: (moment: Moment) => number;
|
|
14
12
|
readonly verify: boolean;
|
|
15
13
|
readonly protocol: DatabaseProtocol;
|
|
16
14
|
_metadata: z.infer<typeof FMHostMetadata> | null;
|
|
17
|
-
constructor(_hostname: string,
|
|
15
|
+
constructor(_hostname: string, verify?: boolean);
|
|
18
16
|
get metadata(): z.infer<typeof FMHostMetadata>;
|
|
19
17
|
/** FileMaker host date format converted to Moment-compatible tokens. */
|
|
20
18
|
get dateFormat(): string;
|
|
@@ -13,17 +13,15 @@ import { DatabaseConstantSession } from './databaseConstantSession.js';
|
|
|
13
13
|
*/
|
|
14
14
|
export default class FMHost {
|
|
15
15
|
hostname;
|
|
16
|
-
timezoneOffsetFunc;
|
|
17
16
|
verify;
|
|
18
17
|
protocol;
|
|
19
18
|
_metadata = null;
|
|
20
|
-
constructor(_hostname,
|
|
19
|
+
constructor(_hostname, verify = true) {
|
|
21
20
|
if (!(/^https?:\/\//).test(_hostname))
|
|
22
21
|
throw new Error('hostname MUST begin with either http:// or https://');
|
|
23
22
|
const url = new URL(_hostname);
|
|
24
23
|
this.protocol = url.protocol;
|
|
25
24
|
this.hostname = url.hostname;
|
|
26
|
-
this.timezoneOffsetFunc = timezoneOffset;
|
|
27
25
|
this.verify = verify;
|
|
28
26
|
}
|
|
29
27
|
get metadata() {
|
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
import { type FMHostMetadata } from '../types.js';
|
|
2
|
-
import { type Moment } from 'moment';
|
|
3
2
|
import { type z } from 'zod';
|
|
4
3
|
import { type DatabaseProtocol } from './Session.js';
|
|
5
4
|
export interface HostBase {
|
|
6
5
|
readonly hostname: string;
|
|
7
6
|
readonly protocol: DatabaseProtocol;
|
|
8
|
-
readonly timezoneOffsetFunc: (moment: Moment) => number;
|
|
9
7
|
readonly verify: boolean;
|
|
10
8
|
metadata: z.infer<typeof FMHostMetadata>;
|
|
11
9
|
getMetadata: () => PromiseLike<any>;
|
package/dist/index.d.ts
CHANGED
|
@@ -13,7 +13,7 @@ export { RecordBase } from './records/recordBase.js';
|
|
|
13
13
|
/** Record returned from a layout. Supports fetch, commit, duplicate, and delete. */
|
|
14
14
|
export { LayoutRecord } from './records/layoutRecord.js';
|
|
15
15
|
/** Type helpers for describing typed layouts and portals. */
|
|
16
|
-
export { type LayoutInterface, type PortalInterface } from './layouts/layoutInterface.js';
|
|
16
|
+
export { type LayoutInterface, type PortalInterface, type RecordFieldsMap } from './layouts/layoutInterface.js';
|
|
17
17
|
/** Record returned from a portal row. */
|
|
18
18
|
export { PortalRecord } from './records/portalRecord.js';
|
|
19
19
|
/** Wrapper around portal rows included in a layout record. */
|
|
@@ -22,12 +22,15 @@ export { Portal } from './records/portal.js';
|
|
|
22
22
|
export { type FindRequest, type FindRequestRaw, RecordGetOperation } from './records/getOperations/recordGetOperation.js';
|
|
23
23
|
/** Entry point for `layout.records.*` operations. */
|
|
24
24
|
export { LayoutRecordManager } from './layouts/layoutRecordManager.js';
|
|
25
|
-
/** Type alias for map of fields on a typed record. */
|
|
26
|
-
export { type RecordFieldsMap } from './layouts/recordFieldsMap.js';
|
|
27
25
|
/** Helpers for safe FileMaker find query construction and date/time formatting. */
|
|
28
26
|
export { asDate, asTime, asTimestamp, query, queryEscape } from './utils/query.js';
|
|
27
|
+
/** Conversion helpers for FileMaker-formatted Temporal values. */
|
|
28
|
+
export * from './records/fields/valueField.js';
|
|
29
|
+
export * from './records/fields/containerField.js';
|
|
30
|
+
export { type Field } from './records/fields/field.js';
|
|
31
|
+
export { stringToTemporal, temporalToString, type TemporalValue, type TemporalValueType } from './utils/temporal.js';
|
|
29
32
|
/** Field wrapper used for reading, editing, and container access. */
|
|
30
|
-
export { type
|
|
33
|
+
export { type BaseField } from './records/fields/baseField.js';
|
|
31
34
|
/** Default export. Represents FileMaker host/server. */
|
|
32
35
|
export default FMHost;
|
|
33
36
|
export {
|
package/dist/index.js
CHANGED
|
@@ -22,7 +22,9 @@ export { RecordGetOperation } from './records/getOperations/recordGetOperation.j
|
|
|
22
22
|
export { LayoutRecordManager } from './layouts/layoutRecordManager.js';
|
|
23
23
|
/** Helpers for safe FileMaker find query construction and date/time formatting. */
|
|
24
24
|
export { asDate, asTime, asTimestamp, query, queryEscape } from './utils/query.js';
|
|
25
|
-
/**
|
|
26
|
-
export
|
|
25
|
+
/** Conversion helpers for FileMaker-formatted Temporal values. */
|
|
26
|
+
export * from './records/fields/valueField.js';
|
|
27
|
+
export * from './records/fields/containerField.js';
|
|
28
|
+
export { stringToTemporal, temporalToString } from './utils/temporal.js';
|
|
27
29
|
/** Default export. Represents FileMaker host/server. */
|
|
28
30
|
export default FMHost;
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { type Portal } from '../records/portal.js';
|
|
2
|
-
import { type
|
|
2
|
+
import { type Field } from '../records/fields/field.js';
|
|
3
3
|
export interface LayoutInterface {
|
|
4
4
|
fields: RecordFieldsMap;
|
|
5
5
|
portals: PortalInterface;
|
|
6
6
|
}
|
|
7
|
+
export type RecordFieldsMap = Record<string, Field>;
|
|
7
8
|
export type PortalInterface = Record<string | number | symbol, Portal<RecordFieldsMap>>;
|
|
@@ -21,6 +21,7 @@ export declare const ApiFieldResultTypes: {
|
|
|
21
21
|
readonly TIMESTAMP: "timeStamp";
|
|
22
22
|
readonly CONTAINER: "container";
|
|
23
23
|
};
|
|
24
|
+
export type FieldResultTypes = z.infer<typeof ApiFieldResultTypes>;
|
|
24
25
|
export declare const ApiMessage: z.ZodObject<{
|
|
25
26
|
code: z.ZodCoercedNumber<unknown>;
|
|
26
27
|
message: z.ZodString;
|
|
@@ -166,7 +167,7 @@ export declare const ApiScriptResult: z.ZodObject<{
|
|
|
166
167
|
scriptError: z.ZodString;
|
|
167
168
|
scriptResult: z.ZodOptional<z.ZodString>;
|
|
168
169
|
}, z.core.$strip>;
|
|
169
|
-
export declare const ApiFieldData: z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>>;
|
|
170
|
+
export declare const ApiFieldData: z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodNull]>>;
|
|
170
171
|
export declare const ApiResultSetObj: z.ZodObject<{
|
|
171
172
|
database: z.ZodString;
|
|
172
173
|
layout: z.ZodString;
|
|
@@ -175,10 +176,10 @@ export declare const ApiResultSetObj: z.ZodObject<{
|
|
|
175
176
|
foundCount: z.ZodNumber;
|
|
176
177
|
returnedCount: z.ZodNumber;
|
|
177
178
|
}, z.core.$strip>;
|
|
178
|
-
export declare const ApiPortalData: z.ZodRecord<z.ZodString, z.ZodArray<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>>>>;
|
|
179
|
+
export declare const ApiPortalData: z.ZodRecord<z.ZodString, z.ZodArray<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodNull]>>>>;
|
|
179
180
|
export declare const ApiRowDataDef: z.ZodObject<{
|
|
180
|
-
fieldData: z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>>;
|
|
181
|
-
portalData: z.ZodRecord<z.ZodString, z.ZodArray<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>>>>;
|
|
181
|
+
fieldData: z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodNull]>>;
|
|
182
|
+
portalData: z.ZodRecord<z.ZodString, z.ZodArray<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodNull]>>>>;
|
|
182
183
|
modId: z.ZodString;
|
|
183
184
|
recordId: z.ZodString;
|
|
184
185
|
portalDataInfo: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
@@ -190,8 +191,8 @@ export declare const ApiRowDataDef: z.ZodObject<{
|
|
|
190
191
|
}, z.core.$strip>;
|
|
191
192
|
export declare const ApiRecordResponseObj: z.ZodObject<{
|
|
192
193
|
data: z.ZodArray<z.ZodObject<{
|
|
193
|
-
fieldData: z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>>;
|
|
194
|
-
portalData: z.ZodRecord<z.ZodString, z.ZodArray<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>>>>;
|
|
194
|
+
fieldData: z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodNull]>>;
|
|
195
|
+
portalData: z.ZodRecord<z.ZodString, z.ZodArray<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodNull]>>>>;
|
|
195
196
|
modId: z.ZodString;
|
|
196
197
|
recordId: z.ZodString;
|
|
197
198
|
portalDataInfo: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
@@ -70,7 +70,7 @@ export const ApiScriptResult = z.object({
|
|
|
70
70
|
scriptError: z.string(),
|
|
71
71
|
scriptResult: z.string().optional()
|
|
72
72
|
});
|
|
73
|
-
export const ApiFieldData = z.record(z.string(), z.union([z.string(), z.number()]));
|
|
73
|
+
export const ApiFieldData = z.record(z.string(), z.union([z.string(), z.number(), z.null()]));
|
|
74
74
|
export const ApiResultSetObj = z.object({
|
|
75
75
|
database: z.string(),
|
|
76
76
|
layout: z.string(),
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { RecordTypes } from '../../types.js';
|
|
2
|
+
import { type ApiFieldMetadata, FieldResultTypes } from '../../models/apiResults.js';
|
|
3
|
+
import { type LayoutBase } from '../../layouts/layoutBase.js';
|
|
4
|
+
import { type z } from 'zod';
|
|
5
|
+
export type ContainerValue = null;
|
|
6
|
+
/**
|
|
7
|
+
* RawValueData refers to the raw shapes of data we receive from or sent to FileMaker.
|
|
8
|
+
*/
|
|
9
|
+
export type RawValueData = string | number | null;
|
|
10
|
+
export interface Parentable {
|
|
11
|
+
layout: LayoutBase;
|
|
12
|
+
type: RecordTypes;
|
|
13
|
+
endpoint: string;
|
|
14
|
+
portal?: {
|
|
15
|
+
name: string;
|
|
16
|
+
};
|
|
17
|
+
getFieldMetadata(fieldId: string): z.infer<typeof ApiFieldMetadata>;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* A class representing a field in a record.
|
|
21
|
+
* API_FIELD_RESULT defines the value of field.metadata.result for the abstracted field type
|
|
22
|
+
*
|
|
23
|
+
* @template T - The type of the field value.
|
|
24
|
+
*/
|
|
25
|
+
export declare abstract class BaseField<T extends unknown, API_FIELD_RESULT extends FieldResultTypes> {
|
|
26
|
+
parent: Parentable;
|
|
27
|
+
id: string;
|
|
28
|
+
protected _value: T;
|
|
29
|
+
/**
|
|
30
|
+
* @internal
|
|
31
|
+
* Use `layout.records.create()` instead to create a new LayoutRecord, as this may be safer.
|
|
32
|
+
* @param record
|
|
33
|
+
* @param id
|
|
34
|
+
* @param value
|
|
35
|
+
* @protected
|
|
36
|
+
*/
|
|
37
|
+
protected constructor(record: Parentable, id: string, value: RawValueData);
|
|
38
|
+
/**
|
|
39
|
+
* Takes in data in the format that FileMaker gives us and converts it to a more JavaScript-friendly format.
|
|
40
|
+
* This method is intended to be overriden.
|
|
41
|
+
* @param value
|
|
42
|
+
*/
|
|
43
|
+
parseRawValue(value: RawValueData): T;
|
|
44
|
+
/**
|
|
45
|
+
* Converts data back into a format that can be sent to FileMaker. Intended to be overriden if conversion is non-standard.
|
|
46
|
+
*/
|
|
47
|
+
serializeRawValue(): RawValueData;
|
|
48
|
+
updateFromRawValue(value: RawValueData): void;
|
|
49
|
+
/** Returns the FileMaker metadata for this field. */
|
|
50
|
+
get metadata(): z.infer<typeof ApiFieldMetadata> & {
|
|
51
|
+
result: API_FIELD_RESULT;
|
|
52
|
+
};
|
|
53
|
+
/** Gets or sets the current field value. */
|
|
54
|
+
get value(): T;
|
|
55
|
+
isCalculationField(): boolean;
|
|
56
|
+
isSummaryField(): boolean;
|
|
57
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2023-2024. See LICENSE file for more information
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* A class representing a field in a record.
|
|
6
|
+
* API_FIELD_RESULT defines the value of field.metadata.result for the abstracted field type
|
|
7
|
+
*
|
|
8
|
+
* @template T - The type of the field value.
|
|
9
|
+
*/
|
|
10
|
+
export class BaseField {
|
|
11
|
+
parent;
|
|
12
|
+
id;
|
|
13
|
+
_value;
|
|
14
|
+
/**
|
|
15
|
+
* @internal
|
|
16
|
+
* Use `layout.records.create()` instead to create a new LayoutRecord, as this may be safer.
|
|
17
|
+
* @param record
|
|
18
|
+
* @param id
|
|
19
|
+
* @param value
|
|
20
|
+
* @protected
|
|
21
|
+
*/
|
|
22
|
+
constructor(record, id, value) {
|
|
23
|
+
this.parent = record;
|
|
24
|
+
this.id = id;
|
|
25
|
+
this._value = this.parseRawValue(value);
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Takes in data in the format that FileMaker gives us and converts it to a more JavaScript-friendly format.
|
|
29
|
+
* This method is intended to be overriden.
|
|
30
|
+
* @param value
|
|
31
|
+
*/
|
|
32
|
+
parseRawValue(value) {
|
|
33
|
+
return value;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Converts data back into a format that can be sent to FileMaker. Intended to be overriden if conversion is non-standard.
|
|
37
|
+
*/
|
|
38
|
+
serializeRawValue() {
|
|
39
|
+
return this._value;
|
|
40
|
+
}
|
|
41
|
+
updateFromRawValue(value) {
|
|
42
|
+
this._value = this.parseRawValue(value);
|
|
43
|
+
}
|
|
44
|
+
/** Returns the FileMaker metadata for this field. */
|
|
45
|
+
get metadata() {
|
|
46
|
+
return this.parent.getFieldMetadata(this.id);
|
|
47
|
+
}
|
|
48
|
+
/** Gets or sets the current field value. */
|
|
49
|
+
get value() {
|
|
50
|
+
// if (this.metadata.result === "container") throw "Use await field.stream() to get the contents of a container field, instead of field.value"
|
|
51
|
+
return this._value;
|
|
52
|
+
}
|
|
53
|
+
isCalculationField() {
|
|
54
|
+
return this.metadata.type === 'calculation';
|
|
55
|
+
}
|
|
56
|
+
isSummaryField() {
|
|
57
|
+
return this.metadata.type === 'summary';
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { BaseField, Parentable, RawValueData } from "./baseField.js";
|
|
2
|
+
import { Readable } from 'node:stream';
|
|
3
|
+
interface StreamOptions {
|
|
4
|
+
abortSignal?: AbortSignal;
|
|
5
|
+
}
|
|
6
|
+
export declare class ContainerField extends BaseField<string, 'container'> {
|
|
7
|
+
#private;
|
|
8
|
+
static firstContainerDownload: Promise<any> | null;
|
|
9
|
+
constructor(record: Parentable, id: string, value: RawValueData);
|
|
10
|
+
/**
|
|
11
|
+
* Uploads a file to the container field.
|
|
12
|
+
*
|
|
13
|
+
* @param {Buffer} file - The file content as a buffer.
|
|
14
|
+
*
|
|
15
|
+
* @throws {Error} - Cannot upload a file to the field if it's not a container field.
|
|
16
|
+
* @throws {Error} - Upload failed with HTTP error.
|
|
17
|
+
*
|
|
18
|
+
* @returns {Promise<void>} - A promise that resolves when the file is successfully uploaded.
|
|
19
|
+
*/
|
|
20
|
+
upload(file: File): Promise<void>;
|
|
21
|
+
stream(): Promise<{
|
|
22
|
+
data: Readable;
|
|
23
|
+
mime: string;
|
|
24
|
+
}>;
|
|
25
|
+
/**
|
|
26
|
+
* Returns the container download as a Web `Response`.
|
|
27
|
+
*/
|
|
28
|
+
webStream(options: StreamOptions): Promise<Response>;
|
|
29
|
+
/** Downloads the full container contents into memory. */
|
|
30
|
+
arrayBuffer(): Promise<{
|
|
31
|
+
data: ArrayBuffer;
|
|
32
|
+
mime: string;
|
|
33
|
+
}>;
|
|
34
|
+
}
|
|
35
|
+
export {};
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { BaseField } from "./baseField.js";
|
|
2
|
+
import { FMError } from '../../FMError.js';
|
|
3
|
+
import { Readable } from 'node:stream';
|
|
4
|
+
import { HttpError } from '../../connection/Session.js';
|
|
5
|
+
import { ApiResults } from "../../models/apiResults.js";
|
|
6
|
+
export class ContainerField extends BaseField {
|
|
7
|
+
static firstContainerDownload = null;
|
|
8
|
+
constructor(record, id, value) {
|
|
9
|
+
super(record, id, value);
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Uploads a file to the container field.
|
|
13
|
+
*
|
|
14
|
+
* @param {Buffer} file - The file content as a buffer.
|
|
15
|
+
*
|
|
16
|
+
* @throws {Error} - Cannot upload a file to the field if it's not a container field.
|
|
17
|
+
* @throws {Error} - Upload failed with HTTP error.
|
|
18
|
+
*
|
|
19
|
+
* @returns {Promise<void>} - A promise that resolves when the file is successfully uploaded.
|
|
20
|
+
*/
|
|
21
|
+
async upload(file) {
|
|
22
|
+
if (this.metadata.result !== 'container') {
|
|
23
|
+
throw new Error('Cannot upload a file to the field; ' + this.id + ' (not a container field)');
|
|
24
|
+
}
|
|
25
|
+
const form = new FormData();
|
|
26
|
+
form.append('upload', file);
|
|
27
|
+
const res = await this.parent.layout.database.fetch(`${this.parent.endpoint}/containers/${this.id}/1`, {
|
|
28
|
+
method: 'POST',
|
|
29
|
+
body: form
|
|
30
|
+
});
|
|
31
|
+
if (!res.ok) {
|
|
32
|
+
throw await HttpError.new(res);
|
|
33
|
+
}
|
|
34
|
+
const data = ApiResults.parse(await res.json());
|
|
35
|
+
if (data.messages[0].code === 0)
|
|
36
|
+
return;
|
|
37
|
+
else {
|
|
38
|
+
throw new FMError(data.messages[0].code, res.status, res);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
async #streamAsync(options = {}) {
|
|
42
|
+
const req = await this.parent.layout.database.fetch(this.value, {
|
|
43
|
+
signal: options.abortSignal ?? null
|
|
44
|
+
});
|
|
45
|
+
// const req = await this.parent.layout.database._apiRequestRaw(this.string, {useCookieJar: true})
|
|
46
|
+
if (!req.ok || !req.body) {
|
|
47
|
+
throw new Error(`HTTP Error: ${req.status} (${req.statusText})`);
|
|
48
|
+
}
|
|
49
|
+
return req;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* @deprecated use webStream instead.
|
|
53
|
+
*/
|
|
54
|
+
async stream() {
|
|
55
|
+
const stream = await this.#streamAsync();
|
|
56
|
+
if (!stream.body) {
|
|
57
|
+
throw await HttpError.new(stream);
|
|
58
|
+
}
|
|
59
|
+
return {
|
|
60
|
+
// @ts-expect-error stream types are correct
|
|
61
|
+
data: Readable.fromWeb(stream.body),
|
|
62
|
+
mime: stream.headers.get('Content-Type') ?? ''
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Returns the container download as a Web `Response`.
|
|
67
|
+
*/
|
|
68
|
+
async webStream(options) {
|
|
69
|
+
return await this.#streamAsync(options);
|
|
70
|
+
}
|
|
71
|
+
/** Downloads the full container contents into memory. */
|
|
72
|
+
async arrayBuffer() {
|
|
73
|
+
const stream = await this.#streamAsync();
|
|
74
|
+
if (!stream.ok)
|
|
75
|
+
throw await HttpError.new(stream);
|
|
76
|
+
return { data: await stream.arrayBuffer(), mime: stream.headers.get('Content-Type') ?? '' };
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { BaseField, Parentable, RawValueData } from "./baseField.js";
|
|
2
|
+
import { Temporal } from "temporal-polyfill";
|
|
3
|
+
import { FieldResultTypes } from "../../models/apiResults.js";
|
|
4
|
+
export declare abstract class ValueFieldBase<T, API_FIELD_RESULT extends FieldResultTypes> extends BaseField<T, API_FIELD_RESULT> {
|
|
5
|
+
#private;
|
|
6
|
+
/**
|
|
7
|
+
* @internal
|
|
8
|
+
* Use `layout.records.create()` instead to create a new LayoutRecord, as this may be safer.
|
|
9
|
+
* @param record
|
|
10
|
+
* @param id
|
|
11
|
+
* @param value
|
|
12
|
+
* @protected
|
|
13
|
+
*/
|
|
14
|
+
constructor(record: Parentable, id: string, value: RawValueData);
|
|
15
|
+
set(content: T): void;
|
|
16
|
+
/** `true` if this field has changed since it was last loaded or saved. */
|
|
17
|
+
get edited(): boolean;
|
|
18
|
+
/**
|
|
19
|
+
* Resets the 'edited' state of this field, without changing its value
|
|
20
|
+
*/
|
|
21
|
+
updateOriginalContents(): void;
|
|
22
|
+
set value(value: T);
|
|
23
|
+
get value(): T;
|
|
24
|
+
}
|
|
25
|
+
export declare class TextField<T extends string = string> extends ValueFieldBase<T, 'text'> {
|
|
26
|
+
}
|
|
27
|
+
export declare class NumberField<T extends number | null = number | null> extends ValueFieldBase<T, 'number'> {
|
|
28
|
+
}
|
|
29
|
+
export declare class TimeStampField extends ValueFieldBase<Temporal.PlainDateTime | null, 'timeStamp'> {
|
|
30
|
+
parseRawValue(value: RawValueData): Temporal.PlainDateTime | null;
|
|
31
|
+
serializeRawValue(): RawValueData;
|
|
32
|
+
}
|
|
33
|
+
export declare class DateField extends ValueFieldBase<Temporal.PlainDate | null, 'date'> {
|
|
34
|
+
parseRawValue(value: RawValueData): Temporal.PlainDate | null;
|
|
35
|
+
serializeRawValue(): RawValueData;
|
|
36
|
+
}
|
|
37
|
+
export declare class TimeField extends ValueFieldBase<Temporal.PlainTime | null, 'time'> {
|
|
38
|
+
parseRawValue(value: RawValueData): Temporal.PlainTime | null;
|
|
39
|
+
serializeRawValue(): RawValueData;
|
|
40
|
+
}
|
|
41
|
+
export type ValueField = TextField | NumberField | TimeStampField | DateField | TimeField;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { BaseField } from "./baseField.js";
|
|
2
|
+
import { Temporal } from "temporal-polyfill";
|
|
3
|
+
import { stringToTemporal, temporalToString } from "../../utils/temporal.js";
|
|
4
|
+
export class ValueFieldBase extends BaseField {
|
|
5
|
+
#originalContents;
|
|
6
|
+
/**
|
|
7
|
+
* @internal
|
|
8
|
+
* Use `layout.records.create()` instead to create a new LayoutRecord, as this may be safer.
|
|
9
|
+
* @param record
|
|
10
|
+
* @param id
|
|
11
|
+
* @param value
|
|
12
|
+
* @protected
|
|
13
|
+
*/
|
|
14
|
+
constructor(record, id, value) {
|
|
15
|
+
super(record, id, value);
|
|
16
|
+
this.#originalContents = value;
|
|
17
|
+
}
|
|
18
|
+
set(content) {
|
|
19
|
+
/**
|
|
20
|
+
* Sets the value of the field.
|
|
21
|
+
*
|
|
22
|
+
* @param {T | null} content - The new content value to be set. Can be either the type T or null.
|
|
23
|
+
* @throws {Error} Cannot set container value using set(). Use upload() instead, if the result is a 'container'.
|
|
24
|
+
*/
|
|
25
|
+
if (this.isCalculationField() || this.isSummaryField()) {
|
|
26
|
+
throw new Error('Cannot modify the value of an immutable calculation or sumamry field.');
|
|
27
|
+
}
|
|
28
|
+
else
|
|
29
|
+
this._value = content;
|
|
30
|
+
}
|
|
31
|
+
/** `true` if this field has changed since it was last loaded or saved. */
|
|
32
|
+
get edited() {
|
|
33
|
+
return this.#originalContents !== this.serializeRawValue();
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Resets the 'edited' state of this field, without changing its value
|
|
37
|
+
*/
|
|
38
|
+
updateOriginalContents() {
|
|
39
|
+
this.#originalContents = this.serializeRawValue();
|
|
40
|
+
}
|
|
41
|
+
set value(value) {
|
|
42
|
+
this.set(value);
|
|
43
|
+
}
|
|
44
|
+
get value() {
|
|
45
|
+
return super.value;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
export class TextField extends ValueFieldBase {
|
|
49
|
+
}
|
|
50
|
+
export class NumberField extends ValueFieldBase {
|
|
51
|
+
}
|
|
52
|
+
export class TimeStampField extends ValueFieldBase {
|
|
53
|
+
parseRawValue(value) {
|
|
54
|
+
if (value === '' || value === null)
|
|
55
|
+
return null;
|
|
56
|
+
if (typeof value !== 'string')
|
|
57
|
+
throw new Error("Received an invalid value for a temporal field.");
|
|
58
|
+
try {
|
|
59
|
+
return stringToTemporal(value, 'timestamp', this.parent.layout.database.host.timeStampFormat);
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return Temporal.PlainDateTime.from(value);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
serializeRawValue() {
|
|
66
|
+
if (this._value === null)
|
|
67
|
+
return '';
|
|
68
|
+
return temporalToString(this._value, this.parent.layout.database.host.timeStampFormat);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
export class DateField extends ValueFieldBase {
|
|
72
|
+
parseRawValue(value) {
|
|
73
|
+
if (value === '' || value === null)
|
|
74
|
+
return null;
|
|
75
|
+
if (typeof value !== 'string')
|
|
76
|
+
throw new Error("Received an invalid value for a temporal field.");
|
|
77
|
+
try {
|
|
78
|
+
return stringToTemporal(value, 'date', this.parent.layout.database.host.dateFormat);
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
return Temporal.PlainDate.from(value);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
serializeRawValue() {
|
|
85
|
+
if (this._value === null)
|
|
86
|
+
return '';
|
|
87
|
+
return temporalToString(this._value, this.parent.layout.database.host.dateFormat);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
export class TimeField extends ValueFieldBase {
|
|
91
|
+
parseRawValue(value) {
|
|
92
|
+
if (value === '' || value === null)
|
|
93
|
+
return null;
|
|
94
|
+
if (typeof value !== 'string')
|
|
95
|
+
throw new Error("Received an invalid value for a temporal field.");
|
|
96
|
+
try {
|
|
97
|
+
return stringToTemporal(value, 'time', this.parent.layout.database.host.timeFormat);
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
return Temporal.PlainTime.from(value);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
serializeRawValue() {
|
|
104
|
+
if (this._value === null)
|
|
105
|
+
return '';
|
|
106
|
+
return temporalToString(this._value, this.parent.layout.database.host.timeFormat);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
@@ -5,6 +5,8 @@ import { LayoutRecord } from '../layoutRecord.js';
|
|
|
5
5
|
import { ApiRecordResponseObj } from '../../models/apiResults.js';
|
|
6
6
|
import { FMError } from '../../FMError.js';
|
|
7
7
|
import { FindRequestSymbol } from '../../utils/query.js';
|
|
8
|
+
import { Temporal } from "temporal-polyfill";
|
|
9
|
+
import { temporalToString } from "../../utils/temporal.js";
|
|
8
10
|
/** Builder/executor for list and find operations against one layout. */
|
|
9
11
|
export class RecordGetOperation {
|
|
10
12
|
layout;
|
|
@@ -131,15 +133,12 @@ export class RecordGetOperation {
|
|
|
131
133
|
if (typeof item === 'string')
|
|
132
134
|
return item;
|
|
133
135
|
// Re-write date into correct format
|
|
134
|
-
|
|
135
|
-
.
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
: item.type === 'time'
|
|
141
|
-
? this.layout.database.host.timeFormat
|
|
142
|
-
: this.layout.database.host.timeStampFormat);
|
|
136
|
+
if (item instanceof Temporal.PlainDateTime)
|
|
137
|
+
return temporalToString(item, this.layout.database.host.timeStampFormat);
|
|
138
|
+
if (item instanceof Temporal.PlainDate)
|
|
139
|
+
return temporalToString(item, this.layout.database.host.dateFormat);
|
|
140
|
+
if (item instanceof Temporal.PlainTime)
|
|
141
|
+
return temporalToString(item, this.layout.database.host.timeFormat);
|
|
143
142
|
}).join('');
|
|
144
143
|
}
|
|
145
144
|
return out;
|