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

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 (75) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +93 -90
  3. package/dist/bin/cli.js +34 -5
  4. package/dist/bin/generateClient.d.ts +1 -0
  5. package/dist/bin/generateClient.js +48 -0
  6. package/dist/bin/generateTypes.js +20 -19
  7. package/dist/bin/init.d.ts +6 -0
  8. package/dist/bin/init.js +46 -0
  9. package/dist/bin/stressSearch.js +12 -12
  10. package/dist/connection/DataApiProvider.d.ts +15 -0
  11. package/dist/connection/DataApiProvider.js +246 -0
  12. package/dist/connection/FMHost.d.ts +1 -3
  13. package/dist/connection/FMHost.js +1 -3
  14. package/dist/connection/HostBase.d.ts +0 -2
  15. package/dist/connection/dataApiDatabaseSession.d.ts +11 -0
  16. package/dist/connection/dataApiDatabaseSession.js +139 -0
  17. package/dist/connection/database.d.ts +22 -56
  18. package/dist/connection/database.js +175 -111
  19. package/dist/connection/databaseBase.d.ts +9 -22
  20. package/dist/connection/databaseBase.js +0 -3
  21. package/dist/connection/databaseProvider.d.ts +196 -0
  22. package/dist/connection/databaseProvider.js +6 -0
  23. package/dist/connection/databaseSessionPool.d.ts +8 -7
  24. package/dist/connection/databaseSessionPool.js +110 -106
  25. package/dist/index.d.ts +11 -7
  26. package/dist/index.js +7 -5
  27. package/dist/layouts/layout.d.ts +0 -2
  28. package/dist/layouts/layout.js +6 -16
  29. package/dist/layouts/layoutBase.d.ts +0 -1
  30. package/dist/layouts/layoutInterface.d.ts +4 -1
  31. package/dist/models/apiResults.d.ts +7 -6
  32. package/dist/models/apiResults.js +1 -1
  33. package/dist/records/fields/baseField.d.ts +60 -0
  34. package/dist/records/fields/baseField.js +59 -0
  35. package/dist/records/fields/containerField.d.ts +18 -0
  36. package/dist/records/fields/containerField.js +44 -0
  37. package/dist/records/fields/field.d.ts +3 -0
  38. package/dist/records/fields/field.js +1 -0
  39. package/dist/records/fields/valueField.d.ts +41 -0
  40. package/dist/records/fields/valueField.js +108 -0
  41. package/dist/records/getOperations/recordGetOperation.js +24 -24
  42. package/dist/records/layoutRecord.d.ts +15 -8
  43. package/dist/records/layoutRecord.js +61 -98
  44. package/dist/records/portal.d.ts +1 -1
  45. package/dist/records/portalBase.d.ts +1 -2
  46. package/dist/records/portalRecord.d.ts +11 -5
  47. package/dist/records/portalRecord.js +24 -2
  48. package/dist/records/recordBase.d.ts +11 -7
  49. package/dist/records/recordBase.js +38 -38
  50. package/dist/schema.d.ts +54 -0
  51. package/dist/schema.js +87 -0
  52. package/dist/typegen/dataApiSchemaSource.d.ts +3 -0
  53. package/dist/typegen/dataApiSchemaSource.js +32 -0
  54. package/dist/typegen/dependencies.d.ts +11 -0
  55. package/dist/typegen/dependencies.js +67 -0
  56. package/dist/typegen/generate.d.ts +5 -0
  57. package/dist/typegen/generate.js +85 -0
  58. package/dist/typegen/index.d.ts +6 -0
  59. package/dist/typegen/index.js +6 -0
  60. package/dist/typegen/init.d.ts +13 -0
  61. package/dist/typegen/init.js +113 -0
  62. package/dist/typegen/types.d.ts +40 -0
  63. package/dist/typegen/types.js +1 -0
  64. package/dist/typegen/zod.d.ts +3 -0
  65. package/dist/typegen/zod.js +32 -0
  66. package/dist/types.d.ts +1 -1
  67. package/dist/utils/query.d.ts +21 -12
  68. package/dist/utils/query.js +58 -23
  69. package/dist/utils/temporal.d.ts +9 -0
  70. package/dist/utils/temporal.js +110 -0
  71. package/package.json +47 -24
  72. package/dist/layouts/recordFieldsMap.d.ts +0 -2
  73. package/dist/layouts/recordFieldsMap.js +0 -4
  74. package/dist/records/field.d.ts +0 -77
  75. package/dist/records/field.js +0 -192
@@ -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.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.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.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.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.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.timeFormat);
107
+ }
108
+ }
@@ -2,9 +2,10 @@
2
2
  * Copyright (c) 2023-2024. See LICENSE file for more information
3
3
  */
4
4
  import { LayoutRecord } from '../layoutRecord.js';
5
- import { ApiRecordResponseObj } from '../../models/apiResults.js';
6
5
  import { FMError } from '../../FMError.js';
7
6
  import { FindRequestSymbol } from '../../utils/query.js';
7
+ import { Temporal } from "temporal-polyfill";
8
+ import { temporalToString } from "../../utils/temporal.js";
8
9
  /** Builder/executor for list and find operations against one layout. */
9
10
  export class RecordGetOperation {
10
11
  layout;
@@ -131,15 +132,12 @@ export class RecordGetOperation {
131
132
  if (typeof item === 'string')
132
133
  return item;
133
134
  // Re-write date into correct format
134
- return item
135
- .moment
136
- .clone()
137
- .utcOffset(this.layout.database.host.timezoneOffsetFunc(item.moment))
138
- .format(item.type === 'date'
139
- ? this.layout.database.host.dateFormat
140
- : item.type === 'time'
141
- ? this.layout.database.host.timeFormat
142
- : this.layout.database.host.timeStampFormat);
135
+ if (item instanceof Temporal.PlainDateTime)
136
+ return temporalToString(item, this.layout.database.timeStampFormat);
137
+ if (item instanceof Temporal.PlainDate)
138
+ return temporalToString(item, this.layout.database.dateFormat);
139
+ if (item instanceof Temporal.PlainTime)
140
+ return temporalToString(item, this.layout.database.timeFormat);
143
141
  }).join('');
144
142
  }
145
143
  return out;
@@ -165,23 +163,25 @@ export class RecordGetOperation {
165
163
  }
