@nitwel/sandbox 1.0.0

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.
@@ -0,0 +1,10 @@
1
+ services:
2
+ saml:
3
+ image: kristophjunge/test-saml-idp
4
+ ports:
5
+ - ${SAML_PORT}:8080
6
+ environment:
7
+ - SIMPLESAMLPHP_SP_ENTITY_ID=saml-test
8
+ - SIMPLESAMLPHP_SP_ASSERTION_CONSUMER_SERVICE=http://127.0.0.1:8080/auth/login/saml/acs
9
+ extra_hosts:
10
+ - 'host.docker.internal:host-gateway'
@@ -0,0 +1 @@
1
+ export * from './sandbox.js';
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ export * from './sandbox.js';
@@ -0,0 +1,18 @@
1
+ import type Stream from 'stream';
2
+ import type { Env } from './config.js';
3
+ import type { Options } from './sandbox.js';
4
+ export type Logger = {
5
+ addGroup: (group: string) => Logger;
6
+ fatal: (msg: string) => void;
7
+ error: (msg: string) => void;
8
+ warn: (msg: string) => void;
9
+ info: (msg: string) => void;
10
+ debug: (msg: string) => void;
11
+ pipe: (stream: Stream.Readable | null, type?: LogLevel) => void;
12
+ onLog: (listener: LogListener) => void;
13
+ };
14
+ export declare const logLevels: readonly ["fatal", "error", "warn", "info", "debug", "trace"];
15
+ export type LogLevel = (typeof logLevels)[number];
16
+ export type LogListener = (message: string, type: LogLevel, groups: string[]) => void;
17
+ export declare function createLogger(env: Env, opts: Options, ...groups: string[]): Logger;
18
+ export declare function log(env: Env, opts: Options, message: string, type?: LogLevel, ...groups: string[]): void;
package/dist/logger.js ADDED
@@ -0,0 +1,49 @@
1
+ import chalk, {} from 'chalk';
2
+ export const logLevels = ['fatal', 'error', 'warn', 'info', 'debug', 'trace'];
3
+ const listeners = {};
4
+ export function createLogger(env, opts, ...groups) {
5
+ return {
6
+ addGroup: (group) => {
7
+ return createLogger(env, opts, ...groups, group);
8
+ },
9
+ fatal: (msg) => log(env, opts, msg, 'fatal', ...groups),
10
+ error: (msg) => log(env, opts, msg, 'error', ...groups),
11
+ warn: (msg) => log(env, opts, msg, 'warn', ...groups),
12
+ info: (msg) => log(env, opts, msg, 'info', ...groups),
13
+ debug: (msg) => log(env, opts, msg, 'debug', ...groups),
14
+ pipe: (stream, type) => {
15
+ if (stream)
16
+ stream.on('data', (data) => log(env, opts, String(data), type, ...groups));
17
+ },
18
+ onLog: (listener) => {
19
+ const key = groups.join('.');
20
+ if (!listeners[key])
21
+ listeners[key] = [];
22
+ listeners[key].push(listener);
23
+ },
24
+ };
25
+ }
26
+ function logLevel(level) {
27
+ return logLevels.findIndex((l) => l === String(level).toLowerCase());
28
+ }
29
+ const logLevelColor = {
30
+ debug: 'blue',
31
+ error: 'red',
32
+ fatal: 'black',
33
+ info: 'green',
34
+ warn: 'yellow',
35
+ trace: 'blueBright',
36
+ };
37
+ export function log(env, opts, message, type = 'info', ...groups) {
38
+ const formattedMessage = groups.map((group) => chalk.blueBright(`[${group}] `)).join('') +
39
+ chalk[logLevelColor[type]](`[${type}] `) +
40
+ message +
41
+ (message.endsWith('\n') ? '' : '\n');
42
+ if (logLevel(env.LOG_LEVEL ?? 'info') >= logLevel(type) && (!opts.silent || type === 'error' || type === 'fatal'))
43
+ process.stdout.write(formattedMessage);
44
+ for (const [key, listener] of Object.entries(listeners)) {
45
+ if (groups.join('.').startsWith(key)) {
46
+ listener.forEach((l) => l(message, type, groups));
47
+ }
48
+ }
49
+ }
@@ -0,0 +1,5 @@
1
+ import type { Relation } from '@directus/types';
2
+ export declare function getRelationInfo(relations: Relation[], collection: string, field: string): {
3
+ relation: Relation;
4
+ relationType: "m2o" | "o2m" | "m2a";
5
+ } | undefined;
@@ -0,0 +1,8 @@
1
+ import { getRelation, getRelationType } from '@directus/utils';
2
+ export function getRelationInfo(relations, collection, field) {
3
+ const relation = getRelation(relations, collection, field) ?? null;
4
+ if (!relation)
5
+ return undefined;
6
+ const relationType = getRelationType({ relation, collection, field });
7
+ return { relation, relationType };
8
+ }
@@ -0,0 +1,81 @@
1
+ import type { DatabaseClient, DeepPartial } from '@directus/types';
2
+ import { type Env } from './config.js';
3
+ import { type Logger } from './logger.js';
4
+ export type { Env } from './config.js';
5
+ export type Database = Exclude<DatabaseClient, 'redshift'> | 'maria';
6
+ type Port = string | number;
7
+ export type Options = {
8
+ /** Rebuild directus from source */
9
+ build: boolean;
10
+ /** Start directus in developer mode. Not compatible with build */
11
+ dev: boolean;
12
+ /** Restart the api when changes are made */
13
+ watch: boolean;
14
+ /** Port to start the api on */
15
+ port: Port;
16
+ /** Which version of the database to use */
17
+ version: string | undefined;
18
+ /** Configure the behavior of the spun up docker container */
19
+ docker: {
20
+ /** Keep containers running when stopping the sandbox */
21
+ keep: boolean;
22
+ /** Minimum port number to use for docker containers */
23
+ basePort: Port | (() => Port | Promise<Port>);
24
+ /** Overwrite the name of the docker project */
25
+ name: string | undefined;
26
+ /** Adds a suffix to the docker project. Can be used to ensure uniqueness */
27
+ suffix: string;
28
+ };
29
+ /** Horizontally scale the api to a given number of instances */
30
+ instances: string;
31
+ /** Add environment variables that the api should start with */
32
+ env: Record<string, string>;
33
+ /** Prefix the logs, useful when starting multiple sandboxes */
34
+ prefix: string | undefined;
35
+ /** Exports a snapshot and type definition every 2 seconds */
36
+ export: boolean;
37
+ /** Silence all logs except for errors */
38
+ silent: boolean;
39
+ /** Load an additional schema snapshot on startup */
40
+ schema: string | undefined;
41
+ /** Start the api with debugger */
42
+ inspect: boolean;
43
+ /** Forcefully kills all processes that occupy ports that the api would use */
44
+ killPorts: boolean;
45
+ /** Enable redis,maildev,saml or other extras */
46
+ extras: {
47
+ /** Used for caching, forced to true if instances > 1 */
48
+ redis: boolean;
49
+ /** Auth provider */
50
+ saml: boolean;
51
+ /** Storage provider */
52
+ minio: boolean;
53
+ /** Email server */
54
+ maildev: boolean;
55
+ };
56
+ /** Enable or disable caching */
57
+ cache: boolean;
58
+ };
59
+ export type Sandboxes = {
60
+ sandboxes: {
61
+ index: number;
62
+ env: Env;
63
+ logger: Logger;
64
+ }[];
65
+ restartApis(): Promise<void>;
66
+ stop(): Promise<void>;
67
+ };
68
+ export type Sandbox = {
69
+ restartApi(): Promise<void>;
70
+ stop(): Promise<void>;
71
+ env: Env;
72
+ logger: Logger;
73
+ };
74
+ export declare const apiFolder: string;
75
+ export declare const databases: Database[];
76
+ export type SandboxesOptions = {
77
+ database: Database;
78
+ options: DeepPartial<Omit<Options, 'build' | 'dev' | 'watch' | 'export'>>;
79
+ }[];
80
+ export declare function sandboxes(sandboxes: SandboxesOptions, options?: Partial<Pick<Options, 'build' | 'dev' | 'watch'>>): Promise<Sandboxes>;
81
+ export declare function sandbox(database: Database, options?: DeepPartial<Options>): Promise<Sandbox>;
@@ -0,0 +1,144 @@
1
+ import {} from 'child_process';
2
+ import { merge } from 'lodash-es';
3
+ import { join } from 'path';
4
+ import { getEnv } from './config.js';
5
+ import { createLogger } from './logger.js';
6
+ import { buildDirectus, bootstrap, dockerDown, dockerUp, loadSchema, saveSchema, startDirectus, } from './steps/index.js';
7
+ import chalk from 'chalk';
8
+ import getPort from 'get-port';
9
+ async function getOptions(options) {
10
+ if (options?.schema === true)
11
+ options.schema = 'snapshot.json';
12
+ const port = await getPort({ port: 8055 });
13
+ return merge({
14
+ build: false,
15
+ dev: false,
16
+ watch: false,
17
+ port,
18
+ version: undefined,
19
+ docker: {
20
+ keep: false,
21
+ basePort: port + 100,
22
+ name: undefined,
23
+ suffix: '',
24
+ },
25
+ instances: '1',
26
+ inspect: true,
27
+ env: {},
28
+ prefix: undefined,
29
+ schema: undefined,
30
+ silent: false,
31
+ export: false,
32
+ killPorts: false,
33
+ extras: {
34
+ redis: false,
35
+ maildev: false,
36
+ minio: false,
37
+ saml: false,
38
+ },
39
+ cache: false,
40
+ }, options);
41
+ }
42
+ export const apiFolder = join(import.meta.dirname, '..', '..', '..', 'api');
43
+ export const databases = [
44
+ 'maria',
45
+ 'cockroachdb',
46
+ 'mssql',
47
+ 'mysql',
48
+ 'oracle',
49
+ 'postgres',
50
+ 'sqlite',
51
+ ];
52
+ export async function sandboxes(sandboxes, options) {
53
+ if (!sandboxes.every((sandbox) => databases.includes(sandbox.database)))
54
+ throw new Error('Invalid database provided');
55
+ const opts = await getOptions(options);
56
+ const logger = createLogger(process.env, opts);
57
+ let apis = [];
58
+ let build;
59
+ const projects = [];
60
+ try {
61
+ // Rebuild directus
62
+ if (opts.build && !opts.dev) {
63
+ build = await buildDirectus(opts, logger, restartApis);
64
+ }
65
+ await Promise.all(sandboxes.map(async ({ database, options }, index) => {
66
+ const opts = await getOptions(options);
67
+ const env = await getEnv(database, opts);
68
+ const logger = opts.prefix ? createLogger(env, opts, opts.prefix) : createLogger(env, opts);
69
+ try {
70
+ const project = await dockerUp(database, opts, env, logger);
71
+ if (project)
72
+ projects.push({ project, logger, env });
73
+ await bootstrap(env, logger);
74
+ if (opts.schema)
75
+ await loadSchema(opts.schema, env, logger);
76
+ apis.push({ processes: await startDirectus(opts, env, logger), index, opts, env, logger });
77
+ }
78
+ catch (e) {
79
+ logger.error(String(e));
80
+ throw e;
81
+ }
82
+ }));
83
+ }
84
+ catch (e) {
85
+ await stop();
86
+ throw e;
87
+ }
88
+ async function restartApis() {
89
+ apis.forEach((api) => api.processes.forEach((process) => process.kill()));
90
+ apis = await Promise.all(apis.map(async (api) => ({ ...api, processes: await startDirectus(api.opts, api.env, api.logger) })));
91
+ }
92
+ async function stop() {
93
+ build?.kill();
94
+ apis.forEach((api) => api.processes.forEach((process) => process.kill()));
95
+ if (opts.docker.keep)
96
+ await Promise.all(projects.map(({ project, logger, env }) => dockerDown(project, env, logger)));
97
+ }
98
+ return { sandboxes: apis.map(({ env, index, logger }) => ({ index, env, logger })), stop, restartApis };
99
+ }
100
+ export async function sandbox(database, options) {
101
+ if (!databases.includes(database))
102
+ throw new Error('Invalid database provided');
103
+ const opts = await getOptions(options);
104
+ const env = await getEnv(database, opts);
105
+ const logger = opts.prefix ? createLogger(env, opts, opts.prefix) : createLogger(env, opts);
106
+ let apis = [];
107
+ let project;
108
+ let build;
109
+ let interval;
110
+ try {
111
+ // Rebuild directus
112
+ if (opts.build && !opts.dev) {
113
+ build = await buildDirectus(opts, logger, restartApi);
114
+ }
115
+ project = await dockerUp(database, opts, env, logger);
116
+ await bootstrap(env, logger);
117
+ if (opts.schema)
118
+ await loadSchema(opts.schema, env, logger);
119
+ apis = await startDirectus(opts, env, logger);
120
+ if (opts.export)
121
+ interval = await saveSchema(env);
122
+ }
123
+ catch (err) {
124
+ logger.error(err.toString());
125
+ await stop();
126
+ throw err;
127
+ }
128
+ async function restartApi() {
129
+ apis.forEach((api) => api.kill());
130
+ apis = await startDirectus(opts, env, logger);
131
+ }
132
+ async function stop() {
133
+ const start = performance.now();
134
+ logger.info('Stopping sandbox');
135
+ clearInterval(interval);
136
+ build?.kill();
137
+ apis.forEach((api) => api.kill());
138
+ if (project && !opts.docker.keep)
139
+ await dockerDown(project, env, logger);
140
+ const time = chalk.gray(`(${Math.round(performance.now() - start)}ms)`);
141
+ logger.info(`Stopped sandbox ${time}`);
142
+ }
143
+ return { stop, restartApi, env, logger };
144
+ }
@@ -0,0 +1,6 @@
1
+ import { type Env } from '../config.js';
2
+ import { type Logger } from '../logger.js';
3
+ import { type Options } from '../sandbox.js';
4
+ export declare function buildDirectus(opts: Options, logger: Logger, onRebuild: () => void): Promise<import("child_process").ChildProcessWithoutNullStreams | undefined>;
5
+ export declare function bootstrap(env: Env, logger: Logger): Promise<void>;
6
+ export declare function startDirectus(opts: Options, env: Env, logger: Logger): Promise<import("child_process").ChildProcessWithoutNullStreams[]>;
@@ -0,0 +1,170 @@
1
+ import { spawn } from 'child_process';
2
+ import { join } from 'path';
3
+ import {} from '../config.js';
4
+ import {} from '../logger.js';
5
+ import { apiFolder } from '../sandbox.js';
6
+ import chalk from 'chalk';
7
+ import { portToPid } from 'pid-port';
8
+ import { createInterface } from 'readline/promises';
9
+ export async function buildDirectus(opts, logger, onRebuild) {
10
+ const start = performance.now();
11
+ logger.info('Rebuilding Directus');
12
+ let timeout;
13
+ const watch = opts.watch ? ['--watch', '--preserveWatchOutput'] : [];
14
+ const inspect = opts.inspect ? ['--sourceMap'] : [];
15
+ const build = spawn('pnpm', ['tsc', ...watch, ...inspect, '--project tsconfig.prod.json'], {
16
+ cwd: apiFolder,
17
+ shell: true,
18
+ });
19
+ build.on('error', (err) => {
20
+ logger.error(err.toString());
21
+ });
22
+ build.on('close', (code) => {
23
+ if (code === null || code === 0)
24
+ return;
25
+ build.kill();
26
+ const error = new Error(`Building api stopped with error code ${code}`);
27
+ clearTimeout(timeout);
28
+ logger.error(error.toString());
29
+ throw error;
30
+ });
31
+ logger.pipe(build.stderr, 'error');
32
+ if (opts.watch) {
33
+ await new Promise((resolve, reject) => {
34
+ build.stdout.on('data', (data) => {
35
+ logger.debug(String(data));
36
+ if (String(data).includes(`Watching for file changes.`)) {
37
+ onRebuild();
38
+ resolve(undefined);
39
+ }
40
+ });
41
+ // In case the api takes too long to start
42
+ timeout = setTimeout(() => reject(new Error('timeout building directus')), 60_000);
43
+ });
44
+ return build;
45
+ }
46
+ else {
47
+ logger.pipe(build.stdout);
48
+ await new Promise((resolve) => build.on('close', resolve));
49
+ const time = chalk.gray(`(${Math.round(performance.now() - start)}ms)`);
50
+ logger.info(`New Build Completed ${time}`);
51
+ return;
52
+ }
53
+ }
54
+ export async function bootstrap(env, logger) {
55
+ const start = performance.now();
56
+ logger.info('Bootstraping Database');
57
+ const bootstrap = spawn('node', [join(apiFolder, 'dist', 'cli', 'run.js'), 'bootstrap'], {
58
+ env,
59
+ });
60
+ bootstrap.on('error', (err) => {
61
+ bootstrap.kill();
62
+ throw err;
63
+ });
64
+ logger.pipe(bootstrap.stdout, 'debug');
65
+ logger.pipe(bootstrap.stderr, 'error');
66
+ await new Promise((resolve) => bootstrap.on('close', resolve));
67
+ const time = chalk.gray(`(${Math.round(performance.now() - start)}ms)`);
68
+ logger.info(`Completed Bootstraping Database ${time}`);
69
+ }
70
+ export async function startDirectus(opts, env, logger) {
71
+ const apiCount = Math.max(1, Number(opts.instances));
72
+ const apiPorts = [...Array(apiCount).keys()].flatMap((i) => Number(env.PORT) + i * (opts.inspect ? 2 : 1));
73
+ const allPorts = apiPorts.flatMap((port) => (opts.inspect ? [port, port + 1] : [port]));
74
+ const occupiedPorts = (await Promise.allSettled(allPorts.map((port) => portToPid(port))))
75
+ .map((result, i) => (result.status === 'fulfilled' ? [result.value, allPorts[i]] : undefined))
76
+ .filter((val) => val);
77
+ let killPorts;
78
+ for (const [pid, port] of occupiedPorts) {
79
+ logger.warn(`Port ${port} is occupied by pid ${pid}`);
80
+ }
81
+ if (opts.killPorts) {
82
+ killPorts = true;
83
+ }
84
+ else {
85
+ if (occupiedPorts.length > 0) {
86
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
87
+ const result = (await rl.question('Would you like to kill all occupying processes? (Y/N) ')).toLowerCase();
88
+ killPorts = result === 'y' || result === 'yes';
89
+ }
90
+ }
91
+ if (killPorts) {
92
+ for (const [pid] of occupiedPorts) {
93
+ try {
94
+ process.kill(pid, 'SIGKILL');
95
+ logger.info(`Killed process ${pid}`);
96
+ }
97
+ catch (err) {
98
+ logger.error(`Failed to kill process ${pid}: ${err.message}`);
99
+ }
100
+ }
101
+ }
102
+ return await Promise.all(apiPorts.map((port) => {
103
+ const newLogger = apiCount > 1 ? logger.addGroup(`API ${port}`) : logger;
104
+ return startDirectusInstance(opts, { ...env, PORT: String(port) }, newLogger);
105
+ }));
106
+ }
107
+ async function startDirectusInstance(opts, env, logger) {
108
+ const start = performance.now();
109
+ logger.info('Starting Directus');
110
+ const debuggerPort = Number(env.PORT) + 1;
111
+ let api;
112
+ let timeout;
113
+ const inspect = opts.inspect ? [`--inspect=${debuggerPort}`] : [];
114
+ if (opts.dev) {
115
+ const watch = opts.watch ? ['watch', '--clear-screen=false'] : [];
116
+ api = spawn('pnpm ', ['tsx', ...watch, ...inspect, join(apiFolder, 'src', 'start.ts')], {
117
+ env,
118
+ shell: true,
119
+ stdio: 'overlapped', // Has to be here, only god knows why.
120
+ });
121
+ }
122
+ else {
123
+ api = spawn('node', [...inspect, join(apiFolder, 'dist', 'cli', 'run.js'), 'start'], {
124
+ env,
125
+ });
126
+ }
127
+ api.on('error', (err) => {
128
+ logger.error(err.toString());
129
+ });
130
+ api.on('close', (code) => {
131
+ if (code === null || code === 0)
132
+ return;
133
+ const error = new Error(`Api stopped with error code ${code}`);
134
+ clearTimeout(timeout);
135
+ logger.error(error.toString());
136
+ throw error;
137
+ });
138
+ api.stderr.on('data', (data) => {
139
+ const msg = String(data);
140
+ if (msg.startsWith('Debugger listening on ws://'))
141
+ return;
142
+ if (msg.startsWith('Debugger attached')) {
143
+ logger.info(msg);
144
+ return;
145
+ }
146
+ logger.error(msg);
147
+ });
148
+ await new Promise((resolve, reject) => {
149
+ api.stdout.on('data', (data) => {
150
+ const msg = String(data);
151
+ if (msg.includes(`Server started at http://${env.HOST}:${env.PORT}`)) {
152
+ resolve(undefined);
153
+ }
154
+ else if (msg.includes(`ERROR: Port ${env.PORT} is already in use`)) {
155
+ reject(new Error(msg));
156
+ }
157
+ else {
158
+ logger.debug(msg);
159
+ }
160
+ });
161
+ // In case the api takes too long to start
162
+ timeout = setTimeout(() => {
163
+ reject(new Error('timeout starting directus'));
164
+ }, 60_000);
165
+ });
166
+ const time = chalk.gray(`(${Math.round(performance.now() - start)}ms)`);
167
+ logger.info(`Server started at http://${env.HOST}:${env.PORT}, Debugger listening on http://${env.HOST}:${debuggerPort} ${time}`);
168
+ logger.info(`User: ${chalk.cyan(env.ADMIN_EMAIL)} Password: ${chalk.cyan(env.ADMIN_PASSWORD)} Token: ${chalk.cyan(env.ADMIN_TOKEN)}`);
169
+ return api;
170
+ }
@@ -0,0 +1,5 @@
1
+ import { type Env } from '../config.js';
2
+ import { type Logger } from '../logger.js';
3
+ import type { Database, Options } from '../sandbox.js';
4
+ export declare function dockerUp(database: Database, opts: Options, env: Env, logger: Logger): Promise<string | undefined>;
5
+ export declare function dockerDown(project: string, env: Env, logger: Logger): Promise<void>;
@@ -0,0 +1,63 @@
1
+ import { spawn } from 'child_process';
2
+ import { join } from 'path';
3
+ import {} from '../config.js';
4
+ import {} from '../logger.js';
5
+ import chalk from 'chalk';
6
+ export async function dockerUp(database, opts, env, logger) {
7
+ const start = performance.now();
8
+ logger.info('Starting up Docker containers');
9
+ const extrasList = Object.entries(opts.extras)
10
+ .filter(([_, value]) => value)
11
+ .map(([key, _]) => key);
12
+ const project = opts.docker.name ??
13
+ `sandbox_${database}${extrasList.map((extra) => '_' + extra).join('')}` +
14
+ (opts.docker.suffix ? `_${opts.docker.suffix}` : '');
15
+ const files = database === 'sqlite' ? extrasList : [database, ...extrasList];
16
+ if (files.length === 0)
17
+ return;
18
+ const docker = spawn('docker', [
19
+ 'compose',
20
+ '-p',
21
+ project,
22
+ ...files.flatMap((file) => ['-f', join(import.meta.dirname, '..', 'docker', `${file}.yml`)]),
23
+ 'up',
24
+ '-d',
25
+ '--wait',
26
+ ], {
27
+ env: {
28
+ ...env,
29
+ COMPOSE_STATUS_STDOUT: '1', //Ref: https://github.com/docker/compose/issues/7346
30
+ },
31
+ });
32
+ docker.on('error', (err) => {
33
+ docker.kill();
34
+ throw err;
35
+ });
36
+ logger.pipe(docker.stdout, 'debug');
37
+ logger.pipe(docker.stderr, 'error');
38
+ await new Promise((resolve) => docker.on('close', resolve));
39
+ const time = chalk.gray(`(${Math.round(performance.now() - start)}ms)`);
40
+ if ('DB_PORT' in env) {
41
+ logger.info(`Database started at ${env.DB_HOST}:${env.DB_PORT}/${env.DB_DATABASE} ${time}`);
42
+ logger.info(`User: ${chalk.cyan(env.DB_USER)} Password: ${chalk.cyan(env.DB_PASSWORD)}`);
43
+ }
44
+ else if ('DB_FILENAME' in env)
45
+ logger.info(`Database stored at ${env.DB_FILENAME} ${time}`);
46
+ return project;
47
+ }
48
+ export async function dockerDown(project, env, logger) {
49
+ const start = performance.now();
50
+ logger.info('Stopping docker containers');
51
+ const docker = spawn('docker', ['compose', '-p', project, 'down'], {
52
+ env: { ...env, COMPOSE_STATUS_STDOUT: '1' },
53
+ });
54
+ docker.on('error', (err) => {
55
+ docker.kill();
56
+ throw err;
57
+ });
58
+ logger.pipe(docker.stdout, 'debug');
59
+ logger.pipe(docker.stderr, 'error');
60
+ await new Promise((resolve) => docker.on('close', resolve));
61
+ const time = chalk.gray(`(${Math.round(performance.now() - start)}ms)`);
62
+ logger.info(`Docker containers stopped ${time}`);
63
+ }
@@ -0,0 +1,3 @@
1
+ export * from './api.js';
2
+ export * from './docker.js';
3
+ export * from './schema.js';
@@ -0,0 +1,3 @@
1
+ export * from './api.js';
2
+ export * from './docker.js';
3
+ export * from './schema.js';
@@ -0,0 +1,4 @@
1
+ import { type Env } from '../config.js';
2
+ import { type Logger } from '../logger.js';
3
+ export declare function loadSchema(schema_file: string, env: Env, logger: Logger): Promise<void>;
4
+ export declare function saveSchema(env: Env): Promise<NodeJS.Timeout>;
@@ -0,0 +1,78 @@
1
+ import { spawn } from 'child_process';
2
+ import { writeFile } from 'fs/promises';
3
+ import { camelCase, upperFirst } from 'lodash-es';
4
+ import { join, resolve } from 'path';
5
+ import {} from '../config.js';
6
+ import {} from '../logger.js';
7
+ import { getRelationInfo } from '../relation.js';
8
+ import { apiFolder } from '../sandbox.js';
9
+ import chalk from 'chalk';
10
+ export async function loadSchema(schema_file, env, logger) {
11
+ const start = performance.now();
12
+ logger.info('Applying Schema');
13
+ const schema = spawn('node', [join(apiFolder, 'dist', 'cli', 'run.js'), 'schema', 'apply', '-y', resolve(schema_file)], {
14
+ env,
15
+ });
16
+ schema.on('error', (err) => {
17
+ schema.kill();
18
+ throw err;
19
+ });
20
+ logger.pipe(schema.stdout, 'debug');
21
+ logger.pipe(schema.stderr, 'error');
22
+ await new Promise((resolve) => schema.on('close', resolve));
23
+ const time = chalk.gray(`(${Math.round(performance.now() - start)}ms)`);
24
+ logger.info(`Schema Applied ${time}`);
25
+ }
26
+ export async function saveSchema(env) {
27
+ return setInterval(async () => {
28
+ const data = await fetch(`${env.PUBLIC_URL}/schema/snapshot?access_token=${env.ADMIN_TOKEN}`);
29
+ const snapshot = (await data.json());
30
+ const collections = snapshot.data.collections.filter((collection) => collection.schema);
31
+ const schema = `export type Schema = {
32
+ ${collections
33
+ .map((collection) => {
34
+ const collectionName = formatCollection(collection.collection);
35
+ if (collection.meta?.singleton)
36
+ return `${formatField(collection.collection)}: ${collectionName}`;
37
+ return `${formatField(collection.collection)}: ${collectionName}[];`;
38
+ })
39
+ .join('\n ')}
40
+ };
41
+ `;
42
+ const collectionTypes = collections
43
+ .map((collection) => {
44
+ const collectionName = formatCollection(collection.collection);
45
+ return `export type ${collectionName} = {
46
+ ${snapshot.data.fields
47
+ .filter((field) => field.collection === collection.collection)
48
+ .map((field) => {
49
+ const rel = getRelationInfo(snapshot.data.relations, collection.collection, field.field);
50
+ const optional = field.schema?.is_nullable || field.schema?.is_generated || field.schema?.is_primary_key;
51
+ const fieldName = `${formatField(field.field)}${optional ? '?' : ''}:`;
52
+ if (!rel)
53
+ return `${fieldName} string | number;`;
54
+ const { relation, relationType } = rel;
55
+ if (relationType === 'o2m') {
56
+ return `${fieldName} (string | number | ${formatCollection(relation.collection)})[];`;
57
+ }
58
+ else if (relationType === 'm2o') {
59
+ return `${fieldName} string | number | ${formatCollection(relation.related_collection)};`;
60
+ }
61
+ else {
62
+ return `${fieldName} string | number | ${relation.meta.one_allowed_collections.map(formatCollection).join(' | ')};`;
63
+ }
64
+ })
65
+ .join('\n ')}
66
+ };`;
67
+ })
68
+ .join('\n');
69
+ await writeFile('schema.d.ts', schema + collectionTypes);
70
+ await writeFile('snapshot.json', JSON.stringify(snapshot.data, null, 4));
71
+ }, 2000);
72
+ }
73
+ function formatCollection(title) {
74
+ return upperFirst(camelCase(title.replaceAll('_1234', '')));
75
+ }
76
+ function formatField(title) {
77
+ return title.replaceAll('_1234', '');
78
+ }