Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: Extension loader #25

Merged
merged 20 commits into from
Aug 17, 2022
Merged

Feature: Extension loader #25

merged 20 commits into from
Aug 17, 2022

Conversation

oscar60310
Copy link
Contributor

@oscar60310 oscar60310 commented Jul 28, 2022

Description

This PR implements the design of Unite extension loader: Create a central extension loader to import internal/external extensions and manage their config.

Design

Super classes of extensions

  1. We are going to create a super class for each extension type, these classes located at packages/<package-name>/src/models/extensions.

    📦packages
     ┣ 📂build
     ┃ ┣ 📂src
     ┃ ┃ ┣ 📂models
     ┃ ┃ ┃ ┣ 📂extensions
     ┃ ┃ ┃ ┃ ┣ 📜schemaReader.ts
     ┃ ┃ ┃ ┃ ┗ 📜specGenerator.ts
     ┣ 📂core
     ┃ ┣ 📂src
     ┃ ┃ ┣ 📂models
     ┃ ┃ ┃ ┣ 📂extensions
     ┃ ┃ ┃ ┃ ┣ 📜codeLoader.ts
     ┃ ┃ ┃ ┃ ┣ 📜dataSource.ts
     ┃ ┃ ┃ ┃ ┣ 📜filterBuilder.ts
     ┃ ┃ ┃ ┃ ┣ 📜filterRunner.ts
     ┃ ┃ ┃ ┃ ┣ 📜inputValidator.ts
     ┃ ┃ ┃ ┃ ┣ 📜persistentStore.ts
     ┃ ┃ ┃ ┃ ┣ 📜serializer.ts
     ┃ ┃ ┃ ┃ ┣ 📜tagBuilder.ts
     ┃ ┃ ┃ ┃ ┣ 📜tagRunner.ts
     ┃ ┃ ┃ ┃ ┣ 📜templateEngine.ts
     ┃ ┃ ┃ ┃ ┗ 📜templateProvider.ts
     ┣ 📂serve
     ┃ ┣ 📂src
     ┃ ┃ ┣ 📂models
     ┃ ┃ ┃ ┣ 📂extensions
     ┃ ┃ ┃ ┃ ┣ 📜responseFormatter.ts
     ┃ ┃ ┃ ┃ ┗ 📜routeMiddleware.ts
    

    These extension types have been created.

    • Template Provider: Provide the content of .sql files.
    • TagBuilder: Parse template, create custom tags {% someTag %}, traverse AST, generate metadata…
    • TagRunner: Runtime code of custom tags.
    • FilterBuilder: Create custom filter {{ [1,2,3] | customFIlter }}, taverse AST, generate metadata…
    • FilterRunner: Runtime code of custom filter.
    • CompileTimeExtension: Traverse AST, generate metadata…
    • RuntimeExtension
    • SchemaReader: Provide the content of .yaml files.
    • PersistentStore: Persistent store of Vulcan artifacts.
    • Serializer: Vulcan artifacts serializer.
    • SpecGenerator: Vulcan schema → Spec (e.g. OAS)
    • CodeLoader: Load compiled code and provide js functions while needed.
    • Validator: Validate input parameters.
    • ResponseFormatter: Serialize response (e.g. CSV, JSON …)
    • Server Koa Middleware
    • DataSource: Warehouse drivers.
  2. Every superclass must be decorated by VulcanExtension decorator which indicates the identifier of this type of extension.

    @VulcanExtension(TYPES.Extension_PersistentStore)
    export abstract class PersistentStore<C = any> extends ExtensionBase<C> {
      abstract save(data: Buffer): Promise<void>;
      abstract load(): Promise<Buffer>;
    }
  3. Internal extension must be decorated by @VulcanInternalExtension decorator which indicates the module name of this extension. We can use getConfig() to retrieve module-based config, if you don't need module-based config, you can leave the module name empty.

    @VulcanInternalExtension('module-name')
    export class JSONSerializer<T> extends Serializer<T> {
      public serialize(data: T): Buffer {
        return Buffer.from(JSON.stringify(data), 'utf-8');
      }
    
      public deserialize(raw: Buffer): T {
        return JSON.parse(raw.toString('utf-8')) as T;
      }
    }

Vulcan Extension Id

In some cases, we need a name switch among extensions, e.g. chose the serializer for artifact builder:

export class BinaraySerializer<T> extends Serializer<T> {
   xxx
}
artifact:
   serializer: 'bin' # json -> bin

