diff --git a/.gitignore b/.gitignore index e7a5575..01e49cb 100644 --- a/.gitignore +++ b/.gitignore @@ -41,4 +41,3 @@ nbproject *.sublime-* *.atom-* .tern-* -jsconfig.json diff --git a/jsconfig.json b/jsconfig.json new file mode 100644 index 0000000..900a292 --- /dev/null +++ b/jsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "charset": "utf8", + "module": "commonjs", + "moduleResolution": "node", + "target": "esnext", + "noEmit": true, + "checkJs": false, + "noImplicitAny": true + }, + "exclude": [ + "node_modules", + "**/node_modules/*" + ], + "typeAcquisition": { + "enable": true + } +} diff --git a/packages/action/src/index.d.ts b/packages/action/src/index.d.ts new file mode 100644 index 0000000..702f180 --- /dev/null +++ b/packages/action/src/index.d.ts @@ -0,0 +1,25 @@ +declare module '@atlas.js/action' { + import Component from '@atlas.js/component' + + /** + * Use this class to implement custom high-level "actions" or "operations" in your code + * + * An action could be thought of as a controller of sorts in the MVC architecture. The general + * idea of actions is that each class represents a group of releated operations which yield a + * specific end result. They should be callable from anywhere and should not be tied to a + * particular interface (ie. an HTTP server or a REPL session) - in other words, the idea is to + * make them as reusable as possible. + * + * Actions can be completely standalone or they might call other, lower-level actions or interact + * with services to manipulate external resources. Generally actions are called from some kind of + * externally available entry point, like an HTTP interface, a REPL session, a custom socket etc. + * + * Actions are intended to encapsulate "business-specific" or re-usable code/behaviour. + * + * @abstract + */ + export default abstract class Action extends Component { + /** @private */ + static type: 'action' + } +} diff --git a/packages/action/src/index.mjs b/packages/action/src/index.mjs index 6afd0f9..5b6074d 100644 --- a/packages/action/src/index.mjs +++ b/packages/action/src/index.mjs @@ -1,5 +1,22 @@ import Component from '@atlas.js/component' +/** + * Use this class to implement custom high-level "actions" or "operations" in your code + * + * An action could be thought of as a controller of sorts in the MVC architecture. The general + * idea of actions is that each class represents a group of releated operations which yield a + * specific end result. They should be callable from anywhere and should not be tied to a + * particular interface (ie. an HTTP server or a REPL session) - in other words, the idea is to + * make them as reusable as possible. + * + * Actions can be completely standalone or they might call other, lower-level actions or interact + * with services to manipulate external resources. Generally actions are called from some kind of + * externally available entry point, like an HTTP interface, a REPL session, a custom socket etc. + * + * Actions are intended to encapsulate "business-specific" or re-usable code/behaviour. + * + * @abstract + */ class Action extends Component { static type = 'action' } diff --git a/packages/atlas/package-lock.json b/packages/atlas/package-lock.json index d31172f..7914501 100644 --- a/packages/atlas/package-lock.json +++ b/packages/atlas/package-lock.json @@ -4,6 +4,25 @@ "lockfileVersion": 1, "requires": true, "dependencies": { + "@types/events": { + "version": "1.2.0", + "resolved": "http://registry.npmjs.org/@types/events/-/events-1.2.0.tgz", + "integrity": "sha512-KEIlhXnIutzKwRbQkGWb/I4HFqBuUykAdHgDED6xqwXJfONCjF5VoE0cXEiurh3XauygxzeDzgtXUqvLkxFzzA==" + }, + "@types/node": { + "version": "10.12.12", + "resolved": "https://registry.npmjs.org/@types/node/-/node-10.12.12.tgz", + "integrity": "sha512-Pr+6JRiKkfsFvmU/LK68oBRCQeEg36TyAbPhc2xpez24OOZZCuoIhWGTd39VZy6nGafSbxzGouFPTFD/rR1A0A==" + }, + "@types/pino": { + "version": "4.16.1", + "resolved": "https://registry.npmjs.org/@types/pino/-/pino-4.16.1.tgz", + "integrity": "sha512-uYEhZ3jsuiYFsPcR34fbxVlrqzqphc+QQ3fU4rWR6PXH8ka2TKvPBjtkNqj8oBHouVGf4GCRfyPb7FG2TEtPZA==", + "requires": { + "@types/events": "*", + "@types/node": "*" + } + }, "ajv": { "version": "6.5.5", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.5.5.tgz", diff --git a/packages/atlas/package.json b/packages/atlas/package.json index f133035..fb327ac 100644 --- a/packages/atlas/package.json +++ b/packages/atlas/package.json @@ -10,6 +10,7 @@ "@atlas.js/errors": "^0.2.1", "@atlas.js/hook": "^2.0.1", "@atlas.js/service": "^1.1.1", + "@types/pino": "^4.7.0", "ajv": "^6.5.0", "ajv-keywords": "^3.2.0", "lodash": "^4.17.4", diff --git a/packages/atlas/src/atlas.d.ts b/packages/atlas/src/atlas.d.ts new file mode 100644 index 0000000..845cbf2 --- /dev/null +++ b/packages/atlas/src/atlas.d.ts @@ -0,0 +1,271 @@ +import * as Ajv from 'ajv' +import * as Pino from 'pino' +import Component from '@atlas.js/component' + +declare interface ComponentConstructor { + new (): Component +} + +/** Input options for `Atlas.init()` */ +declare interface InitOptions extends Options { + /** Path to a module from which service components should be loaded. Default: `services` */ + services?: string + /** Path to a module from which action components should be loaded. Default: `actions` */ + actions?: string + /** Path to a module from which hook components should be loaded. Default: `hooks` */ + hooks?: string + /** + * Path to a module from which component configuration should be loaded (or the configuration + * object itself). Default: `config` + */ + config?: Options["config"] +} + +/** Input options for `Atlas.bootstrap()` */ +declare interface BootstrapOptions extends Options { + /** + * All services to be added to Atlas. The key is the alias for the component and the value is + * the Component class itself (ie. `new Component()` will be called on the value). + */ + services: { [key: string]: ComponentConstructor } + /** + * All actions to be added to Atlas. The key is the alias for the component and the value is + * the Component class itself (ie. `new Component()` will be called on the value). + */ + actions: { [key: string]: ComponentConstructor } + /** + * All hooks to be added to Atlas. The key is the alias for the component and the value is + * the Component class itself (ie. `new Component()` will be called on the value). + */ + hooks: { [key: string]: ComponentConstructor } +} + +/** + * Input options for the second argument of `atlas.action()`, `atlas.service()`, `atlas.hook()` + */ +declare interface ComponentOptions { + /** + * If this component requires other components, you must specify their aliases you chose for + * them here. The key is the component name which is used by this component, and the value + * is the alias you chose for that compoennt in this instance of Atlas. + */ + aliases: { + [name: string]: string + } +} + +/** Input options for the second argument of `atlas.require()` */ +declare interface RequireOptions { + /** If true, do not throw if the module does not exist */ + optional: boolean, + /** If true, prefer the ES modules' `default` export over named or CommonJS exports */ + normalise: boolean, + /** + * If true, will try to load the module without resolving the specified location to the project + * root (it will load the module using standard Node's mechanism). + */ + absolute: boolean +} + +/** Input options for the `new Atlas()` constructor */ +declare interface Options { + /** Environment to run Atlas in. Default: value of `process.env.NODE_ENV` */ + env: string + /** Absolute path to folder structure where Atlas can expect its components to be defined */ + root: string + /** + * Configuration options for Atlas and all components + * + * If string is provided, it will be used as path relative to `root` and required. The resulting + * object will be used as the configuration object. If an object is provided, it will be used + * as-is. Default: `'config'` + */ + config?: Config | string +} + +/** + * Configuration object which Atlas accepts. Child values are either passed to Atlas directly or + * to individual components. The objects are delivered to components by checking the configuration + * keys against the component's associated alias. + */ +declare interface Config { + /** Configuration options for Atlas itself */ + atlas?: Atlas.Config + + /** + * Configuration options for services. + * + * The key must be the alias used when adding the service to Atlas, and the value will be + * provided to the component as its configuration. + */ + services: { [key: string]: any } + /** + * Configuration options for actions. + * + * The key must be the alias used when adding the action to Atlas, and the value will be + * provided to the component as its configuration. + */ + actions: { [key: string]: any } + /** + * Configuration options for hooks. + * + * The key must be the alias used when adding the hook to Atlas, and the value will be + * provided to the component as its configuration. + */ + hooks: { [key: string]: any } +} + + +/** + * The main point of interaction with Atlas. + */ +declare class Atlas { + /** + * Default values for configuration options which Atlas accepts. + */ + static readonly defaults: Atlas.Config + + /** Current execution environment (usually mirrors `process.env.NODE_ENV` unless overriden) */ + readonly env: string + + /** + * The root folder where all other paths should be relative to + * + * It is recommended that you set this to the project's root directory. + */ + readonly root: string + + /** Is this instance in a prepared state? */ + readonly prepared: boolean + + /** Is this instance in a started state? */ + readonly started: boolean + + /** Configuration for Atlas and all associated components, as passed in to the constructor */ + readonly config: Config + + /** All services added to this instance */ + readonly services: object + + /** All actions added to this instance */ + readonly actions: object + + /** An instance of Ajv used to validate component configuration */ + validator: Ajv.Ajv + + /** Logger used throughout Atlas and its components */ + log: Pino.Logger + + /** + * Initialise Atlas instance from the module paths provided in the `options` object + * + * Use this method to quickly configure Atlas instance by simply telling it where your + * components live on the filesystem, and Atlas will load them from the given module locations + * and add them to the Atlas instance. + * + * This is the preferred way of initialising an Atlas instance. + */ + static init(options: InitOptions): Atlas + + /** + * Bootstrap the given Atlas instance with the provided modules + * + * Use this method to quickly set up the given Atlas instance to use the provided components. + * This is useful if you need to have multiple entry points to your program and some entry + * points should only use some components available. This is especially useful when implementing + * worker processes where you only need a subset of all available components. This method, while + * more verbose as Atlas.init(), still frees you from manually adding all the components by hand + * while providing greater flexibility as to which components will be used. + */ + static bootstrap(atlas: Atlas, options: BootstrapOptions): Atlas + + /** + * Create a new Atlas instance + * + * Generally you should not need to create an Atlas instance via its constructor and use either + * the `Atlas.init()` or `Atlas.bootstrap()` options which are more easy to use. Using the + * constructor is more verbose but provides greatest level of control over the initialisation. + * + * You will have to manually register each component to the instance. + */ + constructor(options: Options) + + /** + * Require a module by path, relative to the project root + * + * @param {string} location Location of the module to load, relative to `atlas.root` + * (unless `{ absolute: true }`) + * @param {RequireOptions} options Options which affect how the module will be loaded + */ + require(location: string, options?: RequireOptions): any + + /** + * Register a service into this instance of Atlas with the given alias + * + * @param {string} alias Alias to associate with this component + * @param {ComponentConstructor} Component The component class. It will be constructed + * using `new Component()` during startup. + * @param {ComponentOptions} options Options for the component + */ + service(alias: string, Component: ComponentConstructor, options: ComponentOptions): this + + /** + * Register a hook into this instance of Atlas with the given alias + * + * @param {string} alias Alias to associate with this component + * @param {ComponentConstructor} Component The component class. It will be constructed + * using `new Component()` during startup. + * @param {ComponentOptions} options Options for the component + */ + hook(alias: string, Component: ComponentConstructor, options: ComponentOptions): this + + /** + * Register an action into this instance of Atlas with the given alias + * + * @param {string} alias Alias to associate with this component + * @param {ComponentConstructor} Component The component class. It will be constructed + * using `new Component()` during startup. + * @param {ComponentOptions} options Options for the component + */ + action(alias: string, Component: ComponentConstructor, options: ComponentOptions): this + + /** + * Prepare all services and hooks for use + * + * Generally you should use `atlas.start()` instead to get your instance up and running. + * However, sometimes it is necessary to get all the services into a "get-ready" state before + * they start connecting to remote resources or doing any intensive I/O operations. + */ + prepare(): Promise + + /** + * Start alll services + * + * This puts all components into a fully functional state, with all connections established (if + * any) and ready for use once this function resolves. + */ + start(): Promise + + /** + * Stop all services, unregister all actions and hooks and unpublish any APIs exposed by them + * + * This puts the whole application into a state as it was before `atlas.prepare()` and/or + * `atlas.start()` was called. + */ + stop(): Promise +} + +declare namespace Atlas { + /** Configuration options for Atlas */ + export interface Config { + /** Configuration options for Atlas' pino logger */ + log?: Pino.LoggerOptions + /** Configuration options for Atlas' component configuration validator */ + validator?: Ajv.Options & { + /** Keywords to enable from the `ajv-keywords` package */ + keywords?: Array + } + } +} + +export = Atlas diff --git a/packages/atlas/src/atlas.mjs b/packages/atlas/src/atlas.mjs index 94c39b1..385fb15 100644 --- a/packages/atlas/src/atlas.mjs +++ b/packages/atlas/src/atlas.mjs @@ -107,7 +107,7 @@ class Atlas { * providing greater flexibility as to which components will be used. * * @param {Atlas} atlas The Atlas instance to bootstrap - * @param {Object} modules={} All the modules which should be added to Atlas + * @param {Object} modules? All the modules which should be added to Atlas * @param {Object} modules.actions Actions to add * @param {Object} modules.hooks Hooks to add * @param {Object} modules.services Services to add @@ -290,7 +290,7 @@ class Atlas { * Require a module by path, relative to the project root * * @param {String} location The module's location, relative to root - * @param {Object} options={} Options + * @param {Object} options? Options * @param {Boolean} options.optional If true, will not throw if the module does not exist * @param {Boolean} options.normalise If true, it will prefer the ES modules' default export * over named exports or the CommonJS exports @@ -315,7 +315,7 @@ class Atlas { } /** - * Register a service into this atlas at given alias + * Register a service into this instance of Atlas with the given alias * * @param {String} alias The alias for the service - it will be used for exposing * the service's API on the atlas.services object and for @@ -337,7 +337,7 @@ class Atlas { } /** - * Register a hook into this atlas using given alias + * Register a hook into this instance of Atlas with the given alias * * @param {String} alias The alias for the hook - it will be used for passing * configuration data to it @@ -358,7 +358,7 @@ class Atlas { } /** - * Register an action into this atlas at given alias + * Register an action into this instance of Atlas with the given alias * * @param {String} alias The alias for the action - it will be used for exposing * the action's API on the atlas.actions object and for diff --git a/packages/atlas/src/index.d.ts b/packages/atlas/src/index.d.ts new file mode 100644 index 0000000..18ada90 --- /dev/null +++ b/packages/atlas/src/index.d.ts @@ -0,0 +1,16 @@ +import * as Atlas from './atlas' + +declare module '@atlas.js/atlas' { + import Action from '@atlas.js/action' + import Service from '@atlas.js/service' + import Hook from '@atlas.js/hook' + import * as errors from '@atlas.js/errors' + + export { + Atlas, + Action, + Service, + Hook, + errors, + } +} diff --git a/packages/atlas/src/private/dispatch.mjs b/packages/atlas/src/private/dispatch.mjs index 5629645..6f8872f 100644 --- a/packages/atlas/src/private/dispatch.mjs +++ b/packages/atlas/src/private/dispatch.mjs @@ -1,7 +1,7 @@ /** * Dispatch an event to hooks * - * This function takes variable number of events to be dispatched to hooks + * Dispatch an event to all hooks listening on this component * * @private * @param {String} event The event's name diff --git a/packages/aws/src/index.d.ts b/packages/aws/src/index.d.ts new file mode 100644 index 0000000..e1511c1 --- /dev/null +++ b/packages/aws/src/index.d.ts @@ -0,0 +1,56 @@ +declare module '@atlas.js/aws' { + import * as AWS from 'aws-sdk' + import { GlobalConfigInstance } from 'aws-sdk/lib/config' + import * as AWSClients from 'aws-sdk/clients/all' + import AtlasService from '@atlas.js/service' + + type AWSServiceApi = { + [key: string]: object + } + + /** + * Load and set up AWS services for use from within Atlas + * + * This class loads the AWS clients for which it will find a configuration object (even if it is + * empty). This is done to reduce memory footprint (the AWS SDK it huge!), so only specified + * clients will be loaded. + */ + class Service extends AtlasService { + /** Runtime configuration values */ + config: Service.Config + + /** + * Prepare an AWS client instance + * + * ⚠️ Note that the client will only include services for which a key in the configuration has + * been defined. + */ + prepare(): Promise + start(service: AWSServiceApi): Promise + stop(service: AWSServiceApi): Promise + } + + namespace Service { + /** Configuration schema available to this service */ + interface Config { + /** Global configuration options which will be applied into every service */ + globals: GlobalConfigInstance + + /** + * Configuration options which will be applied only to specific services + * + * ⚠️ Note that only services for which a configuration object has been defined will be made + * available, so make sure you declare at least an empty object here if you want to use that + * service. + */ + services?: { + [key: string]: GlobalConfigInstance + } + } + } + + export { + Service, + AWS, + } +} diff --git a/packages/braintree/src/index.d.ts b/packages/braintree/src/index.d.ts new file mode 100644 index 0000000..9e25c4d --- /dev/null +++ b/packages/braintree/src/index.d.ts @@ -0,0 +1,41 @@ +declare module '@atlas.js/braintree' { + import AtlasService from '@atlas.js/service' + import { ServiceApi } from '@atlas.js/service' + + // @TODO: Braintree does not have typings. Watch for any changes to that situation. 👀 + type Braintree = object + + class Service extends AtlasService { + /** Runtime configuration values */ + config: Service.Config + + prepare(): Promise + start(service: ServiceApi): Promise + stop(service: ServiceApi): Promise + } + + namespace Service { + /** Configuration schema available to this service */ + interface Config { + /** + * Environment to connect to + * + * Available values: + * + * ```js + * import { Braintree } from '@atlas.js/braintree' + * Braintree.Environment.{choose one} + * ``` + */ + environment: object + merchantId: string + publicKey: string + privateKey: string + } + } + + export { + Service, + Braintree, + } +} diff --git a/packages/component/package-lock.json b/packages/component/package-lock.json new file mode 100644 index 0000000..ebb3bf0 --- /dev/null +++ b/packages/component/package-lock.json @@ -0,0 +1,27 @@ +{ + "name": "@atlas.js/component", + "version": "2.1.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@types/events": { + "version": "1.2.0", + "resolved": "http://registry.npmjs.org/@types/events/-/events-1.2.0.tgz", + "integrity": "sha512-KEIlhXnIutzKwRbQkGWb/I4HFqBuUykAdHgDED6xqwXJfONCjF5VoE0cXEiurh3XauygxzeDzgtXUqvLkxFzzA==" + }, + "@types/node": { + "version": "10.12.12", + "resolved": "https://registry.npmjs.org/@types/node/-/node-10.12.12.tgz", + "integrity": "sha512-Pr+6JRiKkfsFvmU/LK68oBRCQeEg36TyAbPhc2xpez24OOZZCuoIhWGTd39VZy6nGafSbxzGouFPTFD/rR1A0A==" + }, + "@types/pino": { + "version": "4.16.1", + "resolved": "https://registry.npmjs.org/@types/pino/-/pino-4.16.1.tgz", + "integrity": "sha512-uYEhZ3jsuiYFsPcR34fbxVlrqzqphc+QQ3fU4rWR6PXH8ka2TKvPBjtkNqj8oBHouVGf4GCRfyPb7FG2TEtPZA==", + "requires": { + "@types/events": "*", + "@types/node": "*" + } + } + } +} diff --git a/packages/component/package.json b/packages/component/package.json index 1277e15..3007aec 100644 --- a/packages/component/package.json +++ b/packages/component/package.json @@ -5,6 +5,9 @@ "author": "Robert Rossmann ", "bugs": "https://github.com/strvcom/atlas.js/issues", "contributors": [], + "dependencies": { + "@types/pino": "^4.7.0" + }, "engines": { "node": ">=8.3.0", "npm": "^5.3.0" diff --git a/packages/component/src/index.d.ts b/packages/component/src/index.d.ts new file mode 100644 index 0000000..b202622 --- /dev/null +++ b/packages/component/src/index.d.ts @@ -0,0 +1,66 @@ +declare module '@atlas.js/component' { + import * as Pino from 'pino' + import { Atlas } from '@atlas.js/atlas' + + type ComponentName = string + + /** + * A base class implementing common behaviour for Service, Action and Hook subclasses + * + * You should not use this class directly and instead use either `Service`, `Action` or `Hook` + * subclasses. + * + * @abstract + */ + export default abstract class Component { + /** + * By default, a component is marked as "public", ie. accessible on the Atlas instance. With + * this set to `true` you will only be able to access this component only via inter-component + * resolution mechanism (`this.component(name)`). + * + * This is useful to hide various low-level or "glue" components from the public API surface. + */ + static internal: boolean + + /** + * Default configuration for this component + * + * @deprecated Use the `config` static property (JSON schema) to describe your configuration + */ + static defaults: object + + /** JSON Schema describing this component's configuration values */ + static config: object + + /** Array of component aliases which this component consumes/requires */ + static requires: Array + + /** + * A child log created specifically for this component to easily distinguish log entries coming + * from this component + */ + log: Pino.Logger + + /** The Atlas instance which is currently managing this component */ + atlas: Atlas + + /** Runtime configuration values as received from the user, with defaults applied */ + config: object + + /** + * Get the instance of a component + * + * @param componentName The name of the component to retrieve, as defined in this + * component's `static requires` array. + */ + component(componentName: ComponentName): any + + /** + * Dispatch an event to all hooks listening on this component + * + * @param event The event to dispatch (an arbitrary string) + * @param subject The thing to send to the hooks along with this event as an argument + */ + dispatch(event: string, subject: any): Promise + } +} diff --git a/packages/errors/src/index.d.ts b/packages/errors/src/index.d.ts index ee1bac7..b6fef19 100644 --- a/packages/errors/src/index.d.ts +++ b/packages/errors/src/index.d.ts @@ -1,12 +1,12 @@ declare module '@atlas.js/errors' { - import ajv from 'ajv' + import { ErrorObject } from 'ajv' export class FrameworkError extends Error { } export class ValidationError extends FrameworkError { - constructor(errors: Array) + constructor(errors: Array) /** Errors returned by Ajv */ - errors: Array + errors: Array } } diff --git a/packages/firebase/src/index.d.ts b/packages/firebase/src/index.d.ts new file mode 100644 index 0000000..0b44b6a --- /dev/null +++ b/packages/firebase/src/index.d.ts @@ -0,0 +1,42 @@ +declare module '@atlas.js/firebase' { + import * as firebase from 'firebase-admin' + import AtlasService from '@atlas.js/service' + + /** + * Start a Firebase instance from within Atlas + * + * This service allows you to configure a Firebase app and use all available Firebase services, + * like the realtime database, Firestore etc. + */ + class Service extends AtlasService { + /** Service runtime configuration values */ + config: Service.Config + + prepare(): Promise + start(service: firebase.app.App): Promise + stop(service: firebase.app.App): Promise + } + + namespace Service { + /** Configuration schema available to this service */ + interface Config extends firebase.AppOptions { + /** + * Define the Firebase App's name + * Default: `'default'` + */ + name?: string + /** + * Provide the credentials for Firebase + * This can optionally be a `string`, in which case the string will be treated as a module + * location relative to Atlas' root and the actual credentials will be `require()` from that + * module. + */ + credential: firebase.credential.Credential + } + } + + export { + Service, + firebase, + } +} diff --git a/packages/firebase/src/service.mjs b/packages/firebase/src/service.mjs index 4136407..7b52c62 100644 --- a/packages/firebase/src/service.mjs +++ b/packages/firebase/src/service.mjs @@ -2,9 +2,9 @@ import Service from '@atlas.js/service' import * as Admin from 'firebase-admin' class Firebase extends Service { + /** Firebase configuration schema */ static config = { type: 'object', - additionalProperties: false, default: {}, properties: { name: { type: 'string' }, @@ -18,6 +18,11 @@ class Firebase extends Service { }, } + /** + * Start the service + * + * @return {Promise} + */ prepare() { const config = this.config // Either load the credentials from the file (if it's a string) or just pass it as is (object?) @@ -31,6 +36,12 @@ class Firebase extends Service { }, config.name) } + /** + * Stop the service + * + * @param {Admin.app.App} firebase The firebase instance + * @return {Promise} + */ async stop(firebase) { await firebase.delete() } diff --git a/packages/hook/src/index.d.ts b/packages/hook/src/index.d.ts new file mode 100644 index 0000000..c6bb021 --- /dev/null +++ b/packages/hook/src/index.d.ts @@ -0,0 +1,33 @@ +declare module '@atlas.js/hook' { + import Component from '@atlas.js/component' + + /** + * Use this class to implement the "observer" pattern within Atlas + * + * A hook is capable of receiving "events" emitted from other components as method invocations. + * If a hook observes a component "service:database", and that component emits a "didCreateRecord" + * event with the record on input, you can declare a method on your hook like this and Atlas will + * call it when the observing component emits that event: + * + * ```js + * class MyHook extends Hook { + * async didCreaterecord(record) { + * // process the record somehow + * } + * } + * ``` + * + * @abstract + */ + export default abstract class Hook extends Component { + /** @private */ + static type: 'hook' + + /** + * The name of the component this hook wants to receive events from + * + * If `atlas` is specified, this hook will receive events from the Atlas instance itself. + */ + static observes: string + } +} diff --git a/packages/hook/src/index.mjs b/packages/hook/src/index.mjs index 639b900..f759192 100644 --- a/packages/hook/src/index.mjs +++ b/packages/hook/src/index.mjs @@ -1,5 +1,23 @@ import Component from '@atlas.js/component' +/** + * Use this class to implement the "observer" pattern within Atlas + * + * A hook is capable of receiving "events" emitted from other components as method invocations. + * If a hook observes a component "service:database", and that component emits a "didCreateRecord" + * event with the record on input, you can declare a method on your hook like this and Atlas will + * call it when the observing component emits that event: + * + * ```js + * class MyHook extends Hook { + * async didCreaterecord(record) { + * // process the record somehow + * } + * } + * ``` + * + * @abstract + */ class Hook extends Component { static type = 'hook' static observes = null diff --git a/packages/koa/package-lock.json b/packages/koa/package-lock.json index 6d93756..6f963b4 100644 --- a/packages/koa/package-lock.json +++ b/packages/koa/package-lock.json @@ -4,6 +4,149 @@ "lockfileVersion": 1, "requires": true, "dependencies": { + "@types/accepts": { + "version": "1.3.5", + "resolved": "http://registry.npmjs.org/@types/accepts/-/accepts-1.3.5.tgz", + "integrity": "sha512-jOdnI/3qTpHABjM5cx1Hc0sKsPoYCp+DP/GJRGtDlPd7fiV9oXGGIcjW/ZOxLIvjGz8MA+uMZI9metHlgqbgwQ==", + "requires": { + "@types/node": "*" + } + }, + "@types/body-parser": { + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.17.0.tgz", + "integrity": "sha512-a2+YeUjPkztKJu5aIF2yArYFQQp8d51wZ7DavSHjFuY1mqVgidGyzEQ41JIVNy82fXj8yPgy2vJmfIywgESW6w==", + "requires": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "@types/connect": { + "version": "3.4.32", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.32.tgz", + "integrity": "sha512-4r8qa0quOvh7lGD0pre62CAb1oni1OO6ecJLGCezTmhQ8Fz50Arx9RUszryR8KlgK6avuSXvviL6yWyViQABOg==", + "requires": { + "@types/node": "*" + } + }, + "@types/cookies": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/@types/cookies/-/cookies-0.7.1.tgz", + "integrity": "sha512-ku6IvbucEyuC6i4zAVK/KnuzWNXdbFd1HkXlNLg/zhWDGTtQT5VhumiPruB/BHW34PWVFwyfwGftDQHfWNxu3Q==", + "requires": { + "@types/connect": "*", + "@types/express": "*", + "@types/keygrip": "*", + "@types/node": "*" + } + }, + "@types/events": { + "version": "1.2.0", + "resolved": "http://registry.npmjs.org/@types/events/-/events-1.2.0.tgz", + "integrity": "sha512-KEIlhXnIutzKwRbQkGWb/I4HFqBuUykAdHgDED6xqwXJfONCjF5VoE0cXEiurh3XauygxzeDzgtXUqvLkxFzzA==" + }, + "@types/express": { + "version": "4.16.0", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.16.0.tgz", + "integrity": "sha512-TtPEYumsmSTtTetAPXlJVf3kEqb6wZK0bZojpJQrnD/djV4q1oB6QQ8aKvKqwNPACoe02GNiy5zDzcYivR5Z2w==", + "requires": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "*", + "@types/serve-static": "*" + } + }, + "@types/express-serve-static-core": { + "version": "4.16.0", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.16.0.tgz", + "integrity": "sha512-lTeoCu5NxJU4OD9moCgm0ESZzweAx0YqsAcab6OB0EB3+As1OaHtKnaGJvcngQxYsi9UNv0abn4/DRavrRxt4w==", + "requires": { + "@types/events": "*", + "@types/node": "*", + "@types/range-parser": "*" + } + }, + "@types/http-assert": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@types/http-assert/-/http-assert-1.3.0.tgz", + "integrity": "sha512-RObYTpPMo0IY+ZksPtKHsXlYFRxsYIvUqd68e89Y7otDrXsjBy1VgMd53kxVV0JMsNlkCASjllFOlLlhxEv0iw==" + }, + "@types/keygrip": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@types/keygrip/-/keygrip-1.0.1.tgz", + "integrity": "sha1-/1QEYtL7TQqIRBzq8n0oewHD2Hg=" + }, + "@types/koa": { + "version": "2.0.47", + "resolved": "https://registry.npmjs.org/@types/koa/-/koa-2.0.47.tgz", + "integrity": "sha512-llhCaHNWKFDMx1GCrqwgsWgUO+C4Da0SccbgevHIYOKVxwegEjFzl0WaMWHk3wWx0P0AdqHR+gQYZ2ZAb0ez0Q==", + "requires": { + "@types/accepts": "*", + "@types/cookies": "*", + "@types/http-assert": "*", + "@types/keygrip": "*", + "@types/koa-compose": "*", + "@types/node": "*" + } + }, + "@types/koa-compose": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@types/koa-compose/-/koa-compose-3.2.2.tgz", + "integrity": "sha1-3BBuAAu/kqOskA91bfRzRIh+6Ec=" + }, + "@types/koa-websocket": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@types/koa-websocket/-/koa-websocket-5.0.3.tgz", + "integrity": "sha512-VgDEoySsNJTi+g0niUaUlnzYGJTeJmNoAxX+2cUGsHrVg19wczEN76TkQY+KGGh8r8vOSyky3qNqMXv1ncBdoA==", + "dev": true, + "requires": { + "@types/koa": "*", + "@types/koa-compose": "*", + "@types/ws": "*" + } + }, + "@types/mime": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-2.0.0.tgz", + "integrity": "sha512-A2TAGbTFdBw9azHbpVd+/FkdW2T6msN1uct1O9bH3vTerEHKZhTXJUQXy+hNq1B0RagfU8U+KBdqiZpxjhOUQA==" + }, + "@types/node": { + "version": "10.12.11", + "resolved": "https://registry.npmjs.org/@types/node/-/node-10.12.11.tgz", + "integrity": "sha512-3iIOhNiPGTdcUNVCv9e5G7GotfvJJe2pc9w2UgDXlUwnxSZ3RgcUocIU+xYm+rTU54jIKih998QE4dMOyMN1NQ==" + }, + "@types/pino": { + "version": "4.16.1", + "resolved": "https://registry.npmjs.org/@types/pino/-/pino-4.16.1.tgz", + "integrity": "sha512-uYEhZ3jsuiYFsPcR34fbxVlrqzqphc+QQ3fU4rWR6PXH8ka2TKvPBjtkNqj8oBHouVGf4GCRfyPb7FG2TEtPZA==", + "requires": { + "@types/events": "*", + "@types/node": "*" + } + }, + "@types/range-parser": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.2.tgz", + "integrity": "sha512-HtKGu+qG1NPvYe1z7ezLsyIaXYyi8SoAVqWDZgDQ8dLrsZvSzUNCwZyfX33uhWxL/SU0ZDQZ3nwZ0nimt507Kw==" + }, + "@types/serve-static": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.2.tgz", + "integrity": "sha512-/BZ4QRLpH/bNYgZgwhKEh+5AsboDBcUdlBYgzoLX0fpj3Y2gp6EApyOlM3bK53wQS/OE1SrdSYBAbux2D1528Q==", + "requires": { + "@types/express-serve-static-core": "*", + "@types/mime": "*" + } + }, + "@types/ws": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-6.0.1.tgz", + "integrity": "sha512-EzH8k1gyZ4xih/MaZTXwT2xOkPiIMSrhQ9b8wrlX88L0T02eYsddatQlwVFlEPyEqV0ChpdpNnE51QPH6NVT4Q==", + "dev": true, + "requires": { + "@types/events": "*", + "@types/node": "*" + } + }, "accepts": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.5.tgz", diff --git a/packages/koa/package.json b/packages/koa/package.json index 8b4484c..e3c296f 100644 --- a/packages/koa/package.json +++ b/packages/koa/package.json @@ -9,11 +9,15 @@ "@atlas.js/errors": "^0.2.1", "@atlas.js/hook": "^2.0.1", "@atlas.js/service": "^1.1.1", + "@types/pino": "^4.7.0", + "@types/koa": "^2.0.0", "koa": "^2.3.0", "koa-websocket": "^5.0.1" }, "devDependencies": { - "@atlas.js/atlas": "^2.1.0" + "@atlas.js/atlas": "^2.1.0", + "@types/koa": "^2.0.47", + "@types/koa-websocket": "^5.0.3" }, "engines": { "node": ">=8.3.0", diff --git a/packages/koa/src/context.d.ts b/packages/koa/src/context.d.ts new file mode 100644 index 0000000..01a2eab --- /dev/null +++ b/packages/koa/src/context.d.ts @@ -0,0 +1,28 @@ +import AtlasHook from '@atlas.js/hook' + +/** + * This hook allows you to extend the default `Koa.Context` object with custom properties + * + * You specify a module from which to load an object and that object's enumerable properties will + * be copied over onto `Koa.Context`. This means that all those properties will be available from + * within route handlers on the `ctx` function argument. + */ +declare class ContextHook extends AtlasHook { + config: ContextHook.Config + + afterPrepare(): void +} + +declare namespace ContextHook { + /** Configuration schema available to this hook */ + interface Config { + /** + * Location of the module, relative to `atlas.root`, from which to load the properties to extend + * the base Koa.Context with + * Default: `koa-context` + */ + module: string + } +} + +export = ContextHook diff --git a/packages/koa/src/context.mjs b/packages/koa/src/context.mjs index 4599cc4..315e8c8 100644 --- a/packages/koa/src/context.mjs +++ b/packages/koa/src/context.mjs @@ -2,6 +2,7 @@ import Hook from '@atlas.js/hook' import { FrameworkError } from '@atlas.js/errors' class ContextHook extends Hook { + /** ContextHook configuration schema */ static config = { type: 'object', additionalProperties: false, @@ -20,6 +21,7 @@ class ContextHook extends Hook { ] afterPrepare() { + /** @type {import("koa")} */ const koa = this.component('service:koa') // Prefer default export or a standard CommonJS module const context = this.atlas.require(this.config.module, { normalise: true }) diff --git a/packages/koa/src/index.d.ts b/packages/koa/src/index.d.ts new file mode 100644 index 0000000..e42f320 --- /dev/null +++ b/packages/koa/src/index.d.ts @@ -0,0 +1,13 @@ +import * as Server from './server' +import * as ContextHook from './context' +import * as WebsocketHook from './websocket' +import * as Koa from 'koa' + +declare module '@atlas.js/koa' { + export { + Server, + ContextHook, + WebsocketHook, + Koa, + } +} diff --git a/packages/koa/src/middleware.mjs b/packages/koa/src/middleware.mjs index 5b8ba77..b0ef0a0 100644 --- a/packages/koa/src/middleware.mjs +++ b/packages/koa/src/middleware.mjs @@ -1,10 +1,20 @@ +/** + * @typedef {import("koa")} Koa + */ + +/** + * @typedef {Object} Handlers + * @property {function} name The handler function associated with a name through the object + * key + */ + /** * Register the given middleware functions into the given Koa-compatible instance * - * @param {Object} instance Koa-compatible instance. Must implement `.use()`. - * @param {Object} handlers Object where keys are the middlewares' names and the + * @param {Koa} instance Koa-compatible instance. Must implement `.use()`. + * @param {Handlers} handlers Object where keys are the middlewares' names and the * values are the actual middleware functions. - * @param {Object} config={} Configuration for individual middlewares. The keys should + * @param {Object=} config Configuration for individual middlewares. The keys should * match the middleware names. * @return {void} */ diff --git a/packages/koa/src/server.d.ts b/packages/koa/src/server.d.ts new file mode 100644 index 0000000..bee5cd8 --- /dev/null +++ b/packages/koa/src/server.d.ts @@ -0,0 +1,80 @@ +import * as Koa from 'koa' +import * as Pino from 'pino' +import { Atlas } from '@atlas.js/atlas' +import AtlasService from '@atlas.js/service' + +type MiddlewareConfig = object + +/** + * The Server service which manages a Koa instance + * + * Use this service to start a Koa server as part of Atlas. + */ +declare class Server extends AtlasService { + config: Server.Config + + prepare(): Promise + start(service: Koa): Promise + stop(service: Koa): Promise +} + +declare namespace Server { + /** + * A Koa Context, enhanced with some extra properties to make it easy to interact with Atlas from + * within a route handler + */ + interface Context extends Koa.Context { + atlas: Atlas + log: Pino.Logger + } + + /** Configuration schema available to this service */ + interface Config { + /** Configuration for the middleware loader */ + middleware: { + /** + * The middleware loader will load the middleware to register with this Koa instance at this + * module location, relative to `atlas.root`. + * Default: `'middleware'` + */ + module: string + /** + * Configuration options for the individual middleware. The key should match the name of the + * middleware under which it was exported from the module you specified in the `module` + * configuration. The value will be passed directly to the middleware function. + */ + config: { + [key: string]: MiddlewareConfig + } + } + + // @TODO(semver-major): change this to match `net.ListenOptions`. + /** Configuration options specifying where to listen for incoming requests */ + listen: { + /** + * Port to listen on + * Default: `3000` + */ + port: number + /** + * Hostname to listen on + * Default: `127.0.0.1` + */ + hostname: string + } + + /** Configuration options applied to the `http.Server` instance using `Object.assign()` */ + server: { + maxHeadersCount: number; + timeout: number; + keepAliveTimeout: number; + } + + /** Settings applied to the `Koa` instance directly using `Object.assign()` */ + koa: { + proxy: boolean + } + } +} + +export = Server diff --git a/packages/koa/src/server.mjs b/packages/koa/src/server.mjs index ce42daa..ca8ce0a 100644 --- a/packages/koa/src/server.mjs +++ b/packages/koa/src/server.mjs @@ -25,6 +25,7 @@ class KoaService extends Service { }, }, + // @TODO(semver-major): change this to an object matching the http.Server.listen({}) pattern listen: { type: 'object', additionalProperties: false, @@ -77,9 +78,7 @@ class KoaService extends Service { } - prepare(options) { - super.prepare(options) - + prepare() { // Prepare Koa instance const koa = new Koa() koa.env = this.atlas.env @@ -101,6 +100,12 @@ class KoaService extends Service { return koa } + /** + * Start the service + * + * @param {Koa} koa The koa instance + * @return {Promise} + */ async start(koa) { const server = http.createServer(koa.callback()) koa.server = server @@ -123,12 +128,18 @@ class KoaService extends Service { server.once('error', fail) // Listen already! - koa.server.listen(this.config.listen.port, this.config.listen.hostname) + server.listen(this.config.listen.port, this.config.listen.hostname) }) this.log.info({ addrinfo: server.address() }, 'listening') } + /** + * Stop the service + * + * @param {Koa} koa The koa instance + * @return {Promise} + */ async stop(koa) { if (!koa || !koa.server) { throw new FrameworkError('Cannot stop a non-running server') diff --git a/packages/koa/src/websocket.d.ts b/packages/koa/src/websocket.d.ts new file mode 100644 index 0000000..6d16d60 --- /dev/null +++ b/packages/koa/src/websocket.d.ts @@ -0,0 +1,46 @@ +import AtlasHook from '@atlas.js/hook' +import * as ws from 'ws' + + +type MiddlewareConfig = object + +/** + * Attach a websocket interface to a regular Koa server + * + * This allows you to upgrade your regular Koa server with websocker capabilities. + */ +declare class WebsocketHook extends AtlasHook { + config: WebsocketHook.Config + + afterPrepare(): void + afterStart(): void + beforeStop(): Promise +} + +declare namespace WebsocketHook { + /** Configuration schema available to this hook */ + interface Config { + /** Configuration for the websocket's middleware loader */ + middleware: { + /** + * The middleware loader will load the middleware for the websocket interface at this module + * location, relative to `atlas.root`. + * Default: `'websocket/middleware'` + */ + module: string + /** + * Configuration options for the individual middleware. The key should match the name of the + * middleware under which it was exported from the module you specified in the `module` + * configuration. The value will be passed directly to the middleware function. + */ + config: { + [key: string]: MiddlewareConfig + } + } + + /** Configuration options for the `ws.listen()` method */ + listen: ws.ServerOptions + } +} + +export = WebsocketHook diff --git a/packages/koa/src/websocket.mjs b/packages/koa/src/websocket.mjs index cd169df..6479ec6 100644 --- a/packages/koa/src/websocket.mjs +++ b/packages/koa/src/websocket.mjs @@ -46,6 +46,7 @@ class WebsocketHook extends Hook { } afterPrepare() { + /** @type {import("koa")} */ const koa = this.component('service:koa') const config = this.config @@ -59,6 +60,7 @@ class WebsocketHook extends Hook { } afterStart() { + /** @type {import("koa")} */ const koa = this.component('service:koa') koa.ws.listen({ @@ -70,10 +72,13 @@ class WebsocketHook extends Hook { } async beforeStop() { + /** @type {import("koa")} */ + const koa = this.component('service:koa') + this.log.info('websocket:close') await new Promise((resolve, reject) => - this.component('service:koa').ws.server.close(err => + koa.ws.server.close(err => err ? reject(err) : resolve())) } } diff --git a/packages/mongoose/package-lock.json b/packages/mongoose/package-lock.json index 5b2fbfa..f20c58e 100644 --- a/packages/mongoose/package-lock.json +++ b/packages/mongoose/package-lock.json @@ -4,6 +4,41 @@ "lockfileVersion": 1, "requires": true, "dependencies": { + "@types/bson": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@types/bson/-/bson-1.0.11.tgz", + "integrity": "sha512-j+UcCWI+FsbI5/FQP/Kj2CXyplWAz39ktHFkXk84h7dNblKRSoNJs95PZFRd96NQGqsPEPgeclqnznWZr14ZDA==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/mongodb": { + "version": "3.1.15", + "resolved": "https://registry.npmjs.org/@types/mongodb/-/mongodb-3.1.15.tgz", + "integrity": "sha512-JSvtmrdrh88WH0Lo8Hq7sB1FkEChkrt6+fAZdFhEsRXcUetnrdU7wd2yar40tPg5wfRI2t31yduQgPiMUvgEEA==", + "dev": true, + "requires": { + "@types/bson": "*", + "@types/node": "*" + } + }, + "@types/mongoose": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/@types/mongoose/-/mongoose-5.3.2.tgz", + "integrity": "sha512-BodywHKMSpitNsfktZiWfdYT+irLYHeLuknYk59zGTNpm8uoNZ1pLCH+dWMC3k2SlgiP7qL8ij+OlCRXDWkSrQ==", + "dev": true, + "requires": { + "@types/mongodb": "*", + "@types/node": "*" + } + }, + "@types/node": { + "version": "10.12.11", + "resolved": "https://registry.npmjs.org/@types/node/-/node-10.12.11.tgz", + "integrity": "sha512-3iIOhNiPGTdcUNVCv9e5G7GotfvJJe2pc9w2UgDXlUwnxSZ3RgcUocIU+xYm+rTU54jIKih998QE4dMOyMN1NQ==", + "dev": true + }, "async": { "version": "2.6.1", "resolved": "https://registry.npmjs.org/async/-/async-2.6.1.tgz", diff --git a/packages/mongoose/package.json b/packages/mongoose/package.json index ff747b8..19ce1e9 100644 --- a/packages/mongoose/package.json +++ b/packages/mongoose/package.json @@ -12,7 +12,8 @@ "mongoose": "^5.0.0" }, "devDependencies": { - "@atlas.js/atlas": "^2.1.0" + "@atlas.js/atlas": "^2.1.0", + "@types/mongoose": "^5.0.0" }, "engines": { "node": ">=8.3.0", diff --git a/packages/mongoose/src/index.d.ts b/packages/mongoose/src/index.d.ts new file mode 100644 index 0000000..b35e019 --- /dev/null +++ b/packages/mongoose/src/index.d.ts @@ -0,0 +1,14 @@ +import Service from './service' +import ModelsHook from './models' +import * as mongoose from 'mongoose' +import { Schema, SchemaTypes } from 'mongoose' + +declare module '@atlas.js/mongoose' { + export { + Service, + ModelsHook, + Schema, + SchemaTypes, + mongoose, + } +} diff --git a/packages/mongoose/src/index.mjs b/packages/mongoose/src/index.mjs index 37f35db..555c92f 100644 --- a/packages/mongoose/src/index.mjs +++ b/packages/mongoose/src/index.mjs @@ -1,13 +1,14 @@ -import { - Schema, - SchemaTypes, -} from 'mongoose' +import mongoose from 'mongoose' import Service from './service' import ModelsHook from './models' +const Schema = mongoose.Schema +const SchemaTypes = mongoose.SchemaTypes + export { Service, ModelsHook, Schema, SchemaTypes, + mongoose, } diff --git a/packages/mongoose/src/models.d.ts b/packages/mongoose/src/models.d.ts new file mode 100644 index 0000000..0bbc296 --- /dev/null +++ b/packages/mongoose/src/models.d.ts @@ -0,0 +1,27 @@ +import AtlasHook from '@atlas.js/hook' + +/** + * This hook allows you to load your Mongoose schemas from a particular module location and add them + * to the Mongoose service as models. + * + * The models exported at the specified location will then be accessible via + * `atlas.services.mongoose.model('name')`. + */ +declare class ModelsHook extends AtlasHook { + config: ModelsHook.Config + + afterPrepare(): void +} + +declare namespace ModelsHook { + /** Configuration schema available to this hook */ + interface Config { + /** + * The module location, relative to `atlas.root`, from which to load the Mongoose models + * Default: `'models'` + */ + module: string + } +} + +export = ModelsHook diff --git a/packages/mongoose/src/service.d.ts b/packages/mongoose/src/service.d.ts new file mode 100644 index 0000000..6002c01 --- /dev/null +++ b/packages/mongoose/src/service.d.ts @@ -0,0 +1,32 @@ +import AtlasService from '@atlas.js/service' +import { Mongoose, ConnectionOptions } from 'mongoose' + +/** + * Connect to a MongoDB server using the mongoose ODM + * + * This service allows you to connect to a MongoDB server and use the mongoose object document + * model that comes with it from within Atlas. + */ +declare class Service extends AtlasService { + /** Service runtime configuration values, with defaults applied */ + config: Service.Config + + prepare(): Promise + start(service: Mongoose): Promise + stop(service: Mongoose): Promise +} + +declare namespace Service { + /** Configuration schema available to this service */ + interface Config { + /** + * MongoDB URI to connect to + * Default: `mongodb://127.0.0.1:27017` + */ + uri: string + /** Additional connection options */ + options: ConnectionOptions + } +} + +export = Service diff --git a/packages/mongoose/src/service.mjs b/packages/mongoose/src/service.mjs index 877173e..d16a49d 100644 --- a/packages/mongoose/src/service.mjs +++ b/packages/mongoose/src/service.mjs @@ -24,11 +24,11 @@ class Mongoose extends Service { }, } - static defaults = { - uri: 'mongodb://127.0.0.1:27017', - options: {}, - } - + /** + * Prepare the service + * + * @return {Promise} + */ prepare() { const instance = new mongoose.Mongoose() // Add a trace logger to allow users to monitor Mongoose activity @@ -37,6 +37,12 @@ class Mongoose extends Service { return Promise.resolve(instance) } + /** + * Start the service + * + * @param {mongoose.Mongoose} instance The instance being started + * @return {Promise} + */ async start(instance) { for (const name of instance.modelNames()) { // Allow models to use the Atlas instance @@ -47,6 +53,12 @@ class Mongoose extends Service { await instance.connect(this.config.uri, this.config.options) } + /** + * Stop the service + * + * @param {mongoose.Mongoose} instance The instance being started + * @return {Promise} + */ async stop(instance) { await instance.disconnect() } diff --git a/packages/nodemailer/package-lock.json b/packages/nodemailer/package-lock.json index 895dfe6..ccc9579 100644 --- a/packages/nodemailer/package-lock.json +++ b/packages/nodemailer/package-lock.json @@ -4,6 +4,28 @@ "lockfileVersion": 1, "requires": true, "dependencies": { + "@types/events": { + "version": "1.2.0", + "resolved": "http://registry.npmjs.org/@types/events/-/events-1.2.0.tgz", + "integrity": "sha512-KEIlhXnIutzKwRbQkGWb/I4HFqBuUykAdHgDED6xqwXJfONCjF5VoE0cXEiurh3XauygxzeDzgtXUqvLkxFzzA==", + "dev": true + }, + "@types/node": { + "version": "10.12.11", + "resolved": "https://registry.npmjs.org/@types/node/-/node-10.12.11.tgz", + "integrity": "sha512-3iIOhNiPGTdcUNVCv9e5G7GotfvJJe2pc9w2UgDXlUwnxSZ3RgcUocIU+xYm+rTU54jIKih998QE4dMOyMN1NQ==", + "dev": true + }, + "@types/nodemailer": { + "version": "4.6.5", + "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-4.6.5.tgz", + "integrity": "sha512-cbs2HFLj33TBqzcCqTrs+6/mgTX3xl0odbApv3vTdF2+JERLxh5rDZCasXhvy+YqaiUNBr2I1RjNCdbKGs1Bnw==", + "dev": true, + "requires": { + "@types/events": "*", + "@types/node": "*" + } + }, "nodemailer": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-4.7.0.tgz", diff --git a/packages/nodemailer/package.json b/packages/nodemailer/package.json index 35ba699..df9236a 100644 --- a/packages/nodemailer/package.json +++ b/packages/nodemailer/package.json @@ -10,7 +10,8 @@ "nodemailer": "^4.0.1" }, "devDependencies": { - "@atlas.js/atlas": "^2.1.0" + "@atlas.js/atlas": "^2.1.0", + "@types/nodemailer": "^4.0.1" }, "engines": { "node": ">=8.3.0", diff --git a/packages/nodemailer/src/index.d.ts b/packages/nodemailer/src/index.d.ts new file mode 100644 index 0000000..74573fc --- /dev/null +++ b/packages/nodemailer/src/index.d.ts @@ -0,0 +1,56 @@ +declare module '@atlas.js/nodemailer' { + import * as nodemailer from 'nodemailer' + import AtlasService from '@atlas.js/service' + + /** + * Send emails using nodemailer from within Atlas + */ + class Service extends AtlasService { + /** Service runtime configuration values */ + config: Service.Config + + prepare(): Promise + start(service: nodemailer.Transporter): Promise + stop(service: nodemailer.Transporter): Promise + } + + namespace Service { + /** Configuration schema available to this service */ + interface Config { + /** + * The transport to be used for sending emails + * + * Either provide the transport module itself or specify it as a string and it will be + * `require()`d. + */ + transport: Function | string + /** + * Transport-specific options + * + * Check the docs for the transporter you are going to use for available options. + */ + options: object + + plugins?: Array + } + + interface PluginConfiguration { + /** + * The plugin to be added to the transporter + * + * Either provide the plugin module itself or specify it as a string and it will be + * `require()`d. + */ + plugin: Function | string + /** Event to which this plugin should be attached */ + event: 'compile' | 'stream' + /** Plugin configuration options */ + options: object + } + } + + export { + Service, + nodemailer, + } +} diff --git a/packages/nodemailer/src/index.mjs b/packages/nodemailer/src/index.mjs index 7d9a61c..22ea9b8 100644 --- a/packages/nodemailer/src/index.mjs +++ b/packages/nodemailer/src/index.mjs @@ -1,5 +1,7 @@ +import nodemailer from 'nodemailer' import Service from './service' export { Service, + nodemailer, } diff --git a/packages/service/src/index.d.ts b/packages/service/src/index.d.ts new file mode 100644 index 0000000..7b7d401 --- /dev/null +++ b/packages/service/src/index.d.ts @@ -0,0 +1,58 @@ +declare module '@atlas.js/service' { + import Component from '@atlas.js/component' + + type ServiceApi = Object | Function | Array + + /** + * Base service class which all Atlas services should subclass + * + * This class provides basic functionality to integrate a Service into Atlas. Atlas expects all + * Services to have a certain interface, therefore when you want to create your own service, you + * should subclass this base Service class. + * + * @abstract + */ + abstract class Service extends Component { + /** @private */ + static type: 'service' + + /** + * Prepare the service instance your consumers will use in their projects + * + * You must implement this method in a subclass. + * You should not attempt to make any connections or other I/O unless absolutely necessary for + * preparing the service for work. This method should only be used to expose the data structures + * your consumers will have access to. Atlas uses this method to allow component inspection in a + * REPL or to provide some auto-complete suggestions when using the CLI. + * + * @abstract + * @return {Promise} The API your users will be interacting with. It can be + * anything, really, but usually it will be some kind of + * connection object or similar. + */ + abstract prepare(): Promise + + /** + * Start the service or put the service into a fully functional and ready state + * + * @abstract + * @param ServiceApi The API your users will be interacting with. You should provide this + * API as the return value of the `.prepare()` method. + */ + abstract start(service: ServiceApi): Promise + + /** + * Stop the service by terminating any pending connections and finishing all pending work + * + * @abstract + * @param ServiceApi The API your users have interacted with. You created this API as the + * return value of the `.prepare()` method. + */ + abstract stop(service: ServiceApi): Promise + } + + export default Service + export { + ServiceApi, + } +} diff --git a/packages/service/src/index.mjs b/packages/service/src/index.mjs index 61fee28..7ab1f94 100644 --- a/packages/service/src/index.mjs +++ b/packages/service/src/index.mjs @@ -1,7 +1,13 @@ import Component from '@atlas.js/component' /** - * Base service class all other services should inherit from + * Base service class which all Atlas services should subclass + * + * This class provides basic functionality to integrate a Service into Atlas. Atlas expects all + * Services to have a certain interface, therefore when you want to create your own service, you + * should subclass this base Service class. + * + * @abstract */ class Service extends Component { static type = 'service' diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..1dac3fe --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "charset": "utf8", + "module": "commonjs", + "moduleResolution": "node", + "target": "esnext", + "noEmit": true, + "noImplicitAny": true, + "skipLibCheck": false, + "noUnusedParameters": true, + "noImplicitThis": true, + "noUnusedLocals": true, + "strictNullChecks": true + }, + "exclude": [ + "node_modules", + "**/node_modules/*" + ], + "typeAcquisition": { + "enable": true + } +}