@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.
- package/LICENSE +21 -21
- package/README.md +93 -91
- package/dist/bin/cli.js +34 -5
- package/dist/bin/generateClient.d.ts +1 -0
- package/dist/bin/generateClient.js +48 -0
- package/dist/bin/generateTypes.js +19 -18
- package/dist/bin/init.d.ts +5 -0
- package/dist/bin/init.js +43 -0
- package/dist/bin/stressSearch.js +12 -12
- package/dist/connection/DataApiProvider.d.ts +15 -0
- package/dist/connection/DataApiProvider.js +246 -0
- package/dist/connection/dataApiDatabaseSession.d.ts +11 -0
- package/dist/connection/dataApiDatabaseSession.js +139 -0
- package/dist/connection/database.d.ts +22 -56
- package/dist/connection/database.js +175 -111
- package/dist/connection/databaseBase.d.ts +9 -22
- package/dist/connection/databaseBase.js +0 -3
- package/dist/connection/databaseProvider.d.ts +196 -0
- package/dist/connection/databaseProvider.js +6 -0
- package/dist/connection/databaseSessionPool.d.ts +8 -7
- package/dist/connection/databaseSessionPool.js +110 -106
- package/dist/index.d.ts +4 -3
- package/dist/index.js +3 -3
- package/dist/layouts/layout.d.ts +0 -2
- package/dist/layouts/layout.js +6 -16
- package/dist/layouts/layoutBase.d.ts +0 -1
- package/dist/layouts/layoutInterface.d.ts +4 -2
- package/dist/records/fields/baseField.d.ts +4 -1
- package/dist/records/fields/containerField.d.ts +6 -23
- package/dist/records/fields/containerField.js +34 -68
- package/dist/records/fields/valueField.js +6 -6
- package/dist/records/getOperations/recordGetOperation.js +19 -18
- package/dist/records/layoutRecord.d.ts +8 -2
- package/dist/records/layoutRecord.js +45 -74
- package/dist/records/portalRecord.d.ts +4 -1
- package/dist/records/portalRecord.js +12 -2
- package/dist/records/recordBase.d.ts +6 -3
- package/dist/records/recordBase.js +6 -5
- package/dist/schema.d.ts +54 -0
- package/dist/schema.js +87 -0
- package/dist/typegen/dataApiSchemaSource.d.ts +3 -0
- package/dist/typegen/dataApiSchemaSource.js +32 -0
- package/dist/typegen/dependencies.d.ts +11 -0
- package/dist/typegen/dependencies.js +67 -0
- package/dist/typegen/generate.d.ts +6 -0
- package/dist/typegen/generate.js +57 -0
- package/dist/typegen/index.d.ts +6 -0
- package/dist/typegen/index.js +6 -0
- package/dist/typegen/init.d.ts +20 -0
- package/dist/typegen/init.js +141 -0
- package/dist/typegen/types.d.ts +49 -0
- package/dist/typegen/types.js +1 -0
- package/dist/typegen/zod.d.ts +3 -0
- package/dist/typegen/zod.js +32 -0
- package/package.json +26 -4
- package/dist/layouts/recordFieldsMap.d.ts +0 -2
- package/dist/layouts/recordFieldsMap.js +0 -4
- package/dist/records/field.d.ts +0 -77
- 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.
|
|
136
|
+
return temporalToString(item, this.layout.database.timeStampFormat);
|
|
138
137
|
if (item instanceof Temporal.PlainDate)
|
|
139
|
-
return temporalToString(item, this.layout.database.
|
|
138
|
+
return temporalToString(item, this.layout.database.dateFormat);
|
|
140
139
|
if (item instanceof Temporal.PlainTime)
|
|
141
|
-
return temporalToString(item, this.layout.database.
|
|
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
|
|
179
|
-
|
|
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.
|
|
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(
|
|
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.
|
|
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.
|
|
57
|
-
|
|
58
|
-
|
|
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.
|
|
75
|
-
|
|
76
|
-
|
|
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
|
|
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.
|
|
135
|
-
this.
|
|
99
|
+
this.sessionBinding = binding;
|
|
100
|
+
this.modId = +record.modId;
|
|
101
|
+
this.processFieldData(record.fieldData);
|
|
136
102
|
this.portalData = [];
|
|
137
|
-
if (
|
|
138
|
-
this.processPortalData(
|
|
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.
|
|
145
|
-
|
|
146
|
-
|
|
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.
|
|
163
|
-
|
|
164
|
-
|
|
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
|
-
|
|
21
|
-
|
|
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
|
-
|
|
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) {
|
package/dist/schema.d.ts
ADDED
|
@@ -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,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 {};
|