This name often needs to be obtained before it is initialized, so I use a decorator @VulcanExtensionId to record it, we can call getExtensionId() or get the metadata with key EXTENSION_IDENTIFIER_METADATA_KEY to get the name. I replace all name properties with this decorator including formaters, validators ... etc.

Use same container with core package

We used another container and use core container as its parent, it might cause some issues because we'll load external extensions at core package but inject dependencies from build or serve package. So I decided to use the same container in this PR.

# before
const coreContainer = new CoreContainer();
await coreContainer.load(options);
this.inversifyContainer.parent = coreContainer.getInversifyContainer();

# after
this.coreContainer = new CoreContainer();
await this.coreContainer.load(options);
this.inversifyContainer = this.coreContainer.getInversifyContainer();

Integration testing

For testing across packages, we create a package with the name integration-testing, it uses pg-mem as a mock data warehouse

@oscar60310 oscar60310 force-pushed the feature/extension-loader branch 8 times, most recently from 35df3a6 to 4ebc102 Compare August 3, 2022 06:42
- Add extension loader and its module.
- Update import path from ‘@vulcan-sql/core/containers’ to '@vulcan-sql/core/types' to prevent improper dependencies.
- Update extension config from string array to object to receive module aliases.
- Update validatorLoader to receive imported classes.
- Migrate all core extensions to new style.
@oscar60310 oscar60310 force-pushed the feature/extension-loader branch 7 times, most recently from 90910fe to b4138de Compare August 4, 2022 07:21
oscar60310 and others added 6 commits August 4, 2022 16:30
- Use same container with core package instead of creating a new one (for external extensions).
- Add two types of extensions: route middleware and formatter.
- Bind Vulcan application to container.
- Using centralized extension loader, remove the old loader in serve package.
- Use new super class for all extensions.

Co-authored-by: kokokuo <v6610688@gmail.com>
@oscar60310 oscar60310 force-pushed the feature/extension-loader branch 3 times, most recently from c18bfe5 to 91d34ea Compare August 5, 2022 04:19
- Add @VulcanExtensionId decorator
- Expose templateProvider, serializer, and persistenStore interfaces.
- Load dynamic named extensions.
- Change provider configuration from enum to string to support external
extensions.
@oscar60310 oscar60310 force-pushed the feature/extension-loader branch 4 times, most recently from 8fc9055 to 3bc3d81 Compare August 5, 2022 07:44
@oscar60310 oscar60310 force-pushed the feature/extension-loader branch from da6d02c to b65d1dd Compare August 5, 2022 08:55
@oscar60310 oscar60310 force-pushed the feature/extension-loader branch from 0fab9b5 to b2cfd97 Compare August 8, 2022 07:46
@oscar60310 oscar60310 changed the title [WIP] Feature: Extension loader Feature: Extension loader Aug 8, 2022
@oscar60310 oscar60310 requested a review from kokokuo August 8, 2022 07:46
@oscar60310 oscar60310 marked this pull request as ready for review August 8, 2022 07:46
Copy link
Contributor

@kokokuo kokokuo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Extremely powerful code changed 👍

Besides some comment suggestions, and a few more questions, all part is LGTM 💯

bind(TYPES.ExecutorOptions).to(ExecutorOptions);

