@getanthill/datastore
Version:
Event-Sourced Datastore
310 lines (260 loc) • 7.32 kB
text/typescript
process.env.MONGOMS_DOWNLOAD_URL =
process.env.MONGOMS_DOWNLOAD_URL ||
'https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-debian12-8.0.3.tgz';
process.env.OTEL_PROMETHEUS_EXPORTER_PREVENT_SERVER_START = 'true';
import type { DatastoreConfig, ModelConfig, Services } from './typings';
import type { Express } from 'express';
import type { Server } from 'http';
import { MongoDbConnector } from '@getanthill/mongodb-connector';
/* @ts-ignore */
import jestMongodb from '@shelf/jest-mongodb/jest-preset';
import { merge } from 'lodash';
import util from 'util';
import net from 'net';
import config from './config';
import { init, Models } from './models';
import App from './App';
import { build } from './services';
import Datastore from './sdk/Datastore';
// Set the default ACCESS_TOKENS for the tests:
process.env.ADMIN_ACCESS_TOKENS = 'token';
export default async function setup() {
console.log('Global Setup');
process.env.SKIP_MONGODB_IN_MEMORY !== 'true' &&
(await setupMongodbInMemory());
}
async function setupMongodbInMemory(attempt = 0): Promise<void> {
try {
const { default: setupMongoDb } = await import(jestMongodb.globalSetup);
await setupMongoDb({
rootDir: '.',
});
} catch (err) {
if (attempt < 10) {
console.warn(err);
console.log('Retrying the MongoDB setup');
await new Promise((resolve) => setTimeout(resolve, 1000));
setupMongodbInMemory(attempt + 1);
return;
}
console.error(err);
process.exit(1);
}
}
async function teardownMongoDbInMemory() {
try {
const { default: teardownMongoDb } = await import(
jestMongodb.globalTeardown
);
process.env.SKIP_MONGODB_IN_MEMORY !== 'true' &&
(await teardownMongoDb({
rootDir: '.',
}));
} catch (err) {
console.warn(err);
}
}
setup.setupMongodbInMemory = setupMongodbInMemory;
setup.teardownMongoDbInMemory = teardownMongoDbInMemory;
setup.uuid = function uuid() {
return 'uuid' + (Math.random() * 1e16).toFixed(0);
};
const DEFAULT_SERVICES_CONFIG = config;
async function getPort(): Promise<number> {
return new Promise((resolve) => {
const srv = net.createServer();
srv.listen(0, () => {
// @ts-ignore
const port = srv.address()?.port as number;
srv.close(() => {
resolve(port);
});
});
});
}
setup.build = async (config: Partial<DatastoreConfig> = {}): Promise<App> => {
const dbName = `datastore_test_${new Date().getTime()}`;
const _config = merge(
{},
DEFAULT_SERVICES_CONFIG,
{
mode: 'development',
port: await getPort(),
features: {
initInternalModels: false,
},
mongodb: {
databases: [
{
...DEFAULT_SERVICES_CONFIG.mongodb.databases[0],
url: (
process.env.MONGO_URL ||
DEFAULT_SERVICES_CONFIG.mongodb.databases[0].url
)
.replace('/?', '/' + dbName + '?')
.replace('/datastore', `/${dbName}`),
},
{
...DEFAULT_SERVICES_CONFIG.mongodb.databases[1],
url: (
process.env.MONGO_URL ||
DEFAULT_SERVICES_CONFIG.mongodb.databases[1].url
)
.replace('/?', '/' + dbName + '?')
.replace('/datastore', `/${dbName}`),
},
],
ensureIndexInBackground: true,
},
},
config,
);
const services = build(_config);
if (typeof jest !== 'undefined') {
/* @ts-ignore */
global.debugMock = jest
.spyOn(services.telemetry.logger, 'debug')
/* @ts-ignore */
.mockImplementation(() => null);
/* @ts-ignore */
global.infoMock = jest
.spyOn(services.telemetry.logger, 'info')
/* @ts-ignore */
.mockImplementation(() => null);
/* @ts-ignore */
global.warnMock = jest
.spyOn(services.telemetry.logger, 'warn')
/* @ts-ignore */
.mockImplementation(() => null);
/* @ts-ignore */
global.errorMock = jest
.spyOn(services.telemetry.logger, 'error')
/* @ts-ignore */
.mockImplementation(() => null);
}
await services.mongodb.connect();
await services.mongodb.db('datastore_write').dropDatabase();
await services.fhe.connect();
return new App(services);
};
setup.cleanModel = async function cleanModel(
services: Services,
modelName: string,
) {
// Remove internal models
await Promise.all([
services.mongodb.db('datastore_write').collection(modelName).deleteMany({}),
services.mongodb
.db('datastore_write')
.collection(`${modelName}_events`)
.deleteMany({}),
services.mongodb
.db('datastore_write')
.collection(`${modelName}_snapshots`)
.deleteMany({}),
]);
};
setup.cleanModels = async function cleanModels(services: Services) {
await setup.cleanModel(services, 'internal_models');
};
setup.initModels = async function initModels(
services: Services,
modelConfigs: ModelConfig[] = [],
) {
const models = init(
{
models: [],
},
services,
);
await setup.cleanModels(services);
await Promise.all(
modelConfigs.map((modelConfig) =>
models.createModel({
is_enabled: true,
...modelConfig,
}),
),
);
await models.reload();
await Promise.all(
modelConfigs.map((modelConfig) => models.createModelIndexes(modelConfig)),
);
models.services = services;
services.models = models;
return models;
};
setup.teardownDb = async function teardownDb(mongodb: MongoDbConnector) {
try {
if (!mongodb.db('datastore_write')) {
return;
}
await mongodb.db('datastore_write').dropDatabase();
await mongodb.disconnect();
} catch (err) {
console.error(err);
}
};
setup.startApi = async function startApi(
_config: Partial<DatastoreConfig> = {},
modelConfigs: ModelConfig[] = [],
): Promise<
[
DatastoreConfig,
MongoDbConnector,
Models,
Express | null,
Server | null,
Datastore,
Services,
App,
]
> {
const app = await setup.build(_config);
app.services.models = await setup.initModels(app.services, modelConfigs);
try {
await app.services.models.initInternalModels();
} catch {
await app.services.models.initInternalModels();
}
await app.start();
const sdk = new Datastore({
baseUrl: `http://localhost:${app.services.config.port}`,
// Set to true to inspect SDK responses:
debug: process.env.DATASTORE_DEBUG === 'true',
token: 'token',
timeout: 300000,
telemetry: app.services.telemetry,
});
let res = { state: 'down' };
do {
const { data } = await sdk.heartbeat();
res = data;
} while (
res.state !== 'up' &&
(await new Promise((resolve) => setTimeout(resolve, 10)))
);
return [
app.services.config,
app.services.mongodb,
app.services.models,
app.express,
app.server,
sdk,
app.services,
app,
];
};
setup.stopApi = async function stopApi(app: App) {
await app.stop();
};
setup.restartApi = async function restartApi(
app: App,
_config: Partial<DatastoreConfig> = {},
) {
await setup.stopApi(app);
return setup.startApi(_config);
};
setup.inspect = (obj: any) => {
console.log(util.inspect(obj, false, null, true));
};