166
164
  async performFind(offset, limit) {
167
165
  await this.layout.getLayoutMeta();
168
- const isFind = this.isFindRequest;
169
- let endpoint = this.layout.endpoint + (isFind ? '/_find' : '/records');
170
- if (!isFind)
171
- endpoint += '?' + new URLSearchParams(this.generateParamsURL(offset, limit)).toString();
172
- const reqData = {
173
- // port: 443,
174
- method: isFind ? 'POST' : 'GET',
175
- type: ApiRecordResponseObj,
176
- body: isFind ? JSON.stringify(this.generateParamsBody(offset, limit)) : undefined
177
- };
178
166
  try {
179
- const res = await this.layout.database.fetchJSON(endpoint, reqData);
180
- // console.log("RESOLVING")
167
+ const portals = {};
168
+ for (const [name, paging] of Object.entries(this.portals))
169
+ if (paging)
170
+ portals[name] = paging;
171
+ const scripts = {};
172
+ for (const [name, script] of Object.entries(this.scriptData))
173
+ if (script)
174
+ scripts[name] = { name: script.name, parameter: script.parameter ?? '' };
175
+ const { value: res, binding } = await this.layout.database.execute({
176
+ type: 'record.list',
177
+ layout: this.layout.name,
178
+ query: this.requests.map(request => ({ fields: request.req, omit: request.omit ?? false })),
179
+ options: { limit, offset, sort: this.sortData, portals, scripts }
180
+ });
181
181
  if (!this.layout.metadata)
182
182
  await this.layout.getLayoutMeta();
183
- return res.data.map(item => {
184
- return new LayoutRecord(this.layout, item.recordId, item.modId, item.fieldData, item.portalData);
183
+ return res.map(item => {
184
+ return new LayoutRecord(this.layout, item.recordId, item.modId, item.fieldData, item.portalData, [], binding);
185
185
  });
186
186
  }
187
187
  catch (e) {
@@ -4,14 +4,16 @@ 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
+ import { ContainerField } from './fields/containerField.js';
12
+ import { type SessionBinding } from '../connection/databaseProvider.js';
11
13
  export declare class LayoutRecord<LAYOUT extends LayoutInterface> extends RecordBase<LAYOUT['fields']> implements LayoutRecordBase {
12
14
  portals: LAYOUT['portals'];
13
15
  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']>);
16
+ 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']>, sessionBinding?: SessionBinding);
15
17
  /** Returns all loaded portals on this record as an array. */
16
18
  get portalsArray(): Array<Portal<any>>;
17
19
  /**
@@ -21,6 +23,7 @@ export declare class LayoutRecord<LAYOUT extends LayoutInterface> extends Record
21
23
  * @throws {FMError} - If an error occurs during the commit process.
22
24
  */
23
25
  commit(extraBody?: extraBodyOptions): Promise<this>;
26
+ getFieldMetadata(fieldId: string): z.infer<typeof ApiFieldMetadata>;
24
27
  protected processPortalData(portalData: z.infer<typeof ApiPortalData>): void;
25
28
  /**
26
29
  * Reloads this record from FileMaker.
@@ -30,17 +33,21 @@ export declare class LayoutRecord<LAYOUT extends LayoutInterface> extends Record
30
33
  * @throws {Error} If commit() has not been called.
31
34
  * @throws {FMError} If the retrieval fails.
32
35
  */
33
- get(): Promise<this>;
36
+ get(portals?: Record<string, {
37
+ limit: number;
38
+ offset: number;
39
+ }>): Promise<this>;
34
40
  /** Creates a duplicate of this record in FileMaker. */
35
41
  duplicate(): Promise<LayoutRecord<LAYOUT>>;
36
42
  /** Deletes this record from FileMaker. */
37
43
  delete(): Promise<void>;
44
+ refreshContainer(fieldId: string): Promise<ContainerField>;
38
45
  /** Returns this record's edited fields as a FileMaker API payload. */
39
- fieldsToObject(filter?: (a: Field<FieldValue>) => boolean): Omit<z.infer<typeof ApiRowDataDef>, 'portalData'>;
46
+ fieldsToObject(filter?: (a: Field) => boolean): Omit<z.infer<typeof ApiRowDataDef>, 'portalData'>;
40
47
  /** 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>[]>;
48
+ toObject(filter?: (a: Field) => any, portalFilter?: (a: Portal<any>) => any, portalRowFilter?: (a: PortalRecord<any>) => any, portalFieldFilter?: (a: Field) => any): {
49
+ fieldData: Record<string, string | number | null>;
50
+ portalData: Record<string, Record<string, string | number | null>[]>;
44
51
  modId: string;
45
52
  recordId: string;
46
53
  portalDataInfo?: {
@@ -5,14 +5,13 @@ 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';
9
- import moment from 'moment';
10
- import z from 'zod';
8
+ import { ValueFieldBase } from "./fields/valueField.js";
9
+ import { ContainerField } from './fields/containerField.js';
11
10
  export class LayoutRecord extends RecordBase {
12
11
  portals = {};
13
12
  portalsToInclude;
14
- constructor(layout, recordId, modId = recordId, fieldData = {}, portalData = null, portalsToInclude = []) {
15
- super(layout, parseInt(recordId), parseInt(modId), fieldData);
13
+ constructor(layout, recordId, modId = recordId, fieldData = {}, portalData = null, portalsToInclude = [], sessionBinding) {
14
+ super(layout, parseInt(recordId), parseInt(modId), fieldData, sessionBinding);
16
15
  this.portalsToInclude = portalsToInclude;
17
16
  if (portalData) {
18
17
  this.processPortalData(portalData);
@@ -32,64 +31,42 @@ export class LayoutRecord extends RecordBase {
32
31
  const data = this.toObject();
33
32
  delete data.recordId;
34
33
  delete data.modId;
35
- if (extraBody.scripts?.after) {
36
- data.script = extraBody.scripts.after.name;
37
- if (extraBody.scripts.after.parameter)
38
- data['script.param'] = extraBody.scripts.after.parameter;
39
- }
40
- if (extraBody.scripts?.prerequest) {
41
- data['script.prerequest'] = extraBody.scripts.prerequest.name;
42
- if (extraBody.scripts.prerequest.parameter)
43
- data['script.prerequest.param'] = extraBody.scripts.prerequest.parameter;
44
- }
45
- if (extraBody.scripts?.presort) {
46
- data['script.presort'] = extraBody.scripts.presort.name;
47
- if (extraBody.scripts.presort.parameter)
48
- data['script.presort.param'] = extraBody.scripts.presort.parameter;
49
- }
50
34
  if (extraBody.options)
51
35
  data.options = extraBody.options;
52
36
  if (extraBody.deleteRelatedRecords)
53
- data.fieldData.deleteRelated = extraBody.deleteRelatedRecords.map(i => `${i.table}.${i.recordId}`);
37
+ data.deleteRelatedRecords = extraBody.deleteRelatedRecords;
38
+ const options = operationOptions(extraBody);
54
39
  if (this.recordId === -1) {
55
40
  // This is a new LayoutRecord
56
- const res = await this.layout.database.fetchJSON(`${this.layout.endpoint}/records`, {
57
- method: 'POST',
58
- body: JSON.stringify(data),
59
- type: z.object({
60
- recordId: z.string(),
61
- modId: z.string(),
62
- scriptError: z.string().optional()
63
- })
64
- });
65
- if (typeof res.scriptError !== 'undefined' && res.scriptError !== '0') {
66
- throw new FMError(res.scriptError, res.httpStatus, res);
41
+ const { value: res, binding } = await this.layout.database.execute({ type: 'record.create', layout: this.layout.name, body: data, options });
42
+ if (res.scriptError) {
43
+ throw new FMError(res.scriptError, res.status ?? 200, res);
67
44
  }
68
45
  this.recordId = parseInt(res.recordId);
69
46
  this.modId = parseInt(res.modId);
47
+ this.sessionBinding = binding;
48
+ this._onSave();
70
49
  return this;
71
50
  }
72
51
  // for (let item of Object.keys(data)) extraBody[item] = data[item]
73
- const res = await this.layout.database.fetchJSON(this.endpoint, {
74
- method: 'PATCH',
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
- })
85
- });
86
- if (typeof res.scriptError !== 'undefined' && res.scriptError !== '0') {
87
- throw new FMError(res.scriptError, res.httpStatus, res);
52
+ const { value: res, binding } = await this.layout.database.execute({ type: 'record.update', layout: this.layout.name, recordId: this.recordId, body: data, options });
53
+ if (res.scriptError) {
54
+ throw new FMError(res.scriptError, res.status ?? 200, res);
88
55
  }
89
56
  this.modId = +res.modId;
57
+ this.sessionBinding = binding;
90
58
  this._onSave();
91
59
  return this;
92
60
  }
61
+ getFieldMetadata(fieldId) {
62
+ if (!this.layout.metadata) {
63
+ throw new Error("Field metadata not found. Ensure you run layout.getLayoutMeta() first.");
64
+ }
65
+ let result = this.layout.metadata.fieldMetaData.find(i => i.name === fieldId);
66
+ if (!result)
67
+ throw new Error("Field metadata not found. Ensure you run layout.getLayoutMeta() first.");
68
+ return result;
69
+ }
93
70
  processPortalData(portalData) {
94
71
  for (const portalName of Object.keys(portalData)) {
95
72
  const _portal = new Portal(this, portalName);
@@ -97,7 +74,7 @@ export class LayoutRecord extends RecordBase {
97
74
  const fieldData = item;
98
75
  delete fieldData.recordId;
99
76
  delete fieldData.modId;
100
- return new PortalRecord(this, _portal, parseInt(item.recordId), parseInt(item.modId), fieldData);
77
+ return new PortalRecord(this, _portal, parseInt(item.recordId), parseInt(item.modId), fieldData, this.sessionBinding);
101
78
  });
102
79
  // @ts-expect-error - This code is actually correct, but throws a typescript error
103
80
  this.portals[portalName] = _portal;
@@ -111,87 +88,63 @@ export class LayoutRecord extends RecordBase {
111
88
  * @throws {Error} If commit() has not been called.
112
89
  * @throws {FMError} If the retrieval fails.
113
90
  */
114
- async get() {
91
+ async get(portals = {}) {
115
92
  if (this.recordId === -1) {
116
93
  throw new Error('Cannot get this RecordBase until a commit() is done.');
117
94
  }
118
95
  if (!this.layout.metadata)
119
96
  await this.layout.getLayoutMeta();
120
- const res = await this.layout.database.fetchJSON(this.endpoint, {
121
- type: ApiRecordResponseObj
122
- });
97
+ const { value: record, binding } = await this.layout.database.execute({ type: 'record.get', layout: this.layout.name, recordId: this.recordId, options: { scripts: {}, portals } });
123
98
  // console.log(res, res.response.data)
124
- this.modId = +res.data[0].modId;
125
- this.processFieldData(res.data[0].fieldData);
99
+ this.sessionBinding = binding;
100
+ this.modId = +record.modId;
101
+ this.processFieldData(record.fieldData);
126
102
  this.portalData = [];
127
- if (res.data[0].portalData)
128
- this.processPortalData(res.data[0].portalData);
103
+ if (record.portalData)
104
+ this.processPortalData(record.portalData);
129
105
  return this;
130
106
  }
131
107
  /** Creates a duplicate of this record in FileMaker. */
132
108
  async duplicate() {
133
109
  const trace = new Error();
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
- })
141
- });
142
- if (typeof res.scriptError !== 'undefined' && res.scriptError !== '0') {
143
- throw new FMError(res.scriptError, res.httpStatus, res, trace);
110
+ const { value: res, binding } = await this.layout.database.execute({ type: 'record.duplicate', layout: this.layout.name, recordId: this.recordId, options: emptyOperationOptions() });
111
+ if (res.scriptError) {
112
+ throw new FMError(res.scriptError, res.status ?? 200, res, trace);
144
113
  }
145
114
  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);
115
+ const _res = new LayoutRecord(this.layout, res.recordId, res.modId, data.fieldData, data.portalData, [], binding);
147
116
  this.emit('duplicated');
148
117
  return _res;
149
118
  }
150
119
  /** Deletes this record from FileMaker. */
151
120
  async 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()
157
- });
158
- if (typeof res.scriptError !== 'undefined' && res.scriptError !== '0') {
159
- throw new FMError(res.scriptError, res.httpStatus, res);
121
+ const { value: res } = await this.layout.database.execute({ type: 'record.delete', layout: this.layout.name, recordId: this.recordId, options: emptyOperationOptions() });
122
+ if (res.scriptError) {
123
+ throw new FMError(res.scriptError, res.status ?? 200, res);
160
124
  }
161
125
  this.emit('deleted');
162
126
  }
