Skip to content

Commit

Permalink
feat(database): add import/export extensions logic to existing endpoi…
Browse files Browse the repository at this point in the history
…nts (#683)

* feat: add import/export extensions to existing endpoint

* fix: add more specific checks & filtering for importing only db extensions

* fix: correct expression

* fix(database): cms schema/extension exports/imports

This commit intruduces a fixes and refactors export/import logic as follows:
- Export endpoint returns schemas and extensions as separate fields
- Import endpoint optionally accepts an extensions field (backwards-compat)
- Import handler imports all schemas prior to extensions
- Import handler throws on non-CMS schema import attempts
- Import handler throws on non-existing schema extension attempts

---------

Co-authored-by: Konstantinos Feretos <[email protected]>
  • Loading branch information
ChrisPdgn and kon14 authored Nov 21, 2023
1 parent 88496f1 commit 22c1e10
Show file tree
Hide file tree
Showing 3 changed files with 99 additions and 35 deletions.
5 changes: 4 additions & 1 deletion modules/database/src/adapters/DatabaseAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -309,7 +309,10 @@ export abstract class DatabaseAdapter<T extends Schema> {
!baseSchema.modelOptions.conduit.permissions ||
!baseSchema.modelOptions.conduit.permissions.extendable
) {
throw new GrpcError(status.INVALID_ARGUMENT, 'Schema is not extendable');
throw new GrpcError(
status.INVALID_ARGUMENT,
`Schema ${schemaName} is not extendable`,
);
}
// Hacky input type conversion, clean up input flow types asap // @dirty-type-cast
const schema: ConduitDatabaseSchema = baseSchema as ConduitDatabaseSchema;
Expand Down
10 changes: 6 additions & 4 deletions modules/database/src/admin/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,24 +59,26 @@ export class AdminHandlers {
{
path: '/schemas/export',
action: ConduitRouteActions.GET,
description: `Export custom schemas.`,
description: `Exports CMS schemas and extensions. Only returns database-owned entries.`,
},
new ConduitRouteReturnDefinition('ExportSchemas', {
schemas: [ConduitJson.Required],
extensions: [ConduitJson.Required],
}),
this.schemaAdmin.exportCustomSchemas.bind(this.schemaAdmin),
this.schemaAdmin.exportSchemas.bind(this.schemaAdmin),
);
this.routingManager.route(
{
path: '/schemas/import',
action: ConduitRouteActions.POST,
description: `Import custom schemas.`,
description: `Imports CMS schemas and extensions. Doesn't drop non-specified extensions.`,
bodyParams: {
schemas: { type: [TYPE.JSON], required: true },
extensions: { type: [TYPE.JSON], required: false },
},
},
new ConduitRouteReturnDefinition('ImportSchemas', 'String'),
this.schemaAdmin.importCustomSchemas.bind(this.schemaAdmin),
this.schemaAdmin.importSchemas.bind(this.schemaAdmin),
);
this.routingManager.route(
{
Expand Down
119 changes: 89 additions & 30 deletions modules/database/src/admin/schema.admin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,26 @@ import { CustomEndpointController } from '../controllers/customEndpoints/customE
import { DatabaseAdapter } from '../adapters/DatabaseAdapter';
import { MongooseSchema } from '../adapters/mongoose-adapter/MongooseSchema';
import { SequelizeSchema } from '../adapters/sequelize-adapter/SequelizeSchema';
import { _ConduitSchema, ParsedQuery } from '../interfaces';
import {
_ConduitSchema,
ConduitDatabaseSchema,
DeclaredSchemaExtension,
ParsedQuery,
} from '../interfaces';
import { SchemaConverter } from '../utils/SchemaConverter';
import { parseSortParam } from '../handlers/utils';
import escapeStringRegexp from 'escape-string-regexp';

type ExportedCmsSchema = Pick<
ConduitDatabaseSchema,
'name' | 'fields' | 'modelOptions' | 'ownerModule' | 'collectionName'
>;

type ExportedCmsExtension = {
schemaName: string;
extension: Omit<DeclaredSchemaExtension, 'ownerModule'>;
};

export class SchemaAdmin {
constructor(
private readonly grpcSdk: ConduitGrpcSdk,
Expand All @@ -26,42 +41,81 @@ export class SchemaAdmin {
private readonly customEndpointController: CustomEndpointController,
) {}

async exportCustomSchemas(): Promise<UnparsedRouterResponse> {
return await this.database
async exportSchemas(): Promise<UnparsedRouterResponse> {
const cmsSchemas: ExportedCmsSchema[] = await this.database
.getSchemaModel('_DeclaredSchema')
.model.findMany(
{ 'modelOptions.conduit.cms.enabled': true },
{
select:
'name parentSchema fields extensions modelOptions ownerModule collectionName',
select: 'name fields modelOptions ownerModule collectionName',
sort: {
updatedAt: 1,
},
},
)
.then(r => {
return { schemas: r };
});
);
const cndSchemas: Pick<ConduitDatabaseSchema, 'name' | 'extensions'>[] =
await this.database.getSchemaModel('_DeclaredSchema').model.findMany(
{
$and: [
{ 'modelOptions.conduit.cms': { $exists: false } },
{ 'modelOptions.conduit.permissions.extendable': true },
{ extensions: { $exists: true } },
{
$or: [
{ parentSchema: { $exists: false } },
{ parentSchema: { $eq: null } },
{ parentSchema: { $eq: '' } },
],
},
],
},
{
select: 'name extensions',
sort: {
updatedAt: 1,
},
},
);
const dbExtensions = cndSchemas.flatMap(schema => {
const dbExtension = schema.extensions.find(ext => ext.ownerModule === 'database');
if (!dbExtension) return [];
const { extensions: _, name: schemaName, ...rest } = schema;
const { ownerModule: __, ...extension } = dbExtension;
return { ...rest, schemaName, extension };
});
return {
schemas: cmsSchemas,
extensions: dbExtensions as ExportedCmsExtension[],
};
}

async importCustomSchemas(call: ParsedRouterRequest): Promise<UnparsedRouterResponse> {
const schemas = call.request.params.schemas;
async importSchemas(call: ParsedRouterRequest): Promise<UnparsedRouterResponse> {
const { schemas, extensions } = call.request.params as {
schemas: ExportedCmsSchema[];
extensions?: ExportedCmsExtension[];
};
const targetSchemas = [
...schemas.map(s => s.name),
...(extensions?.map(e => e.schemaName) ?? []),
];
const models: ConduitDatabaseSchema[] = await this.database
.getSchemaModel('_DeclaredSchema')
.model.findMany({ name: { $in: targetSchemas } });
for (const schema of schemas) {
const existingSchema = await this.database
.getSchemaModel('_DeclaredSchema')
.model.findOne({ name: schema.name });
const operation = isNil(existingSchema) ? 'create' : 'update';
const imported = schema.modelOptions.conduit.imported === true;
const model = models.find(m => m.name === schema.name);
if (model && model.ownerModule !== 'database') {
throw new GrpcError(
status.PERMISSION_DENIED,
`Cannot import '${schema.name}'. Schema already owned by a module!`,
);
}
const operation = model ? 'update' : 'create';
const imported = !!(model ?? schema).modelOptions.conduit?.imported;
const modelOptions = SchemaConverter.getModelOptions({
cmsSchema: true,
existingModelOptions: schema.modelOptions,
existingModelOptions: (model ?? schema).modelOptions,
importedSchema: imported,
});
try {
validateSchemaInput(schema.name, schema.fields, modelOptions);
} catch (err: unknown) {
throw new GrpcError(status.INTERNAL, (err as Error).message);
}
await this.schemaController.createSchema(
new ConduitSchema(
schema.name,
Expand All @@ -72,15 +126,20 @@ export class SchemaAdmin {
operation,
imported,
);
if (schema.extensions.length > 0) {
for (const extension of schema.extensions) {
await this.database.setSchemaExtension(
schema.name,
extension.owner,
extension.fields,
);
}
}
for (const ext of extensions ?? []) {
const model = models.find(m => m.name === ext.schemaName);
if (!model) {
throw new GrpcError(
status.FAILED_PRECONDITION,
`Cannot create an extension for '${ext.schemaName}'. Schema doesn't exist!`,
);
}
await this.database.setSchemaExtension(
ext.schemaName,
'database',
ext.extension.fields,
);
}
return 'Schemas imported successfully';
}
Expand Down

0 comments on commit 22c1e10

Please sign in to comment.