@jd-data-limited/easy-fm 5.0.0-beta.5 → 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 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);
@@ -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
- const result = await session.execute(operation);
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
- if (operation.type === 'record.get')
37
+ }
38
+ if (operation.type === 'record.get') {
39
+ result = stripRecord(result);
36
40
  validateRecord(schema, operation.layout, result);
37
- if (operation.type === 'record.list')
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
- 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
- };
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) {
@@ -3,4 +3,7 @@ export declare function generate(config: TypegenConfig): Promise<{
3
3
  output: string;
4
4
  layouts: number;
5
5
  databases: number;
6
+ diagnostics: Array<import('./types.js').TypegenDiagnostic & {
7
+ database: string;
8
+ }>;
6
9
  }>;
@@ -2,8 +2,14 @@ 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 diagnostics = [];
5
6
  const databases = await Promise.all(databaseEntries(config).map(async ([key, databaseConfig]) => {
6
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
+ }
7
13
  const selected = databaseConfig.layouts ? discovered.layouts.filter(layout => databaseConfig.layouts.includes(layout.name)) : discovered.layouts;
8
14
  const missing = databaseConfig.layouts?.filter(name => !selected.some(layout => layout.name === name)) ?? [];
9
15
  if (missing.length)
@@ -20,8 +26,9 @@ export async function generate(config) {
20
26
  await mkdir(dirname(output), { recursive: true });
21
27
  await rm(output, { recursive: true, force: true });
22
28
  await rename(temporary, output);
23
- return { output, databases: databases.length, layouts: databases.reduce((sum, db) => sum + db.schema.layouts.length, 0) };
29
+ return { output, databases: databases.length, layouts: databases.reduce((sum, db) => sum + db.schema.layouts.length, 0), diagnostics };
24
30
  }
31
+ function defaultDiagnosticReporter(diagnostic) { console.warn(`[easyfm:typegen] warning [${diagnostic.database}${diagnostic.layout ? `/${diagnostic.layout}` : ''}${diagnostic.portal ? `/${diagnostic.portal}` : ''}]: ${diagnostic.message}`); }
25
32
  function databaseEntries(config) {
26
33
  if (config.databases && Object.keys(config.databases).length)
27
34
  return Object.entries(config.databases);
@@ -3,4 +3,4 @@ export { dataApiSchemaSource } from './dataApiSchemaSource.js';
3
3
  export { zodTypegenValidator } from './zod.js';
4
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 TypegenDatabaseConfig, 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';
@@ -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 {
@@ -38,6 +45,9 @@ export interface TypegenConfig {
38
45
  /** Defaults to the built-in strict Zod validator. */
39
46
  validator?: TypegenValidator;
40
47
  databases?: Record<string, TypegenDatabaseConfig>;
48
+ onDiagnostic?: (diagnostic: TypegenDiagnostic & {
49
+ database: string;
50
+ }) => void;
41
51
  }
42
52
  export interface TypegenDatabaseConfig {
43
53
  source: SchemaSource;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jd-data-limited/easy-fm",
3
- "version": "5.0.0-beta.5",
3
+ "version": "5.0.0-beta.7",
4
4
  "dependencies": {
5
5
  "chalk": "^5.6.2",
6
6
  "commander": "^15.0.0",