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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +93 -90
  3. package/dist/bin/cli.js +34 -5
  4. package/dist/bin/generateClient.d.ts +1 -0
  5. package/dist/bin/generateClient.js +48 -0
  6. package/dist/bin/generateTypes.js +20 -19
  7. package/dist/bin/init.d.ts +6 -0
  8. package/dist/bin/init.js +46 -0
  9. package/dist/bin/stressSearch.js +12 -12
  10. package/dist/connection/DataApiProvider.d.ts +15 -0
  11. package/dist/connection/DataApiProvider.js +246 -0
  12. package/dist/connection/FMHost.d.ts +1 -3
  13. package/dist/connection/FMHost.js +1 -3
  14. package/dist/connection/HostBase.d.ts +0 -2
  15. package/dist/connection/dataApiDatabaseSession.d.ts +11 -0
  16. package/dist/connection/dataApiDatabaseSession.js +139 -0
  17. package/dist/connection/database.d.ts +22 -56
  18. package/dist/connection/database.js +175 -111
  19. package/dist/connection/databaseBase.d.ts +9 -22
  20. package/dist/connection/databaseBase.js +0 -3
  21. package/dist/connection/databaseProvider.d.ts +196 -0
  22. package/dist/connection/databaseProvider.js +6 -0
  23. package/dist/connection/databaseSessionPool.d.ts +8 -7
  24. package/dist/connection/databaseSessionPool.js +110 -106
  25. package/dist/index.d.ts +11 -7
  26. package/dist/index.js +7 -5
  27. package/dist/layouts/layout.d.ts +0 -2
  28. package/dist/layouts/layout.js +6 -16
  29. package/dist/layouts/layoutBase.d.ts +0 -1
  30. package/dist/layouts/layoutInterface.d.ts +4 -1
  31. package/dist/models/apiResults.d.ts +7 -6
  32. package/dist/models/apiResults.js +1 -1
  33. package/dist/records/fields/baseField.d.ts +60 -0
  34. package/dist/records/fields/baseField.js +59 -0
  35. package/dist/records/fields/containerField.d.ts +18 -0
  36. package/dist/records/fields/containerField.js +44 -0
  37. package/dist/records/fields/field.d.ts +3 -0
  38. package/dist/records/fields/field.js +1 -0
  39. package/dist/records/fields/valueField.d.ts +41 -0
  40. package/dist/records/fields/valueField.js +108 -0
  41. package/dist/records/getOperations/recordGetOperation.js +24 -24
  42. package/dist/records/layoutRecord.d.ts +15 -8
  43. package/dist/records/layoutRecord.js +61 -98
  44. package/dist/records/portal.d.ts +1 -1
  45. package/dist/records/portalBase.d.ts +1 -2
  46. package/dist/records/portalRecord.d.ts +11 -5
  47. package/dist/records/portalRecord.js +24 -2
  48. package/dist/records/recordBase.d.ts +11 -7
  49. package/dist/records/recordBase.js +38 -38
  50. package/dist/schema.d.ts +54 -0
  51. package/dist/schema.js +87 -0
  52. package/dist/typegen/dataApiSchemaSource.d.ts +3 -0
  53. package/dist/typegen/dataApiSchemaSource.js +32 -0
  54. package/dist/typegen/dependencies.d.ts +11 -0
  55. package/dist/typegen/dependencies.js +67 -0
  56. package/dist/typegen/generate.d.ts +5 -0
  57. package/dist/typegen/generate.js +85 -0
  58. package/dist/typegen/index.d.ts +6 -0
  59. package/dist/typegen/index.js +6 -0
  60. package/dist/typegen/init.d.ts +13 -0
  61. package/dist/typegen/init.js +113 -0
  62. package/dist/typegen/types.d.ts +40 -0
  63. package/dist/typegen/types.js +1 -0
  64. package/dist/typegen/zod.d.ts +3 -0
  65. package/dist/typegen/zod.js +32 -0
  66. package/dist/types.d.ts +1 -1
  67. package/dist/utils/query.d.ts +21 -12
  68. package/dist/utils/query.js +58 -23
  69. package/dist/utils/temporal.d.ts +9 -0
  70. package/dist/utils/temporal.js +110 -0
  71. package/package.json +47 -24
  72. package/dist/layouts/recordFieldsMap.d.ts +0 -2
  73. package/dist/layouts/recordFieldsMap.js +0 -4
  74. package/dist/records/field.d.ts +0 -77
  75. package/dist/records/field.js +0 -192
@@ -1,10 +1,12 @@
1
- import { EventEmitter } from 'events';
1
+ import { EventEmitter } from 'node:events';
2
2
  import { RecordTypes } from '../types.js';
3
- import { type RecordFieldsMap } from '../layouts/recordFieldsMap.js';
4
3
  import { type LayoutBase } from '../layouts/layoutBase.js';
5
- import { type ApiFieldData } from '../models/apiResults.js';
6
- import { Field, type FieldValue } from './field.js';
4
+ import { type ApiFieldData, type ApiFieldMetadata } from '../models/apiResults.js';
7
5
  import { type z } from 'zod';
6
+ import { RecordFieldsMap } from "../layouts/layoutInterface.js";
7
+ import { Field } from "./fields/field.js";
8
+ import { ContainerField } from "./fields/containerField.js";
9
+ import { type SessionBinding } from '../connection/databaseProvider.js';
8
10
  export declare abstract class RecordBase<T extends RecordFieldsMap> extends EventEmitter {
9
11
  readonly layout: LayoutBase;
10
12
  readonly type: RecordTypes;
@@ -17,15 +19,17 @@ 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
  *
25
27
  * @returns {boolean} A boolean value indicating whether any of the fields have been edited.
26
28
  */
27
29
  get edited(): boolean;
28
- get fieldsArray(): Array<Field<FieldValue>>;
30
+ get fieldsArray(): Array<Field>;
31
+ abstract getFieldMetadata(fieldId: string): z.infer<typeof ApiFieldMetadata>;
32
+ abstract refreshContainer(fieldId: string): Promise<ContainerField>;
29
33
  protected processFieldData(fieldData: z.infer<typeof ApiFieldData>): T;
30
34
  _onSave(): void;
31
35
  }
@@ -1,10 +1,10 @@
1
1
  /*
2
2
  * Copyright (c) 2023-2024. See LICENSE file for more information
3
3
  */
4
- import { EventEmitter } from 'events';
5
- import * as moment from 'moment';
4
+ import { EventEmitter } from 'node:events';
6
5
  import { RecordTypes } from '../types.js';
7
- import { Field } from './field.js';
6
+ import { DateField, NumberField, TextField, TimeField, TimeStampField, ValueFieldBase } from "./fields/valueField.js";
7
+ import { ContainerField } from "./fields/containerField.js";
8
8
  export class RecordBase extends EventEmitter {
9
9
  layout;
10
10
  type = RecordTypes.UNKNOWN;
@@ -17,65 +17,65 @@ 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
  *
33
32
  * @returns {boolean} A boolean value indicating whether any of the fields have been edited.
34
33
  */
35
34
  get edited() {
36
- return !!this.fieldsArray.find(i => i.edited);
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) {
42
43
  const fields = {};
43
- for (const key of Object.keys(fieldData)) {
44
- const _field = new Field(this, key, fieldData[key]);
45
- if (fieldData[key]) {
46
- if (_field.metadata.result === 'timeStamp') {
47
- let date = moment.default(fieldData[key]);
48
- date = date
49
- .utcOffset(this.layout.database.host.timezoneOffsetFunc(date), true)
50
- .local();
51
- _field.set(date);
52
- _field.updateOriginalContents();
53
- }
54
- else if (_field.metadata.result === 'time') {
55
- let date = moment.default(fieldData[key]);
56
- date = date
57
- .utcOffset(this.layout.database.host.timezoneOffsetFunc(date), true)
58
- .local();
59
- _field.set(date);
60
- _field.updateOriginalContents();
61
- }
62
- else if (_field.metadata.result === 'date') {
63
- let date = moment.default(fieldData[key]);
64
- date = date
65
- .utcOffset(this.layout.database.host.timezoneOffsetFunc(date), true)
66
- .local();
67
- _field.set(date);
68
- _field.updateOriginalContents();
69
- }
44
+ for (const [key, value] of Object.entries(fieldData)) {
45
+ const fieldMeta = this.getFieldMetadata(key);
46
+ let field;
47
+ switch (fieldMeta.result) {
48
+ case "text":
49
+ field = new TextField(this, key, value);
50
+ break;
51
+ case "number":
52
+ field = new NumberField(this, key, value);
53
+ break;
54
+ case "container":
55
+ field = new ContainerField(this, key, value);
56
+ break;
57
+ case "timeStamp":
58
+ field = new TimeStampField(this, key, value);
59
+ break;
60
+ case "time":
61
+ field = new TimeField(this, key, value);
62
+ break;
63
+ case "date":
64
+ field = new DateField(this, key, value);
65
+ break;
66
+ default:
67
+ throw new Error(`Attempted to parse unknown field type: ${fieldMeta.result}`);
70
68
  }
71
- fields[key] = _field;
69
+ fields[key] = field;
72
70
  }
73
71
  this.fields = fields;
74
72
  return fields;
75
73
  }
76
74
  _onSave() {
77
75
  this.emit('saved');
78
- for (const field of this.fieldsArray)
79
- field.updateOriginalContents();
76
+ for (const field of this.fieldsArray) {
77
+ if (field instanceof ValueFieldBase)
78
+ field.updateOriginalContents();
79
+ }
80
80
  }
81
81
  }
@@ -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 {};
@@ -0,0 +1,67 @@
1
+ import { access, readFile } from 'node:fs/promises';
2
+ import { dirname, parse, resolve } from 'node:path';
3
+ import { spawn } from 'node:child_process';
4
+ const ZOD_VERSION = '^4.6.5';
5
+ /** Ensures generated code can resolve its direct `zod` import. */
6
+ export async function ensureZodInstalled(start) {
7
+ const plan = await getZodInstallPlan(start);
8
+ if (!plan)
9
+ return false;
10
+ await runInstall(plan);
11
+ return true;
12
+ }
13
+ /** Returns null when Zod is already declared. Exported for alternate CLIs. */
14
+ export async function getZodInstallPlan(start) {
15
+ const packagePath = await findUp(resolve(start), 'package.json');
16
+ if (!packagePath)
17
+ throw new Error(`Cannot install Zod: no package.json found above ${resolve(start)}`);
18
+ const manifest = JSON.parse(await readFile(packagePath, 'utf8'));
19
+ const dependencyGroups = ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies'];
20
+ if (dependencyGroups.some(group => hasDependency(manifest[group], 'zod')))
21
+ return null;
22
+ const cwd = dirname(packagePath);
23
+ const manager = await detectPackageManager(cwd, manifest.packageManager);
24
+ return { cwd, command: manager, args: installArguments(manager) };
25
+ }
26
+ function hasDependency(value, name) {
27
+ return typeof value === 'object' && value !== null && name in value;
28
+ }
29
+ async function detectPackageManager(cwd, declared) {
30
+ if (typeof declared === 'string') {
31
+ const name = declared.split('@')[0];
32
+ if (name === 'pnpm' || name === 'npm' || name === 'yarn' || name === 'bun')
33
+ return name;
34
+ }
35
+ const locks = [['pnpm-lock.yaml', 'pnpm'], ['yarn.lock', 'yarn'], ['bun.lock', 'bun'], ['bun.lockb', 'bun'], ['package-lock.json', 'npm']];
36
+ for (const [file, manager] of locks)
37
+ if (await findUp(cwd, file))
38
+ return manager;
39
+ return 'npm';
40
+ }
41
+ function installArguments(manager) {
42
+ if (manager === 'npm')
43
+ return ['install', `zod@${ZOD_VERSION}`];
44
+ return ['add', `zod@${ZOD_VERSION}`];
45
+ }
46
+ async function runInstall({ cwd, command, args }) {
47
+ await new Promise((resolvePromise, reject) => {
48
+ const child = spawn(command, args, { cwd, stdio: 'inherit', shell: process.platform === 'win32' });
49
+ child.once('error', reject);
50
+ child.once('exit', code => code === 0 ? resolvePromise() : reject(new Error(`${command} ${args.join(' ')} exited with code ${code ?? 'unknown'}`)));
51
+ });
52
+ }
53
+ async function findUp(start, name) {
54
+ let directory = start;
55
+ while (true) {
56
+ const candidate = resolve(directory, name);
57
+ try {
58
+ await access(candidate);
59
+ return candidate;
60
+ }
61
+ catch { }
62
+ const parent = dirname(directory);
63
+ if (parent === directory || directory === parse(directory).root)
64
+ return undefined;
65
+ directory = parent;
66
+ }
67
+ }
@@ -0,0 +1,5 @@
1
+ import type { TypegenConfig } from './types.js';
2
+ export declare function generate(config: TypegenConfig): Promise<{
3
+ output: string;
4
+ layouts: number;
5
+ }>;
@@ -0,0 +1,85 @@
1
+ import { mkdir, rename, rm, writeFile } from 'node:fs/promises';
2
+ import { dirname, resolve } from 'node:path';
3
+ import { zodTypegenValidator } from './zod.js';
4
+ export async function generate(config) {
5
+ const discovered = await config.source.introspect();
6
+ const selected = config.layouts ? discovered.layouts.filter(layout => config.layouts.includes(layout.name)) : discovered.layouts;
7
+ const missing = config.layouts?.filter(name => !selected.some(layout => layout.name === name)) ?? [];
8
+ if (missing.length)
9
+ throw new Error(`Layouts not found: ${missing.join(', ')}`);
10
+ const schema = { version: 1, layouts: [...selected].sort((a, b) => a.name.localeCompare(b.name)) };
11
+ const output = resolve(config.output);
12
+ const temporary = `${output}.tmp-${process.pid}`;
13
+ await rm(temporary, { recursive: true, force: true });
14
+ await mkdir(temporary, { recursive: true });
15
+ await writeFile(resolve(temporary, 'client.ts'), renderClient(schema, config.validator ?? zodTypegenValidator()), 'utf8');
16
+ await writeFile(resolve(temporary, 'schema.json'), `${JSON.stringify({ ...schema, transports: config.transports ?? ['data-api', 'odata'] }, null, 2)}\n`, 'utf8');
17
+ await mkdir(dirname(output), { recursive: true });
18
+ await rm(output, { recursive: true, force: true });
19
+ await rename(temporary, output);
20
+ return { output, layouts: schema.layouts.length };
21
+ }
22
+ function renderClient(schema, validator) {
23
+ const interfaces = schema.layouts.map(renderLayout).join('\n\n');
24
+ const structure = schema.layouts.map(layout => `${quote(layout.name)}: ${layoutTypeName(layout)}`).join(',\n ');
25
+ const validators = schema.layouts.map(layout => `${quote(layout.name)}: {\n record: ${validator.renderRecord(layout)},\n metadata: ${renderMetadata(layout)}\n }`).join(',\n ');
26
+ const properties = schema.layouts.map(layout => `${safeProperty(layout.clientName ?? layout.name)}: database.layout<${layoutTypeName(layout)}>(${quote(layout.name)})`).join(',\n ');
27
+ return `/* Generated by easyfm typegen. Do not edit. */
28
+ import {Database, withSchemaValidation, ${validator.runtimeValidator}, 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'
29
+ ${validator.imports.join('\n')}
30
+
31
+ ${interfaces}
32
+
33
+ export interface GeneratedDatabase extends DatabaseStructure {
34
+ layouts: {
35
+ ${structure}
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
+ }
53
+ `;
54
+ }
55
+ function renderLayout(layout) {
56
+ const fields = layout.fields.map(field => `${quote(field.name)}: ${fieldType(field)}`).join('\n ');
57
+ const portals = Object.entries(layout.portals).map(([name, portalFields]) => `${quote(name)}: Portal<{\n ${portalFields.map(field => `${quote(field.name)}: ${fieldType(field)}`).join('\n ')}\n }>`).join('\n ');
58
+ return `export interface ${layoutTypeName(layout)} extends LayoutInterface {
59
+ fields: {
60
+ ${fields}
61
+ }
62
+ portals: {
63
+ ${portals}
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
+ }
85
+ function quote(value) { return JSON.stringify(value); }
@@ -0,0 +1,6 @@
1
+ export { generate } from './generate.js';
2
+ export { dataApiSchemaSource } from './dataApiSchemaSource.js';
3
+ export { zodTypegenValidator } from './zod.js';
4
+ export { configExists, readEnvironment, writeDataApiInitialization, type DataApiInitAnswers } from './init.js';
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';
@@ -0,0 +1,6 @@
1
+ export { generate } from './generate.js';
2
+ export { dataApiSchemaSource } from './dataApiSchemaSource.js';
3
+ export { zodTypegenValidator } from './zod.js';
4
+ export { configExists, readEnvironment, writeDataApiInitialization } from './init.js';
5
+ export { ensureZodInstalled, getZodInstallPlan } from './dependencies.js';
6
+ export { defineTypegenConfig } from './types.js';
@@ -0,0 +1,13 @@
1
+ export interface DataApiInitAnswers {
2
+ provider: 'data-api/odata';
3
+ transports: 'data-api' | 'odata' | 'both';
4
+ hostname: string;
5
+ database: string;
6
+ username: string;
7
+ password: string;
8
+ output: string;
9
+ }
10
+ export declare function configExists(path: string): Promise<boolean>;
11
+ /** Non-interactive write step, usable by alternate setup UIs. */
12
+ export declare function writeDataApiInitialization(config: string, answers: DataApiInitAnswers): Promise<void>;
13
+ export declare function readEnvironment(path: string): Promise<Record<string, string>>;
@@ -0,0 +1,113 @@
1
+ import { mkdir, open, readFile, writeFile } from 'node:fs/promises';
2
+ import { dirname, resolve } from 'node:path';
3
+ export async function configExists(path) {
4
+ try {
5
+ await readFile(path);
6
+ return true;
7
+ }
8
+ catch (error) {
9
+ if (error.code === 'ENOENT')
10
+ return false;
11
+ throw error;
12
+ }
13
+ }
14
+ /** Non-interactive write step, usable by alternate setup UIs. */
15
+ export async function writeDataApiInitialization(config, answers) {
16
+ const configPath = resolve(config);
17
+ const directory = dirname(configPath);
18
+ const envPath = resolve(directory, '.env');
19
+ await mkdir(directory, { recursive: true });
20
+ const file = await open(configPath, 'wx');
21
+ try {
22
+ await file.writeFile(renderConfig(answers), 'utf8');
23
+ }
24
+ finally {
25
+ await file.close();
26
+ }
27
+ await updateEnvironment(envPath, { FM_HOST: answers.hostname, FM_DATABASE: answers.database, FM_USERNAME: answers.username, FM_PASSWORD: answers.password });
28
+ await ensureEnvironmentIgnored(directory);
29
+ }
30
+ export async function readEnvironment(path) {
31
+ try {
32
+ const contents = await readFile(path, 'utf8');
33
+ return Object.fromEntries(contents.split(/\r?\n/).map(line => line.match(/^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/)).filter(match => match).map(match => [match[1], decodeEnvironmentValue(match[2])]));
34
+ }
35
+ catch (error) {
36
+ if (error.code === 'ENOENT')
37
+ return {};
38
+ throw error;
39
+ }
40
+ }
41
+ function renderConfig(answers) {
42
+ return `import {dataApiSchemaSource, defineTypegenConfig, zodTypegenValidator} from '@jd-data-limited/easy-fm/typegen'
43
+
44
+ function required (name) {
45
+ const value = process.env[name]
46
+ if (!value) throw new Error(\`Missing required environment variable \${name}\`)
47
+ return value
48
+ }
49
+
50
+ export default defineTypegenConfig({
51
+ output: ${JSON.stringify(answers.output)},
52
+ transports: ${JSON.stringify(answers.transports === 'both' ? ['data-api', 'odata'] : [answers.transports])},
53
+ validator: zodTypegenValidator(),
54
+ source: dataApiSchemaSource({
55
+ hostname: required('FM_HOST'),
56
+ database: required('FM_DATABASE'),
57
+ credentials: {method: 'filemaker', username: required('FM_USERNAME'), password: required('FM_PASSWORD')},
58
+ externalSources: []
59
+ })
60
+ })
61
+ `;
62
+ }
63
+ async function updateEnvironment(path, values) {
64
+ let contents = '';
65
+ try {
66
+ contents = await readFile(path, 'utf8');
67
+ }
68
+ catch (error) {
69
+ if (error.code !== 'ENOENT')
70
+ throw error;
71
+ }
72
+ const lines = contents ? contents.split(/\r?\n/) : [];
73
+ for (const [name, value] of Object.entries(values)) {
74
+ const replacement = `${name}=${JSON.stringify(value)}`;
75
+ const index = lines.findIndex(line => line.startsWith(`${name}=`));
76
+ if (index >= 0)
77
+ lines[index] = replacement;
78
+ else
79
+ lines.push(replacement);
80
+ }
81
+ await writeFile(path, `${lines.filter((line, index) => line || index < lines.length - 1).join('\n')}\n`, { encoding: 'utf8', mode: 0o600 });
82
+ }
83
+ function decodeEnvironmentValue(value) {
84
+ const trimmed = value.trim();
85
+ if (trimmed.startsWith('"') && trimmed.endsWith('"')) {
86
+ try {
87
+ return JSON.parse(trimmed);
88
+ }
89
+ catch {
90
+ return trimmed.slice(1, -1);
91
+ }
92
+ }
93
+ return trimmed;
94
+ }
95
+ async function ensureEnvironmentIgnored(directory) {
96
+ const path = resolve(directory, '.gitignore');
97
+ let contents = '';
98
+ try {
99
+ contents = await readFile(path, 'utf8');
100
+ }
101
+ catch (error) {
102
+ if (error.code !== 'ENOENT')
103
+ throw error;
104
+ }
105
+ const alreadyIgnored = contents.split(/\r?\n/).some(line => {
106
+ const rule = line.trim();
107
+ return rule === '.env' || rule === '/.env' || rule === '.env*' || rule === '/.env*';
108
+ });
109
+ if (alreadyIgnored)
110
+ return;
111
+ const prefix = contents && !contents.endsWith('\n') ? '\n' : '';
112
+ await writeFile(path, `${contents}${prefix}.env\n`, 'utf8');
113
+ }