@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
|
@@ -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,57 @@
|
|
|
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 databases = await Promise.all(databaseEntries(config).map(async ([key, databaseConfig]) => {
|
|
6
|
+
const discovered = await databaseConfig.source.introspect();
|
|
7
|
+
const selected = databaseConfig.layouts ? discovered.layouts.filter(layout => databaseConfig.layouts.includes(layout.name)) : discovered.layouts;
|
|
8
|
+
const missing = databaseConfig.layouts?.filter(name => !selected.some(layout => layout.name === name)) ?? [];
|
|
9
|
+
if (missing.length)
|
|
10
|
+
throw new Error(`Layouts not found in database ${key}: ${missing.join(', ')}`);
|
|
11
|
+
return { key, config: databaseConfig, schema: { version: 1, layouts: [...selected].sort((a, b) => a.name.localeCompare(b.name)) }, validator: databaseConfig.validator ?? config.validator ?? zodTypegenValidator() };
|
|
12
|
+
}));
|
|
13
|
+
const output = resolve(config.output);
|
|
14
|
+
const temporary = `${output}.tmp-${process.pid}`;
|
|
15
|
+
await rm(temporary, { recursive: true, force: true });
|
|
16
|
+
await mkdir(temporary, { recursive: true });
|
|
17
|
+
await writeFile(resolve(temporary, 'client.ts'), renderClient(databases), 'utf8');
|
|
18
|
+
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'] }])) };
|
|
19
|
+
await writeFile(resolve(temporary, 'schema.json'), `${JSON.stringify(manifest, null, 2)}\n`, 'utf8');
|
|
20
|
+
await mkdir(dirname(output), { recursive: true });
|
|
21
|
+
await rm(output, { recursive: true, force: true });
|
|
22
|
+
await rename(temporary, output);
|
|
23
|
+
return { output, databases: databases.length, layouts: databases.reduce((sum, db) => sum + db.schema.layouts.length, 0) };
|
|
24
|
+
}
|
|
25
|
+
function databaseEntries(config) {
|
|
26
|
+
if (config.databases && Object.keys(config.databases).length)
|
|
27
|
+
return Object.entries(config.databases);
|
|
28
|
+
if (!config.source)
|
|
29
|
+
throw new Error('Typegen config requires source or databases');
|
|
30
|
+
return [['database', { source: config.source, transports: config.transports, layouts: config.layouts, validator: config.validator }]];
|
|
31
|
+
}
|
|
32
|
+
function renderClient(databases) {
|
|
33
|
+
const imports = [...new Set(databases.flatMap(db => db.validator.imports))].join('\n');
|
|
34
|
+
const interfaces = databases.flatMap(db => db.schema.layouts.map(layout => renderLayout(db.key, layout))).join('\n\n');
|
|
35
|
+
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');
|
|
36
|
+
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(',');
|
|
37
|
+
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');
|
|
38
|
+
return `/* Generated by easyfm typegen. Do not edit. */
|
|
39
|
+
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'
|
|
40
|
+
${imports}
|
|
41
|
+
${interfaces}
|
|
42
|
+
${structures}
|
|
43
|
+
export const easyFMSchemas = {${schemas}}
|
|
44
|
+
${factories}
|
|
45
|
+
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(',')}} }
|
|
46
|
+
`;
|
|
47
|
+
}
|
|
48
|
+
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(',')}} }`; }
|
|
49
|
+
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}>`; }
|
|
50
|
+
function renderMetadata(layout) { return `{fields: ${metadataFields(layout.fields)}, portals: {${Object.entries(layout.portals).map(([name, fields]) => `${quote(name)}: ${metadataFields(fields)}`).join(',')}}}`; }
|
|
51
|
+
function metadataFields(fields) { return `{${fields.map(f => `${quote(f.name)}: {result: ${quote(f.result)}, type: ${quote(f.type)}}`).join(',')}}`; }
|
|
52
|
+
function databaseType(db) { return `${typeId(db, 'Database')}Database`; }
|
|
53
|
+
function layoutTypeName(db, layout) { return `${typeId(db, 'Database')}${typeId(layout.clientName ?? layout.name, 'Layout')}Layout`; }
|
|
54
|
+
function prop(value) { return id(value, 'item'); }
|
|
55
|
+
function typeId(value, fallback) { const valueId = id(value, fallback); return valueId[0].toUpperCase() + valueId.slice(1); }
|
|
56
|
+
function id(value, fallback) { const clean = value.replace(/[^a-zA-Z0-9_$]/g, '_').replace(/^[^a-zA-Z_$]+/, ''); return clean || fallback; }
|
|
57
|
+
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, writeMultiDatabaseInitialization, type DataApiInitAnswers, type MultiDatabaseInitAnswers } from './init.js';
|
|
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';
|
|
@@ -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, writeMultiDatabaseInitialization } from './init.js';
|
|
5
|
+
export { ensureZodInstalled, getZodInstallPlan } from './dependencies.js';
|
|
6
|
+
export { defineTypegenConfig } from './types.js';
|
|
@@ -0,0 +1,20 @@
|
|
|
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 interface MultiDatabaseInitAnswers {
|
|
11
|
+
output: string;
|
|
12
|
+
databases: Array<DataApiInitAnswers & {
|
|
13
|
+
key: string;
|
|
14
|
+
}>;
|
|
15
|
+
}
|
|
16
|
+
export declare function configExists(path: string): Promise<boolean>;
|
|
17
|
+
/** Non-interactive write step, usable by alternate setup UIs. */
|
|
18
|
+
export declare function writeDataApiInitialization(config: string, answers: DataApiInitAnswers): Promise<void>;
|
|
19
|
+
export declare function writeMultiDatabaseInitialization(config: string, answers: MultiDatabaseInitAnswers): Promise<void>;
|
|
20
|
+
export declare function readEnvironment(path: string): Promise<Record<string, string>>;
|
|
@@ -0,0 +1,141 @@
|
|
|
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 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
|
+
}
|
|
49
|
+
export async function readEnvironment(path) {
|
|
50
|
+
try {
|
|
51
|
+
const contents = await readFile(path, 'utf8');
|
|
52
|
+
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])]));
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
if (error.code === 'ENOENT')
|
|
56
|
+
return {};
|
|
57
|
+
throw error;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
function renderConfig(answers) {
|
|
61
|
+
return `import {dataApiSchemaSource, defineTypegenConfig, zodTypegenValidator} from '@jd-data-limited/easy-fm/typegen'
|
|
62
|
+
|
|
63
|
+
function required (name) {
|
|
64
|
+
const value = process.env[name]
|
|
65
|
+
if (!value) throw new Error(\`Missing required environment variable \${name}\`)
|
|
66
|
+
return value
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export default defineTypegenConfig({
|
|
70
|
+
output: ${JSON.stringify(answers.output)},
|
|
71
|
+
transports: ${JSON.stringify(answers.transports === 'both' ? ['data-api', 'odata'] : [answers.transports])},
|
|
72
|
+
validator: zodTypegenValidator(),
|
|
73
|
+
source: dataApiSchemaSource({
|
|
74
|
+
hostname: required('FM_HOST'),
|
|
75
|
+
database: required('FM_DATABASE'),
|
|
76
|
+
credentials: {method: 'filemaker', username: required('FM_USERNAME'), password: required('FM_PASSWORD')},
|
|
77
|
+
externalSources: []
|
|
78
|
+
})
|
|
79
|
+
})
|
|
80
|
+
`;
|
|
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()}`; }
|
|
91
|
+
async function updateEnvironment(path, values) {
|
|
92
|
+
let contents = '';
|
|
93
|
+
try {
|
|
94
|
+
contents = await readFile(path, 'utf8');
|
|
95
|
+
}
|
|
96
|
+
catch (error) {
|
|
97
|
+
if (error.code !== 'ENOENT')
|
|
98
|
+
throw error;
|
|
99
|
+
}
|
|
100
|
+
const lines = contents ? contents.split(/\r?\n/) : [];
|
|
101
|
+
for (const [name, value] of Object.entries(values)) {
|
|
102
|
+
const replacement = `${name}=${JSON.stringify(value)}`;
|
|
103
|
+
const index = lines.findIndex(line => line.startsWith(`${name}=`));
|
|
104
|
+
if (index >= 0)
|
|
105
|
+
lines[index] = replacement;
|
|
106
|
+
else
|
|
107
|
+
lines.push(replacement);
|
|
108
|
+
}
|
|
109
|
+
await writeFile(path, `${lines.filter((line, index) => line || index < lines.length - 1).join('\n')}\n`, { encoding: 'utf8', mode: 0o600 });
|
|
110
|
+
}
|
|
111
|
+
function decodeEnvironmentValue(value) {
|
|
112
|
+
const trimmed = value.trim();
|
|
113
|
+
if (trimmed.startsWith('"') && trimmed.endsWith('"')) {
|
|
114
|
+
try {
|
|
115
|
+
return JSON.parse(trimmed);
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
return trimmed.slice(1, -1);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return trimmed;
|
|
122
|
+
}
|
|
123
|
+
async function ensureEnvironmentIgnored(directory) {
|
|
124
|
+
const path = resolve(directory, '.gitignore');
|
|
125
|
+
let contents = '';
|
|
126
|
+
try {
|
|
127
|
+
contents = await readFile(path, 'utf8');
|
|
128
|
+
}
|
|
129
|
+
catch (error) {
|
|
130
|
+
if (error.code !== 'ENOENT')
|
|
131
|
+
throw error;
|
|
132
|
+
}
|
|
133
|
+
const alreadyIgnored = contents.split(/\r?\n/).some(line => {
|
|
134
|
+
const rule = line.trim();
|
|
135
|
+
return rule === '.env' || rule === '/.env' || rule === '.env*' || rule === '/.env*';
|
|
136
|
+
});
|
|
137
|
+
if (alreadyIgnored)
|
|
138
|
+
return;
|
|
139
|
+
const prefix = contents && !contents.endsWith('\n') ? '\n' : '';
|
|
140
|
+
await writeFile(path, `${contents}${prefix}.env\n`, 'utf8');
|
|
141
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { ApiFieldResultTypes, ApiFieldTypes } from '../models/apiResults.js';
|
|
2
|
+
export interface TypegenField {
|
|
3
|
+
name: string;
|
|
4
|
+
result: typeof ApiFieldResultTypes[keyof typeof ApiFieldResultTypes];
|
|
5
|
+
type: typeof ApiFieldTypes[keyof typeof ApiFieldTypes];
|
|
6
|
+
writable: boolean;
|
|
7
|
+
required: boolean;
|
|
8
|
+
}
|
|
9
|
+
export interface TypegenLayout {
|
|
10
|
+
name: string;
|
|
11
|
+
clientName?: string;
|
|
12
|
+
fields: TypegenField[];
|
|
13
|
+
portals: Record<string, TypegenField[]>;
|
|
14
|
+
}
|
|
15
|
+
export interface TypegenSchema {
|
|
16
|
+
version: 1;
|
|
17
|
+
layouts: TypegenLayout[];
|
|
18
|
+
}
|
|
19
|
+
export type TypegenTransport = 'data-api' | 'odata';
|
|
20
|
+
export interface SchemaSource {
|
|
21
|
+
introspect(): Promise<TypegenSchema>;
|
|
22
|
+
}
|
|
23
|
+
/** Code-generation plugin for a runtime validation library. */
|
|
24
|
+
export interface TypegenValidator {
|
|
25
|
+
readonly imports: readonly string[];
|
|
26
|
+
readonly runtimeValidator: string;
|
|
27
|
+
/** Render a validator for the complete canonical ProviderRecord. */
|
|
28
|
+
renderRecord(layout: TypegenLayout): string;
|
|
29
|
+
}
|
|
30
|
+
export interface TypegenConfig {
|
|
31
|
+
output: string;
|
|
32
|
+
/** Single-database shorthand. */
|
|
33
|
+
source?: SchemaSource;
|
|
34
|
+
/** Placeholder describing intended runtime transports. */
|
|
35
|
+
transports?: TypegenTransport[];
|
|
36
|
+
/** Restrict output to these FileMaker layout names. */
|
|
37
|
+
layouts?: string[];
|
|
38
|
+
/** Defaults to the built-in strict Zod validator. */
|
|
39
|
+
validator?: TypegenValidator;
|
|
40
|
+
databases?: Record<string, TypegenDatabaseConfig>;
|
|
41
|
+
}
|
|
42
|
+
export interface TypegenDatabaseConfig {
|
|
43
|
+
source: SchemaSource;
|
|
44
|
+
provider?: string;
|
|
45
|
+
transports?: TypegenTransport[];
|
|
46
|
+
layouts?: string[];
|
|
47
|
+
validator?: TypegenValidator;
|
|
48
|
+
}
|
|
49
|
+
export declare function defineTypegenConfig(config: TypegenConfig): TypegenConfig;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export function defineTypegenConfig(config) { return config; }
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/** Strict Zod generator. Extra, missing, and wrongly typed fields fail validation. */
|
|
2
|
+
export function zodTypegenValidator() {
|
|
3
|
+
return {
|
|
4
|
+
imports: ["import {z} from 'zod'"],
|
|
5
|
+
runtimeValidator: 'zodValidator',
|
|
6
|
+
renderRecord: renderProviderRecord
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
function renderProviderRecord(layout) {
|
|
10
|
+
const fieldData = objectSchema(layout.fields);
|
|
11
|
+
const portals = Object.entries(layout.portals).map(([name, fields]) => `${quote(name)}: z.array(${portalRowSchema(fields)})`).join(', ');
|
|
12
|
+
const portalData = `z.object({${portals}}).partial().strict().optional()`;
|
|
13
|
+
return `z.object({recordId: z.string(), modId: z.string(), fieldData: ${fieldData}, portalData: ${portalData}}).strict()`;
|
|
14
|
+
}
|
|
15
|
+
function objectSchema(fields) {
|
|
16
|
+
return `z.object({${fields.map(field => `${quote(field.name)}: ${fieldSchema(field)}`).join(', ')}}).strict()`;
|
|
17
|
+
}
|
|
18
|
+
function portalRowSchema(fields) {
|
|
19
|
+
const entries = fields.map(field => `${quote(field.name)}: ${fieldSchema(field)}`);
|
|
20
|
+
entries.push('recordId: z.union([z.string(), z.number()])', 'modId: z.union([z.string(), z.number()])');
|
|
21
|
+
return `z.object({${entries.join(', ')}}).strict()`;
|
|
22
|
+
}
|
|
23
|
+
function fieldSchema(field) {
|
|
24
|
+
if (field.result === 'number')
|
|
25
|
+
return 'z.number().nullable()';
|
|
26
|
+
if (field.result === 'date' || field.result === 'time' || field.result === 'timeStamp')
|
|
27
|
+
return 'z.string().nullable()';
|
|
28
|
+
if (field.result === 'container')
|
|
29
|
+
return 'z.string().nullable()';
|
|
30
|
+
return 'z.string()';
|
|
31
|
+
}
|
|
32
|
+
function quote(value) { return JSON.stringify(value); }
|
package/package.json
CHANGED
|
@@ -1,13 +1,27 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jd-data-limited/easy-fm",
|
|
3
|
-
"version": "5.0.0-beta.
|
|
3
|
+
"version": "5.0.0-beta.5",
|
|
4
4
|
"dependencies": {
|
|
5
|
+
"chalk": "^5.6.2",
|
|
5
6
|
"commander": "^15.0.0",
|
|
7
|
+
"inquirer": "^14.2.2",
|
|
6
8
|
"temporal-polyfill": "^1.0.5",
|
|
7
9
|
"zod": "^4.6.5"
|
|
8
10
|
},
|
|
9
11
|
"type": "module",
|
|
10
12
|
"main": "dist/index.js",
|
|
13
|
+
"types": "dist/index.d.ts",
|
|
14
|
+
"sideEffects": false,
|
|
15
|
+
"exports": {
|
|
16
|
+
".": {
|
|
17
|
+
"types": "./dist/index.d.ts",
|
|
18
|
+
"import": "./dist/index.js"
|
|
19
|
+
},
|
|
20
|
+
"./typegen": {
|
|
21
|
+
"types": "./dist/typegen/index.d.ts",
|
|
22
|
+
"import": "./dist/typegen/index.js"
|
|
23
|
+
}
|
|
24
|
+
},
|
|
11
25
|
"module": "es2022",
|
|
12
26
|
"homepage": "https://jd-data.com/project/easyfm/",
|
|
13
27
|
"url": "https://github.com/JD-Data-Limited/easy-fm",
|
|
@@ -44,6 +58,7 @@
|
|
|
44
58
|
"@babel/core": "^8.0.6",
|
|
45
59
|
"@babel/preset-env": "^8.0.6",
|
|
46
60
|
"@babel/preset-typescript": "^8.0.1",
|
|
61
|
+
"@cloudflare/vitest-plugin": "^1.2.2",
|
|
47
62
|
"@types/btoa": "^1.2.5",
|
|
48
63
|
"@types/events": "^3.0.3",
|
|
49
64
|
"@types/inquirer": "^9.0.10",
|
|
@@ -57,17 +72,20 @@
|
|
|
57
72
|
"eslint-plugin-import": "^2.32.0",
|
|
58
73
|
"eslint-plugin-n": "^18.3.0",
|
|
59
74
|
"eslint-plugin-promise": "^7.3.0",
|
|
60
|
-
"inquirer": "^14.2.2",
|
|
61
75
|
"jest": "^30.5.2",
|
|
62
76
|
"ts-jest": "^29.4.12",
|
|
63
77
|
"ts-morph": "^28.0.0",
|
|
64
78
|
"tsx": "^4.23.15",
|
|
65
79
|
"typedoc": "^0.28.20",
|
|
66
|
-
"typescript": "^6.0.3"
|
|
80
|
+
"typescript": "^6.0.3",
|
|
81
|
+
"vitest": "^4.1.11"
|
|
67
82
|
},
|
|
68
83
|
"bin": {
|
|
69
84
|
"easyfm": "dist/bin/cli.js"
|
|
70
85
|
},
|
|
86
|
+
"publishConfig": {
|
|
87
|
+
"access": "public"
|
|
88
|
+
},
|
|
71
89
|
"scripts": {
|
|
72
90
|
"build": "tsc",
|
|
73
91
|
"test": "npm run build && jest",
|
|
@@ -75,7 +93,11 @@
|
|
|
75
93
|
"test:coverage": "jest --coverage --testPathIgnorePatterns='/__tests__/database.test.ts$'",
|
|
76
94
|
"test:integration": "npm run build && jest __tests__/database.test.ts --runInBand",
|
|
77
95
|
"test:unit": "jest __tests__/query.test.ts __tests__/CookieJar.test.ts --runInBand",
|
|
96
|
+
"test:runtime": "pnpm build && pnpm test:runtime:bun && pnpm test:runtime:deno && pnpm test:runtime:workers",
|
|
97
|
+
"test:runtime:bun": "bun test ./runtime-tests/bun.test.ts",
|
|
98
|
+
"test:runtime:deno": "deno test --no-check --allow-read --node-modules-dir=manual ./runtime-tests/deno.test.ts",
|
|
99
|
+
"test:runtime:workers": "vitest run --config ./runtime-tests/workers/vitest.config.ts",
|
|
78
100
|
"stress:search": "tsx src/bin/stressSearch.ts",
|
|
79
|
-
"docs:build": "typedoc --options
|
|
101
|
+
"docs:build": "typedoc --options ../../docs/theme.json"
|
|
80
102
|
}
|
|
81
103
|
}
|
package/dist/records/field.d.ts
DELETED
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
import { RecordTypes } from '../types.js';
|
|
2
|
-
import { type ApiFieldMetadata } from '../models/apiResults.js';
|
|
3
|
-
import { type LayoutBase } from '../layouts/layoutBase.js';
|
|
4
|
-
import { type Moment } from 'moment';
|
|
5
|
-
import { Readable } from 'node:stream';
|
|
6
|
-
import { type z } from 'zod';
|
|
7
|
-
export type FieldValue = string | number | Moment | Container;
|
|
8
|
-
export type Container = null;
|
|
9
|
-
export interface Parentable {
|
|
10
|
-
layout: LayoutBase;
|
|
11
|
-
type: RecordTypes;
|
|
12
|
-
endpoint: string;
|
|
13
|
-
portal?: {
|
|
14
|
-
name: string;
|
|
15
|
-
};
|
|
16
|
-
}
|
|
17
|
-
interface StreamOptions {
|
|
18
|
-
abortSignal?: AbortSignal;
|
|
19
|
-
}
|
|
20
|
-
/**
|
|
21
|
-
* A class representing a field in a record.
|
|
22
|
-
*
|
|
23
|
-
* @template T - The type of the field value.
|
|
24
|
-
*/
|
|
25
|
-
export declare class Field<T extends FieldValue> {
|
|
26
|
-
#private;
|
|
27
|
-
parent: Parentable;
|
|
28
|
-
id: string;
|
|
29
|
-
protected _value: T | null;
|
|
30
|
-
static firstContainerDownload: Promise<any> | null;
|
|
31
|
-
constructor(record: Parentable, id: string, originalContents: T);
|
|
32
|
-
/** `true` if this field has changed since it was last loaded or saved. */
|
|
33
|
-
get edited(): boolean;
|
|
34
|
-
/**
|
|
35
|
-
* Resets the 'edited' state of this field, without changing its value
|
|
36
|
-
*/
|
|
37
|
-
updateOriginalContents(): void;
|
|
38
|
-
/**
|
|
39
|
-
* Sets the value of the field.
|
|
40
|
-
*
|
|
41
|
-
* @param {T | null} content - The new content value to be set. Can be either the type T or null.
|
|
42
|
-
* @throws {Error} Cannot set container value using set(). Use upload() instead, if the result is a 'container'.
|
|
43
|
-
*/
|
|
44
|
-
set(content: T | null): void;
|
|
45
|
-
/** Returns the FileMaker metadata for this field. */
|
|
46
|
-
get metadata(): z.infer<typeof ApiFieldMetadata>;
|
|
47
|
-
/** Gets or sets the current field value. */
|
|
48
|
-
get value(): T | null;
|
|
49
|
-
set value(value: T);
|
|
50
|
-
/** Returns this field value as a string. Throws if the value is not a string. */
|
|
51
|
-
get string(): string;
|
|
52
|
-
/**
|
|
53
|
-
* Uploads a file to the container field.
|
|
54
|
-
*
|
|
55
|
-
* @param {Buffer} file - The file content as a buffer.
|
|
56
|
-
*
|
|
57
|
-
* @throws {Error} - Cannot upload a file to the field if it's not a container field.
|
|
58
|
-
* @throws {Error} - Upload failed with HTTP error.
|
|
59
|
-
*
|
|
60
|
-
* @returns {Promise<void>} - A promise that resolves when the file is successfully uploaded.
|
|
61
|
-
*/
|
|
62
|
-
upload(file: File): Promise<void>;
|
|
63
|
-
stream(): Promise<{
|
|
64
|
-
data: Readable;
|
|
65
|
-
mime: string;
|
|
66
|
-
}>;
|
|
67
|
-
/**
|
|
68
|
-
* Returns the container download as a Web `Response`.
|
|
69
|
-
*/
|
|
70
|
-
webStream(options: StreamOptions): Promise<Response>;
|
|
71
|
-
/** Downloads the full container contents into memory. */
|
|
72
|
-
arrayBuffer(): Promise<{
|
|
73
|
-
data: ArrayBuffer;
|
|
74
|
-
mime: string;
|
|
75
|
-
}>;
|
|
76
|
-
}
|
|
77
|
-
export {};
|