├── .circleci └── config.yml ├── .github ├── CONTRIBUTING.md ├── FUNDING.yml ├── ISSUE_TEMPLATE.md └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── .istanbul.yml ├── .npmignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── ROADMAP.md ├── archetype ├── .gitignore ├── package.json ├── src │ ├── api │ │ ├── controllers │ │ │ └── index.ts │ │ ├── index.ts │ │ ├── models │ │ │ └── index.ts │ │ ├── policies │ │ │ └── index.ts │ │ ├── resolvers │ │ │ └── index.ts │ │ └── services │ │ │ └── index.ts │ ├── config │ │ ├── env │ │ │ ├── development │ │ │ │ ├── index.ts │ │ │ │ └── main.ts │ │ │ ├── index.ts │ │ │ ├── production │ │ │ │ └── index.ts │ │ │ ├── staging │ │ │ │ └── index.ts │ │ │ └── testing │ │ │ │ └── index.ts │ │ ├── i18n.ts │ │ ├── index.ts │ │ ├── locales │ │ │ └── en.json │ │ ├── log.ts │ │ ├── main.ts │ │ ├── models.ts │ │ ├── routes.ts │ │ ├── stores.ts │ │ └── web.ts │ ├── index.ts │ ├── server.ts │ ├── tsconfig.json │ ├── tsconfig.release.json │ ├── tslint.json │ └── typings │ │ └── index.d.ts └── test │ ├── index.js │ ├── integration │ └── FabrixApp.test.js │ └── mocha.opts ├── lib ├── Configuration.ts ├── Core.ts ├── Fabrix.ts ├── LoggerProxy.ts ├── Pathfinder.ts ├── Templates.ts ├── common │ ├── Controller.ts │ ├── Generic.ts │ ├── Model.ts │ ├── Policy.ts │ ├── Resolver.ts │ ├── Service.ts │ ├── Spool.ts │ ├── decorators │ │ ├── configurable.ts │ │ ├── enumerable.ts │ │ ├── spool.ts │ │ └── writable.ts │ ├── index.ts │ ├── interfaces │ │ ├── IApi.ts │ │ ├── IConfig.ts │ │ ├── IEnv.ts │ │ ├── ILifecycle.ts │ │ ├── IPkg.ts │ │ ├── ISpool.ts │ │ ├── ISpoolConfig.ts │ │ └── IVersions.ts │ └── spools │ │ ├── archetype │ │ ├── .gitignore │ │ ├── README.md │ │ ├── lib │ │ │ ├── Archetype.ts │ │ │ ├── api │ │ │ │ └── index.ts │ │ │ └── config │ │ │ │ ├── index.ts │ │ │ │ └── spool.ts │ │ ├── package.json │ │ └── test │ │ │ ├── app.js │ │ │ ├── index.js │ │ │ ├── mocha.opts │ │ │ └── spool.test.js │ │ ├── datastore.ts │ │ ├── extension.ts │ │ ├── index.ts │ │ ├── misc.ts │ │ ├── server.ts │ │ ├── system.ts │ │ └── tool.ts ├── errors │ ├── ApiNotDefinedErrort.ts │ ├── ConfigNotDefinedError.ts │ ├── ConfigValueError.ts │ ├── GraphCompletenessError.ts │ ├── IllegalAccessError.ts │ ├── NamespaceConflictError.ts │ ├── PackageNotDefinedError.ts │ ├── SanityError.ts │ ├── SpoolError.ts │ ├── TimeoutError.ts │ ├── ValidationError.ts │ └── index.ts ├── index.ts ├── tsconfig.json ├── tsconfig.release.json ├── tslint.json ├── typings │ └── index.d.ts └── utils │ ├── index.ts │ └── require-main-filename.ts ├── package-lock.json ├── package.json ├── test-browser ├── fixtures │ ├── app.js │ └── bundle.js ├── index.test.html ├── integration │ └── browser.test.js ├── run.js ├── setup.js └── webpack.config.js ├── test-performance ├── lib │ └── pathfinder.test.js ├── mocha.opts └── testapp.js └── test ├── fixtures ├── app.js ├── testapp.js ├── testspool.js ├── testspool2.js ├── testspool3.js ├── testspool4.js └── testspool5.js ├── index.js ├── integration ├── archetype.test.js └── fabrixapp.test.js ├── lib ├── Configuration.test.js ├── Controller.test.js ├── Core.test.js ├── LoggerProxy.test.js ├── Model.test.js ├── Policy.test.js ├── Resolver.test.js ├── Service.test.js ├── common │ └── spools │ │ ├── DatastoreSpool.test.js │ │ ├── ServerSpool.test.js │ │ ├── Spool.test.js │ │ ├── app.js │ │ ├── datastorespool.js │ │ ├── interfaces.test.js │ │ ├── serverspool.js │ │ ├── spool.js │ │ └── spool │ │ ├── config │ │ ├── index.js │ │ ├── main.js │ │ └── spool.js │ │ ├── index.js │ │ └── package.json ├── errors │ └── errors.test.js ├── pathfinder.test.js └── utils │ └── require-main-filename.test.js ├── mocha.opts └── runkitExample.js /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # Javascript Node CircleCI 2.0 configuration file 2 | # 3 | # Check https://circleci.com/docs/2.0/language-javascript/ for more details 4 | # 5 | 6 | version: 2 7 | 8 | defaults: &defaults 9 | working_directory: ~/fabrix 10 | 11 | jobs: 12 | test: 13 | docker: 14 | - image: circleci/node:10.0.0 15 | <<: *defaults 16 | steps: 17 | - checkout 18 | - run: 19 | name: update-npm 20 | command: 'sudo npm install -g npm@5' 21 | - restore_cache: 22 | key: dependency-cache-{{ checksum "package.json" }} 23 | - run: 24 | name: install-npm-wee 25 | command: npm install 26 | - save_cache: 27 | key: dependency-cache-{{ checksum "package.json" }} 28 | paths: 29 | - ./node_modules 30 | - run: 31 | name: test 32 | command: npm test 33 | - run: 34 | name: test performance 35 | command: npm run test:performance 36 | - run: 37 | name: test archetype 38 | command: npm run test:archetype 39 | - run: 40 | name: code-coverage 41 | command: './node_modules/.bin/nyc report --reporter=text-lcov' 42 | - store_artifacts: 43 | path: test-results.xml 44 | prefix: tests 45 | - store_artifacts: 46 | path: coverage 47 | prefix: coverage 48 | - store_test_results: 49 | path: test-results.xml 50 | - persist_to_workspace: 51 | root: ~/fabrix 52 | paths: . 53 | deploy: 54 | docker: 55 | - image: circleci/node:12.0.0 56 | <<: *defaults 57 | steps: 58 | - attach_workspace: 59 | at: ~/fabrix 60 | - run: 61 | name: Authenticate with registry 62 | command: echo "//registry.npmjs.org/:_authToken=$npm_TOKEN" > ~/fabrix/.npmrc 63 | - run: 64 | name: Publish package 65 | command: npm publish 66 | 67 | test-node12: 68 | docker: 69 | - image: circleci/node:10.0.0 70 | <<: *defaults 71 | steps: 72 | - run: echo "Running node v12" 73 | - checkout 74 | - run: 75 | name: update-npm 76 | command: 'sudo npm install -g npm@5' 77 | - restore_cache: 78 | key: dependency-cache-{{ checksum "package.json" }} 79 | - run: 80 | name: install-npm-wee 81 | command: npm install 82 | - save_cache: 83 | key: dependency-cache-{{ checksum "package.json" }} 84 | paths: 85 | - ./node_modules 86 | - run: 87 | name: test 88 | command: npm test 89 | - run: 90 | name: test performance 91 | command: npm run test:performance 92 | - run: 93 | name: test archetype 94 | command: npm run test:archetype 95 | - run: 96 | name: code-coverage 97 | command: './node_modules/.bin/nyc report --reporter=text-lcov' 98 | - store_artifacts: 99 | path: test-results.xml 100 | prefix: tests 101 | - store_artifacts: 102 | path: coverage 103 | prefix: coverage 104 | - store_test_results: 105 | path: test-results.xml 106 | - persist_to_workspace: 107 | root: ~/fabrix 108 | paths: . 109 | 110 | workflows: 111 | version: 2 112 | test-deploy: 113 | jobs: 114 | - test: 115 | filters: 116 | tags: 117 | only: /^v.*/ 118 | - test-node12: 119 | filters: 120 | tags: 121 | only: /^v.*/ 122 | - deploy: 123 | requires: 124 | - test 125 | filters: 126 | tags: 127 | only: /^v.*/ 128 | branches: 129 | ignore: /.*/ 130 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Fabrix and Spools 2 | 3 | This guide is designed to help you get off the ground quickly contributing to Fabrix, Spools and the Fabrix ecosystem. The goal of our community is to make it easy for members of all skill levels to contribute. This guide will help you write useful issues, propose eloquent feature requests, and submit top-notch code that can be merged quickly. 4 | 5 | Maintaining a open source project is a labor of love, meaning the core maintainers of Fabrix or Spools are volunteering their time. Respecting the guidelines laid out below helps the maintainers be efficient and make the most of the time they spend working on the project. This, in turn, creates a better experience of working with Fabrix more enjoyable for the community at large. 6 | 7 | 8 | ## Submitting Issues 9 | 10 | > Fabrix is composed of a core library, [Fabrix](https://github.com/fabrix-app/fabrix), and a number of [Spools](https://github.com/fabrix-app), which have their own dedicated repositories. These repositories may also live outside the Fabrix-app official Github organization and NPM scope @fabrix. 11 | > 12 | > _*Please open issues with spools, generators, etc. in the relevant repository.*_ 13 | > 14 | > This helps us stay on top of issues and keep organized. 15 | 16 | When submitting an issue, please follow these simple instructions: 17 | 18 | 1. Search for issues similar to yours in [GitHub search](https://github.com/fabrix-app/fabrix/search?type=Issues) and [Google](https://www.google.nl/search?q=fabrix+app). 19 | 2. Feature requests are welcome; see [Requesting Features](#requesting-features) below for submission guidelines. 20 | 3. If there's an open issue, please contribute to that issue. 21 | 4. If there's a closed issue, open a new issue and link the url of the already closed issue(s). 22 | 5. If there is no issue, open a new issue and specify the following: 23 | - A short description of your issue in the title 24 | - The fabrix version (find this with in the package.json file) 25 | - Detailed explanation of how to recreate the issue, including necessary setup setps 26 | 6. If you are experiencing more than one problem, create a separate issue for each one. If you think they might be related, please reference the other issues you've created. 27 | 28 | ## Submitting Features 29 | 30 | > New feature requests should be made as pull requests to the `backlog` section of [ROADMAP.MD](https://github.com/fabrix-app/fabrix/blob/master/ROADMAP.md) or as issues on the `Backlog` milestone in the [issue queue](https://github.com/fabrix-app/fabrix/milestones/Backlog). We will monitor community discussion on these PRs and issues and if they are wanted by the community/fabrix devs, they will be merged. Further discussion is welcome even after a PR has been merged. 31 | 32 | ##### Submitting a new feature request 33 | 1. First, look at the `backlog` table in [ROADMAP.MD](https://github.com/fabrix-app/fabrix/blob/master/ROADMAP.md) or the [Backlog Milestone](https://github.com/fabrix-app/fabrix/milestones/Backlog) in the issue queue and also search open pull requests in that file to make sure your change hasn't already been proposed. If it has, join the discussion. 34 | 2. If it doesn't already exist, create a pull request editing the `backlog` table of [ROADMAP.MD](https://github.com/fabrix-app/fabrix/blob/master/ROADMAP.md). 35 | 3. Start a discussion about why your feature should be built (or better yet, build it). Get feedback in the [Fabrix-app Gitter](https://gitter.im/fabrix-app/Lobby) Channel. The more feedback we get from our community, the better we are able to build the framework of your dreams :evergreen_tree: 36 | 37 | ## Writing Tests 38 | 39 | Ideally, all code contributions should be accompanied by functional and/or unit tests (as appropriate). 40 | 41 | Test Coverage: 42 | 43 | | Edge (master branch) | 44 | |----------------------| 45 | | [![Coverage Status](https://coveralls.io/repos/fabrix-app/fabrix/badge.png)](https://coveralls.io/r/fabrix-app/fabrix) | 46 | 47 | 48 | ## Code Submission Guidelines 49 | 50 | The community is what makes Fabrix great, without you we wouldn't have come so far. But to help us keep our sanity and reach code-nirvana together, please follow these quick rules whenever contributing. 51 | 52 | > Note: This section is based on the [Node.js contribution guide](https://github.com/joyent/node/blob/master/CONTRIBUTING.md#contributing). 53 | 54 | ###### Contributing to an Spool 55 | 56 | If the Spool is in the Fabrix Github organization, please send feature requests, patches and pull requests to that organization. Other Spools may have their own contribution guidelines. Please follow the guidelines of the Spool you are contributing to. 57 | 58 | ###### Authoring a new Spool 59 | 60 | You are welcome to author a new Spool at any time. Spools must inherit from the main [Spool](https://github.com/fabrix-app/fabrix) interface to inherit the API. Feel free to start work on a new spool, just make sure and do a thorough search on npm, Google and Github to make sure someone else hasn't already started working on the same thing. 61 | 62 | It is recommended that you maintain your Spool in your own Github repository. If you would like to submit your Spool to be listed in the [Fabrix-app Github Organization](https://github.com/fabrix-app) and @fabrix NPM scope, please submit an issue to the [Fabrix Issue queue](https://github.com/fabrix-app/fabrix/issues). 63 | 64 | ###### Contributing to a generator 65 | 66 | Fabrix generators are based upon a cli. Please follow the core best practices for contributing to generators. If it is located in a different repo, please send feature requests, patches, and issues there. 67 | 68 | ###### Contributing to core 69 | 70 | Fabrix has several dependencies referenced in the `package.json` file that are not part of the project proper. Any proposed changes to those dependencies or _their_ dependencies should be sent to their respective projects (i.e. Sequelize etc.) Please do not send your patch or feature request to this repository, we cannot accept or fulfill it. 71 | 72 | In case of doubt, open an issue in the [issue tracker](https://github.com/fabrix-app/fabrix/issues), ask your question in the [Gitter room](http://gitter.im/fabrix-app/Lobby). Especially if you plan to work on something big. Nothing is more frustrating than seeing your hard work go to waste because your vision does not align with a project's roadmap. At the end of the day, we just want to be able to merge your code. 73 | 74 | ###### Submitting Pull Requests 75 | 76 | 0. If you don't know how to fork and PR, [Github has some great documentation](https://help.github.com/articles/using-pull-requests/). Here's the quick version: 77 | 1. Fork the repo. 78 | 2. Add a test for your change. Only refactoring and documentation changes require no new tests. If you are adding functionality or fixing a bug, we need a test! 79 | 4. Make the tests pass and make sure you follow our syntax guidelines. 80 | 5. Add a line of what you did to CHANGELOG.md (right under `master`). 81 | 6. Push to your fork and submit a pull request to the appropriate branch 82 | 83 | ## Publishing Releases 84 | 85 | All releases are tagged and published by the [Fabrix Maintainers](https://github.com/orgs/fabrix-app/teams) automatically via [Cicle-CI](https://circleci.com/gh/fabrix-app/fabrix). For a patch release, the deployment process is as follows: 86 | 87 | 1. Tag a release 88 | ```sh 89 | $ npm version patch 90 | ``` 91 | 92 | 2. Push the tag upstream (the "fabrix-app" org) 93 | ```sh 94 | $ git push upstream --tags 95 | ``` 96 | 97 | 3. Circle-CI will publish the release to npm. 98 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 2 | patreon: # Replace with a single Patreon username 3 | open_collective: # Replace with a single Open Collective username 4 | ko_fi: # Replace with a single Ko-fi username 5 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 6 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 7 | liberapay: # Replace with a single Liberapay username 8 | issuehunt: # Replace with a single IssueHunt username 9 | otechie: # Replace with a single Otechie username 10 | custom: # Replace with a Custom 11 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | #### Issue Description 2 | ??? 3 | 4 | #### Environment 5 | - node version: ??? 6 | - fabrix version: ??? 7 | - operating system: ??? 8 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | #### Description 2 | ??? 3 | 4 | #### Issues 5 | - resolves #??? 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled version placed here by typescript compiler 2 | dist 3 | 4 | /.idea 5 | # Logs 6 | logs 7 | *.log 8 | 9 | # Tmp 10 | .tmp 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | 17 | # Directory for instrumented libs generated by jscoverage/JSCover 18 | lib-cov 19 | 20 | # Coverage directory used by tools like istanbul 21 | coverage 22 | .nyc_output 23 | 24 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 25 | .grunt 26 | 27 | # node-waf configuration 28 | .lock-wscript 29 | 30 | # Compiled binary addons (http://nodejs.org/api/addons.html) 31 | build/Release 32 | 33 | # Dependency directory 34 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 35 | node_modules 36 | 37 | *.sw* 38 | 39 | # Archetype for CLI 40 | archetype/package-lock.json 41 | archetype/.nyc_output 42 | -------------------------------------------------------------------------------- /.istanbul.yml: -------------------------------------------------------------------------------- 1 | --- 2 | instrumentation: 3 | root: . 4 | # Include all source files so we know which files are not tested at all 5 | include-all-sources: true 6 | excludes: [ 'test/**', 'test-performance/**' ] 7 | 8 | reporting: 9 | reports: 10 | # Lcov output can be used by some other program, and the html report can be viewed by humans in 11 | # browser - coverage/lcov-report/index.html 12 | - lcov 13 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .github/ 2 | .tmp/ 3 | .nyc_output/ 4 | test/ 5 | test-performance/ 6 | 7 | .istanbul.yml 8 | .travis.yml 9 | appveyor.yml 10 | .circleci 11 | .idea 12 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 4 | 5 | ### [1.6.4](https://github.com/fabrix-app/fabrix/compare/v1.6.3...v1.6.4) (2019-10-09) 6 | 7 | 8 | ### Features 9 | 10 | * Error Handeling ([32d5ca1](https://github.com/fabrix-app/fabrix/commit/32d5ca1b5efa3e0e72651652379f4285eaab1029)) 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 CST 4 | 2018 Scott Wyatt 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | [![Gitter][gitter-image]][gitter-url] 4 | [![NPM version][npm-image]][npm-url] 5 | [![Build Status][ci-image]][ci-url] 6 | [![Test Coverage][coverage-image]][coverage-url] 7 | [![Dependency Status][daviddm-image]][daviddm-url] 8 | [![Follow @FabrixApp on Twitter][twitter-image]][twitter-url] 9 | 10 | Fabrix is a strongly typed modern web application framework for Node.js or even Browsers. It builds on the pedigree of [Rails](http://rubyonrails.org/) and [Grails](https://grails.org/) to accelerate development by adhering to a straightforward, convention-based, API-driven design philosophy. 11 | 12 | _Note: Because Fabrix is lightweight and easily extendable, it's also very __very__ fast and well suited for function based environments._ 13 | 14 | ## Getting Started 15 | 16 | #### Install 17 | _Note: The CLI is under construction, search for [example apps](https://github.com/fabrix-app/example-app) in the meantime_ 18 | 19 | Install the Fabrix CLI. This will help you generate a Fabrix app and more. 20 | 21 | ```sh 22 | $ npm install @fabrix/fab-cli -g 23 | ``` 24 | 25 | #### Generate A New Fabrix App 26 | ```sh 27 | $ fab app 28 | ``` 29 | 30 | #### Start Sewing 31 | 32 | Fabrix uses a CLI to generate scaffolding for new 33 | applications, and to create resources inside the application. 34 | 35 | ```sh 36 | $ fab --help 37 | 38 | Usage: 39 | fab 40 | 41 | Generators: 42 | Create New App 43 | fab app 44 | 45 | Create New Model 46 | fab model 47 | 48 | Create New Controller 49 | fab controller 50 | 51 | Create New Policy 52 | fab policy 53 | 54 | Create New Service 55 | fab service 56 | 57 | Create New Spool 58 | fab spool 59 | ``` 60 | 61 | #### Run 62 | 63 | Once installation is complete, begin weaving! 64 | ```sh 65 | $ npm run compile && node dist/server.js 66 | ``` 67 | 68 | or simply 69 | 70 | ```sh 71 | $ npm start 72 | ``` 73 | 74 | #### Sew on and Sew Forth! 75 | Fabrix is really focused on bringing great libraries together in congruent ways. So if you have something like a web [scrapping library](https://github.com/fabrix-app/spool-scraper), then it's easy to implement that as a Spool and share it with the ever growing fabrix ecosystem. 76 | 77 | ## Spools 78 | 79 | [Spools](https://github.com/search?q=topic%3Aspools+org%3Afabrix-app&type=Repositories) extend the framework's 80 | capabilities and allow developers to leverage existing ecosystem tools through a simple and well-defined API. New features, behavior, APIs, and other functionality can be added to the Fabrix framework through Spools. 81 | 82 | Many Fabrix installations will include some of the following Spools: 83 | 84 | - [router](https://github.com/fabrix-app/spool-router) 85 | - [errors](https://github.com/fabrix-app/spool-errors) 86 | - [i18n](https://github.com/fabrix-app/spool-i18n) 87 | - [repl](https://github.com/fabrix-app/spool-repl) 88 | - [express](https://github.com/fabrix-app/spool-express) 89 | - [sequelize](https://github.com/fabrix-app/spool-sequelize) 90 | 91 | ## Compatibility 92 | 93 | - Windows, Mac, and Linux 94 | - Node 8.0 and newer 95 | 96 | ## Documentation 97 | 98 | See [**fabrix.app/docs**](http://fabrix.app/docs) for complete documentation. 99 | 100 | ## More Resources 101 | 102 | #### Tutorials 103 | Coming soon! 104 | 105 | #### Videos 106 | Coming soon! 107 | 108 | #### Support 109 | - [Live Gitter Chat](https://gitter.im/fabrix-app/Lobby) 110 | - [Twitter](https://twitter.com/FabrixApp) 111 | - [Fabrix.app Website](http://fabrix.app/support) 112 | - [Stackoverflow](http://stackoverflow.com/questions/tagged/fabrix) 113 | 114 | ## FAQ 115 | 116 | See https://github.com/fabrix-app/fabrix/wiki/FAQ 117 | 118 | ## Contributing 119 | We love contributions! Please check out our [Contributor's Guide](https://github.com/fabrix-app/fabrix/blob/master/.github/CONTRIBUTING.md) for more 120 | information on how our projects are organized and how to get started. 121 | 122 | ## Development 123 | Fabrix uses a continuous integration process and all tests must pass for Fabrix to release a new version. CircleCI releases a new version when a PR is merged into master. For local development, you can download [CircleCI's local development tools](https://circleci.com/docs/2.0/local-cli/#installing-the-circleci-local-cli-on-macos-and-linux-distros) and run local tests before submitting a Pull Request. 124 | 125 | Fabrix maintains a high score of coverage tests, any Pull Request should have well written Integration and Unit tests that increase the overall coverage score. 126 | 127 | ### Browser Support 128 | Browser support provided by Webpack is on it's way. 129 | 130 | `webpack --config ./test-browser/webpack.config.js` 131 | 132 | ## License 133 | [MIT](https://github.com/fabrix-app/fabrix/blob/master/LICENSE) 134 | 135 | ## Legacy 136 | Fabrix would not have been possible without the substantial work done by the [Trails.js team](https://github.com/trailsjs). While Fabrix maintains a different code base and system of best practices, none of this would have been possible without the contributions from the Trails community. 137 | 138 | [npm-image]: https://img.shields.io/npm/v/@fabrix/fabrix.svg?style=flat-square 139 | [npm-url]: https://npmjs.org/package/@fabrix/fabrix 140 | [ci-image]: https://img.shields.io/circleci/project/github/fabrix-app/fabrix/master.svg 141 | [ci-url]: https://circleci.com/gh/fabrix-app/fabrix/tree/master 142 | [daviddm-image]: http://img.shields.io/david/fabrix-app/fabrix.svg?style=flat-square 143 | [daviddm-url]: https://david-dm.org/fabrix-app/fabrix 144 | [gitter-image]: http://img.shields.io/badge/+%20GITTER-JOIN%20CHAT%20%E2%86%92-1DCE73.svg?style=flat-square 145 | [gitter-url]: https://gitter.im/fabrix-app/Lobby 146 | [twitter-image]: https://img.shields.io/twitter/follow/FabrixApp.svg?style=social 147 | [twitter-url]: https://twitter.com/FabrixApp 148 | [coverage-image]: https://img.shields.io/codeclimate/coverage/github/fabrix-app/fabrix.svg?style=flat-square 149 | [coverage-url]: https://codeclimate.com/github/fabrix-app/fabrix/coverage 150 | -------------------------------------------------------------------------------- /ROADMAP.md: -------------------------------------------------------------------------------- 1 | # Our Tapestry 2 | 3 | ## Purpose 4 | 5 | The fabrix.app team maintains this document to serve as a forward-looking plan for how the framework will grow and evolve. The goal of the roadmap is to assist developers in planning the future of their projects. Please submit an Issue or Pull Request if you feel that something should be added or changed in this document, and we are happy to discuss. 6 | 7 | ## Organization 8 | 9 | Each major and minor release will be represented by a Github Milestone. Each major version series will be developed on a separate branch. Please submit feature requests, bug reports, and other suggestions as Github Issues for inclusion in a future release. 10 | 11 | ## Release Schedule 12 | 13 | Fabrix intentionally parallels its release schedule with the [Node.js release schedule](https://github.com/nodejs/LTS#lts-plan). 14 | 15 | ![Fabrix Release and Maintenance Roadmap](https://s3.amazonaws.com/fabrix.app/images/Fabrix+Maintenance+Schedule+v4.5.png) 16 | 17 | Major and minor version releases occur according to a regular calendar schedule, and versions are assigned according to semver. Major releases occur in April. The Fabrix team releases a minor version once per month with the latest feature updates for the current series. The fifth minor release, in October of the same year, will be tagged for LTS (Long Term Support) which lasts 18 months. 18 | 19 | ## Upcoming Releases 20 | 21 | ### v2.0 (Sept 2019) 22 | 23 | Fabrix v2 development takes place on the [v2 branch](https://github.com/fabrix-app/fabrix/tree/v2). Some key changes and additions: 24 | - Standardize `Policy` interface 25 | - Stricter Types and Interfaces 26 | -------------------------------------------------------------------------------- /archetype/.gitignore: -------------------------------------------------------------------------------- 1 | # node/npm 2 | node_modules/ 3 | npm-debug.log 4 | .npm 5 | 6 | # fabrix 7 | .tmp/ 8 | dist 9 | # other 10 | *.sw* 11 | .nyc_output 12 | -------------------------------------------------------------------------------- /archetype/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.0.0", 3 | "keywords": [ 4 | "fabrix" 5 | ], 6 | "scripts": { 7 | "build": "tsc -p ./src/tsconfig.release.json", 8 | "lint": "tslint -p ./src", 9 | "watch": "tsc -w -p ./src/tsconfig.release.json", 10 | "test": "npm run compile && nyc mocha", 11 | "prepublishOnly": "npm run compile", 12 | "compile": "npm run clean && npm run build && npm run copy", 13 | "copy": "copyfiles -u 1 src/config/**/*.json dist", 14 | "clean": "rm -rf dist" 15 | }, 16 | "main": "dist/index.js", 17 | "typings": "dist/index.d.ts", 18 | "dependencies": { 19 | "@fabrix/fabrix": "^1.5.9", 20 | "@fabrix/spool-router": "^1.5.1" 21 | }, 22 | "devDependencies": { 23 | "@fabrix/spool-repl": "^1.5.0", 24 | "@fabrix/lint": "^1.0.0-alpha.3", 25 | "@types/node": "~10.3.4", 26 | "copyfiles": "^2.0.0", 27 | "mocha": "^5", 28 | "nyc": "^13.0.1", 29 | "supertest": "^3", 30 | "tslib": "~1.9.0", 31 | "tslint": "~5.10.0", 32 | "tslint-microsoft-contrib": "~5.0.3", 33 | "tsutils": "~2.27.1", 34 | "typescript": "~2.8.1" 35 | }, 36 | "engines": { 37 | "node": ">= 7.6.0 =< 10.0.0", 38 | "npm": ">= 3.10.0" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /archetype/src/api/controllers/index.ts: -------------------------------------------------------------------------------- 1 | export {} 2 | -------------------------------------------------------------------------------- /archetype/src/api/index.ts: -------------------------------------------------------------------------------- 1 | import * as controllers from './controllers' 2 | import * as models from './models' 3 | import * as policies from './policies' 4 | import * as services from './services' 5 | import * as resolvers from './resolvers' 6 | 7 | export { 8 | controllers, 9 | models, 10 | policies, 11 | services, 12 | resolvers 13 | } 14 | -------------------------------------------------------------------------------- /archetype/src/api/models/index.ts: -------------------------------------------------------------------------------- 1 | export {} 2 | -------------------------------------------------------------------------------- /archetype/src/api/policies/index.ts: -------------------------------------------------------------------------------- 1 | export {} 2 | -------------------------------------------------------------------------------- /archetype/src/api/resolvers/index.ts: -------------------------------------------------------------------------------- 1 | export {} 2 | -------------------------------------------------------------------------------- /archetype/src/api/services/index.ts: -------------------------------------------------------------------------------- 1 | export {} 2 | -------------------------------------------------------------------------------- /archetype/src/config/env/development/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Development Env manifest 3 | */ 4 | 5 | import { main } from './main' 6 | 7 | export { 8 | main 9 | } 10 | -------------------------------------------------------------------------------- /archetype/src/config/env/development/main.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Development Configuration 3 | * Merges with (app.config.main) 4 | * 5 | * @see {@link http://fabrix.app/docs/config/main} 6 | * @see {@link http://fabrix.app/docs/config/env} 7 | */ 8 | 9 | import { REPLSpool } from '@fabrix/spool-repl' 10 | 11 | export const main = { 12 | /** 13 | * This array of spools will be merged with the parent config array 14 | */ 15 | spools: [ 16 | REPLSpool 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /archetype/src/config/env/index.ts: -------------------------------------------------------------------------------- 1 | import * as development from './development' 2 | import * as staging from './staging' 3 | import * as production from './production' 4 | import * as testing from './testing' 5 | 6 | export { 7 | development, 8 | staging, 9 | production, 10 | testing 11 | } 12 | -------------------------------------------------------------------------------- /archetype/src/config/env/production/index.ts: -------------------------------------------------------------------------------- 1 | export {} 2 | -------------------------------------------------------------------------------- /archetype/src/config/env/staging/index.ts: -------------------------------------------------------------------------------- 1 | export {} 2 | -------------------------------------------------------------------------------- /archetype/src/config/env/testing/index.ts: -------------------------------------------------------------------------------- 1 | export {} 2 | -------------------------------------------------------------------------------- /archetype/src/config/i18n.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Internationalization / Localization Settings 3 | * (app.config.i18n) 4 | * 5 | * If your app will touch people from all over the world, i18n (or internationalization) 6 | * may be an important part of your international strategy. 7 | * 8 | * @see http://fabrix.app/docs/config/i18n 9 | */ 10 | 11 | export const i18n = { 12 | 13 | /** 14 | * The default locale 15 | */ 16 | lng: 'en', 17 | 18 | /** 19 | * The path to the locales 20 | */ 21 | resources: { 22 | en: require('./locales/en.json') 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /archetype/src/config/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Fabrix Configuration Manifest 3 | * @see {@link http://fabrix.app/docs/config/manifest} 4 | */ 5 | import * as env from './env' 6 | import { i18n } from './i18n' 7 | import { log } from './log' 8 | import { main } from './main' 9 | import { models } from './models' 10 | import { routes } from './routes' 11 | import { stores } from './stores' 12 | import { web } from './web' 13 | 14 | export { 15 | env, 16 | i18n, 17 | log, 18 | main, 19 | models, 20 | routes, 21 | stores, 22 | web 23 | } 24 | -------------------------------------------------------------------------------- /archetype/src/config/locales/en.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /archetype/src/config/log.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Logging Configuration 3 | * (app.config.log) 4 | * 5 | * @see {@link http://fabrix.app/docs/config/log} 6 | */ 7 | 8 | export const log = { 9 | level: 'info' 10 | } 11 | -------------------------------------------------------------------------------- /archetype/src/config/main.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Main App Configuration 3 | * (app.config.main) 4 | * 5 | * @see {@link http://fabrix.app/docs/config/main} 6 | */ 7 | 8 | import { resolve } from 'path' 9 | 10 | /** 11 | * Spools: import spools 12 | */ 13 | import { RouterSpool } from '@fabrix/spool-router' 14 | 15 | export const main = { 16 | 17 | /** 18 | * Order matters. Each module is loaded according to its own 19 | * requirements, however, when there are spools 20 | * with conflicting configuration, the last spool loaded 21 | * takes priority. 22 | */ 23 | spools: [ 24 | RouterSpool 25 | ], 26 | 27 | /** 28 | * Define application paths here. "root" is the only required path. 29 | */ 30 | paths: { 31 | root: resolve(__dirname, '..'), 32 | temp: resolve(__dirname, '..', '.tmp') 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /archetype/src/config/models.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Models Configuration 3 | * (app.config.models) 4 | * 5 | * Configure the model defaults 6 | * 7 | * @see {@link http://fabrix.app/docs/config/models} 8 | */ 9 | export const models = { 10 | /** 11 | * The default store used by models 12 | */ 13 | defaultStore: 'dev', 14 | /** 15 | * The default migration operation if not specified by store 16 | */ 17 | migrate: 'alter' 18 | } 19 | -------------------------------------------------------------------------------- /archetype/src/config/routes.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Routes Configuration 3 | * 4 | * Configure how url patterns map to controllers, views, and static files. 5 | * 6 | * @see {@link http://fabrix.app/docs/config/routes} 7 | */ 8 | 9 | export const routes = { } 10 | -------------------------------------------------------------------------------- /archetype/src/config/stores.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Datastores Configuration 3 | * (app.config.stores) 4 | * 5 | * Configure the ORM layer, connections, etc. 6 | * 7 | * @see {@link http://fabrix.app/docs/config/stores} 8 | */ 9 | 10 | export const stores = { 11 | 12 | } 13 | -------------------------------------------------------------------------------- /archetype/src/config/web.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Web Configuration 3 | * (app.config.web) 4 | * 5 | * Pattern for common Web servers 6 | * 7 | * @see {@link http://fabrix.app/docs/config/web} 8 | */ 9 | 10 | export const web = { 11 | 12 | } 13 | -------------------------------------------------------------------------------- /archetype/src/index.ts: -------------------------------------------------------------------------------- 1 | import * as config from './config/index' 2 | import * as pkg from '../package.json' 3 | import * as api from './api/index' 4 | 5 | export { 6 | config, 7 | pkg, 8 | api 9 | } 10 | -------------------------------------------------------------------------------- /archetype/src/server.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module server 3 | * 4 | * Fabrix framework. 5 | * @see {@link http://fabrix.app} 6 | */ 7 | 8 | import { FabrixApp } from '@fabrix/fabrix' 9 | import * as App from './' 10 | 11 | const server = new FabrixApp(App) 12 | 13 | server.start() 14 | .catch((err: any) => server.stop(err)) 15 | -------------------------------------------------------------------------------- /archetype/src/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "emitDecoratorMetadata": true, 4 | "experimentalDecorators": true, 5 | "baseUrl": "./", 6 | "outDir": "../dist", 7 | "target": "es2016", 8 | "module": "commonjs", 9 | "moduleResolution": "node", 10 | "declaration": true, 11 | "strict": false, 12 | "noImplicitAny": false, 13 | "typeRoots": [ 14 | "node_modules/@types" 15 | ], 16 | "lib": [ 17 | "es2016", 18 | "es2017.object" 19 | ] 20 | }, 21 | "include": [ 22 | "../package.json", 23 | "test/**/*.ts", 24 | "**/*.ts", 25 | "index.ts" 26 | ], 27 | "exclude": [], 28 | "paths": {} 29 | } 30 | -------------------------------------------------------------------------------- /archetype/src/tsconfig.release.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "removeComments": true 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /archetype/src/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@fabrix/lint" 3 | } 4 | -------------------------------------------------------------------------------- /archetype/src/typings/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.json" { 2 | const value: any 3 | export default value 4 | } 5 | -------------------------------------------------------------------------------- /archetype/test/index.js: -------------------------------------------------------------------------------- 1 | const FabrixApp = require('@fabrix/fabrix').FabrixApp 2 | 3 | before(() => { 4 | global.app = new FabrixApp(require('../dist')) 5 | return global.app.start() 6 | }) 7 | 8 | after(() => { 9 | return global.app.stop() 10 | }) 11 | -------------------------------------------------------------------------------- /archetype/test/integration/FabrixApp.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert') 2 | 3 | describe('Fabrix App', () => { 4 | it('should exist', () => { 5 | assert(global.app) 6 | }) 7 | }) 8 | -------------------------------------------------------------------------------- /archetype/test/mocha.opts: -------------------------------------------------------------------------------- 1 | --reporter spec 2 | --recursive 3 | --full-trace 4 | --no-exit 5 | --check-leaks 6 | --timeout 1200000 7 | --globals app,shopID,shopProducts,__core-js_shared__,__coverage__,$$bole 8 | 9 | test/index.js test/**/*.test.js 10 | -------------------------------------------------------------------------------- /lib/Configuration.ts: -------------------------------------------------------------------------------- 1 | import { merge, isArray, defaults, union } from 'lodash' 2 | import { resolve, dirname } from 'path' 3 | import { IllegalAccessError, ConfigValueError } from './errors' 4 | import { requireMainFilename } from './utils' 5 | import { Core } from './Core' 6 | 7 | // Proxy Handler for get requests to the configuration 8 | const ConfigurationProxyHandler: ProxyHandler = { 9 | get (target: any, key: string) { 10 | if (target.has && target.has(key)) { 11 | const value = target.immutable === true ? Object.freeze(target.get(key)) : target.get(key) 12 | return new Proxy(value, ConfigurationProxyHandler) 13 | } 14 | else { 15 | return target.immutable === true ? Object.freeze(target[key]) : target[key] 16 | } 17 | } 18 | } 19 | 20 | /** 21 | * Extend map class for getter/setter tuple config 22 | */ 23 | export class Configuration extends Map { 24 | public immutable: boolean 25 | public env: {} 26 | /** 27 | * Flattens configuration tree 28 | * Recursive 29 | */ 30 | static flattenTree (tree = { }) { 31 | const toReturn: { [key: string]: any } = {} 32 | // Try to flatten and fail if unable to resolve circular object 33 | try { 34 | Object.entries(tree).forEach(([k, v]) => { 35 | // if (typeof v === 'object' && v !== null) { 36 | if ( 37 | v !== null 38 | && v instanceof Object 39 | && typeof v !== 'function' 40 | ) { 41 | // If value is an array, flatten by index and don't try to flatten further 42 | // Configs with Array will throw a warning in v2.0 and an error in v3.0 43 | if (Array.isArray(v)) { 44 | v.forEach((val, i) => { 45 | toReturn[`${k}.${i}`] = val 46 | }) 47 | } 48 | else if (!Core.isNotCircular(v)) { 49 | toReturn[k] = v 50 | } 51 | // If the value is a normal object, keep flattening 52 | else { 53 | const flatObject = Configuration.flattenTree(v) 54 | Object.keys(flatObject).forEach(flatKey => { 55 | toReturn[`${k}.${flatKey}`] = flatObject[flatKey] 56 | }) 57 | } 58 | } 59 | // Other wise, the value is a function, string, or number etc and should stop flattening 60 | toReturn[k] = v 61 | }) 62 | 63 | // Return the consturcted return object 64 | return toReturn 65 | } 66 | catch (err) { 67 | if (err !== Core.BreakException) { 68 | throw new RangeError('Tree is circular and can not be resolved, check that there are no circular references in the config') 69 | } 70 | return toReturn 71 | } 72 | } 73 | 74 | /** 75 | * Defines the initial api resources 76 | */ 77 | static initialResources (tree, resources = []) { 78 | if (tree.hasOwnProperty('main') && tree.main.hasOwnProperty('resources')) { 79 | // Configs with Array will throw a warning in v2.0 and an error in v3.0 80 | if (!isArray(tree.main['resources'])) { 81 | throw new ConfigValueError('if set, main.resources must be an array') 82 | } 83 | return tree.main['resources'] 84 | } 85 | else { 86 | return resources 87 | } 88 | } 89 | 90 | /** 91 | * Copy and merge the provided configuration into a new object, decorated with 92 | * necessary default and environment-specific values. 93 | */ 94 | static buildConfig (initialConfig: {env?: {[key: string]: any}} = { }, nodeEnv?: string) { 95 | const root = resolve(dirname(requireMainFilename())) 96 | const temp = resolve(root, '.tmp') 97 | const envConfig = initialConfig.env && initialConfig.env[nodeEnv] || { } 98 | 99 | const configTemplate = { 100 | main: { 101 | resources: Configuration.initialResources(initialConfig), 102 | lockResources: false, 103 | maxListeners: 128, 104 | spools: [ ], 105 | paths: { 106 | root: root, 107 | temp: temp, 108 | sockets: resolve(temp, 'sockets'), 109 | logs: resolve(temp, 'log') 110 | }, 111 | freezeConfig: true, 112 | createPaths: true 113 | } 114 | } 115 | 116 | return merge(configTemplate, initialConfig, envConfig, { env: nodeEnv }) 117 | } 118 | 119 | constructor ( 120 | configTree: {[key: string]: any} = { }, 121 | processEnv: { 122 | [key: string]: any, 123 | NODE_ENV?: string 124 | } = { } 125 | ) { 126 | // Constants for configuration 127 | const config = Configuration.buildConfig(configTree, processEnv['NODE_ENV']) 128 | const configEntries = Object.entries(Configuration.flattenTree(config)) 129 | // Add to the map constructor 130 | super(configEntries) 131 | 132 | // Initial values 133 | this.immutable = false 134 | this.env = processEnv 135 | 136 | // Bind methods 137 | this.get = this.get.bind(this) 138 | this.set = this.set.bind(this) 139 | this.entries = this.entries.bind(this) 140 | this.has = this.has.bind(this) 141 | 142 | // Return Proxy 143 | return new Proxy(this, ConfigurationProxyHandler) 144 | } 145 | 146 | /** 147 | * Recursively sets the tree values on the config map 148 | */ 149 | private _reverseFlattenSet(key, value) { 150 | if (/\.[0-9a-z]+$/.test(key)) { 151 | const decedent = (key).match(/\.([0-9a-z]+)$/)[1] 152 | const parent = key.replace(/\.[0-9a-z]+$/, '') 153 | const proto = Array.isArray(value) ? [] : {} 154 | const newParentValue = Core.defaultsDeep({[decedent]: value}, this.get(parent) || proto) 155 | super.set(key, value) 156 | // Recursively reverse flatten the set back up the tree 157 | return this._reverseFlattenSet(parent, newParentValue) 158 | } 159 | else { 160 | // This is as high as it goes 161 | return super.set(key, value) 162 | } 163 | } 164 | /** 165 | * Flattens what is being called to .set 166 | */ 167 | private _flattenSet(key, value) { 168 | if ( 169 | value !== null 170 | && value instanceof Object 171 | && typeof value !== 'function' 172 | && !Array.isArray(value) 173 | ) { 174 | // Flatten the new value 175 | const configEntries = Object.entries(Configuration.flattenTree({[key]: value})) 176 | // Set the flat values 177 | configEntries.forEach(([_key, _value]) => { 178 | return super.set(_key, _value) 179 | }) 180 | } 181 | // Reverse flatten up the tree 182 | return this._reverseFlattenSet(key, value) 183 | } 184 | /** 185 | * Throws IllegalAccessError if the configuration has already been set to immutable 186 | * and an attempt to set value occurs. 187 | */ 188 | set (key: string, value: any) { 189 | if (this.immutable === true) { 190 | throw new IllegalAccessError('Cannot set properties directly on config. Use .set(key, value) (immutable)') 191 | } 192 | return this._flattenSet(key, value) 193 | } 194 | 195 | /** 196 | * Merge tree into this configuration if allowed. Return overwritten keys 197 | */ 198 | merge (configTree: {[key: string]: any}, configAction = 'hold'): { hasKey: boolean, key: any }[] { 199 | const configEntries = Object.entries(Configuration.flattenTree(configTree)) 200 | return configEntries.map(([ key, value ]) => { 201 | const hasKey = this.has(key) 202 | // If the key has never been set, it is added to the config 203 | // If configAction is set to hold, then it will replace the initial config 204 | if (!hasKey || configAction === 'hold') { 205 | this.set(key, value) 206 | } 207 | // If configAction is set to merge, it will default values over the initial config 208 | else if (hasKey && configAction === 'merge') { 209 | if (value === null) { 210 | // Do Nothing 211 | } 212 | else if (typeof value === 'undefined') { 213 | // Do Nothing 214 | } 215 | else if (Array.isArray(value)) { 216 | // Do Nothing 217 | } 218 | else if (typeof value === 'number') { 219 | // Do Nothing 220 | } 221 | else if (typeof value === 'string') { 222 | // Do Nothing 223 | } 224 | else if (typeof value === 'function') { 225 | // Do Nothing 226 | } 227 | else { 228 | this.set(key, Core.defaultsDeep(this.get(key), value)) 229 | } 230 | } 231 | // If configAction is replaceable, and the key already exists, it's ignored completely 232 | // This is because it was set by a higher level app config 233 | else if (hasKey && configAction === 'replaceable') { 234 | // Do Nothing 235 | } 236 | return { hasKey, key } 237 | }) 238 | } 239 | 240 | /** 241 | * Prevent changes to the app configuration 242 | */ 243 | freeze (): void { 244 | this.immutable = true 245 | } 246 | 247 | /** 248 | * Allow changes to the app configuration 249 | */ 250 | unfreeze (): void { 251 | this.immutable = false 252 | } 253 | } 254 | 255 | -------------------------------------------------------------------------------- /lib/Core.ts: -------------------------------------------------------------------------------- 1 | import { union, defaultsDeep, isArray, toArray, mergeWith } from 'lodash' 2 | import { FabrixApp } from './' 3 | import { Templates } from './' 4 | import { 5 | ApiNotDefinedError, 6 | ConfigNotDefinedError, 7 | ConfigValueError, 8 | GraphCompletenessError, 9 | IllegalAccessError, 10 | NamespaceConflictError, 11 | PackageNotDefinedError, 12 | TimeoutError, 13 | SanityError, 14 | SpoolError, 15 | ValidationError 16 | } from './errors' 17 | 18 | import { FabrixService } from './common/Service' 19 | import { FabrixController } from './common/Controller' 20 | import { FabrixPolicy } from './common/Policy' 21 | import { FabrixModel } from './common/Model' 22 | import { FabrixResolver } from './common/Resolver' 23 | import { Spool, ILifecycle, FabrixGeneric } from './common' 24 | 25 | declare global { 26 | namespace NodeJS { 27 | interface Global { 28 | [key: string]: any 29 | } 30 | } 31 | } 32 | 33 | export const Errors = { 34 | ApiNotDefinedError, 35 | ConfigNotDefinedError, 36 | ConfigValueError, 37 | GraphCompletenessError, 38 | IllegalAccessError, 39 | NamespaceConflictError, 40 | PackageNotDefinedError, 41 | TimeoutError, 42 | SanityError, 43 | SpoolError, 44 | ValidationError 45 | } 46 | 47 | export const Core = { 48 | // An Exception convenience: added v1.5 49 | BreakException: {}, 50 | // Methods reserved so that they are not autobound 51 | reservedMethods: [ 52 | 'app', 53 | 'api', 54 | 'log', 55 | '__', // this reserved method comes from i18n 56 | 'constructor', 57 | 'undefined', 58 | 'methods', 59 | 'config', 60 | 'schema', 61 | // Should additional resource types be added to reserved or should this be removed completely? 62 | 'services', 63 | 'models' 64 | ], 65 | 66 | // Deprecated v1.6 67 | globals: Object.freeze(Object.assign({ 68 | Service: FabrixService, 69 | Controller: FabrixController, 70 | Policy: FabrixPolicy, 71 | Model: FabrixModel, 72 | Resolver: FabrixResolver 73 | }, Errors)), 74 | 75 | // Deprecated v1.6 76 | globalPropertyOptions: Object.freeze({ 77 | writable: false, 78 | enumerable: false, 79 | configurable: false 80 | }), 81 | 82 | /** 83 | * Deprecated v1.6 84 | * Prepare the global namespace with required Fabrix types. Ignore identical 85 | * values already present; fail on non-matching values. 86 | * 87 | * @throw NamespaceConflictError 88 | */ 89 | assignGlobals (): void { 90 | Object.entries(Core.globals).forEach(([name, type]) => { 91 | if (global[name] === type) { 92 | return 93 | } 94 | if (global[name] && global[name] !== type) { 95 | throw new NamespaceConflictError(name, Object.keys(Core.globals)) 96 | } 97 | const descriptor = Object.assign({ value: type }, Core.globalPropertyOptions) 98 | Object.defineProperty(global, name, descriptor) 99 | }) 100 | }, 101 | 102 | /** 103 | * Bind the context of API resource methods. 104 | */ 105 | bindMethods (app: FabrixApp, resource: string): any { 106 | return Object.entries(app.api[resource] || { }) 107 | .map(([ resourceName, Resource ]: [string, any]) => { 108 | const objContext = Resource 109 | const obj = new objContext(app) 110 | 111 | obj.methods = Core.getClassMethods(obj) || [ ] 112 | Object.entries(obj.methods).forEach(([ _, method]: [any, string]) => { 113 | obj[method] = obj[method].bind(obj) 114 | }) 115 | return [ resourceName, obj ] 116 | }) 117 | .reduce((result, [ resourceName, _resource ]: [string, any]) => Object.assign(result, { 118 | [resourceName]: _resource 119 | }), { }) 120 | }, 121 | 122 | /** 123 | * Instantiate resource classes and bind resource methods 124 | */ 125 | bindResourceMethods(app: FabrixApp, resources: string[]): void { 126 | resources.forEach(resource => { 127 | try { 128 | app[resource] = Core.bindMethods(app, resource) 129 | } 130 | catch (err) { 131 | app.log.error(err) 132 | } 133 | }) 134 | }, 135 | 136 | /** 137 | * Traverse prototype chain and aggregate all class method names 138 | */ 139 | getClassMethods (obj: any): string[] { 140 | const props: string[] = [ ] 141 | const objectRoot = new Object() 142 | 143 | while (!obj.isPrototypeOf(objectRoot)) { 144 | Object.getOwnPropertyNames(obj).forEach(prop => { 145 | if ( 146 | props.indexOf(prop) === -1 147 | && !Core.reservedMethods.some(p => p === prop) 148 | && typeof obj[prop] === 'function' 149 | ) { 150 | props.push(prop) 151 | } 152 | }) 153 | obj = Object.getPrototypeOf(obj) 154 | } 155 | return props 156 | }, 157 | 158 | /** 159 | * Get the property names of an Object 160 | */ 161 | getPropertyNames(obj) { 162 | return Object.getOwnPropertyNames(obj.prototype) 163 | }, 164 | 165 | /** 166 | * If the object has a prototype property 167 | */ 168 | hasPrototypeProperty(obj, proto) { 169 | return obj.prototype.hasOwnProperty(proto) 170 | }, 171 | 172 | /** 173 | * Merge the Prototype of uninitiated classes 174 | */ 175 | mergePrototype(obj, next, proto) { 176 | obj.prototype[proto] = next.prototype[proto] 177 | }, 178 | 179 | /** 180 | * Merge the app api resources with the ones provided by the spools 181 | * Given that they are allowed by app.config.main.resources 182 | */ 183 | mergeApi (app: FabrixApp) { 184 | const spools = Object.keys(app.spools).reverse() 185 | app.resources.forEach(resource => { 186 | spools.forEach(s => { 187 | // Add the defaults from the spools Apis 188 | defaultsDeep(app.api, app.spools[s].api) 189 | // Deep merge the Api 190 | Core.mergeApiResource(app, app.spools[s], resource) 191 | }) 192 | }) 193 | }, 194 | 195 | /** 196 | * Adds Api resources that were not merged by default 197 | */ 198 | mergeApiResource (app: FabrixApp, spool: Spool, resource: string) { 199 | if (app.api.hasOwnProperty(resource) && spool.api.hasOwnProperty(resource)) { 200 | Object.keys(spool.api[resource]).forEach(method => { 201 | Core.mergeApiResourceMethod(app, spool, resource, method) 202 | }) 203 | } 204 | }, 205 | 206 | /** 207 | * Adds Api resource methods that were not merged by default 208 | */ 209 | mergeApiResourceMethod (app: FabrixApp, spool: Spool, resource: string, method: string) { 210 | if (spool.api[resource].hasOwnProperty(method) && app.api[resource].hasOwnProperty(method)) { 211 | const spoolProto = Core.getPropertyNames(spool.api[resource][method]) 212 | spoolProto.forEach(proto => { 213 | if (!Core.hasPrototypeProperty(app.api[resource][method], proto)) { 214 | Core.mergePrototype(app.api[resource][method], spool.api[resource][method], proto) 215 | app.log.silly(`${spool.name}.api.${resource}.${method}.${proto} extending app.api.${resource}.${method}.${proto}`) 216 | } 217 | }) 218 | } 219 | }, 220 | 221 | /** 222 | * Merge the spool api resources with the ones provided by other spools 223 | * Given that they are allowed by app.config.main.resources 224 | */ 225 | mergeSpoolApi (app: FabrixApp, spool: Spool) { 226 | app.resources = union(app.resources, Object.keys(app.api), Object.keys(spool.api)) 227 | 228 | const spools = Object.keys(app.spools) 229 | .filter(s => s !== spool.name) 230 | 231 | app.resources.forEach(resource => { 232 | spools.forEach(s => { 233 | Core.mergeSpoolApiResource(app, app.spools[s], spool, resource) 234 | }) 235 | }) 236 | }, 237 | 238 | /** 239 | * Merge two Spools Api Resources's in order of their load 240 | */ 241 | mergeSpoolApiResource (app: FabrixApp, spool: Spool, next: Spool, resource: string) { 242 | if (spool && spool.api.hasOwnProperty(resource) && next.api.hasOwnProperty(resource)) { 243 | Object.keys(next.api[resource]).forEach(method => { 244 | Core.mergeSpoolApiResourceMethod(app, spool, next, resource, method) 245 | }) 246 | } 247 | }, 248 | 249 | /** 250 | * Merge two Spools Api Resources Method's in order of their load 251 | */ 252 | mergeSpoolApiResourceMethod (app: FabrixApp, spool: Spool, next: Spool, resource: string, method: string) { 253 | if (spool && spool.api[resource].hasOwnProperty(method) && next.api[resource].hasOwnProperty(method)) { 254 | const spoolProto = Core.getPropertyNames(spool.api[resource][method]) 255 | spoolProto.forEach(proto => { 256 | if (!Core.hasPrototypeProperty(next.api[resource][method], proto)) { 257 | Core.mergePrototype(next.api[resource][method], spool.api[resource][method], proto) 258 | app.log.silly(`${spool.name}.api.${resource}.${method}.${proto} extending ${next.name}.api.${resource}.${method}.${proto}`) 259 | } 260 | }) 261 | } 262 | }, 263 | 264 | /** 265 | * Merge extensions provided by spools into the app 266 | */ 267 | mergeExtensions ( 268 | app: FabrixApp, 269 | spool: Spool 270 | ): void { 271 | const extensions = spool.extensions || {} 272 | for (const ext of Object.keys(extensions)) { 273 | if (!extensions.hasOwnProperty(ext)) { 274 | continue 275 | } 276 | if (app.hasOwnProperty(ext)) { 277 | app.log.warn(`Spool Extension ${spool.name}.${ext} overriding app.${ext}`) 278 | } 279 | Object.defineProperty(app, ext, spool.extensions[ext]) 280 | } 281 | }, 282 | 283 | defaultsDeep: (...args) => { 284 | const output = {} 285 | toArray(args).reverse().forEach(function (item) { 286 | mergeWith(output, item, function (objectValue, sourceValue) { 287 | return isArray(sourceValue) ? sourceValue : undefined 288 | }) 289 | }) 290 | return output 291 | }, 292 | 293 | collector: (stack, key, val) => { 294 | let idx: any = stack[stack.length - 1].indexOf(key) 295 | try { 296 | const props: any = Object.keys(val) 297 | if (!props.length) { 298 | throw props 299 | } 300 | props.unshift({idx: idx}) 301 | stack.push(props) 302 | } 303 | catch (e) { 304 | while (!(stack[stack.length - 1].length - 2)) { 305 | idx = stack[stack.length - 1][0].idx 306 | stack.pop() 307 | } 308 | 309 | if (idx + 1) { 310 | stack[stack.length - 1].splice(idx, 1) 311 | } 312 | } 313 | return val 314 | }, 315 | 316 | isNotCircular: (obj): boolean => { 317 | let stack = [[]] 318 | 319 | try { 320 | return !!JSON.stringify(obj, Core.collector.bind(null, stack)) 321 | } 322 | catch (e) { 323 | if (e.message.indexOf('circular') !== -1) { 324 | let idx = 0 325 | let path = '' 326 | let parentProp = '' 327 | while (idx + 1) { 328 | idx = stack.pop()[0].idx 329 | parentProp = stack[stack.length - 1][idx] 330 | if (!parentProp) { 331 | break 332 | } 333 | path = '.' + parentProp + path 334 | } 335 | } 336 | return false 337 | } 338 | }, 339 | 340 | /** 341 | * Create configured paths if they don't exist and target is Node.js 342 | */ 343 | async createDefaultPaths (app: FabrixApp): Promise { 344 | const paths: {[key: string]: string} = app.config.get('main.paths') || { } 345 | const target: string = app.config.get('main.target') || 'node' 346 | if (target !== 'browser') { 347 | const mkdirp = await import('mkdirp') 348 | for (const [, dir] of Object.entries(paths)) { 349 | await mkdirp(dir, null, function (err: Error) { 350 | if (err) { 351 | app.log.error(err) 352 | } 353 | }) 354 | } 355 | } 356 | }, 357 | 358 | /** 359 | * Bind listeners to fabrix application events 360 | */ 361 | bindApplicationListeners (app: FabrixApp): void { 362 | app.once('spool:all:configured', () => { 363 | if (app.config.get('main.freezeConfig') === false) { 364 | app.log.warn('freezeConfig is disabled. Configuration will not be frozen.') 365 | app.log.warn('Please only use this flag for testing/debugging purposes.') 366 | } 367 | else { 368 | app.config.freeze() 369 | } 370 | }) 371 | app.once('spool:all:initialized', () => { 372 | app.log.silly(Templates.silly.initialized) 373 | app.log.info(Templates.info.initialized) 374 | }) 375 | app.once('spool:all:sane', () => { 376 | app.log.silly(Templates.silly.sane) 377 | app.log.info(Templates.info.sane) 378 | }) 379 | app.once('fabrix:ready', () => { 380 | app.log.info(Templates.info.ready(app)) 381 | app.log.debug(Templates.debug.ready(app)) 382 | app.log.silly(Templates.silly.ready(app)) 383 | 384 | app.log.info(Templates.hr) 385 | app.log.info(Templates.docs) 386 | }) 387 | app.once('fabrix:stop', () => { 388 | app.log.info(Templates.info.stop) 389 | app.config.unfreeze() 390 | }) 391 | }, 392 | 393 | /** 394 | * Bind lifecycle boundary event listeners. That is, when all spools have 395 | * completed a particular phase, e.g. "configure" or "initialize", emit an 396 | * :all: event. 397 | */ 398 | bindSpoolPhaseListeners ( 399 | app: FabrixApp, 400 | spools: Spool[] = [] 401 | ): void { 402 | // TODO, eliminate waiting on lifecycle events that will not exist 403 | // https://github.com/fabrix-app/fabrix/issues/14 404 | const validatedEvents = spools.map(spool => `spool:${spool.name}:validated`) 405 | const configuredEvents = spools.map(spool => `spool:${spool.name}:configured`) 406 | const initializedEvents = spools.map(spool => `spool:${spool.name}:initialized`) 407 | const sanityEvents = spools.map(spool => `spool:${spool.name}:sane`) 408 | 409 | app.after(configuredEvents) 410 | .then(async () => { 411 | await this.createDefaultPaths(app) 412 | app.emit('spool:all:configured') 413 | }) 414 | .catch(err => { 415 | app.log.error(err) 416 | throw err 417 | }) 418 | 419 | app.after(validatedEvents) 420 | .then(() => app.emit('spool:all:validated')) 421 | .catch(err => { 422 | app.log.error(err) 423 | throw err 424 | }) 425 | 426 | app.after(initializedEvents) 427 | .then(() => { 428 | app.emit('spool:all:initialized') 429 | }) 430 | .catch(err => { 431 | app.log.error(err) 432 | throw err 433 | }) 434 | 435 | app.after(sanityEvents) 436 | .then(() => { 437 | app.emit('spool:all:sane') 438 | app.emit('fabrix:ready') 439 | }) 440 | .catch(err => { 441 | app.log.error(err) 442 | throw err 443 | }) 444 | }, 445 | 446 | /** 447 | * Bind individual lifecycle method listeners. That is, when each spool 448 | * completes each lifecycle, fire individual events for those spools. 449 | */ 450 | bindSpoolMethodListeners ( 451 | app: FabrixApp, 452 | spool: Spool 453 | ) { 454 | const lifecycle = spool.lifecycle 455 | 456 | app.after((lifecycle.sanity.listen).concat('spool:all:initialized')) 457 | .then(() => app.log.debug('spool: sanity check', spool.name)) 458 | .then(() => spool.stage = 'sanity') 459 | .then(() => spool.sanity()) 460 | .then(() => app.emit(`spool:${spool.name}:sane`)) 461 | .then(() => spool.stage = 'sane') 462 | .catch(err => this.handlePromiseRejection(app, err)) 463 | 464 | app.after((lifecycle.initialize.listen).concat('spool:all:configured')) 465 | .then(() => app.log.debug('spool: initializing', spool.name)) 466 | .then(() => spool.stage = 'initializing') 467 | .then(() => spool.initialize()) 468 | .then(() => app.emit(`spool:${spool.name}:initialized`)) 469 | .then(() => spool.stage = 'initialized') 470 | .catch(err => this.handlePromiseRejection(app, err)) 471 | 472 | app.after((lifecycle.configure.listen).concat('spool:all:validated')) 473 | .then(() => app.log.debug('spool: configuring', spool.name)) 474 | .then(() => spool.stage = 'configuring') 475 | .then(() => spool.configure()) 476 | .then(() => app.emit(`spool:${spool.name}:configured`)) 477 | .then(() => spool.stage = 'configured') 478 | .catch(err => this.handlePromiseRejection(app, err)) 479 | 480 | app.after(['fabrix:start']) 481 | .then(() => app.log.debug('spool: validating', spool.name)) 482 | .then(() => spool.stage = 'validating') 483 | .then(() => spool.validate()) 484 | .then(() => app.emit(`spool:${spool.name}:validated`)) 485 | .then(() => spool.stage = 'validated') 486 | .catch(err => this.handlePromiseRejection(app, err)) 487 | }, 488 | 489 | /** 490 | * Handle a promise rejection 491 | */ 492 | handlePromiseRejection (app: FabrixApp, err: Error): void { 493 | app.log.error(err) 494 | throw err 495 | 496 | return 497 | } 498 | 499 | } 500 | -------------------------------------------------------------------------------- /lib/Fabrix.ts: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from 'events' 2 | import { union } from 'lodash' 3 | import { Core } from './Core' 4 | import { Configuration } from './Configuration' 5 | import { LoggerProxy } from './LoggerProxy' 6 | import { Spool, IApi, IPkg, IConfig, IEnv } from './common' 7 | import * as Errors from './errors' 8 | import * as pkg from '../package.json' 9 | import { FabrixGeneric } from './common/Generic' 10 | import { ServerSpool } from './common/spools/server' 11 | import { ExtensionSpool } from './common/spools/extension' 12 | import { DatastoreSpool } from './common/spools/datastore' 13 | import { SystemSpool } from './common/spools/system' 14 | import { ToolSpool } from './common/spools/tool' 15 | import { MiscSpool } from './common/spools/misc' 16 | import { enumerable } from './common/decorators/enumerable' 17 | import { IVersions } from './common/interfaces/IVersions' 18 | 19 | // inject Error and Resource types into the global namespace 20 | // Deprecate Globals v1.6 21 | // Core.assignGlobals() 22 | 23 | /** 24 | * The Fabrix Application. Merges the configuration and API resources 25 | * loads Spools, initializes logging and event listeners. 26 | */ 27 | export interface FabrixApp extends EventEmitter { 28 | [key: string]: any 29 | } 30 | 31 | export class FabrixApp extends EventEmitter { 32 | private _logger: LoggerProxy 33 | private _env: IEnv 34 | private _pkg: any // IPkg 35 | private _versions: IVersions 36 | private _config: Configuration 37 | private _api: IApi 38 | private _fabrix: FabrixApp 39 | private _spools: {[key: string]: Spool | ServerSpool | ExtensionSpool | DatastoreSpool | SystemSpool | ToolSpool | MiscSpool } 40 | private _resources: string[] = [ ] 41 | 42 | // Deprecate Globals v1.6 43 | // Fabrix can have any namespace of resource as long as it's unique 44 | // public controllers: {[key: string]: FabrixController } 45 | // public services: {[key: string]: FabrixService } 46 | // public policies: {[key: string]: FabrixPolicy } 47 | // public models: {[key: string]: FabrixModel } 48 | // public resolvers: {[key: string]: FabrixResolver } 49 | 50 | /** 51 | * @param app.pkg The application package.json 52 | * @param app.api The application api (api/ folder) 53 | * @param app.config The application configuration (config/ folder) 54 | * 55 | * Initialize the Fabrix Application and its EventEmitter parent class. Set 56 | * some necessary default configuration. 57 | */ 58 | constructor (app: { 59 | pkg: IPkg, 60 | api: IApi, 61 | config: IConfig 62 | }) { 63 | super() 64 | 65 | if (!app) { 66 | throw new RangeError('No app definition provided to Fabrix constructor') 67 | } 68 | if (!app.pkg) { 69 | throw new Errors.PackageNotDefinedError() 70 | } 71 | if (!app.api) { 72 | throw new Errors.ApiNotDefinedError() 73 | } 74 | 75 | // set the process node env if not established by environment 76 | if (!process.env.NODE_ENV) { 77 | process.env.NODE_ENV = 'development' 78 | } 79 | 80 | // ensure process.env is an immutable object 81 | const processEnv = Object.freeze(Object.assign({}, JSON.parse(JSON.stringify(process.env)))) 82 | 83 | Object.defineProperties(this, { 84 | _logger: { 85 | value: new LoggerProxy(this), 86 | enumerable: false 87 | }, 88 | _env: { 89 | value: processEnv, 90 | enumerable: false 91 | }, 92 | _pkg: { 93 | value: app.pkg, 94 | enumerable: false 95 | }, 96 | _versions: { 97 | value: process.versions, 98 | enumerable: false 99 | }, 100 | _config: { 101 | value: new Configuration(app.config, processEnv), 102 | enumerable: false 103 | }, 104 | _spools: { 105 | value: {}, 106 | enumerable: false 107 | }, 108 | _api: { 109 | value: app.api, 110 | enumerable: false 111 | }, 112 | _fabrix: { 113 | value: pkg, 114 | enumerable: false 115 | } 116 | }) 117 | 118 | // Set the max listeners from the config 119 | this.setMaxListeners(this.config.get('main.maxListeners')) 120 | 121 | // Set the resources from the configuration (this bypasses the setter with the initial config 122 | // in case the resourceLock is configured) 123 | this._resources = this.config.get('main.resources') 124 | // See if additional resources can be set 125 | this.resources = union(Object.keys(app.api), this.config.get('main.resources')) 126 | 127 | // Set each api resource to make sure it's provided as an object in the app 128 | this.resources.forEach(resource => { 129 | app.api[resource] = app.api[resource] || (app.api[resource] = { }) 130 | }) 131 | 132 | // instantiate spools TOTO type of Spool 133 | this.config.get('main.spools').forEach((NewSpool: any) => { 134 | try { 135 | // Create new Instance of the Spool 136 | const spoolContext = NewSpool 137 | const spool = new spoolContext(this, {}) 138 | // Add the spool instance to the app.spools namespace 139 | this.spools[spool.name] = spool 140 | // Reconcile the spool.config with the app.config 141 | this.config.merge(spool.config, spoolContext.configAction) 142 | // Merge extensions into app. 143 | Core.mergeExtensions(this, spool) 144 | // Merge the spool.api with app.api 145 | Core.mergeSpoolApi(this, spool) 146 | // Bind the Spool Listeners to app.emit 147 | Core.bindSpoolMethodListeners(this, spool) 148 | } 149 | catch (e) { 150 | console.log(e.stack) 151 | throw new Errors.SpoolError(Spool, e, 'constructor') 152 | } 153 | }) 154 | 155 | // Merge the API from the spools 156 | Core.mergeApi(this) 157 | // Instantiate resource classes and bind resource methods 158 | Core.bindResourceMethods(this, this.resources) 159 | // Bind Application Listeners 160 | Core.bindApplicationListeners(this) 161 | // Bind the Phase listeners for the Spool lifecycle 162 | Core.bindSpoolPhaseListeners(this, Object.values(this.spools)) 163 | 164 | this.emit('fabrix:constructed') 165 | } 166 | 167 | // @enumerable(false) 168 | get logger (): LoggerProxy { 169 | return this._logger 170 | } 171 | 172 | // @enumerable(false) 173 | get env (): IEnv { 174 | return this._env 175 | } 176 | 177 | // @enumerable(false) 178 | get pkg (): IPkg { 179 | return this._pkg 180 | } 181 | 182 | // @enumerable(false) 183 | get versions (): IVersions { 184 | return this._versions 185 | } 186 | 187 | // @enumerable(false) 188 | get config (): Configuration { 189 | return this._config 190 | } 191 | 192 | /** 193 | * Gets the package.json of the Fabrix module 194 | */ 195 | // @enumerable(false) 196 | get fabrix (): FabrixApp { 197 | return this._fabrix 198 | } 199 | 200 | /** 201 | * Gets the Spools that have been installed 202 | */ 203 | // @enumerable(false) 204 | get spools (): {[key: string]: Spool} { 205 | return this._spools 206 | } 207 | 208 | /** 209 | * Gets the api 210 | */ 211 | // @enumerable(false) 212 | get api (): {[key: string]: FabrixGeneric | {}} { 213 | return this._api 214 | } 215 | 216 | /** 217 | * Return the Fabrix logger 218 | * fires fabrix:log:* log events 219 | */ 220 | // @enumerable(false) 221 | get log (): LoggerProxy { 222 | return this.logger 223 | } 224 | 225 | /** 226 | * Sets available/allowed resources from Api and Spool Apis 227 | */ 228 | set resources (values: string[]) { 229 | if (!this.config.get('main.lockResources')) { 230 | this._resources = Object.assign([], Configuration.initialResources(this.config, values)) 231 | this.config.set('main.resources', this._resources) 232 | } 233 | } 234 | 235 | /** 236 | * Gets the Api resources that have been set 237 | */ 238 | // @enumerable(false) 239 | get resources(): string[] { 240 | return this._resources 241 | } 242 | 243 | /** 244 | * Start the App. Load all Spools. 245 | */ 246 | async start (): Promise { 247 | this.emit('fabrix:start') 248 | await this.after(['fabrix:ready']) 249 | return this 250 | } 251 | 252 | /** 253 | * Shutdown. Unbind listeners, unload spools. 254 | */ 255 | async stop (error?): Promise { 256 | this.emit('fabrix:stop') 257 | if (error) { 258 | this.emit('fabrix:stop:error', error) 259 | } 260 | 261 | await Promise 262 | .all(Object.values(this.spools).map(spool => { 263 | spool.stage = 'unloading' 264 | this.log.debug('Unloading spool', spool.name, '...') 265 | return spool.unload() 266 | })) 267 | .then(() => { 268 | Object.values(this.spools).forEach(spool => { spool.stage = 'unloaded' }) 269 | this.log.debug('All spools unloaded. Done.') 270 | this.removeAllListeners() 271 | }) 272 | .catch(err => { 273 | this.log.error(err, 'while handling stop.') 274 | throw err 275 | }) 276 | 277 | return this 278 | } 279 | 280 | /** 281 | * Resolve Promise once ANY of the events in the list have emitted. 282 | */ 283 | async onceAny (events: string[] = []): Promise { 284 | if (!Array.isArray(events)) { 285 | events = [events] 286 | } 287 | 288 | let resolveCallback: any 289 | 290 | return Promise 291 | .race(events.map((eventName: string) => { 292 | return new Promise(resolve => { 293 | resolveCallback = resolve 294 | this.once(eventName, resolveCallback) 295 | }) 296 | })) 297 | .then((...args: string[]) => { 298 | args = args.filter(n => n) 299 | events.forEach((eventName: string) => this.removeListener(eventName, resolveCallback)) 300 | return args 301 | }) 302 | .catch(err => { 303 | this.log.error(err, 'handling onceAny events', events) 304 | throw err 305 | }) 306 | } 307 | 308 | /** 309 | * Resolve Promise once all events in the list have emitted. Also accepts 310 | * a callback. 311 | */ 312 | async after (events: string | string[] = []): Promise { 313 | if (!Array.isArray(events)) { 314 | events = [ events ] 315 | } 316 | 317 | return Promise 318 | .all(events.map((eventName: string | string[]) => { 319 | return new Promise(resolve => { 320 | if (eventName instanceof Array) { 321 | resolve(this.onceAny(eventName)) 322 | } 323 | else { 324 | this.once(eventName, resolve) 325 | } 326 | }) 327 | })) 328 | .catch(err => { 329 | this.log.error(err, 'handling after events', events) 330 | throw err 331 | }) 332 | } 333 | } 334 | -------------------------------------------------------------------------------- /lib/LoggerProxy.ts: -------------------------------------------------------------------------------- 1 | import { FabrixApp } from './' 2 | import { FabrixGeneric } from './common/Generic' 3 | 4 | // declare global { 5 | // interface Console { 6 | // log: (message?: any, ...optionalParams: any[]) => void 7 | // info: (message?: any, ...optionalParams: any[]) => void 8 | // debug: (message?: any, ...optionalParams: any[]) => void 9 | // warn: (message?: any, ...optionalParams: any[]) => void 10 | // error: (message?: any, ...optionalParams: any[]) => void 11 | // silly: (message?: any, ...optionalParams: any[]) => void 12 | // } 13 | // } 14 | // 15 | // export interface LoggerProxy { 16 | // // [key: string]: (message?: any, ...optionalParams: any[]) => void 17 | // log: (message?: any, ...optionalParams: any[]) => void 18 | // info: (message?: any, ...optionalParams: any[]) => void 19 | // debug: (message?: any, ...optionalParams: any[]) => void 20 | // warn: (message?: any, ...optionalParams: any[]) => void 21 | // error: (message?: any, ...optionalParams: any[]) => void 22 | // silly: (message?: any, ...optionalParams: any[]) => void 23 | // app: FabrixApp 24 | // } 25 | 26 | // export interface LoggerProxyProxy { 27 | // // [key: string]: (message?: any, ...optionalParams: any[]) => void 28 | // log: (message?: any, ...optionalParams: any[]) => void 29 | // info: (message?: any, ...optionalParams: any[]) => void 30 | // debug: (message?: any, ...optionalParams: any[]) => void 31 | // warn: (message?: any, ...optionalParams: any[]) => void 32 | // error: (message?: any, ...optionalParams: any[]) => void 33 | // silly: (message?: any, ...optionalParams: any[]) => void 34 | // app: FabrixApp 35 | // } 36 | 37 | // export interface LoggerProxy { 38 | // (message?: any, ...optionalParams: any[]): void 39 | // } 40 | 41 | export class LoggerProxy extends FabrixGeneric { 42 | public app: FabrixApp 43 | public error 44 | public warn 45 | public info 46 | public debug 47 | public silly 48 | public dir 49 | public time 50 | public timeEnd 51 | public trace 52 | public assert 53 | 54 | public levelHierarchy = ['silly', 'debug', 'dir', 'timeEnd', 'trace', 'assert', 'time', 'info', 'warn', 'error'] 55 | 56 | // TODO The proxy is not telling JavaScript compilier that there is a label called timeEnd 57 | // TODO the proxy is not allowing a trace 58 | /** 59 | * Instantiate Proxy; bind log events to default console.log 60 | */ 61 | constructor (app: FabrixApp) { 62 | super(app) 63 | 64 | this.app.on('fabrix:log', (level: string, msg: any[] = [ ]) => ( 65 | console[level] || console.log)(level, ...msg) 66 | ) 67 | 68 | return new Proxy(this.emitLogEvent.bind(this), { 69 | /** 70 | * Trap calls to log., e.g. log.info(msg), log.debug(msg) and proxy 71 | * them to emitLogEvent 72 | */ 73 | get (target: any, key: string): Function { 74 | return target(key) 75 | }, 76 | 77 | /** 78 | * Trap invocations of log, e.g. log(msg) and treat them as invocations 79 | * of log.info(msg). 80 | */ 81 | apply (target: any, thisArg: any, argumentsList: any): Function { 82 | const [ level, ...msg ] = argumentsList 83 | return target(level)(...msg) 84 | } 85 | }) 86 | } 87 | 88 | /** 89 | * Emit fabrix:log, pass the "level" parameter to the event handler as the 90 | * first argument. 91 | */ 92 | emitLogEvent (level: string, current: string = 'silly') { 93 | const currentLevel = this.app && this.app.config ? this.app.config.get('log.level') || current : current 94 | level = level || current 95 | 96 | const log = this.levelHierarchy.indexOf(currentLevel) <= this.levelHierarchy.indexOf(level) ? 'fabrix:log' : 'fabrix:log:ignored' 97 | return (...msg: any[]) => this.app.emit(log, level, msg) 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /lib/Pathfinder.ts: -------------------------------------------------------------------------------- 1 | import { GraphCompletenessError } from './errors' 2 | import { Spool, ILifecycle } from './common' 3 | 4 | const lifecycleStages = [ 5 | 'configure', 6 | 'initialize' 7 | ] 8 | 9 | export const Pathfinder = { 10 | 11 | /** 12 | * Return true if the spool dependency graph is "complete"; i.e., that 13 | * the lifecycle paths for all spools and all stages are valid. 14 | */ 15 | isComplete (spools: Spool[]): boolean { 16 | return spools.every(spool => { 17 | return Pathfinder.isLifecycleValid(spool, spools) 18 | }) 19 | }, 20 | 21 | /** 22 | * Return true if all stages for a spool are valid; false otherwise 23 | */ 24 | isLifecycleValid (spool: Spool, spools: Spool[]): boolean { 25 | return lifecycleStages.every(stageName => { 26 | return Pathfinder.isLifecycleStageValid(spool, stageName, spools) 27 | }) 28 | }, 29 | 30 | /** 31 | * Return true if a particular stage is valid for the given spool; false 32 | * otherwise 33 | */ 34 | isLifecycleStageValid (spool: Spool, stageName: string, spools: Spool[]): boolean { 35 | if (!stageName || lifecycleStages.indexOf(stageName) === -1) { 36 | throw new TypeError(`isLifecycleStageValid: stageName must be one of ${lifecycleStages}`) 37 | } 38 | const path = Pathfinder.getLifecyclePath(spool, stageName, spools) 39 | const terminals = Pathfinder.getPathErrors(path) 40 | return !terminals.some((t: any) => t instanceof Error) 41 | }, 42 | 43 | /** 44 | * Traverse the lifecycle path and return the terminal values for each of its 45 | * branches (lifecycle events) 46 | */ 47 | getPathErrors (path: any): any[] { 48 | if (typeof path === 'boolean') { 49 | return [ ] 50 | } 51 | if (path instanceof Error) { 52 | return [ path ] 53 | } 54 | return Object.keys(path) 55 | .map(key => Pathfinder.getPathErrors(path[key])) 56 | .reduce((terminals, t) => terminals.concat(t), [ ]) 57 | }, 58 | 59 | /** 60 | * 61 | */ 62 | getLifecyclePath (spool: Spool, stageName: string, spools: Spool[], path?: any[]): boolean | {[key: string]: any} { 63 | const stage = spool.lifecycle[stageName] || { } 64 | 65 | if (!path) { 66 | return Pathfinder.getLifecyclePath(spool, stageName, spools, [ spool ]) 67 | } 68 | 69 | // terminate traversal. if current spoolwaits for no events, then it 70 | // necessarily reaches the sink, and indicates a complete path. 71 | if (!stage.listen || (stage.listen! && stage.listen.length === 0)) { 72 | return true 73 | } 74 | 75 | // find all spools that produce the event(s) that the current spoolrequires 76 | const producers = stage.listen 77 | .map((eventName: string) => { 78 | return Pathfinder.getEventProducer(eventName, stageName, spools, path) 79 | }) 80 | .filter((producer: any) => !!producer) 81 | 82 | 83 | // return first error encountered in this path. terminate traversal. 84 | // one or more of the required events are not available. 85 | const error = producers.find((producer: any) => producer instanceof GraphCompletenessError) 86 | if (error) { 87 | return error 88 | } 89 | 90 | // all producers must themselves have complete paths. 91 | return producers.reduce((level: any, producer: Spool) => { 92 | const subpath = path.concat(producer) 93 | level[producer.name] = Pathfinder.getLifecyclePath(producer, stageName, spools, subpath) 94 | return level 95 | }, { }) 96 | }, 97 | 98 | /** 99 | * Return all spools that produce a given event, but which is not contained 100 | * in the given "path" list. 101 | */ 102 | getEventProducer (eventName: string, stageName: string, spools: Spool[], path: any[] = [ ]): Error | Spool { 103 | const producers = spools 104 | .filter(spool => { 105 | const stage = spool.lifecycle[stageName] 106 | return path.indexOf(spool) === -1 && stage.emit.indexOf(eventName) >= 0 107 | }) 108 | 109 | if (producers.length > 1) { 110 | return new Error(`More than one spool produces the event ${eventName}`) 111 | } 112 | if (producers.length === 0) { 113 | return new GraphCompletenessError(path[path.length - 1], stageName, eventName) 114 | } 115 | 116 | return producers[0] 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /lib/Templates.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable no-trailing-whitespace 2 | 3 | import { FabrixApp } from './index' 4 | 5 | export const Templates = { 6 | hr: '---------------------------------------------------------------', 7 | docs: 'Fabrix Documentation: https://fabrix.app/docs', 8 | 9 | info: { 10 | start: 'Spooling up...', 11 | stop: 'Spooling down...', 12 | initialized: 'All spools are loaded.', 13 | sane: `Sanity check complete`, 14 | ready (app: FabrixApp): string { 15 | const baseUrl = app.config.get('web.baseUrl') || 16 | `http://${app.config.get('web.host') || 'localhost'}:${app.config.get('web.port') || '80'}` 17 | return ( 18 | `--------------------------------------------------------------- 19 | Now: ${new Date()} 20 | Basic Info 21 | Application : ${app.pkg.name || 'UNNAMED'} 22 | Base URL : ${baseUrl} 23 | Version : ${app.pkg.version || 'N/A'} 24 | Environment : ${app.env.NODE_ENV || 'UNKNOWN'}` 25 | ) 26 | } 27 | }, 28 | 29 | debug: { 30 | ready (app: FabrixApp): string { 31 | return ( 32 | ` Database Info 33 | Stores : ${Object.keys(app.config.get('stores') || { })} 34 | Web Servers Info 35 | Servers : ${app.config.get('web.server') || 'NOT INSTALLED'} 36 | Ports : ${app.config.get('web.port') || 'N/A'} 37 | Routes : ${(app.routes || new Map()).size}` 38 | ) 39 | } 40 | }, 41 | 42 | silly: { 43 | ready (app: FabrixApp): string { 44 | const resources = (app.resources || []).map(resource => { 45 | let prefix = resource.charAt(0).toUpperCase() + resource.slice(1) + (` (${ Object.keys(app.api[resource]).length })`) 46 | while (prefix.length < 17) { prefix = prefix + ' '} 47 | return `${ prefix } : ${Object.keys(app.api[resource] || {})}` 48 | }).join('\n ') 49 | 50 | let apiResourcesLabel = `API Resources (${ (app.resources || []).length })` 51 | while (apiResourcesLabel.length < 18) { apiResourcesLabel = apiResourcesLabel + ' '} 52 | 53 | let spoolsLabel = `Spools (${ Object.keys(app.spools || {}).length})` 54 | while (spoolsLabel.length < 18) { spoolsLabel = spoolsLabel + ' '} 55 | 56 | return ( 57 | ` API 58 | ${ apiResourcesLabel }: ${resources ? app.resources : 'NONE INSTALLED'} 59 | ${ resources } 60 | ${ spoolsLabel }: ${Object.keys(app.spools || {})}` 61 | ) 62 | }, 63 | 64 | sane: `Sanity checks keep us happy`, 65 | // tslint:disable:max-line-length 66 | initialized: ` 67 | 68 | ███████ ██ 69 | █████ █ ████ ████ 70 | ████ █████ ██ 71 | ██████ ████ ████ ████ ███ █████ ███ ████ ████ ████ 72 | ████ █████ ████ ████ ████ ███████████████ █████ ██ 73 | █████ ████ ████ ████ ███ ████ █ ████ ████ ██ 74 | ████ █████ █████ ████ ███ ████ ████ ██████ 75 | █████ ████ ████ █████ ███ ████ ████ ████ 76 | ████ ████ █████ ████ ████ █████ ████ ██████ 77 | ████ ████████████████████████ ████ ████████ ██████ 78 | ████ ███ ███ ████ █████ ████ ███ 79 | ██ ████ 80 | ██ ████ 81 | ██████ 82 | 83 | ` 84 | } 85 | } 86 | 87 | -------------------------------------------------------------------------------- /lib/common/Controller.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable no-unused-expression 2 | 3 | import { FabrixApp } from '../index' 4 | import { EventEmitter } from 'events' 5 | import { FabrixGeneric } from './Generic' 6 | 7 | /** 8 | * Fabrix Controller Class. 9 | */ 10 | export class FabrixController extends FabrixGeneric { 11 | 12 | constructor (app: FabrixApp) { 13 | super(app) 14 | 15 | if (!(app instanceof EventEmitter)) { 16 | throw new Error('The "app" argument must be of type EventEmitter') 17 | } 18 | this.app.emit(`controller:${this.id}:constructed`, this) 19 | } 20 | 21 | get __ () { 22 | if (this.app.__) { 23 | return this.app.__ 24 | } 25 | else { 26 | throw new Error('Missing spool-i18n, make sure it is included in app.main.spools') 27 | } 28 | } 29 | 30 | /** 31 | * Return the id of this controller 32 | */ 33 | get id (): string { 34 | return this.constructor.name.replace(/(\w+)Controller/, '$1').toLowerCase() 35 | } 36 | 37 | /** 38 | * Return a reference to the Fabrix logger 39 | */ 40 | get log (): FabrixApp['log'] { 41 | return this.app.log 42 | } 43 | 44 | /** 45 | * Return a reference to the Fabrix configuration map. 46 | */ 47 | get config (): FabrixApp['config'] { 48 | return this.app.config 49 | } 50 | 51 | /** 52 | * Return a reference to the Fabrix resource services. 53 | */ 54 | get services (): FabrixApp['services'] { 55 | return this.app.services 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /lib/common/Generic.ts: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from 'events' 2 | import { FabrixApp } from '../index' 3 | 4 | export interface FabrixGeneric { 5 | [key: string]: any 6 | app: FabrixApp 7 | methods: string[] 8 | } 9 | /** 10 | * Fabrix Generic Class. 11 | */ 12 | export class FabrixGeneric { 13 | public methods: string[] = [] 14 | 15 | constructor ( 16 | public app: FabrixApp 17 | ) { 18 | 19 | if (!(app instanceof EventEmitter)) { 20 | throw new Error('The "app" argument must be of type EventEmitter') 21 | } 22 | 23 | Object.defineProperties(this, { 24 | app: { 25 | value: app, 26 | writable: false, 27 | enumerable: false 28 | } 29 | }) 30 | } 31 | 32 | /** 33 | * Return the id of this controller 34 | */ 35 | public get id (): string { 36 | return this.constructor.name.toLowerCase() 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /lib/common/Model.ts: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from 'events' 2 | import { FabrixApp } from '../index' 3 | import { FabrixResolver } from './' 4 | import { IllegalAccessError } from '../errors' 5 | import { FabrixGeneric } from './Generic' 6 | 7 | // export interface FabrixModel { 8 | // new(): FabrixModel 9 | // config(app: FabrixApp, datastore?): { 10 | // [key: string]: any, 11 | // tableName?: string, 12 | // store?: any, 13 | // migrate?: string 14 | // } 15 | // schema(app: FabrixApp, datastore?): { 16 | // [key: string]: any 17 | // } 18 | // resolver(): any 19 | // } 20 | 21 | /** 22 | * Fabrix Model Class. 23 | */ 24 | export class FabrixModel extends FabrixGeneric { 25 | private _datastore: any 26 | private _instance: any 27 | private _config: {[key: string]: any} 28 | private _schema: any 29 | private _resolver: any 30 | 31 | public store: any 32 | public migrate: any 33 | 34 | /** 35 | * Model configuration 36 | */ 37 | public static config (app: FabrixApp, datastore?): {[key: string]: any} { 38 | return { 39 | tableName: null, 40 | store: null, 41 | migrate: null 42 | } 43 | } 44 | 45 | /** 46 | * Model schema. The definition of its fields, their types, indexes, 47 | * foreign keys, etc go here. Definition will differ based on which 48 | * ORM/datastore Spool is being used. 49 | */ 50 | public static schema (app: FabrixApp, datastore?): {[key: string]: any} { 51 | return {} 52 | } 53 | 54 | /** 55 | * The Resolver for this Model. Use Resolvers for resolver-based ORMs such as 56 | * GraphQL and Falcor. Will typically look something like: 57 | * return 58 | * 59 | * Or, you can define the resolver right here, e.g. 60 | * return class MyResolver extends Resolver { 61 | * findById (...) { 62 | * ... 63 | * } 64 | * } 65 | */ 66 | public static get resolver () { 67 | return FabrixResolver 68 | } 69 | 70 | /** 71 | * Construct the model and bind the Resolver 72 | */ 73 | constructor (app: FabrixApp, datastore?) { 74 | super(app) 75 | 76 | if (!(app instanceof EventEmitter)) { 77 | throw new Error('The "app" argument must be of type EventEmitter') 78 | } 79 | 80 | 81 | this._datastore = datastore 82 | 83 | this.resolver = new (this.constructor).resolver(this, datastore) 84 | this.app.emit(`model:${this.name}:constructed`, this) 85 | } 86 | 87 | get datastore() { 88 | if (!this._datastore) { 89 | // This will throw an error if the logger is not set yet 90 | // this.app.log.warn(`${this.name} did not receive an instance of the datastore`) 91 | } 92 | return this._datastore 93 | } 94 | 95 | set datastore(datastore) { 96 | this._datastore = datastore 97 | } 98 | 99 | get instance() { 100 | if (!this._resolver) { 101 | return 102 | // This will throw an error if the logger is not set yet 103 | // this.app.log.warn(`${this.name} did not receive an instance from the datastore`) 104 | } 105 | return this._resolver.instance 106 | } 107 | 108 | /** 109 | * Return the config of this model 110 | */ 111 | get config () { 112 | if (!this._config) { 113 | this._config = (this.constructor).config // (this.app, this.datastore) 114 | } 115 | return this._config 116 | } 117 | 118 | /** 119 | * Return the schema of this model 120 | */ 121 | get schema () { 122 | if (!this._schema) { 123 | this._schema = (this.constructor).config // (this.app, this.datastore) 124 | } 125 | return this._schema 126 | } 127 | 128 | /** 129 | * Set the resolver for this model 130 | */ 131 | set resolver (r) { 132 | if (this.resolver) { 133 | throw new IllegalAccessError('Cannot change the resolver on a Model') 134 | } 135 | 136 | this._resolver = r 137 | } 138 | 139 | /** 140 | * Return the resolver for this model 141 | */ 142 | get resolver () { 143 | return this._resolver 144 | } 145 | 146 | /** 147 | * Return the name of this model 148 | */ 149 | get name (): string { 150 | return this.constructor.name.toLowerCase() 151 | } 152 | 153 | /** 154 | * Return the name of the database table or collection 155 | */ 156 | get tableName (): string { 157 | const config = (this.constructor).config(this.app, this.datastore) 158 | return config.tableName || this.name 159 | } 160 | 161 | /** 162 | * Bound Methods from the Resolver Class 163 | save (...args) { 164 | return this.resolver.save(...args) 165 | } 166 | 167 | update (...args) { 168 | return this.resolver.update(...args) 169 | } 170 | 171 | delete (...args) { 172 | return this.resolver.delete(...args) 173 | } 174 | 175 | get (...args) { 176 | return this.resolver.get(...args) 177 | } 178 | */ 179 | } 180 | -------------------------------------------------------------------------------- /lib/common/Policy.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable no-unused-expression 2 | 3 | import { FabrixApp } from '../index' 4 | import { EventEmitter } from 'events' 5 | // import { enumerable, writable } from './' 6 | import { FabrixGeneric } from './Generic' 7 | 8 | /** 9 | * Fabrix Policy Class. 10 | */ 11 | export class FabrixPolicy extends FabrixGeneric { 12 | 13 | /** 14 | * Policy configuration 15 | */ 16 | static config () { 17 | } 18 | 19 | constructor (app: FabrixApp) { 20 | super(app) 21 | 22 | if (!(app instanceof EventEmitter)) { 23 | throw new Error('The "app" argument must be of type EventEmitter') 24 | } 25 | this.app.emit(`policy:${this.id}:constructed`, this) 26 | } 27 | 28 | get __ () { 29 | if (this.app.__) { 30 | return this.app.__ 31 | } 32 | else { 33 | throw new Error('Missing spool-i18n, make sure it is included in app.main.spools') 34 | } 35 | } 36 | 37 | /** 38 | * Return the id of this policy 39 | */ 40 | get id (): string { 41 | return this.constructor.name.replace(/(\w+)Policy/, '$1').toLowerCase() 42 | } 43 | 44 | /** 45 | * Return a reference to the Fabrix logger 46 | */ 47 | get log (): FabrixApp['log'] { 48 | return this.app.log 49 | } 50 | 51 | /** 52 | * Return a reference to the Fabrix configuration map. 53 | */ 54 | get config (): FabrixApp['config'] { 55 | return this.app.config 56 | } 57 | 58 | get services (): FabrixApp['services'] { 59 | return this.app.services 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /lib/common/Resolver.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable no-unused-expression 2 | 3 | import { FabrixApp } from '../index' 4 | import { FabrixModel } from './' 5 | 6 | /** 7 | * Fabrix Resolver Class. 8 | */ 9 | export class FabrixResolver { 10 | private _model: FabrixModel 11 | 12 | constructor (model: FabrixModel, datastore?) { 13 | if (!model) { 14 | throw new RangeError('Resolver must be given a Model to bind to') 15 | } 16 | this._model = model 17 | this.app.emit(`resolver:${this.model.name}:constructed`, this) 18 | } 19 | 20 | get instance(): any { 21 | return 22 | } 23 | 24 | /** 25 | * Return the parent model 26 | */ 27 | get model(): FabrixModel { 28 | return this._model 29 | } 30 | 31 | /** 32 | * Return the schema of the parent model 33 | */ 34 | get schema (): FabrixModel['schema'] { 35 | return this.model.schema 36 | } 37 | 38 | /** 39 | * Return the schema of the parent model 40 | */ 41 | get config (): FabrixModel['config'] { 42 | return this.model.config 43 | } 44 | 45 | /** 46 | * Returns the instance of the parent model 47 | */ 48 | get app(): FabrixApp { 49 | return this.model.app 50 | } 51 | 52 | // public save(...args) { 53 | // throw new Error('Orm method for Save not defined') 54 | // } 55 | // 56 | // public update(...args) { 57 | // throw new Error('Orm method for Update not defined') 58 | // } 59 | // 60 | // public delete(...args) { 61 | // throw new Error('Orm method for Delete not defined') 62 | // } 63 | // 64 | // public get(...args) { 65 | // throw new Error('Orm method for Get not defined') 66 | // } 67 | } 68 | 69 | -------------------------------------------------------------------------------- /lib/common/Service.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable no-unused-expression 2 | 3 | import { FabrixApp} from '../index' 4 | import { EventEmitter } from 'events' 5 | // import { enumerable, writable } from './' 6 | import { FabrixGeneric } from './Generic' 7 | 8 | /** 9 | * Fabrix Service Class. 10 | */ 11 | export class FabrixService extends FabrixGeneric { 12 | 13 | constructor ( 14 | app: FabrixApp 15 | ) { 16 | 17 | super(app) 18 | 19 | if (!(app instanceof EventEmitter)) { 20 | throw new Error('The "app" argument must be of type EventEmitter') 21 | } 22 | 23 | this.app.emit(`service:${this.id}:constructed`, this) 24 | } 25 | 26 | get __ () { 27 | if (this.app.__) { 28 | return this.app.__ 29 | } 30 | else { 31 | throw new Error('Missing spool-i18n, make sure it is included in app.main.spools') 32 | } 33 | } 34 | 35 | /** 36 | * Return the id of this controller 37 | */ 38 | get id (): string { 39 | return this.constructor.name.replace(/(\w+)Service/, '$1').toLowerCase() 40 | } 41 | 42 | /** 43 | * Return a reference to the Fabrix logger 44 | */ 45 | get log (): FabrixApp['log'] { 46 | return this.app.log 47 | } 48 | 49 | /** 50 | * Return a reference to the Fabrix configuration map. 51 | */ 52 | get config (): FabrixApp['config'] { 53 | return this.app.config 54 | } 55 | 56 | get services (): FabrixApp['services'] { 57 | return this.app.services 58 | } 59 | 60 | get models (): FabrixApp['models'] { 61 | return this.app.models 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /lib/common/Spool.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable no-unused-expression 2 | 3 | import { EventEmitter } from 'events' 4 | import { defaultsDeep, omit } from 'lodash' 5 | import { IApi, IPkg, ISpoolConfig, ILifecycle } from './index' 6 | import { FabrixApp } from '../index' 7 | import { FabrixGeneric } from './Generic' 8 | 9 | /** 10 | * @class Spool 11 | * @see {@link https://fabrix.app/docs/en/ref/spool} 12 | */ 13 | export interface Spool extends FabrixGeneric { 14 | [key: string]: any 15 | } 16 | 17 | export class Spool extends FabrixGeneric { 18 | private _address: string 19 | private _port: string 20 | private _exclusive: boolean 21 | 22 | private _stage = 'pre' 23 | private _config: ISpoolConfig 24 | private _pkg: any // IPkg 25 | private _api: IApi 26 | private _lifecycle: ILifecycle 27 | private _spoolConfigKeys = ['lifecycle', 'spool', 'trailpack'] 28 | 29 | /** 30 | * Return the action for the config file of this spool into the app.config 31 | * replaceable: allow config provided by spool to be completely overridden by app.config 32 | * hold: do not allow config provided by spool to be overridden by app.config 33 | * merge: attempt to deep merge config provided by spool with app.config 34 | * This method can be overridden for spools by declaring a 'configAction' getter 35 | * in the extending spool class. 36 | */ 37 | static get configAction (): string { 38 | return 'merge' 39 | } 40 | 41 | /** 42 | * Return the type of this Spool. By default, this 'misc'. 43 | * This method can be overridden for spools by declaring a 'type' getter 44 | * in the extending spool class or by using a spool template such as: 45 | * webserver, datastore, etc. 46 | */ 47 | static get type (): string { 48 | return 'misc' 49 | } 50 | 51 | /** 52 | * The Spool lifecycle. At each stage (configure, initialize) define 53 | * preconditions ("listen") and post-conditions ("emit") of the spool. 54 | */ 55 | static get defaultLifecycle (): ILifecycle { 56 | return { 57 | configure: { 58 | listen: [ ], 59 | emit: [ ] 60 | }, 61 | initialize: { 62 | listen: [ ], 63 | emit: [ ] 64 | }, 65 | sanity: { 66 | listen: [ ], 67 | emit: [ ] 68 | } 69 | } 70 | } 71 | 72 | static configuredSpoolLifecycle (config) { 73 | const level1 = config.lifecycle || {} 74 | const level2 = config.spool && config.spool.lifecycle 75 | ? config.spool.lifecycle : config.trailpack && config.trailpack.lifecycle 76 | ? config.trailpack.lifecycle : {} 77 | const level3 = Spool.defaultLifecycle 78 | return defaultsDeep({}, level1, level2, level3) 79 | } 80 | 81 | /** 82 | * @constructor 83 | * @param app FabrixApp instance 84 | * @param api The api entities defined in this spool (api/ folder) 85 | * @param config The spool configuration (config/ folder) 86 | * @param pkg The spool package.json 87 | * 88 | * Instantiate the Spool and set some initial properties. All Spools 89 | * should implement their own constructors, and call super(app, pack) with 90 | * their own spool definitions. Implementing application logic in the spool 91 | * constructor is not recommended. 92 | */ 93 | constructor ( 94 | app: FabrixApp, 95 | { 96 | pkg = null, 97 | config = { }, 98 | api = { } 99 | }: { pkg?: {[key: string]: any}, config?: ISpoolConfig, api?: IApi } 100 | ) { 101 | super(app) 102 | if (!(app instanceof EventEmitter)) { 103 | throw new Error('The "app" argument must be of type EventEmitter') 104 | } 105 | if (!pkg) { 106 | throw new Error('Spool is missing package definition ("spool.pkg")') 107 | } 108 | 109 | this._pkg = Object.freeze(pkg) 110 | this._api = api 111 | 112 | /** 113 | * Config will pollute the app.config, "lifecycle" and "spool" are spool specific 114 | * configuration arguments and should be omitted from config 115 | */ 116 | this._config = omit(config, this._spoolConfigKeys) 117 | this._lifecycle = Spool.configuredSpoolLifecycle(config) 118 | this.app.emit(`spool:${this.name}:constructed`, this) 119 | } 120 | 121 | set stage (val) { 122 | this._stage = val 123 | } 124 | 125 | /** 126 | * Returns the lifecylce stage that the spool is currently in 127 | */ 128 | get stage () { 129 | return this._stage 130 | } 131 | 132 | get api () { 133 | return this._api 134 | } 135 | 136 | /** 137 | * Virtual Setter for `api` 138 | */ 139 | set api (api) { 140 | this._api = api 141 | } 142 | 143 | get config () { 144 | return this._config 145 | } 146 | 147 | /** 148 | * Virtual Setter for `config` 149 | */ 150 | set config (config) { 151 | this._config = config 152 | } 153 | 154 | get pkg () { 155 | return this._pkg 156 | } 157 | 158 | /** 159 | * Return a reference to the Fabrix logger 160 | */ 161 | get log (): FabrixApp['log'] { 162 | return this.app.log 163 | } 164 | 165 | /** 166 | * Validate any necessary preconditions for this spool. We strongly 167 | * recommend that all Spools override this method and use it to check 168 | * preconditions. 169 | */ 170 | validate (): any { 171 | 172 | } 173 | 174 | /** 175 | * Set any configuration required before the spools are initialized. 176 | * Spools that require configuration, or need to alter/extend the app's 177 | * configuration, should override this method. 178 | */ 179 | configure (): any { 180 | 181 | } 182 | 183 | /** 184 | * Start any services or listeners necessary for this spool. Spools that 185 | * run daemon-like services should override this method. 186 | */ 187 | async initialize (): Promise { 188 | 189 | } 190 | 191 | /** 192 | * Check any configured or initialized state to prove that the Fabrix app in 193 | * fact does have the values specified by the lifecylce. Spools that require 194 | * runtime specifications should override this method 195 | */ 196 | sanity (): any { 197 | 198 | } 199 | 200 | /** 201 | * Unload this Spool. This method will instruct the spool to perform 202 | * any necessary cleanup with the expectation that the app will stop or reload 203 | * soon thereafter. If your spool runs a daemon or any other thing that may 204 | * occupy the event loop, implementing this method is important for Fabrix to 205 | * exit correctly. 206 | */ 207 | async unload (): Promise { 208 | 209 | } 210 | 211 | /** 212 | * Return the name of this Spool. By default, this is the name of the 213 | * npm module (in package.json). This method can be overridden for spools 214 | * which do not follow the "spool-" prefix naming convention. 215 | */ 216 | get name (): string { 217 | return this.pkg.name 218 | ? this.pkg.name.replace(/(^@fabrix\/)?spool\-|trailpack\-/, '') 219 | : this.constructor.name.toLowerCase().replace(/Spool$/, '') 220 | } 221 | 222 | /** 223 | * The final Spool lifecycle merged with the configured lifecycle 224 | */ 225 | get lifecycle (): ILifecycle { 226 | return this._lifecycle 227 | } 228 | 229 | 230 | } 231 | -------------------------------------------------------------------------------- /lib/common/decorators/configurable.ts: -------------------------------------------------------------------------------- 1 | export function configurable(value: boolean) { 2 | return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) { 3 | descriptor.configurable = value 4 | return descriptor 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /lib/common/decorators/enumerable.ts: -------------------------------------------------------------------------------- 1 | export function enumerable(value: boolean) { 2 | return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) { 3 | descriptor.enumerable = value 4 | return descriptor 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /lib/common/decorators/spool.ts: -------------------------------------------------------------------------------- 1 | export function spool(constructor: T) { 2 | return class extends constructor { 3 | isSpool = true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /lib/common/decorators/writable.ts: -------------------------------------------------------------------------------- 1 | export function writable(value: boolean) { 2 | return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) { 3 | descriptor.writable = value 4 | return descriptor 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /lib/common/index.ts: -------------------------------------------------------------------------------- 1 | // decorators 2 | // export { configurable } from './decorators/configurable' 3 | // export { enumerable } from './decorators/enumerable' 4 | // export { writable } from './decorators/writable' 5 | 6 | // Fabrix Spool Classes 7 | export { Spool } from './Spool' 8 | 9 | // Fabrix Default Classes 10 | export { FabrixController } from './Controller' 11 | export { FabrixGeneric } from './Generic' 12 | export { FabrixModel } from './Model' 13 | export { FabrixPolicy } from './Policy' 14 | export { FabrixResolver } from './Resolver' 15 | export { FabrixService } from './Service' 16 | 17 | // interfaces 18 | export { IApi } from './interfaces/IApi' 19 | export { IConfig } from './interfaces/IConfig' 20 | export { ISpoolConfig } from './interfaces/ISpoolConfig' 21 | export { IPkg } from './interfaces/IPkg' 22 | export { ILifecycle } from './interfaces/ILifecycle' 23 | export { IEnv } from './interfaces/IEnv' 24 | -------------------------------------------------------------------------------- /lib/common/interfaces/IApi.ts: -------------------------------------------------------------------------------- 1 | import { FabrixModel, FabrixService, FabrixResolver, FabrixPolicy, FabrixController, FabrixGeneric } from '../' 2 | 3 | export interface IApi { 4 | [key: string]: FabrixGeneric | {} 5 | // models?: {[key: string]: FabrixModel}, 6 | // services?: {[key: string]: FabrixService}, 7 | // resolvers?: {[key: string]: FabrixResolver}, 8 | // policies?: {[key: string]: FabrixPolicy}, 9 | // controllers?: {[key: string]: FabrixController} 10 | } 11 | -------------------------------------------------------------------------------- /lib/common/interfaces/IConfig.ts: -------------------------------------------------------------------------------- 1 | import { Spool } from '../Spool' 2 | 3 | export interface IConfig { 4 | [key: string]: any, 5 | main: { 6 | [key: string]: any, 7 | spools: any[] // typeof Spool[] 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /lib/common/interfaces/IEnv.ts: -------------------------------------------------------------------------------- 1 | export interface IEnv { 2 | NODE_ENV: string, 3 | [key: string]: any 4 | } 5 | -------------------------------------------------------------------------------- /lib/common/interfaces/ILifecycle.ts: -------------------------------------------------------------------------------- 1 | export interface ILifecycle { 2 | configure: { 3 | listen: string[], 4 | emit: string[] 5 | }, 6 | initialize: { 7 | listen: string[], 8 | emit: string[] 9 | }, 10 | sanity: { 11 | listen: string[], 12 | emit: string[] 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /lib/common/interfaces/IPkg.ts: -------------------------------------------------------------------------------- 1 | export interface IPkg { 2 | [key: string]: any 3 | } 4 | -------------------------------------------------------------------------------- /lib/common/interfaces/ISpool.ts: -------------------------------------------------------------------------------- 1 | export interface ISpool {} 2 | -------------------------------------------------------------------------------- /lib/common/interfaces/ISpoolConfig.ts: -------------------------------------------------------------------------------- 1 | import { ILifecycle } from './ILifecycle' 2 | 3 | export interface ISpoolConfig { 4 | [key: string]: any 5 | } 6 | -------------------------------------------------------------------------------- /lib/common/interfaces/IVersions.ts: -------------------------------------------------------------------------------- 1 | export interface IVersions { 2 | [key: string]: string, 3 | node?: string, 4 | v8?: string, 5 | uv?: string, 6 | zlib?: string, 7 | brotli?: string, 8 | ares?: string, 9 | modules?: string, 10 | nghttp2?: string, 11 | napi?: string, 12 | llhttp?: string, 13 | http_parser?: string, 14 | openssl?: string, 15 | cldr?: string, 16 | icu?: string, 17 | tz?: string, 18 | unicode?: string 19 | 20 | } 21 | -------------------------------------------------------------------------------- /lib/common/spools/archetype/.gitignore: -------------------------------------------------------------------------------- 1 | /.idea 2 | # Logs 3 | logs 4 | *.log 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 18 | .grunt 19 | 20 | # node-waf configuration 21 | .lock-wscript 22 | 23 | # Compiled binary addons (http://nodejs.org/api/addons.html) 24 | build/Release 25 | 26 | # Dependency directory 27 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 28 | node_modules 29 | 30 | *.sw* 31 | -------------------------------------------------------------------------------- /lib/common/spools/archetype/README.md: -------------------------------------------------------------------------------- 1 | # <%= spoolName %> 2 | 3 | [![Gitter][gitter-image]][gitter-url] 4 | [![NPM version][npm-image]][npm-url] 5 | [![Build status][ci-image]][ci-url] 6 | [![Dependency Status][daviddm-image]][daviddm-url] 7 | [![Code Climate][codeclimate-image]][codeclimate-url] 8 | 9 | <%= description %> 10 | 11 | ## Install 12 | 13 | ```sh 14 | $ npm install --save <%= spoolName %> 15 | ``` 16 | 17 | ## Configure 18 | 19 | ```js 20 | // config/main.ts 21 | import { <%= spoolName %> } from '<%= spoolName %>' 22 | export const main = { 23 | spools: [ 24 | // ... other spools 25 | <%= spoolName %> 26 | ] 27 | } 28 | ``` 29 | 30 | [npm-image]: https://img.shields.io/npm/v/<%= spoolName %>.svg?style=flat-square 31 | [npm-url]: https://npmjs.org/spoolage/<%= spoolName %> 32 | [ci-image]: https://img.shields.io/travis/<%= githubAccount %>/<%= spoolName %>/master.svg?style=flat-square 33 | [ci-url]: https://travis-ci.org/<%= githubAccount %>/<%= spoolName %> 34 | [daviddm-image]: http://img.shields.io/david/<%= githubAccount %>/<%= spoolName %>.svg?style=flat-square 35 | [daviddm-url]: https://david-dm.org/<%= githubAccount %>/<%= spoolName %> 36 | [codeclimate-image]: https://img.shields.io/codeclimate/github/<%= githubAccount %>/<%= spoolName %>.svg?style=flat-square 37 | [codeclimate-url]: https://codeclimate.com/github/<%= githubAccount %>/<%= spoolName %> 38 | [gitter-image]: http://img.shields.io/badge/+%20GITTER-JOIN%20CHAT%20%E2%86%92-1DCE73.svg?style=flat-square 39 | [gitter-url]: https://gitter.im/fabrix-app/Lobby 40 | [twitter-image]: https://img.shields.io/twitter/follow/FabrixApp.svg?style=social 41 | [twitter-url]: https://twitter.com/FabrixApp 42 | -------------------------------------------------------------------------------- /lib/common/spools/archetype/lib/Archetype.ts: -------------------------------------------------------------------------------- 1 | import { Spool } from '../../../' 2 | 3 | import * as config from './config/index' 4 | import * as pkg from '../package.json' 5 | import * as api from './api/index' 6 | 7 | export class Archetype extends Spool { 8 | 9 | static get lifecycle () { 10 | return { } 11 | } 12 | 13 | constructor (app) { 14 | super(app, { 15 | config: config, 16 | api: api, 17 | pkg: pkg 18 | }) 19 | } 20 | 21 | /** 22 | * TODO document method 23 | */ 24 | validate () { 25 | 26 | } 27 | 28 | /** 29 | * TODO document method 30 | */ 31 | configure () { 32 | 33 | } 34 | 35 | /** 36 | * TODO document method 37 | */ 38 | async initialize () { 39 | 40 | } 41 | 42 | /** 43 | * TODO document method 44 | */ 45 | async unload () { 46 | 47 | } 48 | } 49 | 50 | -------------------------------------------------------------------------------- /lib/common/spools/archetype/lib/api/index.ts: -------------------------------------------------------------------------------- 1 | export {} 2 | -------------------------------------------------------------------------------- /lib/common/spools/archetype/lib/config/index.ts: -------------------------------------------------------------------------------- 1 | export { spool } from './spool' 2 | -------------------------------------------------------------------------------- /lib/common/spools/archetype/lib/config/spool.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Spool Configuration 3 | * 4 | * @see {@link http://fabrixjs.io/doc/spool/config 5 | */ 6 | export const spool = { 7 | type: 'misc', 8 | /** 9 | * A searchable list of what this spool provides 10 | */ 11 | provides: { 12 | /** 13 | * `resources` are the API namespaces that this spool provides 14 | */ 15 | resources: [], 16 | /** 17 | * `api` is an object of keys containing arrays that declare things like: 18 | * api: { controllers: ['HelloWorldController'] } 19 | */ 20 | api: {}, 21 | /** 22 | * `config` is an array declaring the configurations included in this spool like: 23 | * config: ['', 'routes'] 24 | */ 25 | config: [] 26 | }, 27 | /** 28 | * Configure the lifecycle of this spool; that is, how it boots up, and which 29 | * order it loads relative to other spools. 30 | */ 31 | lifecycle: { 32 | configure: { 33 | /** 34 | * List of events that must be fired before the configure lifecycle 35 | * method is invoked on this Spool 36 | */ 37 | listen: [], 38 | 39 | /** 40 | * List of events emitted by the configure lifecycle method 41 | */ 42 | emit: [] 43 | }, 44 | initialize: { 45 | listen: [], 46 | emit: [] 47 | } 48 | } 49 | } 50 | 51 | -------------------------------------------------------------------------------- /lib/common/spools/archetype/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.0.0", 3 | "keywords": [ 4 | "spool", 5 | "fabrix", 6 | "trailjs" 7 | ], 8 | "main": "dist/index.js", 9 | "typings": "dist/index.d.ts", 10 | "files": [ 11 | "dist/index.js", 12 | "dist/index.d.ts", 13 | "dist/lib", 14 | "dist/config" 15 | ], 16 | "devDependencies": { 17 | "@fabrix/fabrix": "next", 18 | "lodash": "^4.11.1", 19 | "@fabrix/lint": "^1.0.0-alpha.3", 20 | "@types/lodash": "^4.14.109", 21 | "@types/node": "~10.3.2", 22 | "mocha": "^5", 23 | "nyc": "^12.0.2", 24 | "smokesignals": "next", 25 | "tslib": "~1.9.0", 26 | "tslint": "~5.10.0", 27 | "tslint-microsoft-contrib": "~5.0.3", 28 | "tsutils": "~2.27.1", 29 | "typescript": "~2.8.1" 30 | }, 31 | "scripts": { 32 | "test": "nyc mocha" 33 | }, 34 | "engines": { 35 | "node": ">= 7.6.0" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /lib/common/spools/archetype/test/app.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | pkg: { 3 | name: require('../package').name + '-test' 4 | }, 5 | api: { 6 | models: { }, 7 | controllers: { }, 8 | services: { } 9 | }, 10 | config: { 11 | main: { 12 | spools: [ 13 | require('../dist/').Archetype 14 | ] 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /lib/common/spools/archetype/test/index.js: -------------------------------------------------------------------------------- 1 | const FabrixApp = require('fabrix').FabrixApp 2 | 3 | before(() => { 4 | global.app = new FabrixApp(require('./app')) 5 | return global.app.start() 6 | }) 7 | 8 | after(() => { 9 | return global.app.stop() 10 | }) 11 | -------------------------------------------------------------------------------- /lib/common/spools/archetype/test/mocha.opts: -------------------------------------------------------------------------------- 1 | --reporter spec 2 | --recursive 3 | --full-trace 4 | --no-exit 5 | --check-leaks 6 | --globals app 7 | 8 | test/index.js test/*.test.js 9 | -------------------------------------------------------------------------------- /lib/common/spools/archetype/test/spool.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert') 2 | 3 | describe('<%= spoolName %>', () => { 4 | let spool 5 | before(() => { 6 | spool= global.app.spools.<%= spoolBasename %> 7 | assert(spool) 8 | }) 9 | describe('#validate', () => { 10 | it.skip('TODO test') 11 | }) 12 | describe('#configure', () => { 13 | it.skip('TODO test') 14 | }) 15 | describe('#initialize', () => { 16 | it.skip('TODO test') 17 | }) 18 | describe('#unload', () => { 19 | it.skip('TODO test') 20 | }) 21 | }) 22 | -------------------------------------------------------------------------------- /lib/common/spools/datastore.ts: -------------------------------------------------------------------------------- 1 | import { FabrixApp } from '../../index' 2 | import { Spool } from '../' 3 | import { FabrixModel } from '../Model' 4 | 5 | /** 6 | * Datastore Spool 7 | * 8 | * Datastores should inherit from this Spool in order to provide consistent 9 | * API for all datastores. 10 | */ 11 | export class DatastoreSpool extends Spool { 12 | /** 13 | * A Reference to the Library being used as the ORM 14 | */ 15 | public _datastore: any 16 | 17 | static get type () { 18 | return 'datastore' 19 | } 20 | 21 | constructor (app: FabrixApp, config) { 22 | if (!config) { 23 | throw new Error('DatastoreSpool must be subclassed. Do not load it directly.') 24 | } 25 | super(app, config) 26 | } 27 | 28 | /** 29 | * Map stores to models. 30 | */ 31 | async initialize (): Promise { 32 | Object.entries(this.app.models).forEach(([ modelName, model ]) => { 33 | const modelConfig = (model.constructor).config(this.app, this._datastore) 34 | Object.assign(model, { store: modelConfig['store'] || this.app.config.get('models.defaultStore')}) 35 | Object.assign(model, { migrate: modelConfig['migrate'] || this.app.config.get('models.migrate') || 'safe'}) 36 | }) 37 | return Promise.resolve() 38 | } 39 | } 40 | 41 | -------------------------------------------------------------------------------- /lib/common/spools/extension.ts: -------------------------------------------------------------------------------- 1 | import { Spool } from '../' 2 | 3 | export class ExtensionSpool extends Spool { 4 | public extensions: {[key: string]: any} 5 | 6 | static get type () { 7 | return 'extension' 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /lib/common/spools/index.ts: -------------------------------------------------------------------------------- 1 | export { DatastoreSpool } from './datastore' 2 | export { ExtensionSpool } from './extension' 3 | export { MiscSpool } from './misc' 4 | export { ServerSpool } from './server' 5 | export { ToolSpool } from './tool' 6 | -------------------------------------------------------------------------------- /lib/common/spools/misc.ts: -------------------------------------------------------------------------------- 1 | import { Spool } from '../' 2 | 3 | export class MiscSpool extends Spool { 4 | 5 | static get type () { 6 | return 'misc' 7 | } 8 | } 9 | 10 | -------------------------------------------------------------------------------- /lib/common/spools/server.ts: -------------------------------------------------------------------------------- 1 | import { FabrixApp } from '../../index' 2 | import { Spool } from '../' 3 | import { pick, omit, mapValues } from 'lodash' 4 | 5 | const spindleOptions = [ 6 | 'populate', 7 | 'limit', 8 | 'offset', 9 | 'sort' 10 | ] 11 | 12 | /** 13 | * Web Server Spool 14 | * 15 | * Web Servers should inherit from this Spool in order to provide consistent 16 | * API for all web servers. 17 | */ 18 | export class ServerSpool extends Spool { 19 | 20 | static get type () { 21 | return 'server' 22 | } 23 | 24 | constructor (app: FabrixApp, config) { 25 | if (!config) { 26 | throw new Error('ServerSpool must be subclassed. Do not load it directly.') 27 | } 28 | super(app, config) 29 | } 30 | 31 | private _parseQuery(data) { 32 | return mapValues(data, value => { 33 | if (value === 'true' || value === 'false') { 34 | value = value === 'true' 35 | } 36 | 37 | if (value === '%00' || value === 'null') { 38 | value = null 39 | } 40 | 41 | const parseValue = parseFloat(value) 42 | 43 | if (!isNaN(parseValue) && isFinite(value)) { 44 | value = parseValue 45 | } 46 | 47 | return value 48 | }) 49 | } 50 | 51 | /** 52 | * Extract options from request query and return the object subset. 53 | */ 54 | public getOptionsFromQuery(query) { 55 | return this._parseQuery(pick(query, spindleOptions)) 56 | } 57 | 58 | /** 59 | * Extract the criteria from the query 60 | */ 61 | public getCriteriaFromQuery(query) { 62 | return this._parseQuery(omit(query, spindleOptions)) 63 | } 64 | } 65 | 66 | -------------------------------------------------------------------------------- /lib/common/spools/system.ts: -------------------------------------------------------------------------------- 1 | import { Spool } from '../' 2 | 3 | export class SystemSpool extends Spool { 4 | 5 | static get type () { 6 | return 'system' 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /lib/common/spools/tool.ts: -------------------------------------------------------------------------------- 1 | import { Spool } from '../' 2 | 3 | export class ToolSpool extends Spool { 4 | 5 | static get type (): string { 6 | return 'tool' 7 | } 8 | } 9 | 10 | 11 | -------------------------------------------------------------------------------- /lib/errors/ApiNotDefinedErrort.ts: -------------------------------------------------------------------------------- 1 | export interface ApiNotDefinedError { 2 | 3 | } 4 | 5 | export class ApiNotDefinedError extends RangeError { 6 | constructor() { 7 | super(` 8 | "api" must be given to the Fabrix constructor, or to the start() method. 9 | Application cannot start. 10 | 11 | e.g. 12 | 1) Send "api" to the constructor 13 | const app = new FabrixApp({ 14 | pkg: require('./package'), 15 | --> api: require('./api'), 16 | config: require('./config') 17 | }) 18 | 19 | -- OR -- 20 | 21 | 2) Send "api" to the start() method: 22 | const app = new FabrixApp({ 23 | pkg: require('./package'), 24 | config: require('./config') 25 | }) 26 | app.start({ api: require('./api') }) 27 | 28 | For more info, see the Fabrix archetypes: 29 | - https://github.com/fabrix-app/fabrix/blob/master/archetype/src/index.ts 30 | - https://git.io/vw84F 31 | `) 32 | } 33 | 34 | get name () { 35 | return 'ApiNotDefinedError' 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /lib/errors/ConfigNotDefinedError.ts: -------------------------------------------------------------------------------- 1 | export class ConfigNotDefinedError extends RangeError { 2 | constructor() { 3 | super(` 4 | "config" must be given to the Fabrix constructor, and it must contain 5 | an object called "main". Application cannot start. 6 | e.g. 7 | 8 | const app = new FabrixApp({ 9 | pkg: require('./package'), 10 | api: require('./api'), 11 | --> config: require('./config') 12 | }) 13 | 14 | For more info, see the Fabrix archetypes: 15 | - https://github.com/fabrix-app/fabrix/blob/master/archetype/src/index.ts 16 | - https://git.io/vw84F 17 | `) 18 | } 19 | 20 | get name () { 21 | return 'ConfigNotDefinedError' 22 | } 23 | } 24 | 25 | -------------------------------------------------------------------------------- /lib/errors/ConfigValueError.ts: -------------------------------------------------------------------------------- 1 | export class ConfigValueError extends RangeError { 2 | constructor(msg: string) { 3 | super(msg) 4 | } 5 | 6 | get name () { 7 | return 'ConfigValueError' 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /lib/errors/GraphCompletenessError.ts: -------------------------------------------------------------------------------- 1 | export class GraphCompletenessError extends RangeError { 2 | constructor (spool: any = { }, stageName: string, eventName: string) { 3 | super(` 4 | The spool "${spool.name}" cannot load. 5 | 6 | During the "${stageName}" lifecycle stage, "${spool.name}" waits for 7 | the event "${eventName}". This event will not be emitted for one 8 | of the following reasons: 9 | 10 | 1. The event "${eventName}" is emitted by a another Spool 11 | that, due to its configuration, paradoxically requires that 12 | "${spool.name}" loaded before it. 13 | 14 | 2. The event "${eventName}" is not emitted by any other Spool, 15 | or it is not properly declared in the Spool's lifecycle 16 | config. 17 | 18 | Please check that you have all the Spools correctly installed 19 | and configured. If you think this is a bug, please file an issue: 20 | https://github.com/fabrix-app/fabrix/issues. 21 | `) 22 | 23 | } 24 | 25 | get name () { 26 | return 'GraphCompletenessError' 27 | } 28 | } 29 | 30 | -------------------------------------------------------------------------------- /lib/errors/IllegalAccessError.ts: -------------------------------------------------------------------------------- 1 | export class IllegalAccessError extends Error { 2 | constructor(msg: string) { 3 | super(msg) 4 | } 5 | 6 | get name () { 7 | return 'IllegalAccessError' 8 | } 9 | } 10 | 11 | -------------------------------------------------------------------------------- /lib/errors/NamespaceConflictError.ts: -------------------------------------------------------------------------------- 1 | export class NamespaceConflictError extends Error { 2 | constructor (key: string, globals: any) { 3 | super(` 4 | The extant global variable "${key}" conflicts with the value provided by 5 | Fabrix. 6 | 7 | Fabrix defines the following variables in the global namespace: 8 | ${globals} 9 | `) 10 | } 11 | 12 | get name () { 13 | return 'NamespaceConflictError' 14 | } 15 | } 16 | 17 | -------------------------------------------------------------------------------- /lib/errors/PackageNotDefinedError.ts: -------------------------------------------------------------------------------- 1 | export class PackageNotDefinedError extends RangeError { 2 | constructor() { 3 | super(` 4 | A "pkg" must be given to the Fabrix constructor. Application cannot start. 5 | e.g. 6 | const app = new FabrixApp({ 7 | --> pkg: require('./package'), 8 | api: require('./api'), 9 | config: require('./config') 10 | }) 11 | 12 | For more info, see the Fabrix archetypes: 13 | - https://git.io/vw845 14 | - https://git.io/vw84F 15 | `) 16 | } 17 | 18 | get name () { 19 | return 'PackageNotDefinedError' 20 | } 21 | } 22 | 23 | -------------------------------------------------------------------------------- /lib/errors/SanityError.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Generic Sanity error; commonly used to wrap joi errors, but can be 3 | * used for any Sanity-related exception. 4 | */ 5 | export class SanityError extends Error { 6 | constructor (msg: string, details?: any[]) { 7 | super(msg + '\n' + SanityError.humanizeMessage(details)) 8 | } 9 | 10 | get name () { 11 | return 'SanityError' 12 | } 13 | 14 | /** 15 | * Humanize a list of error details 16 | * 17 | * @param details a "joi-style details" list 18 | * @param details.message 19 | * @param details.path 20 | * @param details.type 21 | * @param details.context 22 | * @return String 23 | */ 24 | static humanizeMessage (details?: any[]) { 25 | const preamble = 'The following configuration values are invalid: ' 26 | const paths = (details || [ ]).map(d => d.path.join('.')) 27 | 28 | return preamble + paths.join(', ') 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /lib/errors/SpoolError.ts: -------------------------------------------------------------------------------- 1 | export class SpoolError extends Error { 2 | constructor (spool: any = { constructor: { }}, error: Error, stage: string) { 3 | super(` 4 | ${spool.name} spool failed in the "${stage}" stage. 5 | ${error} 6 | `) 7 | } 8 | 9 | get name () { 10 | return 'SpoolError' 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /lib/errors/TimeoutError.ts: -------------------------------------------------------------------------------- 1 | export class TimeoutError extends Error { 2 | constructor(phase: string, timeout: number) { 3 | super(` 4 | Timeout during "${phase}". Exceeded configured timeout of ${timeout}ms 5 | `) 6 | } 7 | 8 | get name () { 9 | return 'TimeoutError' 10 | } 11 | } 12 | 13 | -------------------------------------------------------------------------------- /lib/errors/ValidationError.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Generic validation error; commonly used to wrap joi errors, but can be 3 | * used for any validation-related exception. 4 | */ 5 | export class ValidationError extends Error { 6 | constructor (msg: string, details?: any[]) { 7 | super(msg + '\n' + ValidationError.humanizeMessage(details)) 8 | } 9 | 10 | get name () { 11 | return 'ValidationError' 12 | } 13 | 14 | /** 15 | * Humanize a list of error details 16 | * 17 | * @param details a "joi-style details" list 18 | * @param details.message 19 | * @param details.path 20 | * @param details.type 21 | * @param details.context 22 | * @return String 23 | */ 24 | static humanizeMessage (details?: any[]) { 25 | const preamble = 'The following configuration values are invalid: ' 26 | const paths = (details || [ ]).map(d => d.path.join('.')) 27 | 28 | return preamble + paths.join(', ') 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /lib/errors/index.ts: -------------------------------------------------------------------------------- 1 | export { ApiNotDefinedError } from './ApiNotDefinedErrort' 2 | export { ConfigNotDefinedError } from './ConfigNotDefinedError' 3 | export { ConfigValueError } from './ConfigValueError' 4 | export { GraphCompletenessError } from './GraphCompletenessError' 5 | export { IllegalAccessError } from './IllegalAccessError' 6 | export { NamespaceConflictError } from './NamespaceConflictError' 7 | export { PackageNotDefinedError } from './PackageNotDefinedError' 8 | export { SanityError } from './SanityError' 9 | export { SpoolError } from './SpoolError' 10 | export { TimeoutError } from './TimeoutError' 11 | export { ValidationError } from './ValidationError' 12 | -------------------------------------------------------------------------------- /lib/index.ts: -------------------------------------------------------------------------------- 1 | import * as Errors from './errors' 2 | import * as Common from './common' 3 | 4 | export { FabrixApp } from './Fabrix' 5 | export { Common } 6 | export { Configuration } from './Configuration' 7 | export { Core } from './Core' 8 | export { Errors } 9 | export { LoggerProxy } from './LoggerProxy' 10 | export { Pathfinder } from './Pathfinder' 11 | export { Templates } from './Templates' 12 | 13 | -------------------------------------------------------------------------------- /lib/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "emitDecoratorMetadata": true, 4 | "experimentalDecorators": true, 5 | "baseUrl": "./", 6 | "outDir": "../dist", 7 | "target": "es2016", 8 | "module": "commonjs", 9 | "moduleResolution": "node", 10 | "declaration": true, 11 | "strict": false, 12 | "esModuleInterop": false, 13 | "sourceMap": true, 14 | "noImplicitAny": false, 15 | "typeRoots": [ 16 | "node_modules/@types" 17 | ], 18 | "lib": [ 19 | "es2016", 20 | "es2017.object" 21 | ] 22 | }, 23 | "include": [ 24 | "../package.json", 25 | "test/**/*.ts", 26 | "**/*.ts", 27 | "index.ts" 28 | ], 29 | "paths": { 30 | "@fabrix/*": ["./*"], 31 | "@fabrix/fabrix": ["./index"] 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /lib/tsconfig.release.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "removeComments": true 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /lib/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@fabrix/lint" 3 | } 4 | -------------------------------------------------------------------------------- /lib/typings/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.json' { 2 | const value: any 3 | export default value 4 | } 5 | -------------------------------------------------------------------------------- /lib/utils/index.ts: -------------------------------------------------------------------------------- 1 | export { main as requireMainFilename } from './require-main-filename' 2 | -------------------------------------------------------------------------------- /lib/utils/require-main-filename.ts: -------------------------------------------------------------------------------- 1 | export function main (_require = require) { 2 | const reqMain = _require.main 3 | if (reqMain && isIISNode(reqMain)) { 4 | return handleIISNode(reqMain) 5 | } 6 | else { 7 | return reqMain ? reqMain.filename : process.cwd() 8 | } 9 | } 10 | 11 | function isIISNode (reqMain: any) { 12 | return /\\iisnode\\/.test(reqMain.filename) 13 | } 14 | 15 | function handleIISNode (reqMain: any) { 16 | if (!reqMain.children.length) { 17 | return reqMain.filename 18 | } else { 19 | return reqMain.children[0].filename 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@fabrix/fabrix", 3 | "version": "1.6.4", 4 | "description": "Strongly Typed Modern Web Application Framework for Node.js", 5 | "keywords": [ 6 | "framework", 7 | "platform", 8 | "microservices", 9 | "ecosystem", 10 | "rest", 11 | "api", 12 | "rails", 13 | "grails", 14 | "sails", 15 | "trails", 16 | "sails.js", 17 | "trails.js", 18 | "fabrix", 19 | "fabrix.js", 20 | "fabrix.ts", 21 | "typescript", 22 | "server", 23 | "cqrs", 24 | "event-sourcing", 25 | "graphql" 26 | ], 27 | "scripts": { 28 | "release": "standard-version", 29 | "build": "tsc -p ./lib/tsconfig.release.json", 30 | "lint": "tslint -p ./lib", 31 | "watch": "tsc -w -p ./lib/tsconfig.release.json", 32 | "test": "npm run clean && npm run lint && npm run build && nyc mocha", 33 | "test:performance": "mocha test-performance", 34 | "test:archetype": "cd archetype && npm install && npm test", 35 | "prepublishOnly": "npm run compile", 36 | "compile": "npm run clean && npm run build", 37 | "clean": "rm -rf dist", 38 | "ci": "cd .. && ci", 39 | "webpack": "node_modules/.bin/webpack --config ./test-browser/webpack.config.js" 40 | }, 41 | "main": "dist/index", 42 | "typings": "dist/index", 43 | "files": [ 44 | "dist", 45 | "archetype" 46 | ], 47 | "pre-commit": [ 48 | "test" 49 | ], 50 | "repository": { 51 | "type": "git", 52 | "url": "git+https://github.com/fabrix-app/fabrix.git" 53 | }, 54 | "author": "Fabrix-app Team ", 55 | "contributors": [ 56 | { 57 | "name": "Scott Wyatt", 58 | "email": "scottwyatt86@gmail.com", 59 | "url": "https://github.com/scott-wyatt" 60 | }, 61 | { 62 | "name": "Travis Webb", 63 | "email": "me@traviswebb.com", 64 | "url": "https://github.com/tjwebb" 65 | }, 66 | { 67 | "name": "Konstantin Zolotarev", 68 | "url": "https://github.com/konstantinzolotarev" 69 | }, 70 | { 71 | "name": "Weyland Joyner", 72 | "url": "https://github.com/weyj4" 73 | }, 74 | { 75 | "name": "Jimmy Aumard", 76 | "url": "https://github.com/jaumard" 77 | }, 78 | { 79 | "name": "Robert Rossmann", 80 | "url": "https://github.com/Alaneor" 81 | }, 82 | { 83 | "name": "Mike Hostetler", 84 | "email": "mike@epicfirm.com" 85 | } 86 | ], 87 | "license": "MIT", 88 | "bugs": { 89 | "url": "https://github.com/fabrix-app/fabrix/issues" 90 | }, 91 | "homepage": "https://fabrix.app", 92 | "dependencies": { 93 | "lodash": "^4.17.15", 94 | "mkdirp": "0.5.1" 95 | }, 96 | "devDependencies": { 97 | "@fabrix/lint": "^1.0.0-alpha.3", 98 | "@types/lodash": "^4.14.109", 99 | "@types/mkdirp": "^0.5.2", 100 | "@types/node": "~10.12.10", 101 | "copyfiles": "^2.1.1", 102 | "mocha": "^6", 103 | "nyc": "^14.1.1", 104 | "pre-commit": "^1.2.2", 105 | "proxyquire": "^2.1.3", 106 | "rimraf": "~2.6.2", 107 | "smokesignals": "^3", 108 | "standard-version": "^7.0.0", 109 | "tslib": "~1.9.0", 110 | "tslint": "~5.14.0", 111 | "tslint-microsoft-contrib": "~6.1.0", 112 | "tsutils": "~3.9.1", 113 | "typescript": "~3.3.4000", 114 | "webpack": "^4.33.0", 115 | "webpack-cli": "^3.3.3" 116 | }, 117 | "engines": { 118 | "node": ">= 7.6.0 =< 12", 119 | "npm": ">= 3.10.0" 120 | }, 121 | "runkitExample": "test/runkitExample.js" 122 | } 123 | -------------------------------------------------------------------------------- /test-browser/fixtures/app.js: -------------------------------------------------------------------------------- 1 | export const AppConfigLocales = { 2 | en: { 3 | helloworld: 'hello world', 4 | hello: { 5 | user: 'hello {{username}}' 6 | } 7 | }, 8 | de: { 9 | helloworld: 'hallo Welt', 10 | hello: { 11 | user: 'hallo {{username}}' 12 | } 13 | } 14 | } 15 | 16 | export const testApp = { 17 | pkg: { 18 | name: 'browser-spool-test' 19 | }, 20 | api: { 21 | customkey: {} 22 | }, 23 | config: { 24 | main: { 25 | spools: [], 26 | paths: {}, 27 | target: 'browser' 28 | }, 29 | i18n: { 30 | lng: 'en', 31 | resources: { 32 | en: { 33 | translation: AppConfigLocales.en 34 | }, 35 | de: { 36 | translation: AppConfigLocales.de 37 | } 38 | } 39 | } 40 | }, 41 | locales: AppConfigLocales 42 | } 43 | 44 | // _.defaultsDeep(App, smokesignals.FailsafeConfig) 45 | // module.exports.testApp = App 46 | -------------------------------------------------------------------------------- /test-browser/index.test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Mocha Tests 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /test-browser/integration/browser.test.js: -------------------------------------------------------------------------------- 1 | // import * as assert from 'assert' 2 | import { FabrixApp } from '../../dist/index.js' 3 | import { testApp } from '../fixtures/app.js' 4 | 5 | describe('Browser', () => { 6 | describe('sanity', () => { 7 | it('should exist', () => { 8 | const app = new FabrixApp(testApp) 9 | // assert(app) 10 | 11 | }) 12 | }) 13 | }) -------------------------------------------------------------------------------- /test-browser/run.js: -------------------------------------------------------------------------------- 1 | import './integration/browser.test.js' 2 | 3 | mocha.checkLeaks() 4 | mocha.globals(['app']) 5 | mocha.run() -------------------------------------------------------------------------------- /test-browser/setup.js: -------------------------------------------------------------------------------- 1 | // import 'https://unpkg.com/chai@4.1.2/chai.js' 2 | 3 | mocha.setup('bdd') -------------------------------------------------------------------------------- /test-browser/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | module.exports = { 3 | entry: [ 4 | './test-browser/setup.js', 5 | './test-browser/run.js' 6 | ], 7 | output: { 8 | path: path.resolve(__dirname), 9 | filename: './fixtures/bundle.js', 10 | }, 11 | node: { 12 | fs: 'empty' 13 | }, 14 | // resolve: { 15 | // alias: { 16 | // 'https://unpkg.com/chai@4.1.2/chai.js': 'chai/chai.js' 17 | // } 18 | // }, 19 | // module: { 20 | // rules: [ 21 | // { 22 | // // load chai.js like a normal