127
+ async refreshContainer(fieldId) {
128
+ await this.get();
129
+ const field = this.fields[fieldId];
130
+ if (!(field instanceof ContainerField))
131
+ throw new Error(`Container field ${fieldId} was not returned while refreshing record`);
132
+ return field;
133
+ }
163
134
  /** Returns this record's edited fields as a FileMaker API payload. */
164
- fieldsToObject(filter = (a) => a.edited) {
135
+ fieldsToObject(filter = (a) => a instanceof ValueFieldBase && a.edited) {
165
136
  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;
137
+ for (const field of this.fieldsArray.filter(field => filter(field))) {
138
+ fieldsProcessed[field.id] = field.serializeRawValue();
185
139
  }
186
- const obj = {
140
+ return {
187
141
  recordId: this.recordId.toString(),
188
142
  modId: this.modId.toString(),
189
143
  fieldData: fieldsProcessed
190
144
  };
191
- return obj;
192
145
  }
193
146
  /** 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) {
147
+ 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
148
  const obj = {
196
149
  ...this.fieldsToObject(filter),
197
150
  portalData: {}
@@ -209,3 +162,13 @@ export class LayoutRecord extends RecordBase {
209
162
  return obj;
210
163
  }
211
164
  }
165
+ function emptyOperationOptions() {
166
+ return { scripts: {}, portals: {} };
167
+ }
168
+ function operationOptions(extra) {
169
+ const scripts = {};
170
+ for (const [name, script] of Object.entries(extra.scripts ?? {}))
171
+ if (script)
172
+ scripts[name] = { name: script.name, parameter: script.parameter ?? '' };
173
+ return { scripts, portals: {} };
174
+ }
@@ -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,12 @@
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";
8
+ import { ContainerField } from './fields/containerField.js';
9
+ import { type SessionBinding } from '../connection/databaseProvider.js';
6
10
  /**
7
11
  * Represents a PortalRecord, which is a record in a portal within a parent record.
8
12
  * @template T - The type of the record's field map.
@@ -10,7 +14,8 @@ import { type Field, type FieldValue } from './field.js';
10
14
  export declare class PortalRecord<T extends RecordFieldsMap> extends RecordBase<T> {
11
15
  readonly portal: PortalBase<T>;
12
16
  readonly type = RecordTypes.PORTAL;
13
- constructor(record: RecordBase<any>, portal: PortalBase<any>, recordId: number, modId?: number, fieldData?: {});
17
+ constructor(record: RecordBase<any>, portal: PortalBase<any>, recordId: number, modId?: number, fieldData?: {}, sessionBinding?: SessionBinding);
18
+ refreshContainer(fieldId: string): Promise<ContainerField>;
14
19
  _onSave(): void;
15
20
  /**
16
21
  * Commits the parent record, and in turn this one.
@@ -18,8 +23,9 @@ export declare class PortalRecord<T extends RecordFieldsMap> extends RecordBase<
18
23
  * @param {extraBodyOptions} [extraBody={}] - The optional extra body options.
19
24
  * @returns {Promise} - A promise that resolves when the record is committed.
20
25
  */
21
- commit(extraBody?: extraBodyOptions): Promise<import("./layoutRecord.js").LayoutRecord<import("../index.js").LayoutInterface>>;
22
- toObject(fieldFilter: (a: Field<FieldValue>) => boolean): {
26
+ commit(extraBody?: extraBodyOptions): Promise<import("./layoutRecord.js").LayoutRecord<import("../layouts/layoutInterface.js").LayoutInterface>>;
27
+ getFieldMetadata(fieldId: string): z.infer<typeof ApiFieldMetadata>;
28
+ toObject(fieldFilter: (a: Field) => boolean): {
23
29
  modId?: string;
24
30
  recordId?: string;
25
31
  } & Record<string, string>;
@@ -3,6 +3,7 @@
3
3
  */
4
4
  import { RecordBase } from './recordBase.js';
5
5
  import { RecordTypes } from '../types.js';
6
+ import { ContainerField } from './fields/containerField.js';
6
7
  /**
7
8
  * Represents a PortalRecord, which is a record in a portal within a parent record.
8
9
  * @template T - The type of the record's field map.
@@ -10,10 +11,19 @@ import { RecordTypes } from '../types.js';
10
11
  export class PortalRecord extends RecordBase {
11
12
  portal;
12
13
  type = RecordTypes.PORTAL;
13
- constructor(record, portal, recordId, modId = recordId, fieldData = {}) {
14
- super(record.layout, recordId, modId, fieldData);
14
+ constructor(record, portal, recordId, modId = recordId, fieldData = {}, sessionBinding) {
15
+ super(record.layout, recordId, modId, fieldData, sessionBinding);
15
16
  this.portal = portal;
16
17
  }
18
+ async refreshContainer(fieldId) {
19
+ await this.portal.record.get({ [this.portal.name]: { offset: 1, limit: Math.max(1, this.portal.records.length) } });
20
+ const portal = this.portal.record.portalsArray.find(item => item.name === this.portal.name);
21
+ const record = portal?.records.find(item => item.recordId === this.recordId);
22
+ const field = record?.fields[fieldId];
23
+ if (!(field instanceof ContainerField))
24
+ throw new Error(`Container field ${fieldId} was not returned while refreshing record`);
25
+ return field;
26
+ }
17
27
  _onSave() {
18
28
  super._onSave();
19
29
  this.portal.record._onSave();
@@ -27,6 +37,18 @@ export class PortalRecord extends RecordBase {
27
37
  async commit(extraBody = {}) {
28
38
  return await this.portal.record.commit(extraBody);
29
39
  }
40
+ getFieldMetadata(fieldId) {
41
+ if (!this.layout.metadata) {
42
+ throw new Error("Field metadata not found. Ensure you run layout.getLayoutMeta() first.");
43
+ }
44
+ const portalMetadata = this.portal
45
+ ? this.layout.metadata.portalMetaData[this.portal.name] ?? []
46
+ : Object.values(this.layout.metadata.portalMetaData).flat();
47
+ const result = portalMetadata.find(i => i.name === fieldId);
48
+ if (!result)
49
+ throw new Error("Field metadata not found. Ensure you run layout.getLayoutMeta() first.");
50
+ return result;
51
+ }
30
52
  toObject(fieldFilter) {
31
53
  const res = {
32
54
  recordId: this.recordId === -1 ? undefined : this.recordId.toString(),