// Data source
bind(TYPES.Factory_DataSource).toAutoNamedFactory(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just curious, why here not use generic typing annotation for `

bind<interfaces.AutoNamedFactory<DataSource>>(TYPES.Factory_DataSource).toAutoNamedFactory(TYPES.Extension_DataSource);

like PersistentStore in artifactBuilderModule ?

bind<interfaces.AutoNamedFactory<PersistentStore>>(
      TYPES.Factory_PersistentStore
    ).toAutoNamedFactory<PersistentStore>(TYPES.Extension_PersistentStore);

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The generic types here only take effect while binding, we have to specify these interfaces again while resolving: containter.get<SomeInterface>(TYPES.XXXX) , so I didn't define them while binding. But for keeping consistency, I'll add them back~

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for @oscar60310 solving my question :D

).toAutoNamedFactory<TemplateProvider>(TYPES.TemplateProvider);
).toAutoNamedFactory<TemplateProvider>(TYPES.Extension_TemplateProvider);

if (options.provider) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems the provider does not get, according to template engine source code, it will throw an error when calling compile, right?

If so, maybe you could add a simple comment to let others know the reason why the template provider not need to bind to prevent guessing the reason or tracing to much code deeply

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, I'll add some comments at container module.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for @oscar60310 !

@@ -60,8 +61,19 @@ export const templateEngineModule = (
.whenTargetNamed('compileTime');

// Loader
bind<ICodeLoader>(TYPES.CompilerLoader)
.to(InMemoryCodeLoader)
bind(TYPES.Factory_CompilerLoader).toAutoNamedFactory(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above DataSource auto factory question.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've add types definitions for all binding~

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for @oscar60310 !

@@ -0,0 +1,3 @@
export interface IExecutorOptions {
type?: string;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do the type used for? the data source type used for creating different data sources?
If so maybe you could add a comment or name it, because type is a universal word.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it indicated the driver to use while sending queries, single driver design might be changed in the future. The property name here affects the user config, that is, "executor.type" v.s. "executor.executorType". So I think it would be better to use the name "type" and add some comments to the interface.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for @oscar60310 !

export interface IArtifactBuilderOptions {
provider: PersistentStoreType;
serializer: SerializerType;
provider: string;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggest adding a comment on the provider to notice it;s PersistentStoreType, because we could only know it isstring type,and provider could have mutiple meaning, also could give some sample for hint.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll add comments for them.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks alot !

@@ -0,0 +1,4 @@
export interface IDocumentGeneratorOptions {
specs?: string[];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you could add some comment to expose what the data provide to spec and give some sample, e.g: oas3

Copy link
Contributor Author

@oscar60310 oscar60310 Aug 17, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've set the built-in enum as a union type, it might help us to know more about the property.

export enum DocumentGeneratorSpec {
  oas3 = 'oas3',
}

export interface IDocumentGeneratorOptions {
  specs?: (string | DocumentGeneratorSpec)[];
  folderPath: string;
}

BTW, it takes no effect when using the interface:
image

provider: PersistentStoreType;
serializer: SerializerType;
provider: string;
serializer: string;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as provider, the serializer is string type and there is no sample for expose what value we should provide, so we may could add some sample or some comment, e.g: file format type for serializer ... so on

@@ -27,7 +27,7 @@ export const routeGeneratorModule = () =>
.to(PaginationTransformer)
.inSingletonScope();

// Roue Generator
// Route Generator
bind<RouteGenerator>(TYPES.RouteGenerator)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for modifying my typo !!

@@ -17,7 +17,7 @@ export const routeGeneratorModule = () =>
.to(RequestTransformer)
.inSingletonScope();

// Request Transformer
// Request Validator
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for modifying my typo !!


export const applicationModule = () =>
new AsyncContainerModule(async (bind) => {
bind(TYPES.VulcanApplication).to(VulcanApplication);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just curious for bind, seems the interface/class no need to indicate explicitly e.g: bind<VulcanApplication>(TYPES.VulcanApplication).to(VulcanApplication); ?

In the beginning, I think the only interface should define explicitly in bind, but seems classes could also indicate class type in bind, e.g schemaPaser module in build package:

bind<SchemaParser>(TYPES.SchemaParser).to(SchemaParser).inSingletonScope();

but sometimes you would define indicate class type, e.g: documentGenerator module in build package

bind(TYPES.DocumentGenerator).to(DocumentGenerator);

And also sometimes, even the interface, won't indicate directly when using bind, but sometimes use it:

// schemaPaser module  in build package
 bind<ISchemaParserOptions>(TYPES.SchemaParserInputOptions).toConstantValue(
      options || ({} as any)
    );

// executor module
bind(TYPES.ExecutorInputOptions).toConstantValue(options);
bind(TYPES.ExecutorOptions).to(ExecutorOptions);

Although I know its just to indicate the used class/interface for generic function/method for typescript feature, and also could let vscode could IntelliSense know, but still curious does it exist any reason inversify.js needed to indicate interface /class type explicitly that I missed ?

Copy link
Contributor Author

@oscar60310 oscar60310 Aug 17, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Although I know its just to indicate the used class/interface for generic function/method for typescript feature, and also could let vscode could IntelliSense know, but still curious does it exist any reason inversify.js needed to indicate interface /class type explicitly that I missed ?

No, Inversify doesn't need the generics, it only cares about the identifiers. The generics here just make sure we bind the correct target on the identifier.

I've added generics for all bindings with interfaces/abstract classes when possible, but in some cases (like VulcanApplication) we need to use the class directly as the generics type.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for solving my multiple similar questions again and again!

@oscar60310
Copy link
Contributor Author

Hi @kokokuo , all issues have been fixed vai new commits, thanks a lot!

@kokokuo
Copy link
Contributor

kokokuo commented Aug 17, 2022

Thanks for fixing the suggestion and relying my questions !

@kokokuo kokokuo merged commit 66e58aa into develop Aug 17, 2022
@kokokuo kokokuo deleted the feature/extension-loader branch August 17, 2022 06:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants