@jd-data-limited/easy-fm 5.0.0-beta.2 → 5.0.0-beta.5

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 (59) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +93 -91
  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 +19 -18
  7. package/dist/bin/init.d.ts +5 -0
  8. package/dist/bin/init.js +43 -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/dataApiDatabaseSession.d.ts +11 -0
  13. package/dist/connection/dataApiDatabaseSession.js +139 -0
  14. package/dist/connection/database.d.ts +22 -56
  15. package/dist/connection/database.js +175 -111
  16. package/dist/connection/databaseBase.d.ts +9 -22
  17. package/dist/connection/databaseBase.js +0 -3
  18. package/dist/connection/databaseProvider.d.ts +196 -0
  19. package/dist/connection/databaseProvider.js +6 -0
  20. package/dist/connection/databaseSessionPool.d.ts +8 -7
  21. package/dist/connection/databaseSessionPool.js +110 -106
  22. package/dist/index.d.ts +4 -3
  23. package/dist/index.js +3 -3
  24. package/dist/layouts/layout.d.ts +0 -2
  25. package/dist/layouts/layout.js +6 -16
  26. package/dist/layouts/layoutBase.d.ts +0 -1
  27. package/dist/layouts/layoutInterface.d.ts +4 -2
  28. package/dist/records/fields/baseField.d.ts +4 -1
  29. package/dist/records/fields/containerField.d.ts +6 -23
  30. package/dist/records/fields/containerField.js +34 -68
  31. package/dist/records/fields/valueField.js +6 -6
  32. package/dist/records/getOperations/recordGetOperation.js +19 -18
  33. package/dist/records/layoutRecord.d.ts +8 -2
  34. package/dist/records/layoutRecord.js +45 -74
  35. package/dist/records/portalRecord.d.ts +4 -1
  36. package/dist/records/portalRecord.js +12 -2
  37. package/dist/records/recordBase.d.ts +6 -3
  38. package/dist/records/recordBase.js +6 -5
  39. package/dist/schema.d.ts +54 -0
  40. package/dist/schema.js +87 -0
  41. package/dist/typegen/dataApiSchemaSource.d.ts +3 -0
  42. package/dist/typegen/dataApiSchemaSource.js +32 -0
  43. package/dist/typegen/dependencies.d.ts +11 -0
  44. package/dist/typegen/dependencies.js +67 -0
  45. package/dist/typegen/generate.d.ts +6 -0
  46. package/dist/typegen/generate.js +57 -0
  47. package/dist/typegen/index.d.ts +6 -0
  48. package/dist/typegen/index.js +6 -0
  49. package/dist/typegen/init.d.ts +20 -0
  50. package/dist/typegen/init.js +141 -0
  51. package/dist/typegen/types.d.ts +49 -0
  52. package/dist/typegen/types.js +1 -0
  53. package/dist/typegen/zod.d.ts +3 -0
  54. package/dist/typegen/zod.js +32 -0
  55. package/package.json +26 -4
  56. package/dist/layouts/recordFieldsMap.d.ts +0 -2
  57. package/dist/layouts/recordFieldsMap.js +0 -4
  58. package/dist/records/field.d.ts +0 -77
  59. package/dist/records/field.js +0 -192
@@ -2,7 +2,6 @@
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';
8
7
  import { Temporal } from "temporal-polyfill";
@@ -134,11 +133,11 @@ export class RecordGetOperation {
134
133
  return item;
135
134
  // Re-write date into correct format
136
135
  if (item instanceof Temporal.PlainDateTime)
137
- return temporalToString(item, this.layout.database.host.timeStampFormat);
136
+ return temporalToString(item, this.layout.database.timeStampFormat);
138
137
  if (item instanceof Temporal.PlainDate)
139
- return temporalToString(item, this.layout.database.host.dateFormat);
138
+ return temporalToString(item, this.layout.database.dateFormat);
140
139
  if (item instanceof Temporal.PlainTime)
141
- return temporalToString(item, this.layout.database.host.timeFormat);
140
+ return temporalToString(item, this.layout.database.timeFormat);
142
141
  }).join('');
143
142
  }
144
143
  return out;
