@jd-data-limited/easy-fm 5.0.0-beta.4 → 5.0.0-beta.7
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/dist/bin/cli.js +4 -1
- package/dist/bin/init.d.ts +0 -1
- package/dist/bin/init.js +26 -29
- package/dist/layouts/layout.js +2 -2
- package/dist/models/apiResults.d.ts +27 -14
- package/dist/models/apiResults.js +23 -3
- package/dist/records/recordBase.js +2 -1
- package/dist/schema.js +14 -4
- package/dist/typegen/dataApiSchemaSource.js +18 -11
- package/dist/typegen/generate.d.ts +4 -0
- package/dist/typegen/generate.js +47 -68
- package/dist/typegen/index.d.ts +2 -2
- package/dist/typegen/index.js +1 -1
- package/dist/typegen/init.d.ts +7 -0
- package/dist/typegen/init.js +28 -0
- package/dist/typegen/types.d.ts +20 -1
- package/package.json +1 -1
package/dist/bin/cli.js
CHANGED
|
@@ -40,7 +40,10 @@ program
|
|
|
40
40
|
if (await typegen.ensureZodInstalled(dirname(configPath)))
|
|
41
41
|
console.log(chalk.green('Installed Zod for generated runtime validation.'));
|
|
42
42
|
const loaded = await import(pathToFileURL(configPath).href);
|
|
43
|
-
const result = await typegen.generate(loaded.default)
|
|
43
|
+
const result = await typegen.generate({ ...loaded.default, onDiagnostic: (diagnostic) => {
|
|
44
|
+
const location = [diagnostic.database, diagnostic.layout, diagnostic.portal].filter(Boolean).join('/');
|
|
45
|
+
console.warn(chalk.yellow(`Warning [${location}]: ${diagnostic.message}`));
|
|
46
|
+
} });
|
|
44
47
|
console.log(chalk.green(`Generated ${result.layouts} layouts in ${result.output}`));
|
|
45
48
|
});
|
|
46
49
|
program.parse(process.argv);
|
package/dist/bin/init.d.ts
CHANGED
|
@@ -2,5 +2,4 @@ export { configExists } from '../typegen/init.js';
|
|
|
2
2
|
export interface InitOptions {
|
|
3
3
|
config: string;
|
|
4
4
|
}
|
|
5
|
-
/** Interactive initializer shared by `easyfm init` and first `easyfm typegen`. */
|
|
6
5
|
export declare function initializeTypegen({ config }: InitOptions): Promise<boolean>;
|
package/dist/bin/init.js
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import inquirer from 'inquirer';
|
|
3
3
|
import { dirname, resolve } from 'node:path';
|
|
4
|
-
import { configExists,
|
|
4
|
+
import { configExists, writeMultiDatabaseInitialization } from '../typegen/init.js';
|
|
5
5
|
export { configExists } from '../typegen/init.js';
|
|
6
|
-
/** Interactive initializer shared by `easyfm init` and first `easyfm typegen`. */
|
|
7
6
|
export async function initializeTypegen({ config }) {
|
|
8
7
|
const configPath = resolve(config);
|
|
9
8
|
if (await configExists(configPath)) {
|
|
@@ -11,36 +10,34 @@ export async function initializeTypegen({ config }) {
|
|
|
11
10
|
return false;
|
|
12
11
|
}
|
|
13
12
|
console.log(chalk.bold.cyan('EasyFM typegen setup'));
|
|
14
|
-
const
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
await
|
|
13
|
+
const { output } = await inquirer.prompt([{ type: 'input', name: 'output', message: 'Generated client directory', default: './src/easyfm-generated', validate: required('Output directory') }]);
|
|
14
|
+
const databases = [];
|
|
15
|
+
let more = true;
|
|
16
|
+
while (more) {
|
|
17
|
+
const answers = await inquirer.prompt([
|
|
18
|
+
{ type: 'input', name: 'key', message: 'Database client name', validate: (value) => !value.trim() ? 'Client name is required' : databases.some(db => db.key === value) ? 'Client name must be unique' : true },
|
|
19
|
+
{ type: 'select', name: 'provider', message: 'Provider', choices: [{ name: 'Data API/OData', value: 'data-api/odata' }] },
|
|
20
|
+
{ type: 'select', name: 'transports', message: 'Transports', default: 'both', choices: [{ name: 'Both', value: 'both' }, { name: 'Data API', value: 'data-api' }, { name: 'OData', value: 'odata' }] },
|
|
21
|
+
{ type: 'input', name: 'hostname', message: 'FileMaker host URL', default: 'https://', validate: validHost },
|
|
22
|
+
{ type: 'input', name: 'database', message: 'Database filename', validate: required('Database filename') },
|
|
23
|
+
{ type: 'input', name: 'username', message: 'Username', validate: required('Username') },
|
|
24
|
+
{ type: 'password', name: 'password', message: 'Password', mask: '*', validate: required('Password') }
|
|
25
|
+
]);
|
|
26
|
+
databases.push(answers);
|
|
27
|
+
const result = await inquirer.prompt([{ type: 'confirm', name: 'more', message: 'Add another database?', default: false }]);
|
|
28
|
+
more = result.more;
|
|
29
|
+
}
|
|
30
|
+
await writeMultiDatabaseInitialization(configPath, { output, databases });
|
|
32
31
|
console.log(chalk.green(`Created ${configPath}`));
|
|
33
|
-
console.log(chalk.green(`Saved
|
|
32
|
+
console.log(chalk.green(`Saved database-scoped credentials in ${resolve(dirname(configPath), '.env')}`));
|
|
34
33
|
console.log(chalk.dim('Keep .env out of version control.'));
|
|
35
34
|
return true;
|
|
36
35
|
}
|
|
37
36
|
function required(label) { return (value) => value.trim() ? true : `${label} is required`; }
|
|
38
|
-
function validHost(value) {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
return url.protocol === 'http:' || url.protocol === 'https:' ? true : 'Use an http:// or https:// URL';
|
|
42
|
-
}
|
|
43
|
-
catch {
|
|
44
|
-
return 'Use a valid http:// or https:// URL';
|
|
45
|
-
}
|
|
37
|
+
function validHost(value) { try {
|
|
38
|
+
const url = new URL(value);
|
|
39
|
+
return url.protocol === 'http:' || url.protocol === 'https:' ? true : 'Use an http:// or https:// URL';
|
|
46
40
|
}
|
|
41
|
+
catch {
|
|
42
|
+
return 'Use a valid http:// or https:// URL';
|
|
43
|
+
} }
|
package/dist/layouts/layout.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { LayoutRecordManager } from './layoutRecordManager.js';
|
|
5
5
|
import { FMError } from '../FMError.js';
|
|
6
|
-
import { ApiLayoutMetadata } from '../models/apiResults.js';
|
|
6
|
+
import { ApiLayoutMetadata, stripInaccessibleMetadata } from '../models/apiResults.js';
|
|
7
7
|
export class Layout {
|
|
8
8
|
database;
|
|
9
9
|
name;
|
|
@@ -37,7 +37,7 @@ export class Layout {
|
|
|
37
37
|
return this.metadata;
|
|
38
38
|
}
|
|
39
39
|
const { value: res } = await this.database.execute({ type: 'layout.metadata', layout: this.name });
|
|
40
|
-
this.metadata = ApiLayoutMetadata.parse(res);
|
|
40
|
+
this.metadata = stripInaccessibleMetadata(ApiLayoutMetadata.parse(res));
|
|
41
41
|
return this.metadata;
|
|
42
42
|
}
|
|
43
43
|
}
|
|
@@ -7,6 +7,7 @@ export declare const ApiFieldTypes: {
|
|
|
7
7
|
export declare const ApiFieldDisplayTypes: {
|
|
8
8
|
readonly EDIT_TEXT: "editText";
|
|
9
9
|
readonly POPUP_LIST: "popupList";
|
|
10
|
+
readonly POPUP_MENU: "popupMenu";
|
|
10
11
|
readonly CHECKBOX: "checkBox";
|
|
11
12
|
readonly RADIO_BUTTONS: "radioButtons";
|
|
12
13
|
readonly SELECTION_LIST: "selectionList";
|
|
@@ -39,28 +40,29 @@ export declare const ApiLayout: z.ZodType<{
|
|
|
39
40
|
}>;
|
|
40
41
|
export declare const ApiFieldMetadata: z.ZodObject<{
|
|
41
42
|
name: z.ZodString;
|
|
42
|
-
type: z.ZodEnum<{
|
|
43
|
+
type: z.ZodUnion<readonly [z.ZodEnum<{
|
|
43
44
|
readonly NORMAL: "normal";
|
|
44
45
|
readonly CALCULATION: "calculation";
|
|
45
46
|
readonly SUMMARY: "summary";
|
|
46
|
-
}>;
|
|
47
|
+
}>, z.ZodLiteral<"invalid">]>;
|
|
47
48
|
displayType: z.ZodEnum<{
|
|
48
49
|
readonly EDIT_TEXT: "editText";
|
|
49
50
|
readonly POPUP_LIST: "popupList";
|
|
51
|
+
readonly POPUP_MENU: "popupMenu";
|
|
50
52
|
readonly CHECKBOX: "checkBox";
|
|
51
53
|
readonly RADIO_BUTTONS: "radioButtons";
|
|
52
54
|
readonly SELECTION_LIST: "selectionList";
|
|
53
55
|
readonly CALENDAR: "calendar";
|
|
54
56
|
readonly SECURE_TEXT: "secureText";
|
|
55
57
|
}>;
|
|
56
|
-
result: z.ZodEnum<{
|
|
58
|
+
result: z.ZodUnion<readonly [z.ZodEnum<{
|
|
57
59
|
readonly TEXT: "text";
|
|
58
60
|
readonly NUMBER: "number";
|
|
59
61
|
readonly DATE: "date";
|
|
60
62
|
readonly TIME: "time";
|
|
61
63
|
readonly TIMESTAMP: "timeStamp";
|
|
62
64
|
readonly CONTAINER: "container";
|
|
63
|
-
}>;
|
|
65
|
+
}>, z.ZodLiteral<"invalid">]>;
|
|
64
66
|
global: z.ZodBoolean;
|
|
65
67
|
autoEnter: z.ZodBoolean;
|
|
66
68
|
fourDigitYear: z.ZodBoolean;
|
|
@@ -78,34 +80,35 @@ export declare const ApiValueList: z.ZodObject<{
|
|
|
78
80
|
type: z.ZodString;
|
|
79
81
|
values: z.ZodArray<z.ZodObject<{
|
|
80
82
|
value: z.ZodString;
|
|
81
|
-
displayName: z.ZodString
|
|
83
|
+
displayName: z.ZodOptional<z.ZodString>;
|
|
82
84
|
}, z.core.$strip>>;
|
|
83
85
|
}, z.core.$strip>;
|
|
84
86
|
export declare const ApiLayoutMetadata: z.ZodObject<{
|
|
85
87
|
fieldMetaData: z.ZodArray<z.ZodObject<{
|
|
86
88
|
name: z.ZodString;
|
|
87
|
-
type: z.ZodEnum<{
|
|
89
|
+
type: z.ZodUnion<readonly [z.ZodEnum<{
|
|
88
90
|
readonly NORMAL: "normal";
|
|
89
91
|
readonly CALCULATION: "calculation";
|
|
90
92
|
readonly SUMMARY: "summary";
|
|
91
|
-
}>;
|
|
93
|
+
}>, z.ZodLiteral<"invalid">]>;
|
|
92
94
|
displayType: z.ZodEnum<{
|
|
93
95
|
readonly EDIT_TEXT: "editText";
|
|
94
96
|
readonly POPUP_LIST: "popupList";
|
|
97
|
+
readonly POPUP_MENU: "popupMenu";
|
|
95
98
|
readonly CHECKBOX: "checkBox";
|
|
96
99
|
readonly RADIO_BUTTONS: "radioButtons";
|
|
97
100
|
readonly SELECTION_LIST: "selectionList";
|
|
98
101
|
readonly CALENDAR: "calendar";
|
|
99
102
|
readonly SECURE_TEXT: "secureText";
|
|
100
103
|
}>;
|
|
101
|
-
result: z.ZodEnum<{
|
|
104
|
+
result: z.ZodUnion<readonly [z.ZodEnum<{
|
|
102
105
|
readonly TEXT: "text";
|
|
103
106
|
readonly NUMBER: "number";
|
|
104
107
|
readonly DATE: "date";
|
|
105
108
|
readonly TIME: "time";
|
|
106
109
|
readonly TIMESTAMP: "timeStamp";
|
|
107
110
|
readonly CONTAINER: "container";
|
|
108
|
-
}>;
|
|
111
|
+
}>, z.ZodLiteral<"invalid">]>;
|
|
109
112
|
global: z.ZodBoolean;
|
|
110
113
|
autoEnter: z.ZodBoolean;
|
|
111
114
|
fourDigitYear: z.ZodBoolean;
|
|
@@ -120,28 +123,29 @@ export declare const ApiLayoutMetadata: z.ZodObject<{
|
|
|
120
123
|
}, z.core.$strip>>;
|
|
121
124
|
portalMetaData: z.ZodRecord<z.ZodString, z.ZodArray<z.ZodObject<{
|
|
122
125
|
name: z.ZodString;
|
|
123
|
-
type: z.ZodEnum<{
|
|
126
|
+
type: z.ZodUnion<readonly [z.ZodEnum<{
|
|
124
127
|
readonly NORMAL: "normal";
|
|
125
128
|
readonly CALCULATION: "calculation";
|
|
126
129
|
readonly SUMMARY: "summary";
|
|
127
|
-
}>;
|
|
130
|
+
}>, z.ZodLiteral<"invalid">]>;
|
|
128
131
|
displayType: z.ZodEnum<{
|
|
129
132
|
readonly EDIT_TEXT: "editText";
|
|
130
133
|
readonly POPUP_LIST: "popupList";
|
|
134
|
+
readonly POPUP_MENU: "popupMenu";
|
|
131
135
|
readonly CHECKBOX: "checkBox";
|
|
132
136
|
readonly RADIO_BUTTONS: "radioButtons";
|
|
133
137
|
readonly SELECTION_LIST: "selectionList";
|
|
134
138
|
readonly CALENDAR: "calendar";
|
|
135
139
|
readonly SECURE_TEXT: "secureText";
|
|
136
140
|
}>;
|
|
137
|
-
result: z.ZodEnum<{
|
|
141
|
+
result: z.ZodUnion<readonly [z.ZodEnum<{
|
|
138
142
|
readonly TEXT: "text";
|
|
139
143
|
readonly NUMBER: "number";
|
|
140
144
|
readonly DATE: "date";
|
|
141
145
|
readonly TIME: "time";
|
|
142
146
|
readonly TIMESTAMP: "timeStamp";
|
|
143
147
|
readonly CONTAINER: "container";
|
|
144
|
-
}>;
|
|
148
|
+
}>, z.ZodLiteral<"invalid">]>;
|
|
145
149
|
global: z.ZodBoolean;
|
|
146
150
|
autoEnter: z.ZodBoolean;
|
|
147
151
|
fourDigitYear: z.ZodBoolean;
|
|
@@ -159,10 +163,19 @@ export declare const ApiLayoutMetadata: z.ZodObject<{
|
|
|
159
163
|
type: z.ZodString;
|
|
160
164
|
values: z.ZodArray<z.ZodObject<{
|
|
161
165
|
value: z.ZodString;
|
|
162
|
-
displayName: z.ZodString
|
|
166
|
+
displayName: z.ZodOptional<z.ZodString>;
|
|
163
167
|
}, z.core.$strip>>;
|
|
164
168
|
}, z.core.$strip>>>;
|
|
165
169
|
}, z.core.$strip>;
|
|
170
|
+
export type ApiLayoutMetadataValue = z.infer<typeof ApiLayoutMetadata>;
|
|
171
|
+
export declare function isAccessibleFieldMetadata(field: z.infer<typeof ApiFieldMetadata>): field is z.infer<typeof ApiFieldMetadata> & {
|
|
172
|
+
type: z.infer<typeof ApiFieldTypes>;
|
|
173
|
+
result: z.infer<typeof ApiFieldResultTypes>;
|
|
174
|
+
};
|
|
175
|
+
/** Removes FileMaker metadata placeholders for fields unavailable to the current account. */
|
|
176
|
+
export declare function stripInaccessibleMetadata(metadata: ApiLayoutMetadataValue): ApiLayoutMetadataValue;
|
|
177
|
+
/** Removes inaccessible placeholder values before strict generated validation. */
|
|
178
|
+
export declare function stripInaccessibleFieldData<T extends Record<string, unknown>>(fields: T): T;
|
|
166
179
|
export declare const ApiScriptResult: z.ZodObject<{
|
|
167
180
|
scriptError: z.ZodString;
|
|
168
181
|
scriptResult: z.ZodOptional<z.ZodString>;
|
|
@@ -10,6 +10,7 @@ export const ApiFieldTypes = {
|
|
|
10
10
|
export const ApiFieldDisplayTypes = {
|
|
11
11
|
EDIT_TEXT: 'editText',
|
|
12
12
|
POPUP_LIST: 'popupList',
|
|
13
|
+
POPUP_MENU: 'popupMenu',
|
|
13
14
|
CHECKBOX: 'checkBox',
|
|
14
15
|
RADIO_BUTTONS: 'radioButtons',
|
|
15
16
|
SELECTION_LIST: 'selectionList',
|
|
@@ -38,9 +39,9 @@ export const ApiLayout = z.lazy(() => z.object({
|
|
|
38
39
|
}));
|
|
39
40
|
export const ApiFieldMetadata = z.object({
|
|
40
41
|
name: z.string(),
|
|
41
|
-
type: z.enum(ApiFieldTypes),
|
|
42
|
+
type: z.union([z.enum(ApiFieldTypes), z.literal('invalid')]),
|
|
42
43
|
displayType: z.enum(ApiFieldDisplayTypes),
|
|
43
|
-
result: z.enum(ApiFieldResultTypes),
|
|
44
|
+
result: z.union([z.enum(ApiFieldResultTypes), z.literal('invalid')]),
|
|
44
45
|
global: z.boolean(),
|
|
45
46
|
autoEnter: z.boolean(),
|
|
46
47
|
fourDigitYear: z.boolean(),
|
|
@@ -58,7 +59,7 @@ export const ApiValueList = z.object({
|
|
|
58
59
|
type: z.string(),
|
|
59
60
|
values: z.array(z.object({
|
|
60
61
|
value: z.string(),
|
|
61
|
-
displayName: z.string()
|
|
62
|
+
displayName: z.string().optional()
|
|
62
63
|
}))
|
|
63
64
|
});
|
|
64
65
|
export const ApiLayoutMetadata = z.object({
|
|
@@ -66,6 +67,25 @@ export const ApiLayoutMetadata = z.object({
|
|
|
66
67
|
portalMetaData: z.record(z.string(), z.array(ApiFieldMetadata)),
|
|
67
68
|
valueLists: z.array(ApiValueList).optional()
|
|
68
69
|
});
|
|
70
|
+
export function isAccessibleFieldMetadata(field) {
|
|
71
|
+
return field.type !== 'invalid' && field.result !== 'invalid' && field.name !== '<No Access>';
|
|
72
|
+
}
|
|
73
|
+
/** Removes FileMaker metadata placeholders for fields unavailable to the current account. */
|
|
74
|
+
export function stripInaccessibleMetadata(metadata) {
|
|
75
|
+
return {
|
|
76
|
+
...metadata,
|
|
77
|
+
fieldMetaData: metadata.fieldMetaData.filter(isAccessibleFieldMetadata),
|
|
78
|
+
portalMetaData: Object.fromEntries(Object.entries(metadata.portalMetaData).map(([name, fields]) => [name, fields.filter(isAccessibleFieldMetadata)]))
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
/** Removes inaccessible placeholder values before strict generated validation. */
|
|
82
|
+
export function stripInaccessibleFieldData(fields) {
|
|
83
|
+
if (!Object.hasOwn(fields, '<No Access>'))
|
|
84
|
+
return fields;
|
|
85
|
+
const result = { ...fields };
|
|
86
|
+
delete result['<No Access>'];
|
|
87
|
+
return result;
|
|
88
|
+
}
|
|
69
89
|
export const ApiScriptResult = z.object({
|
|
70
90
|
scriptError: z.string(),
|
|
71
91
|
scriptResult: z.string().optional()
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { EventEmitter } from 'node:events';
|
|
5
5
|
import { RecordTypes } from '../types.js';
|
|
6
|
+
import { stripInaccessibleFieldData } from '../models/apiResults.js';
|
|
6
7
|
import { DateField, NumberField, TextField, TimeField, TimeStampField, ValueFieldBase } from "./fields/valueField.js";
|
|
7
8
|
import { ContainerField } from "./fields/containerField.js";
|
|
8
9
|
export class RecordBase extends EventEmitter {
|
|
@@ -41,7 +42,7 @@ export class RecordBase extends EventEmitter {
|
|
|
41
42
|
}
|
|
42
43
|
processFieldData(fieldData) {
|
|
43
44
|
const fields = {};
|
|
44
|
-
for (const [key, value] of Object.entries(fieldData)) {
|
|
45
|
+
for (const [key, value] of Object.entries(stripInaccessibleFieldData(fieldData))) {
|
|
45
46
|
const fieldMeta = this.getFieldMetadata(key);
|
|
46
47
|
let field;
|
|
47
48
|
switch (fieldMeta.result) {
|
package/dist/schema.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { stripInaccessibleFieldData, stripInaccessibleMetadata } from './models/apiResults.js';
|
|
1
2
|
/** Built-in runtime adapter used by Zod-generated clients. */
|
|
2
3
|
export const zodValidator = {
|
|
3
4
|
validate(schema, value) {
|
|
@@ -29,19 +30,28 @@ export function withSchemaValidation(provider, schema) {
|
|
|
29
30
|
return { formatting: connection.formatting, maxSessions: connection.maxSessions, close: connection.close?.bind(connection), async openSession(signal) {
|
|
30
31
|
const session = await connection.openSession(signal);
|
|
31
32
|
return { close: session.close.bind(session), fetchContainer: session.fetchContainer.bind(session), async execute(operation) {
|
|
32
|
-
|
|
33
|
-
if (operation.type === 'layout.metadata')
|
|
33
|
+
let result = await session.execute(operation);
|
|
34
|
+
if (operation.type === 'layout.metadata') {
|
|
35
|
+
result = stripInaccessibleMetadata(result);
|
|
34
36
|
validateMetadata(schema, operation.layout, result);
|
|
35
|
-
|
|
37
|
+
}
|
|
38
|
+
if (operation.type === 'record.get') {
|
|
39
|
+
result = stripRecord(result);
|
|
36
40
|
validateRecord(schema, operation.layout, result);
|
|
37
|
-
|
|
41
|
+
}
|
|
42
|
+
if (operation.type === 'record.list') {
|
|
43
|
+
result = result.map(stripRecord);
|
|
38
44
|
for (const record of result)
|
|
39
45
|
validateRecord(schema, operation.layout, record);
|
|
46
|
+
}
|
|
40
47
|
return result;
|
|
41
48
|
} };
|
|
42
49
|
} };
|
|
43
50
|
} };
|
|
44
51
|
}
|
|
52
|
+
function stripRecord(record) {
|
|
53
|
+
return { ...record, fieldData: stripInaccessibleFieldData(record.fieldData), portalData: record.portalData && Object.fromEntries(Object.entries(record.portalData).map(([name, rows]) => [name, rows.map(row => stripInaccessibleFieldData(row))])) };
|
|
54
|
+
}
|
|
45
55
|
function validateRecord(schema, layout, record) {
|
|
46
56
|
const layoutSchema = schema.layouts[layout];
|
|
47
57
|
if (!layoutSchema)
|
|
@@ -1,22 +1,26 @@
|
|
|
1
1
|
import { Database } from '../connection/database.js';
|
|
2
2
|
import { DataApiProvider } from '../connection/DataApiProvider.js';
|
|
3
|
+
import { ApiLayoutMetadata, isAccessibleFieldMetadata } from '../models/apiResults.js';
|
|
3
4
|
export function dataApiSchemaSource(options) {
|
|
4
5
|
return {
|
|
5
6
|
async introspect() {
|
|
6
7
|
const database = Database.create({ provider: new DataApiProvider(options) });
|
|
7
8
|
try {
|
|
8
9
|
const layouts = await database.listLayouts();
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
10
|
+
const inspected = await Promise.all(layouts.map(async (layout) => {
|
|
11
|
+
// Read raw metadata here so inaccessible placeholders can produce diagnostics before stripping.
|
|
12
|
+
const { value } = await database.execute({ type: 'layout.metadata', layout: layout.name });
|
|
13
|
+
const metadata = ApiLayoutMetadata.parse(value);
|
|
14
|
+
const diagnostics = metadata.fieldMetaData.filter(isInaccessible).map(field => ({ level: 'warning', layout: layout.name, message: `Field ${JSON.stringify(field.name)} is inaccessible and was excluded from generated types. Check this account's field privileges.` }));
|
|
15
|
+
for (const [portal, fields] of Object.entries(metadata.portalMetaData))
|
|
16
|
+
for (const field of fields.filter(isInaccessible))
|
|
17
|
+
diagnostics.push({ level: 'warning', layout: layout.name, portal, message: `Portal field ${JSON.stringify(field.name)} is inaccessible and was excluded from generated types. Check this account's field privileges.` });
|
|
18
|
+
return {
|
|
19
|
+
layout: { name: layout.name, fields: unique(metadata.fieldMetaData.filter(isAccessibleFieldMetadata).map(toField)), portals: Object.fromEntries(Object.entries(metadata.portalMetaData).map(([name, fields]) => [name, unique(fields.filter(isAccessibleFieldMetadata).map(toField))])) },
|
|
20
|
+
diagnostics
|
|
21
|
+
};
|
|
22
|
+
}));
|
|
23
|
+
return { version: 1, layouts: inspected.map(item => item.layout), diagnostics: inspected.flatMap(item => item.diagnostics) };
|
|
20
24
|
}
|
|
21
25
|
finally {
|
|
22
26
|
await database.close();
|
|
@@ -24,7 +28,10 @@ export function dataApiSchemaSource(options) {
|
|
|
24
28
|
}
|
|
25
29
|
};
|
|
26
30
|
}
|
|
31
|
+
function isInaccessible(field) { return field.type === 'invalid' || field.result === 'invalid' || field.name === '<No Access>'; }
|
|
27
32
|
function toField(field) {
|
|
33
|
+
if (!isAccessibleFieldMetadata(field))
|
|
34
|
+
throw new Error('Cannot generate an inaccessible field');
|
|
28
35
|
return { name: field.name, result: field.result, type: field.type, writable: field.type === 'normal', required: field.notEmpty };
|
|
29
36
|
}
|
|
30
37
|
function unique(fields) {
|
|
@@ -2,4 +2,8 @@ import type { TypegenConfig } from './types.js';
|
|
|
2
2
|
export declare function generate(config: TypegenConfig): Promise<{
|
|
3
3
|
output: string;
|
|
4
4
|
layouts: number;
|
|
5
|
+
databases: number;
|
|
6
|
+
diagnostics: Array<import('./types.js').TypegenDiagnostic & {
|
|
7
|
+
database: string;
|
|
8
|
+
}>;
|
|
5
9
|
}>;
|
package/dist/typegen/generate.js
CHANGED
|
@@ -2,84 +2,63 @@ import { mkdir, rename, rm, writeFile } from 'node:fs/promises';
|
|
|
2
2
|
import { dirname, resolve } from 'node:path';
|
|
3
3
|
import { zodTypegenValidator } from './zod.js';
|
|
4
4
|
export async function generate(config) {
|
|
5
|
-
const
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
5
|
+
const diagnostics = [];
|
|
6
|
+
const databases = await Promise.all(databaseEntries(config).map(async ([key, databaseConfig]) => {
|
|
7
|
+
const discovered = await databaseConfig.source.introspect();
|
|
8
|
+
for (const diagnostic of discovered.diagnostics ?? []) {
|
|
9
|
+
const item = { ...diagnostic, database: key };
|
|
10
|
+
diagnostics.push(item);
|
|
11
|
+
(config.onDiagnostic ?? defaultDiagnosticReporter)(item);
|
|
12
|
+
}
|
|
13
|
+
const selected = databaseConfig.layouts ? discovered.layouts.filter(layout => databaseConfig.layouts.includes(layout.name)) : discovered.layouts;
|
|
14
|
+
const missing = databaseConfig.layouts?.filter(name => !selected.some(layout => layout.name === name)) ?? [];
|
|
15
|
+
if (missing.length)
|
|
16
|
+
throw new Error(`Layouts not found in database ${key}: ${missing.join(', ')}`);
|
|
17
|
+
return { key, config: databaseConfig, schema: { version: 1, layouts: [...selected].sort((a, b) => a.name.localeCompare(b.name)) }, validator: databaseConfig.validator ?? config.validator ?? zodTypegenValidator() };
|
|
18
|
+
}));
|
|
11
19
|
const output = resolve(config.output);
|
|
12
20
|
const temporary = `${output}.tmp-${process.pid}`;
|
|
13
21
|
await rm(temporary, { recursive: true, force: true });
|
|
14
22
|
await mkdir(temporary, { recursive: true });
|
|
15
|
-
await writeFile(resolve(temporary, 'client.ts'), renderClient(
|
|
16
|
-
|
|
23
|
+
await writeFile(resolve(temporary, 'client.ts'), renderClient(databases), 'utf8');
|
|
24
|
+
const manifest = { version: 1, databases: Object.fromEntries(databases.map(db => [db.key, { ...db.schema, provider: db.config.provider, transports: db.config.transports ?? ['data-api', 'odata'] }])) };
|
|
25
|
+
await writeFile(resolve(temporary, 'schema.json'), `${JSON.stringify(manifest, null, 2)}\n`, 'utf8');
|
|
17
26
|
await mkdir(dirname(output), { recursive: true });
|
|
18
27
|
await rm(output, { recursive: true, force: true });
|
|
19
28
|
await rename(temporary, output);
|
|
20
|
-
return { output, layouts: schema.layouts.length };
|
|
29
|
+
return { output, databases: databases.length, layouts: databases.reduce((sum, db) => sum + db.schema.layouts.length, 0), diagnostics };
|
|
21
30
|
}
|
|
22
|
-
function
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
31
|
+
function defaultDiagnosticReporter(diagnostic) { console.warn(`[easyfm:typegen] warning [${diagnostic.database}${diagnostic.layout ? `/${diagnostic.layout}` : ''}${diagnostic.portal ? `/${diagnostic.portal}` : ''}]: ${diagnostic.message}`); }
|
|
32
|
+
function databaseEntries(config) {
|
|
33
|
+
if (config.databases && Object.keys(config.databases).length)
|
|
34
|
+
return Object.entries(config.databases);
|
|
35
|
+
if (!config.source)
|
|
36
|
+
throw new Error('Typegen config requires source or databases');
|
|
37
|
+
return [['database', { source: config.source, transports: config.transports, layouts: config.layouts, validator: config.validator }]];
|
|
38
|
+
}
|
|
39
|
+
function renderClient(databases) {
|
|
40
|
+
const imports = [...new Set(databases.flatMap(db => db.validator.imports))].join('\n');
|
|
41
|
+
const interfaces = databases.flatMap(db => db.schema.layouts.map(layout => renderLayout(db.key, layout))).join('\n\n');
|
|
42
|
+
const structures = databases.map(db => `export interface ${databaseType(db.key)} extends DatabaseStructure {\n layouts: {${db.schema.layouts.map(layout => `${quote(layout.name)}: ${layoutTypeName(db.key, layout)}`).join(',')}}\n}`).join('\n\n');
|
|
43
|
+
const schemas = databases.map(db => `${prop(db.key)}: {validator: ${db.validator.runtimeValidator}, layouts: {${db.schema.layouts.map(layout => `${quote(layout.name)}: {record: ${db.validator.renderRecord(layout)}, metadata: ${renderMetadata(layout)}}`).join(',')}}}`).join(',');
|
|
44
|
+
const factories = databases.map(db => `function create${typeId(db.key, 'Database')}Client (provider: DatabaseProvider) {\n const database = Database.create<${databaseType(db.key)}>({provider: withSchemaValidation(provider, easyFMSchemas.${prop(db.key)})})\n return {database, ${db.schema.layouts.map(layout => `${prop(layout.clientName ?? layout.name)}: database.layout<${layoutTypeName(db.key, layout)}>(${quote(layout.name)})`).join(',')}}\n}`).join('\n\n');
|
|
27
45
|
return `/* Generated by easyfm typegen. Do not edit. */
|
|
28
|
-
import {Database, withSchemaValidation,
|
|
29
|
-
${
|
|
30
|
-
|
|
46
|
+
import {Database, withSchemaValidation, zodValidator, type DatabaseProvider, type DatabaseStructure, type LayoutInterface, type Portal, type ReadonlyField, type TextField, type NumberField, type DateField, type TimeField, type TimeStampField, type ContainerField} from '@jd-data-limited/easy-fm'
|
|
47
|
+
${imports}
|
|
31
48
|
${interfaces}
|
|
32
|
-
|
|
33
|
-
export
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export const easyFMSchema = {
|
|
40
|
-
validator: ${validator.runtimeValidator},
|
|
41
|
-
layouts: {
|
|
42
|
-
${validators}
|
|
43
|
-
}
|
|
44
|
-
} satisfies Parameters<typeof withSchemaValidation>[1]
|
|
45
|
-
|
|
46
|
-
export function createEasyFMClient (provider: DatabaseProvider) {
|
|
47
|
-
const database = Database.create<GeneratedDatabase>({provider: withSchemaValidation(provider, easyFMSchema)})
|
|
48
|
-
return {
|
|
49
|
-
database,
|
|
50
|
-
${properties}
|
|
51
|
-
}
|
|
52
|
-
}
|
|
49
|
+
${structures}
|
|
50
|
+
export const easyFMSchemas = {${schemas}}
|
|
51
|
+
${factories}
|
|
52
|
+
export function createEasyFMClient (providers: {${databases.map(db => `${prop(db.key)}: DatabaseProvider`).join(',')}}) { return {${databases.map(db => `${prop(db.key)}: create${typeId(db.key, 'Database')}Client(providers.${prop(db.key)})`).join(',')}} }
|
|
53
53
|
`;
|
|
54
54
|
}
|
|
55
|
-
function renderLayout(layout) {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
}
|
|
65
|
-
}`;
|
|
66
|
-
}
|
|
67
|
-
function fieldType(field) {
|
|
68
|
-
const type = { text: 'TextField', number: 'NumberField', date: 'DateField', time: 'TimeField', timeStamp: 'TimeStampField', container: 'ContainerField' }[field.result];
|
|
69
|
-
return field.writable ? type : `ReadonlyField<${type}>`;
|
|
70
|
-
}
|
|
71
|
-
function renderMetadata(layout) {
|
|
72
|
-
const fields = renderMetadataFields(layout.fields);
|
|
73
|
-
const portals = Object.entries(layout.portals).map(([name, portalFields]) => `${quote(name)}: ${renderMetadataFields(portalFields)}`).join(', ');
|
|
74
|
-
return `{fields: ${fields}, portals: {${portals}}}`;
|
|
75
|
-
}
|
|
76
|
-
function renderMetadataFields(fields) {
|
|
77
|
-
return `{${fields.map(field => `${quote(field.name)}: {result: ${quote(field.result)}, type: ${quote(field.type)}}`).join(', ')}}`;
|
|
78
|
-
}
|
|
79
|
-
function layoutTypeName(layout) { return `${identifier(layout.clientName ?? layout.name, 'Layout')}Layout`; }
|
|
80
|
-
function safeProperty(value) { return identifier(value, 'layout'); }
|
|
81
|
-
function identifier(value, fallback) {
|
|
82
|
-
const cleaned = value.replace(/[^a-zA-Z0-9_$]/g, '_').replace(/^[^a-zA-Z_$]+/, '');
|
|
83
|
-
return cleaned || fallback;
|
|
84
|
-
}
|
|
55
|
+
function renderLayout(db, layout) { return `export interface ${layoutTypeName(db, layout)} extends LayoutInterface { fields: {${layout.fields.map(f => `${quote(f.name)}: ${fieldType(f)}`).join(',')}}; portals: {${Object.entries(layout.portals).map(([name, fields]) => `${quote(name)}: Portal<{${fields.map(f => `${quote(f.name)}: ${fieldType(f)}`).join(',')}}>`).join(',')}} }`; }
|
|
56
|
+
function fieldType(f) { const t = { text: 'TextField', number: 'NumberField', date: 'DateField', time: 'TimeField', timeStamp: 'TimeStampField', container: 'ContainerField' }[f.result]; return f.writable ? t : `ReadonlyField<${t}>`; }
|
|
57
|
+
function renderMetadata(layout) { return `{fields: ${metadataFields(layout.fields)}, portals: {${Object.entries(layout.portals).map(([name, fields]) => `${quote(name)}: ${metadataFields(fields)}`).join(',')}}}`; }
|
|
58
|
+
function metadataFields(fields) { return `{${fields.map(f => `${quote(f.name)}: {result: ${quote(f.result)}, type: ${quote(f.type)}}`).join(',')}}`; }
|
|
59
|
+
function databaseType(db) { return `${typeId(db, 'Database')}Database`; }
|
|
60
|
+
function layoutTypeName(db, layout) { return `${typeId(db, 'Database')}${typeId(layout.clientName ?? layout.name, 'Layout')}Layout`; }
|
|
61
|
+
function prop(value) { return id(value, 'item'); }
|
|
62
|
+
function typeId(value, fallback) { const valueId = id(value, fallback); return valueId[0].toUpperCase() + valueId.slice(1); }
|
|
63
|
+
function id(value, fallback) { const clean = value.replace(/[^a-zA-Z0-9_$]/g, '_').replace(/^[^a-zA-Z_$]+/, ''); return clean || fallback; }
|
|
85
64
|
function quote(value) { return JSON.stringify(value); }
|
package/dist/typegen/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { generate } from './generate.js';
|
|
2
2
|
export { dataApiSchemaSource } from './dataApiSchemaSource.js';
|
|
3
3
|
export { zodTypegenValidator } from './zod.js';
|
|
4
|
-
export { configExists, readEnvironment, writeDataApiInitialization, type DataApiInitAnswers } from './init.js';
|
|
4
|
+
export { configExists, readEnvironment, writeDataApiInitialization, writeMultiDatabaseInitialization, type DataApiInitAnswers, type MultiDatabaseInitAnswers } from './init.js';
|
|
5
5
|
export { ensureZodInstalled, getZodInstallPlan, type InstallPlan } from './dependencies.js';
|
|
6
|
-
export { defineTypegenConfig, type SchemaSource, type TypegenConfig, type TypegenField, type TypegenLayout, type TypegenSchema, type TypegenTransport, type TypegenValidator } from './types.js';
|
|
6
|
+
export { defineTypegenConfig, type SchemaSource, type TypegenConfig, type TypegenDatabaseConfig, type TypegenDiagnostic, type TypegenField, type TypegenLayout, type TypegenSchema, type TypegenTransport, type TypegenValidator } from './types.js';
|
package/dist/typegen/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { generate } from './generate.js';
|
|
2
2
|
export { dataApiSchemaSource } from './dataApiSchemaSource.js';
|
|
3
3
|
export { zodTypegenValidator } from './zod.js';
|
|
4
|
-
export { configExists, readEnvironment, writeDataApiInitialization } from './init.js';
|
|
4
|
+
export { configExists, readEnvironment, writeDataApiInitialization, writeMultiDatabaseInitialization } from './init.js';
|
|
5
5
|
export { ensureZodInstalled, getZodInstallPlan } from './dependencies.js';
|
|
6
6
|
export { defineTypegenConfig } from './types.js';
|
package/dist/typegen/init.d.ts
CHANGED
|
@@ -7,7 +7,14 @@ export interface DataApiInitAnswers {
|
|
|
7
7
|
password: string;
|
|
8
8
|
output: string;
|
|
9
9
|
}
|
|
10
|
+
export interface MultiDatabaseInitAnswers {
|
|
11
|
+
output: string;
|
|
12
|
+
databases: Array<DataApiInitAnswers & {
|
|
13
|
+
key: string;
|
|
14
|
+
}>;
|
|
15
|
+
}
|
|
10
16
|
export declare function configExists(path: string): Promise<boolean>;
|
|
11
17
|
/** Non-interactive write step, usable by alternate setup UIs. */
|
|
12
18
|
export declare function writeDataApiInitialization(config: string, answers: DataApiInitAnswers): Promise<void>;
|
|
19
|
+
export declare function writeMultiDatabaseInitialization(config: string, answers: MultiDatabaseInitAnswers): Promise<void>;
|
|
13
20
|
export declare function readEnvironment(path: string): Promise<Record<string, string>>;
|
package/dist/typegen/init.js
CHANGED
|
@@ -27,6 +27,25 @@ export async function writeDataApiInitialization(config, answers) {
|
|
|
27
27
|
await updateEnvironment(envPath, { FM_HOST: answers.hostname, FM_DATABASE: answers.database, FM_USERNAME: answers.username, FM_PASSWORD: answers.password });
|
|
28
28
|
await ensureEnvironmentIgnored(directory);
|
|
29
29
|
}
|
|
30
|
+
export async function writeMultiDatabaseInitialization(config, answers) {
|
|
31
|
+
const configPath = resolve(config);
|
|
32
|
+
const directory = dirname(configPath);
|
|
33
|
+
await mkdir(directory, { recursive: true });
|
|
34
|
+
const file = await open(configPath, 'wx');
|
|
35
|
+
try {
|
|
36
|
+
await file.writeFile(renderMultiConfig(answers), 'utf8');
|
|
37
|
+
}
|
|
38
|
+
finally {
|
|
39
|
+
await file.close();
|
|
40
|
+
}
|
|
41
|
+
const environment = {};
|
|
42
|
+
for (const db of answers.databases) {
|
|
43
|
+
const prefix = envPrefix(db.key);
|
|
44
|
+
Object.assign(environment, { [`${prefix}_HOST`]: db.hostname, [`${prefix}_DATABASE`]: db.database, [`${prefix}_USERNAME`]: db.username, [`${prefix}_PASSWORD`]: db.password });
|
|
45
|
+
}
|
|
46
|
+
await updateEnvironment(resolve(directory, '.env'), environment);
|
|
47
|
+
await ensureEnvironmentIgnored(directory);
|
|
48
|
+
}
|
|
30
49
|
export async function readEnvironment(path) {
|
|
31
50
|
try {
|
|
32
51
|
const contents = await readFile(path, 'utf8');
|
|
@@ -60,6 +79,15 @@ export default defineTypegenConfig({
|
|
|
60
79
|
})
|
|
61
80
|
`;
|
|
62
81
|
}
|
|
82
|
+
function renderMultiConfig(answers) {
|
|
83
|
+
const databases = answers.databases.map(db => {
|
|
84
|
+
const prefix = envPrefix(db.key);
|
|
85
|
+
const transports = db.transports === 'both' ? ['data-api', 'odata'] : [db.transports];
|
|
86
|
+
return `${JSON.stringify(db.key)}: {provider: 'data-api/odata', transports: ${JSON.stringify(transports)}, source: dataApiSchemaSource({hostname: required('${prefix}_HOST'), database: required('${prefix}_DATABASE'), credentials: {method: 'filemaker', username: required('${prefix}_USERNAME'), password: required('${prefix}_PASSWORD')}, externalSources: []})}`;
|
|
87
|
+
}).join(',\n ');
|
|
88
|
+
return `import {dataApiSchemaSource, defineTypegenConfig, zodTypegenValidator} from '@jd-data-limited/easy-fm/typegen'\n\nfunction required(name) { const value = process.env[name]; if (!value) throw new Error(\`Missing required environment variable \${name}\`); return value }\n\nexport default defineTypegenConfig({output: ${JSON.stringify(answers.output)}, validator: zodTypegenValidator(), databases: {\n ${databases}\n }})\n`;
|
|
89
|
+
}
|
|
90
|
+
function envPrefix(key) { return `FM_${key.replace(/[^a-zA-Z0-9]/g, '_').toUpperCase()}`; }
|
|
63
91
|
async function updateEnvironment(path, values) {
|
|
64
92
|
let contents = '';
|
|
65
93
|
try {
|
package/dist/typegen/types.d.ts
CHANGED
|
@@ -12,9 +12,16 @@ export interface TypegenLayout {
|
|
|
12
12
|
fields: TypegenField[];
|
|
13
13
|
portals: Record<string, TypegenField[]>;
|
|
14
14
|
}
|
|
15
|
+
export interface TypegenDiagnostic {
|
|
16
|
+
level: 'warning';
|
|
17
|
+
message: string;
|
|
18
|
+
layout?: string;
|
|
19
|
+
portal?: string;
|
|
20
|
+
}
|
|
15
21
|
export interface TypegenSchema {
|
|
16
22
|
version: 1;
|
|
17
23
|
layouts: TypegenLayout[];
|
|
24
|
+
diagnostics?: TypegenDiagnostic[];
|
|
18
25
|
}
|
|
19
26
|
export type TypegenTransport = 'data-api' | 'odata';
|
|
20
27
|
export interface SchemaSource {
|
|
@@ -28,13 +35,25 @@ export interface TypegenValidator {
|
|
|
28
35
|
renderRecord(layout: TypegenLayout): string;
|
|
29
36
|
}
|
|
30
37
|
export interface TypegenConfig {
|
|
31
|
-
source: SchemaSource;
|
|
32
38
|
output: string;
|
|
39
|
+
/** Single-database shorthand. */
|
|
40
|
+
source?: SchemaSource;
|
|
33
41
|
/** Placeholder describing intended runtime transports. */
|
|
34
42
|
transports?: TypegenTransport[];
|
|
35
43
|
/** Restrict output to these FileMaker layout names. */
|
|
36
44
|
layouts?: string[];
|
|
37
45
|
/** Defaults to the built-in strict Zod validator. */
|
|
38
46
|
validator?: TypegenValidator;
|
|
47
|
+
databases?: Record<string, TypegenDatabaseConfig>;
|
|
48
|
+
onDiagnostic?: (diagnostic: TypegenDiagnostic & {
|
|
49
|
+
database: string;
|
|
50
|
+
}) => void;
|
|
51
|
+
}
|
|
52
|
+
export interface TypegenDatabaseConfig {
|
|
53
|
+
source: SchemaSource;
|
|
54
|
+
provider?: string;
|
|
55
|
+
transports?: TypegenTransport[];
|
|
56
|
+
layouts?: string[];
|
|
57
|
+
validator?: TypegenValidator;
|
|
39
58
|
}
|
|
40
59
|
export declare function defineTypegenConfig(config: TypegenConfig): TypegenConfig;
|