@@ -164,23 +163,25 @@ export class RecordGetOperation {
164
163
  }
165
164
  async performFind(offset, limit) {
166
165
  await this.layout.getLayoutMeta();
167
- const isFind = this.isFindRequest;
168
- let endpoint = this.layout.endpoint + (isFind ? '/_find' : '/records');
169
- if (!isFind)
170
- endpoint += '?' + new URLSearchParams(this.generateParamsURL(offset, limit)).toString();
171
- const reqData = {
172
- // port: 443,
173
- method: isFind ? 'POST' : 'GET',
174
- type: ApiRecordResponseObj,
175
- body: isFind ? JSON.stringify(this.generateParamsBody(offset, limit)) : undefined
176
- };
177
166
  try {
178
- const res = await this.layout.database.fetchJSON(endpoint, reqData);
179
- // 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
+ });
180
181
  if (!this.layout.metadata)
181
182
  await this.layout.getLayoutMeta();
182
- return res.data.map(item => {
183
- 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);
184
185
  });
185
186
  }
186
187
  catch (e) {
@@ -8,10 +8,12 @@ import { type ApiFieldData, ApiFieldMetadata, type ApiPortalData, type ApiRowDat
8
8
  import { type LayoutBase } from '../layouts/layoutBase.js';
9
9
  import z from 'zod';
10
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?: z.infer<typeof ApiFieldData>, 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
  /**
@@ -31,11 +33,15 @@ export declare class LayoutRecord<LAYOUT extends LayoutInterface> extends Record
31
33
  * @throws {Error} If commit() has not been called.
32
34
  * @throws {FMError} If the retrieval fails.
33
35
  */
34
- get(): Promise<this>;
36
+ get(portals?: Record<string, {
37
+ limit: number;
38
+ offset: number;
39
+ }>): Promise<this>;
35
40
  /** Creates a duplicate of this record in FileMaker. */
36
41
  duplicate(): Promise<LayoutRecord<LAYOUT>>;
37
42
  /** Deletes this record from FileMaker. */
38
43
  delete(): Promise<void>;
44
+ refreshContainer(fieldId: string): Promise<ContainerField>;
39
45
  /** Returns this record's edited fields as a FileMaker API payload. */
40
46
  fieldsToObject(filter?: (a: Field) => boolean): Omit<z.infer<typeof ApiRowDataDef>, 'portalData'>;
41
47
  /** Returns this record and its portal edits as a FileMaker API payload. */
@@ -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 z from 'zod';
10
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,62 +31,30 @@ 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;
70
48
  this._onSave();
71
49
  return this;
72
50
  }
73
51
  // for (let item of Object.keys(data)) extraBody[item] = data[item]
74
- const res = await this.layout.database.fetchJSON(this.endpoint, {
75
- method: 'PATCH',
76
- body: JSON.stringify(data),
77
- type: z.object({
78
- modId: z.string(),
79
- newPortalRecordInfo: z.array(z.object({
80
- tableName: z.string(),
81
- recordId: z.string(),
82
- modId: z.string()
83
- })).optional(),
84
- scriptError: z.string().optional()
85
- })
86
- });
87
- if (typeof res.scriptError !== 'undefined' && res.scriptError !== '0') {
88
- 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);
89
55
  }
90
56
  this.modId = +res.modId;
57
+ this.sessionBinding = binding;
91
58
  this._onSave();
92
59
  return this;
93
60
  }
@@ -107,7 +74,7 @@ export class LayoutRecord extends RecordBase {
107
74
  const fieldData = item;
108
75
  delete fieldData.recordId;
109
76
  delete fieldData.modId;
110
- 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);
111
78
  });
112
79
  // @ts-expect-error - This code is actually correct, but throws a typescript error
113
80
  this.portals[portalName] = _portal;
@@ -121,55 +88,49 @@ export class LayoutRecord extends RecordBase {
121
88
  * @throws {Error} If commit() has not been called.
122
89
  * @throws {FMError} If the retrieval fails.
123
90
  */
124
- async get() {
91
+ async get(portals = {}) {
125
92
  if (this.recordId === -1) {
126
93
  throw new Error('Cannot get this RecordBase until a commit() is done.');
127
94
  }
128
95
  if (!this.layout.metadata)
129
96
  await this.layout.getLayoutMeta();
130
- const res = await this.layout.database.fetchJSON(this.endpoint, {
131
- type: ApiRecordResponseObj
132
- });
97
+ const { value: record, binding } = await this.layout.database.execute({ type: 'record.get', layout: this.layout.name, recordId: this.recordId, options: { scripts: {}, portals } });
133
98
  // console.log(res, res.response.data)
134
- this.modId = +res.data[0].modId;
135
- this.processFieldData(res.data[0].fieldData);
99
+ this.sessionBinding = binding;
100
+ this.modId = +record.modId;
101
+ this.processFieldData(record.fieldData);
136
102
  this.portalData = [];
137
- if (res.data[0].portalData)
138
- this.processPortalData(res.data[0].portalData);
103
+ if (record.portalData)
104
+ this.processPortalData(record.portalData);
139
105
  return this;
140
106
  }
141
107
  /** Creates a duplicate of this record in FileMaker. */
142
108
  async duplicate() {
143
109
  const trace = new Error();
144
- const res = await this.layout.database.fetchJSON(this.endpoint, {
145
- method: 'POST',
146
- type: z.object({
147
- recordId: z.string(),
148
- modId: z.string(),
149
- scriptError: z.string().optional()
150
- })
151
- });
152
- if (typeof res.scriptError !== 'undefined' && res.scriptError !== '0') {
153
- 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);
154
113
  }
155
114
  const data = this.toObject((a) => true, (a) => true, (a) => false, (a) => false);
156
- 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);
157
116
  this.emit('duplicated');
158
117
  return _res;
159
118
  }
160
119
  /** Deletes this record from FileMaker. */
161
120
  async delete() {
162
- const res = await this.layout.database.fetchJSON(this.endpoint, {
163
- method: 'DELETE',
164
- type: z.object({
165
- scriptError: z.string().optional()
166
- }).optional()
167
- });
168
- if (typeof res.scriptError !== 'undefined' && res.scriptError !== '0') {
169
- 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);
170
124
  }
171
125
  this.emit('deleted');
172
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
+ }
173
134
  /** Returns this record's edited fields as a FileMaker API payload. */
174
135
  fieldsToObject(filter = (a) => a instanceof ValueFieldBase && a.edited) {
175
136
  const fieldsProcessed = {};
@@ -201,3 +162,13 @@ export class LayoutRecord extends RecordBase {
201
162
  return obj;
202
163
  }
203
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
+ }
@@ -5,6 +5,8 @@ import z from "zod";
5
5
  import { ApiFieldMetadata } from "../models/apiResults.js";
6
6
  import { RecordFieldsMap } from "../layouts/layoutInterface.js";
7
7
  import { Field } from "./fields/field.js";
8
+ import { ContainerField } from './fields/containerField.js';
9
+ import { type SessionBinding } from '../connection/databaseProvider.js';
8
10
  /**
9
11
  * Represents a PortalRecord, which is a record in a portal within a parent record.
10
12
  * @template T - The type of the record's field map.
@@ -12,7 +14,8 @@ import { Field } from "./fields/field.js";
12
14
  export declare class PortalRecord<T extends RecordFieldsMap> extends RecordBase<T> {
13
15
  readonly portal: PortalBase<T>;
14
16
  readonly type = RecordTypes.PORTAL;
15
- 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>;
16
19
  _onSave(): void;
17
20
  /**
18
21
  * Commits the parent record, and in turn this one.
@@ -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();
@@ -1,10 +1,12 @@
1
- import { EventEmitter } from 'events';
1
+ import { EventEmitter } from 'node:events';
2
2
  import { RecordTypes } from '../types.js';
3
3
  import { type LayoutBase } from '../layouts/layoutBase.js';
4
4
  import { type ApiFieldData, type ApiFieldMetadata } from '../models/apiResults.js';
5
5
  import { type z } from 'zod';
6
6
  import { RecordFieldsMap } from "../layouts/layoutInterface.js";
7
7
  import { Field } from "./fields/field.js";
8
+ import { ContainerField } from "./fields/containerField.js";
9
+ import { type SessionBinding } from '../connection/databaseProvider.js';
8
10
  export declare abstract class RecordBase<T extends RecordFieldsMap> extends EventEmitter {
9
11
  readonly layout: LayoutBase;
10
12
  readonly type: RecordTypes;
@@ -17,8 +19,8 @@ export declare abstract class RecordBase<T extends RecordFieldsMap> extends Even
17
19
  */
18
20
  fields: T;
19
21
  protected portalData: any[];
20
- protected constructor(layout: LayoutBase, recordId: number, modId: number | undefined, fieldData: z.infer<typeof ApiFieldData>);
21
- get endpoint(): string;
22
+ sessionBinding?: SessionBinding;
23
+ protected constructor(layout: LayoutBase, recordId: number, modId: number | undefined, fieldData: z.infer<typeof ApiFieldData>, sessionBinding?: SessionBinding);
22
24
  /**
23
25
  * A boolean indicating whether this record has been modified and should be committed
24
26
  *
@@ -27,6 +29,7 @@ export declare abstract class RecordBase<T extends RecordFieldsMap> extends Even
27
29
  get edited(): boolean;
28
30
  get fieldsArray(): Array<Field>;
29
31
  abstract getFieldMetadata(fieldId: string): z.infer<typeof ApiFieldMetadata>;
32
+ abstract refreshContainer(fieldId: string): Promise<ContainerField>;
30
33
  protected processFieldData(fieldData: z.infer<typeof ApiFieldData>): T;
31
34
  _onSave(): void;
32
35
  }
@@ -1,7 +1,7 @@
1
1
  /*
2
2
  * Copyright (c) 2023-2024. See LICENSE file for more information
3
3
  */
4
- import { EventEmitter } from 'events';
4
+ import { EventEmitter } from 'node:events';
5
5
  import { RecordTypes } from '../types.js';
6
6
  import { DateField, NumberField, TextField, TimeField, TimeStampField, ValueFieldBase } from "./fields/valueField.js";
7
7
  import { ContainerField } from "./fields/containerField.js";
@@ -17,16 +17,15 @@ export class RecordBase extends EventEmitter {
17
17
  */
18
18
  fields;
19
19
  portalData = [];
20
- constructor(layout, recordId, modId = recordId, fieldData) {
20
+ sessionBinding;
21
+ constructor(layout, recordId, modId = recordId, fieldData, sessionBinding) {
21
22
  super();
22
23
  this.layout = layout;
23
24
  this.recordId = recordId;
24
25
  this.modId = modId;
26
+ this.sessionBinding = sessionBinding;
25
27
  this.fields = this.processFieldData(fieldData);
26
28
  }
27
- get endpoint() {
28
- return `${this.layout.endpoint}/records/${this.recordId}`;
29
- }
30
29
  /**
31
30
  * A boolean indicating whether this record has been modified and should be committed
32
31
  *
@@ -36,6 +35,8 @@ export class RecordBase extends EventEmitter {
36
35
  return !!this.fieldsArray.find(i => i instanceof ValueFieldBase && i.edited);
37
36
  }
38
37
  get fieldsArray() {
38
+ // Generated layout types may expose a narrower read-only view, while
39
+ // runtime records always contain concrete Field instances.
39
40
  return Object.values(this.fields);
40
41
  }
41
42
  processFieldData(fieldData) {
@@ -0,0 +1,54 @@
1
+ import type { DatabaseProvider } from './connection/databaseProvider.js';
2
+ import type { Field } from './records/fields/field.js';
3
+ export type ReadonlyField<T extends Field> = T extends Field ? Omit<T, 'set' | 'upload' | 'value'> & {
4
+ readonly value: T['value'];
5
+ } : never;
6
+ export interface ValidationIssue {
7
+ path: PropertyKey[];
8
+ message: string;
9
+ code?: string;
10
+ }
11
+ export type ValidationResult = {
12
+ success: true;
13
+ } | {
14
+ success: false;
15
+ issues: ValidationIssue[];
16
+ cause?: unknown;
17
+ };
18
+ export interface RuntimeValidator {
19
+ validate(schema: unknown, value: unknown): ValidationResult;
20
+ }
21
+ /** Built-in runtime adapter used by Zod-generated clients. */
22
+ export declare const zodValidator: RuntimeValidator;
23
+ export interface ExpectedFieldMetadata {
24
+ result: string;
25
+ type: string;
26
+ }
27
+ export interface LayoutRuntimeSchema {
28
+ /** Validates the complete canonical ProviderRecord returned by a provider. */
29
+ record: unknown;
30
+ metadata?: {
31
+ fields: Record<string, ExpectedFieldMetadata>;
32
+ portals: Record<string, Record<string, ExpectedFieldMetadata>>;
33
+ };
34
+ }
35
+ export interface DatabaseRuntimeSchema {
36
+ validator: RuntimeValidator;
37
+ layouts: Record<string, LayoutRuntimeSchema>;
38
+ }
39
+ export declare class SchemaDriftError extends Error {
40
+ readonly layout: string;
41
+ readonly recordId?: string;
42
+ readonly portal?: string;
43
+ readonly phase: 'metadata' | 'record' | 'portal';
44
+ readonly issues: ValidationIssue[];
45
+ constructor(options: {
46
+ layout: string;
47
+ recordId?: string;
48
+ portal?: string;
49
+ phase?: SchemaDriftError['phase'];
50
+ issues: ValidationIssue[];
51
+ }, cause?: unknown);
52
+ }
53
+ /** Decorates any provider with generated, provider-neutral metadata and result validation. */
54
+ export declare function withSchemaValidation(provider: DatabaseProvider, schema: DatabaseRuntimeSchema): DatabaseProvider;
package/dist/schema.js ADDED
@@ -0,0 +1,87 @@
1
+ /** Built-in runtime adapter used by Zod-generated clients. */
2
+ export const zodValidator = {
3
+ validate(schema, value) {
4
+ const result = schema.safeParse(value);
5
+ return result.success ? { success: true } : { success: false, issues: result.error.issues, cause: result.error };
6
+ }
7
+ };
8
+ export class SchemaDriftError extends Error {
9
+ layout;
10
+ recordId;
11
+ portal;
12
+ phase;
13
+ issues;
14
+ constructor(options, cause) {
15
+ const location = [options.layout, options.recordId && `record ${options.recordId}`, options.portal && `portal ${options.portal}`].filter(Boolean).join(', ');
16
+ super(`Schema drift detected in ${location}: ${options.issues.map(issue => `${issue.path.map(String).join('.')}: ${issue.message}`).join('; ')}`, { cause });
17
+ this.name = 'SchemaDriftError';
18
+ this.layout = options.layout;
19
+ this.recordId = options.recordId;
20
+ this.portal = options.portal;
21
+ this.phase = options.phase ?? (options.portal ? 'portal' : options.recordId ? 'record' : 'metadata');
22
+ this.issues = options.issues;
23
+ }
24
+ }
25
+ /** Decorates any provider with generated, provider-neutral metadata and result validation. */
26
+ export function withSchemaValidation(provider, schema) {
27
+ return { name: provider.name, formatting: provider.formatting, async connect(context) {
28
+ const connection = await provider.connect(context);
29
+ return { formatting: connection.formatting, maxSessions: connection.maxSessions, close: connection.close?.bind(connection), async openSession(signal) {
30
+ const session = await connection.openSession(signal);
31
+ return { close: session.close.bind(session), fetchContainer: session.fetchContainer.bind(session), async execute(operation) {
32
+ const result = await session.execute(operation);
33
+ if (operation.type === 'layout.metadata')
34
+ validateMetadata(schema, operation.layout, result);
35
+ if (operation.type === 'record.get')
36
+ validateRecord(schema, operation.layout, result);
37
+ if (operation.type === 'record.list')
38
+ for (const record of result)
39
+ validateRecord(schema, operation.layout, record);
40
+ return result;
41
+ } };
42
+ } };
43
+ } };
44
+ }
45
+ function validateRecord(schema, layout, record) {
46
+ const layoutSchema = schema.layouts[layout];
47
+ if (!layoutSchema)
48
+ return;
49
+ assertValid(schema.validator, layoutSchema.record, record, { layout, recordId: record.recordId, phase: 'record' });
50
+ }
51
+ function assertValid(validator, validationSchema, value, context) {
52
+ const result = validator.validate(validationSchema, value);
53
+ if (!result.success)
54
+ throw new SchemaDriftError({ ...context, issues: result.issues }, result.cause);
55
+ }
56
+ function validateMetadata(schema, layout, value) {
57
+ const expected = schema.layouts[layout]?.metadata;
58
+ if (!expected)
59
+ return;
60
+ const actual = value;
61
+ const issues = [];
62
+ compareFields(expected.fields, actual.fieldMetaData ?? [], ['fieldMetaData'], issues);
63
+ for (const [portal, fields] of Object.entries(expected.portals))
64
+ compareFields(fields, actual.portalMetaData?.[portal] ?? [], ['portalMetaData', portal], issues);
65
+ for (const portal of Object.keys(actual.portalMetaData ?? {}))
66
+ if (!(portal in expected.portals))
67
+ issues.push({ path: ['portalMetaData', portal], message: 'Unexpected portal' });
68
+ if (issues.length)
69
+ throw new SchemaDriftError({ layout, phase: 'metadata', issues });
70
+ }
71
+ function compareFields(expected, actualFields, path, issues) {
72
+ const actual = new Map(actualFields.map(field => [field.name, field]));
73
+ for (const [name, field] of Object.entries(expected)) {
74
+ const found = actual.get(name);
75
+ if (!found)
76
+ issues.push({ path: [...path, name], message: 'Missing field' });
77
+ else {
78
+ if (found.result !== field.result)
79
+ issues.push({ path: [...path, name, 'result'], message: `Expected ${field.result}, received ${found.result}` });
80
+ if (found.type !== field.type)
81
+ issues.push({ path: [...path, name, 'type'], message: `Expected ${field.type}, received ${found.type}` });
82
+ }
83
+ }
84
+ for (const name of actual.keys())
85
+ if (!(name in expected))
86
+ issues.push({ path: [...path, name], message: 'Unexpected field' });
87
+ }
@@ -0,0 +1,3 @@
1
+ import { type DataApiProviderOptions } from '../connection/DataApiProvider.js';
2
+ import type { SchemaSource } from './types.js';
3
+ export declare function dataApiSchemaSource(options: DataApiProviderOptions): SchemaSource;
@@ -0,0 +1,32 @@
1
+ import { Database } from '../connection/database.js';
2
+ import { DataApiProvider } from '../connection/DataApiProvider.js';
3
+ export function dataApiSchemaSource(options) {
4
+ return {
5
+ async introspect() {
6
+ const database = Database.create({ provider: new DataApiProvider(options) });
7
+ try {
8
+ const layouts = await database.listLayouts();
9
+ return {
10
+ version: 1,
11
+ layouts: await Promise.all(layouts.map(async (layout) => {
12
+ const metadata = await layout.getLayoutMeta();
13
+ return {
14
+ name: layout.name,
15
+ fields: unique(metadata.fieldMetaData.map(toField)),
16
+ portals: Object.fromEntries(Object.entries(metadata.portalMetaData).map(([name, fields]) => [name, unique(fields.map(toField))]))
17
+ };
18
+ }))
19
+ };
20
+ }
21
+ finally {
22
+ await database.close();
23
+ }
24
+ }
25
+ };
26
+ }
27
+ function toField(field) {
28
+ return { name: field.name, result: field.result, type: field.type, writable: field.type === 'normal', required: field.notEmpty };
29
+ }
30
+ function unique(fields) {
31
+ return [...new Map(fields.map(field => [field.name, field])).values()];
32
+ }
@@ -0,0 +1,11 @@
1
+ type PackageManager = 'pnpm' | 'npm' | 'yarn' | 'bun';
2
+ export interface InstallPlan {
3
+ cwd: string;
4
+ command: PackageManager;
5
+ args: string[];
6
+ }
7
+ /** Ensures generated code can resolve its direct `zod` import. */
8
+ export declare function ensureZodInstalled(start: string): Promise<boolean>;
9
+ /** Returns null when Zod is already declared. Exported for alternate CLIs. */
10
+ export declare function getZodInstallPlan(start: string): Promise<InstallPlan | null>;
11
+ export {};