Repository: balderdashy/sails Branch: master Commit: fd71efbd4f13 Files: 718 Total size: 2.7 MB Directory structure: gitextract_abogsmgr/ ├── .editorconfig ├── .eslintrc ├── .github/ │ ├── ISSUE_TEMPLATE │ └── PULL_REQUEST_TEMPLATE ├── .gitignore ├── .npmignore ├── .travis.yml ├── CHANGELOG.md ├── CODE-OF-CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE.md ├── MODULES.md ├── README.md ├── ROADMAP.md ├── accessible/ │ ├── generate.js │ └── rc.js ├── appveyor.yml ├── bin/ │ ├── private/ │ │ ├── patched-commander.js │ │ └── read-repl-history-and-start-transcribing.js │ ├── sails-console.js │ ├── sails-debug-console.js │ ├── sails-debug.js │ ├── sails-deploy.js │ ├── sails-generate.js │ ├── sails-inspect.js │ ├── sails-lift.js │ ├── sails-migrate.js │ ├── sails-new.js │ ├── sails-run.js │ ├── sails-upgrade.js │ ├── sails-www.js │ └── sails.js ├── docs/ │ ├── PAGE_NEEDED.md │ ├── README.md │ ├── anatomy/ │ │ ├── .editorconfig.md │ │ ├── .eslintignore.md │ │ ├── .eslintrc.md │ │ ├── .htmlhintrc.md │ │ ├── Gruntfile.js.md │ │ ├── README.md │ │ ├── README.md.md │ │ ├── anatomy.md │ │ ├── api/ │ │ │ ├── api.md │ │ │ ├── controllers/ │ │ │ │ ├── controllers.md │ │ │ │ └── gitkeep.md │ │ │ ├── helpers/ │ │ │ │ ├── .gitkeep.md │ │ │ │ └── helpers.md │ │ │ ├── models/ │ │ │ │ ├── .gitkeep.md │ │ │ │ └── models.md │ │ │ └── policies/ │ │ │ ├── .gitkeep.md │ │ │ └── policies.md │ │ ├── app.js.md │ │ ├── assets/ │ │ │ ├── .eslintrc.md │ │ │ ├── assets.md │ │ │ ├── dependencies/ │ │ │ │ ├── dependencies.md │ │ │ │ └── sails.io.js.md │ │ │ ├── favicon.ico.md │ │ │ ├── images/ │ │ │ │ ├── gitkeep.md │ │ │ │ └── images.md │ │ │ ├── js/ │ │ │ │ ├── gitkeep.md │ │ │ │ └── js.md │ │ │ ├── styles/ │ │ │ │ ├── importer.less.md │ │ │ │ └── styles.md │ │ │ └── templates/ │ │ │ ├── gitkeep.md │ │ │ └── templates.md │ │ ├── config/ │ │ │ ├── blueprints.js.md │ │ │ ├── bootstrap.js.md │ │ │ ├── config.md │ │ │ ├── custom.js.md │ │ │ ├── datastores.js.md │ │ │ ├── env/ │ │ │ │ ├── env.md │ │ │ │ └── production.js.md │ │ │ ├── globals.js.md │ │ │ ├── http.js.md │ │ │ ├── i18n.js.md │ │ │ ├── local.js.md │ │ │ ├── locales/ │ │ │ │ ├── de.json.md │ │ │ │ ├── en.json.md │ │ │ │ ├── es.json.md │ │ │ │ ├── fr.json.md │ │ │ │ └── locales.md │ │ │ ├── log.js.md │ │ │ ├── models.js.md │ │ │ ├── policies.js.md │ │ │ ├── routes.js.md │ │ │ ├── security.js.md │ │ │ ├── session.js.md │ │ │ ├── sockets.js.md │ │ │ └── views.js.md │ │ ├── gitignore.md │ │ ├── package.json.md │ │ ├── sailsrc.md │ │ ├── tasks/ │ │ │ ├── config/ │ │ │ │ ├── babel.js.md │ │ │ │ ├── clean.js.md │ │ │ │ ├── coffee.js.md │ │ │ │ ├── concat.js.md │ │ │ │ ├── config.md │ │ │ │ ├── copy.js.md │ │ │ │ ├── cssmin.js.md │ │ │ │ ├── hash.js.md │ │ │ │ ├── jst.js.md │ │ │ │ ├── less.js.md │ │ │ │ ├── sails-linker.js.md │ │ │ │ ├── sync.js.md │ │ │ │ ├── uglify.js.md │ │ │ │ └── watch.js.md │ │ │ ├── pipeline.js.md │ │ │ ├── register/ │ │ │ │ ├── build.js.md │ │ │ │ ├── buildProd.js.md │ │ │ │ ├── compileAssets.js.md │ │ │ │ ├── default.js.md │ │ │ │ ├── linkAssets.js.md │ │ │ │ ├── linkAssetsBuild.js.md │ │ │ │ ├── linkAssetsBuildProd.js.md │ │ │ │ ├── polyfill.js.md │ │ │ │ ├── prod.js.md │ │ │ │ ├── register.md │ │ │ │ └── syncAssets.js.md │ │ │ └── tasks.md │ │ └── views/ │ │ ├── .eslintrc.md │ │ ├── 404.ejs.md │ │ ├── 500.ejs.md │ │ ├── layouts/ │ │ │ ├── layout.ejs.md │ │ │ └── layouts.md │ │ ├── pages/ │ │ │ ├── homepage.ejs.md │ │ │ └── pages.md │ │ └── views.md │ ├── concepts/ │ │ ├── ActionsAndControllers/ │ │ │ ├── ActionsAndControllers.md │ │ │ ├── GeneratingActions.md │ │ │ └── RoutingToActions.md │ │ ├── Assets/ │ │ │ ├── Assets.md │ │ │ ├── DefaultTasks.md │ │ │ ├── DisablingGrunt.md │ │ │ └── TaskAutomation.md │ │ ├── Blueprints/ │ │ │ ├── Blueprint Actions.md │ │ │ ├── Blueprint Routes.md │ │ │ └── Blueprints.md │ │ ├── Configuration/ │ │ │ ├── Configuration.md │ │ │ ├── localjsfile.md │ │ │ └── usingsailsrcfiles.md │ │ ├── Deployment/ │ │ │ ├── Deployment.md │ │ │ ├── FAQ.md │ │ │ ├── Hosting.md │ │ │ └── Scaling.md │ │ ├── E-commerce/ │ │ │ └── E-commerce.md │ │ ├── File Uploads/ │ │ │ ├── File Uploads.md │ │ │ ├── uploading-to-amazon-s3.md │ │ │ └── uploading-to-mongo-gridfs.md │ │ ├── Globals/ │ │ │ ├── DisablingGlobals.md │ │ │ └── Globals.md │ │ ├── Helpers/ │ │ │ ├── ExampleHelper.md │ │ │ └── Helpers.md │ │ ├── Internationalization/ │ │ │ ├── Internationalization.md │ │ │ ├── Locales.md │ │ │ └── TranslatingDynamicContent.md │ │ ├── Logging/ │ │ │ ├── Custom log messages.md │ │ │ └── Logging.md │ │ ├── Middleware/ │ │ │ ├── ConventionalDefaults.md │ │ │ └── Middleware.md │ │ ├── ORM/ │ │ │ ├── Associations/ │ │ │ │ ├── Associations.md │ │ │ │ ├── ManytoMany.md │ │ │ │ ├── OneWayAssociation.md │ │ │ │ ├── OnetoMany.md │ │ │ │ ├── OnetoOne.md │ │ │ │ ├── Reflexive.md │ │ │ │ └── ThroughAssociations.md │ │ │ ├── Attributes.md │ │ │ ├── Lifecyclecallbacks.md │ │ │ ├── Models.md │ │ │ ├── ORM.md │ │ │ ├── Querylanguage.md │ │ │ ├── Records.md │ │ │ ├── Validations.md │ │ │ ├── errors.md │ │ │ ├── model-settings.md │ │ │ └── standalone-usage.md │ │ ├── Policies/ │ │ │ ├── Permissions.md │ │ │ └── Policies.md │ │ ├── Programmatic Usage/ │ │ │ ├── Programmatic Usage.md │ │ │ └── Tips and Tricks.md │ │ ├── README.md │ │ ├── Realtime/ │ │ │ ├── Multi-server environments.md │ │ │ ├── On the client.md │ │ │ ├── On the server.md │ │ │ └── Realtime.md │ │ ├── Routes/ │ │ │ ├── RouteTargetSyntax.md │ │ │ └── Routes.md │ │ ├── Security/ │ │ │ ├── CORS.md │ │ │ ├── CSRF.md │ │ │ ├── Clickjacking.md │ │ │ ├── ContentSecurityPolicy.md │ │ │ ├── DDOS.md │ │ │ ├── P3P.md │ │ │ ├── Security.md │ │ │ ├── SocketHijacking.md │ │ │ ├── StrictTransportSecurity.md │ │ │ └── XSS.md │ │ ├── Services/ │ │ │ └── Services.md │ │ ├── Sessions/ │ │ │ └── sessions.md │ │ ├── Testing/ │ │ │ └── Testing.md │ │ ├── Views/ │ │ │ ├── Layouts.md │ │ │ ├── Locals.md │ │ │ ├── Partials.md │ │ │ ├── ViewEngines.md │ │ │ └── Views.md │ │ ├── concepts.md │ │ ├── extending-sails/ │ │ │ ├── Adapters/ │ │ │ │ ├── Adapters.md │ │ │ │ ├── adapterList.md │ │ │ │ └── customAdapters.md │ │ │ ├── Custom Responses/ │ │ │ │ ├── AddingCustomResponse.md │ │ │ │ └── Custom Responses.md │ │ │ ├── Generators/ │ │ │ │ ├── Generators.md │ │ │ │ ├── customGenerators.md │ │ │ │ └── generatorList.md │ │ │ ├── Hooks/ │ │ │ │ ├── Hooks.md │ │ │ │ ├── available-hooks.md │ │ │ │ ├── events.md │ │ │ │ ├── hookspec/ │ │ │ │ │ ├── configure.md │ │ │ │ │ ├── defaults.md │ │ │ │ │ ├── hookspec.md │ │ │ │ │ ├── initialize.md │ │ │ │ │ ├── register-actions.md │ │ │ │ │ └── routes.md │ │ │ │ ├── installablehooks.md │ │ │ │ ├── projecthooks.md │ │ │ │ └── usinghooks.md │ │ │ └── extending-sails.md │ │ └── shell-scripts/ │ │ └── shell-scripts.md │ ├── contributing/ │ │ ├── adapter-specification.md │ │ ├── code-of-conduct.md │ │ ├── code-submission-guidelines/ │ │ │ ├── best-practices.md │ │ │ ├── code-submission-guidelines.md │ │ │ ├── sending-pull-requests.md │ │ │ └── writing-tests.md │ │ ├── contributing-to-the-documentation.md │ │ ├── contributors-pledge.md │ │ ├── core-maintainers.md │ │ ├── intro-to-custom-adapters.md │ │ ├── issue-contributions.md │ │ ├── preface.md │ │ ├── proposing-features/ │ │ │ ├── proposing-features.md │ │ │ └── submitting-a-proposal.md │ │ └── stability-index.md │ ├── faq/ │ │ ├── README.md │ │ └── faq.md │ ├── irc/ │ │ └── irc.md │ ├── reference/ │ │ ├── README.md │ │ ├── application/ │ │ │ ├── advanced-usage/ │ │ │ │ ├── advanced-usage.md │ │ │ │ ├── lifecycle.md │ │ │ │ ├── sails.LOOKS_LIKE_ASSET_RX.md │ │ │ │ ├── sails.getActions.md │ │ │ │ ├── sails.getBaseUrl.md │ │ │ │ ├── sails.getRouteFor.md │ │ │ │ ├── sails.lift.md │ │ │ │ ├── sails.load.md │ │ │ │ ├── sails.lower.md │ │ │ │ ├── sails.registerAction.md │ │ │ │ ├── sails.registerActionMiddleware.md │ │ │ │ ├── sails.reloadActions.md │ │ │ │ ├── sails.renderView.md │ │ │ │ └── sails.request.md │ │ │ ├── application.md │ │ │ ├── sails.config.custom.md │ │ │ ├── sails.getDatastore.md │ │ │ ├── sails.getUrlFor.md │ │ │ └── sails.log.md │ │ ├── blueprint-api/ │ │ │ ├── Add.md │ │ │ ├── Create.md │ │ │ ├── Destroy.md │ │ │ ├── Find.md │ │ │ ├── FindOne.md │ │ │ ├── Populate.md │ │ │ ├── Remove.md │ │ │ ├── Replace.md │ │ │ ├── Update.md │ │ │ └── blueprint-api.md │ │ ├── cli/ │ │ │ ├── cli.md │ │ │ ├── sailsconsole.md │ │ │ ├── sailsdebug.md │ │ │ ├── sailsgenerate.md │ │ │ ├── sailsinspect.md │ │ │ ├── sailslift.md │ │ │ ├── sailsnew.md │ │ │ └── sailsversion.md │ │ ├── reference.md │ │ ├── req/ │ │ │ ├── req._startTime.md │ │ │ ├── req.accepts.md │ │ │ ├── req.acceptsCharsets.md │ │ │ ├── req.acceptsLanguages.md │ │ │ ├── req.allParams.md │ │ │ ├── req.body.md │ │ │ ├── req.cookies.md │ │ │ ├── req.file.md │ │ │ ├── req.fresh.md │ │ │ ├── req.get.md │ │ │ ├── req.headers.md │ │ │ ├── req.host.md │ │ │ ├── req.hostname.md │ │ │ ├── req.ip.md │ │ │ ├── req.ips.md │ │ │ ├── req.is.md │ │ │ ├── req.isSocket.md │ │ │ ├── req.md │ │ │ ├── req.method.md │ │ │ ├── req.options/ │ │ │ │ └── req.options.md │ │ │ ├── req.originalUrl.md │ │ │ ├── req.param.md │ │ │ ├── req.params.md │ │ │ ├── req.path.md │ │ │ ├── req.protocol.md │ │ │ ├── req.query.md │ │ │ ├── req.secure.md │ │ │ ├── req.setLocale.md │ │ │ ├── req.setTimeout.md │ │ │ ├── req.signedCookies.md │ │ │ ├── req.socket.md │ │ │ ├── req.subdomains.md │ │ │ ├── req.url.md │ │ │ ├── req.wantsJSON.md │ │ │ └── req.xhr.md │ │ ├── res/ │ │ │ ├── res.attachment.md │ │ │ ├── res.badRequest.md │ │ │ ├── res.clearCookie.md │ │ │ ├── res.cookie.md │ │ │ ├── res.forbidden.md │ │ │ ├── res.get.md │ │ │ ├── res.json.md │ │ │ ├── res.jsonp.md │ │ │ ├── res.location.md │ │ │ ├── res.md │ │ │ ├── res.negotiate.md │ │ │ ├── res.notFound.md │ │ │ ├── res.ok.md │ │ │ ├── res.redirect.md │ │ │ ├── res.send.md │ │ │ ├── res.serverError.md │ │ │ ├── res.set.md │ │ │ ├── res.status.md │ │ │ ├── res.type.md │ │ │ └── res.view.md │ │ ├── sails.config/ │ │ │ ├── miscellaneous.md │ │ │ ├── sails.config.blueprints.md │ │ │ ├── sails.config.bootstrap.md │ │ │ ├── sails.config.connections.md │ │ │ ├── sails.config.custom.md │ │ │ ├── sails.config.globals.md │ │ │ ├── sails.config.http.md │ │ │ ├── sails.config.i18n.md │ │ │ ├── sails.config.log.md │ │ │ ├── sails.config.md │ │ │ ├── sails.config.models.md │ │ │ ├── sails.config.policies.md │ │ │ ├── sails.config.routes.md │ │ │ ├── sails.config.security.md │ │ │ ├── sails.config.session.md │ │ │ ├── sails.config.sockets.md │ │ │ └── sails.config.views.md │ │ ├── waterline/ │ │ │ ├── datastores/ │ │ │ │ ├── datastores.md │ │ │ │ ├── driver.md │ │ │ │ ├── leaseConnection.md │ │ │ │ ├── manager.md │ │ │ │ ├── sendNativeQuery.md │ │ │ │ └── transaction.md │ │ │ ├── models/ │ │ │ │ ├── addToCollection.md │ │ │ │ ├── archive.md │ │ │ │ ├── archiveOne.md │ │ │ │ ├── avg.md │ │ │ │ ├── count.md │ │ │ │ ├── create.md │ │ │ │ ├── createEach.md │ │ │ │ ├── destroy.md │ │ │ │ ├── destroyOne.md │ │ │ │ ├── find.md │ │ │ │ ├── findOne.md │ │ │ │ ├── findOrCreate.md │ │ │ │ ├── getDatastore.md │ │ │ │ ├── models.md │ │ │ │ ├── native.md │ │ │ │ ├── query.md │ │ │ │ ├── removeFromCollection.md │ │ │ │ ├── replaceCollection.md │ │ │ │ ├── stream.md │ │ │ │ ├── sum.md │ │ │ │ ├── update.md │ │ │ │ ├── updateOne.md │ │ │ │ └── validate.md │ │ │ ├── queries/ │ │ │ │ ├── catch.md │ │ │ │ ├── decrypt.md │ │ │ │ ├── exec.md │ │ │ │ ├── fetch.md │ │ │ │ ├── intercept.md │ │ │ │ ├── limit.md │ │ │ │ ├── meta.md │ │ │ │ ├── populate.md │ │ │ │ ├── queries.md │ │ │ │ ├── skip.md │ │ │ │ ├── sort.md │ │ │ │ ├── then.md │ │ │ │ ├── toPromise.md │ │ │ │ ├── tolerate.md │ │ │ │ ├── usingConnection.md │ │ │ │ └── where.md │ │ │ ├── records/ │ │ │ │ ├── records.md │ │ │ │ └── toJSON.md │ │ │ └── waterline.md │ │ └── websockets/ │ │ ├── resourceful-pubsub/ │ │ │ ├── get-room-name.md │ │ │ ├── publish.md │ │ │ ├── resourceful-pubsub.md │ │ │ ├── subscribe.md │ │ │ └── unsubscribe.md │ │ ├── sails.io.js/ │ │ │ ├── SailsSocket/ │ │ │ │ ├── SailsSocket.md │ │ │ │ ├── methods.md │ │ │ │ └── properties.md │ │ │ ├── io.sails.md │ │ │ ├── io.socket.md │ │ │ ├── io.socket.off.md │ │ │ ├── io.socket.on.md │ │ │ ├── sails.io.js.md │ │ │ ├── socket.delete.md │ │ │ ├── socket.get.md │ │ │ ├── socket.patch.md │ │ │ ├── socket.post.md │ │ │ ├── socket.put.md │ │ │ └── socket.request.md │ │ ├── sails.sockets/ │ │ │ ├── sails.sockets.addRoomMembersToRooms.md │ │ │ ├── sails.sockets.blast.md │ │ │ ├── sails.sockets.broadcast.md │ │ │ ├── sails.sockets.getid.md │ │ │ ├── sails.sockets.id.md │ │ │ ├── sails.sockets.join.md │ │ │ ├── sails.sockets.leave.md │ │ │ ├── sails.sockets.leaveAll.md │ │ │ ├── sails.sockets.md │ │ │ └── sails.sockets.removeRoomMembersFromRoom.md │ │ └── websockets.md │ ├── security/ │ │ ├── README.md │ │ └── SAILS-SECURITY-POLICY.md │ ├── tutorials/ │ │ ├── coffeeScript.md │ │ ├── full-stack-javascript.md │ │ ├── low-level-mysql-access.md │ │ ├── mongo.md │ │ ├── tutorials.md │ │ └── typeScript.md │ ├── upgrading/ │ │ ├── To0.10.md │ │ ├── To0.11.md │ │ ├── To0.12.md │ │ ├── To1.0.md │ │ └── upgrading.md │ └── version-notes/ │ ├── 0.10.x/ │ │ ├── 0.10.x.md │ │ ├── Changelog0.10.0-rc9.md │ │ └── Changelog0.10x.md │ ├── 0.11.x/ │ │ ├── 0.11.x.md │ │ └── MigrationGuide0.11.md │ ├── 0.12.x/ │ │ ├── 0.12.x.md │ │ └── migration-guide-0.12.md │ ├── 0.8.x/ │ │ ├── 0.8.x.md │ │ ├── Changelog0.8.7x.md │ │ ├── Changelog0.8.8x.md │ │ ├── Changelog0.8.9.md │ │ └── ChangelogPre-0.8.77.md │ ├── 0.9.x/ │ │ ├── 0.9.x.md │ │ ├── Changelog0.9.0.md │ │ ├── Changelog0.9.16.md │ │ ├── Changelog0.9.4.md │ │ └── Changelog0.9.7.md │ └── 1.0.x/ │ └── migration-guide-1.0.md ├── errors/ │ ├── README.md │ ├── fatal.js │ ├── index.js │ └── warn.js ├── lib/ │ ├── EVENTS.md │ ├── README.md │ ├── app/ │ │ ├── README.md │ │ ├── Sails.js │ │ ├── configuration/ │ │ │ ├── default-hooks.js │ │ │ ├── index.js │ │ │ ├── load.js │ │ │ └── rc.js │ │ ├── get-actions.js │ │ ├── get-route-for.js │ │ ├── get-url-for.js │ │ ├── index.js │ │ ├── lift.js │ │ ├── load.js │ │ ├── lower.js │ │ ├── private/ │ │ │ ├── after.js │ │ │ ├── bootstrap.js │ │ │ ├── checkGruntConfig.js │ │ │ ├── controller/ │ │ │ │ ├── README.md │ │ │ │ ├── help-register-action.js │ │ │ │ └── load-action-modules.js │ │ │ ├── exposeGlobals.js │ │ │ ├── initialize.js │ │ │ ├── inspect.js │ │ │ ├── isLocalSailsValid.js │ │ │ ├── isSailsAppSync.js │ │ │ ├── loadHooks.js │ │ │ ├── toJSON.js │ │ │ └── toString.js │ │ ├── register-action-middleware.js │ │ ├── register-action.js │ │ ├── reload-actions.js │ │ └── request.js │ ├── hooks/ │ │ ├── README.md │ │ ├── blueprints/ │ │ │ ├── README.md │ │ │ ├── actionUtil.js │ │ │ ├── actions/ │ │ │ │ ├── add.js │ │ │ │ ├── create.js │ │ │ │ ├── destroy.js │ │ │ │ ├── find.js │ │ │ │ ├── findOne.js │ │ │ │ ├── populate.js │ │ │ │ ├── remove.js │ │ │ │ ├── replace.js │ │ │ │ └── update.js │ │ │ ├── formatUsageError.js │ │ │ ├── index.js │ │ │ ├── onRoute.js │ │ │ └── parse-blueprint-options.js │ │ ├── helpers/ │ │ │ ├── index.js │ │ │ └── private/ │ │ │ ├── iterate-helpers.js │ │ │ └── load-helpers.js │ │ ├── http/ │ │ │ ├── README.md │ │ │ ├── get-configured-http-middleware-fns.js │ │ │ ├── index.js │ │ │ ├── initialize.js │ │ │ ├── start.js │ │ │ └── view.js │ │ ├── i18n/ │ │ │ └── index.js │ │ ├── index.js │ │ ├── logger/ │ │ │ ├── README.md │ │ │ ├── index.js │ │ │ └── ship.js │ │ ├── moduleloader/ │ │ │ ├── README.md │ │ │ └── index.js │ │ ├── policies/ │ │ │ ├── README.md │ │ │ └── index.js │ │ ├── pubsub/ │ │ │ ├── README.md │ │ │ └── index.js │ │ ├── request/ │ │ │ ├── README.md │ │ │ ├── index.js │ │ │ ├── locals.js │ │ │ ├── metadata.js │ │ │ ├── param.js │ │ │ ├── params.all.js │ │ │ ├── qualifiers.js │ │ │ └── validate.js │ │ ├── responses/ │ │ │ ├── README.md │ │ │ ├── defaults/ │ │ │ │ ├── badRequest.js │ │ │ │ ├── forbidden.js │ │ │ │ ├── negotiate.js │ │ │ │ ├── notFound.js │ │ │ │ ├── ok.js │ │ │ │ └── serverError.js │ │ │ ├── index.js │ │ │ └── onRoute.js │ │ ├── security/ │ │ │ ├── README.md │ │ │ ├── cors/ │ │ │ │ ├── index.js │ │ │ │ ├── set-headers.js │ │ │ │ └── set-preflight-config.js │ │ │ ├── csrf/ │ │ │ │ ├── grant-csrf-token.js │ │ │ │ └── index.js │ │ │ └── index.js │ │ ├── services/ │ │ │ └── index.js │ │ ├── session/ │ │ │ ├── README.md │ │ │ └── index.js │ │ ├── userconfig/ │ │ │ ├── README.md │ │ │ └── index.js │ │ ├── userhooks/ │ │ │ ├── README.md │ │ │ └── index.js │ │ └── views/ │ │ ├── configure.js │ │ ├── default-view-rendering-fn.js │ │ ├── escape-html-entities-deep.js │ │ ├── get-implicit-defaults.js │ │ ├── html-scriptify.js │ │ ├── index.js │ │ ├── onRoute.js │ │ ├── render.js │ │ ├── res.view.js │ │ ├── stat-views.js │ │ └── unescape-html-entities-deep-lite.min.string.js │ ├── index.js │ ├── router/ │ │ ├── README.md │ │ ├── bind.js │ │ ├── bindDefaultHandlers.js │ │ ├── index.js │ │ ├── mock-req.js │ │ ├── mock-res.js │ │ ├── req.js │ │ └── res.js │ └── util/ │ ├── check-origin-url.js │ ├── deep-extend.js │ ├── detect-verb.js │ └── rc.js ├── package.json └── test/ ├── .eslintrc ├── README.md ├── benchmarks/ │ ├── README.md │ ├── helpers/ │ │ └── benchmarx.js │ ├── sails.load.test.js │ └── sails.request.generic.test.js ├── fixtures/ │ ├── constants.js │ ├── customHooks.js │ └── middleware.js ├── helpers/ │ ├── RouteFactory.helper.js │ ├── router.js │ ├── sails.js │ ├── test-spawning-sails-child-process-in-cwd.js │ └── test-spawning-sails-lift-child-process-in-cwd.js ├── hooks/ │ ├── blueprints/ │ │ └── initialize.test.js │ ├── http/ │ │ └── initialize.test.js │ ├── pubsub/ │ │ └── initialize.test.js │ ├── request/ │ │ ├── initialize.test.js │ │ ├── req.metadata.test.js │ │ └── req.options.sticky.test.js │ └── views/ │ ├── ejs/ │ │ └── index.i18n.ejs │ ├── intialize.test.js │ ├── locales/ │ │ ├── en.json │ │ ├── es.json │ │ └── eu.json │ ├── res.render.i18n.js │ ├── res.view.test.js │ └── skipAssets.test.js ├── init.js ├── integration/ │ ├── README.md │ ├── cert/ │ │ ├── sailstest-cert.pem │ │ └── sailstest-key.pem │ ├── fixtures/ │ │ ├── hooks/ │ │ │ └── installable/ │ │ │ ├── add-policy/ │ │ │ │ ├── index.js │ │ │ │ └── package.json │ │ │ ├── async/ │ │ │ │ └── index.js.txt │ │ │ └── shout/ │ │ │ ├── index.js │ │ │ └── package.json │ │ ├── sampleapp/ │ │ │ ├── api/ │ │ │ │ ├── controllers/ │ │ │ │ │ ├── EmptyController.js │ │ │ │ │ ├── PetController.js │ │ │ │ │ ├── QuizController.js │ │ │ │ │ ├── TestController.js │ │ │ │ │ ├── UserController.js │ │ │ │ │ ├── UserProfileController.js │ │ │ │ │ └── ViewTestController.js │ │ │ │ ├── models/ │ │ │ │ │ ├── Empty.js │ │ │ │ │ ├── Pet.js │ │ │ │ │ ├── Quiz.js │ │ │ │ │ ├── Test.js │ │ │ │ │ ├── User.js │ │ │ │ │ └── UserProfile.js │ │ │ │ ├── policies/ │ │ │ │ │ ├── error_policy.js │ │ │ │ │ └── fake_auth.js │ │ │ │ └── services/ │ │ │ │ └── TestService.js │ │ │ ├── config/ │ │ │ │ └── local.js │ │ │ └── views/ │ │ │ ├── app/ │ │ │ │ ├── index.ejs │ │ │ │ └── user/ │ │ │ │ └── homepage.ejs │ │ │ ├── pages/ │ │ │ │ └── homepage.ejs │ │ │ └── viewtest/ │ │ │ ├── create.ejs │ │ │ ├── csrf.ejs │ │ │ ├── index.ejs │ │ │ └── viewOptions.ejs │ │ └── users.js │ ├── generate.test.js │ ├── globals.test.js │ ├── helpers/ │ │ ├── appHelper.js │ │ ├── httpHelper.js │ │ └── socketHelper.js │ ├── hook.3rdparty.test.js │ ├── hook.blueprints.action.routes.test.js │ ├── hook.blueprints.blacklist.test.js │ ├── hook.blueprints.index.routes.test.js │ ├── hook.blueprints.restful.routes.test.js │ ├── hook.blueprints.shortcut.routes.test.js │ ├── hook.cors.test.js │ ├── hook.csrf.test.js │ ├── hook.helpers.test.js │ ├── hook.i18n.test.js │ ├── hook.policies.test.js │ ├── hook.pubsub.modelEvents.noSubscribers.test.js │ ├── hook.pubsub.modelEvents.subscribers.test.js │ ├── hook.sockets.interpreter.test.js │ ├── hook.userconfig.test.js │ ├── hook.views.test.js │ ├── hooks.user.test.js │ ├── lift.https.test.js │ ├── lift.lower.test.js │ ├── lift.test.js │ ├── middleware.404.test.js │ ├── middleware.500.test.js │ ├── middleware.compression.test.js │ ├── middleware.cookieParser.test.js │ ├── middleware.favicon.test.js │ ├── middleware.handleBodyParserError.test.js │ ├── middleware.sails.test.js │ ├── middleware.session.redis.test.js │ ├── middleware.session.test.js │ ├── middleware.startRequestTimer.test.js │ ├── middleware.static.test.js │ ├── new.test.js │ ├── router.params.test.js │ ├── router.specifiedRoutes.test.js │ ├── router.viewRendering.test.js │ └── www.test.js ├── mocha.opts └── unit/ ├── App.prototype.load.test.js ├── README.md ├── app.getRouteFor.test.js ├── app.getUrlFor.test.js ├── app.initializeHooks.test.js ├── app.lower.test.js ├── app.registerAction.test.js ├── app.reloadActions.test.js ├── bootstrap.test.js ├── controller.test.js ├── req.errors.test.js ├── req.session.test.js ├── req.test.js ├── res.test.js ├── router.bind.test.js ├── router.ordering.test.js ├── router.test.js ├── router.unbind.test.js └── virtual-request-interpreter.test.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .editorconfig ================================================ # ╔═╗╔╦╗╦╔╦╗╔═╗╦═╗┌─┐┌─┐┌┐┌┌─┐┬┌─┐ # ║╣ ║║║ ║ ║ ║╠╦╝│ │ ││││├┤ ││ ┬ # o╚═╝═╩╝╩ ╩ ╚═╝╩╚═└─┘└─┘┘└┘└ ┴└─┘ # # This file (`.editorconfig`) exists to help maintain consistent formatting # throughout this package, the Sails framework, and the Node-Machine project. # # To review what each of these options mean, see: # http://editorconfig.org/ root = true [*] indent_style = space indent_size = 2 end_of_line = lf charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true ================================================ FILE: .eslintrc ================================================ { // ╔═╗╔═╗╦ ╦╔╗╔╔╦╗┬─┐┌─┐ // ║╣ ╚═╗║ ║║║║ ║ ├┬┘│ // o╚═╝╚═╝╩═╝╩╝╚╝ ╩ ┴└─└─┘ // A set of basic conventions (similar to .jshintrc) for use within any // arbitrary JavaScript / Node.js package -- inside or outside Sails.js. // For the master copy of this file, see the `.eslintrc` template file in // the `sails-generate` package (https://www.npmjs.com/package/sails-generate.) // Designed for ESLint v4. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // For more information about any of the rules below, check out the relevant // reference page on eslint.org. For example, to get details on "no-sequences", // you would visit `http://eslint.org/docs/rules/no-sequences`. If you're unsure // or could use some advice, come by https://sailsjs.com/support. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "env": { "node": true }, "parserOptions": { "ecmaVersion": 8 }, "globals": { "Promise": true, "Symbol": true, // ^^Available since Node v4 }, "rules": { "callback-return": ["error", ["done", "proceed", "next", "onwards", "callback", "cb"]], "camelcase": ["warn", {"properties": "always"}], "comma-style": ["warn", "last"], "curly": ["error"], "eqeqeq": ["error", "always"], "eol-last": ["warn"], "handle-callback-err": ["error"], "indent": ["warn", 2, { "SwitchCase": 1, "MemberExpression": "off", "FunctionDeclaration": {"body":1, "parameters": "off"}, "FunctionExpression": {"body":1, "parameters": "off"}, "CallExpression": {"arguments":"off"}, "ArrayExpression": 1, "ObjectExpression": 1, "ignoredNodes": ["ConditionalExpression"] }], "linebreak-style": ["error", "unix"], "no-dupe-keys": ["error"], "no-duplicate-case": ["error"], "no-extra-semi": ["warn"], "no-labels": ["error"], "no-mixed-spaces-and-tabs": ["error", "smart-tabs"], "no-redeclare": ["warn"], "no-return-assign": ["error", "always"], "no-sequences": ["error"], "no-trailing-spaces": ["warn"], "no-undef": ["error"], "no-unexpected-multiline": ["warn"], "no-unreachable": ["warn"], "no-unused-vars": ["warn", {"caughtErrors":"all", "caughtErrorsIgnorePattern": "^unused($|[A-Z].*$)", "argsIgnorePattern": "^unused($|[A-Z].*$)", "varsIgnorePattern": "^unused($|[A-Z].*$)" }], "no-use-before-define": ["error", {"functions":false}], "one-var": ["warn", "never"], "quotes": ["warn", "single", {"avoidEscape":false, "allowTemplateLiterals":true}], "semi": ["error", "always"], "semi-spacing": ["warn", {"before":false, "after":true}], "semi-style": ["warn", "last"] } } ================================================ FILE: .github/ISSUE_TEMPLATE ================================================ **Node version**: **Sails version** _(sails)_: **ORM hook version** _(sails-hook-orm)_: **Sockets hook version** _(sails-hook-sockets)_: **Organics hook version** _(sails-hook-organics)_: **Grunt hook version** _(sails-hook-grunt)_: **Uploads hook version** _(sails-hook-uploads)_: **DB adapter & version** _(e.g. sails-mysql@5.55.5)_: **Skipper adapter & version** _(e.g. skipper-s3@5.55.5)_:
================================================ FILE: .github/PULL_REQUEST_TEMPLATE ================================================ ================================================ FILE: .gitignore ================================================ # ┌─┐┬┌┬┐╦╔═╗╔╗╔╔═╗╦═╗╔═╗ # │ ┬│ │ ║║ ╦║║║║ ║╠╦╝║╣ # o└─┘┴ ┴ ╩╚═╝╝╚╝╚═╝╩╚═╚═╝ # # This file (`.gitignore`) exists to signify to `git` that certain files # and/or directories should be ignored for the purposes of version control. # # This is primarily useful for excluding temporary files of all sorts; stuff # generated by IDEs, build scripts, automated tests, package managers, or even # end-users (e.g. file uploads). `.gitignore` files like this also do a nice job # at keeping sensitive credentials and personal data out of version control systems. # ############################ # sails / node.js / npm ############################ node_modules .tmp npm-debug.log package-lock.json package-lock.* .waterline .node_history ############################ # editor & OS files ############################ *.swo *.swp *.swn *.swm *.seed *.log *.out *.pid lib-cov .DS_STORE *# *\# .\#* *~ .idea .netbeans nbproject ############################ # misc ############################ dump.rdb ================================================ FILE: .npmignore ================================================ .git ./.gitignore ./.jshintrc ./.editorconfig ./.travis.yml ./appveyor.yml ./example ./examples ./test ./tests ./sails-docs ./.github node_modules npm-debug.log .node_history *.swo *.swp *.swn *.swm *.seed *.log *.out *.pid lib-cov .DS_STORE *# *\# .\#* *~ .idea .netbeans nbproject .tmp dump.rdb ================================================ FILE: .travis.yml ================================================ # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # ╔╦╗╦═╗╔═╗╦ ╦╦╔═╗ ┬ ┬┌┬┐┬ # # ║ ╠╦╝╠═╣╚╗╔╝║╚═╗ └┬┘││││ # # o ╩ ╩╚═╩ ╩ ╚╝ ╩╚═╝o ┴ ┴ ┴┴─┘ # # # # This file configures Travis CI. # # (i.e. how we run the tests... mainly) # # # # https://docs.travis-ci.com/user/customizing-the-build # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # language: node_js node_js: - "12" - "14" - "16" branches: only: - master notifications: email: - ci@sailsjs.com env: TEST_REDIS_SESSION=true before_script: sudo redis-server /etc/redis/redis.conf --daemonize yes --port 6380 --requirepass 'secret' ================================================ FILE: CHANGELOG.md ================================================ # Sails Changelog ## 1.2.0 - Added `sails migrate` for quickly running auto-migrations by hand - The output of `sails inspect` no longer includes controller information - When loading user hooks, if `sails.config.loadHooks` is specified, skip hooks whose names aren't explicitly included - Increased time to display warning message in `config/bootstrap.js` from 5 seconds to 30 seconds - Switched to using `updateOne` in the "update" blueprint - Blueprint queries no longer include `fetch: true` by default, to avoid warnings from `updateOne` - Update error mesage in default `serverError` response to use flaverr - In `lib/router/res.js`, instead of always setting 'content-type' to 'application/json', only set it if `res.get('content-type')` is falsy - Update flaverr dependency - Update i18n-2 dependency to resolve deprecation warning - Update rc dependency to address potential vulnerabilities - Update machinepack-process dependency to address potential vulnerabilities - Update machinepack-redis dependency to address potential vulnerabilities ## 1.1.0 > As always, we owe a debt of gratitude to our contributors-- we mentioned you below next to the features/enhancements/fixes you contributed to. (Apologies to anyone we missed, there was a lot in this release! Let us know and we'll add you to the list.) > > We especially want to thank those contributors who helped out with the documentation: > - [Rachael Shaw](https://github.com/rachaelshaw) > - [Mike McNeil](https://github.com/mikermcneil) > - Ali Norouzi > - [Ronny Medina](https://github.com/ronnymedinave) > - [Alex Schwarz](https://github.com/alexschwarz89) > - [Xavier Spriet](https://github.com/loginx) > - [Ian Harris](https://github.com/harrisi) > - [Vladyslav Piskunov](https://github.com/vpiskunov) > - [Michael Frederick](https://github.com/mdfrederick) > - [Mark Strelecky](https://github.com/streleck) > - [Freddy](https://github.com/mdfrederick) > - [pavan](https://github.com/pavanmehta91) > - [Scott Reed](https://github.com/AdJesumPerMariam) > - [Mario Colque](https://github.com/colkito) > - [Okoli Lemuel](https://github.com/okolilemuel) > - [ultimate-tester](https://github.com/ultimate-tester) > - [@snidell](https://github.com/snidell) > - [Pika](https://github.com/ThatNerdyPikachu) > - [AYEDOUN Fiacre](https://github.com/afidosstar) > - [Tim Wisniewski](https://github.com/timwis) > - [Tom Saleeba](https://github.com/tomsaleeba) > - [s-slavchev](https://github.com/s-slavchev) > - [Daniel Harvey](https://github.com/danielsharvey) > - [Julien Le Coupanec](https://github.com/LeCoupa) > - [floriancummings](https://github.com/floriancummings) > - [Bernardo Gomes](https://github.com/Bernardoow) > - [Eli Peters](https://github.com/elipeters) > - [Dennis Cheung](https://github.com/oaksofmamre) > - [Minh Tri Nguyen](https://github.com/kevinvn1709) > > Finally, big thanks to [Dennis Cheung](https://github.com/oaksofmamre) who did the painstaking work of compiling this changelog! > > ~[mike](https://twitter.com/mikermcneil) #### Framework: - **New model methods: `.updateOne()`, `.destroyOne()`, and `.archiveOne()`** - **`exits` argument may now be excluded from your `fn` function in all helpers, actions2 definitions, and shell scripts.** - **Cleaner usage for `.stream()`, `.transaction()`, and `.leaseConnection()`. Functions provided as procedural parameters (i.e. `during` and iteratees) no longer expect a callback to be invoked, as long as you omit their 2nd callabck argument from the function signature.** - **The bootstrap function (`config/bootstrap.js`) and the `initialize` function of hooks no longer expect a callback to be invoked, as long as the callback argument is excluded from the function signature.** - **New chainable methods available on helper invocations: `.timeout()` and `.retry()`.** - Use vuejs VueRouter (and a virtual page example) by default, and make vuejs more apparent - Update helper, action, and hook generators, plus a few other updates to boilerplate output from generators. - Update boilerplate bootstrap and hook initialize functions. - Disable 'async' dependency when generated the expanded starter app, to avoid confusion. (Can always still be installed, it just won't be bundled by default anymore, and auto-globalization is also, of course, disabled.) - Include FontAwesome 4 files, and update instructions for removal if desired - Update check for production/staging in vuejs to assume we're in production if no SAILS_LOCALS are present - Provide documentation tip on how to configure Mongo as default datastore w/ link to tutorial - Take advantage of next-gen whelk. - Improve Windows compatibility (for 'sails generate page', sails generate action, etc) - Exclude 'async' dep by default in all new apps - Update boilerplate to take advantage of optional 'exits' argument in helpers, actions, and shell scripts. - Use updated bootstrap for web app template - Implement Google fonts/typekit/google tag manager/google analytics - Include (hard-code) Bowser inline now - Improve to also include 'is' rule - Redirect away ALL unrecognized base subdomains (not just webhooks and click), but only do it in staging/production. - Fix "sails g page" with nested folders on Windows - Add FileList and FormData to reserved list to protect them from mangling. (Should be fine anyway, but this protects against potential issues from changing uglify versions.) - Take advantage of patch in Parasails 0.7.7 - Leverage .updateOne() - Add 5 second timeout for all four of those calls, to protect user experience from any 3rd party API call issues. (For context, this is rare, but it happens. I've noticed Stripe go all 500 on a customer app twice in the last 6 months: once for about 28 minutes earlier this summer, and once last month for a split second. I've never seen it hang, but this is such a common thing with other 3rd party apis better to be safe than sorry. An error message after a reasonable wait time is a far better user experience than having to wait 2 whole minutes for the TCP timeout. And this way, any error handling logic to unroll stuff etc that gets added here will still be able to run.) - Integrate Cloud SDK into Parasails - Use top-level implementationSniffingTactic and pass through to whelk and MaA - Force homogeneous sniffing tactic for helpers+actions. - Force minimum SVRs (re implementationType: 'classical'). - Finish up support for smarter .bootstrap and .initialize. (Continued from 21bc1f1ab88dfa1064f2e312a4b1135b92763d2a) - Add faux callbacks for easier debugging - Add `create` method to datastore fixture, and test "no validation errors" case. [(Scott Gress)](https://github.com/sgress454) - Fix test descriptions for `update` [(Scott Gress)](https://github.com/sgress454) - Add test cases for validations on primary key [(Scott Gress)](https://github.com/sgress454) - Hoist flag for determining whether to apply validation rules, and use it for both PKs and generic attributes. [(Scott Gress)](https://github.com/sgress454) - Add implementation-sniffing to .stream() - Setup updateOne() method - Set up destroyOne() and archiveOne(), and use an omen in the 'found too many' error in findOne() to improve the stack trace. - Implement implementation sniffing in .transaction() and .leaseConnection(). - Enable ORM to accept attributes named 'length'. Replace `_.each()` with `_.forIn()` to enable ORM to accept attributes named 'length'. [(Daniel Harvey)](https://github.com/danielsharvey) - Add an isError check when handling errors thrown from validation rules to prevent potential issues in extreme edge cases (where something weird gets thrown) - Clean up tests for greater clarity and usefulness - Finish restructuring things to match latest conventions in parley, etc. Leave standalone/ alias for backwards-compatibility - Update docs about other adapter methods - Improve error messaging to assist in debugging - Add E_TOO_MANY_FILES error code. - Add Cloud.on() and Cloud.off(), improve error msgs, and remove idempotency guarantee - Add typeof check to ensure reasonable behavior even with strange input. - Enhance built-in catchall error msg for Cloud.on() - Refactor UMD approach to make it easier to see what's going on at a glance. - Add experimental support for FileList instances (multi-file upload via Cloud SDK). - Improve performance of +500K ops/sec by collapsing IIFE - Setup for being able to use AsyncFunctions with .intercept() and .tolerate(). - Add new method: getInvocationinfo() -- a public API for accessing private info such as _timeout. Add .interruptions to getInvocationInfo(). - Introduce working impl of .retry() - Clone (shallow) metadata on the way in during .retry(), just to avoid any potential edge cases with a fn attaching additional properties and that actually mattering for subsequent invocations with the same arguments. - Take advantage of new getInvocationInfo() method in parley related to .retry() - Implement partial support for blended tolerate/intercept with retry (Note: works only for completely parallel error conditions). - Optimize AsyncFunction function constructor check. - Allow implementationSniffingTactic to be customized. - Add failsafe to ensure app lifts inside of Mocha tests. Related to https://github.com/balderdashy/sails/issues/4395 and https://github.com/balderdashy/captains-log/pull/22 - Also in new Sails apps: Force min 3.10.3 of lodash in new projects - Also in new Sails apps: Update eslintrc templates to tolerate unused args and vars that begin with 'unused' (or are just named 'unused') - Also in new Sails apps: Add 'custom' validation rule. - Also in new Sails apps: Replace autofocus with focus-first, and ensure this feature is opt-in for and . Also add typeof check for bowser. - Also in new Sails apps: Tolerate required: false, etc in - Also in new Sails apps: Improve error msg in weird edge case - Also in new Sails apps: Allow completely numeric passwords by default when using 'isHalfwayDecentPassword' validation rule for - Also in new Sails apps: Update email templates - Also in new Sails apps: Add a couple opinionated-yet-reasonable global styles to the 'bootstrap-overrides' file - Also in new Sails apps: Bump appveyor+travis files to always include Node 10 - Also in new Sails apps: Standardize redis url format to be consistent with site documentation for db (numerical database names only). [Scott Reed](https://github.com/AdJesumPerMariam) - Also in new Sails apps: Ignore files generated by 'npm link' in recent NPM releases - Also in new Sails apps: Add note about conditional requiredness and validation rules. - Also in new Sails apps: Add autocomplete attributes to form fields where applicable - Also in new Sails apps: Disable lesshint rule for border-boxing everything in bootstrap-overrides - Also in new Sails apps: Fix mismatched tags in email templates - Also in new Sails apps: Fix that silly checkbox alignment on login/signup templates - Also in new Sails apps: Fix camelCasing issue in intermediate subdirectories for newly created view actions. - Also in new Sails apps: Fix fullName validation on edit profile page [(S.Slavchev)](https://github.com/s-slavchev) - Also in new Sails apps: Don't include inputs in generated shell script. - Also in new Sails apps: Make 'semi' and 'curly' lint violations burn yellow instead of burning red - Also in new Sails apps: Tweak generated comment about 'no lodash' - Also in new Sails apps: Add note about the function signature of 'leave' - Also in new Sails apps: Simplify readme links - Also in new Sails apps: Tweak inline docs for - Also in new Sails apps: Bring in self-updating timestamp component for web app template - Also in new Sails apps: Update model examples - Also in new Sails apps: Use kebab-cased status to avoid database-dependent behavior in edge cases (b/c case sensitivity) - Also in new Sails apps: Rename var and kebab-case desired effect strings for consistency - Also in new Sails apps: Use _.extend for consistency - Also in new Sails apps: Include moment in eslintrc-override - Also in new Sails apps: Tweak , and then include a demo in the welcome page. - Also in new Sails apps: Normalize indentation in "send template email" helper - Also in new Sails apps: Make sure the HTML email contents are actually being logged properly - Also in new Sails apps: Change method names on welcome page for consistency, and also use `this.modal`, since there can only be one modal on the page at a time anyway - Also in new Sails apps: Rearrange virtual page example - Also in new Sails apps: Bring in unsupported browser overlay - Also in new Sails apps: Change href so it makes more sense - Also in new Sails apps: Expand comment re virtual pages. - Also in new Sails apps: Expand comments in js-timestamp a bit. - Also in new Sails apps: Rearrange note about filtering argins now that handleSubmitting exists. - Also in new Sails apps: Don't specify testMode when calling out to Mailgun, to avoid confusion. - Also in new Sails apps: Never indicate that the email was logged instead of sent if it wasn't. - Also in new Sails apps: Add retries w/ exponentional back-off for idempotent / low-risk 3rd party API calls. - Also in new Sails apps: Add 'ensureAck' flag to sendTemplateEmail(), and change behavior to background the sending by default. - Also in new Sails apps: Simplify rebuild-cloud-sdk script a bit. - Also in new Sails apps: Use for instead of _.each() - Also in new Sails apps: Fix typo introduced in 22df26e832aeef817a6e3c01561041eda83830cd - Also in new Sails apps: Tweak verbiage in boilerplate FAQ for clarity. (action not controller) - Also in new Sails apps: Normalize capitalization in generated footer - Also in new Sails apps: Include a placeholder logo image for the branding in the masthead - Also in new Sails apps: On homepage for web app template, add more space between "feature" sections on small screens - Also in new Sails apps: Use consistent link styles all across the web app template, and also add some special blockquote styles - Also in new Sails apps: Verbiage tweaks / capitalization normalizaish - Also in new Sails apps: 'No matter how legal it is, capitalization should never look this ugly.' -mike, amateur lawyer - Also in new Sails apps: Add FAQ item with info about placeholders, and give the questions some more breathing room - Also in new Sails apps: Cleanup and friendlify boilerplate legal documents - Also in new Sails apps: Provide better example config - Also in new Sails apps: Tweak constant name to match - Also in new Sails apps: Fix inconsistent constants - Also in new Sails apps: Avoid find/replace conflicts with constants. Also, add the link we mention in the FAQ. - Also in new Sails apps: Correct the boilerplate language - Also in new Sails apps: Add clarifications to boilerplate stylesheet - Also in new Sails apps: Remove custom `a:not(.btn)` styles from homepage, because those are set elsewhere now - Parasails: Tweak varname for clarity. - Parasails: Rename private method for clarity. - Parasails: Leave better error sniffability, but rip out attempt to actually use it from 173685357364baaf9e2e2b3c16307c3fa7ec5d44 - Parasails: An additional layer to Cloud.on() and Cloud.off() (intermediate commit to track concept of wildcard key -- ends up being too confusing for custom error handling, but might be worth revisiting as a kind of lifecycle callback in the future) - Parasails: Rename "*" catchall handler to "error" and finish implementation - Parasails: Rename to Cloud.listen() and Cloud.ignore(), add aliases, and improve error msgs. - Parasails: Intermediate commit: Tracks how we'd approach making .listen() idempotent-- but actually this probably isn't the right granularity to do that (save that for parasails proper on a per-Vue-component basis) - Parasails: Change .listen() and .ignore() back to .on() and .off(), and remove idempotency guarantee (that'll need to live at the component/Vue instance level in parasails proper -- see also https://github.com/mikermcneil/parasails/commit/36f87501cd174104e6b75a18b7c16d83ec74 edaa and https://github.com/mikermcneil/parasails/commit/b54258a9f244fabbbd3c89e35c7be5095c5d8dfa) - Parasails: Add back conveniences for multiple bindings. - Parasails: 'error' => '*' - Parasails: Add uncaught error handler - Parasails: Bring in most recent updates - Parasails: Remove double-binding - Parasails: Update parasails.js [(Mark Strelecky)](https://github.com/streleck) - Parasails: Throw error when upload request is no good because it contains structured data as text parameter values. - Parasails: Add tip about uglify - Parasails: Follow-up to 4941181912646ec27132afb82c2be32bb3cf6203 to make $.ajax() transport work with FileList instances (on browsers where this is supported). - Parasails: Fix hard-coded version number in parasails.js file. - Parasails: Allow sparser shorthand for setting up virtual pages, while also implementing more error-checking and edge-case handling for various configurations of virtualPages, virtualPagesRegExp, and html5HistoryMode. - Parasails: Improve 'did you mean?' intelligence and add two FUTURE nice-to-haves. - Force sails-generate@1.15.19 - Fix linting and global var issues for globals tests [(Scott Gress)](https://github.com/sgress454) - Fix paths to Lodash for globals tests [(Scott Gress)](https://github.com/sgress454) - Fix lint and global var issues in www test [(Scott Gress)](https://github.com/sgress454) - Use `tmp` in www tests to attempt to fix Appveyor issues [(Scott Gress)](https://github.com/sgress454) - Address https://github.com/balderdashy/sails/commit/8168dffb8052c0a2df3de923a18c60dd27875915#diff-78ca47cd74fd541e85ff2100564371ddR656 - Use `tmp` in remaining www test to try and address random Appveyor failures [(Scott Gress)](https://github.com/sgress454) - Use more accurate verb in secure cookie info msg - Contextualize the 'yes secure cookies, no trustProxy' debug mesage. - Clarify message and change it to be verbose - Closes #4392 (by removing coffee-script, checksum, and istanbul devDependencies) - Ignore 'npm link' collateral - Stub implementation opts for helpers re upcoming opt-ins - Add note about implementationSniffingTactic - Implicitly set implementationSniffingTactic - Add bookends about 'customize' for whelk and machine-as-action - Don't use explicit implementationSniffingTactic yet. - Bump machine runner SVR and remove unnecessary code - Note optimization - Setup for smarter .bootstrap and .initialize - Correctly apply implementationSniffingTactic for helpers - Default value for implementationSniffingTactic - Add note in 'sails run' - Expand explanations in changelog. - For compatibility, tolerate action identities that contain '$'. - Change variable name to prevent confusion with built-in 'module'. - Follow-up to a22598eb26f108768be91a31f4baa8fc92a9c5c7 which covers the rest of the cases - Change var name for clarity (same idea as in 2eaaf968daae8f00ebc0007210baa32b5d50f7f0) - Log warnings if special actions-only props are in use in helper defs. - Update changelog to reflect what's happening w/ the responses hook - Bump skipper dep - Clarify optional-ness of bootstrap callback in comment. - Update error messages for bootstrap/initialize to reflect new reality. - 2nd follow up to 97082b8e067a490531758f6bcbc471e08874c209 to allow for "$" in route bindings - Use prerelease of sails-generate - Fix sails-generate SVR - Pin merge-dictionaries - Fix test now that async is no longer included as a dep. in newly generated sails apps (thanks to await and sails.helpers.flow via organics) - Fix expectations of test to match correct child process output (follow up to c694f2d2b1db4b9f397c52f540b19d7d4a4125f4) - Fix one more test related to c694f2d2b1db4b9f397c52f540b19d7d4a4125f4 - Waterline: [PATCH] Fix typo and swap unit test description misplaced in fix #1554 to issue #4360 [(Luis Lobo Borobia)](https://github.com/luislobo) - Waterline: Add link to drawing demonstrating how Waterline works underneath the hood - Waterline: Stub out the spots where implementationSniffingTactic needs to apply - Waterline: Improve error msg when attempting to use a too-generic WHERE clause with updateOne/destroyOne/archiveOne - Waterline: Don't use skipEncryption for archiveOne() and destroyOne() -- doesn't make sense. - sails-hook-orm: Add missing comma and link to lifecycle hooks in docs [(Tom Saleeba)](https://github.com/tomsaleeba) - sails-hook-orm: Update error message about adapter compatibility - sails-hook-orm: Stub out the spots where implementationSniffingTactic needs to apply - waterline-utils: Update boilerplate - waterline-utils: Resolve lint issues - waterline-utils: Ignore fun new files generated by 'npm link' - waterline-utils: Add normalize-datastore-config from sails-mongo (needs more love) - waterline-utils: Set 'protocol' property automatically. - waterline-utils: Expose .normalizeDatastoreConfig() - waterline-utils: Change .protocol -> protocolPrefix (and get rid of trailing colon) - waterline-utils: Add missing change from prev. commit, and also set .protocolPrefix when appropriate, even if no url was specified. - waterline-utils: Grab dictionary of models (w/ backwards compatibility) - waterline-utils: Fix backwards conditional - anchor: Fix build status urls - anchor: Bump validator version re https://snyk.io/test/npm/anchor?severity=high&severity=medium&severity=low#npm:validator:20160218 (without applying any of the necessary changes yet, if there are any). Also upgrade to latest eslintrc file, etc, and bump eslint dep. - anchor: Fix out of date test label - anchor: Tweak checkConfig error msgs for isBefore+isAfter - anchor: Clean up checkConfig for isBefore and isAfter, and clarify another comment. - anchor: Reroll isBefore and isAfter the dumb, explicit way. - anchor: Added a few more tests. - anchor: Add validator upgrade - sails-hook-sockets: Fix typo in function being checked when preparing driver in case of unexpected failure [(Luis Lobo Borobia)](https://github.com/luislobo) - sails-hook-sockets: Modify deprecation message to recommend install Sails owned socket.io-redis, to match up documentation https://sailsjs.com/documentation/concepts/deployment/scaling [(Luis Lobo Borobia)](https://github.com/luislobo) - skipper: Update boilerplate. - skipper: Add eslint dev dep - skipper: Set up lint script and add docs for quota-related error codes in README - skipper: Some lint fixes plus remove stringfile and 2 unnecessary deps. - skipper: sailshq/lodash - skipper: Improve linter - skipper: Consolidate roadmap w/ sails core - skipper: Consolidate contributor info - skipper: Remove old logger in favor of consistently using 'debug' - skipper: Documentation - skipper: Conslidate into lib/ (part 1) - skipper: Move index.js to lib/skipper.js - skipper: Remove standalone/ alias - skipper: Latest SVR for skipper-adapter-tests - Backport https://github.com/lodash/lodash/commit/d8e069cc3410082e44eb18fcf8e7f3d08ebe1d4a - Flaverr: Fix bad error message - Parley: Shorten error message and pull out exec countdown secs into a constant. - Parley: Tweak verbiage in warning msg one more time for further clarity. - Parley: Implement .timeout() and initial pass at .retry(). - Parley: Set up more of .retry(), and add relevant TODO about what needs to happen now - Parley: Avoid 1MM ops/sec perf. loss by using .slice() alternative - Parley: 200k ops/sec to general-case performance (and take care of annoying arguments keyword usage) - Parley: Implement IIFE but prepare for about-face since it takes away 1MM ops/sec - Parley: Regain 1MM ops/sec by moving IIFE out - Parley: Cleanup, deduplication, and docs - Parley: +350K ops/sec - Parley: Change where bindUserlandAfterExecLC lives, for consistency - Parley: Rearrange for clarity, and update comments - Parley: Boilerplate update - Parley: Setup first pass at async function support for userland afterExec LCs - Parley: Remove usage assertion now that AsyncFunctions are supported - Parley: Added _hasStartedButNotFinishedAfterExecLC - Parley: Eliminate todos now that "one tick" flag takes afterexec LCs into account - Parley: Rough .retry() impl - Parley: Rip out retry from parley (realized it needs to live a level higher up in the stack) - Parley: Remove two now-unused reserved keys from blacklist - Parley: Update examples - Parley: Add 'thenable' option for backwards-compatibility for older node releases (specifically important for .tolerate()) - Parley: Fix backwards conditional - Parley: Revert "3.6.0" - Parley: Prevent throwing when no afterExec LC handler is specified - Parley: Don't throw a special error when handler provided to .tolerate() throws -- instead just allow it through unscathed. - Machine (runner for actions2, helpers, & shell scripts): Upgrade eslint - Machine: Obey new 'instructionTimeout' meta key - Machine: Reverse 8af993b01c1480aca45d7e7e2952cfd8d288a293 because all metadata isn't known at build time. (This will need more shuffling to accomplish) - Machine: Update boilerplate and bump min parley version. - Machine: Finish "Too many retries" error, and make note of situation with [Circular] - Machine: Disable old tests, remove now-irrelevant future note - Machine: More cleanup, after verifying it works w/ defaultArgins and defaultMeta. - Machine: Add note explaining why we don't pass in a build-time omen (or any omen) to .retry()'s extra .build() call. - Machine: Respect original invocation timeout - Machine: Remove strict idempotency check. - Machine: Eliminate support for variadic usage (because it's potentially confusing/imprecise -- negotation rules and retry series can BOTH be arrays) - Machine: Fix an oversight/edge case: an error message would have itself thrown an error. - Machine: Fix typo in .retry(). - Machine: Setup for implementationSniffingTacticgls - Machine: Catch attempt to use AsyncFunction in sync:true method at build time instead of waiting until exec-time. - Machine: Cursory setup of exits arg detection - Machine: Don't freak out about new customization option - Machine: Validate implementationSniffingTactic - Machine: Eliminate old comments, and one trivial optimization - Machine: Tweak comments and add clarification - Machine: Handle implementationType: 'classical' for 'exits'-less functions. - Machine: Skip 'await' tests for Node versions that dont support it. - Machine: Avoid returning from .catch() (this is not tied to any known issue, just cleanup) - Machine: Fix to properly simulate chaining: promise = promise.catch() - Machine: Swap around 71eb66400f73a52baf0a78a38c1b8d44061737ac so that .then() gets handled first. Otherwise, you get either an unhandled promise rejection OR a called-it-twice warning. - whelk (shell scripts): Add note about customizations - whelk (shell scripts): Add support for new `customize` option - whelk (shell scripts): Ensure at least machine@15.2.2 - Defined a req.path for socket requests. - Ensured req.path is good and stringy. #### Documentation: - Add documentation for new model methods in Sails 1.1.0. - Sails 1.1.0 updates for .transaction() and .leaseConnection() -- also some follow up from b126f7b66a1c46be0208c2a81235e0584cc50225 for .stream() - Update examples for .stream() in advance of sails 1.1.0 - Update Blueprint API docs for clarity - Update Platzi course links - Update adapterList.md [(Ronny Medina)](https://github.com/ronnymedinave) - Update policies.md [(Alex Schwarz)](https://github.com/alexschwarz89) - Update waterline.md - Update dependencies.md - Normalize capittalization - Update upgrading.md - Update routes.md - Add link and clarify a bit further - Update res.status.md - Updated link [(Simon)](https://github.com/svict4) - Fix event documentation for `lower` [(Xavier Spriet)](https://github.com/loginx) - Add note to upgrading guide re: i18n (see https://gitter.im/balderdashy/sails?at=5ac46d9cc574b1aa3e6533f6) - Fix commented-out code block - Fix getLocale link - Update locales.md - Update req.setLocale.md - Fix broken links in old 0.12 upgrading guide - Fix RESTful route examples for `add` and `remove` - Update content for req.acceptsLanguages() - Update content for req.acceptsCharsets(), and rename both files - Clean up req.accepts() reference page. - Update upgrading guide - Update logging.md - Fixed typo in routing actions description [(Scott Reed)](https://github.com/AdJesumPerMariam) - Fix action name in docs [(Ian Harris)](https://github.com/harrisi) - Update views.md - Fixed broken links to req.wantsJSON & req.allParams() [(Vladyslav Piskunov)](https://github.com/vpiskunov) - Update Lifecyclecallbacks.md - Update cors.md link from cors.js to security.js [(Michael Frederick)](https://github.com/mdfrederick) - Update cors.md - Adjusting links and updating examples [(Michael Frederick)](https://github.com/mdfrederick) - Remove link to guide that doesn't exist anymore (fixes #1000) - Update faq.md - Fix some links, and create a page in tutorials linking to the course/demo app - Create req.hostname [(Mark Strelecky)](https://github.com/streleck) - Update req.host.md and change to deprecated [(Mark Strelecky)](https://github.com/streleck) - Update req.isSocket.md [(Mark Strelecky)](https://github.com/streleck) - Update req.wantsJSON.md [(Mark Strelecky)](https://github.com/streleck) - Update req.hostname - Fix docmeta tag - Update req.isSocket.md - Update req.wantsJSON.md - Update res.send.md [(Mark Strelecky)](https://github.com/streleck) - Add res headers to examples - Add fragments to cors links. Update another link to use /documentation [(Freddy)](https://github.com/mdfrederick) - Update low-level-mysql-access.md - Update mongo.md - Update cors.md since there's no config/cors.js in 1.0 and the settings are in config/security.js [(pavan)](https://github.com/pavanmehta91) - Fix broken link [(Scott Reed)](https://github.com/AdJesumPerMariam) - Add the ) and } forgotten, to close the queries in the right way. [(Mario Colque)](https://github.com/colkito) - Add hooks + a few tweaks - Update hooks.md - Update services.md - Update sendNativeQuery.md - Update findOrCreate.md - Clarify sentence, and add link - Add file extension - Update req.host.md - Update ExampleHelper.md [(Okoli Lemuel)](https://github.com/okolilemuel) - Update standalone-usage.md to 1.0 [(ultimate-tester)](https://github.com/ultimate-tester) - Correct syntax typo for log messages [(Alex Schwarz)](https://github.com/alexschwarz89) - Update Validations.md [(Okoli Lemuel)](https://github.com/okolilemuel) - Take first pass at e-commerce page - Remove irrelevant example - Add note re: customToJSON does not support async functions [(Scott)](https://github.com/snidell) - Add sails-hook-organics to the Hooks page [(Pika)](https://github.com/ThatNerdyPikachu) - Fix missing comma [(Scott Reed)](https://github.com/AdJesumPerMariam) - Waterline initialize function must be async [(Scott Reed)](https://github.com/AdJesumPerMariam) - Update sails.sockets.md [(AYEDOUN Fiacre)](https://github.com/afidosstar) - Fix custom model methods example [(Tim Wisniewski)](https://github.com/timwis) - Add quotes to key in headers example re: dictionary keys [(Tom Saleeba)](https://github.com/tomsaleeba) - Fix typo in ActionsAndControllers.md [(s-slavchev)](https://github.com/s-slavchev) - Fix typo in URL link [(Daniel Harvey)](https://github.com/danielsharvey) - Update GeneratingActions.md to fix typo manging => managing [(Julien Le Coupanec)](https://github.com/LeCoupa) - Update extending-sails.md to remove remove broken link [(Julien Le Coupanec)](https://github.com/LeCoupa) - Update Testing.md to add dd Semaphore [(Julien Le Coupanec)](https://github.com/LeCoupa) - Update To1.0.md [(floriancummings)](https://github.com/floriancummings) - Add information about optional param. [(Bernardo Gomes)](https://github.com/Bernardoow) - Update sort.md to fix code example typo [(Eli Peters)](https://github.com/elipeters) - Update standalone-usage.md - Add back and correct link - Update Models.md - Update To1.0.md - Edit Sails versioning for consistency (v1.0 and JavaScript) [(Dennis Cheung)](https://github.com/oaksofmamre) - Update globals.md to fix broken link for Services and Models [(Minh Tri Nguyen)](https://github.com/kevinvn1709) - Update res.redirect.md [(Julien Le Coupanec)](https://github.com/LeCoupa) - Update res.status.md [(Julien Le Coupanec)](https://github.com/LeCoupa) - Add link to info about negotiating errrors - Update errors.md - Simplify examples. - Fix typo in .update() usage example. - Update sails.config.bootstrap.md - Update decrypt.md - Update attributes.md #### Organics: - Improve security - Add note for future about new exit from .saveBillingInfo() - Update package.json [(Pika)](https://github.com/ThatNerdyPikachu) - Use `encodeURIComponent` instead of `encoreURI` for `url-friendly` style. - Replace all special characters and skip encoding entirely. [(Scott Gress)](https://github.com/sgress454) - Swap open with opn to remove critical error on audit ([Peter Barrett)](https://github.com/Peter-Barrett) - Pin opn version number and move back to json5 [(Peter Barrett)](https://github.com/Peter-Barrett) - Remove unused dep #### Adapters: - Check `exits` against raw instead of built machine as it seems as though the built machine doesn’t expose the exits [(Yuki von Kanel)](https://github.com/Rua-Yuki) - Update debug to fix ReDoS vulnerability [(Alec Fenichel)](https://github.com/fenichelar) - Remove auto setting JSON to LONGTEXT [(Betanu701)](https://github.com/Betanu701) - sails-disk: Update eslint - sails-disk: Don't test node 0.10 and 0.12 because eslint doesn't even run on them anymore - MySQL: Pin debug version [(Alec Fenichel)](https://github.com/fenichelar) - MySQL: machine@15 adjustment - MySQL: Follow up to d41eb10e1804ccc884cf6e290fd5a340f8f12c0b - MySQL: Test on Node 10 - MySQL: eslint for tests - MySQL: Use the right port - MySQL: Get rid of ajv keywords dev install warning and clean up machine SVR - MySQL: Update skipped test - MySQL: Rename test files for easier quick-switching and add back linting for tests - MySQL: Move the files (still need to update require paths-- see next commit) - MySQL: Fix require paths (follow up to d9db8cda4fcd9ffb37b738402d855991991c892b) - MySQL: Update travis.yml and remove Dockerfile - MySQL: Normalize the license stuff - MySQL: Fix those badge things - PostgreSQL: Correctly raises error when you have bad model attributes [(Tony Buser)](https://github.com/tbuser) - PostgreSQL: Allow for other auto increment scenarios other than numeric [(Andrew Greenstreet)](https://github.com/gstreetmedia) - PostgreSQL: Single quotes (eslint) and a couple of other minor adjustments to comments and varnames - PostgreSQL: Improve linter - PostgreSQL: Change it's => its - PostgreSQL: Fix failing test (https://travis-ci.org/balderdashy/sails-postgresql/jobs/368925613). This is because https://github.com/balderdashy/sails-postgresql/pull/278 actually breaks normal usage (because logical types are not made accessible to the adapter in the per-table DDL spec passed into the define() adapter method- instead, another approach must be used) - PostgreSQL: Update node versions tested to include all LTS releases - PostgreSQL: Improve error message - PostgreSQL: Adjust tests to stop using 'integer' columnType for auto-incrementing pk - PostgreSQL: Close https://github.com/balderdashy/sails-postgresql/pull/279 - PostgreSQL: Include additional number type property to model. This will cause the registerDatastore method to throw an error due to autoMigrations being undefined for anotherGood property on the model. [(Andrew Salib)](https://github.com/andezzat) - PostgreSQL: Check that property autoMigrations exists before checking its child property columnType resolving undefined errors [(Andrew Salib)](https://github.com/andezzat) - MongoDB: Upgrade 'machine' dependency to ^15.0.0 [(Yuki von Kanel)](https://github.com/Rua-Yuki) - MongoDB: Use `.switch(...)` where needed [(Yuki von Kanel)](https://github.com/Rua-Yuki) - MongoDB: Allow tests to run on PRs (hopefully) - see https://docs.travis-ci.com/user/environment-variables/ - MongoDB: Bump devdep SVRs #### Tools: - sails-hook-dev: Recommend simple solution to setting up staging environment in readme.md #### Raw diffs: ##### Framework: - https://github.com/balderdashy/sails-generate/compare/master@%7B2018-03-29%7D...master - https://github.com/balderdashy/sails/compare/master@%7B2018-03-29%7D...master - https://github.com/balderdashy/waterline/compare/master@%7B2018-03-29%7D...master - https://github.com/balderdashy/sails-hook-orm/compare/master@%7B2018-03-29%7D...master - https://github.com/sailshq/waterline-utils/compare/master@%7B2018-03-29%7D...master - https://github.com/balderdashy/waterline-schema/compare/master@%7B2018-03-29%7D...master - https://github.com/sailshq/anchor/compare/master@%7B2018-03-29%7D...master - https://github.com/balderdashy/sails-hook-sockets/compare/master@%7B2018-03-29%7D...master - https://github.com/balderdashy/sails.io.js/compare/master@%7B2018-03-29%7D...master - https://github.com/mikermcneil/socket.io-redis/compare/master@%7B2018-03-29%7D...master - https://github.com/balderdashy/skipper/compare/master@%7B2018-03-29%7D...master - https://github.com/rachaelshaw/sails-hook-uploads/compare/master@%7B2018-03-29%7D...master - https://github.com/sailshq/sails-hook-grunt/compare/master@%7B2018-03-29%7D...master - https://github.com/sailshq/lodash/compare/master@%7B2018-03-29%7D...master - https://github.com/mikermcneil/connect-redis/compare/master@%7B2018-03-29%7D...master - https://github.com/mikermcneil/flaverr/compare/master@%7B2018-03-29%7D...master - https://github.com/mikermcneil/parley/compare/master@%7B2018-03-29%7D...master - https://github.com/node-machine/machine/compare/master@%7B2018-03-29%7D...master - https://github.com/node-machine/rttc/compare/master@%7B2018-03-29%7D...master - https://github.com/sailshq/whelk/compare/master@%7B2018-03-29%7D...master - https://github.com/sailshq/whelk/compare/master@%7B2018-03-29%7D...master - https://github.com/balderdashy/captains-log/compare/master@%7B2018-03-29%7D...master - https://github.com/mikermcneil/reportback/compare/master@%7B2018-03-29%7D...master - https://github.com/balderdashy/include-all/compare/master@%7B2018-03-29%7D...master - https://github.com/sailshq/machinepack-redis/compare/master@%7B2018-03-29%7D...master - https://github.com/sailshq/sort-route-addresses/compare/master@%7B2018-03-29%7D...master - https://github.com/balderdashy/sails-stringfile/compare/master@%7B2018-03-29%7D...master ##### Documentation: - https://github.com/balderdashy/sails-docs/compare/master@%7B2018-03-29%7D...master ##### Organics: - https://github.com/mikermcneil/parasails/compare/master@%7B2018-03-29%7D...master - https://github.com/sailshq/sails-hook-organics/compare/master@%7B2018-03-29%7D...master - https://github.com/mikermcneil/machinepack-http/compare/master@%7B2018-03-29%7D...master - https://github.com/sailshq/machinepack-strings/compare/master@%7B2018-03-29%7D...master - https://github.com/mikermcneil/machinepack-fs/compare/master@%7B2018-03-29%7D...master - https://github.com/sailshq/machinepack-process/compare/master@%7B2018-03-29%7D...master ##### Adapters: - https://github.com/balderdashy/sails-disk/compare/master@%7B2018-03-29%7D...master - https://github.com/balderdashy/sails-mysql/compare/master@%7B2018-03-29%7D...master - https://github.com/sailshq/machinepack-mysql/compare/master@%7B2018-03-29%7D...master - https://github.com/sailshq/waterline-sql-builder/compare/master@%7B2018-03-29%7D...master - https://github.com/balderdashy/sails-postgresql/compare/master@%7B2018-03-29%7D...master - https://github.com/sailshq/machinepack-postgresql/compare/master@%7B2018-03-29%7D...master - https://github.com/balderdashy/sails-mongo/compare/master@%7B2018-03-29%7D...master - https://github.com/balderdashy/sails-redis/compare/master@%7B2018-03-29%7D...master - https://github.com/balderdashy/skipper-disk/compare/master@%7B2018-03-29%7D...master - https://github.com/balderdashy/skipper-s3/compare/master@%7B2018-03-29%7D...master ##### Tools: - https://github.com/mikermcneil/sails-hook-apianalytics/compare/master@%7B2018-03-29%7D...master - https://github.com/balderdashy/sails-hook-dev/compare/master@%7B2018-03-29%7D...master - https://github.com/mikermcneil/eslint/compare/master@%7B2018-03-29%7D...master - https://github.com/mikermcneil/htmlhint/compare/master@%7B2018-03-29%7D...master - https://github.com/mikermcneil/lesshint/compare/master@%7B2018-03-29%7D...master ## 1.0.0 Sails v1.0 comes with a host of new features and improvements as well as some breaking changes to previous versions. Please see the [migration guide](http://sailsjs.com/upgrading) if you're upgrading from a previous version of Sails! * [ENHANCEMENT] Introduce Actions 2 -- ability to declare actions as [Machines](http://node-machine.org) in individual files. See the [Actions and controllers](http://sailsjs.com/documentation/concepts/actions-and-controllers) and [Action target syntax](http://sailsjs.com/documentation/concepts/routes/custom-routes#?action-target-syntax) docs for more info. * [ENHANCEMENT] Introduce Helpers -- the successor to services. See the [Helpers](http://sailsjs.com/documentation/concepts/helpers) docs for more info. * [BUGFIX] Improve path resolution in moduleloader for Windows [f13bb77](https://github.com/balderdashy/sails/commit/f13bb77eb49b9b61aa225c43cc2aacaadd4f07be) * [BUGFIX] Fix output for virtual requests that have non 2xx/3xx status codes and no body [525c7c5](https://github.com/balderdashy/sails/commit/525c7c503347ef586ebf26730af77e2aa8626060) * [REMOVAL] Remove support for (undocumented) "action/model" route syntax for binding routes to blueprint actions * [BUGFIX] Fix issue causing Sails to sometimes crash when using Redis sessions if it receives a request after the Redis server has unexpectedly disconnected [3f29dce](https://github.com/balderdashy/sails/commit/3f29dce22b5a36403108d8d1aab7a903aa4488a5) * [ENHANCEMENT] Add `sails.getActions()` method to return a dictionary of registered Sails actions [5598179](https://github.com/balderdashy/sails/commit/55981792ea52febfa5c343bbae4517c6a38f20c9) * [ENHANCEMENT] Add `sails.registerAction()` method to programmatically register a Sails action [dd9af88](https://github.com/balderdashy/sails/commit/dd9af88b114db696dde2fbeee79a1dc745f2d748) * [ENHANCEMENT] Add `exposeLocalsToBrowser` local in all views * [ENHANCEMENT] Add `sails.registerActionMiddleware()` to programmatically register middleware to be applied to one or more actions [2c281d5](https://github.com/balderdashy/sails/commit/2c281d51301e684f00c91da5cc9d8fa37814094c) * [ENHANCEMENT] Allow explicitly defined actions in Sails config via `sails.config.controllers.moduleDefinitions` [3b264fa](https://github.com/balderdashy/sails/commit/3b264facc2e0a72bd2aa5366271c588c903e6f5c), [4ad23dd](https://github.com/balderdashy/sails/commit/4ad23dd353ff1d7e607c0a75377a0ba6d9dd2f3d) * [BUGFIX] Default `req.options.populate` to the value of `sails.config.blueprints.populate [d8f4df8](https://github.com/balderdashy/sails/commit/d8f4df8e19cfb7eaefa00bbcbbc6cab870483194) * [ENHANCEMENT] Add `sails.reloadActions()` method to reload actions from disk / config while an app is running [df2ee46](https://github.com/balderdashy/sails/commit/df2ee4657774a092fb0bd25b3013448c9f155700) * [ENHANCEMENT] Allow loaded modules (actions, controllers, policies, models, etc.) to have _any_ file extension besides `.md` and `.txt` (which are ignored). Direct support for anything besides plain Javascript has been removed; variants like CoffeeScript and TypeScript can be used by registering them in the `app.js` file. See the [Using CoffeeScript tutorial](http://sailsjs.com/documentation/tutorials/using-coffee-script) for an example. * [REMOVAL] Remove support for custom blueprints. These can easily be replaced by regular actions. See the [migration guide](http://sailsjs.com/upgrading#custom-blueprints) for more info. [0fd4362](https://github.com/balderdashy/sails/commit/0fd4362795ee338ea5b18c3ee42dbdae4b08fd43) * [REMOVAL] Remove support for blueprint route target syntax [0fd4362](https://github.com/balderdashy/sails/commit/0fd4362795ee338ea5b18c3ee42dbdae4b08fd43) * [REMOVAL] Remove deprecated `dontFlattenConfig` option [56c9b5b](https://github.com/balderdashy/sails/commit/56c9b5bc4b4f876778653a3995981516bb0767a7) * [ENHANCEMENT] Humanize config vars that are loaded from the environment [7eb6af6](https://github.com/balderdashy/sails/commit/7eb6af659b67eb856719c8c0d08c7e7043c9e3e8) * [REMOVAL] Remove most Resourceful PubSub methods, leaving just `.subscribe()`, `.unsubscribe()` and `.publish()`. [c981c6e](https://github.com/balderdashy/sails/commit/c981c6edf8573fea0d3a13451d51292ba9038b3b) * [ENHANCEMENT] Expose Sails generator for programmatic use [e008a6b](https://github.com/balderdashy/sails/commit/e008a6b7e76cf0c272ec556f0ab48b67499269f5) * [UPGRADE] Upgrade to Express 4.14.0. Thanks to [josebaseba](https://github.com/josebaseba) for his help in the transition to Express 4! * [ENHANCEMENT] Improved CORS support. See the [migration guide](http://sailsjs.com/upgrading#security) and [CORS docs](http://sailsjs.com/documentation/concepts/security/cors) for more info. * [REMOVAL] Removed `req.validate()` functionality. Use Actions 2 instead, which automatically validate parameters. [68fa8ff](https://github.com/balderdashy/sails/commit/68fa8ff0327530973237dfd602b3155e0386cad5) * [ENHANCEMENT] Updated syntax for policies. See the [migration guide](http://sailsjs.com/upgrading#policies) and the [policies documentation](http://sailsjs.com/documentation/concepts/policies) for more details. [66d2b2d](https://github.com/balderdashy/sails/commit/66d2b2d8d9d7f23b35cad7693eab79a58e888d77) * [ENHANCEMENT] Automatically sort routes to avoid wildcards swallowing static routes. See the [sort-route-addresses](https://github.com/treelinehq/sort-route-addresses) module for more info on route sorting. * [ENHANCEMENT] Combine CORS and CSRF hooks into new "Security" hook. See the [migration guide](http://sailsjs.com/upgrading#security) for more details. * [ENHANCEMENT] Update CSRF configuration and replace `/csrfToken` route with an action that can be bound to any route. See the [migration guide](http://sailsjs.com/upgrading#security) and [CSRF docs](http://sailsjs.com/documentation/concepts/security/csrf) for more info. [7328c05](https://github.com/balderdashy/sails/commit/7328c0527de4d6ae7bfd48f0c6959ff90c7e3d30) * [REMOVAL] `res.created()` will no longer be included by default. [7988866](https://github.com/balderdashy/sails/commit/7988866f68f3cb9926d750265acf1fa9e1cb25c6) * [BUGFIX] Ignore default installed hooks when searching `node_modules` for hooks. [#3855](https://github.com/balderdashy/sails/issues/3855). * [SECURITY] Don't serve CSRF token via websockets [50b0684](https://github.com/balderdashy/sails/commit/50b06845a0075f4a45f5e2b5d66c05ea3ed62c4b) * [INTERNAL] Bring EJS layout code into Sails rather than relying on the unmaintained `ejs-locals` package [d3ba9bd](https://github.com/balderdashy/sails/commit/d3ba9bdd3c7301b6ac438d2b4591c74c40e2b06c) * [REMOVAL] Remove handlebars dependency and support for layouts in view engines other than EJS. See the [migration guide](http://sailsjs.com/upgrading#layouts) for more details. [ae7e656](https://github.com/balderdashy/sails/commit/ae7e656cf815480d135a8d124db6c3269bad1b92), [9f1f2fb](https://github.com/balderdashy/sails/commit/9f1f2fb91b69a8f6a6e29e9a171a981e7a109f51) * [ENHANCEMENT] Don't set NODE_ENV based on Sails environment, except in special circumstances. See the [migration guide](http://sailsjs.com/upgrading#node-env) for more details. [cf20d07](https://github.com/balderdashy/sails/commit/cf20d070700230f73b158bd37fb7109739f16519), [abbf1f7](https://github.com/balderdashy/sails/commit/abbf1f724f4b45176d9c8cd46db1a94bef7c37f9) * [ENHANCEMENT] Make session hook `routesDisabled` use Sails route address syntax (including regular expression syntax) [aba8f2f](https://github.com/balderdashy/sails/commit/aba8f2fe9856e43168b423d813e17f1b3067746a) * [BUGFIX] Supply `res.locals._csrf` to _all_ routes when CSRF protection is enabled. Fixes [#3865](https://github.com/balderdashy/sails/issues/3865) * [REMOVAL] Remove Consolidate dependency -- view template engines are now configurable via the `sails.config.views.getRenderFn` setting. See the [migration guide](http://sailsjs.com/upgrading#views) for more details. [6316452](https://github.com/balderdashy/sails/commit/63164527b2e0b7c268488539aa3a0e011ff332f9) * [REMOVAL] Remove deprecated RPS "firehose" [86b88](https://github.com/balderdashy/sails/commit/86b8884f15b647ebc963cadff8502f2449ce04a2) * [REMOVAL] Remove deprecated 0.9.x socket support [6464d8f](https://github.com/balderdashy/sails/commit/6464d8f89bd74aaaed380cee15143e6ad0aad614) * [BUGFIX] Update and standardize precedence of route param, query and body in `req.param()` and `req.allParams()` [820d1eb](https://github.com/balderdashy/sails/commit/820d1eb53e12774cd644f516d5952c8da8f02da8) * [REMOVAL] Remove JSONP support from blueprints. CORS is fairly ubiquitous now. [effd6c3](https://github.com/balderdashy/sails/commit/effd6c315f2ef9fe55cf5dafdf65b59b8c5cbe1e) * [REMOVAL] Remove deprecated `sails.getBaseUrl` method. [d0fe4ff](https://github.com/balderdashy/sails/commit/d0fe4ff169ae9e01a799d4fd32da45700daba21e) * [INTERNAL] Remove `sails-util` [1fee468](https://github.com/balderdashy/sails/commit/1fee468bacf98b847d2455d3c40f92bae8b33233) * [INTERNAL] Remove grunt, sockets and ORM hooks [48750d7](https://github.com/balderdashy/sails/commit/48750d7010218bcb224623c532474430063dbdb3), [07c59ce](https://github.com/balderdashy/sails/commit/07c59ceaccf3e5a7258962b01f9eaae03fd888d9) * [REMOVAL] Remove deprecated `req.params.all()` method [9c6b217](https://github.com/balderdashy/sails/commit/9c6b21773b07ab816fea28f67d3e614e5eb2210b) * [BUGFIX] Correctly load custom adapters (stored in either `FooAdapter.js` files or in subfolders) [#3884](https://github.com/balderdashy/sails/issues/3884) * [INTERNAL] Add benchmarks [6b4ba32](https://github.com/balderdashy/sails/commit/6b4ba32d1d5219976be81fe7a5bf57a38635d681) * [ENHANCEMENT] Requests for assets will skip running session middleware by default [3c5ddf7](https://github.com/balderdashy/sails/commit/3c5ddf719e78acbe66d0896b9d0d013d9e02279b) * [ENHANCEMENT] Support `routesDisabled` for sessions in virtual requests [a00cf78](https://github.com/balderdashy/sails/commit/a00cf78895836f3ab982fab00c52fdcb668c4eef) * [INTERNAL] Standardize log level for warnings to always use "debug" except for warning related to security. * [ENHANCEMENT] Add 404, 500 and `startRequestTimer` to middleware order automatically (and remove them from default sails.config.http.middleware.order) [a22e4e7](https://github.com/balderdashy/sails/commit/a22e4e730f42e9868f604bc62eaf9dcc4d2abd6b), [f788e39](https://github.com/balderdashy/sails/commit/f788e3956cf2daf521dbe36fdb8d64a2a8dbb5c8) * [REMOVAL] Remove the `handleBodyParserError` middleware from the stack, use the `onBodyParserError` option in Skipper instead. [f788e39](https://github.com/balderdashy/sails/commit/f788e3956cf2daf521dbe36fdb8d64a2a8dbb5c8) * [BUGFIX] Take locale into account in views.render() [#3833](https://github.com/balderdashy/sails/issues/3833) * [ENHANCEMENT] Allow session adapter to be required directly [039d245](https://github.com/balderdashy/sails/commit/039d24580f55c4e03f187de87339f88dd31afbc6) * [REMOVAL] Remove support for Express 3 session adapters [628a55b](https://github.com/balderdashy/sails/commit/628a55b687ed95adc7b6f7667023ddb96bd939fd) * [INTERNAL] Switch from i18n to i18n-2. See the [migration guide](http://sailsjs.com/upgrading#i18n) for more details. [4a90de2](https://github.com/balderdashy/sails/commit/4a90de293883545fb0418851826fa84c5331dad9) * [BUGFIX] Fail to lift Sails if `connect-redis` can't initialize. [#3590](https://github.com/balderdashy/sails/issues/3590) * [REMOVAL] Remove `method-override` from default middleware list. [575c4a3](https://github.com/balderdashy/sails/commit/575c4a37613992af92a3e40338fe5b470ec2531b) * [BUGFIX] Use _.clone() instead of _.cloneDeep() for config overrides, to preserve things like Redis clients passed into config. [1b970b6](https://github.com/balderdashy/sails/commit/1b970b6c445659a731967550ec6132c582613bea) * [ENHANCEMENT] Update `sails.config.globals` functionality. See the [migration guide](http://sailsjs.com/upgrading#globals) for more details. [44c6d9b](https://github.com/balderdashy/sails/commit/44c6d9b41e1b1ffd38150788085890c8c2cbe286) * [ENHANCEMENT] Add `sails.config.session.onDisconnect` and `sails.config.session.onReconnect` functions [1e55c63](https://github.com/balderdashy/sails/commit/1e55c639b617e13199db7ee4c3a17f62db87b8ea) * [ENHANCEMENT] Add `sails.config.sockets.adapterOptions.onDisconnect` and `sails.config.sockets.adapterOptions.onReconnect` functions [3a25971](https://github.com/balderdashy/sails-hook-sockets/commit/3a2597155eefa086bba54b70b1f3274aaa97a1f5) * [DEPRECATION] Deprecate `.jsonx()` [cdcc3c0](https://github.com/balderdashy/sails/commit/cdcc3c09ad864329474ae50b5888571cd6dc2654) * [INTERNAL] Update default responses [510504e](https://github.com/balderdashy/sails/commit/510504e210047a2ae8aced98a3a17ced5b499140) * [REMOVAL] Remove update-via-POST blueprint route [5c3814d](https://github.com/balderdashy/sails/commit/5c3814d0162535d8f5b9e907ed9a25aa263c2eff) * [INTERNAL] Disconnect Redis session client when Sails is lowering [80fb71b](https://github.com/balderdashy/sails/commit/80fb71b45fa7dab94f4c6054b6d365a9319150f7) * [UPGRADE] Upgrade EJS dependency to v2.5.3 [6bfad70](https://github.com/balderdashy/sails/commit/6bfad700445038533486affb1637b52b73ed2eac) * [REMOVAL] Removed deprecated `connect-flash` middleware [c5c4900](https://github.com/balderdashy/sails/commit/c5c49007f9d55ed74b3ca3062b465c470a15e82b) * [REMOVAL] Removed 16 unused dependencies (see full list [here](https://gist.github.com/sgress454/4a4930fec3520b24bdf0df552f70a45c)) > Also see the [Waterline changelog](https://github.com/balderdashy/waterline/blob/a7cad817e831af5367be2d2c89c021d12a674b86/CHANGELOG.md#edge). ## 0.12.11 * [BUGFIX] fix typo in error message (see https://github.com/balderdashy/sails/pull/3902) Thanks [Johnny](https://github.com/Hiro-Nakamura) and [@appdevdesigns](https://github.com/appdevdesigns)! * [BUGFIX] backport fix for `_.isFunction()` from Lodash 4 (see https://github.com/lodash/lodash/issues/2768 and https://github.com/balderdashy/sails/issues/3863) Thanks [@adnan-kamili](https://github.com/adnan-kamili) and [@jdalton](https://github.com/jdalton)! * [INTERNAL] rebase changelog updates from master [223567c](https://github.com/balderdashy/sails/commit/223567cf986dd62317d958cb29a6683a4cf1e140) ## 0.12.10 * [BUGFIX] Fix issue where incorrect file size was computed for incoming (multi-)file uploads on skipper-disk and skipper-s3 [#3847](https://github.com/balderdashy/sails/issues/3847) (thanks [@crobinson42](https://github.com/crobinson42), [@NAlexandrov](https://github.com/NAlexandrov) and [@vbogdanov](https://github.com/vbogdanov)!) See also https://github.com/balderdashy/skipper/issues/109. * [BUGFIX] Internationalization fix ([#3833](https://github.com/balderdashy/sails/issues/3833) fixes [#3889](https://github.com/balderdashy/sails/pull/3889) (thanks [@josebaseba](https://github.com/Josebaseba)!) * [INTERNAL] Added automated request latency benchmarks -- primarily in advance of 1.0 for the purpose of comparison (thanks [@sgress454](http://github.com/sgress454)!) * [BUGFIX] Fixed issue with defined-in-app adapters being improperly loaded [#3884](https://github.com/balderdashy/sails/issues/3884) (thanks [@richdunajewski](https://github.com/richdunajewski)!) ## 0.12.9 * [INTERNAL] Fix deprecation warning from express-session [3872](https://github.com/balderdashy/sails/issues/3872) (thanks [@Boycce](https://github.com/Boycce) and [@dougwilson](https://github.com/dougwilson)!) ## 0.12.8 * [BUGFIX] Fix issue with multiple config files that have the same filename [3850](https://github.com/balderdashy/sails/issues/3850) * [ENHANCEMENT] Add criteria validation for the `find` and `findOne` blueprint actions. [ab9c2c3...c6e8ad0](https://github.com/balderdashy/sails/compare/ab9c2c3431a5298e8fd140e5c1e2ed2c7260526c...c6e8ad0940e1034222b958b97e8f28287fae32b6) * [INTERNAL] Fix Gitter link in README so it displays properly on NPM [284c660](https://github.com/balderdashy/sails/commit/284c66008632d906524b2238447a0b79715855e7) ## 0.12.7 * [BUGFIX] Fix issue with multiple config files that have the same filename [3846](https://github.com/balderdashy/sails/issues/3846) * [ENHANCEMENT] Warn about overly permissive CORS settings when lifting in production [ca43e05](https://github.com/balderdashy/sails/commit/ca43e0507af79f15361789a3489013b01c8e1825) ## 0.12.6 * [BUGFIX] Revert inadvertent breaking change to CORS config in 0.12.5, see [f80252f](https://github.com/balderdashy/sails/commit/f80252f66edc0bf00cf6ed317d9a3e68b4e8d948) for details) ## 0.12.5 * [INTERNAL] Upgrade version of `include-all` to ^1.0.0 [f6e8d32](https://github.com/balderdashy/sails/commit/f6e8d3243d7d695983a3816e6cf7c43ca4237948) * [ENHANCEMENT] Add experimental `sails console --dontLift` option [029fe06](https://github.com/balderdashy/sails/commit/029fe0683ea4f01a962b91381b948136f5c18f63) * [UPGRADE] Dependencies in captains-log * [UPGRADE] Moduleloader now uses include-all@1, and sails-build-dictionary is deprecated (all of its methods were folded into include-all) * [BUGFIX] In moduleloader: Improve path resolution on windows * [BUGFIX] Fix property name for ('status' => 'statusCode') in virtual request header ## 0.12.4 * [INTERNAL] Upgrade Mocha to 3.0.0 to remove more of the deprecation notices when installing dependencies (see [mocha:#2200](https://github.com/mochajs/mocha/issues/2200)) * [INTERNAL] Simplify config-merging code in captains-log. This also gets rid of more deprecation notices during install (see [captains-log:49f433eff348c05115a2caf292b4da0db9499887](https://github.com/balderdashy/captains-log/commit/49f433eff348c05115a2caf292b4da0db9499887)) * [BUGFIX] Fix long-standing, low-priority (but super annoying) issue with logged dictionaries/arrays getting extra quote marks (due to a pecularity in the usage of util.format()) [d67e9c8e6775](https://github.com/balderdashy/captains-log/commit/d67e9c8e67759e8dda3a2d664c3607e9127d209c) * [ENHANCEMENT] Add `sails.config.session.routesDisabled` config option to specify routes that should not use session middleware [c712acf](https://github.com/balderdashy/sails/commit/c712acf29de257d438b422b2c47e67a4d5126ddc) * [ENHANCEMENT] Use `res.forbidden()` when denying access to a route via a policy. Thanks [@wulfsolter](https://github.com/wulfsolter)! [3764](https://github.com/balderdashy/sails/pull/3764) * [ENHANCEMENT] Allow use of Express style path and RegExp in `sails.config.csrf.routesDisabled`. Thanks [@bolasblack](https://github.com/bolasblack)! * [BUGFIX] Fix for query / body params called `length` [3738](https://github.com/balderdashy/sails/issues/3738) * [BUGFIX] Fix view rendering when i18n hook is disabled. Thanks [@mordred](https://github.com/Mordred)! [3741](https://github.com/balderdashy/sails/pull/3741) * [BUGFIX] Allow `sails.renderView` to work with globals turned off [3753](https://github.com/balderdashy/sails/issues/3753) [d3f634c](https://github.com/balderdashy/sails/commit/d3f634c9ac0c5e2172710fe27ab3f61f8303d840) * [BUGFIX] Fix typo which could cause crashing when attempting to serialize non-json-compatible output in the response to a socket request. * [UPGRADE] Update all Grunt dependencies [5f6be05](https://github.com/balderdashy/sails/commit/5f6be059823aeb235ef3b4cf53a8d40a341c5873) * [UPGRADE] Update "connect" dependency to 3.4.1 [1d3c9e6](https://github.com/balderdashy/sails/commit/1d3c9e6459253261e0f763d133c559641bcbfa33) * [UPGRADE] Update "compression" dependency to version 1.6.2 * [UPGRADE] Upgraded version of Consolidate to `0.14.1` [a70623c](https://github.com/balderdashy/sails/commit/a70623ce2809d497b3581268354f06904d862268) * [UPGRADE] Upgraded version of grunt-contrib-watch to `1.0.0` [3678](https://github.com/balderdashy/sails/issues/3678) * [INTERNAL] Use standalone CSRF package instead of using the one (formerly) bundled with Connect. Sails should be using all standalone middleware now. [1d3c9e6](https://github.com/balderdashy/sails/commit/1d3c9e6459253261e0f763d133c559641bcbfa33) [98861ef](https://github.com/balderdashy/sails/commit/98861ef12ddca0ff6d57cf7ea6d4bb9f8bca9656) * [INTERNAL] Add some assertions to ensure custom hooks don't use reserved properties [2e76dac](https://github.com/balderdashy/sails/commit/2e76dac2f961a1f20c591fb0a5d7ea6556d2ab70) * [INTERNAL] Update code that virtual response uses to read buffer to work with all Node versions [a5ab134](https://github.com/balderdashy/sails/commit/a5ab134c4bafa40db6b2b2133145f8a5462e4abc) * [INTERNAL] Remove un-maintained "wrench" module from tests; use "fs-extra" instead. Thanks [@Ignigena](https://github.com/Ignigena)! [4f90f78](https://github.com/balderdashy/sails/commit/4f90f78fbfb1b2edf088c5e57d5e4cab56e3cf47) ## 0.12.3 * [BUGFIX] Allow `skipAssets` and `skipRegex` to be used with direct/static view route target syntax [3682](https://github.com/balderdashy/sails/issues/3682). Thanks [@dottodot](https://github.com/dottodot), [@nikhilbedi](https://github.com/nikhilbedi), and [@AlexanderKozhevin](https://github.com/AlexanderKozhevin)! * [BUGFIX] Automatically route to `index/` in deeply nested views when using direct/static view route target syntax * [BUGFIX] Add assertion about views which contain extra dots (`.`) in their paths when using direct/static view route target syntax * [INTERNAL] Use `chalk` instead of `colors` for console output. Thanks [@markelog](https://github.com/markelog)! [3680](https://github.com/balderdashy/sails/pull/3680) ## 0.12.2 * [ENHANCEMENT] Allow use of `fn` in expanded route targets [e1790b7](https://github.com/balderdashy/sails/commit/e1790b70b35cd7dc50743a63bb169585f8a927f2) * [BUGFIX] Add blacklist to "update" blueprint action so that it can be used with primary keys that are not "id" [3625](https://github.com/balderdashy/sails/issues/3625) * [ENHANCEMENT] Allow hooks to be turned off by setting their environment var to the string "false" [3618](https://github.com/balderdashy/sails/issues/3618) * [BUGFIX] Allow view target syntax for routes to specify deeply-nested views [3604](https://github.com/balderdashy/sails/issues/3604) * [BUGFIX] Allow custom bodyParser middleware config [3592](https://github.com/balderdashy/sails/issues/3592) * [BUGFIX] When lifting with unknown validation rule, exit gracefully instead of throwing. * [BUGFIX] Update validation rules from anchor [3649](https://github.com/balderdashy/sails/issues/3649) * [BUGFIX] Respond with an error if attempting to use `req.file()` from a virtual request (i.e. when Skipper is not available). And don't pass in `res` when building the mock request, since it is not available yet. [3656](https://github.com/balderdashy/sails/issues/3656) * [BUGFIX] Fix incorrect handling of errors in responses hook. Thanks [@tapuzzo-fsi](https://github.com/tapuzzo-fsi)! [3645](https://github.com/balderdashy/sails/pull/3645) * [BUGFIX] Fix error from `routeCorsConfig` sometimes being undefined [3662](https://github.com/balderdashy/sails/issues/3662) * [INTERNAL] Replace `ready` event with an async `handleLift` lifecycle callback in order to simplify the behavior of `sails lift` and ensure the timing of the "done" callback is correct when using it programmatically. * [INTERNAL] Massive overhaul of tests. See [b033f2d thru e85810a](https://github.com/balderdashy/sails/compare/71aa56db59129da58825b22f32030234a4f5ae2c...b033f2d9af4953fd65c3e1bbb44ed4df15da1f68). * [INTERNAL] Extrapolate ORM hook into sails-hook-orm. * [INTERNAL] Force asynchronicity in the optional third argument of `res.view()`/`res.render()` to pave the way for better, request-agnostic view rendering methods. This prevents double-calling of the callback if userland code throws an error. Thanks [@lennym](https://github.com/lennym)! [cd413e15435947aa855e27aab16d9cd9e65ad493](https://github.com/balderdashy/sails/commit/cd413e15435947aa855e27aab16d9cd9e65ad493) * [ENHANCEMENT] Update version of i18n to `0.8.1` [3631](https://github.com/balderdashy/sails/pull/3631). * [ENHANCEMENT] Improve auto-migrate prompt, and skip the prompt and log an info message instead if `sails.config.models.migrate` is being automatically set to production anyways. [sails-hook-orm/commit/3161c34edbe0aa07055f8665493734dda1688c2a](https://github.com/balderdashy/sails-hook-orm/commit/3161c34edbe0aa07055f8665493734dda1688c2a) * [ENHANCEMENT] Add production check in case sails-disk is being used, and experimental `sails.config.orm.skipProductionWarnings` flag for preventing the warning. [sails-hook-orm/commit/9a0d46e135dadf00bc4576341624a31e50b12838](https://github.com/balderdashy/sails-hook-orm/commit/9a0d46e135dadf00bc4576341624a31e50b12838) * [INTERNAL] Don't clone target function in expanded route syntax [6cfb2de](https://github.com/balderdashy/sails/commit/6cfb2de17ccafd789d4af001934b286bc189d1a4) * [BUGFIX] Replace naughty code in implicit default res.forbidden() response; relevant when api/responses/ is deleted. See #3667 for more info. Thanks [@Biktop](https://github.com/biktop)! [4767585994c45e7a7040402a057f0e41660d3419](https://github.com/balderdashy/sails/commit/4767585994c45e7a7040402a057f0e41660d34)19 * [INCONSISTENCY] Fix embarassing old link that was being shown when you `console.log` the `sails` app instance. Thanks [@wulfsolter](https://github.com/wulfsolter) [52d45688fcfb6c4437348115f3e9c91595a8d379](https://github.com/balderdashy/sails/commit/52d45688fcfb6c4437348115f3e9c91595a8d379)! * [INTERNAL] Get rid of a whimsical little `--require` in mocha.opts that must have gotten lost. Don't ask us how it ended up there. Thanks [@markelog](https://github.com/markelog)! [06837a53b48352de7c46a1be84e87e28a084ffe2](https://github.com/balderdashy/sails/commit/06837a53b48352de7c46a1be84e87e28a084ffe2) * [INTERNAL] Remove needless require from mocha opts. Thanks [@markelog](https://github.com/markelog)! [3681](https://github.com/balderdashy/sails/pull/3681) ## 0.12.1 * [INTERNAL] Expose private `loadAndRegisterControllers` method for now, since certain apps are relying on it [0ba7829](https://github.com/balderdashy/sails/commit/0ba78296047874debd33ce62588e97c371b7138c) * [BUGFIX] Updated default HTTP cache config property to match what's documented [750d434](https://github.com/balderdashy/sails/commit/750d434a5592b422686ef0217ecab6cc2abcce7a) * [BUGFIX] Check for `sails.io` before checking for `sails.io.httpServer` when lowering [92c4b19](https://github.com/balderdashy/sails/commit/92c4b1907073336b879c7c6abf57d5d34b3fca46) * [ENHANCEMENT] Keep cookie middleware even if session middleware is deactivated [d21ae2d](https://github.com/balderdashy/sails/commit/d21ae2d8cf16df1169187392c9f522f99d556a85) * [BUGFIX] Reset process.env.NODE_ENV after Sails lowers to whatever it was originally (to make it non-sticky when lifting/lowering multiple apps) [f9db888](https://github.com/balderdashy/sails/commit/f9db888a4fd39d43138bad4b279ad86046c27482) * [BUGFIX] Use correct extension config for Handlebars [3559](https://github.com/balderdashy/sails/issues/3559) * [BUGFIX] Update usage of `sails.sockets.id()` in pubsub hook to `sails.sockets.getId()` to avoid deprecation warning [3552](https://github.com/balderdashy/sails/issues/3552) * [INTERNAL] Replace usage of Express middleware (e.g. `require('express').favicon`) with equivalent standalone packages (e.g. `require('serve-favicon')`) * [BUGFIX] Allow passing in non-model instances to `publishCreate` [3558](https://github.com/balderdashy/sails/issues/3558) ## 0.12.0 * [UPGRADE] Bump Waterline dependency to `0.11.0` and Sails-Disk to `0.10.9` * [ENHANCEMENT] More core hooks are now fully documented ([controllers](https://github.com/balderdashy/sails/tree/master/lib/hooks/controllers)|[grunt](https://github.com/balderdashy/sails/tree/master/lib/hooks/grunt)|[logger](https://github.com/balderdashy/sails/tree/master/lib/hooks/logger)|[cors](https://github.com/balderdashy/sails/tree/master/lib/hooks/cors)|[responses](https://github.com/balderdashy/sails/tree/master/lib/hooks/responses)|[orm](https://github.com/balderdashy/sails/tree/master/lib/hooks/orm)) * [ENHANCEMENT] Improve `sails --help` output (note that this removes support for common misspellings) [#3539](https://github.com/balderdashy/sails/issues/3539) * [ENHANCEMENT] Detect EMFILE warnings from grunt-contrib-watch and treat them as fatal (this is the too many open files / `ulimit -n 1024` thing) [#3523](https://github.com/balderdashy/sails/issues/3523) * [BUGFIX] Downgrade default grunt-contrib-watch dependency installed in new Sails apps to use v0.5.3 [#3526](https://github.com/balderdashy/sails/issues/3526) * [BUGFIX] Use locally-installed Sails (when available) with `sails console` instead of always using global [093ec01](https://github.com/balderdashy/sails/commit/093ec01754f1caa54333e97cfb9a095f1697a2f1) * [UPGRADE] Update `express-handlebars` to `3.0.0` [1760604](https://github.com/balderdashy/sails/commit/1760604b5a78eacc2d5a1facd4db2de3ea930972) * [BUGFIX] Don't attempt to run CSRF protection methods if session is not available * [BUGFIX] Properly remove process listeners on sails.lower() to avoid EventEmitter leaks when lifting/lowering multiple apps (e.g. in tests) [#2693](https://github.com/balderdashy/sails/issues/2693) * [UPGRADE] Updated versions of Lodash (v3.10.1) and Async (v1.5.0) used in Sails (and globalized in Sails apps by default) * [ENHANCEMENT] Support for newer versions of connect-redis session adapter (and other session adapters using express-session) * [ENHANCEMENT] Set the useGlobal config option for REPL while using sails console, allows autoreload hook to reflect changes on global models and services * [ENHANCEMENT] Support JSON sorting syntax in blueprints [#2449](https://github.com/balderdashy/sails/issues/2449) * [ENHANCEMENT] Support namespaced modules as hooks [#3022](https://github.com/balderdashy/sails/issues/3022), [#3514](https://github.com/balderdashy/sails/pull/3514) * [ENHANCEMENT] Allow installable hooks to override their default names [#3168](https://github.com/balderdashy/sails/pull/3168) * [BUGFIX] Fixed issues with subscribing sockets to new model instances in a clustered environment [#2990](https://github.com/balderdashy/sails/issues/2990), [#3008](https://github.com/balderdashy/sails/issues/3008) * [UPGRADE] Update `consolidate` to `^0.12.1` * [BUGFIX] Don't allow changing a model's primary key via blueprints * [ENHANCEMENT] Added sails.config.keepResponseErrors option to keep response errors in production mode [#2853](https://github.com/balderdashy/sails/pull/2853) * [ENHANCEMENT] Added Livescript support [#2662](https://github.com/balderdashy/sails/pull/2662), [#2599](https://github.com/balderdashy/sails/pull/2599) * [ENHANCEMENT] Added IcedCoffeeScript support (brrr) [#2599](https://github.com/balderdashy/sails/pull/2599) * [BUGFIX] Fix req.param() to work correctly with falsy params [#2756](https://github.com/balderdashy/sails/pull/2756) * [ENHANCEMENT] Support "exposeHeaders" option in CSRF config [#2712](https://github.com/balderdashy/sails/pull/2712) * [BUGFIX] Honor all route options when using policy target syntax (https://github.com/balderdashy/sails/issues/2609#issuecomment-77527609) * [ENHANCEMENT] New `sails deploy` CLI command. See https://github.com/mikermcneil/sails-deploy-azure for an example deployment strategy. * [ENHANCEMENT] Support CSRF hook route configuration [#2366](https://github.com/balderdashy/sails/issues/2366) * [BUGFIX] Fix [RangeError: Maximum call stack size exceeded] error in PubSub hook * [ENHANCEMENT] Support layout for Ractive template engine * [ENHANCEMENT] Body parser error logs no longer outputted in production, unless `sails.config.keepResponseErrors` is set [#3347](https://github.com/balderdashy/sails/pull/3347) * [BUGFIX] Pluralize option works correctly for all routes [#3223](https://github.com/balderdashy/sails/pull/3223) * [BUGFIX] Blueprint create now works when POSTing arrays [#3228](https://github.com/balderdashy/sails/pull/3228) * [UPGRADE] Updated `sails-hook-sockets` to `^0.13.0`, which uses an updated socket.io-client module and has some bugfixes * [BUGFIX] Default responses now work correctly when views hook is disabled [#2770](https://github.com/balderdashy/sails/pull/2770) * [BUGFIX] Restored troubleshooting messages in console when Sails server fails to lift * [BUGFIX] app-wide locals (sails.config.views.locals) are combined using a shallow merge (`_.extend()` instead of `_.merge()`) [#3500](https://github.com/balderdashy/sails/issues/3500) * [ENHANCEMENT] Added `sails.getRouteFor()` and `sails.getUrlFor()`, utility methods for reverse routing [#3402](https://github.com/balderdashy/sails/issues/3402#issuecomment-167137610) * [BUGFIX] Improve interoperability of virtual requests to provide a more consistent API to Socket.io and `sails.request()` (e.g. for tests) [121f3feb8702d44420e86707ef05e3282461d136](https://github.com/balderdashy/sails/commit/121f3feb8702d44420e86707ef05e3282461d136) * [INTERNAL] Use shallow merge in services hook when loading modules (37eceee9b0ff0a20a285ac2889f4a5e96f3f5b30) * [INTERNAL] Don't expose sails.services until `loadModules` is called in the services hook (37eceee9b0ff0a20a285ac2889f4a5e96f3f5b30) ## 0.11.5 * [BUGFIX] Allow disabling of installed hooks [#3550](https://github.com/balderdashy/sails/pull/3550) * [ENHANCEMENT] Support namespaced modules as hooks (hotfix from [#3022](https://github.com/balderdashy/sails/issues/3022), [#3514](https://github.com/balderdashy/sails/pull/3514)) * [ENHANCEMENT] Allow installable hooks to override their default names (hotfix from [#3168](https://github.com/balderdashy/sails/pull/3168)) ## 0.11.4 * [SECURITY] Updated several dependencies due to security vulnerabilities (https://github.com/balderdashy/sails/issues/3464#issuecomment-169255559) ## 0.11.3 * [BUGFIX] Fix [RangeError: Maximum call stack size exceeded] error in PubSub hook (https://github.com/balderdashy/sails/issues/2636) * [ENHANCEMENT] Allow custom route options in policy target syntax (https://github.com/balderdashy/sails/commit/0990fc10709520a9f6c55923b991708d5eaf8aa0) * [ENHANCEMENT] Support CSRF hook route configuration [#2366](https://github.com/balderdashy/sails/issues/2366) * [ENHANCEMENT] Added "exposeHeaders" option in CORS configuration (https://github.com/balderdashy/sails/pull/2712) ## 0.11.2 * [BUGFIX] Fixes to allow proper installation / execution in environments using Node 4 and/or NPM 3. ## 0.11.1 * Shhhh nothing to see here (version skipped) ## 0.11.0 * [ENHANCEMENT] Allow hooks to be installed in node_modules and dynamic changing of hook name * [ENHANCEMENT] Pull out the `sockets` hook to its own repository * [ENHANCEMENT] Allow hooks to have individual timeouts, and a global `sails.config.hookTimeout` * [ENHANCEMENT] Pull out `sails.io`.js to its own generator * [UPGRADE] Update `sails.io.js` for the latest version of the sockets hook * [UPGRADE] Upgrade from Socket.IO 0.9.17 to 1.2.1 * [FEATURE] Add `restPrefix` setting in addition to `prefix` setting for blueprints for finer control * [ENHANCEMENT] Support partials and layout with Handlebars for the `backend` generator * [BUGFIX] Blueprint creation returns 201 status code instead of 200 * [BUGFIX] `ractive.toHTML()` replaces `ractive.renderHTML()` for Ractive template engine * [BUGFIX] Fix arguments for publishAdd, publishRemove and publishUpdate * [ENHANCEMENT] Enable views hook for all methods * [BUGFIX] Resolve depreciation warnings * [BUGFIX] Fix dependency for npm 2.0.0 * [BUGFIX] Fix Grunt launching when it's a peer dep * [ENHANCEMENT] Upgrade express and skipper because of security vulnerabilities * [BUGFIX] Fix Sails crashes if Redis goes down [#2277](https://github.com/balderdashy/sails/pull/2277) * [BUGFIX] Fix crash when using sessionless requests over WebSockets [#2107](https://github.com/balderdashy/sails/pull/2107) * [ENHANCEMENT] Checking npm-version on install * [ENHANCEMENT] Updated "skipAssets" regex to ignore query string ## 0.10.5 * [ENHANCEMENT] Updated `waterline` to `~0.10.9` * [ENHANCEMENT] Added new `routesDisabled` option for CSRF [#2121](https://github.com/balderdashy/sails/pull/2121) * [ENHANCEMENT] Refactoring and cleanup. * [ENHANCEMENT] Switched from `express3-handlebars` to `express-handlebars` * [BUGFIX] Add missing require for async module [#2101](https://github.com/balderdashy/sails/pull/2101) ## 0.10.4 and earlier? See https://github.com/balderdashy/sails/commits/eea3b43b4e79d6b9f1b03b318a3ccab80704f22a. ================================================ FILE: CODE-OF-CONDUCT.md ================================================ > The Code of Conduct now lives in the 'Contributing' section of the documentation: [http://sailsjs.com/documentation/contributing/code-of-conduct](http://sailsjs.com/documentation/contributing/code-of-conduct) ================================================ FILE: CONTRIBUTING.md ================================================ > The Contribution guide now lives in the 'Contributing' section of the documentation: [sailsjs.com/documentation/contributing](http://sailsjs.com/documentation/contributing) ================================================ FILE: LICENSE.md ================================================ The MIT License (MIT) -- Copyright © 2012-present, Mike McNeil Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: MODULES.md ================================================ # Modules Sails is a large project, with many modular components. Each module is located in its own repository, and in most cases is tested individually. Below, you'll find an overview of the modules maintained by the core team and community members. ## Sails core The modules comprising the Sails framework, as well as the other plugins maintained by our core team, are spread across a number of different code repositories. Some modules can be used outside of the context of Sails, while others are not intended for external use. #### Framework and ORM > For more information on the available releases of the Sails framework as a whole, check out the [contribution guide](https://github.com/balderdashy/sails/blob/master/CONTRIBUTING.md). | Package | Latest Stable Release | Build Status (edge) | |------------------|--------------------------|---------------------------------------| | Sails.js logo (small) | [![NPM version](https://badge.fury.io/js/sails.png)](http://badge.fury.io/js/sails) | [![Build Status](https://travis-ci.org/balderdashy/sails.png?branch=master)](https://travis-ci.org/balderdashy/sails) | Waterline logo (small) | [![NPM version](https://badge.fury.io/js/waterline.png)](http://badge.fury.io/js/waterline) | [![Build Status](https://travis-ci.org/balderdashy/waterline.png?branch=master)](https://travis-ci.org/balderdashy/waterline) #### Core hooks As of Sails v1, some hooks are no longer included in Sails core. Instead, they're published as standalone packages: | Hook | Package | Latest Stable Release | Build Status (edge) | Purpose | |:---------------|---------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------|:---------------------------------------------------------| | `orm` | [sails-hook-orm](https://npmjs.com/package/sails-hook-orm) | [![NPM version](https://badge.fury.io/js/sails-hook-orm.png)](http://badge.fury.io/js/sails-hook-orm) | [![Build Status](https://travis-ci.org/balderdashy/sails-hook-orm.png?branch=master)](https://travis-ci.org/balderdashy/sails-hook-orm) | Implements support for Waterline ORM in Sails. | | `sockets` | [sails-hook-sockets](https://npmjs.com/package/sails-hook-sockets) | [![NPM version](https://badge.fury.io/js/sails-hook-sockets.png)](http://badge.fury.io/js/sails-hook-sockets) | [![Build Status](https://travis-ci.org/balderdashy/sails-hook-sockets.png?branch=master)](https://travis-ci.org/balderdashy/sails-hook-sockets) | Implements Socket.io support in Sails. | > These are not _all_ the core hooks in Sails. There are other core hooks built in to the `sails` package itself (see [`lib/hooks/`](https://github.com/balderdashy/sails/tree/master/lib/hooks)). These other, _built-in hooks_ can still be disabled or overridden using the same configuration. #### Bundled hooks Certain additional hooks are bundled as dependencies of a new Sails app, especially when using the "Web app" template: | Hook | Package | Latest Stable Release | Build Status (edge) | Purpose | |:---------------|---------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------|:---------------------------------------------------------| | `grunt` | [sails-hook-grunt](https://npmjs.com/package/sails-hook-grunt) | [![NPM version](https://badge.fury.io/js/sails-hook-grunt.png)](http://badge.fury.io/js/sails-hook-grunt) | [![Build Status](https://travis-ci.org/balderdashy/sails-hook-grunt.png?branch=master)](https://travis-ci.org/balderdashy/sails-hook-grunt) | Governs the built-in asset pipeline in Sails. | | `organics` | [sails-hook-organics](https://npmjs.com/package/sails-hook-organics) | [![NPM version](https://badge.fury.io/js/sails-hook-organics.png)](http://badge.fury.io/js/sails-hook-organics) | [![Build Status](https://travis-ci.org/sailshq/sails-hook-organics.png?branch=master)](https://travis-ci.org/sailshq/sails-hook-organics) | Evolving library of well-tested, well-documented, and officially supported modules for the most common everyday tasks in apps (e.g. password hashing, emails, billing, etc.) | `apianalytics` | [sails-hook-apianalytics](https://npmjs.com/package/sails-hook-apianalytics) | [![NPM version](https://badge.fury.io/js/sails-hook-apianalytics.png)](http://badge.fury.io/js/sails-hook-apianalytics) | [![Build Status](https://travis-ci.org/sailshq/sails-hook-apianalytics.png?branch=master)](https://travis-ci.org/sailshq/sails-hook-apianalytics) | A Sails hook for logging detailed request metadata and monitoring your API. | `dev` | [sails-hook-dev](https://npmjs.com/package/sails-hook-dev) | [![NPM version](https://badge.fury.io/js/sails-hook-dev.png)](http://badge.fury.io/js/sails-hook-dev) | [![Build Status](https://travis-ci.org/sailshq/sails-hook-dev.png?branch=master)](https://travis-ci.org/sailshq/sails-hook-dev) | A Sails hook that provides diagnostic / debugging information and levers during development. #### Core socket client SDKs | Platform | Package | Latest Stable Release | Build Status (edge) | |--------------|---------------------|----------------------------------|------------------------------| | Browser | [sails.io.js-dist](https://npmjs.com/package/sails.io.js-dist) | [![NPM version](https://badge.fury.io/js/sails.io.js-dist.png)](http://badge.fury.io/js/sails.io.js-dist) | [![Build Status](https://travis-ci.org/balderdashy/sails.io.js.png?branch=master)](https://travis-ci.org/balderdashy/sails.io.js) | | Node.js | [sails.io.js](https://npmjs.com/package/sails.io.js) | [![NPM version](https://badge.fury.io/js/sails.io.js.png)](http://badge.fury.io/js/sails.io.js) | [![Build Status](https://travis-ci.org/balderdashy/sails.io.js.png?branch=master)](https://travis-ci.org/balderdashy/sails.io.js) | #### Other browser libraries The "Web App" template in Sails comes with a lightweight client-side JavaScript wrapper for Vue.js called `parasails`: [![NPM version](https://badge.fury.io/js/parasails.png)](https://npmjs.com/package/parasails) #### Core database adapters | Package | Latest Stable Release | Build Status (edge) | Platform | |:-----------------------------------------------------------------| -------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------|:------------------------------------------------------------------| | [sails-disk](https://npmjs.com/package/sails-disk) | [![NPM version](https://badge.fury.io/js/sails-disk.png)](http://badge.fury.io/js/sails-disk) | [![Build Status](https://travis-ci.org/balderdashy/sails-disk.png?branch=master)](https://travis-ci.org/balderdashy/sails-disk) | Local disk (`.tmp/`) | | [sails-mysql](https://npmjs.com/package/sails-mysql) | [![NPM version](https://badge.fury.io/js/sails-mysql.png)](http://badge.fury.io/js/sails-mysql) | [![Build Status](https://travis-ci.org/balderdashy/sails-mysql.png?branch=master)](https://travis-ci.org/balderdashy/sails-mysql) | [MySQL](http://dev.mysql.com/) | | [sails-postgresql](https://npmjs.com/package/sails-postgresql) | [![NPM version](https://badge.fury.io/js/sails-postgresql.png)](http://badge.fury.io/js/sails-postgresql) | [![Build Status](https://travis-ci.org/balderdashy/sails-postgresql.png?branch=master)](https://travis-ci.org/balderdashy/sails-postgresql) | [PostgreSQL](https://www.postgresql.org/) | | [sails-mongo](https://npmjs.com/package/sails-mongo) | [![NPM version](https://badge.fury.io/js/sails-mongo.png)](http://badge.fury.io/js/sails-mongo) | [![Build Status](https://travis-ci.org/balderdashy/sails-mongo.png?branch=master)](https://travis-ci.org/balderdashy/sails-mongo) | [MongoDB](https://www.mongodb.com/) | | [sails-redis](https://npmjs.com/package/sails-redis) | [![NPM version](https://badge.fury.io/js/sails-redis.png)](http://badge.fury.io/js/sails-redis) | [![Build Status](https://travis-ci.org/balderdashy/sails-redis.png?branch=master)](https://travis-ci.org/balderdashy/sails-redis) | [Redis](http://redis.io) | #### Core filesystem adapters | Package | Latest Stable Release | Build Status (edge) | Platform | |:-----------------------------------------------------------------| -------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------|:------------------------------------------------------------------| | [skipper-disk](https://npmjs.com/package/skipper-disk) | [![NPM version](https://badge.fury.io/js/skipper-disk.png)](http://badge.fury.io/js/skipper-disk) | [![Build Status](https://travis-ci.org/balderdashy/skipper-disk.png?branch=master)](https://travis-ci.org/balderdashy/skipper-disk) | Local disk (`.tmp/uploads/`) | | [skipper-s3](https://npmjs.com/package/skipper-s3) | [![NPM version](https://badge.fury.io/js/skipper-s3.png)](http://badge.fury.io/js/skipper-s3) | [![Build Status](https://travis-ci.org/balderdashy/skipper-s3.png?branch=master)](https://travis-ci.org/balderdashy/skipper-s3) | [Amazon S3 (AWS)](https://aws.amazon.com/s3) | #### Core generators _As of Sails v1.0, core generators are now bundled in [sails-generate](https://github.com/balderdashy/sails-generate). All generators can still be overridden the same way. For examples, see below._ #### Core framework utilities | Package | Latest Stable Release | Build Status (edge) | |-----------------------------------------------------------------------|--------------------------|----------------------------| | [**skipper**](http://npmjs.com/package/skipper) | [![NPM version](https://badge.fury.io/js/skipper.png)](http://badge.fury.io/js/skipper) | [![Build Status](https://travis-ci.org/balderdashy/skipper.png?branch=master)](https://travis-ci.org/balderdashy/skipper) | | [**machine**](http://npmjs.com/package/machine) | [![NPM version](https://badge.fury.io/js/machine.png)](http://badge.fury.io/js/machine) | [![Build Status](https://travis-ci.org/node-machine/machine.png?branch=master)](https://travis-ci.org/node-machine/machine) | | [**machine-as-action**](http://npmjs.com/package/machine-as-action) | [![NPM version](https://badge.fury.io/js/machine-as-action.png)](http://badge.fury.io/js/machine-as-action) | [![Build Status](https://travis-ci.org/sailshq/machine-as-action.png?branch=master)](https://travis-ci.org/treelinehq/machine-as-action) | | [**whelk**](http://npmjs.com/package/whelk) | [![NPM version](https://badge.fury.io/js/whelk.png)](http://badge.fury.io/js/whelk) | [![Build Status](https://travis-ci.org/sailshq/whelk.png?branch=master)](https://travis-ci.org/treelinehq/whelk) | | [**captains-log**](http://npmjs.com/package/captains-log) | [![NPM version](https://badge.fury.io/js/captains-log.png)](http://badge.fury.io/js/captains-log) | [![Build Status](https://travis-ci.org/balderdashy/captains-log.png?branch=master)](https://travis-ci.org/balderdashy/captains-log) | | [**anchor**](http://npmjs.com/package/anchor) | [![NPM version](https://badge.fury.io/js/anchor.png)](http://badge.fury.io/js/anchor) | [![Build Status](https://travis-ci.org/sailsjs/anchor.png?branch=master)](https://travis-ci.org/sailsjs/anchor) | | [**sails-generate**](http://npmjs.com/package/sails-generate) | [![NPM version](https://badge.fury.io/js/sails-generate.png)](http://badge.fury.io/js/sails-generate) | [![Build Status](https://travis-ci.org/balderdashy/sails-generate.png?branch=master)](https://travis-ci.org/balderdashy/sails-generate) | | [**waterline-schema**](http://npmjs.com/package/waterline-schema) | [![NPM version](https://badge.fury.io/js/waterline-schema.png)](http://badge.fury.io/js/waterline-schema) | [![Build Status](https://travis-ci.org/balderdashy/waterline-schema.svg?branch=master)](https://travis-ci.org/balderdashy/waterline-schema) | | [**waterline-utils**](http://npmjs.com/package/waterline-utils) | [![NPM version](https://badge.fury.io/js/waterline-utils.png)](http://badge.fury.io/js/waterline-utils) | [![Build Status](https://travis-ci.org/sailshq/waterline-utils.svg?branch=master)](https://travis-ci.org/balderdashy/waterline-utils) | [**include-all**](http://npmjs.com/package/include-all) | [![NPM version](https://badge.fury.io/js/include-all.png)](http://badge.fury.io/js/include-all) | [![Build Status](https://travis-ci.org/balderdashy/include-all.png?branch=master)](https://travis-ci.org/balderdashy/include-all) | | [**reportback**](http://npmjs.com/package/reportback) | [![NPM version](https://badge.fury.io/js/reportback.png)](http://badge.fury.io/js/reportback) | _n/a_ | [**switchback**](http://npmjs.com/package/switchback) | [![NPM version](https://badge.fury.io/js/switchback.png)](http://badge.fury.io/js/switchback) | [![Build Status](https://travis-ci.org/node-machine/switchback.png?branch=master)](https://travis-ci.org/node-machine/switchback) | | [**rttc**](http://npmjs.com/package/rttc) | [![NPM version](https://badge.fury.io/js/rttc.png)](http://badge.fury.io/js/rttc) | [![Build Status](https://travis-ci.org/node-machine/rttc.png?branch=master)](https://travis-ci.org/node-machine/rttc) | | [**@sailshq/lodash**](http://npmjs.com/package/@sailshq/lodash) | [![npm version](https://badge.fury.io/js/%40sailshq%2Flodash.svg)](https://badge.fury.io/js/%40sailshq%2Flodash) | _n/a_ #### Forks - [@sailshq/lodash](https://npmjs.com/package/@sailshq/lodash) · _(A fork of Lodash 3.10.x that fixes security issues. Ongoing maintenance provided by the Sails core team.)_ - [@sailshq/connect-redis](https://npmjs.com/package/@sailshq/connect-redis) - [@sailshq/socket.io-redis](https://npmjs.com/package/@sailshq/socket.io-redis) - [@sailshq/eslint](https://npmjs.com/package/@sailshq/eslint) - [@sailshq/htmlhint](https://npmjs.com/package/@sailshq/htmlhint) - [@sailshq/lesshint](https://npmjs.com/package/@sailshq/lesshint) ## Official documentation The official documentation for the Sails framework is written in Markdown, and is automatically compiled for the [Sails website](http://sailsjs.com). | Repo | Purpose | |------------|:----------------------------------| | [sails-docs](https://github.com/balderdashy/sails-docs) | Raw content for reference, conceptual, anatomical, and other documentation on the Sails website (in Markdown). | [www.sailsjs.com](https://sailsjs.com) | The Sails app that powers [sailsjs.com](http://sailsjs.com). HTML content is automatically compiled from [`sails-docs`](https://github.com/balderdashy/sails-docs). | [doc-templater](https://github.com/uncletammy/doc-templater) | The module we use to pre-process, compile, and format Markdown documentation files into the HTML markup and tree menus at [`sailsjs.com/documentation`](http://sailsjs.com/documentation). _All known translation projects for the Sails documentation are listed in the README [**sails-docs**](https://github.com/balderdashy/sails-docs)._ ## Community projects In addition to the official code repositories that are supported by the Sails.js core team, there are countless other plugins created by members of the Sails.js community. #### Hooks There are at least 200 community hooks for Sails.js [available on NPM](https://www.npmjs.com/search?q=sails+hook). > [Learn about custom hooks in Sails](http://sailsjs.com/documentation/concepts/extending-sails/hooks). #### Asset pipeline Need to customize your build? Want automatically-generated spritesheets? Source maps? Sails.js uses Grunt for its asset pipeline, which means it supports any Grunt plugin. out of the box. There are thousands of Grunt plugins [available on NPM](http://gruntjs.com/plugins). > [Learn how to customize your app's asset pipeline](http://sailsjs.com/documentation/concepts/assets). #### Generators Don't like Grunt? Want to use WebPack or Gulp instead? Prefer your generated backend files to be written in CoffeeScript? There are at least 100 community generators for Sails.js [available on NPM](https://www.npmjs.com/search?q=sails%20generate). > [Learn how to use community generators, and how to build your own](http://sailsjs.com/documentation/concepts/extending-sails/generators). #### Database adapters Is your database not supported by one of the core adapters? Good news! There are many different community database adapters for Sails.js and Waterline [available on NPM](https://www.npmjs.com/search?q=sails+adapter). > [Learn how to install and configure community adapters](http://sailsjs.com/documentation/concepts/extending-sails/adapters). #### Filesystem adapters Need to upload files to a cloud file store like S3, GridFS, or Azure Cloud Files? Check out the community filesystem adapters for Sails.js and Skipper [available on NPM](https://www.npmjs.com/search?q=skipper+adapter). > [Learn how to wire up one or more custom filesystem adapters for your application](https://github.com/balderdashy/skipper#use-cases). #### 3rd party integrations Need to process payments with Stripe? Fetch video metadata from YouTube? Process user email data via Google APIs? Choose from hundreds of community machinepacks for Sails.js/Node [available on NPM](http://node-machine.org/machinepacks). > [Learn how to install and use machinepacks in your controller actions and helpers.](http://node-machine.org/) #### Database drivers Want to work with your database at a low level? Need to get extra performance out of your database queries? Dynamic database connections? > [Learn about Waterline drivers](https://github.com/node-machine/driver-interface). #### View engines Is EJS bumming you out? Prefer to use a different templating language like pug (/jade), handlebars, or dust? Sails.js supports almost any Consolidate/Express-compatible view engine-- meaning you can use just about any imaginable markup language for your Sails.js views. Check out the community view engines for Sails.js and Express [available on NPM](http://sailsjs.com/documentation/concepts/views/view-engines). > [Learn how to set up a custom view engine for your app](http://sailsjs.com/documentation/reference/configuration/sails-config-views). #### Session stores The recommended production session store for Sails.js is Redis... but we realize that, for some apps, that isn't an option. Fortunately, Sails.js supports almost any Connect/Express-compatible session store-- meaning you can store your sessions almost anywhere, whether that's Mongo, on the local filesystem, or even in a relational database. Check out the community session stores for Sails.js, Express, and Connect [available on NPM](https://www.npmjs.com/search?q=connect%20session-). > [Learn how to install and configure a custom session store in your Sails app](http://sailsjs.com/documentation/reference/configuration/sails-config-session#?production-config). #### Community socket client SDKs & examples Need to connect to Sails from a native iPhone or Android app? | Platform | Repo | Build Status (edge) | |--------------|------------|----------------------------------| | iOS | [sails.ios](https://github.com/ChrisChares/sails.ios) | [![CI Status](http://img.shields.io/travis/ChrisChares/sails.ios.svg?style=flat)](https://travis-ci.org/ChrisChares/sails.ios) | | Objective C | [sails.io.objective-c](https://github.com/fishrod-interactive/sails-io.objective-c) | _N/A_ | | Android | [Sails Messenger](https://github.com/TheFinestArtist/Sails-Messenger) | _N/A_ | | React Native | [React Native example](https://github.com/mikermcneil/chatkin/tree/master/mobileapp) | _N/A_ | | Cordova | [Phonegap tips](https://stackoverflow.com/questions/33378104/how-to-implement-sailsjs-phonegap-cordova-application) | _N/A_ | #### Misc. projects | Package | Latest Stable Release | Purpose |-------------------------------------------------------------------------------------|---------------------------------|:------------| | [sails-migrations](https://github.com/BlueHotDog/sails-migrations) | [![NPM version](https://badge.fury.io/js/sails-migrations.png)](http://badge.fury.io/js/sails-migrations) | Manual migration tool for Sails, built on Knex. | [sails-mysql-transactions](https://github.com/postmanlabs/sails-mysql-transactions) | [![NPM version](https://badge.fury.io/js/sails-mysql-transactions.png)](http://badge.fury.io/js/sails-mysql-transactions) | Augmented database adapter for mySQL with transaction and replication support. | [sails-inverse-model](https://www.npmjs.com/package/sails-inverse-model) | Generate Sails/Waterline model definitions from a pre-existing database. ## FAQ #### What happened to the core generators? For easier maintainence, they were pulled into [`sails-generate`](https://github.com/balderdashy/sails-generate). #### What release of XYZ should I install? You can read about naming conventions for plugins and core modules [here](https://gist.github.com/mikermcneil/baa3eed1030e67f1b0670fb05a2b1f53). Covers NPM dist tags, git tags, and version strings, as well as recommendations for hotfix branches. ================================================ FILE: README.md ================================================ # [![Sails.js](http://balderdashy.github.io/sails/images/logo.png "Sails.js")](http://sailsjs.com) ### [Website](https://sailsjs.com/)   [Get Started](https://sailsjs.com/get-started)   [Docs](http://sailsjs.com/documentation)   [News](http://twitter.com/sailsjs)   [Submit Issue](http://sailsjs.com/bugs) [![NPM version](https://badge.fury.io/js/sails.svg)](http://badge.fury.io/js/sails)   [![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/balderdashy/sails)   [![Twitter Follow](https://img.shields.io/twitter/follow/sailsjs.svg?style=social&maxAge=3600)](https://twitter.com/sailsjs) Sails.js is a [web framework](http://sailsjs.com/whats-that) that makes it easy to build custom, enterprise-grade Node.js apps. It is designed to resemble the MVC architecture from frameworks like Ruby on Rails, but with support for the more modern, data-oriented style of web app & API development. It's especially good for building realtime features like chat. Since version 1.0, Sails supports `await` out of the box. This replaces nested callbacks (and the commensurate error handling) with simple, familiar usage: ```javascript var orgs = await Organization.find(); ``` ## Installation   **With [node](http://nodejs.org) [installed](http://nodejs.org/en/download):** ```sh # Get the latest stable release of Sails $ npm install sails -g ``` > ##### Upgrading from an earlier version of Sails? > Upgrade guides for all major releases since 2013 are available on the Sails website under [**Upgrading**](http://sailsjs.com/upgrading). ## Your First Sails Project **Create a new app:** ```sh # Create the app sails new my-app ``` **Lift sails:** ```sh # cd into the new folder cd my-app # fire up the server sails lift ``` [![Screenshot from the original Sails video](http://i.imgur.com/Ii88jlhl.png)](https://sailsjs.com/get-started) For the most up-to-date introduction to Sails, [get started here](https://sailsjs.com/get-started). ## Compatibility Sails is built on [Node.js](http://nodejs.org/), [Express](http://expressjs.com/), and [Socket.io](http://socket.io/). Sails [actions](http://sailsjs.com/documentation/concepts/actions-and-controllers) are compatible with Connect middleware, so in most cases, you can paste code into Sails from an existing Express project and everything will work-- plus you'll be able to use WebSockets to talk to your API, and vice versa. The ORM, [Waterline](https://github.com/balderdashy/waterline), has a well-defined adapter system for supporting all kinds of datastores. Officially supported databases include [MySQL](https://npmjs.com/package/sails-mysql), [PostgreSQL](https://npmjs.com/package/sails-postgresql), [MongoDB](https://npmjs.com/package/sails-mongo), [Redis](https://npmjs.com/package/sails-redis), and [local disk / memory](https://npmjs.com/package/sails-disk). Community adapters exist for [CouchDB](https://github.com/search?q=sails+couch&nwo=codeswarm%2Fsails-couchdb-orm&search_target=global&ref=cmdform), [neDB](https://github.com/adityamukho/sails-nedb), [SQLite](https://github.com/AndrewJo/sails-sqlite3/tree/0.10), [Oracle](https://github.com/search?utf8=%E2%9C%93&q=%22sails+oracle%22+OR+%22waterline+oracle%22&type=Repositories&ref=searchresults), [MSSQL](https://github.com/misterGF/sails-mssqlserver), [DB2](https://github.com/search?q=sails+db2&type=Repositories&ref=searchresults), [ElasticSearch](https://github.com/search?q=%28elasticsearch+AND+sails%29+OR+%28elasticsearch+AND+waterline%29+&type=Repositories&ref=searchresults), [Riak](https://github.com/search?q=sails+riak&type=Repositories&ref=searchresults), [neo4j](https://www.npmjs.org/package/sails-neo4j), [OrientDB](https://github.com/appscot/sails-orientdb), [Amazon RDS](https://github.com/TakenPilot/sails-rds), [DynamoDB](https://github.com/TakenPilot/sails-dynamodb), [Azure Tables](https://github.com/azuqua/sails-azuretables), [RethinkDB](https://github.com/gutenye/sails-rethinkdb) and [Solr](https://github.com/sajov/sails-solr); for various 3rd-party REST APIs like Quickbooks, Yelp, and Twitter, including a configurable generic [REST API adapter](https://github.com/zohararad/sails-rest); plus some [eclectic projects](https://www.youtube.com/watch?v=OmcQZD_LIAE). Powered by MySQL                                 > For the latest core adapters and notable community adapters, see [Available Adapters](http://sailsjs.com/documentation/concepts/extending-sails/adapters/available-adapters). ## Tutorial Course - [Sailscasts](https://sailscasts.com/), taught by [Kelvin Omereshone](https://twitter.com/Dominus_Kelvin) _(English)_ - [Full-Stack JavaScript with Sails.js and Vue.js](https://platzi.com/cursos/javascript-pro/), taught by [Mike McNeil](https://twitter.com/mikermcneil) _(in English, with optional Spanish subtitles)_ ## Books - [Sails.js in Action](https://www.manning.com/books/sails-js-in-action) by Mike McNeil and Irl Nathan (Manning Publications). - [Sails.js Essentials](https://www.packtpub.com/web-development/sailsjs-essentials) by Shaikh Shahid (Packt) - [Pro Express.js: Part 3](http://link.springer.com/chapter/10.1007%2F978-1-4842-0037-7_18) by Azat Mardan (Apress). ## Support Need help or have a question? - [Frequently Asked Questions (FAQ)](http://sailsjs.com/faq) - [Tutorials](http://sailsjs.com/faq#?what-are-some-good-community-tutorials) - [Community support](http://sailsjs.com/support) - [Professional/Enterprise options](http://sailsjs.com/faq#?are-there-professional-support-options) ## Issue submission Please read the [submission guidelines](http://sailsjs.com/documentation/contributing/issue-contributions) and [code of conduct](http://sailsjs.com/documentation/contributing/code-of-conduct) before opening a new issue. Click [here](https://github.com/balderdashy/sails/search?q=&type=Issues) to search/post issues in this repository. ## Contribute There are many different ways you can contribute to Sails: - answering questions on [StackOverflow](http://stackoverflow.com/questions/tagged/sails.js), [Gitter](https://gitter.im/balderdashy/sails), [Facebook](https://www.facebook.com/sailsjs), or [Twitter](https://twitter.com/search?f=tweets&vertical=default&q=%40sailsjs%20OR%20%23sailsjs%20OR%20sails.js%20OR%20sailsjs&src=typd) - improving the [documentation](https://github.com/balderdashy/sails-docs#contributing-to-the-docs) - translating the [documentation](https://github.com/balderdashy/sails-docs/issues/580) to your native language - writing [tests](https://github.com/balderdashy/sails/blob/master/test/README.md) - writing a [tutorial](https://github.com/sails101/contribute-to-sails101), giving a [talk](https://speakerdeck.com/mikermcneil), or supporting [your local Sails meetup](https://www.meetup.com/find/?allMeetups=false&keywords=node.js&radius=Infinity&sort=default) - troubleshooting [reported issues](http://sailsjs.com/bugs) - and [submitting patches](http://sailsjs.com/documentation/contributing/code-submission-guidelines). _Please carefully read our [contribution guide](http://sailsjs.com/documentation/contributing) and check the [build status](http://sailsjs.com/architecture) for the relevant branch before submitting a pull request with code changes._ ## Links - [Website](http://sailsjs.com/) - [Documentation](http://sailsjs.com/documentation) - [Ask a question](http://sailsjs.com/support) - [Tutorial](https://platzi.com/cursos/javascript-pro/) - [Roadmap](https://trello.com/b/s9zEnyG7/sails-v1) - [Twitter (@sailsjs)](https://twitter.com/sailsjs) - [Facebook](https://www.facebook.com/sailsjs) ## Team Sails is actively maintained with the help of many amazing [contributors](https://github.com/balderdashy/sails/graphs/contributors). Our core team consists of: [![Mike McNeil](https://www.gravatar.com/avatar/4b02a9d5780bdd282151f7f9b8a4d8de?s=144&d=identicon&rating=g)](https://twitter.com/mikermcneil) | [![Kelvin Omereshone](https://avatars.githubusercontent.com/u/24433274?s=144&v=3)](https://twitter.com/dominus_kelvin) | [![Eric Shaw](https://avatars2.githubusercontent.com/u/7445991?s=144&v=3)](https://github.com/eashaw) |:---:|:---:|:---:| [Mike McNeil](http://github.com/mikermcneil) | [Kelvin Omereshone](https://github.com/DominusKelvin) | [Eric Shaw](https://github.com/eashaw) [Our company](https://sailsjs.com/about) designs/builds Node.js websites and apps for startups and enterprise customers. After building a few applications and taking them into production, we realized that the Node.js development landscape was very much still the Wild West. Over time, after trying lots of different methodologies, we decided to crystallize all of our best practices into this framework. Six years later, Sails is now one of the most widely-used web application frameworks in the world. I hope it saves you some time! :) ## License [MIT License](https://opensource.org/licenses/MIT) Copyright © 2012-present, Mike McNeil > Sails is built around so many great open-source technologies that it would never have crossed our minds to keep it proprietary. We owe huge gratitude and props to Ryan Dahl ([@ry](https://github.com/ry)), TJ Holowaychuk ([@tj](https://github.com/tj)), Doug Wilson ([@dougwilson](https://github.com/dougwilson)) and Guillermo Rauch ([@rauchg](https://github.com/rauchg)) for the work they've done, as well as the stewards of all the other open-source modules we use. Sails could never have been developed without your tremendous contributions to the JavaScript community. ![A squid peering inside a book, halation and cosmic Sails.js knowledge emanating from the pages of the substantial tome](https://sailsjs.com/images/get_started_hero.png) ================================================ FILE: ROADMAP.md ================================================ # Sails Roadmap As of November 2017, the Sails project roadmap is now managed [on Trello](https://trello.com/b/s9zEnyG7) to allow for simpler feedback and collaboration. The "Bugs/Priority" and "Frontlog" columns on Trello consist of relatively hashed-out proposals for useful features or patches which would be excellent places to contribute code to the Sails framework. We would exuberantly accept a pull request implementing any of the Trello cards in these columns, so long as that pull request was accompanied with reasonable tests that prove it, all code changes adhere to the style guide laid out in the `.eslintrc` file, and it doesn't cause breaking changes to any other core functionality. > Community proposals can still be made as pull requests against this file-- but instead of managing status updates here, once approved, they are now managed on the [Trello board](https://trello.com/b/s9zEnyG7). See "Pending Proposals" below for more on that. > ##### What's up with Sails v1.0? > > For the latest news on Sails v1.0 and beyond, and to check out specific changes and new features, see https://trello.com/b/s9zEnyG7. (Please feel free to contribute by leaving comments on cards! It helps the core team to verify that the new release is working as expected.) > > You can find more information about installing v1.0 here: http://sailsjs.com/documentation/upgrading/to-v-1-0 ## Pending Proposals The table below consists of pending proposals for useful features which are not currently in the official roadmap on Trello. To submit a proposal, send a pull request adding a row to this column. Please see the Sails [contribution guide](https://github.com/balderdashy/sails/blob/master/CONTRIBUTING.md) to get started. > - If you would like to see a new feature or an enhancement to an existing feature in Sails, please review the [Sails contribution guide](https://github.com/balderdashy/sails/blob/master/CONTRIBUTING.md). When you are ready, submit a pull request adding a new row to the bottom of this table. > - Check [the official Sails roadmap on Trello](https://trello.com/b/s9zEnyG7) to make sure there isn't already something similar feature already planned, in discussion, or under active development. > - In your pull request, please include a detailed proposal with a short summary of your use case, the reason why you cannot implement the feature as a hook, adapter, or generator, and a well-reasoned explanation of how you think that feature could be implemented. Your proposal should include changes or additions to usage, expected return values, and any errors or exit conditions. > - Once your pull request has been created, add an additional commit which links to it from your new row in the table below. > - If there is sufficient interest in the proposal from other contributors, a core team member will close your PR and add a new card for the proposal to the appropriate column [on Trello](https://trello.com/b/s9zEnyG7). Feature | Proposal | Summary :---------------------------------------------- | :------------------------------------------------------------------------------------ | :---------------------------------------------------------------------------------------------------------- Allow select/omit clauses when populating a singular association | https://trello.com/c/yM9WPxzr/107-waterline-fs2q-tolerate-a-subcriteria-being-provided-to-populate-for-a-singular-associations-but-only-if-it-exclusively-contains | Don't throw an error if these clauses are included in a `populate` for a singular association (but still error if actual "where" criteria are used) Generate `test/` folder in new Sails apps | [#2499](https://github.com/balderdashy/sails/pull/2499#issuecomment-171556544) | Generate a generic setup for mocha tests in all new Sails apps. Originally suggested by [@jedd-ahyoung](https://github.com/jedd-ahyoung). ================================================ FILE: accessible/generate.js ================================================ /** * Module dependencies */ var sailsgen = require('sails-generate'); // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // TODO: Remove this at next opportunity to simplify maintenance. // (Check docs, but I don't think it's documented, and it's not being used // anywhere anymore. Now that NPM is faster than it used to be, there's no // reason to work towards separating the core generators from the main // framework's NPM package anymore. So this doesn't really need to exist, // unless there are a lot of really good use cases for why generators need to be // easily expoed for programmatic usage. If you have such a use case, let us // know at https://sailsjs.com/bugs) // // But note that this is a breaking change. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /** * require('sails/accessible/generate') * * Generate files or folders. * * > This is an exposed version of sails-generate for programmatic use. * > (available on `require('sails').Sails.generate()`) * * @param {Dictionary} scope * @param {Function|Dictionary} cbOrHandlers */ module.exports = function generate (){ return sailsgen.apply(this, Array.prototype.slice.call(arguments)); }; ================================================ FILE: accessible/rc.js ================================================ /** * Module dependencies */ var rc = require('../lib/app/configuration/rc'); /** * require('sails/accessible/rc') * * A direct reference to Sails' built-in `rc` dependency. * * > This should not be modified. * > It's job is to eliminate the need for an extra `rc` dep. in userland * > just to load cmdline config in app.js. * * @type {Ref} */ module.exports = rc; ================================================ FILE: appveyor.yml ================================================ # # # # # # # # # # # # # # # # # # # # # # # # # # # ╔═╗╔═╗╔═╗╦ ╦╔═╗╦ ╦╔═╗╦═╗ ┬ ┬┌┬┐┬ # # ╠═╣╠═╝╠═╝╚╗╔╝║╣ ╚╦╝║ ║╠╦╝ └┬┘││││ # # ╩ ╩╩ ╩ ╚╝ ╚═╝ ╩ ╚═╝╩╚═o ┴ ┴ ┴┴─┘ # # # # This file configures Appveyor CI. # # (i.e. how we run the tests on Windows) # # # # https://www.appveyor.com/docs/lang/nodejs-iojs/ # # # # # # # # # # # # # # # # # # # # # # # # # # # # Test against these versions of Node.js. environment: matrix: - nodejs_version: "12" - nodejs_version: "14" - nodejs_version: "16" # Install scripts. (runs after repo cloning) install: # Get the latest stable version of Node.js # (Not sure what this is for, it's just in Appveyor's example.) - ps: Install-Product node $env:nodejs_version # Install declared dependencies - npm install # Post-install test scripts. test_script: # Output Node and NPM version info. # (Presumably just in case Appveyor decides to try any funny business? # But seriously, always good to audit this kind of stuff for debugging.) - node --version - npm --version # Run the actual tests (but note that we skip linting.) - npm run custom-tests # Don't actually build. # (Not sure what this is for, it's just in Appveyor's example. # I'm not sure what we're not building... but I'm OK with not # building it. I guess.) build: off ================================================ FILE: bin/private/patched-commander.js ================================================ /** * Module dependencies */ var _ = require('@sailshq/lodash'); var program = require('commander'); // // // Monkey-patch commander // // // Override the `usage` method to always strip out the `*` command, // which we added so that `sails someunknowncommand` will output // the Sails help message instead of nothing. var usage = program.Command.prototype.usage; program.Command.prototype.usage = program.usage = function( /* str */ ) { program.commands = _.reject(program.commands, { _name: '*' }); return usage.apply(this, Array.prototype.slice.call(arguments)); }; // Force commander to display version information. program.Command.prototype.versionInformation = program.versionInformation = function() { program.emit('version'); }; module.exports = program; ================================================ FILE: bin/private/read-repl-history-and-start-transcribing.js ================================================ /** * Module dependencies */ var fs = require('fs'); /** * readReplHistoryAndBeginTranscribing() * * Load from a REPL history file, then bind notifier functions to * track history-making events as changes occur in the future. * * > Originally based on https://github.com/tmpvar/repl.history * * @param {Ref} repl * An already-started REPL instance. * * @param {String} file * The absolute path to the `.node_history` file to use. */ module.exports = function readReplHistoryAndBeginTranscribing(repl, file) { // Check that the REPL history file exists. var historyFileExists = fs.existsSync(file); if (historyFileExists) { // If so, then read it, and set the initial REPL history. repl.history = fs.readFileSync(file, 'utf-8').split('\n').reverse(); repl.history.shift(); repl.historyIndex = -1; }//>- // Attempt to open the history file. var fd = fs.openSync(file, 'a'); // Track whether we've logged a warning about writing the REPL history yet. // (Just to avoid making everybody tear their hair out.) var alreadyLoggedWarningAboutREPLHistory; // Bind alistener that will fire each time a newline is entered on the REPL. repl.addListener('line', function (code) { // Update the REPL history file accordingly. if (code && code !== '.history') { var buffer = Buffer.from(code + '\n'); // Send all arguments to fs.write to support Node v0.10.x. fs.write(fd, buffer, 0, buffer.length, null, function (err /*, written */){ if (!err) { // If everything worked, then there's nothing to worry about. We're done. return; } // Otherwise, log a warning about the REPL history. // (Unless the spinlock has already been spun.) if (alreadyLoggedWarningAboutREPLHistory) { return; } alreadyLoggedWarningAboutREPLHistory = true; console.warn('WARNING: Could not write REPL history. Details: '+err.stack); });// _∏_ } else { repl.historyIndex++; repl.history.pop(); } });// // Bind a one-time-use listener that will fire when the process exits. process.once('exit', function () { // Close the history file. fs.closeSync(fd); });// }; ================================================ FILE: bin/sails-console.js ================================================ /** * Module dependencies */ var nodepath = require('path'); var REPL = require('repl'); var stream = require('stream'); var _ = require('@sailshq/lodash'); var chalk = require('chalk'); var CaptainsLog = require('captains-log'); var rconf = require('../lib/app/configuration/rc')(); var Sails = require('../lib/app'); var SharedErrorHelpers = require('../errors'); var readReplHistoryAndStartTranscribing = require('./private/read-repl-history-and-start-transcribing'); /** * `sails console` * * Enter the interactive console (aka REPL) for the app * in our working directory. This is just like the default * Node REPL except that it starts with the Sails app in the * current directory lifted, and with console history enabled * (i.e. so you can press up arrow to browse and potentially * replay commands from past runs) * * @stability 3 * @see http://sailsjs.com/documentation/reference/command-line-interface/sails-console * ------------------------------------------------------------------------ * This lifts the Sails app in the current working directory, then uses * the core `repl` package to spin up an interactive console. * * Note that, if `--dontLift` was set, then `sails.load()` will be used * instead. (By default, the `sails console` cmd runs `sails.lift()`.) * ------------------------------------------------------------------------ */ module.exports = function() { // Get a temporary logger just for use in `sails console`. // > This is so that logging levels are configurable, even when a // > Sails app hasn't been loaded yet. var cliLogger = CaptainsLog(rconf.log); // Now grab our dictionary of configuration overrides to pass in // momentarily when we lift (or load) our Sails app. This is the // dictionary of configuration settings built from `.sailsrc` file(s), // command-line options, and environment variables. // (No need to clone, since, even through we're modifying it below, // it's not being used anywhere else.) var configOverrides = rconf; // Then tweak this configuration to make sure we always disable // the ASCII ship. It just doesn't look good in the REPL. if (!_.isObject(configOverrides.log)) { configOverrides.log = {}; } configOverrides.log.noShip = true; // Determine whether to use the local or global Sails install. var sailsApp = (function _determineAppropriateSailsAppInstance(){ // Use the app's locally-installed Sails dependency (in `node_modules/sails`), // assuming it's extant and valid. // > Note that we always assume the current working directory to be the // > root directory of the app. var appPath = process.cwd(); var localSailsPath = nodepath.resolve(appPath, 'node_modules/sails'); if (Sails.isLocalSailsValid(localSailsPath, appPath)) { cliLogger.verbose('Using locally-installed Sails.'); cliLogger.silly('(which is located at `'+localSailsPath+'`)'); return require(localSailsPath); }// --• // Otherwise, since no workable locally-installed Sails exists, // run the app using the currently running version of Sails. // > This is probably always the global install. cliLogger.info('No local Sails install detected; using globally-installed Sails.'); return Sails(); })(); console.log(); if (configOverrides.dontLift) { cliLogger.info(chalk.blue('Loading app in interactive mode...')); cliLogger.info(chalk.gray('Sails is not listening for requests (since `dontLift` was enabled).')); cliLogger.info(chalk.gray('You still have access to your models, helpers, and `sails`.')); } else { cliLogger.info(chalk.blue('Starting app in interactive mode...')); } console.log(); // Lift (or load) Sails (function _loadOrLift(proceed){ // If `--dontLift` was set, then use `.load()` instead. if (configOverrides.dontLift) { sailsApp.load(configOverrides, proceed); } // Otherwise, go with the default behavior (`.lift()`) else { sailsApp.lift(configOverrides, proceed); } })(function afterwards(err){// ~∞%° if (err) { return SharedErrorHelpers.fatal.failedToLoadSails(err); } // Get the current global _ value, if any. var underscore = global._; cliLogger.info('Welcome to the Sails console.'); cliLogger.info(chalk.grey('( to exit, type ' + '+' + ' )')); console.log(); // Define a custom output stream that will replace global._ after every command. // This works around the issue where the Node REPL uses the underscore to hold // the result of the last command. var outputStream = (function() { // Create a new writable stream. var writableStream = new stream.Writable(); // Add the `_write` method to it (can't do this in the constructor b/c that's not supported in older Node versions). writableStream._write = function(chunk, encoding, callback) { // Ignore the output generated the first time the global _ is set in Node 6+. if (chunk.toString('utf8').indexOf('Expression assignment to _ now disabled.') !== -1) { return callback(); } // Set the global underscore again (for Node < 6). // See code after `REPL.start` for more info. if (typeof underscore !== 'undefined') { global._ = underscore; } // Forward the chunk on to stdout. process.stdout.write(chunk, encoding, callback); }; // Return the new writable stream. return writableStream; })(); // Start a REPL. var repl = REPL.start({ // Set the REPL prompt. prompt: 'sails> ', // Allow the REPL to use the same global space as the Sails app, giving it access // to things like globalized models. useGlobal: true, // Specify the custom output stream we created above. output: outputStream, // When an output stream is specified, an input stream must be specified as well // or else the REPL crashes. input: process.stdin, // Set `terminal` to true to allow arrow keys to work correctly, // even when we're using a custom output stream. Otherwise pressing // the up arrow just outputs ^[[A instead of accessing history. terminal: true, preview: false, // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // FUTURE: Potentially use custom `eval` as stopgap for `await` support in Node ` prompt, making for a very confusing // introduction to the Sails console. In the custom output stream we defined above, we filter out // that message. if (typeof underscore !== 'undefined') { global._ = underscore; } // Now attempt to read the existing REPL history file, if there is one. var pathToReplHistoryFile = nodepath.join(sailsApp.config.paths.tmp, '.node_history'); try { // Read the REPL history file, and bind notifier functions that will listen // for history-making events, and keep track of them for future generations. readReplHistoryAndStartTranscribing(repl, pathToReplHistoryFile); } catch (e) { cliLogger.verbose('Encountered an error attempting to access/interpret a `.node_history` file at `'+pathToReplHistoryFile+'`.'); cliLogger.verbose('(This session of `sails console` will still work, it just won\'t support REPL history.)'); cliLogger.verbose('Error details:\n',e); }//>- // Bind a one-time-use handler that will run when the REPL instance emits its "exit" event. repl.once('exit', function(err) { // If an error occurred, log it, then terminate the process with an exit code of 1. if (err) { cliLogger.error(err); return process.exit(1); }// --• // Otherwise, everything is cool. // Call the core 'lower' function and terminate the process with an exit code of 0. sailsApp.lower(function () { return process.exit(0); }); });// });// }; ================================================ FILE: bin/sails-debug-console.js ================================================ #!/usr/bin/env node /** * Module dependencies */ var path = require('path'); var Womb = require('child_process'); var CaptainsLog = require('captains-log'); var chalk = require('chalk'); var Sails = require('../lib/app'); /** * `sails debug-console` * * Attach the Node debugger and enter the interactive console * (aka REPL) for the app in our working directory by calling * `sails-console.js`. You can then use the console to invoke * methods and Node inspector, or your favorite IDE, to debug * your app as it runs. * * @stability 2 * @see http://sailsjs.org/documentation/reference/command-line-interface/sails-debug-console */ module.exports = function(cmd) { var extraArgs = cmd.parent.rawArgs.slice(3); var log = CaptainsLog(); // Use the app's local Sails in `node_modules` if one exists // But first make sure it'll work... var appPath = process.cwd(); var pathToSails = path.resolve(appPath, '/node_modules/sails'); if (!Sails.isLocalSailsValid(pathToSails, appPath)) { // otherwise, use the currently-running instance of Sails pathToSails = path.resolve(__dirname, './sails.js'); } console.log(); log.info('Running console in debug mode...'); log.info(chalk.grey('( to exit, type ' + '+' + ' )')); console.log(); // Spin up child process for the Sails console Womb.spawn('node', ['--debug', pathToSails, 'console'].concat(extraArgs), { stdio: 'inherit' }); }; ================================================ FILE: bin/sails-debug.js ================================================ /** * Module dependencies */ var path = require('path'); var Womb = require('child_process'); var CaptainsLog = require('captains-log'); var chalk = require('chalk'); var Sails = require('../lib/app'); /** * `sails debug` * * Attach the Node debugger and lift a Sails app. * You can then use Node inspector to debug your app as it runs. * * @stability 2 * @see http://sailsjs.com/documentation/reference/command-line-interface/sails-debug */ module.exports = function(cmd) { var extraArgs = cmd.parent.rawArgs.slice(3); var log = CaptainsLog(); // Use the app's local Sails in `node_modules` if one exists // But first make sure it'll work... var appPath = process.cwd(); var pathToSails = path.resolve(appPath, '/node_modules/sails'); if (!Sails.isLocalSailsValid(pathToSails, appPath)) { // otherwise, use the currently-running instance of Sails pathToSails = path.resolve(__dirname, './sails.js'); } console.log(); log.info('Running app in debug mode...'); log.info(chalk.grey('( to exit, type ' + '+' + ' )')); console.log(); // Spin up child process for Sails Womb.spawn('node', ['--debug', pathToSails, 'lift'].concat(extraArgs), { stdio: 'inherit' }); }; ================================================ FILE: bin/sails-deploy.js ================================================ /** * Module dependencies */ var path = require('path'); var rconf = require('../lib/app/configuration/rc')(); /** * `sails deploy` * * Deploy the Sails app in the current directory to a hosting provider. * * @stability 1 */ module.exports = function() { var commands = rconf.commands; var deploy = commands && commands.deploy; var modulePath = deploy && deploy.module; var module; // If no module path was specified, bail out if (!modulePath) { console.error('No module specified for the `deploy` command.'); console.error('To use `sails deploy`, set a `commands.deploy.module` setting in your .sailsrc file'); return; } // Attempt to require the specified module from the project node_modules folder try { module = require(path.resolve(process.cwd(), 'node_modules', modulePath)); } catch (unusedErr) { // FUTURE: provide access to error details instead of swallowing // If the module couldn't be required, bail out console.error('Could not require module at path: ' + modulePath + '. Please check the path and try again.'); return; }//• try { // Attempt to run the deploy command module({config: rconf}, function(err) { // If there were any issues, log them to the console. if (err) { console.error('=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-'); console.error('Deployment failed! Details below:'); console.error('=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-'); console.error(err); } }); } // Chances are we won't catch any errors internal to the deploy command here; // this would probably be an error at the top level of the deploy script. catch(e) { console.error('=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-'); console.error('Could not run deploy! Details below:'); console.error('=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-'); console.error(e); } }; ================================================ FILE: bin/sails-generate.js ================================================ /** * Module dependencies */ var util = require('util'); var path = require('path'); var assert = require('assert'); var _ = require('@sailshq/lodash'); var CaptainsLog = require('captains-log'); var sailsGen = require('sails-generate'); var package = require('../package.json'); var rconf = require('../lib/app/configuration/rc')(); /** * `sails generate` * * Generate one or more file(s) in our working directory. * This runs an appropriate generator. * * @see http://sailsjs.com/docs/reference/command-line-interface/sails-generate */ module.exports = function () { // Build initial scope for our call to sails-generate. var scope = { rootPath: process.cwd(), sailsRoot: path.resolve(__dirname, '..'), modules: {}, sailsPackageJSON: package, }; // Mix-in rc config // (note that we mix in everything namespaced under `generators` at the top level- // but also that anything at the top level takes precedence) _.merge(scope, rconf.generators); _.merge(scope, rconf); // Get a temporary logger just for use in `sails generate`. // > This is so that logging levels are configurable, even when a // > Sails app hasn't been loaded yet. var log = CaptainsLog(rconf.log); // Pass down the original serial args from the CLI. // > Note that (A) first, we remove the last arg from commander using `_.initial`, // > and then (B) second, we remove ANOTHER arg -- the one representing the // > generator type -- in favor of just setting `scope.generatorType`. var cliArguments = _.initial(arguments); scope.generatorType = cliArguments.shift(); scope.args = cliArguments; // If no generator type was defined, then log the expected usage. if (!scope.generatorType) { console.log('Usage: sails generate [something]'); return; } assert(arguments.length === (scope.args.length + 2), new Error('Consistency violation: Should have trimmed exactly two args.')); // Call out to `sails-generate`. return sailsGen(scope, { // Handle unexpected errors. error: function (err) { log.error(err); return process.exit(1); },// // Attend to invalid usage. invalid: function (err) { // If this is an Error, don't bother logging the stack, just log the `.message`. // (This is purely for readability.) if (_.isError(err)) { log.error(err.message); } else { log.error(err); } return process.exit(1); },// // Enjoy success. success: function (){ // Infer the `outputPath` if necessary/possible. if (!scope.outputPath && scope.filename && scope.destDir) { scope.outputPath = scope.destDir + scope.filename; } // Humanize the output path var humanizedPath; if (scope.outputPath) { humanizedPath = ' at ' + scope.outputPath; } else if (scope.destDir) { humanizedPath = ' in ' + scope.destDir; } else { humanizedPath = ''; } // Humanize the module identity var humanizedId; if (scope.id) { humanizedId = util.format(' ("%s")',scope.id); } else { humanizedId = ''; } // If this isn't the "new" generator, and we're not explicitly // asked not to, output a final success message. if (scope.generatorType !== 'new' && !scope.suppressFinalLog) { log.info(util.format( 'Created a new %s%s%s!', scope.generatorType, humanizedId, humanizedPath )); } }// });// }; ================================================ FILE: bin/sails-inspect.js ================================================ /** * Module dependencies */ var path = require('path'); var Womb = require('child_process'); var CaptainsLog = require('captains-log'); var chalk = require('chalk'); var Sails = require('../lib/app'); /** * `sails inspect` * * Attach the Node inspector and lift a Sails app. * You can then use Node inspector to debug your app as it runs. * */ module.exports = function(cmd) { var extraArgs = cmd.parent.rawArgs.slice(3); var log = CaptainsLog(); // Use the app's local Sails in `node_modules` if one exists // But first make sure it'll work... var appPath = process.cwd(); var pathToSails = path.resolve(appPath, '/node_modules/sails'); if (!Sails.isLocalSailsValid(pathToSails, appPath)) { // otherwise, use the currently-running instance of Sails pathToSails = path.resolve(__dirname, './sails.js'); } console.log(); log.info('Running app in inspect mode...'); if (process.version[1] >= 8) { log.info('In Google Chrome, go to chrome://inspect for interactive debugging.'); log.info('For other options, see the link below.'); } log.info(chalk.grey('( to exit, type ' + '+' + ' )')); console.log(); // Spin up child process for Sails Womb.spawn('node', ['--inspect', pathToSails, 'lift'].concat(extraArgs), { stdio: 'inherit' }); }; ================================================ FILE: bin/sails-lift.js ================================================ /** * Module dependencies */ var nodepath = require('path'); var _ = require('@sailshq/lodash'); var chalk = require('chalk'); var captains = require('captains-log'); var rconf = require('../lib/app/configuration/rc')(); var Sails = require('../lib/app'); var SharedErrorHelpers = require('../errors'); /** * `sails lift` * * Fire up the Sails app in our working directory, using the * appropriate version of Sails. * * > This uses the locally-installed Sails, if available. * > Otherwise, it uses the currently-running Sails (which, * > 99.9% of the time, is the globally-installed version.) * * @stability 3 * @see http://sailsjs.com/documentation/reference/command-line-interface/sails-lift */ module.exports = function() { // Get a temporary logger just for use in `sails lift`. // > This is so that logging levels are configurable, even when a // > Sails app hasn't been loaded yet. var cliLogger = captains(rconf.log); console.log(); cliLogger.info(chalk.grey('Starting app...')); console.log(); // Now grab our dictionary of configuration overrides to pass in // momentarily when we lift (or load) our Sails app. This is the // dictionary of configuration settings built from `.sailsrc` file(s), // command-line options, and environment variables. // (No need to clone, since it's not being used anywhere else) var configOverrides = rconf; // Determine whether to use the local or global Sails install. var sailsApp = (function _determineAppropriateSailsAppInstance(){ // Use the app's locally-installed Sails dependency (in `node_modules/sails`), // assuming it's extant and valid. // > Note that we always assume the current working directory to be the // > root directory of the app. var appPath = process.cwd(); var localSailsPath = nodepath.resolve(appPath, 'node_modules/sails'); if (Sails.isLocalSailsValid(localSailsPath, appPath)) { cliLogger.verbose('Using locally-installed Sails.'); cliLogger.silly('(which is located at `'+localSailsPath+'`)'); return require(localSailsPath); }// --• // Otherwise, since no workable locally-installed Sails exists, // run the app using the currently running version of Sails. // > This is probably always the global install. cliLogger.info('No local Sails install detected; using globally-installed Sails.'); return Sails(); })(); // Lift (or load) Sails (function _loadOrLift(proceed){ // If `--dontLift` was set, then use `.load()` instead. if (!_.isUndefined(configOverrides.dontLift)) { sailsApp.load(configOverrides, proceed); } // Otherwise, go with the default behavior (`.lift()`) else { sailsApp.lift(configOverrides, proceed); } })(function afterwards(err){// ~∞%° if (err) { return SharedErrorHelpers.fatal.failedToLoadSails(err); }// --• // If we made it here, the app is all lifted and ready to go. // The server will lower when the process is terminated-- either by a signal, // or via an uncaught fatal error. });// }; ================================================ FILE: bin/sails-migrate.js ================================================ /** * Module dependencies */ var nodepath = require('path'); var _ = require('@sailshq/lodash'); var captains = require('captains-log'); var rconf = require('../lib/app/configuration/rc')(); var Sails = require('../lib/app'); var SharedErrorHelpers = require('../errors'); /** * `sails migrate` * * Load (but don't lift) the Sails app in our working directory, using the * appropriate version of Sails, and skipping the Grunt hook. Then run the * app's bootstrap function, and simply exit. * * (Useful for quickly running auto-migrations by hand.) * * > This uses the locally-installed Sails, if available. * > Otherwise, it uses the currently-running Sails (which, * > 99.9% of the time, is the globally-installed version.) * * Example usage: * ``` * # Run "alter" auto-migrations to attempt to adjust all data * # (but possibly delete it) * sails migrate * * # Run "drop" auto-migrations to wipe all data * sails migrate --drop * ``` * * @stability EXPERIMENTAL * @see http://sailsjs.com/documentation/reference/command-line-interface/sails-migrate */ module.exports = function() { // Get a temporary logger just for use in this file. // > This is so that logging levels are configurable, even when a // > Sails app hasn't been loaded yet. var cliLogger = captains(rconf.log); cliLogger.warn('`sails migrate` is currently experimental.'); // Now grab our dictionary of configuration overrides to pass in // momentarily when we lift (or load) our Sails app. This is the // dictionary of configuration settings built from `.sailsrc` file(s), // command-line options, and environment variables. // (No need to clone, since it's not being used anywhere else) var configOverrides = rconf; // Determine whether to use the local or global Sails install. var sailsApp = (function _determineAppropriateSailsAppInstance(){ // Use the app's locally-installed Sails dependency (in `node_modules/sails`), // assuming it's extant and valid. // > Note that we always assume the current working directory to be the // > root directory of the app. var appPath = process.cwd(); var localSailsPath = nodepath.resolve(appPath, 'node_modules/sails'); if (Sails.isLocalSailsValid(localSailsPath, appPath)) { cliLogger.verbose('Using locally-installed Sails.'); cliLogger.silly('(which is located at `'+localSailsPath+'`)'); return require(localSailsPath); }// --• // Otherwise, since no workable locally-installed Sails exists, // run the app using the currently running version of Sails. // > This is probably always the global install. cliLogger.info('No local Sails install detected; using globally-installed Sails.'); return Sails(); })();//† // Skip the grunt hook. // (Note that we can't really use `sails.config.loadHooks` because we don't // know what kinds of stuff you might be relying on in your bootstrap function.) // // > FUTURE: if no orm hook actually installed, then fail with an error // > explaining you can't really run auto-migrations without that. configOverrides = _.extend(_.clone(configOverrides), { hooks: _.extend(configOverrides.hooks||{}, { grunt: false }), }); // Load the Sails app sailsApp.load(configOverrides, function(err) { if (err) { return SharedErrorHelpers.fatal.failedToLoadSails(err); }// --• // Run the app bootstrap sailsApp.runBootstrap(function afterBootstrap(err) { if (err) { sailsApp.log.error('Bootstrap function encountered an error during `sails migrate`: (see below)'); sailsApp.log.error(err); return; }// --• // Tear down the Sails app sailsApp.lower(); });//_∏_. });//_∏_ }; ================================================ FILE: bin/sails-new.js ================================================ /** * Module dependencies */ var nodepath = require('path'); var _ = require('@sailshq/lodash'); var sailsgen = require('sails-generate'); var CaptainsLog = require('captains-log'); var package = require('../package.json'); var rconf = require('../lib/app/configuration/rc')(); /** * `sails new` * * Generate a new Sails app. * * ``` * # In the current directory: * sails new * ``` * * ``` * # As a new directory or within an existing directory: * sails new foo * ``` * * @stability 3 * @see http://sailsjs.com/documentation/reference/command-line-interface/sails-new * ------------------------------------------------------------------------ * This command builds `scope` for the generator by scooping up any available * configuration using `rc` (merging config from env vars, CLI opts, and * relevant `.sailsrc` files). Then it runs the `sails-generate-new` * generator (https://github.com/balderdashy/sails-generate-new). */ module.exports = function () { // Build initial scope var scope = { rootPath: process.cwd(), modules: {}, sailsRoot: nodepath.resolve(__dirname, '..'), sailsPackageJSON: package, viewEngine: rconf.viewEngine }; // Support --template option for backwards-compat. if (!scope.viewEngine && rconf.template) { scope.viewEngine = rconf.template; } // Mix-in rconf _.merge(scope, rconf.generators); // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // FUTURE: Verify that we can just do a top-level merge here, // and then reference `scope.generators.modules` as needed // (would be simpler- but would be a breaking change, though // unlikely to affect most people. The same issue exists in // other places where we read rconf and then call out to // sails-generate) // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - _.merge(scope, rconf); // Get a temporary logger just for use in `sails new`. // > This is so that logging levels are configurable, even when a // > Sails app hasn't been loaded yet. var log = CaptainsLog(rconf.log); // Pass the original CLI arguments down to the generator // (but first, remove commander's extra argument) var cliArguments = Array.prototype.slice.call(arguments); cliArguments.pop(); scope.args = cliArguments; scope.generatorType = 'new'; return sailsgen(scope, { // Handle unexpected errors. error: function (err) { log.error(err); return process.exit(1); },// // Attend to invalid usage. invalid: function (err) { // If this is an Error, don't bother logging the stack, just log the `.message`. // (This is purely for readability.) if (_.isError(err)) { log.error(err.message); } else { log.error(err); } return process.exit(1); },// success: function() { // Good to go. } }); }; ================================================ FILE: bin/sails-run.js ================================================ /** * Module dependencies */ var path = require('path'); var fs = require('fs'); var _ = require('@sailshq/lodash'); var chalk = require('chalk'); var COMMON_JS_FILE_EXTENSIONS = require('common-js-file-extensions'); var flaverr = require('flaverr'); // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // Note that `whelk`, `machinepack-process`, and `../lib/app` are // conditionally required below, only in the cases where they are actually used. // (That way you don't have to wait for them to load if you're not using them.) // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /** * Module constants */ // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // Supported file extensions for imperative code files such as hooks: // • 'js' (.js) // • 'ts' (.ts) // • 'es6' (.es6) // • ...etc. // // > For full list, see: // > https://github.com/luislobo/common-js-file-extensions/blob/210fd15d89690c7aaa35dba35478cb91c693dfa8/README.md#code-file-extensions // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - var BASIC_SUPPORTED_FILE_EXTENSIONS = COMMON_JS_FILE_EXTENSIONS.code; /** * `sails run` * * Run a script for the Sails app in the current working directory. * * This matches either a JavaScript file in the `scripts/` directory, or one of the command-line scripts * declared within the `scripts: {}` dictionary in the package.json file. * * @see https://sailsjs.com/documentation/reference/command-line-interface/sails-run */ module.exports = function(scriptName) { // If there is only one argument, it means there is actually no scriptName at all. // (A detail of how commander works.) if (arguments.length === 1) { scriptName = undefined; } // Sanitize the script name, for comfort. if (!scriptName) { console.error('Which one? (To run a script, provide its name.)'); console.error('For example:'); console.error(' sails run rebuild-cloud-sdk'); console.error(); console.error('^ runs `scripts/rebuild-cloud-sdk.js`.'); console.error(); console.error('(For more help, visit '+chalk.underline('https://sailsjs.com/support')+'.)'); return process.exit(1); } // Remove `scripts/` prefix if it exists (allows users to do sails run scripts/foo, so they can use tab autocomplete). scriptName = _.trim(scriptName); scriptName = scriptName.replace(/^scripts\//, ''); // Unless the script name is under a "scope" (as in `@sailshq/some-package`), don't allow slashes in the name. if (scriptName.match(/\//) && scriptName[0] !== '@') { // FUTURE: Do allow this so scripts can be nested in subdirectories (doesn't work for package.json scripts obviously) console.error('Cannot run `'+scriptName+'`. Script name should never contain any slashes.'); return process.exit(1); }//-• // Examine the script name and determine if it has a file extension included. // If so, we'll rip it out of the script name, but keep a reference to it. // Otherwise, we'll always assume that we're looking for a normal `.js` file. var X_BASIC_SUPPORTED_FILE_EXTENSION = new RegExp('^([^.]+)\\.(' + BASIC_SUPPORTED_FILE_EXTENSIONS.join('|') + ')$'); var matchedFileExtension = scriptName.match(X_BASIC_SUPPORTED_FILE_EXTENSION); var fileExtension; if (matchedFileExtension) { fileExtension = matchedFileExtension[2]; scriptName = scriptName.replace(X_BASIC_SUPPORTED_FILE_EXTENSION, '$1'); } else { fileExtension = 'js'; } // First, we need to determine the appropriate script to run. // (either a terminal command or a specially-formatted Node.js/Sails.js module) // We begin by figuring out whether this is a script from the package.json // or a definition in the `scripts/` folder. var pjCommandToRun; // Check the package.json file. try { var pathToLocalPj = path.resolve(process.cwd(), 'package.json'); var packageJson; try { packageJson = require(pathToLocalPj); } catch (e) { switch (e.code) { case 'MODULE_NOT_FOUND': throw flaverr('E_NO_PACKAGE_JSON', new Error('No package.json file. Are you sure you\'re in the root directory of a Node.js/Sails.js app?')); default: throw e; } } if (!_.isUndefined(packageJson.scripts) && (!_.isObject(packageJson.scripts) || _.isArray(packageJson.scripts))) { throw flaverr('E_MALFORMED_PACKAGE_JSON', new Error('This package.json file has an invalid `scripts` property -- should be a dictionary (plain JS object).')); } pjCommandToRun = packageJson.scripts[scriptName]; } catch (e) { switch (e.code) { case 'E_NO_PACKAGE_JSON': case 'E_MALFORMED_PACKAGE_JSON': console.error('--'); console.error(chalk.red(e.message)); return process.exit(1); default: console.error('--'); console.error(chalk.bold('Oops, something unexpected happened:')); console.error(chalk.red(e.stack)); console.error('--'); console.error('Please read the error message above and troubleshoot accordingly.'); console.error('(You can report suspected bugs at '+chalk.underline('http://sailsjs.com/bugs')+'.)'); return process.exit(1); } } // Now check both the `scripts/` directory and node_modules to see if a matching script exists. var relativePathToAppScript = 'scripts/'+scriptName+'.'+fileExtension; var relativePathToInstalledScript = (function(){ // Handle scripts organized under org subdirectories in node_modules. var installedScriptName = scriptName; var org = ''; if (scriptName[0] === '@') { org = scriptName.split('/')[0] + '/'; installedScriptName = scriptName.split('/')[1]; } installedScriptName = installedScriptName.replace(/^sails-run-/,''); return 'node_modules/' + org + 'sails-run-'+installedScriptName; })(); var installedScriptExists = fs.existsSync(path.resolve(relativePathToInstalledScript)); var appScriptExists = fs.existsSync(path.resolve(relativePathToAppScript)); var doesScriptFileExist = appScriptExists || installedScriptExists; // Ensure that this script is not defined in BOTH places. if (pjCommandToRun && doesScriptFileExist) { console.error('Cannot run `'+scriptName+'` because it is too ambiguous.'); console.error('A script should only be defined once, but that script is defined in both the package.json file'); console.error('AND as a file in the `scripts/` directory.'); return process.exit(1); } // Ensure that this script exists one place or the other. if (!pjCommandToRun && !doesScriptFileExist) { console.error('Unknown script: `'+scriptName+'`'); console.error('No matching script is defined at `'+relativePathToAppScript+'`.'); console.error('(And there is no matching NPM script in the package.json file.)'); return process.exit(1); } // If this is a Node.js/Sails.js script (machine def), then require the script file // to get the module definition, then run it using MaS. if (!pjCommandToRun) { try { var pathToScriptDef = path.resolve(process.cwd(), appScriptExists ? relativePathToAppScript : relativePathToInstalledScript); var scriptDef; try { scriptDef = require(pathToScriptDef); } catch (e) { switch (e.code) { case 'MODULE_NOT_FOUND': throw flaverr('E_FAILED_TO_REQUIRE_SCRIPT_DEF', new Error('Encountered an error while loading the script definition. Are you sure this is a well-formed Node.js/Sails.js script definition? Error details:\n'+e.stack)); default: throw e; } } // Make sure the script is at least basically valid. // (MaS will check it more later -- this is just preliminary -- and also to make sure that it's not `{}`, // the special indicator that the script definition didn't export _ANYTHING_ at all.) if (!_.isObject(scriptDef) || _.isArray(scriptDef) || _.isEqual(scriptDef, {})) { console.error(''); console.error(''); console.error('Invalid script: `'+scriptName+'`'); console.error(''); console.error('A well-formed Node.js/Sails.js script should export a script definition.'); console.error('In other words, it should be defined more or less like this:'); console.error(''); console.error(' ```````````````````````````````````````````````````````````'); console.error(' module.exports = {'); console.error(' description: \'Do a thing given some stuff.\','); console.error(' inputs: {'); console.error(' someStuff: { type: \'string\', required: true }'); console.error(' },'); console.error(' fn: async function (inputs, exits) {'); console.error(' // ...'); console.error(' sails.log(\'Hello world!\');'); console.error(' return exits.success();'); console.error(' }'); console.error(' };'); console.error(' ```````````````````````````````````````````````````````````'); console.error(''); console.error(' [?] Visit https://sailsjs.com/support for assistance.'); console.error(''); return process.exit(1); } // Modify the script definition to add `sails: require('sails')` and `habitat: 'sails'` // (unless it explicitly disables this behavior with `sails: false` or by explicitly // declaring some other habitat) var isLifecycleMgmtExplicitlyDisabled = ( scriptDef.sails === false || (scriptDef.habitat !== undefined && scriptDef.habitat !== 'sails') ); if (!isLifecycleMgmtExplicitlyDisabled) { // (Only require the rest of the Sails framework if it's needed.) var Sails = require('../lib/app'); scriptDef.habitat = 'sails'; scriptDef.sails = Sails(); } // (Only require whelk if it's needed.) var whelk = require('whelk'); // console.log('process.argv ->', require('util').inspect(process.argv,{depth:null})); // console.log('arguments ->', require('util').inspect(arguments,{depth:null})); // console.log('scriptName ->', scriptName); // console.log('Array.prototype.slice.call(arguments, 1, -1) ->', Array.prototype.slice.call(arguments, 1, -1)); // Pass in override for runtime array of serial command-line arguments // (we rely on commander having parsed them for us so that we don't include `sails`, `run`, `node`, etc) scriptDef.rawSerialCommandLineArgs = Array.prototype.slice.call(arguments, 1, -1); // Now actually run the script. whelk(scriptDef); } catch (err) { console.error(err); return process.exit(1); } } // Otherwise, this is an NPM script of some kind, from the package.json file. else { // So execute the command like you would on the terminal. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // FUTURE: Consider pulling this boilerplate setup code into spawnChildProcess() machine // as a way of leveraging a subshell to remove the need to pass in CLI args directly. // Maybe as an option at least. // // > Also, we should also consider adding a notifier function to optionally provide // > special instructions of what to do when the current (parent) process receives a SIGINT. // > (Otherwise, by default, the SIGINT behavior implemented below could be used instead.) // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // // -AND/OR- // // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // FUTURE: Consider exposing an optional `onData` input to the more basic `executeCommand()` // machine. That way, you can just pass in a notifier function that handles the child's // writes to its stdout and stderr streams without having to dig into all of these // annoying complexities. Also, it'd then be possible to add another input: a flag that // allows you to choose whether or not to store output and pass it to the callback // (e.g. `bufferOutput`). // // > Finally, we should also consider adding the same SIGINT notifier function mentioned above. // // Here's an example of how we might put it all together: // ``` // Process.executeCommand({ // command: pjCommandToRun, // bufferOutput: false, // killOnParentSigint: false, // onData: function (data, stdStreamName){ // process[stdStreamName].write(data); // } // }).exec(function (err) { // if (err) { // console.error('Error occurred running `'+ pjCommandToRun+ '`'); // console.error('Please resolve any issues and try `sails run '+scriptName+'` again.'); // console.error('Details:'); // console.error(err); // return process.exit(1); // }//-• // // return process.exit(0); // });//< Process.executeCommand().exec() > _∏_ // ``` // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // (Only require machinepack-process if it's needed.) var Process = require('machinepack-process'); // Determine an appropriate name for our shell. // > This is mainly just so we don't have to try and do any fancy parsing of the command, // > allowing for more platform-specific customization. (Mirroring the NPM CLI here) var shProcName; var shFlag; if (process.platform === 'win32') { shProcName = process.env.comspec || 'cmd'; shFlag = '/d /s /c'; } else { shProcName = 'sh'; shFlag = '-c'; } var childProcess = Process.spawnChildProcess({ command: shProcName, cliArgs: [ shFlag, pjCommandToRun ] }).now(); // Pipe output from the child process to the current (parent) process. childProcess.stdout.pipe(process.stdout); childProcess.stderr.pipe(process.stderr); // Set up CTRL+C listener on the parent process that will force-kill this child process. // (Note that we define the event listener as a named function so we can unbind it below.) var onSigTerm = function (){ Process.killChildProcess({ childProcess: childProcess, force: true }).exec(function (_forceKillErr){ if (_forceKillErr) { console.error('There was a problem terminating this script:\n'+_forceKillErr.stack+'\nHere are some details which might be helpful:\n' + _forceKillErr.stack); } }); }; process.once('SIGTERM', onSigTerm); var spinlocked; (function (proceed){ childProcess.on('error', function (err) { return proceed(err); }); childProcess.stderr.on('error', function (err) { return proceed(err); }); childProcess.stdout.on('error', function (err) { return proceed(err); }); childProcess.on('close', function (code, signal) { // log.silly('lifecycle', logid(pkg, stage), 'Returned: code:', code, ' signal:', signal) // If a signal was received, terminate the current parent process (i.e. `sails run`). if (signal) { // Note that, in this case, `proceed()` is never called. // (But it doesn't actually matter, because we'll have killed the process.) return process.kill(process.pid, signal); } // Otherwise if we got a non-zero exit code, then consider this an error. if (code !== 0) { return proceed(new Error('Exit status '+code)); } // Otherwise, consider it a success. return proceed(); }); })(function(err){ if (err) { if (spinlocked) { console.error(err); return; } spinlocked = true; console.error('- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - '); console.error('Error occurred running `'+ pjCommandToRun+ '`'); console.error('Please resolve any issues and try `sails run '+scriptName+'` again.'); console.error('Details:'); console.error(err); process.removeListener('SIGTERM', onSigTerm); return process.exit(1); }//-• return process.exit(0); });//_∏_ (†) }// }; ================================================ FILE: bin/sails-upgrade.js ================================================ /** * Module dependencies */ var path = require('path'); var chalk = require('chalk'); var _ = require('@sailshq/lodash'); var sailsgen = require('sails-generate'); var flaverr = require('flaverr'); var semver = require('semver'); var rconf = require('../lib/app/configuration/rc')(); /** * `sails upgrade` * * Upgrade a pre v1.0.x app to Sails v1.0.x. * * ``` * # In the root directory of your Sails app: * sails upgrade * ``` */ module.exports = function () { if (!rconf.reportOnly) { console.log(chalk.gray('Checking compatibility for Sails v1.0 upgrade...')); try { var packageJson; try { var pathToLocalPackageJson = path.resolve(process.cwd(), 'package.json'); packageJson = require(pathToLocalPackageJson); } catch (e) { switch (e.code) { case 'MODULE_NOT_FOUND': throw flaverr('E_NO_PACKAGE_JSON', new Error('No package.json file. Are you sure you\'re in the root directory of a Sails app?')); default: throw e; } } if (_.isUndefined(packageJson.dependencies)) { throw flaverr('E_NO_SAILS_DEP', new Error('This package.json file does not declare any dependencies. Are you sure you\'re in the root directory of a Sails app?')); } if (!_.isObject(packageJson.dependencies) || _.isArray(packageJson.dependencies)) { throw flaverr('E_NO_SAILS_DEP', new Error('This package.json file has an invalid `dependencies` property -- should be a dictionary (plain JS object).')); } var sailsDepSVR = packageJson.dependencies.sails; if (!sailsDepSVR) { throw flaverr('E_NO_SAILS_DEP', new Error('This package.json file does not declare `sails` as a dependency. Are you sure you\'re in the root directory of a Sails app?')); } if (!semver.ltr('0.9.9999', sailsDepSVR)) { throw flaverr('E_SAILS_DEP_DEFINITELY_TOO_OLD', new Error('this app depends on sails@'+sailsDepSVR+'.')); } if (!semver.ltr('0.11.9999', sailsDepSVR)) { throw flaverr('E_SAILS_DEP_MIGHT_BE_TOO_OLD', new Error('this app depends on sails@'+sailsDepSVR+'.')); } // if (semver.ltr('0.12.9999', sailsDepSVR)) { // throw flaverr('E_SAILS_DEP_IS_ALREADY_V1', new Error('this app already depends on sails@'+sailsDepSVR+'...')); // } console.log(); console.log('----------------------------------------------------'); console.log('This utility will kickstart the process of migrating'); console.log('this Sails v0.12.x app to Sails v1.'); console.log('----------------------------------------------------'); console.log(); } catch (e) { switch (e.code) { case 'E_SAILS_DEP_IS_ALREADY_V1': console.log(); console.log('----------------------------------------------------'); console.log('This utility is designed to kickstart the process of'); console.log('migrating a '+chalk.bold('v0.12.x')+' app to Sails v1.'); console.log(); console.log(chalk.yellow.bold('But '+e.message)); console.log(chalk.reset('Maybe you already started upgrading it?')); console.log(chalk.reset('If so, then please press CTRL+C to cancel now, or')); console.log(chalk.reset('otherwise feel free to proceed with care-- this')); console.log(chalk.reset('upgrade tool may still work partially as-is.')); console.log(chalk.gray('For more help, visit '+chalk.underline('http://sailsjs.com/support')+'.')); console.log('----------------------------------------------------'); console.log(); break; case 'E_SAILS_DEP_MIGHT_BE_TOO_OLD': console.log(); console.log('----------------------------------------------------'); console.log('This utility is designed to kickstart the process of'); console.log('migrating a '+chalk.bold('v0.12.x')+' app to Sails v1.'); console.log(); console.log(chalk.yellow.bold('But '+e.message)); console.log(chalk.reset('This upgrade tool may partially work as-is, but we recommend')); console.log(chalk.reset('using the appropriate guide(s) to upgrade to Sails v0.12 first.')); console.log(chalk.reset('See '+chalk.underline('http://sailsjs.com/upgrading')+' for details.')); console.log(chalk.gray('(Press CTRL+C to cancel -- or proceed at your own risk!)')); console.log('----------------------------------------------------'); console.log(); break; case 'E_SAILS_DEP_DEFINITELY_TOO_OLD': console.log('--'); console.log(chalk.red.bold('Well, '+e.message)); console.log(chalk.reset('It looks to be built for a version of Sails that is probably too')); console.log(chalk.reset('old to work with this upgrade tool as-is. We recommend using')); console.log(chalk.reset('the appropriate guide(s) to upgrade to Sails v0.12 first.')); console.log(chalk.gray('For more assistance, visit '+chalk.underline('http://sailsjs.com/support')+' or, if')); console.log(chalk.gray('you\'re using Sails Flagship, '+chalk.underline('https://flagship.sailsjs.com')+'.')); return process.exit(1); case 'E_NO_PACKAGE_JSON': case 'E_NO_SAILS_DEP': console.log('--'); console.log(chalk.red(e.message)); return process.exit(1); default: console.log('--'); console.log(chalk.bold('Oops, something unexpected happened:')); console.log(chalk.red(e.stack)); console.log('--'); console.log('Please read the error message above and troubleshoot accordingly.'); console.log('(You can report suspected bugs at '+chalk.underline('http://sailsjs.com/bugs')+'.)'); return process.exit(1); } } } // Attempt to require the upgrade tool. var generator; try { var requirePath = path.resolve(process.cwd(), 'node_modules/@sailshq/upgrade'); generator = require(requirePath); } catch (e) { if (e.code === 'MODULE_NOT_FOUND') { console.log(chalk.blue.bold('Could not find the `@sailshq/upgrade` package in your local app folder.')); console.log('Please run `npm install @sailshq/upgrade` and try again.'); console.log(chalk.gray('(Or just use the Sails v1.0.x upgrade guide on sailsjs.com.)')); console.log(); return process.exit(1); }//-• // Some other unexpected error from within this package: console.log(chalk.bold('Oops, something unexpected happened:')); console.log(chalk.red(e.stack)); console.log('--'); console.log('Please report this bug at '+chalk.underline('https://flagship.sailsjs.com')+'.'); process.exit(1); }// // Build initial scope var scope = { rootPath: process.cwd(), sailsRoot: path.resolve(__dirname, '..'), generatorType: 'upgrade', modules: { upgrade: generator }, }; // Mix-in rconf _.merge(scope, rconf.generators); // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // FUTURE: Verify that we can just do a top-level merge here, // and then reference `scope.generators.modules` as needed // (would be simpler- but would be a breaking change, though // unlikely to affect most people. The same issue exists in // other places where we read rconf and then call out to // sails-generate) // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - _.merge(scope, rconf); // Pass the original CLI arguments down to the generator // (but first, remove commander's extra argument) var cliArguments = Array.prototype.slice.call(arguments); cliArguments.pop(); scope.args = cliArguments; return sailsgen(scope, { // Handle unexpected errors. error: function (err) { console.log(chalk.bold('Oops, something unexpected happened:')); console.log(chalk.red(err.stack)); console.log('--'); console.log('Please report this bug at '+chalk.underline('https://flagship.sailsjs.com')+'.'); return process.exit(1); },// // Attend to invalid usage. invalid: function (err) { // If this is an Error, don't bother logging the other details, just log the `.message`. // (This is purely for readability.) if (_.isError(err)) { console.log(err.message); } else { console.log(err); } console.log('--'); console.log('For assistance, visit '+chalk.underline('https://flagship.sailsjs.com')+'.'); return process.exit(1); },// success: function() { // Good to go. } }); }; ================================================ FILE: bin/sails-www.js ================================================ /** * Module dependencies */ var path = require('path'); var _ = require('@sailshq/lodash'); var CaptainsLog = require('captains-log'); var Process = require('machinepack-process'); var chalk = require('chalk'); var flaverr = require('flaverr'); var rconf = require('../lib/app/configuration/rc')(); /** * `sails www` * * Run the `build` or `buildProd` Grunt task (depending on whether this is the production environment) * for the Sails app in the current working directory. * * @see http://sailsjs.com/documentation/reference/command-line-interface/sails-www */ module.exports = function() { // Check compatibility try { var pathToLocalPackageJson = path.resolve(process.cwd(), 'package.json'); var packageJson; try { packageJson = require(pathToLocalPackageJson); } catch (e) { switch (e.code) { case 'MODULE_NOT_FOUND': throw flaverr('E_NO_PACKAGE_JSON', new Error('No package.json file. Are you sure you\'re in the root directory of a Sails app?')); default: throw e; } } if (_.isUndefined(packageJson.dependencies)) { throw flaverr('E_NO_SAILS_DEP', new Error('This package.json file does not declare any dependencies. Are you sure you\'re in the root directory of a Sails app?')); } if (!_.isObject(packageJson.dependencies) || _.isArray(packageJson.dependencies)) { throw flaverr('E_NO_SAILS_DEP', new Error('This package.json file has an invalid `dependencies` property -- should be a dictionary (plain JS object).')); } var sailsDepSVR = packageJson.dependencies.sails; if (!sailsDepSVR) { throw flaverr('E_NO_SAILS_DEP', new Error('This package.json file does not declare `sails` as a dependency.\nAre you sure you\'re in the root directory of a Sails app?')); } var shGruntDepSVR = packageJson.dependencies['sails-hook-grunt'] || packageJson.devDependencies['sails-hook-grunt']; if (!shGruntDepSVR) { throw flaverr('E_NO_SH_GRUNT_DEP', new Error('This app\'s package.json file does not declare `sails-hook-grunt` in "dependencies" or "devDependencies".\nAre you sure this is a Sails v1.0 app that is using Grunt?')); } } catch (e) { switch (e.code) { case 'E_NO_PACKAGE_JSON': case 'E_NO_SAILS_DEP': console.log('--'); console.log(chalk.red(e.message)); return process.exit(1); case 'E_NO_SH_GRUNT_DEP': console.log('--'); console.log(chalk.red(e.message)); console.log(chalk.gray('(Maybe try running `npm install sails-hook-grunt --save`?)')); return process.exit(1); default: console.log('--'); console.log(chalk.bold('Oops, something unexpected happened:')); console.log(chalk.red(e.stack)); console.log('--'); console.log('Please read the error message above and troubleshoot accordingly.'); console.log('(You can report suspected bugs at '+chalk.underline('http://sailsjs.com/bugs')+'.)'); return process.exit(1); } } var log = CaptainsLog(rconf.log); // The destination path. var wwwPath = path.resolve(process.cwd(), 'www'); // Determine the appropriate Grunt task to run based on `process.env.NODE_ENV`, `rconf.prod`, and `rconf.environment`. var overrideGruntTask; if (rconf.prod || rconf.environment === 'production' || process.env.NODE_ENV === 'production') { overrideGruntTask = 'buildProd'; } else { overrideGruntTask = 'build'; } log.info('Compiling assets into standalone directory with `grunt ' + overrideGruntTask + '`...'); // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // Execute a command like you would on the terminal. Process.executeCommand({ command: path.join('node_modules', '.bin', 'grunt')+' '+overrideGruntTask, }).exec(function (err) { if (err) { log.error('Error occured running `grunt ' + overrideGruntTask + '`'); log.error('Please resolve any issues and try running `sails www` again.'); log.error('Hint: you must have the Grunt CLI installed! Try `npm install grunt -g`.'); log.error(); log.error('Error details:'); log.error(err); return process.exit(1); } log.info(); log.info('Created directory of compiled assets at:'); log.info(wwwPath); return process.exit(0); });// }; ================================================ FILE: bin/sails.js ================================================ #!/usr/bin/env node /** * Module dependencies */ var _ = require('@sailshq/lodash'); var program = require('./private/patched-commander'); var sailsPackageJson = require('../package.json'); var NOOP = function() {}; program .version(sailsPackageJson.version, '-v, --version'); // // Normalize version argument, i.e. // // $ sails -v // $ sails -V // $ sails --version // $ sails version // // make `-v` option case-insensitive process.argv = _.map(process.argv, function(arg) { return (arg === '-V') ? '-v' : arg; }); // $ sails version (--version synonym) program .command('version') .description('') .action(program.versionInformation); program .unknownOption = NOOP; program.usage('[command]'); // $ sails lift var cmd; cmd = program.command('lift'); cmd.option('--prod', 'Lift in "production" environment.'); cmd.option('--staging', 'Lift in "staging" environment.'); cmd.option('--port [port]', 'Listen on the specified port (defaults to 1337).'); cmd.option('--silent', 'Set log level to "silent".'); cmd.option('--verbose', 'Set log level to "verbose".'); cmd.option('--silly', 'Set log level to "silly".'); cmd.unknownOption = NOOP; cmd.description(''); cmd.alias('l'); cmd.action(require('./sails-lift')); // $ sails new cmd = program.command('new [path_to_new_app]'); // cmd.option('--dry'); cmd.option('--no-frontend', 'Don\'t generate "assets", "views" or "task" folders.'); cmd.option('--fast', 'Don\'t install node modules after generating app.'); cmd.usage('[path_to_new_app]'); cmd.unknownOption = NOOP; cmd.description(''); cmd.action(require('./sails-new')); // $ sails generate cmd = program.command('generate'); // cmd.option('--dry'); cmd.unknownOption = NOOP; cmd.description(''); cmd.usage('[something]'); cmd.action(require('./sails-generate')); // $ sails upgrade cmd = program.command('upgrade'); cmd.unknownOption = NOOP; cmd.description(''); cmd.action(require('./sails-upgrade')); // $ sails migrate cmd = program.command('migrate'); cmd.unknownOption = NOOP; cmd.description(''); cmd.action(require('./sails-migrate')); // $ sails console cmd = program.command('console'); cmd.option('--silent', 'Set log level to "silent".'); cmd.option('--verbose', 'Set log level to "verbose".'); cmd.option('--silly', 'Set log level to "silly".'); cmd.option('--dontLift', 'Start console session without lifting an HTTP server.'); cmd.unknownOption = NOOP; cmd.description(''); cmd.alias('c'); cmd.action(require('./sails-console')); // $ sails www // Compile `assets` directory into a standalone `www` folder. cmd = program.command('www'); cmd.unknownOption = NOOP; cmd.description(''); cmd.action(require('./sails-www')); // $ sails debug cmd = program.command('debug'); cmd.unknownOption = NOOP; cmd.description('(for Node v5 and below)'); cmd.action(require('./sails-debug')); // $ sails inspect cmd = program.command('inspect'); cmd.unknownOption = NOOP; cmd.description('(for Node v6 and above)'); cmd.action(require('./sails-inspect')); // $ sails run cmd = program.command('run'); cmd.usage('[name-of-script]'); cmd.unknownOption = NOOP; cmd.description(''); cmd.action(require('./sails-run')); // $ sails test cmd = program.command('test'); cmd.unknownOption = NOOP; cmd.description(''); cmd.action(function(){ require('./sails-run')('test', _.last(arguments)); }); // $ sails lint cmd = program.command('lint'); cmd.unknownOption = NOOP; cmd.description(''); cmd.action(function(){ require('./sails-run')('lint', _.last(arguments)); }); // - - - - - - - - - - - - - - - - - - - - - - - - - - - // $ sails deploy cmd = program.command('deploy'); // cmd.option('--dry'); cmd.unknownOption = NOOP; cmd.description(''); cmd.usage(''); cmd.action(require('./sails-deploy')); // FUTURE: ^^ Consider simplifying this into a script. // - - - - - - - - - - - - - - - - - - - - - - - - - - - // $ sails debug-console cmd = program.command('debug-console'); cmd.unknownOption = NOOP; cmd.description(''); cmd.alias('dc'); cmd.action(require('./sails-debug-console')); // // Normalize help argument, i.e. // // $ sails --help // $ sails help // $ sails // $ sails // // $ sails help (--help synonym) cmd = program.command('help [command]'); cmd.description(''); cmd.action(function(){ if (program.args.length > 1 && _.isString(program.args[0])) { var helpCmd = _.find(program.commands, {_name: program.args[0]}); if (helpCmd) { helpCmd.help(); return; } } program.help(); }); // $ sails // Output Sails help when an unrecognized command is used. program .command('*') .action(function(cmd){ console.log('\n ** Unrecognized command:', cmd, '**'); program.help(); }); // Don't balk at unknown options program.unknownOption = NOOP; // $ sails // program.parse(process.argv); var NO_COMMAND_SPECIFIED = program.args.length === 0; if (NO_COMMAND_SPECIFIED) { program.help(); } ================================================ FILE: docs/PAGE_NEEDED.md ================================================ # Page Needed If you’re seeing this page, it means you've clicked on a link to a Sails doc that has yet to be written. Help make Sails better by contributing to the docs! Please send a pull request to master with corrections/additions and they'll be double-checked and merged as soon as possible. Secondly, we are open to suggestions about the process we're using to manage our documentation, and to work with the community in general. Please post to the Google Group with your ideas, or if you're interested in helping directly, contact @fancydoilies, @aaaaanxiety, or @mikermcneil on Twitter. Love, The Sails Team ================================================ FILE: docs/README.md ================================================ ![Squiddy reads the docs](https://sailsjs.com/images/squidford_swimming.png) # Sails.js Documentation The official documentation for the current stable release of Sails is on the master branch of this repository. Content for most sections on the [official Sails website](https://sailsjs.com) is compiled from here. ## In other languages The documentation for Sails has been translated to a number of different languages. The list below is a reference of the translation projects we are aware of. | Language | [IETF Language Tag](https://en.wikipedia.org/wiki/IETF_language_tag) | Version | Maintainer(s) | Repo | | ---------------------------- | ------- | ------- | ------------------ | ---------------------------------- | | Brazilian Portuguese | `pt-BR` | **v1.0.x** | [@Avlye](https://github.com/Avlye) | [sails-docs-pt-BR](https://github.com/Avlye/sails-docs-pt-BR) | Chinese | `zh-cn` | v0.12.x | [@linxiaowu66](https://github.com/linxiaowu66) | [sails-docs-zh-cn](https://github.com/linxiaowu66/sails-docs-zh-cn) | French | `fr` | v0.12.x | [@marrouchi](https://github.com/marrouchi) | [sails-docs-fr](https://github.com/marrouchi/sails-docs-fr) | Spanish | `es` | v0.12.x | [@eduartua](https://github.com/eduartua/) & [@alejandronanez](https://github.com/alejandronanez) | [sails-docs-es](https://github.com/eduartua/sails-docs-es) | Japanese | `ja` | v0.11.x | [@kory-yhg](https://github.com/kory-yhg) | [sails-docs-ja](https://github.com/balderdashy/sails-docs/tree/ja) | Brazilian Portuguese | | v0.10.x | [@marceloboeira](https://github.com/marceloboeira) | [sails-docs-pt-BR](https://github.com/balderdashy/sails-docs/tree/pt-BR) | Korean | `ko` | v0.10.x | [@sapsaldog](https://github.com/sapsaldog) | [sails-docs-ko](https://github.com/balderdashy/sails-docs/tree/ko) | Taiwanese Mandarin | `zh-TW` | v0.10.x | [@CalvertYang](https://github.com/CalvertYang) | [sails-docs-zh-TW](https://github.com/balderdashy/sails-docs/tree/zh-TW) > Since we are now using branches to keep track of different versions of the Sails documentation, we are moving away from the original approach of using branches for different languages. Before embarking on a new translation project, we ask that you review the [updated information below](#how-can-i-help-translate-the-documentation)-- the process has changed a little bit. ## Contributing to the Sails docs We welcome your help! Please send a pull request with corrections/additions and they'll be double-checked and merged as soon as possible. #### How are these docs compiled and pushed to the website? We use a module called `doc-templater` to convert the .md files to the html for the website. You can learn more about how it works in [the doc-templater repo](https://github.com/uncletammy/doc-templater). Each .md file has its own page on the website (i.e. all reference, concepts, and anatomy files), and should include a special `` tag with a `value` property specifying the title for the page. This will impact how the doc page appears in search engine results, and it will also be used as its display name in the navigation menu on sailsjs.com. For example: ```markdown ``` #### When will my change appear on the Sails website? Once your change to the documentation is merged, you can see how it will appear on sailsjs.com by visiting [next.sailsjs.com](https://next.sailsjs.com). The preview site updates itself automatically as changes are merged. #### How can I help translate the documentation? A great way to help the Sails project, especially if you speak a language other than English natively, is to volunteer to translate the Sails documentation. If you are interested in collaborating with any of the translation projects listed in the table above, contact the maintainer of the translation project using the instructions in the README of that fork. If your language is not represented in the table above, and you are interested in beginning a translation project, follow these steps: + Bring the documentation folder (`balderdashy/sails/docs`) into a new repo named `sails-docs-{{IETF}}` where {{IETF}} is the [IETF language tag](https://en.wikipedia.org/wiki/IETF_language_tag) for your language. + Edit the README to summarize your progress so far, provide any other information you think would be helpful for others reading your translation, and let interested contributors know how to contact you. + Send a pull request editing the table above to add a link to your fork. + When you are satisfied with the first complete version of your translation, open an issue and someone from our docs team will be happy to help you get preview it in the context of the Sails website, get it live on a domain (yours, or a subdomain of sailsjs.com, whichever makes the most sense), and share it with the rest of the Sails community. #### How else can I help? For more information on contributing to Sails in general, see the [Contribution Guide](sailsjs.com/contributing). ## License [MIT](https://sailsjs.com/license) The [Sails framework](https://sailsjs.com) is free and open-source under the [MIT License](https://sailsjs.com/license). ================================================ FILE: docs/anatomy/.editorconfig.md ================================================ # .editorconfig This file exists to help maintain consistent formatting throughout the files in your Sails app. For more information, see [editorconfig.org](http://editorconfig.org/). ================================================ FILE: docs/anatomy/.eslintignore.md ================================================ # .eslintignore This file exists to signify to [ESLint](https://eslint.org/) that certain files and/or directories should be ignored for the purposes of linting. ================================================ FILE: docs/anatomy/.eslintrc.md ================================================ # .eslintrc This file defines a set of basic code conventions designed to encourage quality and consistency across your Sails app's code base. For more information, see [eslint.org](https://eslint.org/). ================================================ FILE: docs/anatomy/.htmlhintrc.md ================================================ # .htmlhintrc This file defines the rules for your app's [HTMLHint](http://htmlhint.com/), to encourage quality and consistency in your views and templates. ================================================ FILE: docs/anatomy/Gruntfile.js.md ================================================ # Gruntfile.js Sails uses [Grunt](http://gruntjs.com) for asset management. This file contains the entry point for the default asset pipeline in Sails; that is, the code that does stuff like compiling LESS stylesheets, minifying scripts for production, and precompiling and injecting client-side templates. Sails' integration with Grunt is fully customizable, but for most use cases, this file (`Gruntfile.js`) should remain unchanged. Instead, you can install Grunt plugins or add your own custom logic as new files in the [`tasks/`](./tasks) folder. > + To learn more about working with static assets in Sails, check out the [conceptual documentation on assets](https://sailsjs.com/documentation/concepts/assets). > + For a broader introduction to Grunt tasks in general, see [Grunt's docs on configuring tasks](http://gruntjs.com/configuring-tasks). ================================================ FILE: docs/anatomy/README.md ================================================ # docs/anatomy This section contains the "Anatomy" documentation which is eventually available at https://sailsjs.com/documentation/anatomy. ### Notes > - This README file **is not compiled to HTML** for the website. It is just here to explain what you're looking at. > - Depending on what branch of `sails` you are currently viewing, the domain may vary. See the top-level documentation README file for information about working with the markdown files in this repo, and to understand the branching/versioning strategy. ================================================ FILE: docs/anatomy/README.md.md ================================================ # README.md This is a generic README that you can edit to describe your app. ================================================ FILE: docs/anatomy/anatomy.md ================================================ # Anatomy of a Sails app An interactive guide to the structure of the Sails app generated by default with `sails new`. Choose from any of the files or folders in the list to learn more about its purpose. ================================================ FILE: docs/anatomy/api/api.md ================================================ # api/ This folder contains the vast majority of your app's back-end logic. It is home to the 'M' and 'C' in MVC Framework. In it you will find the following: - Controllers: [Actions](https://sailsjs.com/documentation/concepts/actions-and-controllers) contain back-end logic that handle incoming requests (like handling a form submission or responding with personalized, server-rendered HTML). - Helpers: [Helpers](https://sailsjs.com/documentation/concepts/helpers) are shared functions that can be called from anywhere in your app. - Models: [Models](https://sailsjs.com/documentation/concepts/models-and-orm) are the structures that contain data for your Sails App. - Policies: [Policies](https://sailsjs.com/documentation/concepts/policies) are middleware that restrict access to certain actions in your app. You may also find these folders, which are not always generated by default in new Sails apps: - Hooks: [Hooks](https://sailsjs.com/documentation/concepts/extending-sails/hooks) are modules that add functionality to Sails core. You can use hooks to run custom code when your app lifts and before handling every incoming request. Hooks can also be installed as plugins, but the hooks in this folder are always custom for your application. - Responses: [Custom responses](https://sailsjs.com/documentation/concepts/extending-sails/custom-responses) can help maintain consistent HTTP status codes and behavior across your app. (Since not every Sails application needs to define its own custom responses, this folder is sometimes excluded.) - Services: [Services](https://sailsjs.com/documentation/concepts/services) are shared utilities common in Sails apps written before version 1.0. They can be _just about anything_, so for new apps, it's recommended that you use [helpers](https://sailsjs.com/documentation/concepts/helpers) instead. ================================================ FILE: docs/anatomy/api/controllers/controllers.md ================================================ # api/controllers/ This is the directory that holds your controllers. In Sails, controllers are JavaScript files that contain logic for interacting with models and rendering appropriate views to the client. When you call `sails generate api cats` via the command line from inside your project's root directory, Sails will generate the file `api/controllers/CatsController.js` along with a matching model. The `api/controllers` directory can also contain _standalone actions_, which are JavaScript files containing a _single_ controller action, rather than a dictionary of actions. See the [main actions and controllers documentation](https://sailsjs.com/documentation/concepts/actions-and-controllers) for more info. ================================================ FILE: docs/anatomy/api/controllers/gitkeep.md ================================================ # api/controllers/.gitkeep Ignore this file. It only exists because `git` refuses to push empty directories to a remote server. `.gitkeep` is an unofficial convention that has emerged as a workaround for people who don't discriminate against empty directories. ================================================ FILE: docs/anatomy/api/helpers/.gitkeep.md ================================================ # api/helpers/.gitkeep Ignore this file. It only exists because `git` refuses to push empty directories to a remote server. `.gitkeep` is an unofficial convention that has emerged as a workaround for people who don't discriminate against empty directories. ================================================ FILE: docs/anatomy/api/helpers/helpers.md ================================================ # api/helpers/ This is the directory that holds your helpers. In Sails, helpers are shared functions that can be called from anywhere in your app. When you call `sails generate helper tickle-user` via the command line from inside your project's root directory, Sails will generate the file `api/helpers/tickle-user.js`, with a skeleton helper file to get you started. See the [main helpers documentation](https://sailsjs.com/documentation/concepts/helpers) for more info. ================================================ FILE: docs/anatomy/api/models/.gitkeep.md ================================================ # api/models/.gitkeep Ignore this file. It only exists because `git` refuses to push empty directories to a remote server. `.gitkeep` is an unofficial convention that has emerged as a workaround for people who don't discriminate against empty directories. ================================================ FILE: docs/anatomy/api/models/models.md ================================================ # api/models/ This is the directory that holds your models. In Sails, models are the structures that contain data for your Sails App. You can learn more about how to define and use models in [Concepts > Models and ORM > Models](https://sailsjs.com/documentation/concepts/models-and-orm/models), and about how to generate them [here](https://sailsjs.com/documentation/reference/command-line-interface/sails-generate#?core-generators). ================================================ FILE: docs/anatomy/api/policies/.gitkeep.md ================================================ # api/policies/.gitkeep Ignore this file. It only exists because `git` refuses to push empty directories to a remote server. `.gitkeep` is an unofficial convention that has emerged as a workaround for people who don't discriminate against empty directories. ================================================ FILE: docs/anatomy/api/policies/policies.md ================================================ # api/policies/ This is the folder you will store your “policy” files in. A policy file is a JavaScript file that contains what is essentially Express middleware for authenticating access to controller actions in your app. For example, if you want to make sure only authenticated admin users can access `http://yourapp.com/admin/dashboard`, this is the folder you would put that logic in. For more information about policies and how to use them in your app, see [Concepts > Policies](https://sailsjs.com/documentation/concepts/policies). ================================================ FILE: docs/anatomy/app.js.md ================================================ # app.js This file is the conventional entry point for a _production_ Sails/Node.js app. When developing on your local computer, and you run `sails lift`, the code in `app.js` is not executed. Instead, this file exists to provide an easy, out-of-the-box way to run your app _without_ typing `sails lift`. This is most likely how you'll start your app in production (i.e. `node app`, or `npm start`). For example, when you deploy to most PaaS vendors like [Heroku](http://heroku.com), they will automatically detect that you're running a Sails/Node.js app and execute this file with the `NODE_ENV` environment variable set to production. > Whatever stage of the development lifecycle you're at, you can safely ignore `app.js`. It's good to go out of the box for most apps. But the code in `app.js` also serves as an easy-to-reference example of how to use Sails programmatically. So you might want to take a look at it if you plan on writing automated tests, scheduled jobs, manual database migrations, or administration scripts. ================================================ FILE: docs/anatomy/assets/.eslintrc.md ================================================ # assets/.eslintrc This file is for [ESLint](https://eslint.org/) configuration overrides for the `assets/` directory. These override the code conventions defined in the top-level [`.eslintrc`](https://sailsjs.com/documentation/anatomy/.eslintrc), to allow for variations between front-end JavaScript code vs. backend code designed to run in a Node.js/Sails process. ================================================ FILE: docs/anatomy/assets/assets.md ================================================ # assets/ This is your assets folder. It houses all of the static files that your app will need to host. Feel free to create your own files and folders in here. Upon lifting, a file called `assets/newFolder/data.txt` could be accessed at `http://localhost:1337/newFolder/data.txt`. ================================================ FILE: docs/anatomy/assets/dependencies/dependencies.md ================================================ # assets/dependencies/ As a rule of thumb, if it's code written by you or someone on your team, it _does not belong in this folder._ Instead, `assets/dependencies/` is for your client-side dependencies such as Vue.js, Bootstrap, or jQuery. This folder can include client-side JavaScript files, stylesheets, and even images. (See the "Web App" template for an example.) JavaScript files and stylesheets in the `assets/dependencies/` folder are loaded first, before your other assets. This conventional behavior is orchestrated by [tasks/pipeline.js](https://sailsjs.com/documentation/anatomy/tasks/pipeline.js), so head over there if you need to tweak this behavior (for example, if some of your client-side dependencies need to load before others.) ================================================ FILE: docs/anatomy/assets/dependencies/sails.io.js.md ================================================ # assets/dependencies/sails.io.js This file adds a few custom methods to socket.io which provide the "built-in" websockets functionality for Sails. Specifically, those methods allow you to send and receive Socket.IO messages to and from Sails by simulating a REST client interface on top of Socket.IO. It models its API after the $.ajax pattern from jQuery which you might be familiar with. See the [Socket client reference](https://sailsjs.com/documentation/reference/web-sockets/socket-client) for more info about using the methods that this file provides. ================================================ FILE: docs/anatomy/assets/favicon.ico.md ================================================ # assets/favicon.ico This file is the [Favicon](http://en.wikipedia.org/wiki/Favicon) for your app. ================================================ FILE: docs/anatomy/assets/images/gitkeep.md ================================================ # assets/images/.gitkeep Ignore this file. It only exists because `git` refuses to push empty directories to a remote server. `.gitkeep` is an unofficial convention that has emerged as a workaround for people who don't discriminate against empty directories. ================================================ FILE: docs/anatomy/assets/images/images.md ================================================ # assets/images/ This is where you should put image files that need to be statically hosted by your app. Upon lifting your app, an image called `omgCat.jpg` could be found at `http://localhost:1337/images/omgCat.jpg` ================================================ FILE: docs/anatomy/assets/js/gitkeep.md ================================================ # assets/js/.gitkeep Ignore this file. It only exists because `git` refuses to push empty directories to a remote server. `.gitkeep` is an unofficial convention that has emerged as a workaround for people who don't discriminate against empty directories. ================================================ FILE: docs/anatomy/assets/js/js.md ================================================ # assets/js/ This is where you put client-side JavaScript files that you want to be statically hosted by your app. Sails puts a few in there for making communication via socket.io easier. ================================================ FILE: docs/anatomy/assets/styles/importer.less.md ================================================ # assets/styles/importer.less By default, new Sails projects are configured to compile this file from LESS to CSS. Unlike CSS files, LESS files are not compiled and included automatically unless they are imported here. The LESS files imported in this file are compiled and included in the order they are listed. Mixins, variables, etc. should be imported first so that they can be accessed by subsequent LESS stylesheets. (Just like the rest of the asset pipeline bundled in Sails, you can always omit, customize, or replace this behavior with SASS, SCSS, or any other Grunt tasks you like.) ================================================ FILE: docs/anatomy/assets/styles/styles.md ================================================ # assets/styles/ This is where you will put all of the .css files that you would like to be statically hosted by your app. ================================================ FILE: docs/anatomy/assets/templates/gitkeep.md ================================================ # assets/templates/.gitkeep Ignore this file. It only exists because `git` refuses to push empty directories to a remote server. `.gitkeep` is an unofficial convention that has emerged as a workaround for people who don't discriminate against empty directories. ================================================ FILE: docs/anatomy/assets/templates/templates.md ================================================ # assets/templates/ Client-side HTML templates are important prerequisites for certain types of modern, rich client applications built for browsers; particularly [SPAs](https://en.wikipedia.org/wiki/Single-page_application). To work their magic, frameworks like Backbone, Angular, Ember, and Knockout require that you load templates client-side; completely separate from your traditional [server-side views](https://sailsjs.com/documentation/concepts/views). Out of the box, new Sails apps support the best of both worlds. Whether or not you use client-side templates in your app and where you put them is, of course, completely up to you. But for the sake of convention, new apps generated with Sails include a `templates/` folder for you by default. ### How do I use these templates? By default, your Gruntfile is configured to automatically load and precompile client-side JST templates in your `assets/templates` folder, then include them in your `layout.ejs` view automatically (between TEMPLATES and TEMPLATES END). This exposes your HTML templates as precompiled functions on `window.JST` for use from your client-side JavaScript. To customize this behavior to fit your needs, just edit your Gruntfile. For example, here are a few things you could do: - Import templates from other directories - Use a different template engine (handlebars, jade, dust, etc) - Internationalize your client-side templates using a server-side stringfile before they're served. For more information, check out the conceptual documentation on the [default Grunt tasks](https://sailsjs.com/documentation/concepts/assets/default-tasks) that make up Sails' asset pipeline. ================================================ FILE: docs/anatomy/config/blueprints.js.md ================================================ # config/blueprints.js This file is for the configuration of blueprint routes and actions. For an overview of blueprints, see the [main Blueprints API concepts docs](https://sailsjs.com/documentation/concepts/blueprints). For more information on configuring the blueprint API, check out the [reference documentation on blueprints](https://sailsjs.com/documentation/reference/configuration/sails-config-blueprints). ### Usage See [`sails.config.blueprints`](https://sailsjs.com/documentation/reference/configuration/sails-config-blueprints) for all available options. ================================================ FILE: docs/anatomy/config/bootstrap.js.md ================================================ # config/bootstrap.js This is a server-side JavaScript file that is executed by Sails just before your app is lifted. This gives you an opportunity to set up your data model, run jobs, or perform some special logic. ### Usage See [`sails.config.bootstrap`](https://sailsjs.com/documentation/reference/configuration/sails-config-bootstrap) for more info. ================================================ FILE: docs/anatomy/config/config.md ================================================ # config/ This folder contains various files that will allow you to customize and configure your Sails app. ================================================ FILE: docs/anatomy/config/custom.js.md ================================================ # config/custom This is your custom configuration file. It is useful for one-off settings specific to your application-- like your base URL for linkbacks, the no-reply "From" address to use when sending automated emails, or 3rd party API keys for Stripe, Mailgun, Twilio, etc. > Use [`sails.config.custom`](https://sailsjs.com/documentation/reference/application/sails-config-custom) to access these values from your actions and helpers. You can learn more about custom configuration [here](https://sailsjs.com/documentation/reference/configuration/sails-config-custom). ================================================ FILE: docs/anatomy/config/datastores.js.md ================================================ # config/datastores A set of datastore configurations which tell Sails where to fetch or save data when you execute built-in model methods like `.find()` and `.create()`. > This file is mainly useful for configuring your development database, as well as any additional one-off databases used by individual models. ### Usage See [`sails.config.datastores`](https://sailsjs.com/documentation/reference/configuration/sails-config-datastores) for all available options. ================================================ FILE: docs/anatomy/config/env/env.md ================================================ # config/env/ This folder contains various environment-specific settings such as API keys or remote database passwords. Depending on the environment Sails is lifted in, the appropriate configuration file in this folder will load. To read more about environent-specific config in Sails, see [**Concepts > Configuration**](https://sailsjs.com/documentation/concepts/configuration#?environmentspecific-files-config-env). ================================================ FILE: docs/anatomy/config/env/production.js.md ================================================ # config/env/production.js This file will be loaded when Sails is running in `production` mode. If using the CLI command `sails lift --prod`, these settings will be loaded. ================================================ FILE: docs/anatomy/config/globals.js.md ================================================ # config/globals.js Configuration for the global variables Sails exposes to its Node process. ### Usage See [`sails.config.globals`](https://sailsjs.com/documentation/reference/configuration/sails-config-globals) for all available options. ================================================ FILE: docs/anatomy/config/http.js.md ================================================ # config/http.js This file is for configuring the underlying HTTP server in Sails, as well as any HTTP middleware your app may need. ### Usage See [`sails.config.http`](https://sailsjs.com/documentation/reference/configuration/sails-config-http) for all available options. ================================================ FILE: docs/anatomy/config/i18n.js.md ================================================ # config/i18n.js This file contains your Sails app's internationalization settings. ### Usage See [`sails.config.i18n`](https://sailsjs.com/documentation/reference/configuration/sails-config-i-18-n) for all available options. ================================================ FILE: docs/anatomy/config/local.js.md ================================================ # config/local.js This file is used to specify configuration settings for use while developing the app on your personal system. For more information, check out [Concepts > Configuration > The local.js file](https://sailsjs.com/docs/concepts/configuration/the-local-js-file) > Since `config/local.js` is usually used to store sensitive credentials, it is included in your app's [.gitignore](https://sailsjs.com/documentation/anatomy/.gitignore), and isn't pushed to the remote server. If you click the link to this file below, you should see a 404 page; in this case, that's a _good_ thing! ================================================ FILE: docs/anatomy/config/locales/de.json.md ================================================ # config/locales/de.json This file is where German locale information is stored. ================================================ FILE: docs/anatomy/config/locales/en.json.md ================================================ # config/locales/en.json This file is where English locale settings are stored. ================================================ FILE: docs/anatomy/config/locales/es.json.md ================================================ # config/locales/es.json This file is where Spanish locale settings are stored. ================================================ FILE: docs/anatomy/config/locales/fr.json.md ================================================ # config/locales/fr.json This file is where French locale settings are stored. ================================================ FILE: docs/anatomy/config/locales/locales.md ================================================ # config/locales This folder contains the information that is used by your app in supporting visiting client's different [locales](http://en.wikipedia.org/wiki/Locale). ### Usage See **[Concepts > Internationalization](https://sailsjs.com/documentation/concepts/internationalization)**. ================================================ FILE: docs/anatomy/config/log.js.md ================================================ # config/log.js This file contains the logger configuration for your Sails app. Configure the log level for your app, as well as the transport. Underneath the covers, Sails uses Winston for logging, which allows for some pretty neat custom transports/adapters for log messages. ### Usage See [`sails.config.log`](https://sailsjs.com/documentation/reference/configuration/sails-config-log) for all available options. ================================================ FILE: docs/anatomy/config/models.js.md ================================================ # config/models.js Unless you override them, the properties contained in this file will be included in each of your models. ### Usage See [`sails.config.models`](https://sailsjs.com/documentation/reference/configuration/sails-config-models) for all available options. ================================================ FILE: docs/anatomy/config/policies.js.md ================================================ # config/policies.js This file contains the default policies for your app. Policies are simply Express middleware functions which run before your controllers. You can apply one or more policies to a given controller, or protect just one of it's actions. Any policy file (e.g. `api/policies/isLoggedIn.js`) can be dropped into the `api/policies/` folder, at which point it can be accessed by it's filename, minus the extension, (e.g. `isLoggedIn`). ### Usage See [`sails.config.policies`](https://sailsjs.com/documentation/reference/configuration/sails-config-policies) for all available options. ================================================ FILE: docs/anatomy/config/routes.js.md ================================================ # config/routes.js This file contains custom routes. Sails uses these routes to determine what to do each time it receives a request. If Sails receives a URL that doesn't match any of the [custom routes](https://sailsjs.com/documentation/concepts/routes/custom-routes) in this file, it will check for matching [assets](https://sailsjs.com/documentation/concepts/assets) (images, scripts, stylesheets, etc.). Finally, if those don't match either, the [default 404 handler](https://sailsjs.com/documentation/reference/response-res/res-not-found) is triggered. When you first generate your Sails app, there is only one route in this file. Its job is to serve the home page. You'll probably want to add some more. > Sails also injects _shadow routes_, or implicit routes that handle certain kinds of requests behind the scenes. For more information about these kinds of routes, see **[Concepts > Blueprints](https://sailsjs.com/documentation/concepts/blueprints)**. ### Usage See [`sails.config.routes`](https://sailsjs.com/documentation/reference/configuration/sails-config-routes) for all available options. ================================================ FILE: docs/anatomy/config/security.js.md ================================================ # config/security.js This file is the conventional home of your Sails app's global security settings. For a complete reference of available security configuration in Sails, see: * CORS settings reference: [sails.config.security.cors](https://sailsjs.com/documentation/reference/configuration/sails-config-security-cors) * CSRF settings reference: [sails.config.security.csrf](https://sailsjs.com/documentation/reference/configuration/sails-config-security-csrf) For a conceptual explanation of CORS in Sails, see [Security > CORS](https://sailsjs.com/documentation/concepts/security/cors). For a conceptual explanation of CSRF in Sails, see [Security > CSRF](https://sailsjs.com/documentation/concepts/security/csrf). ### Usage See [`sails.config.security`](https://sailsjs.com/documentation/reference/configuration/sails-config-security) for all available options. ================================================ FILE: docs/anatomy/config/session.js.md ================================================ # config/session.js This file contains information that tells Sails where to store your sessions. Sails session integration leans heavily on the great work already done by Express, but also unifies socket.io with the Connect session store. It uses Connect's cookie parser to normalize configuration differences between Express and socket.io and hooks into Sails' middleware interpreter to allow you to access and auto-save to `req.session` with Socket.io the same way you would with Express. This is where you would go to configure a different session store like Redis or Mongo. In this file you will find commented examples of what that configuration should look like. This file also contains your 'Session Secret' that is generated by Sails when you create your app. Do not change or remove this unless you really know what you are doing. ### Usage See [`sails.config.session`](https://sailsjs.com/documentation/reference/configuration/sails-config-session) for all available options. ================================================ FILE: docs/anatomy/config/sockets.js.md ================================================ # config/sockets.js This is a configuration file that allows you to customize the way your app talks to clients over Socket.IO. It provides transparent access to Sails' encapsulated pubsub/socket server for complete customizability. In it you can do things on the list below (and more!). - Override afterDisconnect function (server side) - Define custom authorization logic for client socket connections - Set transport method - Change Heartbeat Interval - Change socket store ### More Info > Socket.IO configuration options can be found [here](https://github.com/LearnBoost/Socket.IO/wiki/Configuring-Socket.IO). ### Usage See [`sails.config.sockets`](https://sailsjs.com/documentation/reference/configuration/sails-config-sockets) for all available options. ================================================ FILE: docs/anatomy/config/views.js.md ================================================ # config/views.js This file is where Sails looks to find out which templating engine to use when rendering server side HTML templates. By default Sails uses ejs, but any view engine can be used by changing the `extension` and supplying a `getRenderFn` value (see the [view engine documentation](https://sailsjs.com/documentation/concepts/views/view-engines) for more info). ### Usage See [`sails.config.views`](https://sailsjs.com/documentation/reference/configuration/sails-config-views) for all available options. ================================================ FILE: docs/anatomy/gitignore.md ================================================ # .gitignore This file is only relevant if you are using git. It informs `git` of any files that you don't want `pushed` to the remote server. Files which match the splat patterns seen in the code below will be ignored by git. This keeps random crap and and sensitive credentials from being uploaded to your repository. It allows you to configure your app for your machine without accidentally committing settings which will smash the local settings of other developers on your team. Some reasonable defaults are included, but, of course, you should modify/extend/prune to fit your needs! [Read more about .gitignore](https://help.github.com/articles/ignoring-files) ================================================ FILE: docs/anatomy/package.json.md ================================================ # package.json This is a standard configuration file for [npm](https://npmjs.org/doc/json.html). Among other things, this file contains the name and version of all of the Node Modules that your app depends on to run. You can change this manually but be careful to adhere to their rules or things might break. ================================================ FILE: docs/anatomy/sailsrc.md ================================================ # .sailsrc This file is useful for setting configuration that you want to generate programmatically. You can also use it to extend the functionality of the Sails CLI tool, for example to add a generator. You can learn more about using sailsrc files [here](https://sailsjs.com/documentation/concepts/configuration/using-sailsrc-files). ================================================ FILE: docs/anatomy/tasks/config/babel.js.md ================================================ # tasks/config/babel.js This file configures a Grunt task called "babel". This task is used to transpile any ES8, ES7, and ES6 syntax in your front-end JavaScript files for compatibility with older browsers. > (By default, only `.js` files in the `assets/js/` folder and subfolders will be transpiled. If you need other things transpiled, such as `assets/dependencies/`, you'll need to modify the configuration of this task accordingly.) For additional usage documentation, see [`grunt-babel`](https://npmjs.com/package/grunt-babel). ================================================ FILE: docs/anatomy/tasks/config/clean.js.md ================================================ # tasks/config/clean.js This file configures a Grunt task called "clean". This task is used when preparing for a new pass through the asset pipeline. Its job is to remove any existing temporary files and folders in your Sails app's web root. > (By default, the [web root in a Sails app](https://sailsjs.com/documentation/concepts/assets) is a hidden directory called `.tmp/public`.) For additional usage documentation, see [`grunt-contrib-clean`](https://npmjs.com/package/grunt-contrib-clean). ================================================ FILE: docs/anatomy/tasks/config/coffee.js.md ================================================ # tasks/config/coffee.js This file configures a Grunt task called "coffee". By default, this compiles CoffeeScript files located in [`assets/js/`](https://sailsjs.com/anatomy/assets/js/) into JavaScript, then generates new `.js` files in `.tmp/public/js/`. ### But I'm not using CoffeeScript... No problem! If you aren't using any kind of pre-processing for your client-side JavaScript, then just ignore this file. If you want to use a _different_ pre-processor like [TypeScript](https://www.typescriptlang.org/) or [Babel](https://babeljs.io/), and you want Sails to process your client-side JavaScript assets automatically as you work, then you're in luck. In most cases, this is as easy as installing the appropriate Grunt plugin as a dependency of your Sails app, and then configuring it to output compiled JavaScript to the same path as in this default task. Here are a couple of popular examples: + [grunt-ts](https://www.npmjs.com/package/grunt-ts) + [grunt-babel](https://www.npmjs.com/package/grunt-babel) ### Usage For additional usage documentation, see [`grunt-contrib-coffee`](https://npmjs.com/package/grunt-contrib-coffee). ================================================ FILE: docs/anatomy/tasks/config/concat.js.md ================================================ # tasks/config/concat.js This file configures a Grunt task called "concat". It concatenates the contents of multiple JavaScript and/or CSS files into two new files, each located at `concat/production.js` and `concat/production.css` respectively in `.tmp/public/concat`. This is used as an intermediate step to generate monolithic files that can then be passed in to `uglify` and/or `cssmin` for [minification](https://en.wikipedia.org/wiki/Minification_(programming)). ### Usage For additional usage documentation, see [`grunt-contrib-concat`](https://npmjs.com/package/grunt-contrib-concat). ================================================ FILE: docs/anatomy/tasks/config/config.md ================================================ # tasks/config/ This folder contains the default Grunt task configuration used by the main entry points in [`tasks/register/`](https://sailsjs.com/anatomy/tasks/register). For more about the files in this folder, see [Assets > Default Tasks](https://sailsjs.com/documentation/concepts/assets/default-tasks). ================================================ FILE: docs/anatomy/tasks/config/copy.js.md ================================================ # tasks/config/copy.js This file configures a Grunt task called "copy". Copy files and/or folders from your `assets/` directory into the web root (`.tmp/public`) so they can be served via HTTP, and also for further pre-processing by other Grunt tasks. ##### Normal usage (`sails lift`) Copies all directories and files (except CoffeeScript and LESS) from the `assets/` folder into the web root -- conventionally a hidden directory located `.tmp/public`. ##### Via the `build` tasklist (`sails www`) Copies all directories and files from the .tmp/public directory into a www directory. ### Usage For additional usage documentation, see [`grunt-contrib-copy`](https://npmjs.com/package/grunt-contrib-copy). ================================================ FILE: docs/anatomy/tasks/config/cssmin.js.md ================================================ # tasks/config/cssmin.js This file configures a Grunt task called "cssmin". It minifies the intermediate, concatenated CSS stylesheet which was prepared by the `concat` task at `.tmp/public/concat/production.css`. Together with the `concat` task, this is the final step that minifies all CSS files from `assets/styles/` (and potentially your LESS importer file from `assets/styles/importer.less`). ### Usage For additional usage documentation, see [`grunt-contrib-cssmin`](https://npmjs.com/package/grunt-contrib-cssmin). ================================================ FILE: docs/anatomy/tasks/config/hash.js.md ================================================ # tasks/config/hash.js This file configures a Grunt task called "hash". This task implements cache-busting for minified CSS and JavaScript files. Specifically, its job is to append a unique hash to the end of a filename. > For example: bar/foo.css => bar/dist/foo.f8494f81.css ### Usage For additional usage documentation, see [`grunt-hash`](https://github.com/jgallen23/grunt-hash/tree/0.5.0#grunt-hash). ================================================ FILE: docs/anatomy/tasks/config/jst.js.md ================================================ # tasks/config/jst.js This file configures a Grunt task called "jst". It precompiles HTML templates using Underscore/Lodash notation into functions, creating a `.jst` file. This can be brought into your HTML via a ` ``` Now consider this more advanced (and less common) use case: let's disable the eager (auto-connecting) socket, and instead create a new client socket manually. When it successfully connects to the server, we'll make it log a message: ```html ``` ### Socket requests vs traditional AJAX requests You may have noticed that a client socket `.get()` is very similar to making an AJAX request, for example by using jQuery's `$.get()` method. This is intentional—the goal is for you to be able to get the same response from Sails no matter where the request originated from. The benefit to making the request using a client socket is that the [controller action](https://sailsjs.com/documentation/concepts/controllers#?actions) in your Sails app will have access to the socket which made the request, allowing it to _subscribe_ that socket to realtime notifications (see [sending realtime messages from the server](https://sailsjs.com/documentation/concepts/realtime/on-the-server)). ### Reference * View the full [sails.io.js library](https://sailsjs.com/documentation/reference/web-sockets/socket-client) reference. * See the [sails.sockets](https://sailsjs.com/documentation/reference/web-sockets/sails-sockets) reference to learn how to send messages from the server to connected sockets * See the [resourceful pub-sub](https://sailsjs.com/documentation/reference/web-sockets/resourceful-pub-sub) reference to learn how to use Sails blueprints to automatically send realtime messages about changes to your [models](https://sailsjs.com/documentation/concepts/models-and-orm/models). * Visit the [Socket.io](http://socket.io) website to learn more about the underlying library Sails uses for realtime communication ================================================ FILE: docs/concepts/Realtime/On the server.md ================================================ # Sending realtime messages from the server to one or more clients ### Overview Sails exposes two APIs for communicating with connected socket clients: the higher-level [resourceful pubsub API](https://sailsjs.com/documentation/reference/web-sockets/resourceful-pub-sub), and the lower-level [sails.sockets API](https://sailsjs.com/documentation/reference/web-sockets/sails-sockets). ### Resourceful PubSub The Resourceful PubSub (Published/Subscribe) API provides a high-level way to subscribe sockets to Sails model classes and instances. It is entirely possible to create a rich realtime experience (for example, a chat app) using just this API. Sails blueprints use Resourceful PubSub to automatically send out notifications about new model instances and changes to existing instances, but you can use them in your custom controller actions as well. ##### Example Create a new User model instance and notify all interested clients ```javascript // Create the new user User.create({ name: 'johnny five' }).exec(function(err, newUser) { if (err) { // Handle errors here! return; } // Tell any socket watching the User model class // that a new User has been created! User.publishCreate(newUser); }); ``` ### `sails.sockets` The `sails.sockets` API allows for lower-level communication directly with sockets, using methods like [`sails.sockets.join()`](https://sailsjs.com/documentation/reference/web-sockets/sails-sockets/sails-sockets-join) (subscribe a socket to all messages sent to a particular "room"), [`sails.sockets.leave()`](https://sailsjs.com/documentation/reference/web-sockets/sails-sockets/sails-sockets-leave) (unsubscribe a socket from a room), and [`sails.sockets.broadcast()`](https://sailsjs.com/documentation/reference/web-sockets/sails-sockets/sails-sockets-broadcast) (broadcast a message to all subscribers in one or more rooms). ##### Example Add a socket to the room "funSockets" ```javascript sails.sockets.join(someSocket, "funSockets"); ``` Broadcast a "hello" message to the "funSockets" room. This message will be received by all client sockets that have (1) been added to the "funSockets" room on the server with `sails.sockets.join()` and (2) added a listener for the "hello" event on the client with [`socket.on('hello', ...)`](https://sailsjs.com/documentation/reference/web-sockets/socket-client/io-socket-on). ```javascript sails.sockets.broadcast("funSockets", "hello", "Hello to all my fun sockets!"); ``` ### Reference * View the full [sails.sockets](https://sailsjs.com/documentation/reference/web-sockets/sails-sockets) API reference * See the reference for the [sails.io.js library](https://sailsjs.com/documentation/reference/web-sockets/socket-client) to learn how to use sockets on the client side to communicate with your Sails app. * See the [resourceful pub-sub](https://sailsjs.com/documentation/reference/web-sockets/resourceful-pub-sub) reference to learn how to use Sails blueprints to automatically send realtime messages about changes to your [models](https://sailsjs.com/documentation/concepts/models-and-orm/models). * Visit the [Socket.io](http://socket.io) website to learn more about the underlying library Sails uses for realtime communication ================================================ FILE: docs/concepts/Realtime/Realtime.md ================================================ # Realtime communication (aka Sockets) ### Overview Sails apps are capable of full-duplex, realtime communication between the client and server. This means that a client (e.g. browser tab, Raspberry Pi, etc.) can maintain a persistent connection to a Sails backend, and messages can be sent from client to server (e.g. AJAX) or from server to client (e.g. "comet") at any time. Two common uses of realtime communication are live chat implementations and multiplayer games. Sails implements realtime on the server using the [socket.io](http://socket.io) library, and on the client using the [sails.io.js](https://sailsjs.com/documentation/reference/web-sockets/socket-client/io-socket-on) library. Throughout the Sails documentation, the terms **socket** and **websocket** are commonly used to refer to a two-way, persistent communication channel between a Sails app and a client. Communicating with a Sails app via sockets is similar to using AJAX, in that both methods allow a web page to interact with the server without refreshing. However, sockets differ from AJAX in two important ways: first, a socket can stay connected to the server for as long as the web page is open, allowing it to maintain _state_ (AJAX requests, like all HTTP requests, are _stateless_). Second, because of the always-on nature of the connection, a Sails app can send data down to a socket at any time (hence the "realtime" moniker), whereas AJAX only allows the server to respond when a request is made. ### Realtime model updates with resourceful pub-sub Sockets making requests to Sails' [blueprint actions](https://sailsjs.com/documentation/reference/blueprint-api) are automatically subscribed to realtime messages about the models they retrieve via the [resourceful pub-sub API](https://sailsjs.com/documentation/reference/web-sockets/resourceful-pub-sub). You can also use this API in your custom controller actions to send out messages to clients interested in certain models. ##### Example Connect a client-side socket to the server, subscribe to the `user` event, and request `/user` to subscribe to current and future User model instances. ```html ``` ### Custom realtime communication with `sails.sockets` Sails exposes a rich API on both the client and the server for sending custom realtime messages. ##### Example Here's the client-side code to connect a socket to the Sails/Node.js server and listen for an socket event named "hello": ```html ``` Then, also on the client, we can send a _socket request_. In this case, we'll wire up the browser to send a socket request when a particular button is clicked: ```js $('button#say-hello').click(function (){ // And use `io.socket.get()` to send a request to the server: io.socket.get('/say/hello', function gotResponse(data, jwRes) { console.log('Server responded with status code ' + jwRes.statusCode + ' and data: ', data); }); }); ``` Meanwhile, on the server... To respond to requests to `GET /say/hello`, we use an action. In our action, we'll subscribe the requesting socket to the "funSockets" room, then broadcast a "hello" message to all sockets in that room (excluding the new one). ```javascript // In /api/controllers/SayController.js module.exports = { hello: function(req, res) { // Make sure this is a socket request (not traditional HTTP) if (!req.isSocket) { return res.badRequest(); } // Have the socket which made the request join the "funSockets" room. sails.sockets.join(req, 'funSockets'); // Broadcast a notification to all the sockets who have joined // the "funSockets" room, excluding our newly added socket: sails.sockets.broadcast('funSockets', 'hello', { howdy: 'hi there!'}, req); // ^^^ // At this point, we've blasted out a socket message to all sockets who have // joined the "funSockets" room. But that doesn't necessarily mean they // are _listening_. In other words, to actually handle the socket message, // connected sockets need to be listening for this particular event (in this // case, we broadcasted our message with an event name of "hello"). The // client-side code you'd need to write looks like this: // // io.socket.on('hello', function (broadcastedData){ // console.log(data.howdy); // // => 'hi there!' // } // // Now that we've broadcasted our socket message, we still have to continue on // with any other logic we need to take care of in our action, and then send a // response. In this case, we're just about wrapped up, so we'll continue on // Respond to the request with a 200 OK. // The data returned here is what we received back on the client as `data` in: // `io.socket.get('/say/hello', function gotResponse(data, jwRes) { /* ... */ });` return res.json({ anyData: 'we want to send back' }); } } ``` ### Reference * See the full reference for the [sails.io.js library](https://sailsjs.com/documentation/reference/web-sockets/socket-client/io-socket-on) to learn how to use sockets on the client side to communicate with your Sails app. * See the [sails.sockets](https://sailsjs.com/documentation/reference/web-sockets/sails-sockets) reference to learn how to send custom messages from the server to connected sockets. * See the [resourceful pub-sub](https://sailsjs.com/documentation/reference/web-sockets/resourceful-pub-sub) reference to learn how Sails' blueprint API automatically sends realtime messages about changes to your [models](https://sailsjs.com/documentation/concepts/models-and-orm/models). * Visit the [Socket.io](http://socket.io) website to learn more about the underlying library Sails uses for realtime communication ================================================ FILE: docs/concepts/Routes/RouteTargetSyntax.md ================================================ # Custom routes ### Overview Sails allows you to explicitly route URLs in several different ways in your **config/routes.js** file. Every route configuration consists of an **address** and a **target**, for example: ```js 'GET /foo/bar': 'UserController.subscribe' ^^^address^^^ ^^^^^^^^^^target^^^^^^^^^^ ``` ### Route address The route address indicates what URL should be matched in order to apply the handler and options defined by the target. A route consists of an optional verb and a mandatory path: ```js 'POST /foo/bar' ^verb^ ^^path^^ ``` If no verb is specified, the route will match any CRUD method (**GET**, **PUT**, **POST**, **DELETE** or **PATCH**). If `ALL` is specified as the verb, the route will match _any_ method. Note the initial `/` in the path--all paths should start with one in order to work properly. ### Wildcards and dynamic parameters In addition to specifying a static path like **foo/bar**, you can use `*` as a wildcard: ```js '/*' ``` will match all paths, where as: ```js '/user/foo/*' ``` will match all paths that *start* with **/user/foo**. > **Note:** When using a route with a wildcard, such as `'/*'`, be aware that this will also match requests to static assets (i.e. `/js/dependencies/sails.io.js`) and override them. To prevent this, consider using the `skipAssets` option [described below](https://sailsjs.com/documentation/concepts/routes/custom-routes#?route-target-options). To receive the runtime value corresponding with this wildcard (`*`) in a [modern Sails action](https://sailsjs.com/documentation/concepts/actions-and-controllers#?what-does-an-action-file-look-like), use `urlWildcardSuffix` at the top level of your action definition to indicate the name of the input you would like to use to represent the dynamic value: ```javascript urlWildcardSuffix: 'template', inputs: { template: { description: 'The relative path to an EJS template within our `views/emails/` folder -- WITHOUT the file extension.', extendedDescription: 'Use strings like "foo" or "foo/bar", but NEVER "foo/bar.ejs" or "/foo/bar". For example, '+ '"internal/email-contact-form" would send an email using the "views/emails/internal/email-contact-form.ejs" template.', example: 'email-reset-password', type: 'string', required: true }, }, fn: async function({ template }) { // … } ``` ### Notes > - Alternatively, in a classic (req,res) action, you can use `req.param('0')` to access the dynamic value of a route's URL wildcard suffix (`*`). > - For more background, see https://www.npmjs.com/package/machine-as-action Another way to capture parts of the address is to use **pattern variables**. This lets a route match special named parameters which _never contain any `/` characters_ by using the `:paramName` pattern variable syntax instead of the `*`: ```js '/user/foo/bar/:name' ``` Or for an optional path parameter, add `?` to the end of the pattern variable: ```js '/user/foo/bar/:name?' ``` This will match _almost_ the same requests as `/user/foo/bar/*`, but will provide the value of the dynamic portions of the route URL to the route handler as parameter values (e.g. `req.param('name')`). > Note that the wildcard (`*`) syntax matches slashes, where the URL pattern variable (`:`) syntax does not. So in the example above, given the route address `GET /user/foo/bar/*`, incoming requests with URLs like `/user/foo/bar/baz/bing/bong/bang` would match (whereas if you used the `:name` syntax, the same URL would not match.) ### URL slugs A common use case for pattern variables is the design of slugs or [vanity URLs](http://en.wikipedia.org/wiki/Clean_URL#Slug). For example, consider the URL of a repository on Github, [`http://www.github.com/balderdashy/sails`](http://www.github.com/balderdashy/sails). In Sails, we might define this route at the **bottom of our `config/routes.js` file** like so: ```javascript 'get /:account/:repo': { controller: 'RepoController', action: 'show', skipAssets: true } ``` In your `RepoController`'s `show` action, we'd use `req.param('account')` and `req.param('repo')` to look up the data for the appropriate repository, then pass it in to the appropriate [view](https://sailsjs.com/documentation/concepts/Views) as [locals](https://sailsjs.com/documentation/concepts/views/locals). The [`skipAssets` option](https://sailsjs.com/documentation/concepts/routes/custom-routes#?route-target-options) ensures that the vanity route doesn't accidentally match any of our [assets](https://sailsjs.com/documentation/concepts/assets) (e.g. `/images/logo.png`), so they are still accessible. ### Regular expressions in addresses In addition to the wildcard address syntax, you may also use regular expressions to define the URLs that a route should match. The syntax for defining an address with a regular expression is: `'r||'` That's the letter "**r**", followed by a pipe character `|`, a regular expression string *without delimiters*, another pipe, and a list of parameter names that should be mapped to parenthesized groups in the regular expression. For example: `'r|^/\\d+/(\\w+)/(\\w+)$|foo,bar": "message/my-action'` Will match `/123/abc/def`, running the action in **api/controllers/message/my-action.js**, and supplying the values `abc` and `def` as `req.param('foo')` and `req.param('bar')`, respectively. Note the double-backslash in `\\d` and `\\w`; this escaping is necessary for the regular expression to work correctly! ##### About route ordering While you are free to add items to your **config/routes.js** file in any order, be aware that Sails will internally sort your routes by _inclusiveness_, a measure of how many potential requests an address can handle. In general, routes with addresses containing no dynamic components will be matched first, followed by routes with dynamic parameters, followed by those with wildcards. This prevents routes from blocking each other (for example, a `/*` route, if left at the top of the list, would respond to all requests and no other routes would ever be matched). If you have any [regular expression addresses](https://sailsjs.com/documentation/concepts/routes/custom-routes#?regular-expressions-in-addresses), they will be left in the order you specify. For example, if your **config/routes.js** file contains a `GET /foo/bar` route followed by a `GET r|^/foo/\\d+$|` route, the second route will always be sorted to appear immediately after `GET /foo/bar`. This is due to the extreme difficulty of determining the inclusiveness of a regular expression route. Take care when specifying these routes that you order them so that they won't match more requests than intended. ### Route target The address portion of a custom route specifies which URLs the route should match. The *target* portion specifies what Sails should do after the match is made. A target can take one of several different forms. In some cases you may want to chain multiple targets to a single address by placing them in an array, but in most cases each address will have only one target. The different types of targets are discussed below, followed by a discussion of the various options that can be applied to them. ##### Controller / action target syntax This syntax binds a route to an action in a [controller file](https://sailsjs.com/documentation/concepts/actions-and-controllers#?controllers). The following four routes are equivalent: ```js 'GET /foo/go': 'FooController.myGoAction', 'GET /foo/go': 'foo.myGoAction', 'GET /foo/go': { controller: 'foo', action: 'myGoAction' }, 'GET /foo/go': { controller: 'FooController', action:'myGoAction' }, ``` Each one maps `GET /foo/go` to the `myGoAction` action of the controller in **api/controllers/FooController.js**, or to the action in **api/controllers/foo/mygoaction.js**. If no such controller or action exists, Sails will output an error message and ignore the route. Otherwise, whenever a **GET** request to **/foo/go** is made, the code in that action will be run. The controller and action names in this syntax are case-insensitive. ##### Standalone action target syntax This syntax binds an address to a [standalone Sails action](https://sailsjs.com/documentation/concepts/actions-and-controllers#?standalone-actions). Simply specify the path of the action (relative to `api/controllers`): ```js 'GET /': { action: 'index' }, // Use the action in api/controllers/index.js 'GET /foo/go': { action: 'foo/go-action' } // Use the action in api/controllers/foo/go-action.js OR // the "go-action" action in api/controllers/FooController.js 'GET /bar/go': 'foo/go-action' // Binds to the same action as above, using shorthand notation ``` ##### Routing to blueprint actions The [blueprint API](https://sailsjs.com/documentation/reference/blueprint-api) adds several actions for each of your models, all of which are available for routing. For example, if you have a model defined in `api/models/User.js`, you’ll automatically be able to do: ```js 'GET /foo/go': 'user/find' // Return a list of users ``` or ```js 'GET /foo/go': 'UserController.find' // Same as above ``` If you have a custom action in `api/controllers/user/find.js` or `api/controllers/UserController.js`, that action will be run instead of the default blueprint `find`. For a full list of the actions provided for your models, see the [blueprint API reference](https://sailsjs.com/documentation/reference/blueprint-api). ##### View target syntax Another common target is one that binds a route to a [view](https://sailsjs.com/documentation/concepts/Views). This is particularly useful for binding static views to a custom URL, and it's how the default homepage for new projects is set up out of the box. The syntax for view targets is simple: it is just the path to the view file, without the file extension (e.g. `.ejs`) and relative to the **views/** folder : ```js 'GET /team': { view: 'brochure/about' } ``` This tells Sails to handle `GET` requests to `/team` by serving the view template located at `views/brochure/about.ejs` (assuming the default EJS [template engine](https://sailsjs.com/documentation/concepts/views/view-engines) is used). As long as that view file exists, a **GET** request to **/home** will display it. For consistency with Express/consolidate, if the specified relative path does not match a view file, then Sails will look for a sub-folder with the same name (e.g. `pages/brochure`) and serve the "index" view in that sub-folder (e.g. `pages/brochure/index.ejs`) if one exists. > Note that since this route is bound directly to the view, none of your configured policies will be applied. If you need to configure a policy, use `res.view()` from a controller action. See [this StackOverflow question](http://stackoverflow.com/questions/21303217/sailsjs-policy-based-route-with-a-view/21340313#21340313) for more background information. ##### Redirect target syntax You can have one address redirect to another, either within your Sails app or on another server entirely. This can be done just by specifying the redirect URL as a string: ```js '/alias' : '/some/other/route/url', 'GET /google': 'http://www.google.com' ``` Be careful to avoid redirect loops when redirecting within your Sails app! Note that when redirecting, the HTTP method of the original request (and any extra headers / parameters) will likely be lost, and the request will be transformed to a simple **GET** request. In the above example, a **POST** request to **/alias** will result in a **GET** request to **/some/other/route**. This is somewhat browser-dependent behavior, but it is recommended that you don't expect request methods and other data to survive a redirect. ##### Response target syntax You can map an address directly to a default or custom [response](https://sailsjs.com/documentation/concepts/extending-sails/custom-responses) using this syntax: ```js '/foo': { response: 'notFound' } ``` Simply specify the name of the response file in your **api/responses** folder, without the **.js** extension. The response name in this syntax is case-sensitive. If you attempt to bind a route to a non-existent response, Sails will output an error and ignore the route. ##### Policy target syntax In most cases, you will want to apply [policies](https://sailsjs.com/documentation/concepts/policies) to your controller actions using the [**config/policies.js**](https://sailsjs.com/documentation/reference/configuration/sails-config-policies) config file. However, there are some instances when you'll want to apply a policy directly to a custom route, particularly when you are using the [view](https://sailsjs.com/documentation/concepts/routes/custom-routes#?view-target-syntax) target syntax. The policy target syntax is: ```js '/foo': { policy: 'my-policy' } ``` Note that you will always want to chain the policy to at least one other type of target using an array: ```js '/foo': [ { policy: 'my-policy' }, { view: 'dashboard' } ] ``` This will apply the **my-policy** policy to the route and, if it passes, continue by displaying the **views/dashboard.ejs** view. ##### Function target syntax For one-off jobs (quick tests, for example), you can assign a route directly to a function: ```js '/foo': function(req, res) { return res.send('hello!'); }, ``` You can also combine this syntax with others using an array. This allows you to define quick, inline middleware: ```js '/foo': [ function(req, res, next) { sails.log('Quick and dirty test:', req.allParams()); return next(); }, { controller: 'user', action: 'find' } ], ``` You can also use a dictionary with an `fn` key to assign a function. This allows you to also specify [other route target options](https://sailsjs.com/documentation/concepts/routes/custom-routes#?route-target-options) at the same time: ```js 'GET /*': { skipAssets: true, fn: function(req, res) { return res.send('hello!'); } }, ``` > Best practice is to use the function syntax only for temporary routes, since doing so goes against the structural conventions that make Sails useful! (Plus, the less cluttered your routes.js file, the better.) ### Route target options In addition to the options discussed in the various route target syntaxes above, any other property added to a route target object will be passed through to the route handler in the `req.options` object. There are several reserved properties that can be used to affect the behavior of the route handlers. These are listed in the table below. | Property | Applicable Target Types | Data Type | Details | |-------------|:----------:|-----------|-----------| |`skipAssets`|all|((boolean))|Set to `true` if you *don't* want the route to match URLs with dots in them (e.g. **myImage.jpg**). This will keep your routes with [wildcard notation](https://sailsjs.com/documentation/concepts/routes/custom-routes#?wildcards-and-dynamic-parameters) from matching URLs of static assets. Useful when creating [URL slugs](https://sailsjs.com/documentation/concepts/routes#url-slugs).| |`skipRegex`|all|((regexp))|If skipping every URL containing a dot is too permissive, or you need a route's handler to be skipped based on different criteria entirely, you can use `skipRegex`. This option allows you to specify a regular expression or array of regular expressions to match the request URL against; if any of the matches are successful, the handler is skipped. Note that unlike the syntax for binding a handler with a regular expression, `skipRegex` expects *actual [RegExp objects](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp)*, not strings.| |`locals`|[controller](https://sailsjs.com/documentation/concepts/routes/custom-routes#?controller-action-target-syntax), [view](https://sailsjs.com/documentation/concepts/routes/custom-routes#?view-target-syntax), [blueprint](https://sailsjs.com/documentation/concepts/routes/custom-routes#?routing-to-blueprint-actions), [response](https://sailsjs.com/documentation/concepts/routes/custom-routes#?response-target-syntax)|((dictionary))|Sets default [local variables](https://sailsjs.com/documentation/reference/response-res/res-view?q=arguments) to pass to any view that is rendered while handling the request.| |`cors`|all|((dictionary)) or ((boolean)) or ((string))|Specifies how to handle requests for this route from a different origin. See the [main CORS documentation](https://sailsjs.com/documentation/concepts/security/cors) for more info.| |`csrf`|all|((boolean))|Indicate whether the route should be protected by requiring a CSRF token to be passed with the request. See the [main CSRF documentation](https://sailsjs.com/documentation/concepts/security/csrf) for more info. |`parseBlueprintOptions`|[blueprint](https://sailsjs.com/documentation/concepts/routes/custom-routes#?routing-to-blueprint-actions)|((function))|Provide this function in order to override the default behavior for a blueprint action (including search criteria, skip, limit, sort and population). See the [blueprints configuration reference](https://sailsjs.com/documentation/reference/configuration/sails-config-blueprints#?using-parseblueprintoptions) for more info. ================================================ FILE: docs/concepts/Routes/Routes.md ================================================ # Routes ### Overview The most basic feature of any web application is the ability to interpret a request sent to a URL, then send back a response. In order to do this, your application has to be able to distinguish one URL from another. Like most web frameworks, Sails provides a router: a mechanism for mapping URLs to actions and views. **Routes** are rules that tell Sails what to do when faced with an incoming request. There are two main types of routes in Sails: **custom** (or "explicit") and **automatic** (or "implicit"). ### Custom routes Sails lets you design your app's URLs in any way you like—there are no framework restrictions. Every Sails project comes with [`config/routes.js`](https://sailsjs.com/documentation/reference/configuration/sails-config-routes), a simple [Node.js module](http://nodejs.org/api/modules.html) that exports an object of custom, or "explicit" **routes**. For example, this `routes.js` file defines six routes; some of them point to actions, while others route directly to views: ```javascript // config/routes.js module.exports.routes = { 'GET /signup': { view: 'conversion/signup' }, 'POST /signup': { action: 'entrance/signup' }, 'GET /login': { view: 'portal/login' }, 'POST /login': { action: 'entrance/login' }, '/logout': { action: 'account/logout' }, 'GET /me': { action: 'account/profile' } ``` Each **route** consists of an **address** on the left (e.g. `'GET /me'`) and a **target** on the right (e.g. `{ action: 'account/profile' }`) The **address** is a URL path and (optionally) a specific [HTTP method](http://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Request_methods). The **target** can be defined in a number of different ways ([see the expanded concepts section on the subject](https://sailsjs.com/documentation/concepts/routes/custom-routes#?route-target)), but the syntax above is the most common. When Sails receives an incoming request, it checks the **address** of all custom routes for matches. If a matching route is found, the request is then passed to its **target**. For example, we might read `'GET /me': { action: 'account/profile' }` as: > "Hey Sails, when you receive a GET request to `http://mydomain.com/me`, run the `account/profile` action, would'ya?" You can also specify the view layout within the route itself: ```javascript 'GET /privacy': { view: 'legal/privacy', locals: { layout: 'users' } }, ``` #### Notes + That a request matches a route address doesn't necessarily mean it will be passed to that route's target _directly_. HTTP requests will usually pass through some [middleware](https://sailsjs.com/documentation/concepts/Middleware) before being passed to a route's target, and if the route points to a controller [action](https://sailsjs.com/documentation/concepts/Controllers?q=actions), the request will first need to pass through any configured [policies](https://sailsjs.com/documentation/concepts/Policies). There are also a few special [route options](https://sailsjs.com/documentation/concepts/routes/custom-routes#?route-target-options) which allow a route to be "skipped" for certain kinds of requests. + The router can also programmatically **bind** a **route** to any valid route target, including canonical Node middleware functions (i.e. `function (req, res, next) {}`). However, you should always use the conventional [route target syntax](https://sailsjs.com/documentation/concepts/routes/custom-routes#?route-target) when possible—it streamlines development, simplifies training, and makes your app more maintainable. ### Automatic routes In addition to your custom routes, Sails binds many routes for you automatically. If a URL doesn't match a custom route, it may match one of the automatic routes and still generate a response. The main types of automatic routes in Sails are: * [blueprint routes](https://sailsjs.com/documentation/reference/blueprint-api?q=blueprint-routes), which provide your [controllers](https://sailsjs.com/documentation/concepts/controllers) and [models](https://sailsjs.com/documentation/concepts//models-and-orm/models) with a full REST API. * [assets](https://sailsjs.com/documentation/concepts/assets), such as images, Javascript and stylesheet files. ##### Unhandled requests If no custom or automatic route matches a request URL, Sails will send back a default 404 response. This response can be customized by adding a `api/responses/notFound.js` file to your app. See [custom responses](https://sailsjs.com/documentation/concepts/extending-sails/custom-responses) for more info. ##### Unhandled errors in request handlers If an unhandled error is thrown during the processing of a request (for instance, in some [action code](https://sailsjs.com/documentation/concepts/actions-and-controllers)), Sails will send back a default 500 response. This response can be customized by adding an `api/responses/serverError.js` file to your app. See [custom responses](https://sailsjs.com/documentation/concepts/extending-sails/custom-responses) for more info. ### Supported protocols The Sails router is "protocol-agnostic"—it knows how to handle both [HTTP requests](http://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol) and messages sent via [WebSockets](http://en.wikipedia.org/wiki/Websockets). It accomplishes this by listening for Socket.io messages sent to reserved event handlers in a simple format, called JWR (JSON-WebSocket Request/Response). This specification is implemented and available out of the box in the [client-side socket SDK](https://sailsjs.com/documentation/reference/web-sockets/socket-client). #### Notes Advanced users may opt to circumvent the router entirely and send low-level, completely customizable WebSocket messages directly to the underlying Socket.io server. You can bind socket events directly in your app's [`onConnect`](https://sailsjs.com/documentation/reference/configuration/sails-config-sockets#?commonlyused-options) function (located in [`config/sockets.js`](https://sailsjs.com/documentation/anatomy/config/sockets.js)), but bear in mind that in most cases you are better off leveraging the request interpreter for socket communication. Maintaining consistent routes across HTTP and WebSockets helps keep your app maintainable. ================================================ FILE: docs/concepts/Security/CORS.md ================================================ # Cross-Origin Resource Sharing (CORS) [CORS](http://en.wikipedia.org/wiki/Cross-origin_resource_sharing) is a mechanism that allows browser scripts on pages served from other domains (e.g. myothersite.com) to talk to your server (e.g. api.mysite.com). Like [JSONP](https://en.wikipedia.org/wiki/JSONP), the goal of CORS is to circumvent the [same-origin policy](http://en.wikipedia.org/wiki/Same-origin_policy), allowing your Sails server to successfully respond to requests from client-side JavaScript code running on a page hosted from some other domain. Unlike JSONP, it works with more than just GET requests, and it allows you to whitelist particular origins (`staging.yoursite.com` or `yourothersite.net`) and prevent requests from others (`evil.com`). Sails can be configured to allow cross-origin requests from a list of domains you specify, or from every domain. This can be done on a per-route basis, or globally for every route in your app. ### Enabling CORS For security reasons, CORS is disabled by default in Sails. But enabling it is simple. To allow cross-origin requests from a whitelist of trusted domains to _any_ route in your app, simply enable `allRoutes` and provide an `origin` setting in [`config/security.js`](https://sailsjs.com/documentation/reference/configuration/sails-config-security#?sailsconfigsecuritycors): ```javascript cors: { allRoutes: true, allowOrigins: ['http://example.com','https://api.example.com','http://blog.example.com:1337','https://foo.com:8888'] } ``` To allow cross-origin requests from _any_ domain to _any_ route in your app, use `allowOrigins: '*'`: ```javascript cors: { allRoutes: true, allowOrigins: '*', allowCredentials: false } ``` Note that when using `allowOrigins: '*'`, the `allowCredentials` setting _must_ be `false`, which means that requests containing cookies will be blocked. This restriction exists to prevent third-party sites from being able to trick your logged-in users into making unauthorized requests to your app. You can lift this restriction (at your own risk!) using the [`allowAnyOriginWithCredentialsUnsafe`](https://sailsjs.com/documentation/reference/configuration/sails-config-security#?sailsconfigsecuritycors) setting. See [`sails.config.security.cors`](https://sailsjs.com/documentation/reference/configuration/sails-config-security#?sailsconfigsecuritycors) for a comprehensive reference of all available options. ### Configuring CORS for individual routes In addition to the global CORS configuration in `config/security.js`, these settings can be configured on a per-route basis in [`config/routes.js`](https://sailsjs.com/documentation/anatomy/config/routes-js). If you set `allRoutes: true` in `config/security.js` but want to exempt a specific route, set `cors: false` in the route's target: ```javascript 'POST /signup': { action: 'user/signup', cors: false } ``` To enable or override global CORS configuration for a particular route, provide `cors` as a dictionary: ```javascript 'GET /videos': { action: 'video/find', cors: { allowOrigins: ['http://example.com','https://api.example.com','http://blog.example.com:1337','https://foo.com:8888'], allowCredentials: false } } ``` ### Notes > + CORS support is only relevant for HTTP requests. Requests made via sockets are not subject to cross-origin restrictions. To ensure that your app is secure via sockets, configure the [`onlyAllowOrigins`](https://sailsjs.com/documentation/reference/configuration/sails-config-sockets) setting (typically in [`config/env/production.js`](https://sailsjs.com/documentation/anatomy/config/env/production-js)). > + CORS is not supported in Internet Explorer 7. Fortunately, it is supported in IE8 and up, as well as in all other modern browsers. > + Read [more about CORS from MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS). > + Read the [CORS spec](https://www.w3.org/TR/cors/). ================================================ FILE: docs/concepts/Security/CSRF.md ================================================ # CSRF Cross-site request forgery ([CSRF](https://www.owasp.org/index.php/Cross-Site_Request_Forgery)) is a type of attack which forces an end user to execute unwanted actions on a web application backend with which he/she is currently authenticated. In other words, without protection, cookies stored in a browser like Google Chrome can be used to send requests to Chase.com from a user's computer whether that user is currently visiting Chase.com or Horrible-Hacker-Site.com. ### About CSRF tokens CSRF tokens are like limited-edition swag. While a session tells the server that a user "is who they say they are", a csrf token tells the server they "were where they say they were". When CSRF protection is enabled in your Sails app, all non-GET requests to the server must be accompanied by a special "CSRF token", which can be included as either the '_csrf' parameter or the 'X-CSRF-Token' header. Using tokens protects your Sails app against cross-site request forgery (or CSRF) attacks. A would-be attacker needs not only a user's session cookie, but also this timestamped, secret CSRF token, which is refreshed/granted when the user visits a URL on your app's domain. This allows you to have certainty that your users' requests haven't been hijacked, and that the requests they're making are intentional and legitimate. Enabling CSRF protection requires managing the token in your front-end app. In traditional form submissions, this can be easily accomplished by sending along the CSRF token as a hidden input in your `
`. Or better yet, include the CSRF token as a request param or header when you send AJAX requests. To do that, you can either fetch the token by sending a request to the route where you mounted `security/grant-csrf-token`, or better yet, harvest the token from view locals using the `exposeLocalsToBrowser` partial. Here are some examples: #### (a) For modern, view-driven hybrid apps that submit forms with AJAX: Use the `exposeLocalsToBrowser` partial to provide access to the token from your client-side JavaScript, e.g.: ```html <%- exposeLocalsToBrowser() %> ``` #### (b) For single-page apps with static HTML: Fetch the token by sending a GET request to the route where you mounted the `security/grant-csrf-token`. It will respond with JSON, e.g.: ```js { _csrf: 'ajg4JD(JGdajhLJALHDa' } ``` #### (c) For traditional HTML form submissions: Render the token directly into a hidden form input element in your HTML, e.g.: ```html
``` ### Enabling CSRF protection Sails bundles optional CSRF protection out of the box. To enable the built-in enforcement, just make the following adjustment to [sails.config.security.csrf](https://sailsjs.com/docs/reference/configuration/sails-config-security-csrf) (conventionally located in your project's [`config/security.js`](https://sailsjs.com/anatomy/config/security-js) file): ```js csrf: true ``` You can also turn CSRF protection on or off on a per-route basis by adding `csrf: true` or `csrf: false` to any route in your [`config/routes.js`](https://sailsjs.com/anatomy/config/routes-js) file. Note that if you have existing code that communicates with your Sails backend via POST, PUT, or DELETE requests, you'll need to acquire a CSRF token and include it as a parameter or header in those requests. More on that in a sec. ### CSRF tokens Like most Node applications, Sails and Express are compatibile with Connect's [CSRF protection middleware](http://www.senchalabs.org/connect/csrf.html) for guarding against such attacks. This middleware implements the [Synchronizer Token Pattern](https://www.owasp.org/index.php/Cross-Site_Request_Forgery_%28CSRF%29_Prevention_Cheat_Sheet#General_Recommendation:_Synchronizer_Token_Pattern). When CSRF protection is enabled, all non-GET requests to the Sails server must be accompanied by a special token, identified by either a header or a parameter in the query string or HTTP body. CSRF tokens are temporary and session-specific; e.g. Imagine Mary and Muhammad are both shoppers accessing our e-commerce site running on Sails, and CSRF protection is enabled. Let's say that on Monday, Mary and Muhammad both make purchases. In order to do so, our site needed to dispense at least two different CSRF tokens- one for Mary and one for Muhammad. From then on, if our web backend received a request with a missing or incorrect token, that request will be rejected. So now we can rest assured that when Mary navigates away to play online poker, the 3rd party website cannot trick the browser into sending malicious requests to our site using her cookies. ### Dispensing CSRF tokens To get a CSRF token, you should either bootstrap it in your view using [locals](https://sailsjs.com/documentation/concepts/views/locals) (good for traditional multi-page web applications) or fetch it using AJAX from a special protected JSON endpoint (handy for single-page-applications (SPAs).) ##### Using view locals: For old-school form submissions, it's as easy as passing the data from a view into a form action. You can grab hold of the token in your view, where it may be accessed as a view local: `<%= _csrf %>` e.g.: ```html
``` If you are doing a `multipart/form-data` upload with the form, be sure to place the `_csrf` field before the `file` input, otherwise you run the risk of a timeout and a 403 firing before the file finishes uploading. ##### Using AJAX/WebSockets In AJAX/Socket-heavy apps, you might prefer to get the CSRF token dynamically rather than having it bootstrapped on the page. You can do so by setting up a route in your [`config/routes.js`](https://sailsjs.com/anatomy/config/routes-js) file pointing to the `security/grant-csrf-token` action: ```json { 'GET /csrfToken': { action: 'security/grant-csrf-token' } } ``` Then send a GET request to the route you defined, and you'll get CSRF token returned as JSON, e.g.: ```json { _csrf: 'ajg4JD(JGdajhLJALHDa' } ``` > For security reasons, you can’t retrieve a CSRF token via a socket request. You can however _spend_ CSRF tokens (see below) via socket requests. > The `security/grant-csrf-token` action is not intended to be used in cross-origin requests, since some browsers block third-party cookies by default. See the [CORS documentation](https://sailsjs.com/documentation/concepts/security/cors) for more info about cross-origin requests. ### Spending CSRF tokens Once you've enabled CSRF protection, any POST, PUT, or DELETE requests (**including** virtual requests, e.g. from Socket.io) made to your Sails app will need to send an accompanying CSRF token as a header or parameter. Otherwise, they'll be rejected with a 403 (Forbidden) response. For example, if you're sending an AJAX request from a webpage with jQuery: ```js $.post('/checkout', { order: '8abfe13491afe', electronicReceiptOK: true, _csrf: 'USER_CSRF_TOKEN' }, function andThen(){ ... }); ``` With some client-side modules, you may not have access to the AJAX request itself. In this case, you can consider sending the CSRF token directly in the URL of your query. However, if you do so, remember to URL-encode the token before spending it: ```js ..., { checkoutAction: '/checkout?_csrf='+encodeURIComponent('USER_CSRF_TOKEN') } ``` ### Notes > + You can choose to send the CSRF token as the `X-CSRF-Token` header instead of the `_csrf` parameter. > + For most developers and organizations, CSRF attacks need only be a concern if you allow users to log into/securely access your Sails backend _from the browser_ (i.e. from your HTML/CSS/JavaScript front-end code). If you _don't_ (e.g. users only access the secured sections from your native iOS or Android app), it is possible you don't need to enable CSRF protection. Why? Because technically, the common CSRF attack discussed on this page is only _possible_ in scenarios where users use the _same client application_ (e.g. Chrome) to access different web services (e.g. Chase.com, Horrible-Hacker-Site.com.) > + For more information on CSRF, check out [Wikipedia](http://en.wikipedia.org/wiki/Cross-site_request_forgery) > + For "spending" CSRF tokens in a traditional form submission, refer to the example above (under "Using view locals".) ================================================ FILE: docs/concepts/Security/Clickjacking.md ================================================ # Clickjacking [Clickjacking](https://www.owasp.org/index.php/Clickjacking) (aka "UI redress attacks") happens when an attacker manages to trick your users into triggering "unintended" UI events (e.g. DOM events). ### X-FRAME-OPTIONS One simple way to help prevent clickjacking attacks is to enable the X-FRAME-OPTIONS header. ##### Using [lusca](https://github.com/krakenjs/lusca#luscaxframevalue) > `lusca` is open-source under the [Apache license](https://github.com/krakenjs/lusca/blob/master/LICENSE.txt) First: ```sh # In your sails app npm install lusca --save ``` Then, in the `middleware` config object in `config/http.js`: ```js // ... // maxAge ==> Number of seconds strict transport security will stay in effect. xframe: require('lusca').xframe('SAMEORIGIN'), // ... order: [ // ... 'xframe' // ... ] ``` ### Additional Resources + [Clickjacking (OWasp)](https://www.owasp.org/index.php/Clickjacking) ================================================ FILE: docs/concepts/Security/ContentSecurityPolicy.md ================================================ # Content security policy [Content Security Policy (CSP)](https://www.owasp.org/index.php/Clickjacking) is a [W3C specification](https://w3c.github.io/webappsec/specs/content-security-policy) for instructing the client browser as to which location and/or which type of resources are allowed to be loaded. This spec uses "directives" to define loading behaviors for target resource types. Directives can be specified using HTTP response headers or HTML `` tags. ### Enabling CSP ##### Using [lusca](https://github.com/krakenjs/lusca#luscacspoptions) > `lusca` is open-source under the [Apache license](https://github.com/krakenjs/lusca/blob/master/LICENSE.txt) First: ```sh # In your sails app npm install lusca --save --save-exact ``` Then add `csp` in [`config/http.js`](https://sailsjs.com/anatomy/config/http-js): ```js // ... csp: require('lusca').csp({ policy: { 'default-src': '*' } }), // ... order: [ // ... 'csp', // ... ] ``` ##### Supported directives To give you an idea how this works, here's a snapshot of supported CSP directives, as of 2017: | Directive | | |:--------------- |:-------------------------- | | default-src | Loading policy for all resources type in case a resource type dedicated directive is not defined (fallback) | | script-src | Defines which scripts the protected resource can execute | | object-src | Defines from where the protected resource can load plugins | | style-src | Defines which styles (CSS) the user applies to the protected resource | | img-src | Defines from where the protected resource can load images | | media-src | Defines from where the protected resource can load video and audio | | frame-src | Defines from where the protected resource can embed frames | | font-src | Defines from where the protected resource can load fonts | | connect-src | Defines which URIs the protected resource can load using script interfaces | | form-action | Defines which URIs can be used as the action of HTML form elements | | sandbox | Specifies an HTML sandbox policy that the user agent applies to the protected resource | | script-nonce | Defines script execution by requiring the presence of the specified nonce on script elements | | plugin-types | Defines the set of plugins that can be invoked by the protected resource by limiting the types of resources that can be embedded | | reflected-xss | Instructs a user agent to activate or deactivate any heuristics used to filter or block reflected cross-site scripting attacks, equivalent to the effects of the non-standard X-XSS-Protection header | | report-uri | Specifies a URI to which the user agent sends reports about policy violation | > For more information, see the [W3C CSP Spec](https://w3c.github.io/webappsec/specs/content-security-policy/). ##### Browser compatibility Different CSP response headers are supported by different browsers. For example, `Content-Security-Policy` is the W3C standard, but various versions of Chrome, Firefox, and IE use `X-Content-Security-Policy` or `X-WebKit-CSP`. For the latest information on browser support, see [OWasp](https://www.owasp.org/index.php/Content_Security_Policy). ### Additional Resources + [Content Security Policy (OWasp)](https://www.owasp.org/index.php/Content_Security_Policy) + Learn more about installing HTTP middleware in [Concepts > Middleware](https://sailsjs.com/documentation/concepts/middleware). ================================================ FILE: docs/concepts/Security/DDOS.md ================================================ # DDOS The prevention of [denial of service attacks](https://www.owasp.org/index.php/Application_Denial_of_Service) is a [complex problem](http://en.wikipedia.org/wiki/Denial-of-service_attack#Handling) which involves multiple layers of protection, up and down the networking stack. This type of attack has achieved [notoriety](http://www.darkreading.com/vulnerabilities-and-threats/10-strategies-to-fight-anonymous-ddos-attacks/d/d-id/1102699) in recent years due to widespread media coverage of groups like Anonymous. At the API layer, there isn't much that can be done in the way of prevention. However, Sails offers a few settings to mitigate certain types of DDOS attacks: + The session in Sails can be [configured](https://sailsjs.com/documentation/reference/configuration/sails-config-session) to use a separate session store (e.g. [Redis](http://redis.io/)), allowing your application to run without relying on the memory state of any one API server. This means that multiple copies of your Sails app may be deployed to as many servers as is necessary to handle traffic. This is achieved by using a [load balancer](https://en.wikipedia.org/wiki/Load_balancing_(computing) ), which directs each incoming request to a free server with the resources to handle it, eliminating any one app server as a single point of failure. + Socket.io connections may be [configured](https://sailsjs.com/docs/reference/configuration/sails-config-sockets) to use a separate [socket store](sailsjs.com/docs/concepts/deployment/scaling) (e.g. Redis) for managing pub/sub state and message queueing. This eliminates the need for sticky sessions at the load balancer, preventing would-be attackers from directing their attacks against the same server again and again. > Note that, if you have the long-polling transport enabled in [sails.config.sockets](https://sailsjs.com/documentation/reference/configuration/sails-config-sockets), you'll still want to make sure TCP sticky sessions are enabled at your load balancer. For more on that, check out this writeup about [sockets on Deis and Kubernetes](https://deis.com/blog/2016/socket.io-applications-kubernetes/). ### Additional Resources + [Backpressure and Unbounded Concurrency in Node.js](http://engineering.voxer.com/2013/09/16/backpressure-in-nodejs/) ([Voxer](http://voxer.com/)) + [Building a Node.js Server That Won't Melt](https://hacks.mozilla.org/2013/01/building-a-node-js-server-that-wont-melt-a-node-js-holiday-season-part-5/) ([Mozilla](https://hacks.mozilla.org/)) + [Security in Node.js](https://www.harrytorry.co.uk/node-js/security-flaws-in-node-js/) - see the "Denial of Service" section ([Harry Torry](https://www.harrytorry.co.uk)) + [Slowloris DDoSAttacks](http://www.ddosattacks.biz/attacks/slowloris-ddos-attack-aka-slow-and-low/) ================================================ FILE: docs/concepts/Security/P3P.md ================================================ # P3P ### Background P3P stands for the "Platform for Privacy Preferences" and is a browser/web standard designed to facilitate better consumer web privacy control. Currently (as of 2014), out of all the major browsers, only Internet Explorer supports it. P3P most often comes into play when dealing with legacy applications. Many modern organizations are willfully ignoring P3P. Here's what [Facebook has to say](https://www.facebook.com/help/327993273962160/) on the subject: > The organization that established P3P, the World Wide Web Consortium, suspended its work on this standard several years ago because most modern web browsers don't fully support P3P. As a result, the P3P standard is now out of date and doesn't reflect technologies that are currently in use on the web, so most websites currently don't have P3P policies. > > See also: http://www.zdnet.com/blog/facebook/facebook-to-microsoft-p3p-is-outdated-what-else-ya-got/9332 ### Supporting P3P with Sails All of that aside, sometimes you have to support P3P anyways. Fortunately, a few different modules exist that bring P3P support to Express and Sails by enabling the relevant P3P headers. To use one of these modules for handling P3P headers, install it from npm using the directions below, then open `config/http.js` in your project and configure it as a custom middleware. To do that, define your P3P middleware as "p3p", and add the string "p3p" to your `middleware.order` array wherever you'd like it to run in the middleware chain (a good place to put it might be right before `cookieParser`): E.g. in `config/http.js`: ```js // ..... module.exports.http = { middleware: { p3p: require('p3p')(p3p.recommmended), // <==== set up the custom middleware here and named it "p3p" order: [ 'startRequestTimer', 'p3p', // <============ configured the order of our "p3p" custom middleware here 'cookieParser', 'session', 'bodyParser', 'handleBodyParserError', 'compress', 'methodOverride', 'poweredBy', '$custom', 'router', 'www', 'favicon', '404', '500' ], // ..... } }; ``` Check out the examples below for more guidance, and be sure and follow the links to see the docs for the module you're using for the latest information, comparative analysis of its features, any recent bug fixes, and advanced usage details. ##### Using [node-p3p](https://github.com/troygoode/node-p3p) > `node-p3p` is open-source under the [MIT license](https://github.com/troygoode/node-p3p/blob/master/LICENSE). ```sh # In your sails app npm install p3p --save ``` Then in the `middleware` config object in `config/http.js`: ```js // ... // node-p3p provides a recommended compact privacy policy out of the box p3p: require('p3p')(require('p3p').recommended) // ... ``` ##### Using [lusca](https://github.com/krakenjs/lusca#luscap3pvalue) > `lusca` is open-source under the [Apache license](https://github.com/krakenjs/lusca/blob/master/LICENSE.txt) ```sh # In your sails app npm install lusca --save ``` Then in the `middleware` config object in `config/http.js`: ```js // ... // "ABCDEF" ==> The compact privacy policy to use. p3p: require('lusca').p3p('ABCDEF') // ... ``` ### Additional Resources: + [Description of the P3P Project (Microsoft)](http://support.microsoft.com/kb/290333) + ["P3P Work suspended" (W3C)](http://www.w3.org/P3P/) + [P3P Compact Specification (CompactPrivacyPolicy.org)](http://compactprivacypolicy.org/compact_specification.htm) ================================================ FILE: docs/concepts/Security/Security.md ================================================ # Security ### Overview Sails and Express provide built-in, easily configurable protection against most known types of web-application-level attacks. > **Note**: If you believe you have found a security vulnerability in Sails, please refer to our [security policy](https://sailsjs.com/security) for instructions for reporting it. ### Security topics Learn about several different types of attacks that Node.js/Sails helps prevent out of the box, and how to enable and configure security settings in your app: + [CORS](https://sailsjs.com/documentation/concepts/security/cors) + [DDOS](https://sailsjs.com/documentation/concepts/security/ddos) + [CSRF](https://sailsjs.com/documentation/concepts/security/csrf) + [Clickjacking](https://sailsjs.com/documentation/concepts/security/clickjacking) + [P3P](https://sailsjs.com/documentation/concepts/security/p3p) + [Content Security Policy](https://sailsjs.com/documentation/concepts/security/content-security-policy) + [Socket hijacking](https://sailsjs.com/documentation/concepts/security/socket-hijacking) + [XSS](https://sailsjs.com/documentation/concepts/security/xss) + [Strict Transport Security](https://sailsjs.com/documentation/concepts/security/strict-transport-security) ================================================ FILE: docs/concepts/Security/SocketHijacking.md ================================================ # Socket hijacking Unfortunately, cross-site request forgery attacks are not limited to the HTTP protocol. WebSocket hijacking (sometimes known as [CSWSH](http://www.christian-schneider.net/CrossSiteWebSocketHijacking.html)) is a commonly overlooked vulnerability in most realtime applications. Fortunately, since Sails treats both HTTP and WebSocket requests as first-class citizens, its built-in [CSRF protection](https://sailsjs.com/documentation/concepts/security/csrf) and [configurable CORS rulesets](https://sailsjs.com/documentation/concepts/security/cors) apply to both protocols. You can prepare your Sails app against CSWSH attacks by enabling the built-in protection in [`config/security.js`](https://sailsjs.com/documentation/anatomy/config/security.js) and ensuring that a `_csrf` token is sent with all relevant incoming socket requests. Additionally, if you're planning on allowing sockets to connect to your Sails app cross-origin (i.e. from a different domain, subdomain, or port) you'll want to configure your CORS settings accordingly. You can also define the `authorization` setting in [`config/sockets.js`](https://sailsjs.com/documentation/anatomy/config/sockets.js) as a custom function which allows or denies the initial socket connection based on your needs. #### Notes + CSWSH prevention is only a concern in scenarios where people use the same client application to connect sockets to multiple web services (e.g. cookies in a browser like Google Chrome can be used to connect a socket to Chase.com from both Chase.com and Horrible-Hacker-Site.com.) ================================================ FILE: docs/concepts/Security/StrictTransportSecurity.md ================================================ # HTTP Strict Transport Security Strict Transport Security (STS) is an opt-in security enhancement that forces usage of `HTTPS` instead of `HTTP` (in modern browsers, at least). ### Enabling STS Implementing STS is actually very simple and [only takes a few lines of code](https://github.com/krakenjs/lusca/blob/master/lib/hsts.js). Better yet, a few different open-source modules exist that bring support for this feature to Express and Sails. To use one of these modules, install it from npm using the directions below, then open `config/http.js` in your project and [configure it as a custom middleware](https://sailsjs.com/documentation/concepts/Middleware). The example below covers basic usage and configuration. For more guidance and advanced usage details, be sure and follow the link to the docs. ##### Using [lusca](https://github.com/krakenjs/lusca#luscahstsoptions) > `lusca` is open-source under the [Apache license](https://github.com/krakenjs/lusca/blob/master/LICENSE.txt) ```sh # In your sails app npm install lusca --save ``` Then in the `middleware` config object in `config/http.js`: ```js // ... // maxAge ==> Number of seconds strict transport security will stay in effect. strictTransportSecurity: require('lusca').hsts({ maxAge: 31536000 }) // ... ``` ### Additional Resources + [HTTP Strict Transport Security (OWasp)](https://www.owasp.org/index.php/HTTP_Strict_Transport_Security) ================================================ FILE: docs/concepts/Security/XSS.md ================================================ # XSS Cross-site scripting (XSS) is a type of attack in which a malicious agent manages to inject client-side JavaScript into your website, so that it runs in the trusted environment of your users' browsers. ### Protecting against XSS attacks The cleanest way to prevent XSS attacks is to escape untrusted data _at the point of injection_. That means at the point where it's actually being injected into the HTML. #### On the server ##### When injecting data into a server-side view... Use `<%= %>` to HTML-encode data: ```html

Hello <%= me.username %>!

<%= owner.username %>'s projects:

``` ##### When exposing view locals to client-side JavaScript... Use the `exposeLocalsToBrowser` partial to safely expose some or all of your view locals to client-side JavaScript: ```html <%- exposeLocalsToBrowser(); %> ' // } // ], // _csrf: 'oon95Uac-wKfWQKC5pHx1rP3HsiN9tjqGMyE' // } ``` > Note that when you use this strategy, the strings in your view locals are no longer HTML unescaped after being exposed to client-side JavaScript. > That's because you'll want to escape them _again_ when you stick them in the DOM. If you always escape at the point of injection, this stuff is a > lot easier to keep track of. This way, you know you can safely escape _any_ string you inject into the DOM from your client-side JavaScript. > (More on that below.) #### On the client A lot of XSS prevention is about what you do in your client-side code. Here are a few examples: ##### When injecting data into a client-side JST template... Use `<%- %>` to HTML-encode data: ```html

Hello <%- me.username %>!

``` ##### When modifying the DOM with client-side JavaScript... Use something like `$(...).text()` to HTML-encode data: ```js var $welcomeMsg = $('#signup').find('[is="welcome-msg"]'); welcomeMsg.text('Hello, '+window.SAILS_LOCALS.me.username+'!'); // Avoid using `$(...).html()` to inject untrusted data. // Even if you know an XSS is not possible under particular circumstances, // accidental escaping issues can cause really, really annoying client-side bugs. ``` > As you've probably figured out, the example above assumes you are using jQuery, but the same concepts apply regardless of what front-end library you are using. ### Additional Resources + [XSS (OWasp)](https://www.owasp.org/index.php/XSS) + [XSS Prevention Cheatsheet](https://www.owasp.org/index.php/XSS_Prevention_Cheat_Sheet) ### Notes > + The examples above assume you are using the default view engine (EJS) and client-side JST/Lodash templates from the default asset pipeline. ================================================ FILE: docs/concepts/Services/Services.md ================================================ # Services > _**Note**_: Although Services are still fully supported in Sails 1.0, it is recommended that you use [helpers](https://sailsjs.com/documentation/concepts/helpers) instead. **Services** are stateless libraries of functions that you can use from anywhere in your Sails app. For example, you might have an `EmailService` which tidily wraps up one or more utility functions so you can use them in more than one place within your application. Another benefit of using services in Sails is that they are *globalized*, which means that you don't have to use `require()` to access them, although you can if you prefer (you can also disable the automatic exposure of global variables in your app's configuration). By default, you can access a service and call its functions (e.g. `EmailService.sendHtmlEmail()` or `EmailService.sendPasswordRecoveryEmail()`) from anywhere: within controller actions, from inside other services, in custom model methods, or even from command-line scripts. Hypothetically, one could create a service for: - Sending an email - Blasting tweets to celebrities - Retrieving data from a third party API But [helpers](https://sailsjs.com/documentation/concepts/helpers) are a better bet. ================================================ FILE: docs/concepts/Sessions/sessions.md ================================================ # How sessions work in Sails (advanced) For our purposes, **sessions** are defined to be a few components that together allow you to store information about a user agent between requests. > A **user agent** is the software (browser or native application) that represents you on a device (e.g. a browser tab on your computer, a smartphone application, or your refrigerator). It is associated one-to-one with a cookie or access token. Sessions can be very useful because the request/response cycle is **stateless**. The request/response cycle is considered stateless because neither the client nor the server inherently stores any information between different requests about a particular request. Therefore, the lifecycle of a request/response ends when a response is made to the requesting user agent (e.g. `res.send()`). Note: we’re going to discuss sessions in the context of a browser user agent. While you can use sessions in Sails for whatever you like, it is generally a best practice to use them purely for storing the state of user agent authentication. Authentication is a process that allows a user agent to prove that they have a certain identity. For example, in order to access some protected functionality, I might need to prove that my browser tab actually corresponds with a particular user record in a database. If I provide you with a unique name and a password, you can look up the name and compare my password with a stored (hopefully [encrypted](http://node-machine.org/machinepack-passwords/encrypt-password)) password. If there's a match, I'm authenticated. But how do you store that "authenticated-ness" between requests? That's where sessions come in. ### What sessions are made of There are three main components to the implementation of sessions in Sails: 1. the **session store** where information is retained 2. the middleware that manages the session 3. a cookie that is sent along with every request and stores a session id (by default, `sails.sid`) The **session store** can either be in memory (this is the default Sails session store) or in a database (Sails has built-in support for using Redis for this purpose). Sails builds on top of Connect middleware to manage the session, which includes using a **cookie** to store a session id (`sid`) on the user agent. ### A day in the life of a request, a response, and a session When a request is sent to Sails, the request header is parsed by the session middleware. ##### Scenario 1: The request header has no cookie If the header does not contain a cookie, a `sid` is created in the session and a default session dictionary is added to `req` (e.g. `req.session`). At this point you can make changes to the session property (usually in a controller/action). For example, let's look at the following login action: ```javascript module.exports = { login: function(req, res) { // Authentication code here // If successfully authenticated req.session.userId = foundUser.id; // returned from a database return res.json(foundUser); } } ``` Here we added a `userId` property to `req.session`. > **Note:** the property will not be stored in the session store, nor will it be available to other requests until the response is sent. Once the response is sent, any new requests will have access to `req.session.userId`. Since we didn't have a cookie in the request header, a cookie will be established for us. ##### Scenario 2: The request header has a cookie with a `Sails.sid` Now when the user agent makes the next request, the `Sails.sid` stored on the cookie is checked for authenticity. If it matches an existing `sid` in the session store, the contents of the session store are added as a property on the `req` dictionary (`req.session`). We can access properties on `req.session` (e.g. `req.session.userId`) or set properties on it (e.g. `req.session.userId == someValue`). The values in the session store might change, but the `Sails.sid` and `sid` generally do not. ### When does the `Sails.sid` change? During development, the Sails session store is in memory. Therefore, when you close the Sails server, the current session store disappears. When Sails is restarted, although a user agent request contains a `Sails.sid` in the cookie, the `sid` is no longer in the session store. Therefore, a new `sid` will be generated and replaced in the cookie. The `Sails.sid` will also change if the user agent cookie expires or is removed. >The lifespan of a Sails cookie can be changed from its default setting (never expires) to a new setting by accessing the `cookie.maxAge` property in `projectName/config/session.js`. ### Using Redis as the session store Redis is a key-value database package that can be used as a session store that is separate from the Sails instance. This configuration for sessions has two benefits. The first is that the session store will remain viable between Sails restarts. The second is that if you have multiple Sails instances behind a load balancer, all of the instances can point to a single consolidated session store. #### Enabling Redis session store in development To enable Redis as your session store in development, first make sure you have a local Redis instance running on your machine (`redis-server`). Then, lift your app with `sails lift --redis`. This is just a shortcut for `sails lift --session.adapter=@sailshq/connect-redis --sockets.adapter=@sailshq/socket.io-redis`. These packages are included as dependencies of new Sails apps by default, but if you're working with an upgraded app you'll need to `npm install @sailshq/connect-redis` and `npm install @sailshq/socket.io-redis`. > Note that this built-in configuration uses your local Redis instance. For advanced session configuration options, see [Reference > Configuration > sails.config.session](https://sailsjs.com/documentation/reference/configuration/sails-config-session). #### Nerdy details of how the session cookie is created The value for the cookie is created by first hashing the `sid` with a configurable *secret* which is just a long string. > You can change the session `secret` property in `projectName/config/session.js`. The Sails `sid` (e.g. `Sails.sid`) then becomes a combination of the plain `sid` followed by a hash of the `sid` plus the `secret`. To take this out of the world of abstraction, let's use an example. Sails creates a `sid` of `234lj232hg234jluy32UUYUHH` and a `session secret` of `9238cca11a83d473e10981c49c4f`. These values are simply two strings that Sails combines and hashes to create a `signature` of `AuSosBAbL9t3Ev44EofZtIpiMuV7fB2oi`. So the `Sails.sid` becomes `234lj232hg234jluy32UUYUHH.AuSosBAbL9t3Ev44EofZtIpiMuV7fB2oi` and is stored in the user agent cookie by sending a `set-cookie` property in the response header. **What does this prevent?** This prevents a user from guessing the `sid`. It also prevents a evildoer from spoofing a user into making an authetication request with a `sid` that the evildoer knows. This could allow the evildoer to use the `sid` to do bad things while the user is authenticated via the session. ### Disabling sessions Even if your Sails app is designed to be accessed by non-browser clients, such as toasters, you are strongly encouraged to use sessions for authentication. While it can sometimes be complex to understand, the built-in session mechanism in Sails (session store + HTTP-only cookies) is a tried and true solution that is generally [less brittle, easier to use, and lower-risk than rolling out something yourself](http://cryto.net/~joepie91/blog/2016/06/13/stop-using-jwt-for-sessions/). That said, sessions may not always be an option (for example, if you must [integrate with a different authentication scheme](https://github.com/sails101/jwt-login) like JWT). In these cases, you can disable sessions on an app-wide or per-request basis. ##### Disabling sessions for your entire app To entirely turn off session support for your app, add the following to your `.sailsrc` file: ```javascript "hooks": { "session": false } ``` This disables the core Sails session hook. You can also accomplish this by setting the `sails_hooks__session` environment variable to `false`. ##### Disabling sessions for certain requests To turn off session support on a per-route (or per-request) basis, use the [`sails.config.session.isSessionDisabled` setting](https://sailsjs.com/documentation/reference/configuration/sails-config-session#?properties). By default, Sails enables session support for all requests except those that [look like](https://sailsjs.com/documentation/reference/application/advanced-usage/sails-looks-like-asset-rx) they're pointed at static assets like images, stylesheets, etc. ================================================ FILE: docs/concepts/Testing/Testing.md ================================================ # Testing your code This section of the documentation runs through how you might go about testing your Sails application. There are countless test frameworks and assertion libraries for Sails and Node.js; pick one that fits your needs. > There is no official strategy for testing in the Sails framework, and this page is a collaborative, community-driven guide that has not been thoroughly vetted by Sails core team members. If you run across something that seems confusing or incorrect, feel free to submit a pull request. ### Preparation For our example test suite, we'll use [mocha](http://mochajs.org/). ```bash npm install mocha --save-dev ``` Before you start building your test cases, organize your `test/` directory structure. Once again, when it comes to automated testing, there are several different organizational approaches you might choose. For this example, we'll go about it as follows: ```bash ./myApp ├── api/ ├── assets/ ├── ... ├── test/ │ ├── integration/ │ │ ├── controllers/ │ │ │ └── UserController.test.js │ │ ├── models/ │ │ │ └── User.test.js │ │ └── helpers/ | | └── ... │ ├── fixtures/ | │ └── ... │ ├── lifecycle.test.js │ └── mocha.opts ├── ... └── views/ ``` ##### lifecycle.test.js This file is useful when you want to execute some code before and after running your tests (e.g. lifting and lowering your Sails application). Since your models are converted to Waterline collections on lift, it is necessary to lift your Sails app before trying to test them (this applies controllers and other parts of your app, too, so be sure to call this file first). ```javascript var sails = require('sails'); // Before running any tests... before(function(done) { // Increase the Mocha timeout so that Sails has enough time to lift, even if you have a bunch of assets. this.timeout(5000); sails.lift({ // Your Sails app's configuration files will be loaded automatically, // but you can also specify any other special overrides here for testing purposes. // For example, we might want to skip the Grunt hook, // and disable all logs except errors and warnings: hooks: { grunt: false }, log: { level: 'warn' }, }, function(err) { if (err) { return done(err); } // here you can load fixtures, etc. // (for example, you might want to create some records in the database) return done(); }); }); // After all tests have finished... after(function(done) { // here you can clear fixtures, etc. // (e.g. you might want to destroy the records you created above) sails.lower(done); }); ``` ##### mocha.opts This file is optional. You can use it as an alternative to command-line options for specifying [custom Mocha configuration](https://mochajs.org/#mochaopts). One notable customization option is timeout. The default timeout in Mocha is 2 seconds, which is sufficient for most test cases but may be too short depending on how often your tests are lifting and lowering Sails. To ensure that Sails lifts in time to finish your first test, you may need to increase the timeout value in mocha.opts: ```bash --timeout 10000 ``` > **Note**: If you are writing your tests in a transpiled language such as CoffeeScript (`.coffee` files instead of `.js` files), you'll need to take an extra step to configure Mocha accordingly. For example, you might add these lines to your `mocha.opts`: > > ```bash > --require coffee-script/register > --compilers coffee:coffee-script/register > ``` > > _If you prefer Typescript, the approach is basically the same, except you'll want to use `--require ts-node/register`. ### Writing tests Once you have prepared your directory, you can start writing your integration tests: ```js // ./test/integration/models/User.test.js var util = require('util'); describe('User (model)', function() { describe('#findBestStudents()', function() { it('should return 5 users', function (done) { User.findBestStudents() .then(function(bestStudents) { if (bestStudents.length !== 5) { return done(new Error( 'Should return exactly 5 students -- the students '+ 'from our test fixtures who are considered the "best". '+ 'But instead, got: '+util.inspect(bestStudents, {depth:null})+'' )); }//-• return done(); }) .catch(done); }); }); }); ``` ### Testing actions & controllers The most fundamental tests for your backend code involve sending an HTTP request and checking the response. There are numerous ways to go about this, whether it's a full-fledged testing tool, like Supertest, or a pure utility like [`request`](https://npmjs.com/package/request) or [`mp-http`](https://npmjs.com/package/machinepack-http), combined with [`assert`](https://nodejs.org/dist/latest/docs/api/assert.html). ##### Using Supertest Let's take [Supertest](https://github.com/visionmedia/supertest) for a spin: ```bash npm install supertest --save-dev ``` The idea behind Supertest is to provide a high-level tool that helps build a specific type of test—specifically, the type of test that send an HTTP request to your Sails app and checks the response. ```js // test/integration/controllers/UserController.test.js var supertest = require('supertest'); describe('UserController.login', function() { describe('#login()', function() { it('should redirect to /my/page', function (done) { supertest(sails.hooks.http.app) .post('/users/login') .send({ name: 'test', password: 'test' }) .expect(302) .expect('location','/my/page', done); }); }); }); ``` ### Running tests In order to run your test using Mocha, you'll have to use `mocha` in the command line then pass as arguments any test you want to run. Be sure to call lifecycle.test.js before the rest of your tests, like this: `mocha test/lifecycle.test.js test/integration/**/*.test.js` ##### Using `npm test` to run your test You can modify your package.json file to use `npm test` instead of Mocha, and thus avoid typing out the Mocha command described above. This is particularly useful when calling lifecycle.test.js. On the scripts dictionary, add a `test` key and use the following as its value: `mocha test/lifecycle.test.js test/integration/**/*.test.js`. This looks like: ```json "scripts": { "start": "node app.js", "debug": "node debug app.js", "test": "node ./node_modules/mocha/bin/mocha test/lifecycle.test.js test/integration/**/*.test.js" } ``` The `*` is a wildcard used to match any file inside the `integration/` folder that ends in `.test.js`. If it suits you, you can modify it to search for `*.spec.js` instead. In the same way, you can use wildcards for your folders by using two `*` instead of one. > As of Sails v1, Sails apps are generated with a `test` script already in their package.json file, but you'll still want to make some modifications to it for this example. If you're upgrading an existing app, you may have to add a `test` key by hand. ### Continuous integration If you'd like to have a system automatically run your tests every time you push to your source code repository, you're in luck! Many different continuous integration systems support Sails/Node.js, so you can have your pick. Here are a few popular choices to get you started: + [Circle CI](https://circleci.com/) + [Travis CI](http://travis-ci.com) + [Semaphore CI](https://semaphoreci.com/) + [Appveyor](http://appveyor.com) _(useful if you'll be deploying to a Windows server)_ > All of the options above charge a monthly fee for proprietary apps but are free for open source. Circle CI is free for proprietary apps as well, but throttled to two builds at a time. Semaphore is also free and and allows you 4x parallel CI/CD jobs. ### Load testing A [number of commercial options](http://www.bing.com/search?q=load+testing) exist for load testing web applications. You can also get a reasonable idea of how your app will perform using tools like [`ab`](http://httpd.apache.org/docs/2.4/programs/ab.html) or [JMeter](http://jmeter.apache.org/). Just remember, the goal is to simulate real traffic. For more help setting up your Sails app to be production-ready and scalable, see [Scalability](https://sailsjs.com/documentation/concepts/deployment/scaling). For additional help or more specific questions, click [here](https://sailsjs.com/support). ### Optimizing performance Usually, the scalability and overall performance of your app is more important than the performance and latency of any given individual request to a particular endpoint. So rather than focusing on one piece of code in isolation, we recommend starting with [the basics](https://sailsjs.com/documentation/concepts/deployment/scaling); for most apps, that's good enough. For some use cases (e.g. serving ads, or apps with very computationally-intensive functionality), though, individual request latency may be important from the get-go. For testing the performance of particular chunks of code, or for benchmarking the latency of individual requests to particular endpoints, a great option is [benchmark.js](https://www.npmjs.com/package/benchmark). Not only is it a robust library that supports high-resolution timers and returns statistically significant results, it also works great with Mocha out of the box. ================================================ FILE: docs/concepts/Views/Layouts.md ================================================ # Layouts When building an app with many different pages, it can be helpful to extrapolate markup shared by several HTML files into a layout. This [reduces the total amount of code](http://en.wikipedia.org/wiki/Don't_repeat_yourself) in your project and helps you avoid making the same changes in multiple files down the road. In Sails and Express, layouts are implemented by the view engines themselves. For instance, `jade` has its own layout system, with its own syntax. For convenience, Sails bundles special support for layouts **when using the default view engine, EJS**. If you'd like to use layouts with a different view engine, check out [that view engine's documentation](https://sailsjs.com/documentation/concepts/views/view-engines) to find the appropriate syntax. ### Creating layouts Sails layouts are special `.ejs` files in your app's `views/` folder you can use to "wrap" or "sandwich" other views. Layouts usually contain the preamble (e.g. `....`) and conclusion (``). The original view file is included using `<%- body %>`. Layouts are never used without a view: that would be like serving someone a bread sandwich. Layout support for your app can be configured or disabled in [`config/views.js`](https://sailsjs.com/documentation/anatomy/config/views.js), and it can be overridden for a particular route or action by setting a special [local](https://sailsjs.com/documentation/concepts/views/locals) called `layout`. By default, Sails will compile all views using the layout located at `views/layouts/layout.ejs`. To specify what layout a view uses, see the example below. There is more information in the docs at [routes](https://sailsjs.com/documentation/concepts/routes). The example route below will use the view located at `./views/users/privacy.ejs` within the layout located at `./views/users.ejs` ```javascript 'get /privacy': { view: 'users/privacy', locals: { layout: 'users' } }, ``` The example controller action below will use the view located at `./views/users/privacy.ejs` within the layout located at `./views/users.ejs` ```javascript privacy: function (req, res) { res.view('users/privacy', {layout: 'users'}) } ``` ### Notes > #### Why do layouts only work for EJS? > A couple of years ago, built-in support for layouts/partials was deprecated in Express. Instead, developers were expected to rely on the view engines themselves to implement this feature. (See https://github.com/balderdashy/sails/issues/494 for more information.) > > Sails supports the legacy `layouts` feature for convenience, backwards compatibility with Express 2.x and Sails 0.8.x apps, and in particular, familiarity for new community members coming from other MVC frameworks. As a result, layouts have only been tested with the default view engine (ejs). > > If layouts aren’t your thing, or (for now) if you’re using a server-side view engine other than ejs, (e.g. Jade, handlebars, haml, dust) you’ll want to set `layout:false` in [`sails.config.views`](https://sailsjs.com/documentation/reference/configuration/sails-config-views) and rely on your view engine’s custom layout/partial support. ================================================ FILE: docs/concepts/Views/Locals.md ================================================ # Locals The variables accessible in a particular view are called `locals`. Locals represent server-side data that is _accessible_ to your view—locals are not actually _included_ in the compiled HTML unless you explicitly reference them using special syntax provided by your view engine. ```ejs
Logged in as <%= user.fullName %>.
``` ### Using locals in your views The notation for accessing locals varies between view engines. In EJS, you use special template markup (e.g. `<%= someValue %>`) to include locals in your views. There are three kinds of template tags in EJS: + `<%= someValue %>` + HTML-escapes the `someValue` local, and then includes it as a string. + `<%- someRawHTML %>` + Includes the `someRawHTML` local verbatim, without escaping it. + Be careful! This tag can make you vulnerable to XSS attacks if you don't know what you're doing. + `<% if (!loggedIn) { %> Logout <% } %>` + Runs the JavaScript inside the `<% ... %>` when the view is compiled. + Useful for conditionals (`if`/`else`), and looping over data (`for`/`each`). Here's an example of a view (`views/backOffice/profile.ejs`) using two locals, `user` and `corndogs`: ```ejs

<%= user.fullName %>'s first view

My corndog collection:

    <% for (let corndog of corndogs) { %>
  • <%= _.capitalize(corndog.name) %>
  • <% } %>
``` > You might have noticed another local: `_`. By default, Sails passes down a few locals to your views automatically, one of which is lodash (`_`). If the data you wanted to pass down to this view was completely static, you wouldn't necessarily need a controller. Instead, you could hard-code the view and its locals in your `config/routes.js` file, like so: ```javascript // ... 'get /profile': { view: 'backOffice/profile', locals: { user: { fullName: 'Frank', emailAddress: 'frank@enfurter.com' }, corndogs: [ { name: 'beef corndog' }, { name: 'chicken corndog' }, { name: 'soy corndog' } ] } }, // ... ``` More likely, though, this data will be dynamic. In this scenario, we'd need to use a controller action to load the data from our models, then pass it to the view using the [res.view()](https://sailsjs.com/documentation/reference/response-res/res-view) method. Assuming we hooked up our route to one of our controller's actions (and our models were set up), we might send down our view like this: ```javascript // in api/controllers/UserController.js... profile: function (req, res) { // ... return res.view('backOffice/profile', { user: theUser, corndogs: theUser.corndogCollection }); }, // ... ``` ### Escaping untrusted data using `exposeLocalsToBrowser` It is often desirable to “bootstrap” data onto a page so that it’s available via Javascript as soon as the page loads, rather than having to fetch the data in a separate AJAX or socket request. Sites like [Twitter and GitHub](https://blog.twitter.com/2012/improving-performance-on-twittercom) rely heavily on this approach in order to optimize page load times and provide an improved user experience. Historically, this problem was commonly solved using hidden form fields or by hand-rolling code that injected server-side locals directly into a client-side script tag. While effective, these techniques can present challenges when some of the data to be bootstrapped is from an _untrusted_ source that might contain HTML tags and Javascript code meant to compromise your app with an XSS attack. To prevent situations like this, Sails provides a built-in view partial called `exposeLocalsToBrowser` that you can use to securely inject data from your view locals for access from client-side JavaScript. To use `exposeLocalsToBrowser`, simply call it from within your view using the _non-escaping syntax_ for your template language. For example, using the default EJS view engine: ```ejs <%- exposeLocalsToBrowser() %> ``` By default, this exposes _all_ of your view locals as the `window.SAILS_LOCALS` global variable. For example, if your action code contained: ```javascript res.view('myView', { someString: 'hello', someNumber: 123, someObject: { owl: 'hoot' }, someArray: [1, 'boot', true], someBool: false someXSS: '' }); ``` then using `exposeLocalsToBrowser` as shown above would cause the locals to be safely bootstrapped in such a way that `window.SAILS_LOCALS.someArray` would contain the array `[1, 'boot', true]`, and `window.SAILS_LOCALS.someXSS` would contain the _string_ `` without causing that code to actually be executed on the page. The `exposeLocalsToBrowser` function has a single `options` parameter that can be used to configure what data is outputted, and how. The `options` parameter is a dictionary that can contain the following properties: |  | Property | Type | Default| Details | |---|:--------------------|----------------------------------------------|:-----------------------------------|-----| | 1 | _keys_ | ((array?)) | `undefined` | A “whitelist” of locals to expose. If left undefined, _all_ locals will be exposed. If specified, this should be an array of property names from the locals dictionary. For example, given the `res.view()` statement shown above, setting `keys: ['someString', 'someBool']` would cause `windows.SAILS_LOCALS` to be set to `{someString: 'hello', someBool: false}`. | 2 | _namespace_ | ((string?)) | `SAILS_LOCALS` | The name of the global variable to which the bootstrapped data should be assigned. | 3| _dontUnescapeOnClient_ | ((boolean?)) | false | **Advanced. Not recommended for most apps.** If set to `true`, any string values that were escaped to avoid XSS attacks will _still be escaped_ when accessed from client-side JS, instead of being transformed back into the original value. For example, given the `res.view()` statement from the example above, using `exposeLocalsToBrowser({dontUnescapeOnClient: true})` would cause `window.SAILS_LOCALS.someXSS` to be set to `<script>alert('hello!');`. ================================================ FILE: docs/concepts/Views/Partials.md ================================================ # Partials When using the default view engine (`ejs`), Sails supports the use of _partials_ (i.e. "view partials"). Partials are basically just views that are designed to be used from within other views. They are particularly useful for reusing the same markup between different views, layouts, and even other partials. ```ejs <%- partial('./partials/navbar.ejs') %> ``` This should render the partial located at `views/partials/navbar.ejs`, which might look something like this: ```ejs <% /** * views/partials/navbar.ejs * * > Note: This EJS comment won't show up in the ejs served to the browser. * > So you can be as verbose as you like. Just be careful not to inadvertently * > type a percent sign followed by a greater-than sign (it'll bust you out of * > the EJS block). * */%> ``` The target path that you pass in as the first argument to `partial()` should be relative from the view, layout, or partial where you call it. So if you are calling `partial()` from within a view file located at `views/pages/dashboard/user-profile.ejs`, and want to load `views/partials/widget.ejs` then you would use: ```ejs <%- partial('../../partials/navbar.ejs') %> ``` ### Partials and view locals Partials automatically inherit the view locals that are available wherever they are used. For example, if you call `partial()` within a view where a variable named `currentUser` is available, then `currentUser` will also be available within the partial: ```ejs <% /** * views/partials/navbar.ejs * * The navbar at the top of the page. * * @needs {Dictionary} currentUser * @property {Boolean} isLoggedIn * @property {String} username */%> ``` ### Overriding locals in a partial Automatic inheritance of view locals takes care of most use cases for partials, but sometimes you might want to pass in additional, dynamic data. For example, imagine your app has duplicate copies of the following code in a few different views: ```ejs <% // A list representing the currently-logged in user's inbox. %>
    <% // Display each message, with a button to delete it. _.each(messages, function (message) { %>
  • <%= message.subject %>
  • <% }); %>
``` To refactor this, you might extrapolate the `
  • ` into a partial to avoid duplicating code. But if we do that, _we cannot rely on automatic inheritance_. Partials only inherit locals that are available to the view, partial, or layout where they're called as a whole, but this `
  • ` relies on a variable called `message`, which comes from the call to [`_.each()`](https://lodash.com/docs/3.10.1#forEach). Fortunately, Sails also allows you to pass in an optional dictionary (i.e. a plain JavaScript object) of overrides as the second argument to `partial()`: ``` <%- partial(relPathToPartial, optionalOverrides) %> ``` These overrides will be accessible in the partial as local variables, where they will take precedence over any automatically inherited locals with the same variable name. Here's our example from above, refactored to take advantage of this: ```ejs <% // A list representing the currently-logged in user's inbox. %>
      <% // Display each message, with a button to delete it. _.each(messages, function (message) { %> <%- partial ('../partials/inbox-message.ejs', { message: message }) %> <% }); %>
    ``` And finally, here is our new partial representing an individual inbox message: ```ejs /** * views/partials/inbox-message.ejs * * An individual inbox message. * * @needs {Dictionary} message * @property {Number} id * @property {String} subject * */%>
  • <%= message.subject %>
  • ``` ### Notes > + Partials are rendered synchronously, so they will block Sails from serving more requests until they're done loading. It's something to keep in mind while developing your app, especially if you anticipate a large number of connections. > + Built-in support for partials in Sails is only for the default view engine, `ejs`. If you decide to customize your Sails install and use a view engine other than `ejs`, then please be aware that support for partials (sometimes known as "blocks", "includes", etc.) may or may not be included, and that the usage will vary. Refer to the documentation for your view engine of choice for more information on its syntax and conventions. ================================================ FILE: docs/concepts/Views/ViewEngines.md ================================================ # View engines The default view engine in Sails is [EJS](https://github.com/mde/ejs). ##### Swapping out the view engine To use a different view engine, you should use npm to install it in your project, then in [`config/views.js`](https://sailsjs.com/documentation/anatomy/config/views.js) set `sails.config.views.extension` to your desired file extension and `sails.config.views.getRenderFn` to a function that returns your view engine's rendering function. If your view engine is supported by [Consolidate](https://github.com/tj/consolidate.js/blob/master/Readme.md#api), you can use that in `getRenderFn` to easily access the rendering function. First, you'll need to use npm to install `consolidate` into your project, if it is not already present: ```bash npm install consolidate --save ``` After the install has completed and you have installed your view engine package, you can then set the view configuration. For example, to use [Swig](https://github.com/paularmstrong/swig) templates you would `npm install swig --save` and then add the following into [`config/views.js`](https://sailsjs.com/documentation/anatomy/config/views.js): ```javascript extension: 'swig', getRenderFn: ()=>{ // Import `consolidate`. var cons = require('consolidate'); // Return the rendering function for Swig. return cons.swig; } ``` The `getRenderFn` allows you to configure your view engine before plugging it into Sails: ```javascript extension: 'swig', getRenderFn: ()=>{ // Import `consolidate`. var cons = require('consolidate'); // Import `swig`. var swig = require('swig'); // Configure `swig`. swig.setDefaults({tagControls: ['{?', '?}']}); // Set the module that Consolidate uses for Swig. cons.requires.swig = swig; // Return the rendering function for Swig. return cons.swig; } ``` ================================================ FILE: docs/concepts/Views/Views.md ================================================ # Views ### Overview In Sails, views are markup templates that are compiled _on the server_ into HTML pages. In most cases, views are used as the response to an incoming HTTP request, e.g. to serve your home page. > Much more rarely, you can also compile a view directly into an HTML string for use in your backend code (see [`sails.renderView()`](https://github.com/balderdashy/sails/blob/master/docs/PAGE_NEEDED.md)). For instance, you might use this approach to send HTML emails, or to build big XML strings for use with a legacy API. ##### Creating a view By default, Sails is configured to use EJS ([Embedded Javascript](http://ejs.co/)) as its view engine. The syntax for EJS is highly conventional; if you've worked with php, asp, erb, gsp, jsp, etc., you'll immediately know what you're doing. If you prefer to use a different view engine, there are a multitude of options. Sails supports all of the view engines compatible with [Express](http://expressjs.com/en/guide/using-template-engines.html) via [Consolidate](https://github.com/visionmedia/consolidate.js). Views are defined in your app's [`views/`](https://sailsjs.com/documentation/anatomy/views) folder by default, but like all of the default paths in Sails, they are [configurable](https://sailsjs.com/documentation/reference/configuration/sails-config-views). If you don't need to serve dynamic HTML pages at all (say, if you're building an API for a mobile app), you can remove the directory from your app. ##### Compiling a view Anywhere you can access the `res` object (e.g. a controller action, custom response, or policy), you can use [`res.view`](https://sailsjs.com/documentation/reference/response-res/res-view) to compile one of your views, then send the resulting HTML down to the user. You can also hook up a view directly to a route in your `routes.js` file. Just indicate the relative path to the view from your app's `views/` directory. For example: ```javascript { 'get /': { view: 'pages/homepage' }, 'get /signup': { view: 'pages/signup/basic-info' }, 'get /signup/password': { view: 'pages/signup/choose-password' }, // and so on. } ``` ##### What about single-page apps? If you are building a web application for the browser, part (or all) of your navigation may take place on the client; i.e. instead of the browser fetching a new HTML page each time the user navigates around, the client-side code preloads some markup templates which are then rendered in the user's browser without needing to hit the server again directly. In this case, you have a couple of options for bootstrapping the single-page app: + Use a single view, e.g. `views/publicSite.ejs`. The advantage of this option is that you can use the view engine in Sails to pass data from the server directly into the HTML that will be rendered on the client. This is an easy way to get stuff like user data to your client-side JavaScript, without having to send AJAX/WebSocket requests from the client. + Use a single HTML page in your assets folder , e.g. `assets/index.html`. Although you can't pass server-side data directly to the client this way, the advantage of this approach is that it allows you to further decouple the client and server-side parts of your application. Note that anything in your assets folder can be moved to a static CDN (like Cloudfront or CloudFlare), allowing you to take advantage of that provider's geographically-distributed data centers to get your content closer to your users. ================================================ FILE: docs/concepts/concepts.md ================================================ # Sails.js Documentation > Core Concepts > The contents of this file are overridden automatically during compilation (please do not edit manually!) ================================================ FILE: docs/concepts/extending-sails/Adapters/Adapters.md ================================================ # Adapters ### What is an adapter? In Sails and Waterline, database adapters (often simply called "adapters", for short) allow the models in your Sails app to communicate with your database(s). In other words, when your code in a controller action or helper calls a model method like `User.find()`, what happens next is determined by the [configured adapter](https://sailsjs.com/documentation/reference/configuration/sails-config-datastores). An adapter is defined as a dictionary (aka JavaScript object, like `{}`) with methods like `find`, `create`, etc. Based on which methods it implements, and the completeness with which they are implemented, adapters are said to implement one or more **interface layers**. Each interface layer implies a contract to implement certain functionality. This allows Sails and Waterline to guarantee conventional usage patterns across multiple models, developers, apps, and even companies, making app code more maintainable, efficient, and reliable. > In previous versions of Sails, adapters were sometimes used for other purposes, like communicating with certain kinds of RESTful web APIs, internal/proprietary web services, or even hardware. But _truly_ RESTful APIs are very rare, and so, in most cases, writing a database adapter to integrate with a _non-database API_ can be limiting. Luckily, there is now a [more straightforward way](https://sailsjs.com/documentation/concepts/helpers) to build these types of integrations. ### What kind of things can I do in an adapter? Adapters are mainly focused on providing model-contextualized CRUD methods. CRUD stands for create, read, update, and delete. In Sails/Waterline, we call these methods `create()`, `find()`, `update()`, and `destroy()`. For example, a `MySQLAdapter` implements a `create()` method which, internally, calls out to a MySQL database using the specified table name and connection information and runs an `INSERT ...` SQL query. ### Next steps Read about [available adapters](https://sailsjs.com/documentation/concepts/extending-sails/adapters/available-adapters), or how to make your own [custom adapter](https://sailsjs.com/documentation/concepts/extending-sails/adapters/custom-adapters). ================================================ FILE: docs/concepts/extending-sails/Adapters/adapterList.md ================================================ # Available database adapters This page is meant to be an up-to-date, comprehensive list of all of the core adapters available for the Sails.js framework, and a reference of a few of the most robust community adapters out there. All supported adapters can be configured in roughly the same way: by passing in a Sails/Waterline adapter (`adapter`), as well as a connection URL (`url`). For more information on configuring datastores, see [sails.config.datastores](https://sailsjs.com/documentation/reference/configuration/sails-config-datastores). > Having trouble connecting? Be sure to check your connection URL for typos. If that doesn't work, review the documentation for your database provider, or [get help](https://sailsjs.com/support). ### Officially-supported database adapters The following core adapters are maintained, tested, and used by the Sails.js core team. > Want to help out with a core adapter? Get started by reading [the Sails project contribution guide](https://sailsjs.com/contributing). | Database technology | Adapter | Connection URL structure | For production? | |:------------------------|:---------------------------------------------------------------|:----------------------------------------------|:--------------------| | MySQL | [require('sails-mysql')](http://npmjs.com/package/sails-mysql) | `mysql://user:password@host:port/database` | Yes | PostgreSQL | [require('sails-postgresql')](http://npmjs.com/package/sails-postgresql) | `postgresql://user:password@host:port/database` | Yes | MongoDB | [require('sails-mongo')](http://npmjs.com/package/sails-mongo) | `mongodb://user:password@host:port/database` | Yes | Local disk / memory | _(built-in, see [sails-disk](http://npmjs.com/package/sails-disk))_ | _n/a_ | **No!** ### sails-mysql [MySQL](http://en.wikipedia.org/wiki/MySQL) is the world's most popular relational database. [![NPM package info for sails-mysql](https://img.shields.io/npm/dm/sails-mysql.svg?style=plastic)](http://npmjs.com/package/sails-mysql)   [![License info](https://img.shields.io/npm/l/sails-mysql.svg?style=plastic)](http://npmjs.com/package/sails-mysql) ```bash npm install sails-mysql --save ``` ```javascript adapter: 'sails-mysql', url: 'mysql://user:password@host:port/database', ``` > + The default port for MySQL is `3306`. > + If you plan on saving special characters—like emojis—in your data, you may need to set the [`charset`](https://dev.mysql.com/doc/refman/5.7/en/charset-charsets.html) configuration option for your datastore. To allow emojis, use `charset: 'utf8mb4'`. You may use the [`columnType` setting](https://sailsjs.com/documentation/concepts/models-and-orm/attributes#?columntype) in a model attribute to set the character set. > + For relational database servers like MySQL and PostgreSQL, you may have to create a "database" first using a free tool like [SequelPro](https://www.sequelpro.com/) or in the MySQL REPL on the command-line (if you're an experience SQL user). It's customary to make a database specifically for your app to use. > + The sails-mysql adapter is also 100% compatible with [Amazon Aurora](https://aws.amazon.com/rds/aurora/) databases. ##### Handshake inactivity timeout errors If you find yourself encountering a "Handshake inactivity timeout" error when your Sails app interacts with MySQL, you can increase the timeout using the `connectTimeout` option. This is [usually only necessary](https://github.com/mysqljs/mysql/issues/1434) when queries are running side-by-side with computationally expensive operations (for example, compiling client-side typescript files or running webpack during development). For example, you might extend the timeout to 20 seconds: ```javascript adapter: 'sails-mysql', url: 'mysql://user:password@host:port/database', connectTimeout: 20000 ``` ### sails-postgresql [PostgreSQL](http://en.wikipedia.org/wiki/postgresql) is a modern relational database with powerful features. [![NPM package info for sails-postgresql](https://img.shields.io/npm/dm/sails-postgresql.svg?style=plastic)](http://npmjs.com/package/sails-postgresql)   [![License info](https://img.shields.io/npm/l/sails-postgresql.svg?style=plastic)](http://npmjs.com/package/sails-postgresql) ```bash npm install sails-postgresql --save ``` ```javascript adapter: 'sails-postgresql', url: 'postgresql://user:password@host:port/database', ``` > + The default port for PostgreSQL is `5432`. > + In addition to `adapter` and `url`, you might also need to set `ssl: true`. This depends on where your PostgreSQL database server is hosted. For example, `ssl: true` is required when connecting to Heroku's hosted PostgreSQL service. > + Note that in `pg` version 8.0, the syntax was updated to `ssl: { rejectUnauthorized: false }`. > + Compatible with most versions of Postgres. See [this issue](https://github.com/balderdashy/sails/issues/6957) to learn more about compatability with Postgres >12 ### sails-mongo [MongoDB](http://en.wikipedia.org/wiki/MongoDB) is the leading NoSQL database. [![NPM package info for sails-mongo](https://img.shields.io/npm/dm/sails-mongo.svg?style=plastic)](http://npmjs.com/package/sails-mongo)   [![License info](https://img.shields.io/npm/l/sails-mongo.svg?style=plastic)](http://npmjs.com/package/sails-mongo) ```bash npm install sails-mongo --save ``` ```javascript adapter: 'sails-mongo', url: 'mongodb://user:password@host:port/database', ``` > + The default port for MongoDB is `27017`. > + If your Mongo deployment keeps track of its internal credentials in a separate database, then you may need to name that database by tacking on [`?authSource=theotherdb`](https://stackoverflow.com/a/40608735/486547) to the end of the connection URL. > + Other [Mongo configuration settings](https://github.com/balderdashy/sails-mongo/blob/master/lib/private/constants/config-whitelist.constant.js) provided via querystring in the connection URL are passed through to the underlying Mongo driver. ### sails-disk Write to your computer's hard disk, or a mounted network drive. Not suitable for at-scale production deployments, but great for a small project, and essential for developing in environments where you may not always have a database set up. This adapter is bundled with Sails and works out of the box with zero configuration. You can also operate `sails-disk` in _memory-only mode_. See the settings table below for details. [![NPM package info for sails-disk](https://img.shields.io/npm/dm/sails-disk.svg?style=plastic)](http://npmjs.com/package/sails-disk)   [![License info](https://img.shields.io/npm/l/sails-disk.svg?style=plastic)](http://npmjs.com/package/sails-disk) _Available out of the box in every Sails app._ _Configured as the default database, by default._ ##### Optional datastore settings for `sails-disk` | Setting | Description | Type | Default | |:--------|:------------|:------|:--------| | `dir` | The directory to place database files in. The adapter creates one file per model. | ((string)) | `.tmp/localDiskDb` | | `inMemoryOnly` | If `true`, no database files will be written to disk. Instead, all data will be stored in memory (and will be lost when the app stops running). | ((boolean)) | `false` | > + You can configure the default `sails-disk` adapter by adding settings to the `default` datastore in `config/datastores.js`. ### Community-supported database adapters Is your database not supported by one of the core adapters? Good news! There are many different community database adapters for Sails.js and Waterline [available on NPM](https://www.npmjs.com/search?q=sails+adapter). Here are a few highlights: | Database technology | Adapter | Maintainer | Interfaces implemented | Stable release | |:--------------------------------|:-----------------------|:-----------|:-----------------------|-----------------------| | **Redis** | [sails-redis](https://npmjs.com/package/sails-redis) | [Ryan Clough / Solnet Solutions](https://github.com/Ryanc1256) | Semantic, Queryable | [![NPM package info for sails-redis](https://img.shields.io/npm/dm/sails-redis.svg?style=plastic)](http://npmjs.com/package/sails-redis) | | **MS SQL Server** | [sails-MSSQLserver](https://github.com/misterGF/sails-mssqlserver) | [misterGF](https://github.com/misterGF) | Semantic, Queryable | [![NPM package info for sails-sqlserver](https://img.shields.io/npm/dm/sails-sqlserver.svg?style=plastic)](http://npmjs.com/package/sails-sqlserver) | **OrientDB** | [sails-orientDB](https://github.com/appscot/sails-orientdb) | [appscot](https://github.com/appscot) | Semantic, Queryable, Associations, Migratable | [![NPM package info for sails-orientdb](https://img.shields.io/npm/dm/sails-orientdb.svg?style=plastic)](http://npmjs.com/package/sails-orientdb) | **Oracle** | [sails-oracleDB](https://npmjs.com/package/sails-oracledb) | [atiertant](https://github.com/atiertant) | Semantic, Queryable | [![NPM package info for sails-oracledb](https://img.shields.io/npm/dm/sails-oracledb.svg?style=plastic)](http://npmjs.com/package/sails-oracledb) | | **Oracle (AnyPresence)** | [waterline-oracle-adapter](https://github.com/AnyPresence/waterline-oracle-adapter) | [AnyPresence](https://github.com/AnyPresence) | Semantic, Queryable | [![Release info for AnyPresence/waterline-oracle-adapter](https://img.shields.io/github/tag/AnyPresence/waterline-oracle-adapter.svg?style=plastic)](https://github.com/AnyPresence/waterline-oracle-adapter) | **Oracle (stored procedures)** | [sails-oracle-SP](https://npmjs.com/sails-oracle-sp) | [Buto](http://github.com/buto) and [nethoncho](http://github.com/nethoncho) | Semantic, Queryable | [![NPM package info for sails-oracle-sp](https://img.shields.io/npm/dm/sails-oracle-sp.svg?style=plastic)](http://npmjs.com/package/sails-oracle-sp) | **SAP HANA DB** | [sails-HANA](https://npmjs.com/sails-hana) | [Enrico Battistella](https://github.com/battistaar) | Semantic, Queryable | [![NPM package info for sails-hana](https://img.shields.io/npm/dm/sails-hana.svg?style=plastic)](http://npmjs.com/package/sails-hana) | **SAP HANA (AnyPresence)** | [waterline-SAP-HANA-adapter](https://github.com/AnyPresence/waterline-sap-hana-adapter) | [AnyPresence](https://github.com/AnyPresence) | Semantic, Queryable | [![Release info for AnyPresence/waterline-sap-hana-adapter](https://img.shields.io/github/tag/AnyPresence/waterline-sap-hana-adapter.svg?style=plastic)](https://github.com/AnyPresence/waterline-sap-hana-adapter) | **IBM DB2** | [sails-DB2](https://npmjs.com/sails-db2) | [ibuildings Italia](https://github.com/IbuildingsItaly) & [Vincenzo Ferrari](https://github.com/wilk) | Semantic, Queryable | [![NPM package info for sails-db2](https://img.shields.io/npm/dm/sails-db2.svg?style=plastic)](http://npmjs.com/package/sails-db2) | **ServiceNow SOAP** | [waterline-ServiceNow-SOAP](https://npmjs.com/waterline-servicenow-soap) | [Sungard Availability Services](http://www.sungardas.com/) | Semantic, Queryable | [![NPM package info for waterline-servicenow-soap](https://img.shields.io/npm/dm/waterline-servicenow-soap.svg?style=plastic)](http://npmjs.com/package/waterline-servicenow-soap) | **Cassandra** | [sails-cassandra](https://github.com/dtoubelis/sails-cassandra) | [dtoubelis](https://github.com/dtoubelis) | Semantic, Migratable, Iterable | [![NPM package info for sails-cassandra](https://img.shields.io/npm/dm/sails-cassandra.svg?style=plastic)](http://npmjs.com/package/sails-cassandra) | **Solr** | [sails-solr](https://github.com/sajov/sails-solr) | [sajov](https://github.com/sajov) | Semantic, Migratable, Queryable | [![NPM package info for sails-solr](https://img.shields.io/npm/dm/sails-solr.svg?style=plastic)](http://npmjs.com/package/sails-solr) | **FileMaker Database** | [sails-FileMaker](https://github.com/geistinteractive/sails-filemaker) | [Geist Interactive](https://www.geistinteractive.com/) | Semantic | [![NPM package info for sails-filemaker](https://img.shields.io/npm/dm/sails-filemaker.svg?style=plastic)](http://npmjs.com/package/sails-filemaker) | **Apache Derby** | [sails-derby](https://github.com/dash-/node-sails-derby) | [dash-](https://github.com/dash-) | Semantic, Queryable, Associations, SQL | [![NPM package info for sails-derby](https://img.shields.io/npm/dm/sails-derby.svg?style=plastic)](http://npmjs.com/package/sails-derby) | **REST API (Generic)** | [sails-REST](https://github.com/zohararad/sails-rest) | [zohararad](https://github.com/zohararad) | Semantic | [![NPM package info for sails-rest](https://img.shields.io/npm/dm/sails-rest.svg?style=plastic)](http://npmjs.com/package/sails-rest) ##### Add your custom adapter to this list If you see out of date information on this page, or if you want to add an adapter you made, please submit a pull request to this file updating the table of community adapters above. Note that, to be listed on this page, an adapter must: 1. Be free and open source (_libre_ and _gratis_), preferably under the MIT license. 2. Pass all of the Waterline adapter tests for the interface layers declared in its package.json file. 3. Support configuration via a connection URL, as `url` (if applicable). If you find that any of these conventions are not true for any of the community adapters above (i.e. for latest stable release published on NPM, not for the code on GitHub), then please reach out to the maintainer of the adapter. If you can't reach them or need further assistance, then please [get in touch](https://sailsjs.com/contact) with a member of the Sails core team. ================================================ FILE: docs/concepts/extending-sails/Adapters/customAdapters.md ================================================ # Custom adapters Sails makes it fairly easy to write your own database adapter. Custom adapters can be built directly in your app (`api/adapters/`) or published as NPM packages. Check out [Intro to Custom Adapters](https://github.com/balderdashy/sails/blob/master/docs/contributing/intro-to-custom-adapters.md), the [Adapter Interface Reference](https://github.com/balderdashy/sails/blob/master/docs/contributing/adapter-specification.md), and [sails-adapter-boilerplate](https://github.com/balderdashy/sails-adapter-boilerplate) for more information about creating your own adapter. ### Where does my adapter go? There are two different places you can build an adapter: ##### In your app's `api/adapters/` folder If an adapter is only going to be used in one app (e.g. a short-term fork of an existing adapter) you can put it in `api/adapters/`. This is what you get out of the box when you run `sails generate adapter`. In this case, the name of the adapter is determined by the name of the folder inside `api/adapters/` (by convention, the entry point for your adapter should be `index.js`). ##### In a separate repo Go with this option if you plan to share your adapter between multiple Sails apps, whether that's within your organization or as an open-source package for other members of the Sails/Node.js community at large. To use an externalized adapter like this, you'll need to do `npm install your-adapter-package-name` or `npm link your-adapter-package-name`. > Before you start on an open-source adapter, we recommend you search GitHub for `sails-databasename` and `waterline-databasename` to check if a project already exists. If it does, it's generally a good idea to approach the author of an existing adapter and offer to contribute instead of starting a new project. Most developers will welcome your help, and the combined efforts will likely result in a better quality adapter. If one doesn't exist, we recommend you create a new project and name it following the convention: `sails-databasename`. ### What goes in a custom adapter? In Sails, database adapters expose **interfaces**, which imply a contract to implement certain functionality. This allows us to guarantee conventional usage patterns across multiple models, developers, apps, and even companies, making app code more maintainable, efficient, and reliable. Adapters are primarily useful for integrating with databases, but they can also be used to support any open API or internal/proprietary web service that is _purely_ RESTful. > Not everything fits perfectly into a RESTful/CRUD mold. Sometimes the service you're integrating with has an RPC-style interface with one-off methods. For example, consider an API request to send an email, or to read a remote sensor on a piece of connected hardware. For that, you'll want to write or extend a machinepack. [Learn more about machinepacks here](http://node-machine.org). ### What kind of things can I do in an adapter? Adapters are mainly focused on providing model-contextualized CRUD methods. CRUD stands for create, read, update, and delete. In Sails/Waterline, we call these methods `create()`, `find()`, `update()`, and `destroy()`. For example, a `MySQLAdapter` implements a `create()` method which, internally, calls out to a MySQL database using the specified table name and connection information and runs an `INSERT ...` SQL query. In practice, your adapter can really do anything it likes—any method you write will be exposed on the raw datastore objects and any models which use them. ### Building a custom adapter Check out the [Sails docs](https://sailsjs.com/documentation), or see [`config/datastores.js`](https://sailsjs.com/anatomy/config/datastores.js) in a new Sails project for information on setting up this adapter in a Sails app. #### Running the tests Configure the interfaces you plan to support (and the targeted version of Sails) in the adapter's `package.json` file: ```javascript { //... "sails": { "adapter": { "sailsVersion": "^1.0.0", "implements": [ "semantic", "queryable" ] } } } ``` In your adapter's directory, run: ```sh $ npm test ``` #### Publish your adapter > You're welcome to write proprietary adapters and use them any way you wish— > these instructions are for releasing an open-source adapter. 1. Create a [new public repo](https://github.com/new) and add it as a remote (`git remote add origin git@github.com:yourusername/sails-youradaptername.git). 2. Make sure you attribute yourself as the author and set the license in the package.json to "MIT". 3. Run the tests one last time. 4. Do a [pull request to the docs](https://github.com/balderdashy/sails/edit/master/docs/concepts/extending-sails/Adapters/adapterList.md), adding your adapter's repo. 5. We'll update the documentation with information about your new adapter. 6. Let the people of the world adore you with lavish praise. 7. Run `npm version patch`. 8. Run `git push && git push --tags`. 9. Run `npm publish`. ### Why would I need a custom adapter? When building a Sails app, the sending or receiving of any asynchronous communication with another piece of hardware can _technically_ be normalized into an adapter (viz. API integrations). > **From Wikipedia:** > *http://en.wikipedia.org/wiki/Create,_read,_update_and_delete* > Although a relational database provides a common persistence layer in software applications, numerous other persistence layers exist. CRUD functionality can be implemented with an object database, an XML database, flat text files, custom file formats, tape, or card, for example. In other words, Waterline is not _necessarily_ just an ORM for your database. It is a purpose-agnostic open standard and toolset for integrating with all kinds of RESTful services, datasources, and devices—whether it's LDAP, Neo4J, or [a lamp](https://www.youtube.com/watch?v=OmcQZD_LIAE). > **But remember:** only use Waterline adapters for communicating with databases and APIs that support a "create", "read", "update", and "destroy" interface. Not everything fits into that mold, and there are [better, more generic ways](http://node-machine.org) to address those other use cases. ### Why should I build a custom adapter? To recap, writing your API integrations as adapters is **easier**, takes **less time**, and **absorbs a considerable amount of risk**, since you get the advantage of a **standardized set of conventions**, a **documented API**, and a **built-in community** of other developers who have gone through the same process. Best of all, you (and your team) can **reuse the adapter** in other projects, **speeding up development** and **saving time and money**. Finally, if you choose to release your adapter as open source, you provide a tremendous boon to our little framework and our budding Sails.js ecosystem. Even if it's not via Sails, I encourage you to give back to the OSS community, even if you've never forked a repo before—don't be intimidated, it's not that bad! The more high-quality adapters the Sails community collectively releases as open source, the less repetitive work we all have to do when we integrate with various databases and services. Our vision is to make building server-side apps more fun and less repetitive for everyone, and that happens one community adapter (or machinepack/driver/generator/view engine/etc.) at a time. ### What is an adapter interface? The functionality of database adapters is as varied as the services they connect. That said, there is a standard library of methods, and a support matrix you should be aware of. Adapters may implement some, all, or none of the interfaces below, but rest assured that **if an adapter implements one method in an interface, it should implement *all* of them**. This is not always the case due to limitations and/or incomplete implementations, but at the very least, a descriptive error message should be used to keep developers informed of what's supported and what's not. > For more information, check out the Sails docs, and specifically the [adapter interface reference](https://github.com/balderdashy/sails/blob/master/docs/contributing/adapter-specification.md). ### Are there examples I can look at? If you're looking for some inspiration, a good place to start is with the core adapters. Take a look at **[MySQL](https://github.com/balderdashy/sails-mysql)**, **[PostgreSQL](https://github.com/balderdashy/sails-postgresql)**, **[MongoDB](https://github.com/balderdashy/sails-mongo)**, **[Redis](https://github.com/balderdashy/sails-redis)**, or local [disk](https://github.com/balderdashy/sails-disk). ### Where do I get help? An active community of Sails and Waterline users exists on GitHub, Stack Overflow, Google groups, IRC, Gitter, and more. See the [Support page](https://sailsjs.com/support) for a list of recommendations. > If you have an unanswered question that isn't covered here, and that you feel would add value for the community, please feel free to send a PR adding it to this section of the docs. ================================================ FILE: docs/concepts/extending-sails/Custom Responses/AddingCustomResponse.md ================================================ # Adding a custom response To add your own custom response method, simply add a file to `/api/responses` with the same name as the method you would like to create. The file should export a function, which can take any parameters you like. ```javascript /** * api/responses/myResponse.js * * This will be available in controllers as res.myResponse('foo'); */ module.exports = function(message) { var req = this.req; var res = this.res; var viewFilePath = 'mySpecialView'; var statusCode = 200; var result = { status: statusCode }; // Optional message if (message) { result.message = message; } // If the user-agent wants a JSON response, send json if (req.wantsJSON) { return res.json(result, result.status); } // Set status code and view locals res.status(result.status); for (var key in result) { res.locals[key] = result[key]; } // And render view res.render(viewFilePath, result, function(err) { // If the view doesn't exist, or an error occured, send json if (err) { return res.json(result, result.status); } // Otherwise, serve the `views/mySpecialView.*` page res.render(viewFilePath); }); } ``` ================================================ FILE: docs/concepts/extending-sails/Custom Responses/Custom Responses.md ================================================ # Custom responses ### Overview Sails apps come bundled with several pre-configured _responses_ that can be called from [action code](https://sailsjs.com/documentation/concepts/actions-and-controllers). These default responses can handle situations like “resource not found” (the [`notFound` response](https://sailsjs.com/documentation/reference/response-res/res-not-found)) and “internal server error” (the [`serverError` response](https://sailsjs.com/documentation/reference/response-res/res-server-error)). If your app needs to modify the way that the default responses work, or create new responses altogether, you can do so by adding files to the `api/responses` folder. > Note: `api/responses` is not generated by default in new Sails apps, so you’ll have to add it yourself if you want to add / customize responses. ### Using responses As a quick example, consider the following action: ```javascript getProfile: function(req, res) { // Look up the currently logged-in user's record from the database. User.findOne({ id: req.session.userId }).exec(function(err, user) { if (err) { res.status(500); return res.view('500', {data: err}); } return res.json(user); }); } ``` This code handles a database error by sending a 500 error status and sending the error data to a view to be displayed. However, this code has several drawbacks, primarily: * The response isn't *content-negotiated*: if the client is expecting a JSON response, they're out of luck * The response *reveals too much* about the error: in production, it'd be best to just log the error to the terminal * It isn't *normalized*: even if we dealt with the other bullet points above, the code is specific to this action, and we'd have to work hard to keep the exact same format for error handling everywhere * It isn't *abstracted*: if we wanted to use a similar approach elsewhere, we'd have to copy / paste the code Now, consider this replacement: ```javascript getProfile: function(req, res) { // Look up the currently logged-in user's record from the database. User.findOne({ id: req.session.userId }).exec(function(err, user) { if (err) { return res.serverError(err); } return res.json(user); }); } ``` This approach has many advantages: - More concise - Error payloads are normalized - Production vs. development logging is taken into account - Error codes are consistent - Content negotiation (JSON vs HTML) is taken care of - API tweaks can be done in one quick edit to the appropriate generic response file ### Response methods and files Any `.js` file saved in the `api/responses/` folder can be executed by calling `res.thatFileName()`. For example, `api/responses/insufficientFunds.js` can be executed with a call to `res.insufficientFunds()`. ##### Accessing `req`, `res`, and `sails` The request and response objects are available inside of a custom response as `this.req` and `this.res`. This allows the actual response function to take arbitrary parameters. For example: ```javascript return res.insufficientFunds(err, { happenedDuring: 'signup' }); ``` And the implementation of the custom response might look something like this: ```javascript module.exports = function insufficientFunds(err, extraInfo){ var req = this.req; var res = this.res; var sails = req._sails; var newError = new Error('Insufficient funds'); newError.raw = err; _.extend(newError, extraInfo); sails.log.verbose('Sent "Insufficient funds" response.'); return res.badRequest(newError); } ``` ### Built-in responses All Sails apps have several pre-configured responses like [`res.serverError()`](https://sailsjs.com/documentation/reference/response-res/res-server-error) and [`res.notFound()`](https://sailsjs.com/documentation/reference/response-res/res-not-found) that can be used even if they don’t have corresponding files in `api/responses/`. Any of the default responses may be overridden by adding a file with the same name to `api/responses/` in your app (e.g. `api/responses/serverError.js`). > You can use the [Sails command-line tool](https://sailsjs.com/documentation/reference/command-line-interface/sails-generate) as a shortcut for doing this. > > For example: > >```bash >sails generate response serverError >``` > ================================================ FILE: docs/concepts/extending-sails/Generators/Generators.md ================================================ # Generators A big part of Sails, like any framework, is automating repetitive tasks. **Generators** are no exception: they're what power the Sails command-line interface any time it generates new files for your Sails projects. In fact, you or someone on your team probably used a _generator_ to create your latest Sails project. When you type ```sh sails new my-project ``` sails uses its built-in "new" generator to prompt you for your app template of choice, then spits out the initial folder structure for a Sails app: ```javascript my-project ├── api/ │ ├─ controllers/ │ ├─ helpers/ │ └─ models/ ├── assets/ │ └─ … ├── config/ │ └─ … ├── views/ │ └─ … ├── .gitignore … ├── package.json └── README.md ``` This conventional folder structure is one of the big advantages of using a framework. But it's usually also one of the trade-offs (what if your team or organization has made firm commitments to a different set of conventions?). Fortunately since Sails v0.11, generators are extensible and easy to check in to a project repository or publish on NPM for re-use. Sails' generators allow you to completely customize what happens when you run `sails new` and `sails generate` from the command-line. By augmenting new apps and newly-generated modules, custom generators can be used to do all sorts of cool things: - to standardize conventions and boilerplate logic for all new apps across your organization - to swap out rules in the default .eslintrc file - to customize how the asset pipeline works in new projects - to use a different asset pipeline altogether (like [Gulp](http://gulpjs.com/) or [webpack](https://webpack.github.io/)) - to use a [different default view engine](https://sailsjs.com/documentation/concepts/views/view-engines) - to automate custom deployments (e.g. white label apps with one server per customer) - to include a different set of dependencies in the package.json file - to generate files in a transpiled language like TypeScript or CoffeeScript - to start off with all documentation and comments in a language other than English - to include ASCII pictures of cats at the top of every code file (or license headers, whatever) - to standardize around a particular version of a front-end dependency (for example, `sails generate jquery`) - to include a particular front-end framework in your new Sails apps - to make it easy to include new Vue / React components or Angular modules from your favorite templates (for example, `sails generate component` or `sails generate ng-module`) > If you are interested in making custom generators, the best place to start is by checking out the [introduction to custom generators](https://sailsjs.com/documentation/concepts/extending-sails/generators/custom-generators). You also might check out [open-source generators from the community](https://sailsjs.com/documentation/concepts/extending-sails/generators/available-generators), in case something already out there will save you some time. ================================================ FILE: docs/concepts/extending-sails/Generators/customGenerators.md ================================================ # Custom generators ### Overview Custom [generators](https://sailsjs.com/documentation/concepts/extending-sails/generators) are a type of plugin for the Sails command line. Through templates, they control which files get generated in your Sails projects when you run `sails new` or `sails generate`, and also what those files look like. ### Creating a generator To make this easier to play with, let's first make a Sails project. If you haven't already created one, go to your terminal and type: ```sh sails new my-project ``` Then `cd` into `my-project` and ask Sails to spit out the template for a new generator: ```sh sails generate generator awesome ``` ### Configuring a generator To enable the generator you need to tell Sails about it via your test project's [`.sailsrc` file](https://sailsjs.com/documentation/concepts/configuration/using-sailsrc-files). If we were using an existing generator, we could just install it from NPM, then specify the name of the package in `.sailsrc`. But since we're developing this generator locally, we'll just connect it to the folder directly: ```javascript { "generators": { "modules": { "awesome": "./my-project/awesome" } } } ``` > **Note:** For now, we'll stick with "awesome", but you can mount the generator under any name you want. Whatever you choose for the name of the key in the `.sailsrc` file will be the name you'll use to run this generator from the terminal (e.g. `sails generate awesome`). ### Running a custom generator To run your generator, just tack its name on to `sails generate`, followed by any desired arguments or command-line options. For example: ```js sails generate awesome ``` ### Publishing to NPM If your generator is useful across different projects, you might consider publishing it as an NPM package (note that this doesn't mean that your generator must be open-source: NPM also supports [private packages](https://docs.npmjs.com/private-modules/intro). First, pop open the `package.json` file and verify the package name (e.g. "@my-npm-name/sails-generate-awesome"), author ("My Name"), license, and other information are correct. If you're unsure, a good open source license to use is "MIT". If you're publishing a private generator and want it to remain proprietary to your organization, use "UNLICENSED". > **Note:** If you don't already have an NPM account, go to [npmjs.com](https://www.npmjs.com/) and create one. Then use `npm login` to get set up. When you're ready to pull the trigger and publish your generator on NPM, cd into the generator's folder in the terminal and type: ```sh npm publish ``` ### Installing a generator To take your newly-published generator for a spin, cd back into your example Sails project (`my-project`), delete the inline generator, and run: ```js npm install @my-npm-name/sails-generate-awesome ``` then change the `.sailsrc` in your example Sails project (`my-project/.sailsrc`): ```javascript { "generators": { "modules": { "awesome": "@my-npm-name/sails-generate-awesome" } } } ``` And, last but not least: ```sh sails generate awesome ``` ================================================ FILE: docs/concepts/extending-sails/Generators/generatorList.md ================================================ # Available generators The Sails framework's built-in [generators](https://sailsjs.com/documentation/concepts/extending-sails/generators) can be customized using command-line options and overridden by [mounting custom generators in the `.sailsrc` file](https://sailsjs.com/documentation/concepts/extending-sails/generators/custom-generators). Other generators that add completely new sub-commands to [`sails generate`](https://sailsjs.com/documentation/reference/command-line-interface/sails-generate) can be mounted in the same way. ### Core generators Certain generators are built in to Sails by default. | Commands that generate a new Sails app |:-----------------------------------| | sails new _name_ | sails new _name_ --fast | sails new _name_ --caviar | sails new _name_ --without=grunt | sails new _name_ --without=lodash,async,grunt,blueprints,i18n | sails new _name_ --no-frontend --without=sockets,lodash | sails new _name_ --minimal | Generators for spitting out new files in an existing Sails app |:-----------------------------------| | sails generate model _identity_ | sails generate action _name_ | sails generate action view-_name_ | sails generate action _some/path/_view-_name_ | sails generate page _name_ | sails generate helper _name_ | sails generate helper view-_name_ | sails generate script _name_ | sails generate script get-_name_ | sails generate controller _name_ | sails generate api _name_ | sails generate hook _name_ | sails generate response _name_ | Commands for generating plugins |:-----------------------------------| | sails generate generator _name_ | sails generate adapter _name_ | Commands for (re)generating client-side dependencies |:-----------------------------------| | sails generate sails.io.js | sails generate parasails | Utils for building your own 3rd party packages |:-----------------------------------| | sails generate etc _Since Sails v1.0, built-in generators are now [bundled](https://npmjs.com/package/sails-generate) in Sails core, rather than in separate NPM packages. All generators can still be overridden the same way. For advice setting up overrides for core generators in your environment, [click here](https://sailsjs.com/support)._ ### Community generators There are over 100 community-supported generators [available on NPM](https://www.npmjs.com/search?q=sails+generate): + [sails-inverse-model](https://github.com/juliandavidmr/sails-inverse-model) + [sails-generate-new-gulp](https://github.com/Karnith/sails-generate-new-gulp) + [sails-generate-archive](https://github.com/jaumard/sails-generate-archive) + [sails-generate-scaffold](https://github.com/irlnathan/sails-generate-scaffold) + [sails-generate-directive](https://github.com/balderdashy/sails-generate-directive) + [sails-generate-bower](https://github.com/smies/sails-generate-bower) + [sails-generate-angular-gulp](https://github.com/Karnith/sails-generate-angular-gulp) + [sails-generate-ember-blueprints](https://github.com/mphasize/sails-generate-ember-blueprints) + And [many more](https://www.npmjs.com/search?q=sails+generate)... ================================================ FILE: docs/concepts/extending-sails/Hooks/Hooks.md ================================================ # Hooks ### What is a hook? A hook is a Node module that adds functionality to the Sails core. The [hook specification](https://sailsjs.com/documentation/concepts/extending-sails/hooks/hook-specification) defines the requirements a module must meet for Sails to be able to import its code and make the new functionality available. Because they can be saved separately from the core, hooks allow Sails code to be shared between apps and developers without having to modify the framework. ### Types of hooks There are three types of hooks available in Sails: 1. **Core hooks** are built in and provide many of the common features essential to a Sails app, such as request handling, blueprint route creation, and database integration via [Waterline](https://sailsjs.com/documentation/concepts/models-and-orm). Core hooks are bundled with the Sails core and are thus available to every app. You will rarely need to call core hook methods in your code. 2. **App-level hooks** live in the `api/hooks/` folder of a Sails app. Project hooks let you take advantage of the features of the hook system for code that doesn’t need to be shared between apps. 3. **Installable hooks** are plugins, installed into an app’s `node_modules` folder using `npm install`. Installable hooks allow developers in the Sails community to create “plug-in”-like modules for use in Sails apps. ### Read more * [Using hooks in your app](https://sailsjs.com/documentation/concepts/extending-sails/Hooks/using-hooks) * [The hook specification](https://sailsjs.com/documentation/concepts/extending-sails/hooks/hook-specification) * [Creating a project hook](https://sailsjs.com/documentation/concepts/extending-sails/hooks/project-hooks) * [Creating an installable hook](https://sailsjs.com/documentation/concepts/extending-sails/Hooks/installable-hooks) ================================================ FILE: docs/concepts/extending-sails/Hooks/available-hooks.md ================================================ # Available hooks This page is meant to be an up to date, comprehensive list of all of the core hooks in the Sails.js framework, and a reference of a few of the most popular community-made hooks. ### Core hooks The following hooks are maintained by the Sails.js core team and are included in your Sails app by default. You can override or disable them using your [sailsrc file](https://sailsjs.com/documentation/concepts/configuration/using-sailsrc-files) or [environment variables](https://sailsjs.com/documentation/concepts/configuration#?setting-sailsconfig-values-directly-using-environment-variables). | Hook | Package | Latest stable release | Purpose | |:---------------|---------------|-------------------------|:------------| | `grunt` | [sails-hook-grunt](https://npmjs.com/package/sails-hook-grunt) | [![NPM version](https://badge.fury.io/js/sails-hook-grunt.png)](http://badge.fury.io/js/sails-hook-grunt) | Governs the built-in asset pipeline in Sails. | `orm` | [sails-hook-orm](https://npmjs.com/package/sails-hook-orm) | [![NPM version](https://badge.fury.io/js/sails-hook-orm.png)](http://badge.fury.io/js/sails-hook-orm) | Implements support for Waterline ORM in Sails. | `sockets` | [sails-hook-sockets](https://npmjs.com/package/sails-hook-sockets) | [![NPM version](https://badge.fury.io/js/sails-hook-sockets.png)](http://badge.fury.io/js/sails-hook-sockets) | Implements Socket.io support in Sails. ### sails-hook-orm Implements support for the Waterline ORM in Sails. [![Release info for sails-hook-orm](https://img.shields.io/npm/dm/sails-hook-orm.svg?style=plastic)](http://npmjs.com/package/sails-hook-orm)   [![License info](https://img.shields.io/npm/l/sails-hook-orm.svg?style=plastic)](http://npmjs.com/package/sails-hook-orm) > + The default configuration set by this hook can be found [here](https://www.npmjs.com/package/sails-hook-orm#implicit-defaults). > + You can find futher details about this hook's purpose [here](https://www.npmjs.com/package/sails-hook-orm#purpose). > + You can disable this hook by following [these instructions](https://www.npmjs.com/package/sails-hook-orm#can-i-disable-this-hook). ### sails-hook-sockets Implements socket.io support in Sails. [![Release info for sails-hook-sockets](https://img.shields.io/npm/dm/sails-hook-sockets.svg?style=plastic)](http://npmjs.com/package/sails-hook-sockets)   [![License info](https://img.shields.io/npm/l/sails-hook-sockets.svg?style=plastic)](http://npmjs.com/package/sails-hook-sockets) > + You can find futher details about this hook's purpose [here](https://www.npmjs.com/package/sails-hook-sockets#purpose). ### sails-hook-grunt Implements support for the built-in asset pipeline and task runner in Sails. [![Release info for sails-hook-grunt](https://img.shields.io/npm/dm/sails-hook-grunt.svg?style=plastic)](http://npmjs.com/package/sails-hook-grunt)   [![License info](https://img.shields.io/npm/l/sails-hook-grunt.svg?style=plastic)](http://npmjs.com/package/sails-hook-grunt) > + You can find futher details about this hook's purpose [here](https://www.npmjs.com/package/sails-hook-grunt#purpose). > + You can disable this hook by following [these instructions](https://www.npmjs.com/package/sails-hook-grunt#can-i-disable-this-hook). ### Community-made hooks There are more than 200 community hooks for Sails.js [available on NPM](https://www.npmjs.com/search?q=sails+hook). Here are a few highlights: | Hook | Maintainer | Purpose | Stable release | |-------------|-------------|:---------------|----------------| | [sails-hook-webpack](https://www.npmjs.com/package/sails-hook-webpack) | [Michael Diarmid](https://github.com/Salakar) | Use Webpack for your Sails app's asset pipeline instead of Grunt. | [![Release info for sails-hook-webpack](https://img.shields.io/npm/dm/sails-hook-webpack.svg?style=plastic)](http://npmjs.com/package/sails-hook-webpack) | [sails-hook-postcss](https://www.npmjs.com/package/sails-hook-postcss) | [Jeff Jewiss](https://github.com/jeffjewiss)| Process your Sails application’s CSS with Postcss. | [![Release info for sails-hook-postcss](https://img.shields.io/npm/dm/sails-hook-postcss.svg?style=plastic)](http://npmjs.com/package/sails-hook-postcss) | [sails-hook-babel](https://www.npmjs.com/package/sails-hook-babel) | [Onoshko Dan](https://github.com/dangreen), [Markus Padourek](https://github.com/globegitter) & [SANE](http://sanestack.com/) | Process your Sails application’s CSS with Postcss. | [![Release info for sails-hook-babel](https://img.shields.io/npm/dm/sails-hook-babel.svg?style=plastic)](http://npmjs.com/package/sails-hook-babel) | [sails-hook-responsetime](https://www.npmjs.com/package/sails-hook-responsetime) | [Luis Lobo Borobia](https://github.com/luislobo)| Add X-Response-Time to both HTTP and Socket request headers. | [![Release info for sails-hook-responsetime](https://img.shields.io/npm/dm/sails-hook-responsetime.svg?style=plastic)](http://npmjs.com/package/sails-hook-responsetime) | [sails-hook-winston](https://www.npmjs.com/package/sails-hook-winston) | [Kikobeats](https://github.com/Kikobeats) | Integrate the Winston logging system with your Sails application. | [![Release info for sails-hook-winston](https://img.shields.io/npm/dm/sails-hook-winston.svg?style=plastic)](http://npmjs.com/package/sails-hook-winston) | [sails-hook-allowed-hosts](https://www.npmjs.com/package/sails-hook-allowed-hosts) | [Akshay Bist](https://github.com/elssar) | Ensure that only requests made from authorized hosts/IP addresses are allowed. | [![Release info for sails-hook-allowed-hosts](https://img.shields.io/npm/dm/sails-hook-allowed-hosts.svg?style=plastic)](http://npmjs.com/package/sails-hook-allowed-hosts) | [sails-hook-cron](https://www.npmjs.com/package/sails-hook-cron) | [Eugene Obrezkov](https://github.com/ghaiklor) | Run cron tasks for your Sails app. | [![Release info for sails-hook-cron](https://img.shields.io/npm/dm/sails-hook-cron.svg?style=plastic)](http://npmjs.com/package/sails-hook-cron) | [sails-hook-organics](https://www.npmjs.com/package/sails-hook-organics) | [Mike McNeil](https://github.com/mikermcneil) | Exposes a set of commonly-used functions ("organics") as built-in helpers in your Sails app. | [![Release info for sails-hook-organics](https://img.shields.io/npm/dm/sails-hook-organics.svg?style=plastic)](http://npmjs.com/package/sails-hook-organics) ##### Add your hook to this list If you see out of date information on this page, or if you want to add a hook you made, please submit a pull request to this file updating the table of community hooks above. Note: to be listed on this page, an adapter must be free and open-source (_libre_ and _gratis_), preferably under the MIT license. ================================================ FILE: docs/concepts/extending-sails/Hooks/events.md ================================================ # Application Events ### Overview Sails app instances inherit Node's [`EventEmitter` interface](https://nodejs.org/api/events.html#events_class_eventemitter), meaning that they can both emit and listen for custom events. While it is not recommended that you utilize Sails events directly in app code (since your apps should strive to be as stateless as possible to facilitate scalability), events can be very useful when extending Sails (via [hooks](https://sailsjs.com/documentation/concepts/extending-sails/hooks) or [adapters](https://sailsjs.com/documentation/concepts/extending-sails/adapters)) and in a testing environment. ### Should I use events? Most Sails developers never have a use case for working with application events. Events emitted by the Sails app instance are designed to be used when building your own custom hooks, and while you _could_ technically use them anywhere, in most cases you _should not_. Never use events in your controllers, models, services, configuration, or anywhere else in the userland code in your Sails app (unless you are building a custom app-level hook in `api/hooks/`). ### Events emitted by Sails The following are the most commonly used built-in events emitted by Sails instances. Like any EventEmitter in Node, you can listen for these events with `sails.on()`: ```javascript sails.on(eventName, eventHandlerFn); ``` None of the events are emitted with extra information, so your `eventHandlerFn` should not have any arguments. | Event name | Emitted when... | |:-----------|:----------------| | `ready` | The app has been loaded and the bootstrap has run, but it is not yet listening for requests | | `lifted` | The app has been lifted and is listening for requests. | | `lower` | The app has is lowering and will stop listening for requests. | | `hook::loaded` | The hook with the specified identity loaded and ran its `initialize()` method successfully. | > In addition to `.on()`, Sails also exposes a useful utility function called `sails.after()`. See the [inline documentation](https://github.com/balderdashy/sails/blob/fd2f9b6866637143eda8e908775365ca52fab27c/lib/EVENTS.md#usage) in Sails core for more information. ================================================ FILE: docs/concepts/extending-sails/Hooks/hookspec/configure.md ================================================ # `.configure` The `configure` feature provides a way to configure a hook after the [`defaults` objects](https://sailsjs.com/documentation/concepts/extending-sails/hooks/hook-specification/defaults) have been applied to all hooks. By the time a custom hook’s `configure()` function runs, all user-level configuration and core hook settings will have been merged into `sails.config`. However, you should *not* depend on the configuration of other custom hooks at this point, as the load order of custom hooks is not guaranteed. `configure` should be implemented as a function with no arguments, and should not return any value. For example, the following `configure` function could be used for a hook that communicates with a remote API, to change the API endpoint based on whether the user set the hook’s `ssl` property to `true`. Note that the hook’s configuration key is available in `configure` as `this.configKey`: ``` configure: function() { // If SSL is on, use the HTTPS endpoint if (sails.config[this.configKey].ssl == true) { sails.config[this.configKey].url = "https://" + sails.config[this.configKey].domain; } // Otherwise use HTTP else { sails.config[this.configKey].url = "http://" + sails.config[this.configKey].domain; } } ``` The main benefit of `configure` is that all hook `configure` functions are guaranteed to run before any [`initialize` functions](https://sailsjs.com/documentation/concepts/extending-sails/hooks/hook-specification/initialize) run; therefore, a hook’s `initialize` function can examine the configuration settings of other hooks. ================================================ FILE: docs/concepts/extending-sails/Hooks/hookspec/defaults.md ================================================ # `.defaults` The `defaults` feature can be implemented either as an object or a function which takes a single argument (see “using `defaults` as a function” below) and returns an object. The object you specify will be used to provide default configuration values for Sails. You should use this feature to specify default settings for your hook. For example, if you were creating a hook that communicates with a remote service, you may want to provide a default domain and timeout length: ``` { myapihook: { timeout: 5000, domain: "www.myapi.com" } } ``` If a `myapihook.timeout` value is provided via a Sails configuration file, that value will be used; otherwise it will default to `5000`. ##### Namespacing your hook configuration For [project hooks](https://sailsjs.com/documentation/concepts/extending-sails/Hooks?q=types-of-hooks), you should namespace your hook’s configuration under a key that uniquely identifies that hook (e.g. `myapihook` above). For [installable hooks](https://sailsjs.com/documentation/concepts/extending-sails/Hooks?q=types-of-hooks), you should use the special `__configKey__` key to allow end-users of your hook to [change the configuration key](https://sailsjs.com/documentation/concepts/extending-sails/hooks/using-hooks?q=changing-the-way-sails-loads-an-installable-hook) if necessary. The default key for a hook using `__configKey__` is the hook name. For example, if you create a hook called `sails-hooks-myawesomehook` which includes the following `defaults` object: ``` { __configKey__: { name: "Super Bob" } } ``` then it will, by default, provide default settings for the `sails.config.myawesomehook.name` value. If the end-user of the hook overrides the hook name to be `foo`, then the `defaults` object will provide a default value for `sails.config.foo.name`. ##### Using `defaults` as a function If you specify a function for the `defaults` feature instead of a plain object, it takes a single argument (`config`) which receives any Sails configuration overrides. Configuration overrides can be made by passing settings to the command line when lifting Sails (e.g. `sails lift --prod`), by passing an object as the first argument when programmatically lifting or loading Sails (e.g. `Sails.lift({port: 1338}, ...)`) or by using a [`.sailsrc`](https://sailsjs.com/documentation/anatomy/.sailsrc) file. The `defaults` function should return a plain object representing configuration defaults for your hook. ================================================ FILE: docs/concepts/extending-sails/Hooks/hookspec/hookspec.md ================================================ # The hook specification ### Overview Each Sails hook is implemeted as a Javascript function that takes a single argument—a reference to the running `sails` instance—and returns an object with one or more of the keys described later in this document. The most basic hook would look like this: ```javascript module.exports = function myBasicHook(sails) { return {}; } ``` It wouldn't do much, but it would work! Each hook should be saved in its own folder with the filename `index.js`. The folder name should uniquely identify the hook, and the folder can contain any number of additional files and subfolders. Extending the previous example, if you saved the file containing `myBasicHook` in a Sails project as `index.js` in the folder `api/hooks/my-basic-hook` and then lifted your app with `sails lift --verbose`, you would see the following in the output: `verbose: my-basic-hook hook loaded successfully.` ### Hook features The following features are available to implement in your hook. All features are optional, and can be implemented by adding them to the object returned by your hook function. * [.defaults](https://sailsjs.com/documentation/concepts/extending-sails/hooks/hook-specification/defaults) * [.configure()](https://sailsjs.com/documentation/concepts/extending-sails/hooks/hook-specification/configure) * [.initialize()](https://sailsjs.com/documentation/concepts/extending-sails/hooks/hook-specification/initialize) * [.routes](https://sailsjs.com/documentation/concepts/extending-sails/hooks/hook-specification/routes) * [.registerActions()](https://sailsjs.com/documentation/concepts/extending-sails/hooks/hook-specification/register-actions) ### Custom hook data and functions Any other keys added to the object returned from the main hook function will be provided in the `sails.hooks[]` object. This is how custom hook functionality is provided to end-users. Any data and functions that you wish to remain private to the hook can be added *outside* the returned object: ```javascript // File api/hooks/myhook/index.js module.exports = function (sails) { // This var will be private var foo = 'bar'; return { // This var will be public abc: 123, // This function will be public sayHi: function (name) { console.log(greet(name)); } }; // This function will be private function greet (name) { return 'Hi, ' + name + '!'; } }; ``` The public var and function above would be available as `sails.hooks.myhook.abc` and `sails.hooks.myhook.sayHi`, respectively. ================================================ FILE: docs/concepts/extending-sails/Hooks/hookspec/initialize.md ================================================ # `.initialize` The `initialize` feature allows a hook to perform startup tasks that may be asynchronous or rely on other hooks. All Sails configuration is guaranteed to be completed before a hook’s `initialize` function runs. Examples of tasks that you may want to put in `initialize` include: * logging in to a remote API * reading from a database that will be used by hook methods * loading support files from a user-configured directory * waiting for another hook to load first Like all hook features, `initialize` is optional and can be left out of your hook definition. If implemented, `initialize` should be an `async function` which must be resolved (i.e. not throw or hang forever) in order for Sails to finish loading: ```javascript initialize: async function() { // Do some stuff here to initialize hook } ``` ##### Hook timeout settings By default, hooks have ten seconds to complete their `initialize` function and resolve before Sails throws an error. That timeout can be configured by setting the `_hookTimeout` key to the number of milliseconds that Sails should wait. This can be done in the hook’s [`defaults`](https://sailsjs.com/documentation/concepts/extending-sails/hooks/hook-specification/defaults): ``` defaults: { __configKey__: { _hookTimeout: 20000 // wait 20 seconds before timing out } } ``` ##### Hook events and dependencies When a hook successfully initializes, it emits an event with the following name: `hook::loaded` For example: * the core `orm` hook emits `hook:orm:loaded` after its initialization is complete * a hook installed into `node_modules/sails-hook-foo` emits `hook:foo:loaded` by default * the same `sails-hook-foo` hook, with `sails.config.installedHooks['sails-hook-foo'].name` set to `bar` would emit `hook:bar:loaded` * a hook installed into `node_modules/mygreathook` would emit `hook:mygreathook:loaded` * a hook installed into `api/hooks/mygreathook` would also emit `hook:mygreathook:loaded` You can use the "hook loaded" events to make one hook dependent on another. To do so, simply wrap your hook’s `initialize` logic in a call to `sails.on()`. For example, to make your hook wait for the `orm` hook to load, you could make your `initialize` similar to the following: ```javascript initialize: async function() { return new Promise((resolve)=>{ sails.on('hook:orm:loaded', ()=>{ // Finish initializing custom hook // Then resolve. resolve(); }); }); } ``` To make a hook dependent on several others, gather the event names to wait for into an array and call `sails.after`: ```javascript initialize: async function() { return new Promise((resolve)=>{ var eventsToWaitFor = ['hook:orm:loaded', 'hook:mygreathook:loaded']; sails.after(eventsToWaitFor, ()=>{ resolve(); }); }); } ``` ================================================ FILE: docs/concepts/extending-sails/Hooks/hookspec/register-actions.md ================================================ # `.registerActions()` If your hook adds new actions to an app, and you want to guarantee that those actions will be maintained even after a call to [`sails.reloadActions()`](https://sailsjs.com/documentation/reference/application/sails-reload-actions), you should register the actions from within a `registerActions` method. For example, the core Sails security hook registers the [`grant-csrf-token` action](https://sailsjs.com/documentation/concepts/security/csrf#?using-ajax-websockets) from within a `registerActions()` method. `registerActions` should be implemented as a function with a single argument (a callback) to be called after the hook is done adding actions. In the interest of avoiding duplicate code, you may want to call this method yourself from within the hook’s [`initialize()` method]((https://sailsjs.com/documentation/concepts/extending-sails/hooks/hook-specification/initialize)). ``` registerActions: function(cb) { // Register an action as `myhook/greet` that an app can bind to any route they like. sails.registerAction(function greet(req, res) { var name = req.param('name') || 'stranger'; return res.status(200).send('Hey there, ' + name + '!'); }, 'myhook/greet'); return cb(); } ``` ================================================ FILE: docs/concepts/extending-sails/Hooks/hookspec/routes.md ================================================ # `.routes` The `routes` feature allows a custom hook to easily bind new routes to a Sails app at load time. If implemented, `routes` should be an object with either a `before` key, an `after` key, or both. The values of those keys should in turn be objects whose keys are [route addresses](https://sailsjs.com/documentation/concepts/routes/custom-routes#?route-address), and whose values are route-handling functions with the standard `(req, res, next)` parameters. Any routes specified in the `before` object will be bound *before* custom user routes (as defined in [sails.config.routes](https://sailsjs.com/documentation/reference/configuration/sails-config-routes)) and [blueprint routes](https://next.sailsjs.com/documentation/reference/blueprint-api#?restful-shortcut-routes-and-actions). Conversely, routes specified in the `after` object will be bound *after* custom and blueprint routes. For example, consider the following `count-requests` hook: ```javascript module.exports = function (sails) { // Declare a var that will act as a reference to this hook. var hook; return { initialize: function(cb) { // Assign this hook object to the `hook` var. // This allows us to add/modify values that users of the hook can retrieve. hook = this; // Initialize a couple of values on the hook. hook.numRequestsSeen = 0; hook.numUnhandledRequestsSeen = 0; // Signal that initialization of this hook is complete // by calling the callback. return cb(); }, routes: { before: { 'GET /*': function (req, res, next) { hook.numRequestsSeen++; return next(); } }, after: { 'GET /*': function (req, res, next) { hook.numUnhandledRequestsSeen++; return next(); } } } }; }; ``` This hook will process all requests via the function provided in the `before` object, and increment its `numRequestsSeen` variable. It will also process any *unhandled* requests via the function provided in the `after` object—that is, any routes that aren't bound in the app via a custom route configuration or a blueprint. > The two variables set up in the hook will be available to other modules in the Sails app as `sails.hooks["count-requests"].numRequestsSeen` and `sails.hooks["count-requests"].numUnhandledRequestsSeen` ================================================ FILE: docs/concepts/extending-sails/Hooks/installablehooks.md ================================================ # Creating an installable hook Installable hooks are custom Sails hooks that reside in an application’s `node_modules` folder. They are useful when you want to share functionality between Sails apps, or publish your hook to [NPM](http://npmjs.org) to share it with the Sails community. If you wish to create a hook for use in *just one* Sails app, see [creating a project hook](https://sailsjs.com/documentation/concepts/extending-sails/hooks/project-hooks) instead. To create a new installable hook: 1. Choose a name for your new hook. It must not conflict with any of the [core hook names](https://github.com/balderdashy/sails/blob/master/lib/app/configuration/default-hooks.js). 1. Create a new folder on your system with the name `sails-hook-`. The `sails-hook-` prefix is optional but recommended for consistency; it is stripped off by Sails when the hook is loaded. 1. Create a `package.json` file in the folder. If you have `npm` installed on your system, you can do this easily by running `npm init` and following the prompts. Otherwise, you can create the file manually, and ensure that it contains at minimum the following: ```json { "name": "sails-hook-your-hook-name", "version": "0.0.0", "description": "a brief description of your hook", "main": "index.js", "sails": { "isHook": true } } ``` If you use `npm init` to create your `package.json`, be sure to open the file afterwards and manually insert the `sails` key containing `isHook: true`. 1. Write your hook code in `index.js` in accordance with the [hook specification](https://sailsjs.com/documentation/concepts/extending-sails/hooks/hook-specification). Your new folder may contain other files as well, which can be loaded in your hook via `require`; only `index.js` will be read automatically by Sails. Use the `dependencies` key of your `package.json` to refer to any dependencies that need to be installed in order for your hook to work (you may also use `npm install --save` to easily save dependency information to `package.json`). ### Specifying the internal name Sails uses for your hook (advanced) In certain cases, especially when using a [scoped NPM package](https://docs.npmjs.com/misc/scope) to override a core Sails hook, you will want to change the name that Sails uses internally when it loads your hook. You can use the `sails.hookName` configuration option in your `package.json` file for this. The value should be the name you want to be loaded into the `sails.hooks` dictionary, so you generally will _not_ want a `sails-hooks-` prefix. For example, if you have a module `@mycoolhooks/sails-hook-sockets` that you wish to use to override the core `sails-hook-sockets` module, the `package.json` might look like: ```json { "name": "@mycoolhooks/sails-hook-sockets", "version": "0.0.0", "description": "my own sockets hook", "main": "index.js", "sails": { "isHook": true, "hookName": "sockets" } } ``` ### Testing your new hook Before you distribute your installable hook to others, you’ll want to write some tests for it. This will help ensure compatibility with future Sails versions and significantly reduce hair-pulling and destruction of nearby objects in fits of rage. While a full guide to writing tests is outside the scope of this doc, the following steps should help get you started: 1. Add Sails as a `devDependency` in your hook’s `package.json` file: ```json "devDependencies": { "sails": "~0.11.0" } ``` 1. Install Sails as a dependency of your hook with `npm install sails` or `npm link sails` (if you have Sails installed globally on your system). 1. Install [Mocha](http://mochajs.org/) on your system with `npm install -g mocha`, if you haven’t already. 1. Add a `test` folder inside your hook’s main folder. 2. Add a `basic.js` file with the following basic test: ```javascript var Sails = require('sails').Sails; describe('Basic tests ::', function() { // Var to hold a running sails app instance var sails; // Before running any tests, attempt to lift Sails before(function (done) { // Hook will timeout in 10 seconds this.timeout(11000); // Attempt to lift sails Sails().lift({ hooks: { // Load the hook "your-hook-name": require('../'), // Skip grunt (unless your hook uses it) "grunt": false }, log: {level: "error"} },function (err, _sails) { if (err) return done(err); sails = _sails; return done(); }); }); // After tests are complete, lower Sails after(function (done) { // Lower Sails (if it successfully lifted) if (sails) { return sails.lower(done); } // Otherwise just return return done(); }); // Test that Sails can lift with the hook in place it ('sails does not crash', function() { return true; }); }); ``` 1. Run the test with `mocha -R spec` to see full results. 1. See the [Mocha](http://mochajs.org/) docs for a full reference. ### Publishing your hook Assuming your hook is tested and looks good, and assuming that the hook name isn’t already in use by another [NPM](http://npmjs.org) module, you can share it with world by running `npm publish`. Go you! * [Hooks overview](https://sailsjs.com/documentation/concepts/extending-sails/hooks) * [Using hooks in your app](https://sailsjs.com/documentation/concepts/extending-sails/hooks/using-hooks) * [The hook specification](https://sailsjs.com/documentation/concepts/extending-sails/hooks/hook-specification) * [Creating a project hook](https://sailsjs.com/documentation/concepts/extending-sails/hooks/project-hooks) ================================================ FILE: docs/concepts/extending-sails/Hooks/projecthooks.md ================================================ # Creating a project hook Project hooks are custom Sails hooks that reside in an application’s `api/hooks` folder. They are most useful when you want to take advantage of hook features like [defaults](https://sailsjs.com/documentation/concepts/extending-sails/hooks/hook-specification/defaults) and [routes](https://sailsjs.com/documentation/concepts/extending-sails/hooks/hook-specification/routes) for code that is used by multiple components in a single app. If you wish to re-use a hook in *more than one* Sails app, see [creating an installable hook](https://sailsjs.com/documentation/concepts/extending-sails/hooks/installable-hooks) instead. To create a new project hook: 1. Choose a name for your new hook. It must not conflict with any of the [core hook names](https://github.com/balderdashy/sails/blob/master/lib/app/configuration/default-hooks.js). 2. Create a folder with that name in your app’s `api/hooks` folder. 3. Add an `index.js` file to that folder. 4. Write your hook code in `index.js` in accordance with the [hook specification](https://sailsjs.com/documentation/concepts/extending-sails/hooks/hook-specification). Your new folder may contain other files as well, which can be loaded in your hook via `require`; only `index.js` will be read automatically by Sails. As an alternative to a folder, you may create a file in your app’s `api/hooks` folder like `api/hooks/myProjectHook.js`. #### Testing that your hook loads properly To test that your hook is being loaded by Sails, lift your app with `sails lift --verbose`. If your hook is loaded, you will see a message like: `verbose: your-hook-name hook loaded successfully.` in the logs. * [Hooks overview](https://sailsjs.com/documentation/concepts/extending-sails/hooks) * [Using hooks in your app](https://sailsjs.com/documentation/concepts/extending-sails/hooks/using-hooks) * [The hook specification](https://sailsjs.com/documentation/concepts/extending-sails/hooks/hook-specification) * [Creating an installable hook](https://sailsjs.com/documentation/concepts/extending-sails/hooks/installable-hooks) ================================================ FILE: docs/concepts/extending-sails/Hooks/usinghooks.md ================================================ # Using hooks in a Sails app ## Using a project hook To use a project hook in your app, first create the `api/hooks` folder if it doesn’t already exist. Then [create the project hook](https://sailsjs.com/documentation/concepts/extending-sails/hooks/project-hooks) or copy the folder for the hook you want to use into `api/hooks`. ## Using an installable hook To use an installable hook in your app, simply run `npm install` with the package name of the hook you wish to install (e.g. `npm install sails-hook-autoreload`). You may also manually copy or link an [installable hook folder that you've created](https://sailsjs.com/documentation/concepts/extending-sails/hooks/installable-hooks) directly into your app’s `node_modules` folder. ## Calling hook methods Any methods that a hook exposes are available in the `sails.hooks[]` object. For example, the `sails-hook-email` hook provides a `sails.hooks.email.send()` method (note that the `sails-hook-` prefix is stripped off). Consult a hook’s documentation to determine which methods it provides. ## Configuring a hook Once you’ve added an installable hook to your app, you can configure it using the regular Sails config files like `config/local.js`, `config/env/development.js`, or a custom config file you create yourself. Hook settings are typically namespaced under the hook’s name, with any `sails-hook-` prefix stripped off. For example, the `from` setting for `sails-hook-email` is available as `sails.config.email.from`. The documentation for the installable hook should describe the available configuration options. ## Changing the way Sails loads an installable hook On rare occassions, you may need to change the name that Sails uses for an installable hook, or change the configuration key that the hook uses. This may be the case if you already have a project hook with the same name as an installable hook, or if you’re already using a configuration key for something else. To avoid these conflicts, Sails provides the `sails.config.installedHooks.` configuration option. The hook identity is *always* the name of the folder that the hook is installed in. ```javascript // config/installedHooks.js module.exports.installedHooks = { "sails-hook-email": { // load the hook into sails.hooks.emailHook instead of sails.hooks.email "name": "emailHook", // configure the hook using sails.config.emailSettings instead of sails.config.email "configKey": "emailSettings" } }; ``` > Note: you may have to create the `config/installedHooks.js` file yourself. * [Hooks overview](https://sailsjs.com/documentation/concepts/extending-sails/hooks) * [The hook specification](https://sailsjs.com/documentation/concepts/extending-sails/hooks/hook-specification) * [Creating a project hook](https://sailsjs.com/documentation/concepts/extending-sails/hooks/project-hooks) * [Creating an installable hook](https://sailsjs.com/documentation/concepts/extending-sails/hooks/installable-hooks) ================================================ FILE: docs/concepts/extending-sails/extending-sails.md ================================================ # Extending Sails In keeping with the Node philosophy, Sails aims to keep its core as small as possible, delegating all but the most critical functions to separate modules. There are currently three types of extensions that you can add to Sails: + [**Generators**](https://sailsjs.com/documentation/concepts/extending-sails/Generators): for adding and overriding functionality in the Sails CLI. *Example*: [sails-generate-model](https://www.npmjs.com/package/sails-generate-model), which allows you to create models on the command line with `sails generate model foo`. + [**Adapters**](https://sailsjs.com/documentation/concepts/extending-sails/Adapters): for integrating Waterline (Sails' ORM) with new data sources, including databases, APIs, or even hardware. *Example*: [sails-postgresql](https://www.npmjs.com/package/sails-postgresql), the official [PostgreSQL](http://www.postgresql.org/) adapter for Sails. + [**Hooks**](https://sailsjs.com/documentation/concepts/extending-sails/Hooks): for overriding or injecting new functionality in the Sails runtime. *Example*: [sails-hook-autoreload](https://www.npmjs.com/package/sails-hook-autoreload), which adds auto-refreshing for a Sails project's API without having to manually restart the server. If you’re interested in developing a plugin for Sails, you will most often want to make a [hook](https://sailsjs.com/documentation/concepts/extending-sails/Hooks). * _Core hooks_, like `http`, `request`, etc. are hooks which are bundled with Sails out of the box. They can be disabled by specifying a `hooks` configuration in your `.sailsrc` file, or when lifting Sails programatically. ================================================ FILE: docs/concepts/shell-scripts/shell-scripts.md ================================================ # Shell scripts Sails comes bundled with [Whelk](https://github.com/sailshq/whelk), which lets you run JavaScript functions as shell scripts. This can be useful for running scheduled jobs (cron, Heroku scheduler), worker processes, and any other custom, one-off scripts that need access to your Sails app's models, configuration, and helpers. ### Your first script To add a new script, just create a file in the `scripts/` folder of your app. ```bash sails generate script hello ``` Then, to run it, use: ```bash sails run hello ``` > If you need to run a script without global access to the `sails` command-line interface (in a Procfile, for example), use `node ./node_modules/sails/bin/sails run hello`. ### Example Here's a more complex example that you might see in a real-world app: ```js // scripts/send-email-proof-reminders.js module.exports = { description: 'Send a reminder to any recent users who haven\'t confirmed their email address yet.', inputs: { template: { description: 'The name of another email template to use as an optional override.', type: 'string', defaultsTo: 'reminder-to-confirm-email' } }, fn: async function (inputs, exits) { await User.stream({ emailStatus: 'pending', emailConfirmationReminderAlreadySent: false, createdAt: { '>': Date.now() - 1000*60*60*24*3 } }) .eachRecord(async (user, proceed)=>{ await sails.helpers.sendTemplateEmail.with({ template: inputs.template, templateData: { user: user }, to: user.emailAddress }); return proceed(); });//∞ return exits.success(); } }; ``` Then you can run: ```bash sails run send-email-proof-reminders ``` For more detailed information on usage, see the [`whelk` README](https://github.com/sailshq/whelk/blob/master/README.md). ================================================ FILE: docs/contributing/adapter-specification.md ================================================ # Adapter interface reference > The adapter interface specification is currently under active development and may change. ## Semantic (interface) > e.g. `RestAPI` or `MySQL` > ##### Stability: [3](http://nodejs.org/api/documentation.html#documentation_stability_index) - Stable Implementing the basic semantic interface (CRUD) is really a step towards a complete implementation of the Queryable interface, but with some services/datasources, about as far as you'll be able to get using native methods. By supporting the Semantic interface, you also get the following: + if you write a `find()` function, developers can also use all of its synonyms, including dynamic finders and `findOne()`. When they're called, they'll automatically be converted into the appropriate criteria object for the basic `find()` definition in your adapter. + as long as you implement basic `where` functionality (see `Queryable` below), Waterline can derive a simplistic version of associations support for you. To optimize the default assumptions with native methods, override the appropriate methods in your adapter. > All officially supported Sails.js database adapters implement the `Semantic` interface. ###### Class methods + `Model.create()` + `Model.find()` + `Model.update()` + `Model.destroy()` + Optimizations: + `findOrCreate()` + `createEach()` + Not yet available: + `destroyEach()` + `updateEach()` + `findOrCreateEach()` + `findAndUpdateOrCreate()` + `findAndUpdateOrCreateEach()` ## Queryable (interface) > ##### Stability: [3](http://nodejs.org/api/documentation.html#documentation_stability_index) - Stable Query building features are common in traditional ORMs, but not at all a guarantee when working with Waterline. Since Waterline adapters can support services as varied as Twitter, SMTP, and Skype, traditional assumptions around structured data don't always apply. If query modifiers are enabled, the adapter must support `Model.find()`, as well as the **complete** query interface, or, where it is impossible to do so, at least provide good error messages. If coverage of the interface is unfinished, it's still not a bad idea to make the adapter available, but it's important to clearly state the unifinished parts, and consequent limitations, up front. This helps prevent the creation of off-topic issues in Sails/Waterline core, protects developers from unexpected consequences, and perhaps most importantly, helps focus contributors on high-value tasks. > All officially supported Sails.js database adapters implement this interface. ###### Query modifiers Query modifiers include normalized syntax: + `where` + `limit` + `skip` + `sort` + `select` And WHERE supports: Boolean logic: + `and` + `or` + `not` `IN` queries: Adapters which implement `where` should recognize a list of values (e.g. `name: ['Gandalf', 'Merlin']`) as an `IN` query. In other words, if `name` is either of those values, a match occured. Sub-attribute modifiers: You are also responsible for sub-attribute modifiers, (e.g. `{ age: { '>=' : 65 } }`) with the notable exception of `contains`, `startsWith`, and `endsWith`, since support for those modifiers can be derived programatically by leveraging your definition of `like`. + `like` (SQL-style, with % wildcards) + `'>' ` (you can also opt to use the more verbose `.greaterThan()`, etc.) + `'<' ` + `'>='` + `'<='` ## Migratable (interface) > ##### Stability: [1](http://nodejs.org/api/documentation.html#documentation_stability_index) - Experimental Adapters which implement the Migratable interface are usually interacting with SQL databases. This interface enables the `migrate` configuration option on a per-model or adapter-global basis, as well as access to the prototypal/class-level CRUD operations for working with tables. ###### Adapter methods > This is not how it actually works, but how it could work soon: + `Adapter.define()` + `Adapter.describe()` + `Adapter.drop()` + `Adapter.alter()` (change table name, other table metadata) + `Adapter.addAttribute()` (add column) + `Adapter.removeAttribute()` (remove column) + `Adapter.alterAttribute()` (rename column, add or remove uniquness constraint to column) + `Adapter.addIndex()` + `Adapter.removeIndex()` ###### Auto-migration strategies + `"safe"` (default in production env) + do nothing + `"drop"` (default in development env) + drop all tables and recreate them each time the server starts-- useful for development + `"alter"` + experimental automigrations + `"create"` + create all missing tables/columns without modifying existing data ## SQL (interface) > ##### Stability: [1](http://nodejs.org/api/documentation.html#documentation_stability_index) - Experimental Adapters which implement the SQL interface interact with databases supporting the SQL language. This interface exposes the method `.query()` allowing the user to run *raw* SQL queries against the database. ###### Adapter methods + `Adapter.query(query,[ data,] cb)` ================================================ FILE: docs/contributing/code-of-conduct.md ================================================ # Code of conduct > The Code of Conduct explains the *bare minimum* behavior expectations the Sails project requires of its contributors. This Code of Conduct is adapted from the version used by the [Node.js core team](https://github.com/nodejs/node/blob/master/CODE_OF_CONDUCT.md). Their version was originally borrowed from [Rust lang's excellent CoC](http://www.rust-lang.org/conduct.html). - We are committed to providing a friendly, safe and welcoming environment for all, regardless of gender, sexual orientation, disability, ethnicity, religion, or similar personal characteristic. - Please avoid using overtly sexual, racial, or political nicknames, or any other nicknames that might detract from a friendly, safe and welcoming environment for all. - Please be kind and courteous. There's no need to be mean or rude. - Avoid the use of personal pronouns in any code comments or documentation where such use could be perceived in a negative light. There is no need to address persons when explaining code (e.g. "When the developer"). - Respect that some individuals and cultures consider the casual use of profanity offensive and off-putting. - Respect that people have differences of opinion and that every design or implementation choice carries a trade-off and numerous costs. There is seldom a right answer. - Please keep unstructured critique to a minimum. If you have ideas you want to experiment with, make a fork and see how it works. - We will exclude you from interaction if you insult, demean or harass anyone. That is not welcome behavior. We interpret the term "harassment" as including the definition in the [Citizen Code of Conduct](http://citizencodeofconduct.org/); if you have any lack of clarity about what might be included in that concept, please read their definition, or ask one of the project maintainers first. In particular, we don't tolerate defamatory remarks or behavior that excludes people in socially marginalized groups, or for whom English is not a native language. - Private harassment is also unacceptable. No matter who you are, if you feel you have been or are being harassed or made uncomfortable by a community member, please contact one of the core maintainers immediately via private message on Twitter or by emailing [inquiries@sailsjs.com](inquiries@sailsjs.com). In either case, include a capture (screenshot, log, photo, email) of the harassment if possible. Whether you're a regular contributor or a newcomer, we care about making this community a safe, comfortable place for you and we've got your back. - Likewise any spamming, trolling, flaming, baiting or other attention-stealing behavior is not welcome, and will result in your exclusion. ================================================ FILE: docs/contributing/code-submission-guidelines/best-practices.md ================================================ # Best practices There are many undocumented best practices and workflow improvements for developing in Sails that contributors have established over the years. This section is an attempt to document some of the basics, but be sure and pop into [Gitter](https://gitter.im/balderdashy/sails) if you ever have a question about how to set things up or want to share your own tool chain. The best way to work with Sails core is to fork the repository, `git clone` it to your filesystem, and then run `npm link`. In addition to writing tests, you'll often want to use a sample project as a harness; to do that, `cd` into the sample app and run `npm link sails`. This will create a symbolic link in the `node_modules` directory of your sample app that points to your local cloned version of Sails. This keeps you from having to copy the framework over every time you make a change. You can force your sample app to use the local Sails dependency by running `node app` instead of `sails lift` (although `sails lift` **should** use the local dependency, if one exists). If you need to test the command line tool this way, you can access it from your sample app as `node node_modules/sails/bin/sails`. For example, if you were working on `sails new`, and you wanted to test it manually, you could run `node node_modules/sails/bin/sails new testProj`. #### Installing different versions of Sails | Release | Install Command | Build Status | |-----------------------|--------------------------|-------------------| | [latest](https://npmjs.com/package/sails) | `npm install sails` | Stable | | [edge](https://github.com/balderdashy/sails/tree/master) | `npm install sails@git://github.com/balderdashy/sails.git` | [![Build Status](https://travis-ci.org/balderdashy/sails.png?branch=master)](https://travis-ci.org/balderdashy/sails/branches) | #### Installing an unreleased branch for testing In general, you can `npm install` Sails directly from Github as follows: ```sh # Install an unreleased branch of Sails in the current directory's `node_modules` $ npm install sails@git://github.com/balderdashy/sails.git#nameOfDesiredBranch ``` This is useful for testing/installing hot-fixes and just a good thing to know how to do in general. #### Submitting Pull Requests 0. If this is your first time forking and submitting a PR, [follow our instructions here](https://sailsjs.com/documentation/contributing/code-submission-guidelines/sending-pull-requests). 1. Fork the repo. 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! 4. Make the tests pass and make sure you follow [our syntax guidelines](https://github.com/balderdashy/sails/blob/master/.jshintrc). 5. Add a line of what you did to CHANGELOG.md (right under `master`). 6. Push to your fork and submit a pull request to the appropriate branch: + [Master](https://github.com/balderdashy/sails/tree/master) + Corresponds with the "edge" version—the latest, not-yet-released version of Sails. Most pull requests should be sent here. + [Latest (or "stable")](https://npmjs.com/package/sails) + Corresponds with the latest stable release on npm (if you have a high-priority hotfix, send the PR explaining that). ================================================ FILE: docs/contributing/code-submission-guidelines/code-submission-guidelines.md ================================================ # Code submission guidelines There are two types of code contributions we can accept in Sails core: patches and new features. **Patches** are small fixes and represent everything from typos to timing issues. Removing an unused `require()` from the top of a file, or fixing a typo that is crashing the master branch tests on Travis are two great examples of patches. Major refactoring projects that change whitespace and variable names across multiple files are **not** patches. Also, keep in mind that even a seemingly trivial change is not a patch if it affects the usage of a documented feature of Sails, or if it adds an undocumented public function. **New features** are TODOs summarized in the [Sails Roadmap](https://github.com/balderdashy/sails/blob/master/ROADMAP.md) file, with more information in an accompanying pull request. Anything that is not specifically in the ROADMAP.md file should not be submitted as a new feature. If in doubt about whether a change you would like to make would be considered a "patch", please open an issue in the [issue tracker](https://github.com/balderdashy/sails/issues/new) or contact someone from our [core team](https://sailsjs.com/about) on Twitter _before_ you begin work on the pull request. Especially do so 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 planned or ongoing development efforts of the project's maintainers. #### General rules - **Javascript supported by [maintained LTS](https://github.com/nodejs/Release/blob/0e0b592273104d1cca9154588092654b932659b1/README.md) only, please**. For consistency, all imperative code in Sails core, including core hooks and core generators, must be written in JavaScript—not CoffeeScript, TypeScript, or any other pre-compiled or transpiled language. Don't get us wrong: we think it's great to use ES6, TypeScript, and/or CoffeeScript syntax in userland code if it boosts your productivity! But for compatibility and consistency reasons, we cannot merge a pull request unless it is written in maintained LTS-supported JavaScript. - Do not auto-format code or attempt to fix perceived style problems in existing files in core. - Keep each pull request narrowly focused on a single goal, and change as few LoC/files as possible. - Do not submit pull requests that implement new features or enhance existing features unless you are working from a very clearly-defined proposal. As stated above, nothing is more frustrating than seeing your hard work go unmerged because your vision does not align with a project's maintainers. - Before beginning work on a feature, be sure to leave a comment telling other contributors that you are working on that feature. Note that if you do not actively keep other contributors informed about your progress, your silence may be taken as inactivity, and someone else may start their own work on that feature. #### Contributing to core Sub-modules within the Sails core are at varying levels of API stability. Bug fixes (patches) are always welcome, but API or behavioral changes cannot be merged without serious planning, as documented in the process for feature proposals above. Sails 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 (e.g. Express, Socket.io, etc.) Please do not send your patch or feature request to the Sails repository—we cannot accept or fulfill it. (Though if you reach out via chat, we'll try to help if we can.) #### Contributing to an adapter If the adapter is part of core (code base located in the Sails repo), please follow the general best practices for contributing to Sails core. If it is located in a different repo, please send feature requests and patches there. #### Authoring a new adapter Sails adapters translate Waterline query syntax into the lower-level language of the integrated database, and they take the results from the database and map them to the response expected by Waterline, the Sails framework's ORM. While creating a new adapter should not be taken lightly, in many cases, writing an adapter is not as hard as it sounds (since you usually end up wrapping around an existing npm package), and it's a great way to get your feet wet with contributing to the ORM hook in Sails and to the Waterline code base. Before starting work on a new adapter, just make sure and do a thorough search on npm, Google and Github to check that someone else hasn't already started working on the same thing. Read more about adapters in [Concepts > Extending Sails > Adapters](https://sailsjs.com/documentation/concepts/extending-sails/adapters). #### Contributing to a hook If the hook is part of core (code base is located in the Sails repo), please follow the general best practices for contributing to Sails core. If the hook is located in a different repo, please send feature requests, patches, and issues there. Many core hooks have README.md files with extensive documentation of their purpose, the methods they attach, the events they trigger, and any other relevant information about their implementation. #### Authoring a new hook Creating a hook is a great way to accomplish _almost anything_ in Sails core. Before starting work on a new custom hook, 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. Read more about custom hooks in [Concepts > Extending Sails > Hooks](https://sailsjs.com/documentation/concepts/extending-sails/hooks). #### Contributing to a generator If the generator is part of core (code base is located in the Sails repo), please follow the general best practices for contributing to Sails core. If it is located in a different repo, please send feature requests, patches, and issues there. #### Authoring a new generator The custom generator API is not 100% stable yet, but it is settling. Feel free to start work on a new custom generator, but first 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. A custom generator is a great way to get your feet wet with contributing to the Sails code base. ================================================ FILE: docs/contributing/code-submission-guidelines/sending-pull-requests.md ================================================ # Sending pull requests This guide is designed to get you started contributing to the Sails framework. It assumes basic familiarity with Github, but it should be useful for contributors of all levels. ## Contribution guidelines Like any open-source project, we must have guidelines for contributions—it helps protect the quality of the code and ensures that our framework stays robust and dependable. For these reasons, it's important that contribution protocols are followed for *all* contributions to Sails, whether they be bug fixes or whole sets of new features. Before submitting a pull request, please make sure: - Any bug fixes have accompanying tests where possible. We use [Mocha](http://visionmedia.github.io/mocha/) for testing. - Code follows our style guide, to maintain consistency (see `.jshint` and/or `.editorconfig` files in repo). If you have a high-priority hot-fix for the currently deployed version, please [post an issue on Github](https://github.com/balderdashy/sails/issues?milestone=none&state=open) and mention @mikermcneil. Also, for emergencies, please feel free to tweet @sailsjs. Now that we are all on the same page, lets get to coding some awesomeness of our own :D ## Fork Start by forking the repository: ![Screen Shot 2013-02-12 at 2.37.04 PM.png](http://i.imgur.com/h0CCcAu.png) ## Clone Then clone your fork into your local filesystem: git clone `git@github.com:YOUR_USER_NAME/sails.git` ## Update To merge recent changes into your fork, inside your project dir: ``` git remote add core https://github.com/balderdashy/sails.git git fetch core git merge core/master ``` For additional details, see [Github](https://help.github.com/articles/fork-a-repo). ## Code Make your enhancements, fix bugs, do your thang. ## Test Please write a test for your addition/fix. I know it kind of sucks if you're not used to it, but it's how we maintain great code. For our test suite, we use [Mocha](http://visionmedia.github.com/mocha/). You can run the tests with `npm test`. See the "Testing" section in the contribution guide for more information. ![Screen Shot 2013-02-12 at 2.56.59 PM.png](http://i.imgur.com/dalbOdZ.png) ## Pull request When you're done, you can commit your fix, push up your changes, and then go into Github and submit a pull request. We'll look it over and get back to you ASAP. ![Screen Shot 2013-02-12 at 2.55.40 PM.png](http://i.imgur.com/GBg0AOi.png) ## Running your fork with your application If you forked Sails and you want to test your Sails app against your fork, here's how you do it: In your local copy of your fork of Sails: `sudo npm link` In your Sails app's repo: `npm link sails` This creates a symbolic link as a local dependency (in your app's `node_modules` folder). This has the effect of letting you run your app with the version Sails you `linked`. ```bash $ sails lift ``` ### *Thanks for your contributions!* ================================================ FILE: docs/contributing/code-submission-guidelines/writing-tests.md ================================================ # Writing tests ### What to test In an ideal world, any possible action you could perform as a Sails user—whether programatically or via the command-line tool—would have a test. However, the number of configuration variations in Sails, along with the fact that userland code can override just about any key piece of core, means we'll never _quite_ get to this point. And that's okay. Instead, the Sails project's goal is for any _feature of Sails_ you might use—programatically or via the command-line tool—to have a test. In cases where these features are implemented within a dependency, the only tests for that feature exist within that dependency (e.g. [Waterline](https://github.com/balderdashy/waterline/tree/master/test), [Skipper](https://github.com/balderdashy/skipper/tree/master/test), and [Captains Log](https://github.com/balderdashy/captains-log/tree/master/test)). Even in these cases, though, tests in Sails inevitably end up retesting certain features that are already verified by Sails' dependencies, and there's nothing wrong with that. ### What _not_ to test We should strive to avoid tests which verify exclusivity: it cripples our ability to develop quickly. In other words, tests should not fail with the introduction of additive features. For instance, if you're writing a test to check that the appropriate files have been created with `sails new`, it would make sense to check for those files, but it would _not_ make sense to ensure that ONLY those files were created (i.e. adding a new file should not break the tests). Another example is a test which verifies the correctness of blueprint configuration, e.g. `sails.config.blueprints.rest`. The test should check that blueprints behave properly with the `rest` config enabled and disabled. We could change the configuration, add more controller-specific options, etc., and we'd only need to write new tests. If, on the other hand, our strategy for testing the behavior of the blueprints involved evaluating the behavior and *then* making a judgement on what the config "_should_" look like, we'd have to modify the tests when we added new options. This may not sound like a big deal, but it can grow out of proportion quickly! ================================================ FILE: docs/contributing/contributing-to-the-documentation.md ================================================ # Contributing to the documentation The official documentation on the Sails website is compiled from markdown files in the [sails](https://github.com/balderdashy/sails/sails-docs) repo. Please send a pull request to the **master** branch with amendments and they'll be double-checked and merged as soon as possible. We are open to suggestions about the process we're using to manage our documentation, and to working with the community in general. Please post to the [Gitter](https://gitter.im/balderdashy/sails) with your ideas; or, if you're interested in helping directly, contact @fancydoilies or @mikermcneil on Twitter. #### What branch should I edit? That depends on what kind of edit you are making. Most often, you'll be making an edit that is relevant for the latest stable version of Sails (i.e. the version on [NPM](npmjs.org/package/sails)) and so you'll want to edit the `master` branch of _this_ repo (what you see in the sails repo by default). The docs team merges master into the appropriate branch for the latest stable release of Sails, and then deploys that to sailsjs.com about once per week. On the other hand, if you are making an edit related to an unreleased feature in an upcoming version—usually as an accompaniment a feature proposal or open pull request to Sails or a related project—then you will want to edit the branch for the next, unreleased version of Sails (sometimes called "edge"). | Branch (in `sails` or `sails-docs`) | Documentation for Sails Version... | Preview At... | |-------------------------------------------------------------------------------------|------------------------|:-------------------| | [`master`](https://github.com/balderdashy/sails/tree/master/docs) | [![NPM version](https://badge.fury.io/js/sails.png)](http://badge.fury.io/js/sails) | [preview.sailsjs.com](http://preview.sailsjs.com) | [`0.12`](https://github.com/balderdashy/sails-docs/tree/0.12) | Sails v0.12.x | [sailsjs.com](https://sailsjs.com) | [`0.11`](https://github.com/balderdashy/sails-docs/tree/0.11) | Sails v0.11.x | [0.11.sailsjs.com](http://0.11.sailsjs.com) #### How are these docs compiled and pushed to the website? We use a module called `doc-templater` to convert the .md files to the HTML for the website. You can learn more about how it works in [the doc-templater repo](https://github.com/uncletammy/doc-templater). Each .md file has its own page on the website (e.g. all reference, concepts, and anatomy files), and should include a special `` tag with a `value` property specifying the title for the page. This will impact how the doc page appears in search engine results, and it will also be used as its display name in the navigation menu on sailsjs.com. For example: ```markdown ``` #### When will my change appear on the Sails website? Documentation changes go live when they are merged onto a special branch corresponding with the current stable version of Sails (e.g. 0.12). We cannot merge pull requests sent directly to this branch—its sole purpose is to reflect the content currently hosted on sailsjs.com, and content is only merged just before redeploying the Sails website. If you want to see how documentation changes will appear on sailsjs.com, you can visit [preview.sailsjs.com](http://preview.sailsjs.com). The preview site updates itself automatically as changes are merged into the master branch of sails. #### How can I help translate the documentation? A great way to help the Sails project, especially if you're a native speaker of a language other than English, is to volunteer to translate the Sails documentation. If you are interested in beginning a translation project, follow these steps: + Bring the documentation folder from the [sails repo](https://github.com/balderdashy/sails/tree/master/docs) (`balderdashy/sails/docs`) into a new repo named `sails-docs-{{IETF}}` where {{IETF}} is the [IETF language tag](https://en.wikipedia.org/wiki/IETF_language_tag) for your language. + Edit [the documentation README](https://github.com/balderdashy/sails/tree/master/docs) to summarize your progress so far, provide any other information you think would be helpful for others reading your translation, and let interested contributors know how to contact you. + When you are satisfied with the first complete version of your translation, open an issue and someone from our docs team will be happy to help you preview it in the context of the Sails website, get it live on a domain (yours, or a subdomain of sailsjs.com, whichever makes the most sense), and share it with the rest of the Sails community. ================================================ FILE: docs/contributing/contributors-pledge.md ================================================ # Contributor's pledge By making a contribution to this project, I certify that: * (a) The contribution was created in whole or in part by me and I have the right to submit it under the MIT license; or * (b) The contribution is based upon previous work that, to the best of my knowledge, is covered under an appropriate open source license and I have the right under that license to submit that work with modifications, whether created in whole or in part by me, under the same open source license (unless I am permitted to submit under a different license), as indicated in the file; or * (c) The contribution was provided directly to me by some other person who certified (a), (b) or (c) and I have not modified it. > The certificate of origin above is based on the "[Developer's Certificate of Origin 1.0](https://github.com/nodejs/node/blob/master/CONTRIBUTING.md#developers-certificate-of-origin-10)" used by Node.js core. ================================================ FILE: docs/contributing/core-maintainers.md ================================================ # Core maintainers The Sails.js core maintainers constitute a small team of individuals located in Austin, TX who are passionate about making it easier for everyone to develop scalable, secure, custom web applications. We fell in love with Node.js at first sight and are firm believers in the continued, unprecedented dominance of JavaScript as a unifying force for good. We see Node.js as the logical continuation of the web standards movement into the world of server-side development. The Sails core team maintains the framework and its related sub-projects, including the Waterline ORM, the Node-Machine project, the Skipper body parser, and all officially-supported generators, adapters, and hooks. We rely heavily on the help of a network of contributors and users all over the world, but we make all final decisions about our releases and roadmap. #### History Sails.js was originally developed by [Mike McNeil](http://twitter.com/mikermcneil) with the help of his company [Balderdash](http://www.bizjournals.com/sanantonio/blog/socialmadness/2013/03/sxsw-2013-Balderdash-startup-web-app.html), a small development and design studio in Austin, TX. The first stable version of Sails was released as open source in 2012. Today, it is still actively maintained by the same [core team members](https://sailsjs.com/about), along with the help of many amazing [contributors](https://github.com/balderdashy/sails/network/members). #### Financial Support Today, Sails.js is financially supported by [The Sails Company](https://sailsjs.com/about) ([YC W15](http://techcrunch.com/2015/03/11/treeline-wants-to-take-the-coding-out-of-building-a-backend/)). Please feel free to [contact us directly](https://sailsjs.com/contact) with questions about the company, our [team](https://sailsjs.com/about), or our mission. ================================================ FILE: docs/contributing/intro-to-custom-adapters.md ================================================ # Introduction to custom adapters for Sails/Waterline > ##### Stability: Varies ## Reference Please see the [adapter interface specification](https://github.com/balderdashy/sails/blob/master/docs/contributing/adapter-specification.md). ### What is an adapter? Adapters expose **interfaces**, which imply a conract to implemnt certain functionality. This allows us to guarantee conventional usage patterns across multiple models, developers, apps, and even companies, making app code more maintainable, efficient, and reliable. Adapters are useful for integrating with databases, open APIs, internal/proprietary web services, or even hardware. ### What kind of things can I do in an adapter? Adapters are mainly focused on providing model-contextualized CRUD methods. CRUD stands for create, read, update, and delete. In Sails/Waterline, we call these methods `create()`, `find()`, `update()`, and `destroy()`. For example, a `MySQLAdapter` implements a `create()` method which, internally, calls out to a MySQL database using the specified table name and connection information and runs an `INSERT ...` SQL query. In practice, your adapter can really do anything it likes-- any method you write will be exposed on the raw datastore objects and any models which use them. ## Why would I need a custom adapter? When building a Sails app, the sending or receiving of any asynchronous communication with another piece of hardware can be normalized into an adapter. (viz. API integrations) > **From Wikipedia:** > *http://en.wikipedia.org/wiki/Create,_read,_update_and_delete* > Although a relational database provides a common persistence layer in software applications, numerous other persistence layers exist. CRUD functionality can be implemented with an object database, an XML database, flat text files, custom file formats, tape, or card, for example. In other words, Waterline is not just an ORM for your database. It is a purpose-agnostic, open standard and toolset for integrating with all kinds of RESTful services, datasources, and devices, whether it's LDAP, Neo4J, or [a lamp](https://www.youtube.com/watch?v=OmcQZD_LIAE). I know, I know... Not everything fits perfectly into a RESTful/CRUD mold! Sometimes the service you're integrating with has more of an RPC-style interface, with one-off method names. That's ok-- you can define any adapter methods you like! You still get all of the trickle-down config and connection-management goodness of Waterline core. ## Why should I build a custom adapter? To recap, writing your API integrations as adapters is **easier**, takes **less time**, and **absorbs a considerable amount of risk**, since you get the advantage of a **standardized set of conventions**, a **documented API**, and a **built-in community** of other developers who have gone through the same process. Best of all, you (and your team) can **reuse the adapter** in other projects, **speeding up development** and **saving time and money**. Finally, if you choose to release your adapter as open-source, you provide a tremendous boon to our little framework and our budding Sails.js ecosystem. Even if it's not via Sails, I encourage you to give back to the OSS community, even if you've never forked a repo before-- don't be intimidated, it's not that bad! The more high-quality adapters we collectively release as open-source, the less repetitive work we all have to do when we integrate with various databases and services. My vision is to make building server-side apps more fun and less repetitive for everyone, and that happens one community adapter at a time. I tip my hat to you in advance :) ## What is an Adapter Interface? The functionality of adapters is as varied as the services they connect. That said, there is a standard library of methods, and a support matrix you should be aware of. Adapters may implement some, all, or none of the interfaces below, but rest assured that **if an adapter implements one method in an interface, it should implement *all* of them**. This is not always the case due to limitations and/or incomplete implementations, but at the very least, a descriptive error message should be used to keep developers informed of what's supported and what's not. ##### Class methods Below, `class methods` refer to the static, or collection-oriented, functions available on the model itself, e.g. `User.create()` or `Menu.update()`. To add custom class methods to your model (beyond what is provided in the adapters it implements), define them as top-level key/function pairs in the model object. ##### Instance methods `instance methods` on the other hand, (also known as object, or model, methods) refer to methods available on the individual result models themselves, e.g. `User.findOne(7).done(function (err, user) { user.someInstanceMethod(); });`. To add custom instance methods to your model (beyond what is provided in the adapters it implements), define them as key/function pairs in the `attributes` object of the model's definition. ##### DDL and auto-migrations `DDL` stands for data-definition language, and is a common fixture of schema-oriented databases. In Sails, auto-migrations are supported out of the box. Since adapters for the most common SQL databases support `alter()`, they also support automatic schema migration! In your own adapter, if you write the `alter()` method, the same behavior will take effect. The feature is configurable using the `migrate` property, which can be set to `safe` (don't touch the schema, period), `drop` (recreate the tables every time the app starts), or `alter` (the default-- merge the schema in the apps' models with what is currently in the database). ## Offcially supported adapters Cody, Mike, and the team behind Sails.js at Balderdash support a handful of commonly used adapters. ### Disk Write to your computer's hard disk, or a mounted network drive. Not suitable for at-scale production deployments, but great for a small project, and essential for developing in environments where you may not always have a database set up. This adapter is bundled with Sails and works out of the box with zero configuration. ###### Interfaces implemented: + Semantic + Queryable + Streaming ### Memory Pretty much like Disk, but doesn't actually write to disk, so it's not persistent. Not suitable for at-scale production deployments, but useful when developing on systems with little or no disk space. ###### Interfaces implemented: + Semantic + Queryable + Streaming ### MySQL MySQL is the world's most popular relational database. http://en.wikipedia.org/wiki/MySQL ###### Interfaces implemented: + Semantic + Queryable + Streaming + Migratable ### PostgreSQL [PostgreSQL](http://en.wikipedia.org/wiki/PostgreSQL) is another popular relational database. ###### Interfaces implemented: + Semantic + Queryable + Streaming + Migratable ### MongoDB [MongoDB](http://en.wikipedia.org/wiki/MongoDB) is the leading NoSQL database. ###### Interfaces implemented: + Semantic + Queryable + Streaming ### Redis [Redis](http://redis.io/) is an open source, BSD licensed, advanced key-value store. ###### Interfaces implemented: + Semantic + Queryable > Under active development: > > + sails-s3 > + sails-local-fs ## Notable Community Adapters > ##### Stability: Varies > in various states of completion Community adapters are crucial to the success and central to the philosophy of an open ecosystem for API integrations. The more high-quality adapters you release as open-source, the less repetitive work we all have to do when we integrate with various databases and services. My vision is to make building server-side apps more fun and less repetitive for everyone, and that happens one community adapter at a time. We welcome your support! ### [Mandrill (email-sending service by MailChimp)](https://github.com/mikermcneil/sails-mandrill) + One-Way ### Heroku > Not currently available as open-source. ### Git > Not currently available. ### [CouchDB](https://github.com/craveprogramminginc/sails-couchdb) + Semantic ### [Riak](https://npmjs.org/package/sails-riak) + Semantic ### [REST](https://github.com/zohararad/sails-rest) + Semantic ### [IRC](https://github.com/balderdashy/sails-irc) + Pubsub ### [Twitter](https://github.com/balderdashy/sails-twitter) ### [ElasticSearch](https://github.com/UsabilityDynamics/waterline-elasticsearch) + Semantic ### [JSDom](https://github.com/mikermcneil/sails-jsdom) ### [Yelp](https://github.com/balderdashy/sails-adapter-boilerplate/pull/2) ### [OrientDB](https://github.com/appscot/sails-orientdb) [OrientDB](http://en.wikipedia.org/wiki/OrientDB) is an Open Source NoSQL DBMS with the features of both Document and Graph DBMSs. ###### Interfaces implemented: + Semantic + Queryable + Associations + Migratable > Search google and NPM for more-- there are new adapters being written all the time. > Check out the docs to learn how to write your own custom adapter (whether it's a private, internal project for a proprietary API or something you can share as open-source) > Want to see your adapter listed here? Send a pull request with a link and we'll merge it! ================================================ FILE: docs/contributing/issue-contributions.md ================================================ # Issue contributions When opening new issues or commenting on existing issues in any of the repositories in this GitHub organization, please make sure discussions are related to concrete technical issues of the Sails.js software. Feature requests and ideas are always welcome, but they should not be submitted as GitHub issues. See [Requesting Features](https://sailsjs.com/documentation/contributing/proposing-features-enhancements) below for submission guidelines. For general help using Sails, please refer to the [official Sails documentation](https://sailsjs.com/documentation). For additional help, ask a question on [StackOverflow](http://stackoverflow.com/questions/ask) or refer to any of the [other recommended avenues of support](https://sailsjs.com/support). If you have found a security vulnerability in Sails or any of its dependencies, _do not report it in a public issue_. Instead, alert the core maintainers immediately using the instructions detailed in the [Sails Security Policy](https://sailsjs.com/security). Please observe this request _even for external dependencies not directly maintained by the core Sails.js team_ (e.g. Socket.io, Express, Node.js, or openssl). Whether or not you believe the core team can do anything to fix an issue, please follow the instructions in our security policy to privately disclose the vulnerability as quickly as possible. Finally, discussion of a non-technical nature, including subjects like team membership, trademark, code of conduct, and high-level questions or concerns about the project should be sent directly to the core maintainers by emailing [inquiries@sailsjs.com](inquiries@sailsjs.com). #### Opening an issue > Sails is composed of a number of different sub-projects, many of which have their [own dedicated repository](https://sailsjs.com/architecture). Even so, the best place to submit a suspected issue with a module maintained by the Sails core team is in the main Sails repo. This helps us stay on top of issues and keep organized. Before submitting an issue, please follow these simple instructions: First, search for issues similar to yours in [GitHub search](https://github.com/balderdashy/sails/search?type=Issues) within the main Sails repo. - If your original bug report is covered by an existing open issue, then add a comment to that issue instead of opening a new one. - If all clearly related issues are closed, then open a new issue and paste links to the URLs of the already closed issue(s) at the bottom. - If you cannot find any related issues, try using different search keywords, if appropriate (in case this affects how you search, at the time of this writing, GitHub uses ElasticSearch, which is based on Lucene, to index content). If you still cannot find any relevant existing issues, then create a new one. - Please consider the importance of backlinks. A contributor responding to your issue will almost always need to search for similar existing issues theirself, so having the URLs all in one place is a huge time-saver. Also keep in mind that backlinking from new issues causes GitHub to insert a link to the URL of the new issue in referenced original issues automatically. This is very helpful, since many visitors to our GitHub issues arrive from search engines. Once you've determined that a new issue should be created, + Make sure your new issue does not report multiple unrelated problems. - If you are experiencing more than one problem—and the problems are clearly distinct—create a separate issue for each one, but start with the most urgent. - If you are experiencing multiple related problems (problems that you have only been able to reproduce in tandem), then please create only a single issue. Be sure to describe both problems thoroughly, though, as well as the steps necessary to cause them both to appear. + Check that your issue has a concise, on-topic title that uses polite, neutral language to explain the problem as best you can in the available space. The ideal title for your issue is one that communicates the problem at a glance. - For example, _"jst.js being removed from layout.ejs on lift"_ is a **very helpful** title for an issue. - Here are some **non-examples**—that is, examples of issue titles which are **not helpful**: - _"templates dont work"_ : This title is too vague. Even if more information cannot be gleaned, wording like _"unexpected behavior with templates"_ is a little more specific and would likely generate a quicker response. - _"app broken cannot access templates on filesystem because it is broken in the asset pipeline please help"_ : This title is repetative and contains unnecessary content ("_please help_"). Remember that a useful title is both desciptive and concise. - _"jst.js is being REMOVED!!!!!!!!!"_: This title contains unnecessary capitalization and punctuation, which is distracting at best, and may be perceived as impolite. In either case, it's unlikely to speed the response to your issue. - _"How does this dumb, useless framework remove jst.js from my app?"_: This title contains unnecessary negativity, which doesn't encourage participant review. Try keeping titles as objective as possible for the best possible issue resolution experience. - _"Thousands of files being corrupted in our currently deployed production app every time the server crashes."_: Language like this might be perceived as hyperbolic and could lessen the credibility of your claim. In this instance, it may even confuse the issue (e.g. "Is this only happening when NODE_ENV===production?"). + Before putting together steps to reproduce your issue, normalize as many of the variables on your personal development environment as possible: - Make sure you have the right app lifted. - Make sure you've killed the Sails server with CTRL+C and started it again. - Make sure you do not have any open browser tabs pointed at localhost. - Make sure you do not have any other Sails apps running in other terminal windows. - Make sure the app you are using to reproduce the issue has a clean `node_modules/` directory, meaning: - no dependencies are linked (e.g. you haven't run `npm link foo`) - you haven't made any inline changes to files in the `node_modules/` folder - you don't have any weird global dependency loops The easiest way to double-check any of the above, if you aren't sure, is to run: `rm -rf node_modules && npm cache clear && npm install`. + Remember to provide the version of Sails that your app is using (`sails -v`). - Note that this could be different than the version of Sails you have globally installed. + Provide your currently-installed version of Node.js (`node -v`), your version of NPM (`npm -v`), and the operating system that you are running (OS X, Windows, Ubuntu, etc.) - If you are using `nvm` or another Node version manager like `n`, please be sure to mention that in the issue. + Provide detailed steps to reproduce the problem from a clean Sails app (i.e. an app created with `sails new` on a computer with no special environment variables or `.sailsrc` files) + Finally, take a moment to think about what you are about to post and how it will be interpreted by the rest of the Sails userbase. Make sure it is aligned with our Code of Conduct, and make sure you are not endangering other Sails users by posting a [security vulnerability](https://sailsjs.com/security) publicly. Issues which do not meet these guidelines will usually be closed without being read, with a response asking that the submitter review this contribution guide. If this happens to you, _realize that it's nothing personal_, and that it may even happen again. Please understand that Sails is a large project that receives hundreds of new issue submissions every month, and that we truly appreciate the time you donate to post detailed issues. The more familiar you become with the conventions and ground rules laid out in this contribution guide, the more helpful your future contributions will be for the community. You will also earn the respect of core team members and set a good example for future contributors. > You might think of these rules as guardrails on a beautiful mountain road: they may not always be pretty, and if you run into them you may get banged up a little bit, but, collectively, they keep us all from sliding off a turn and into the abyss. ================================================ FILE: docs/contributing/preface.md ================================================ # Contributing to Sails This guide is designed to help you get off the ground quickly contributing to Sails. Reading it thoroughly will help you write useful issues, make eloquent proposals, and submit top-notch code that can be merged quickly. Respecting the guidelines laid out here helps make the core maintainers of Sails more productive, and makes the experience of working with Sails positive and enjoyable for the community at large. If you are working on a pull request, **please carefully read the this guide in its entirety**. In case of doubt, [open an issue on GitHub](https://github.com/balderdashy/sails/issues/new) or contact someone from our [core team](https://sailsjs.com/about) on Twitter. Especially do so 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 planned or ongoing development efforts of the project's maintainers. > Note that unless otherwise specified, the content in this section is either straight from the hearts of the Sails.js core team, or based on the [Node.js contribution guide](https://github.com/joyent/node/blob/master/CONTRIBUTING.md#contributing). ================================================ FILE: docs/contributing/proposing-features/proposing-features.md ================================================ # Proposing features and enhancements Sails contributors have learned over the years that keeping track of feature requests in the same bucket as potentially-critical issues leads to a dizzying number of open issues on GitHub, and makes it harder for the community as a whole to respond to bug reports. It also introduces a categorization burden: Imagine a GitHub issue that is 2 parts feature request, 3 parts question, but also has a _teensie pinch_ of immediately-relevant-and-critical-issue-with-the-latest-stable-version-of-Sails-that-needs-immediate-attention. If suggestions, requests, or pleas for features or enhancements are submitted as GitHub issues, they will be closed by [sailsbot](http://asksailsbot.tumblr.com/) or one of her lackeys in the Sails core team. This doesn't mean the core team does not appreciate your willingness to share your experience and ideas with us; we just ask that you use our new process. Instead of creating a GitHub issue, please submit your proposal for a new feature or an extension to an existing feature using the process outlined under [Submitting a Proposal](https://sailsjs.com/documentation/contributing/proposing-features-enhancements/submitting-a-proposal). Please **do not propose _changes to the established conventions or default settings_ of Sails**. These types of discussions tend to start "religious wars" about topics like EJS vs. Jade, Grunt vs. Gulp, Express vs. Hapi, etc., and managing those arguments creates rifts and consumes an inordinate amount of contributors' time. Instead, if you have concerns about the opinions, conventions or default configuration in Sails, please [contact the core maintainers directly](mailto:inquiries@sailsjs.com). ================================================ FILE: docs/contributing/proposing-features/submitting-a-proposal.md ================================================ # Submitting a proposal Before submitting a new proposal, please consider the following: Many individuals and companies (large and small) are happily using Sails in production projects (both greenfield and mature) with the currently-released feature set today, as-is. A lot of the reason for this is that Sails was built while the core team was running a development shop, where it was used to take many different kinds of applications from concept to production, and then to serve as the backend for those applications as they were maintained over the next few years. Much like the canonical case of Ruby on Rails, this means that Sails was designed from the beginning to be both developer-friendly and enterprise-friendly using a convention over configuration methodology. **Conventions** make it quick and easy to build new Sails apps and switch between different existing Sails apps, while **configurability** allows Sails developers to be flexible and customize those apps as they mature using the full power of the underlying tool chain (configuration, plugins/overrides, Express, Socket.io, Node.js, and JavaScript). Over the first year of Sails's life, the **configurability** requirement became even more important. As the user base grew and Sails started to be used on all sorts of different projects, and by developers with all sorts of different preferences, the number of feature requests skyrocketed. Sails solved this in 2013 by rewriting its core and becoming innately interoperable: + Since Sails apps are just Node apps, you can take advantage of any of the [millions](bit.ly/npm-numbers) of NPM packages on http://npmjs.org. (And more recently, you can also take advantage of any of the hundreds of automatically-documented machine functions curated from NPM at http://node-machine.org) + Since Sails uses the same req/res/next pattern as Express and Connect, you can take advantage of any middleware written for those middleware frameworks in your app, such as Lusca (security middleware from Paypal) or morgan (HTTP logging util). + Since Sails uses [Consolidate](https://github.com/tj/consolidate.js/), you can use any of the view engines compatible with Express such as Jade, Dust or Handlebars. + Since Sails uses a familiar MVC project structure, you and/or other developers on your team can quickly get up to speed with how the app works, the database schema, and even have a general notion of where common configuration options live. + Since Sails uses Grunt, you can install and use any of the thousands of available Grunt plugins on http://gruntjs.com/plugins in your app. + Sails's hook system allows you to disable, replace, or customize large swaths of functionality in your app, including pieces of Sails core, such as replacing Grunt with Gulp. + Waterline's adapter interface allows you to plug your models into any database such as Oracle, MSSQL, or Orient DB. + Skipper's adapter interface allows you to plug your incoming streaming file uploads into any blob storage container such as S3, GridFS, or Azure. + Sails's generator system allow you to completely control all files and folders that the Sails command-line tool generates when you run `sails new` or `sails generate *`. It is important to realize that today, most (but certainly not all) new features in Sails can be implemented using one or more of the existing plugin interfaces, rather than making a change to core. If the feature you are requesting is an exception to that rule, then please proceed-- but realize that perhaps the most important part of your proposal is a clear explanation of why what you're suggesting is not possible today. The core maintainers of Sails review all feature proposals, and we do our best to participate in the discussion in these PRs. However, many of these proposals can sometimes involve back and forth discussion that could require them to be open for months at a time. So it is important to understand going in that if you are proposing a feature, the onus is on you to fully specify how that feature would work; i.e. how it would be used, how it would be configured, and in particular its implementation-- that is, which modules would need to change to make it a reality, how it would be tested, whether it would be a major or minor-version breaking change, and the additions and/or modifications that would be necessary to the official Sails documentation. With that in mind, to submit a proposal for a new feature, or an extension to an existing feature, please take the following steps: 0. First, look at the `backlog` table in [ROADMAP.MD](https://github.com/balderdashy/sails/blob/master/ROADMAP.md) and also search open pull requests in that file to make sure your change hasn't already been proposed. - If the PR (pull request) has been merged, it means that a core maintainer has (A) looked over the proposal and discussion in the pull request, (B) personally agreed to him or herself that the feature would be a good fit for Sails core, and (C) confirmed the decision with [@mikermcneil](https://github.com/mikermcneil). It also means that the proposal is now in the backlog in ROADMAP.md, which means that the core team would be willing to merge a pull request with code changes adding the feature to Sails core (assuming that pull request follows our coding style conventions and the guidelines in this section). - If the PR has been closed without being merged, it means that the core team has decided that the feature request should not be a part of Sails core. Just because the proposal is closed does not mean the feature will never be achievable in Sails, it just means that (A) it would need to be specced differently to be merged or (B) it would need to be implemented as a plugin (i.e. a hook, adapter, generator, view engine, grunt/gulp task, etc.) - If the PR is _open_, it means that either (A) it was recently posted, (B) there is still an active discussion in progress, (C) that a core maintainer has not had time to look into it yet, or most commonly (D) that one or more core maintainers have looked at and potentially even responded to the proposal, but the team decided there wasn't enough information to make a firm "yes" or "no" judgement call. This fourth scenario is quite common, since it sometimes takes a great deal of time to develop a specification that is thorough enough to merge into the backlog. The core maintainers review and contribute to proposals as much as time allows, but ultimately it is the responsibility of the developers requesting a feature to do the work of fully speccing it out. - While some of Sails's core maintainers carefully filter email from GitHub (because they also like to get other email sometimes), many contributors receive GitHub notifications every time a new comment is posted. Out of respect for them, please do not `*bump*` or `:+1:` feature proposals. Instead, write a concise (3-5 sentences) explanation of your real-world use case for the feature. 1. If it doesn't already exist, create a pull request editing [ROADMAP.MD](https://github.com/balderdashy/sails/blob/master/ROADMAP.md) (the easiest way to do this is opening ROADMAP.md while logged in to GitHub and clicking the "Edit" button). 2. Add a new row to the **Backlog** table with a very short description of the feature, then submit the change as a pull request (the easiest way to do this is to use the GitHub UI as discussed above, make your changes, then follow the on-screen instructions). 3. In the description for your pull request: - First, write out a high-level summary of the feature you are proposing as a concise description (3-5 sentences) focused around a convincing real-world use case where the Sails app you are building or maintaining for your job, your clients, your company, your non-profit work, or your independent hobby project would be made easier by this feature or change. - Next, describe in clear prose with relevant links to code files exactly why it would be difficult or impossible to implement the feature without changing Sails core (i.e. using one or more of the existing plugin mechanisms). If this is not the case, and this feature could be implemented as a plugin, then please reconsider writing your proposal (it is unlikely the core team will be able to accept it). If you are the author of one or more plugins, and feel that you or other users would benefit from having your work in Sails core, please contact the core team directly (see the instructions for submitting "high-level questions or concerns about the project" above). - Finally, if you have time, take a first pass at proposing a spec for this feature (its configuration, usage, and how it would be implemented). If you do not have time to write out a first draft of a thorough specification, please make that point in your feature request, and clarify that it would be up to other contributors with the same or a similar use case to finish this proposal. Proposals which do not meet these guidelines will be closed with a response asking that the submitter review this contribution guide. If this happens to you, _realize it is nothing personal_ and that it may even happen again. Please consider that a tremendous amount of effort has been put into the existing plugin systems in Sails, and so any proposed change to core must be carefully considered in relation to how it would affect existing plugins, existing apps, and future development of the framework. Many Sails contributors have become intimately familiar with how the various systems in Sails interact and will be willing to help you out; but in order for that process to be efficient, it is important that all new features and enhancements follow a common set of ground rules. > ###### If your feature proposal is merged... > Having your proposal merged does not necessarily mean that you are responsible for _implementing_ the feature; and you certainly won't be responsible for _maintaining_ future changes which might affect that feature for all eternity. _That_ privilege is reserved for Mike and the rest of the core team; which is why it is so important to spec out the vision for the usage, configuration, and implementation of your proposed feature from day 1. Working out this sort of a detailed proposal is not an easy task, and often involves more effort than the actual implementation. But if a proposal is accepted, it becomes part of the project's mission: which means once it is implemented and merged, the core team is committed to maintaining it as a part of Sails. ================================================ FILE: docs/contributing/stability-index.md ================================================ # Stability index Throughout the documentation and in README files in Sails, you will see indications of a section's stability. The Sails framework is still somewhat changing, and as it matures, certain parts are more reliable than others. Some are so proven, and so relied upon, that they are unlikely to ever change at all. Others are brand new and experimental, or known to be hazardous and in the process of being redesigned. Stability indices are used to describe individual methods, events, and configuration settings _as well_ as sub-modules of Sails core such as core hooks. The latter affordance is a soft science-- the core team labels hooks with stability indices in order to provide a better experience for developers building plugins for Sails and/or contributing to Sails core. When a stability index refers to a module like a core hook, note that that index refers to the **features of that hook which are _explicitly public_**. For example, if the documentation for a hook mentions that it "exposes" a property called `foo` on the `sails` app object, then you can _only rely on that property_ to respect the hook's the stability level if it is also clearly marked as "public" elsewhere in the hook documentation. If in doubt, submit a pull request to the relevant hook's README file in the [GitHub repository for Sails core](https://github.com/balderdashy/sails) and add a question to the FAQ section. The stability indices are as follows: ##### Stability: 0 - Deprecated This feature is known to be problematic, and changes are planned. Do not rely on it in new code, and be sure to change existing code before upgrading. Use of the feature may cause warnings. Backwards compatibility should not be expected. ##### Stability: 1 - Experimental This feature is subject to change or removal in future major releases of Sails. ##### Stability: 2 - Stable This feature has proven satisfactory. Compatibility with existing Sails apps and the plugin ecosystem is a high priority, and so stable hooks/features/etc. will not be broken or removed in future major releases unless absolutely necessary. ##### Stability: 3 - Locked This hook/feature/etc. will not undergo any future API changes, except as demanded by critical fixes related to security or performance. Please do not propose usage/philosophical changes for features/hooks/etc. at this stability index; they will be refused. ### Notes > - Sails' stability index, and much of the verbiage of this file, is based on [the Stability Index used by Node.js core](https://nodejs.org/api/documentation.html#documentation_stability_index). ================================================ FILE: docs/faq/README.md ================================================ # docs/faq This section contains the contents that will live on sailsjs.com/faq. ### Notes > - This README file **is not compiled to HTML** for the website. It is just here to explain what you're looking at. ================================================ FILE: docs/faq/faq.md ================================================ # Frequently Asked Questions ### Table of Contents 1. [I'm having trouble installing Sails. What should I do?](https://sailsjs.com/faq#?im-having-trouble-installing-sails-what-should-i-do) 2. [What are the dependencies of Sails?](https://sailsjs.com/faq#?what-are-the-dependencies-of-sails) 3. [Who else is using Sails.js?](https://sailsjs.com/faq#?who-else-is-using-sailsjs) 4. [Are there professional support options?](https://sailsjs.com/faq#?are-there-professional-support-options) 5. [Where do I get help?](https://sailsjs.com/faq#?where-do-i-get-help) 6. [What are some good community tutorials?](https://sailsjs.com/faq#?what-are-some-good-community-tutorials) 7. [How can I convince the other girls/guys on my team?](https://sailsjs.com/faq#?how-can-i-convince-the-other-girls-guys-on-my-team) 8. [Where do I submit ideas? Report bugs?](https://sailsjs.com/faq#?where-do-i-submit-ideas-report-bugs) 9. [What version of Sails should I use?](https://sailsjs.com/faq#?what-version-of-sails-should-i-use) 10. [How do I get involved?](https://sailsjs.com/faq#?how-do-i-get-involved) 11. [How does the documentation end up on the Sails website?](https://sailsjs.com/faq#?how-does-the-documentation-end-up-on-the-sails-website) 12. [Where is the documentation for the different releases of Sails?](https://sailsjs.com/faq#?where-is-the-documentation-for-the-different-releases-of-sails) ### I'm having trouble installing Sails. What should I do? Start with NPM's helpful [troubleshooting guide](https://github.com/npm/npm/wiki/Troubleshooting). If you continue to have problems, and you've tried Google searching but you're still stumped, please carefully review the updated Sails [contribution guide](https://sailsjs.com/documentation/contributing) and then create a GitHub issue in the Sails repo. ### What are the dependencies of Sails? [![Dependency Status](https://david-dm.org/balderdashy/sails.png)](https://david-dm.org/balderdashy/sails) We have learned again and again over the years to take versioning of dependencies very seriously. We lock Sails's dependency versions and only bump those versions if the associated updates fix a security issue or present other substantive advantages to Sails users (improved compatibility, performance, etc.) In addition, the core maintainers of Sails are committed to fixing any major security, performance, or stability bugs that arise in any of our core dependencies-- regardless of whether those modules are [officially maintained by another entity or not](https://github.com/balderdashy/sails/pull/3235#issuecomment-170417122). Sails is tested with [node](http://nodejs.org/) versions 0.10.x and up, though, we recommend using The latest LTS version of Node. The framework is built on the rock-solid foundations of [Express](https://github.com/expressjs/) and [Socket.io](http://socket.io/). Out of the box, it also depends on other great modules, like `grunt`, `waterline`, and `fs-extra`. Click the badge above for the full list of dependencies in the latest stable release of Sails core. > **Sails Flagship users:** We manually verify every dependency of Sails and other officially-maintained modules by hand, every single week. This includes core hooks, adapters, generators, client SDKs, and Flagship packages. We regularly send security/compatibility reports about dependencies to the primary email address associated with your account. If you'd like additional people on your team to receive these reports, no problem! Just [let us know](https://flagship.sailsjs.com/ask) their email addresses and we'll get them set up. _(These email addresses will also receive communications about patches, shrinkwrap updates, and compatibility notices.)_ If you have questions or concerns about our dependencies, [talk to a core team member](https://sailsjs.com/contact). _Please do not submit a pull request changing the version of a dependency without first (1) checking that dependency's changelog, (2) verifying compatibility, and (3) [submitting an accompanying PR to update **roadstead**](https://github.com/treelinehq/roadstead/edit/master/constants/verified-releases.type.js), our dependency wallah._ ### Who else is using Sails.js? Sails is used in production by individuals and companies, non-profits, and government entities all over the world, for all sorts of projects (greenfield and mature). You can see some examples [here](https://sailsjs.com/#?using-sails) of companies that have used Sails for their projects. (This small list is definitely not authoritative, so if you're using Sails in your app/product/service, [we'd love to hear about it](https://sailsjs.com/contact)! ### Are there professional support options? [The Sails Company](https://sailsjs.com/about) offers custom development, services, training, enterprise-class products, and support for teams building applications on Sails. ##### Partner with us Our studio provides development services for startups, SMBs, and the Fortune 500. As you might expect, the Sails core team has done a lot of custom Sails/Node.js development, but we also have experience across the full stack, including: advanced interaction design, practical/scalable JavaScript development practices for huge applications, and building rich user experiences across many different devices and screen resolutions. We can build your app and API from scratch, modernize your legacy web platform, or catalyze the development efforts of your established team. If you're interested in working with us on your next project, [drop us a line](https://sailsjs.com/studio#?contact). ##### Sails Flagship for Enterprise Sails Flagship is a platform on top of Sails which provides a suite of additional services, production-quality accoutrements, and support for enterprise use cases. This includes early access to new features and enhancements, a license for our internal tools, as well as exclusive reports and best-practice guides created by core maintainers. To learn more, [set up a call](https://sailsjs.com/contact) _(or [purchase online now](https://sailsjs.com/flagship/plans))_. > We are actively expanding this product offering with new additions and official re-releases of some formerly-experimental modules. If you have specific suggestions/requests for new Flagship packages, please [let us know](http://flagship.sailsjs.com/contact). ##### Professional support / SLAs The Sails Company also provides a lifeline for organizations using Sails to build their products. If you need guaranteed support in the event of a critical production issue, or just want an extra pair of eyes looking out for your code base during development, take a look at our [basic subscriptions](https://sailsjs.com/flagship/plans), or [contact us](https://flagship.sailsjs.com/contact) and we'll give you a call. ### Where do I get help? Aside from the [official documentation](https://sailsjs.com/documentation), be sure and check out the [recommended support options on the Sails website](https://sailsjs.com/support), and pop in to our [Gitter chat room](https://gitter.im/balderdashy/sails). If you're stumped, make sure and [ask a question on StackOverflow](http://stackoverflow.com/questions/ask), where there's an [active Sails community](http://stackoverflow.com/questions/tagged/sailsjs?sort=newest&days=30). Members of our core team recently taught a [free video course](https://courses.platzi.com/courses/develop-apps-sails-js/) on [Platzi](http://platzi.com) and wrote [a book](https://www.manning.com/books/sails-js-in-action). > If you're using [Sails Flagship](https://sailsjs.com/faq#?are-there-professional-support-options), you can contact the core team [here](http://flagship.sailsjs.com/ask). ### What are some good community tutorials? > If you are the author of a tutorial or guide about Sails, please send us a pull request [here](https://github.com/balderdashy/sails/edit/master/docs/faq/faq.md) and we'll check it out. (Be sure to add your tutorial to the top of the applicable list, as we try to order these from newest to oldest.) ##### Multi-part guides: + [The busy JavaScript developer's guide to Sails.js](https://www.ibm.com/developerworks/library/wa-build-deploy-web-app-sailsjs-1-bluemix/index.html) -- 4-part series from IBM developerWorks. (Also available in [Chinese](http://www.ibm.com/developerworks/cn/web/wa-build-deploy-web-app-sailsjs-1-bluemix/) and [Japanese](http://www.ibm.com/developerworks/jp/web/library/wa-build-deploy-web-app-sailsjs-1-bluemix/).) + [SailsCasts](http://irlnathan.github.io/sailscasts/) - Short screencasts that take you through the basics of building traditional websites, single-page/mobile apps, and APIs using Sails. Perfect for both novice and tenured developers, but does assume some background on MVC. + [Sails.js Development channel on Medium](https://medium.com/sails-js-development/) + [Sails.js Course on Pluralsight](https://www.pluralsight.com/courses/two-tier-enterprise-app-api-development-angular-sails) + Sails API Development + [Datalayer -models, connections, waterline](http://www.codeproject.com/Articles/898221/Sails-API-development-Datalayer-models-connections) + [Custom methods, overriding default actions, and related](http://www.codeproject.com/Articles/985730/Sails-API-development-2-2-Custom-methods-overriding-default) + Desarrollar Webapps Realtime: + [Creación](http://jorgecasar.github.io/blog/desarrollar-webapps-realtime-creacion/) + [Usuarios](http://jorgecasar.github.io/blog/desarrollar-webapps-realtime-usuarios/) + [Auth](http://jorgecasar.github.io/blog/desarrollar-webapps-realtime-auth/) + [Auth con Passport](http://jorgecasar.github.io/blog/desarrollar-webapps-realtime-auth-con-passport/) ##### Articles & blog posts: + [Nanobox Blog: Getting Started - A Simple Sails.js App](https://content.nanobox.io/a-simple-sails-js-example-app/) + [Twitter Dev Blog: Guest Post: Twitter Sign-In with Sails.js](https://blog.twitter.com/2015/guest-post-twitter-sign-in-with-treelineio) + [Guest Post on Segment.io Blog: Webhooks with Slack, Segment, and Sails.js/Treeline](https://segment.com/blog/segment-webhooks-slack/) + [Postman Blog: Manage your Sails.js server bootstrap code](http://blog.getpostman.com/2015/08/28/manage-your-sailsjs-server-bootstrap-code/) + [Sails.js on Heroku](https://vort3x.me/sailsjs-heroku/) + [Angular + Sails.js (0.10.0-rc5) with angular-sails socket.io](https://github.com/maartendb/angular-sails-scrum-tutorial/blob/master/README.md) + [Angular + Sails! Help!](https://github.com/xdissent/spinnaker) - Sails Resources Service for AngularJS + [How to Create a Node.js App using Sails.js on an Ubuntu VPS](https://www.digitalocean.com/community/articles/how-to-create-an-node-js-app-using-sails-js-on-an-ubuntu-vps) + [Working With Data in Sails.js](http://net.tutsplus.com/tutorials/javascript-ajax/working-with-data-in-sails-js/) tutorial on NetTuts ##### Video tutorials: + [Develop Web Apps in Node.js and Sails.js](https://courses.platzi.com/courses/sails-js/) + [Jorge Casar: Introduccion a Sails.js](https://www.youtube.com/watch?v=7_zUNTtXtcg) + [Sails.js - How to render node views via Ajax, single page application, SPA](http://www.youtube.com/watch?v=Di50_eHqI7I&feature=youtu.be) + [Intro to Sails.js](https://www.youtube.com/watch?v=GK-tFvpIR7c) [@mikermcneil](https://github.com/mikermcneil)'s original screencast ### How can I convince the other girls/guys on my team? ##### Articles / interviews / press releases / whitepapers / talks > + If you are the author of an article about Sails, please send us a pull request [here](https://github.com/balderdashy/sails/edit/master/docs/faq/faq.md). We'll check it out! > + If you are a company interested in doing a press release about Sails, please contact [@mikermcneil](https://twitter.com/mikermcneil) on Twitter. We'll do what we can to help. + [InfoWorld: Why Node.js beats Java and .Net for web, mobile, and IoT apps](http://www.infoworld.com/article/2975233/javascript/why-node-js-beats-java-net-for-web-mobile-iot-apps.html) _(Speed, scalability, productivity, and developer politics all played a role in [AnyPresence](http://anypresence.com)’s selection of Sails.js/Node.js for its enterprise development platform)_ + [TechRepublic: Build Robust Applications with the Node.js MVC framework](http://www.techrepublic.com/article/build-robust-node-applications-with-the-sails-js-mvc-framework/) + [Microsoft Case Study: Deploying Sails.js to Azure Web Apps](https://blogs.msdn.microsoft.com/partnercatalystteam/2015/07/16/y-combinator-collaboration-deploying-sailsjs-to-azure-web-apps/) + [Mike's interview w/ @freddier and @cvander from Platzi](https://www.youtube.com/watch?v=WN0YgPdPbRE) + [Smashing Magazine: Sailing with Sails.js](https://www.smashingmagazine.com/2015/11/sailing-sails-js-mvc-style-framework-node-js/) + [Presentation at Smart City Conference & Expo 2015](http://www.goodxense.com/blog/post/our-presentation-at-smart-city-conference-expo-2015/) (George Lu & YJ Yang) + [Radio interview with Mike McNeil w/ ComputerAmerica's Craig Crossman](https://www.youtube.com/watch?v=ERIvf2iUj5U&feature=youtu.be) + Sails.js, Treeline and the future of programming ([Article](https://courses.platzi.com/blog/sails-js-creator-mike-mcneil-on-treeline-and-frameworks/) | [Video](https://www.youtube.com/watch?v=nZKG7hLhbRs) | [Deck](https://speakerdeck.com/mikermcneil/what-even-is-software)) + [UI-First API Design & Development: Apigee's I ♥ APIs, San Francisco, 2015](https://speakerdeck.com/mikermcneil/i-love-apis) + [Choosing the right framework for Node.js development](https://jaxenter.com/choosing-the-right-framework-for-node-js-development-126432.html) + [TechCrunch: Our 10 Favorite Companies From Y Combinator Demo Day](https://techcrunch.com/gallery/our-10-favorite-companies-from-y-combinator-demo-day-day-1/slide/11/) + [Sails.js used on the website for the city of Paris](https://twitter.com/parisnumerique/status/617999231182176256) + [18f Open Source Hack Series: Midas](https://18f.gsa.gov/2014/10/01/open-source-hack-series-midas/) + [From Rags to Open Source](https://speakerdeck.com/mikermcneil/all-things-open) (All Things Open, Raleigh, 2014) + SxSW Conference, Austin, TX: ([2014](https://speakerdeck.com/mikermcneil/2014-intro-to-sails-v0-dot-10-dot-x) | [2015](https://speakerdeck.com/mikermcneil/sxsw-2015)) + [More talks by Mike and the Sails.js core team](http://lanyrd.com/profile/mikermcneil/) + [Dessarolo Web: Interview w/ Mike McNeil](https://www.youtube.com/watch?v=XMpf44oV2Og) (Spanish & English--English starts at 1:30) + [CapitalOne blog: Contrasting Enterprise Node.js Frameworks](http://www.capitalone.io/blog/contrasting-enterprise-nodejs-frameworks/) (by [Azat Mardan](https://www.linkedin.com/in/azatm), author of the book "Pro Express.js") + [Alternatives to MongoDB (Chinese article)](http://www.infoq.com/cn/news/2015/07/never-ever-mongodb) + [Introducción a Sails.js, un framework para crear aplicaciones realtime](https://abalozz.es/introduccion-a-sails-js-un-framework-para-crear-aplicaciones-realtime/) + [Austin startup finds success in responsive design](http://www.bizjournals.com/sanantonio/blog/socialmadness/2013/03/sxsw-2013-Balderdash-startup-web-app.html?ana=twt) + [Interact ATX](http://www.siliconhillsnews.com/2013/03/10/flying-high-with-interact-atx-adventures-in-austin-part-3-2-1/) + [Intro to Sails.js :: Node.js Conf: Italy, 2014](http://2014.nodejsconf.it/) + [Startup America](http://www.prlog.org/12038372-engine-pitches-startup-america-board-of-directors.html) + [Recent tweets about Sails.js](https://twitter.com/search?q=treelinehq%20OR%20%40treelinehq%20OR%20%23treelinehq%20OR%20%40waterlineorm%20OR%20treeline.io%20OR%20sailsjs.com%20OR%20github.com%2Fbalderdashy%2Fsails%20OR%20sailsjs%20OR%20sails.js%20OR%20%23sailsjs%20OR%20%40sailsjs&src=typd) + [How to use more open source](https://18f.gsa.gov/2014/11/26/how-to-use-more-open-source/) _(18F is an office inside the U.s. General Services Administration that helps other federal agencies build, buy, and share efficient and easy-to-use digital services.)_ + [Express Web Server Advances in Node.js Ecosystem](https://adtmag.com/articles/2016/02/11/express-joins-node.aspx) ([auch auf Deutsch](http://www.heise.de/developer/meldung/IBM-uebergibt-JavaScript-Webframework-Express-an-Node-js-Foundation-3099223.html)) + Interview w/ Tim Heckel [on InfoQ](http://www.infoq.com/news/2013/04/Sails-0.8.9-Released) + [Sails.js - Une Architecture MVC pour applications real-time Node.js](http://www.lafermeduweb.net/billet/sails-js-une-architecture-mvc-pour-applications-real-time-node-js-1528.html) + [Hacker News](https://news.ycombinator.com/item?id=5373342) + [Pulling the Plug: dotJS (Paris, 2014)](http://www.thedotpost.com/2014/11/mike-mcneil-pulling-the-plug) + [Intro to Sails.js :: Node PDX, Portland, 2013 (Slides)](http://www.slideshare.net/michaelrmcneil/node-pdx)) + [Sail.js : un framework MVC pour Node.js](http://javascript.developpez.com/actu/52729/Sail-js-un-framework-MVC-pour-Node-js/) + [Build Custom & Enterprise Node.js Apps with Sails.js](http://www.webappers.com/2013/03/29/build-custom-enterprise-node-js-apps-with-sails-js/) + [New tools for web design and development: March 2013](http://www.creativebloq.com/design-tools/new-tools-web-design-and-development-march-2013-4132972) + [Sails 0.8.9: A Rails-Inspired Real-Time Node MVC Framework](http://www.infoq.com/news/2013/04/Sails-0.8.9-Released) + [Node.js の MVCフレームワーク Sails.js が良さげなので少し試してみた](http://nantokaworks.com/?p=1101) + [InfoWorld: 13 fabulous frameworks for Node.js](http://www.infoworld.com/article/3064653/application-development/13-fabulous-frameworks-for-nodejs.html#slide9) + [New web design tools that you need to check out](http://www.designyourway.net/blog/resources/new-web-design-tools-that-you-need-to-check-out/) + [Live code Sails.js avec Mike McNeil](http://www.weezevent.com/live-code-sailsjs-avec-mike-mcneil) + [#hack4good adds cities and welcomes Sails.js creator to speak and hack in Paris!](http://us2.campaign-archive1.com/?u=cf9af451f2674767755b02b35&id=fb98713f48&e=b2d87b15fe) + [TechCrunch: Sails.js Funded by Y-Combinator](http://techcrunch.com/2015/03/11/treeline-wants-to-take-the-coding-out-of-building-a-backend/) ### Where do I submit ideas? Report bugs? The Sails project tracks bug reports in GitHub issues and uses pull requests for feature proposals. Please read the [contribution guide](https://sailsjs.com/documentation/contributing) before you create an issue, submit a proposal, or begin working on pull request. ### What version of Sails should I use? [![NPM version](https://badge.fury.io/js/sails.png)](http://badge.fury.io/js/sails) Unless you are a contributor running a pre-release version of the framework in order to do some testing or work on core, you should use the latest stable version of Sails from NPM (click the badge above). Installing is easy- just follow [these instructions](https://sailsjs.com/get-started). > Note: to install/upgrade to the latest version of Sails locally in an existing project, run `npm install sails@latest --save`. If you are having trouble and are looking for a bazooka, you might also want to run `rm -rf node_modules && npm cache clear && npm install sails@latest --force --save && npm install`. If you are looking to install a pre-release version of Sails, you can install from the `beta` tag on npm (i.e. `npm install sails@beta`). This is a great way to try out a coming release ahead of time and start upgrading before the release becomes official. The beta npm release candidate corresponds with the `beta` branch in the Sails repo. (Just be sure to also use the right version of your favorite adapters and other plugins. If in doubt, [feel free to ask](https://sailsjs.com/support).) Finally, if you like living on the edge, or you're working on adding a feature or fixing a bug in Sails, install the edge version from the `master` branch on github. The edge version is not published on the registry since it's constantly under development, but you can _still use npm to install it_ (e.g. `npm install sails@git://github.com/balderdashy/sails.git`) For more instructions on installing the beta and edge versions of Sails, check out the [contribution guide](https://sailsjs.com/documentation/contributing). ### How do I get involved? There are many different ways to contribute to Sails; for example you could help us improve the [official documentation](https://github.com/balderdashy/sails/tree/master/docs), write a [plugin](https://sailsjs.com/documentation/concepts/extending-sails), answer [StackOverflow questions](http://stackoverflow.com/questions/tagged/sails.js), start a Sails meetup, help troubleshoot GitHub issues, write some tests, or submit a patch to Sails core or one of its dependencies. Please look through the [contribution guide](https://sailsjs.com/documentation/contributing) before you get started. It's a short read that covers guidelines and best practices that ensure your hard work will have the maximum impact. ### How does the documentation end up on the Sails website? The documentation is compiled from the markdown files in the [`sails` repo on github](https://github.com/balderdashy/sails/tree/master/docs). A number of Sails users have expressed interest in emulating the process we use to generate the pages on the Sails website. Good news is it's pretty simple: The compilation process for the Sails docs involves generating HTML from Markdown files in the sails repo, then performing some additional transformations such as adding data type bubbles, tagging permalinks for individual sections of pages, building JSON data to power the side navigation menu and setting HTML `` attributes for better search engine discoverability of individual doc pages. See the [doc-templater](https://github.com/uncletammy/doc-templater) module for more information. ### Where is the documentation for the different releases of Sails? The [documentation on the main website](https://sailsjs.com/documentation) is for the latest stable npm release of Sails, and is mirrored by the docs in the [master branch of the `sails` repo on github](https://github.com/balderdashy/sails/tree/master/docs) (Master is sometimes a few commits ahead, but any critical documentation updates make it onto the website within a day or two.) For older releases of Sails that are still widely used, the documentation is compiled from the relevant `sails-docs` branches and hosted on the following subdomains: + [0.12.sailsjs.com](http://0.12.sailsjs.com/) + [0.11.sailsjs.com](http://0.11.sailsjs.com/) ================================================ FILE: docs/irc/irc.md ================================================ ## Grab An IRC Client Below you'll find some of the more popular IRC Clients. ### Linux - [xChat](http://xchat.org) - [irssi](http://irssi.org) - [weeChat](http://www.weechat.org) #### Using apt package manager for Ubuntu/Debian ``` sudo apt-get install weechat ``` ### OSX - [irssi](http://irssi.org) ``` sudo steveJobsPM --prettyPlease install -m 'is this okay?' irssi ``` ### Windows - [xChat](http://xchat.org) - [hydra IRC](http://www.hydrairc.com/content/downloads) ## Setting Up Your Client ### Registering On Freenode Our chat room is on the Freenode network. Freenode does not require that you register your `nick` name. You do have the option to though. If you want to do this, read about how to do it [on the freenode website](https://freenode.net/faq.shtml#registering) ### Getting on Freenode Each IRC Client is a little different to configure. All of the ones we have recommended have very straight forward configuration process. If your client provides a list of available servers, look for the one called Freenode. Make sure to put in a `nick` to go by. Upon connecting to the Freenode network, join us by typing `/join #sailsjs`. If you registered a nick, you can identify yourself with `/msg nickserv identify <password>` ## Getting help on IRC ### `#sailsjs` on irc.freenode.net If you are looking for a quick answer and you can't find what you're looking for in the docs, come ask in our IRC chat room. While there is typically somebody there who can answer your question, please remember that #sailsjs is 100% community maintained. That means that help is given at the discretion of the community. For best results, be polite and to the point. If you've never been on IRC, now is the perfect time. Getting started is easy. ## Sails Troll Sails Troll is our resident IRC Bot. His job is to write down what people say in case someone wants to find it later. He also informs the room whenever someone pushes up a change to any of the repos in the Sails.js ecosystem. <docmeta name="displayName" value="#sailsjs on IRC"> ================================================ FILE: docs/reference/README.md ================================================ # docs/reference This section contains the official reference documentation for Sails. It is made available at https://sailsjs.com/documentation/reference. ### Notes > - This README file **is not compiled to HTML** for the website. It is just here to explain what you're looking at. > - Depending on what branch of `sails` you are currently viewing, the domain may vary. See the top-level documentation README file for information about working with the markdown files in this repo, and to understand the branching/versioning strategy. <docmeta name="notShownOnWebsite" value="true"> ================================================ FILE: docs/reference/application/advanced-usage/advanced-usage.md ================================================ # Advanced usage Most users of the Sails framework will never need to access more than a few basic methods of the `sails` application object. However, if you have an advanced use case or are considering [contributing to Sails](https://sailsjs.com/documentation/contributing), you may need to delve into some of these lesser-used methods or reference the [loading order of Sails core](https://sailsjs.com/documentation/reference/application/advanced-usage/lifecycle). ### Disabling the `sails` global We recommended using the `sails` global with Sails. However, the auto-globalization of `sails` [can be disabled](https://sailsjs.com/documentation/reference/configuration/sails-config-globals). Disabling the `sails` global might be a good idea for use cases where multiple Sails app instances need to exist at once, or where globals are not an option. If the `sails` global is disabled, then you'll need another way to reference the application instance. Luckily, this is possible from almost anywhere in your app: + in the `fn` of an [action](https://sailsjs.com/documentation/concepts/actions-and-controllers) (`this.sails`) + in the `fn` of a [helper](https://sailsjs.com/documentation/concepts/helpers) (`this.sails`). + on an incoming request (`req._sails`) ### Properties (advanced) ##### sails.hooks A dictionary of all loaded [Sails hooks](https://sailsjs.com/documentation/concepts/extending-sails/hooks), indexed by their _identity_. Use `sails.hooks` to access properties and methods of hooks you've installed to extend Sails—for example, by calling `sails.hooks.email.send()`. You can also use this dictionary to access the Sails [core hooks](https://sailsjs.com/documentation/concepts/extending-sails/hooks#?types-of-hooks), for advanced usage. By default, a hook's identity is the lowercased version of its folder name, with any `sails-hook-` prefix removed. For example, the default identity for a hook loaded from `node_modules/sails-hook-email` would be `email`, and the hook would be accessible via `sails.hooks.email`. An installed hook's identity can be changed via the [`installedHooks` config property](https://sailsjs.com/documentation/concepts/extending-sails/hooks/using-hooks#?changing-the-way-sails-loads-an-installable-hook). See the [hooks concept documentation](https://sailsjs.com/documentation/concepts/extending-sails/hooks) for more information about hooks. ##### `sails.io` The API exposed by the [`sails.sockets.*` methods](https://sailsjs.com/documentation/reference/web-sockets/sails-sockets) is flexible enough out of the box to cover the requirements of most applications, and using them will future-proof your app against possible changes in the underlying implementation. However, if you are working on bringing some legacy code from a vanilla Socket.io app into your Sails app, it can be useful to talk to Socket.io directly. To accomplish this, Sails provides raw access to the underlying [socket.io](http://socket.io/) server instance (`io`) as `sails.io`. See the [Socket.io docs](http://socket.io/docs/) for more information. If you decide to use Socket.io directly, please proceed with care. > Sails bundles `socket.io` as a dependency of [sails-hook-sockets](github.com/balderdashy/sails-hook-sockets), a core hook. ### Where does the application object come from? An application instance automatically created _the first time_ you `require('sails')`. This is what is happening in the generated `app.js` file: ```javascript var sails = require('sails'); ``` Note that any subsequent calls to `require('sails')` return the same app instance. (This is why you might sometimes hear the Sails app instance referred to as a "singleton".) ### Creating a new application object (advanced) If you are implementing something unconventional (e.g. writing tests for Sails core) where you need to create more than one Sails application instance in a process, you _should not_ use the instance returned by `require('sails')`, as this can cause unexpected behavior. Instead, you should obtain application instances by using the Sails constructor: ```javascript var Sails = require('sails').constructor; var sails0 = new Sails(); var sails1 = new Sails(); var sails2 = new Sails(); ``` Each app instance (`sails0`, `sails1`, `sails2`) can be loaded/lifted separately, using different configuration. For more on using Sails programatically, see the conceptual overview on [programmatic usage in Sails](https://sailsjs.com/documentation/concepts/programmatic-usage). <docmeta name="displayName" value="Advanced usage"> ================================================ FILE: docs/reference/application/advanced-usage/lifecycle.md ================================================ # The Sails app lifecycle The Sails core has been iterated upon several times to make it easier to maintain and extend. As a result, it has a very particular loading order, which its hooks depend on heavily. This process is summarized below. ### (1) Load configuration "overrides" Gather the set of configuration values passed in on the command line, in environment variables, and in programmatic configuration (i.e. options passed to [`sails.load`](https://sailsjs.com/documentation/reference/application/sails-load) or [`sails.lift`](https://sailsjs.com/documentation/reference/application/sails-lift)). When an app is started via the command-line interface (by typing `sails lift` or `sails console`), the values of any `.sailsrc` files will also be merged into the config overrides. These override values will take precedence over any user configuration encountered in the next step. ### (2) Load user configuration Unless the `userconfiguration` hook is explicitly disabled, Sails will next load the configuration files in the `config` folder (and subfolders) underneath the current working directory. See [**Concepts > Configuration**](https://sailsjs.com/documentation/concepts/configuration) for more details about user configuration. Configuration settings from step 1 will be merged on top of these values to form the `sails.config` object. ### (3) Load hooks Next, Sails will load the other hooks. [Core hooks](https://sailsjs.com/documentation/concepts/extending-sails/hooks#?types-of-hooks) will load first, followed by user hooks and installable hooks. Note that hooks typically include configuration of their own which will be used as _default values_ in `sails.config`. For example, if no `port` setting is configured by this point, the `http` hook's default value of 1337 will be used. ### (4) Assemble router Sails prepares the core Router, then emits multiple events on the `sails` object informing hooks that they can safely bind routes. ### (5) Expose global variables After all hooks have initialized, Sails exposes global variables (by default: `sails` object, models, services, `_`, and `async`). ### (6) Initialize app runtime > This step does not run when `sails.load()` is used programmatically. > To run the initialization step, use `sails.lift()` instead. + Run the bootstrap function (`sails.config.bootstrap`) + Start attached servers (by default, Express and Socket.io) ### FAQ + What is the difference between `sails.lift()` and `sails.load()`? + `lift()` === `load()` + `initialize()`. It does everything `load()` does, plus it starts any attached servers (e.g. HTTP) and logs a picture of a boat. <docmeta name="displayName" value="Lifecycle"> ================================================ FILE: docs/reference/application/advanced-usage/sails.LOOKS_LIKE_ASSET_RX.md ================================================ # sails.LOOKS_LIKE_ASSET_RX A regular expression designed for use in identifying URL paths that seem like they are _probably_ for a static asset of some kind (e.g. image, stylesheet, `favicon.ico`, `robots.txt`, etc.). ### Usage ```usage sails.LOOKS_LIKE_ASSET_RX; ``` **Type:** ((RegExp)) > This regex is **by no means foolproof**, and may match URLs too aggressively for some applications. It is just a reasonable approximation made available for convenience. ### Example To avoid disabling built-in session support for any request to a URL path that ends in `.json`, but still disable sessions for other requests for static assets, you might use the following configuration: ```javascript // In `config/session.js` isSessionDisabled: function (req){ if (req.path.match(/\.json$/)) { // Don't disable sessions. return; } var seemsToWantSomeOtherStaticAsset = !!req.path.match(sails.LOOKS_LIKE_ASSET_RX); if (seemsToWantSomeOtherStaticAsset) { // Disable sessions. return true; } // Otherwise, don't disable sessions. return; } ``` <docmeta name="displayName" value="sails.LOOKS_LIKE_ASSET_RX"> <docmeta name="pageType" value="constant"> ================================================ FILE: docs/reference/application/advanced-usage/sails.getActions.md ================================================ # sails.getActions() Return a dictionary of Sails [actions](https://sailsjs.com/documentation/concepts/actions-and-controllers). ```usage sails.getActions(); ``` The result is a flat (i.e. one-level) dictionary where the keys are the kebab-cased, dash-delimited action identities, and the values are the action functions. All actions in the dictionary will have been converted to `req, res` functions at this point, even if they were defined using [actions2 syntax](https://sailsjs.com/documentation/concepts/actions-and-controllers#?actions-2). <docmeta name="displayName" value="sails.getActions()"> <docmeta name="pageType" value="method"> ================================================ FILE: docs/reference/application/advanced-usage/sails.getBaseUrl.md ================================================ # sails.getBaseUrl() > ##### _**This method is deprecated and will likely be removed or changed in an upcoming release.**_ > There is no reliable, cross-platform way to automatically detect the external URL of a running Sails app (or any other Node app). Instead, configure your base URL explicitly and save it in [custom configuration](https://sailsjs.com/documentation/reference/configuration/sails-config-custom) (e.g. `sails.config.custom.baseUrl`) that you can reference throughout the app. (This can then be overridden in production, staging, etc. as needed using [environment-dependent configuration](https://sailsjs.com/documentation/concepts/configuration#?environmentspecific-files-config-env).) Return a (possibly incorrect) best guess of the base URL for this app, based on a combination of user-supplied and default configuration values. ```usage sails.getBaseUrl(); ``` `getBaseUrl()` constructs a URL string by inspecting various configuration values and defaults. For example, if `sails.config.ssl.key` and `sails.config.ssl.cert` both have values, the URL will start with `https://` instead of `http://`. If `sails.config.explicitHost` is not undefined, its value will be used as the domain name, otherwise it will be `localhost`. If `sails.config.port` is not 80 or 443, its value will be appended to the URL as well. ### Usage _This function does not accept any arguments._ #### Returns **Type:** ((string)) ```javascript http://localhost:1337 ``` ### Example In an email template... ```html For more information, visit <a href="<%=sails.getBaseUrl()%>">our web site</a>. ``` <docmeta name="displayName" value="sails.getBaseUrl()"> <docmeta name="pageType" value="method"> <docmeta name="isDeprecated" value="true"> ================================================ FILE: docs/reference/application/advanced-usage/sails.getRouteFor.md ================================================ # sails.getRouteFor() Look up the first route pointing at the specified target (e.g. `MeController.login`) and return a dictionary containing its method and URL. ```usage sails.getRouteFor(target); ``` ### Usage | | Argument | Type | Details |---|--------------------------- | ------------------- |:----------- | 1 | target | ((string)) | The route target string; e.g. `MeController.login` #### Returns **Type:** ((dictionary)) ```javascript { method: 'post', url: '/auth/login' } ``` ### Example In a controller action... ```javascript return res.view('pages/some-page-with-a-form-on-it', { formEndpoint: sails.getRouteFor('SomeotherController.someAction'), // ... }); ``` So that in the rendered view... ```ejs <form action="<%=formEndpoint.url%>" method="<%=formEndpoint.method%>"> <!-- ... --> </form> ``` ### Notes > - This function searches the Sails app's explicitly configured routes; [`sails.config.routes`](https://sailsjs.com/documentation/reference/configuration/sails-config-routes). Shadow routes bound by hooks (including [blueprint routes](https://sailsjs.com/documentation/reference/blueprint-api#?blueprint-routes)) will not be matched. > - If a matching target cannot be found, this function throws an `E_NOT_FOUND` error (i.e. if you catch the error and check its `code` property, it will be the string `E_NOT_FOUND`). > - If more than one route matches the specified target, the first match is returned. > - If you only need the URL for a route (e.g. to use as an `href` from within one of your views), you may want to use [`sails.getUrlFor()`](https://sailsjs.com/documentation/reference/application/sails-get-url-for) instead of this function. <docmeta name="displayName" value="sails.getRouteFor()"> <docmeta name="pageType" value="method"> ================================================ FILE: docs/reference/application/advanced-usage/sails.lift.md ================================================ # sails.lift() Lift a Sails app programmatically. > This does exactly what you might be used to seeing by now when you run `sails lift`. It [loads](https://sailsjs.com/documentation/reference/application/sails-load) the app, runs its bootstrap, then starts listening for HTTP requests and WebSocket connections. Useful for building top-to-bottom integration tests that rely on HTTP requests, and for building higher-level tooling on top of Sails. ```usage sailsApp.lift(configOverrides, function (err) { }); ``` _Or:_ + `sailsApp.lift(function (err) {...});` ### Usage | | Argument | Type | Details | |---|:--------------------|----------------------------------------------|:-----------------------------------| | 1 | _configOverrides_ | ((dictionary?)) | A dictionary of config that will override any conflicting options present in configuration files. If provided, this will be merged on top of [`sails.config`](https://sailsjs.com/documentation/reference/configuration). ##### Callback | | Argument | Type | Details | |---|:--------------------|---------------------|:---------------------------------------------------------------------------------| | 1 | _err_ | ((Error?)) | An error encountered while lifting, or `undefined` if there were no errors. ### Example ```javascript var Sails = require('sails').constructor; var sailsApp = new Sails(); sailsApp.lift({ log: { level: 'warn' } }, function (err) { if (err) { console.log('Error occurred lifting Sails app:', err); return; } // --• console.log('Sails app lifted successfully!'); }); ``` ### Notes > - The difference between [`.lift()`](https://sailsjs.com/documentation/reference/application/sails-lift) and [`.load()`](https://sailsjs.com/documentation/reference/application/sails-load) is that `.lift()` takes the additional steps of (1) running the app's [bootstrap](https://sailsjs.com/documentation/reference/configuration/sails-config-bootstrap) (if any), and (2) emitting the `ready` event. The core `http` hook will typically respond to the `ready` event by starting an HTTP server on the port configured via `sails.config.port` (1337 by default). > - When a Sails app is fully lifted, it also emits the [`lifted` event](https://sailsjs.com/documentation/concepts/extending-sails/hooks/events). > - With the exception of `NODE_ENV` and `PORT`, [configuration set via environment variables](https://sailsjs.com/documentation/concepts/configuration#?setting-sailsconfig-values-directly-using-environment-variables) will not automatically apply to apps started using `.lift()`, nor will options set in [`.sailsrc` files](https://sailsjs.com/documentation/concepts/configuration/using-sailsrc-files). If you wish to use those configuration values, you can retrieve them via `require('sails/accessible/rc')('sails')` and pass them in as the first argument to `.lift()`. <docmeta name="displayName" value="sails.lift()"> <docmeta name="pageType" value="method"> ================================================ FILE: docs/reference/application/advanced-usage/sails.load.md ================================================ # sails.load() Load a Sails app into memory, but without lifting an HTTP server. _Useful for writing tests, command-line scripts, and scheduled jobs._ ```usage sailsApp.load(configOverrides, function (err) { }); ``` _Or:_ + `sailsApp.load(function (err) {...});` #### Usage | | Argument | Type | Details | |---|:--------------------|----------------------------------------------|:-----------------------------------| | 1 | _configOverrides_| ((dictionary?)) | A dictionary of config that will override any conflicting options present in configuration files. If provided, this will be merged on top of [`sails.config`](https://sailsjs.com/documentation/reference/configuration). ##### Callback | | Argument | Type | Details | |---|:--------------------|---------------------|:---------------------------------------------------------------------------------| | 1 | _err_ | ((Error?)) | An error encountered while loading, or `undefined` if there were no errors. ### Example ```javascript var Sails = require('sails').constructor; var sailsApp = new Sails(); sailsApp.load({ log: { level: 'error' } }, function (err) { if (err) { console.log('Error occurred loading Sails app:', err); return; } // --• console.log('Sails app loaded successfully!'); }); ``` ### Notes > - This takes care of loading configuration files, initializing hooks (including the ORM), and binding routes. It **does not** run the bootstrap, and it **does not** start listening for HTTP requests and WebSocket connections. > - More specifically, the difference between [`.lift()`](https://sailsjs.com/documentation/reference/application/sails-lift) and [`.load()`](https://sailsjs.com/documentation/reference/application/sails-load) is that `.lift()` takes the additional steps of (1) running the app's [bootstrap](https://sailsjs.com/documentation/reference/configuration/sails-config-bootstrap) (if any), and (2) emitting the `ready` event. The core `http` hook will typically respond to the `ready` event by starting an HTTP server on the port configured via `sails.config.port` (1337 by default). > - Even though a "loaded-but-not-lifted" Sails app does not listen for requests on an HTTP port, you can make "virtual" requests to it using [`sails.request`](https://sailsjs.com/documentation/reference/application/sails-request) > - For an example of this in practice, see [machine-as-script](https://github.com/treelinehq/machine-as-script/blob/ec8972137489afd24562bdf0b6a10ada11e540cc/index.js#L778-L791). > - With the exception of `NODE_ENV` and `PORT`, [configuration set via environment variables](https://sailsjs.com/documentation/concepts/configuration#?setting-sailsconfig-values-directly-using-environment-variables) will not automatically apply to apps started using `.load()`, nor will options set in [`.sailsrc` files](https://sailsjs.com/documentation/concepts/configuration/using-sailsrc-files). If you wish to use those configuration values, you can retrieve them via `require('sails/accessible/rc')('sails')` and pass them in as the first argument to `.load()`. <docmeta name="displayName" value="sails.load()"> <docmeta name="pageType" value="method"> ================================================ FILE: docs/reference/application/advanced-usage/sails.lower.md ================================================ # sails.lower() Shut down a lifted Sails app and have it cease listening for or responding to any future requests. ```usage sails.lower(callback); ``` ### Usage | | Argument | Type | Details |---| --------------------------- | ------------------- | ----------- | 1 | _`callback`_ | ((function?)) | Optional. A function to call when lowering is complete (or if an error occurs) ##### Callback | | Argument | Type | Details | |---|-----------|:------------:|---------| | 1 | _`err`_ | ((Error?)) | An error instance will be sent as the first argument of the callback if any fatal errors occurred while lowering. ### Example ```javascript sailsApp.lower( function (err) { if (err) { return console.log("Error occurred lowering Sails app: ", err); } console.log("Sails app lowered successfully!"); } ) ``` ### Notes > + The app will emit the `lower` event before shutting down the HTTP and WebSocket services. > + Lowered apps cannot be lifted again. <docmeta name="displayName" value="sails.lower()"> <docmeta name="pageType" value="method"> ================================================ FILE: docs/reference/application/advanced-usage/sails.registerAction.md ================================================ # sails.registerAction() Register a new Sails [action](https://sailsjs.com/documentation/concepts/actions-and-controllers) that can then be bound to a route. ```usage sails.registerAction(action, name); ``` While actions are mainly registered automatically when the files in an app’s `api/controllers` folder are loaded, you can use the `registerAction()` method to add a new action programmatically. This is especially useful in custom [hooks](https://sailsjs.com/documentation/concepts/extending-sails/hooks), in situations where you want to provide a new action but let the app developer determine the route to bind the action to, or when you want to ensure that policies and other [action middleware](https://sailsjs.com/documentation/reference/application/sails-register-action-middleware) apply to your action. ### Usage |   | Argument | Type | Details |---|--------------------------- | ------------------- |:----------- | 1 | action | ((function)) or ((dictionary)) | Either a [classic action](https://sailsjs.com/documentation/concepts/actions-and-controllers#?classic-actions) (aka `(req, res)`) function or an [actions2](https://sailsjs.com/documentation/concepts/actions-and-controllers#?actions-2) definition. | 2 | identity | ((string)) | The identifier for the action. This is the string that will be used to reference the action elsewhere in an app, for instance when [binding the action to a route](http://sailsjs.com/documentation/concepts/routes/custom-routes#?standalone-action-target-syntax). <docmeta name="displayName" value="sails.registerAction()"> <docmeta name="pageType" value="method"> ================================================ FILE: docs/reference/application/advanced-usage/sails.registerActionMiddleware.md ================================================ # sails.registerActionMiddleware() > ##### _**This feature is still experimental.**_ > This method is still under development, and its interface and/or behavior could change at any time. Register a new action middleware function that will be applied to actions with the specified identities. ```usage sails.registerActionMiddleware(actionMiddlewareFns, actionIdentities); ``` Action middleware functions are essentially [policies](https://sailsjs.com/documentation/concepts/policies#?writing-your-first-policy) that you declare programmatically (rather than via [sails.config.policies](https://sailsjs.com/documentation/reference/configuration/sails-config-policies)). In fact, policies are implemented under-the-hood using action middleware. The `registerActionMiddleware()` method is mainly useful in [custom hooks](https://sailsjs.com/documentation/concepts/extending-sails/hooks) as a way of adding new policies to an app. ### Usage |   | Argument | Type | Details |---|--------------------------- | ------------------- |:----------- | 1 | actionMiddlewareFns | ((function)) or ((array)) | One or more middleware functions to register. Action middleware (like policies) must be functions which accept `req`, `res` and `next` arguments. | 2 | actionIdentities | ((string)) | An expression that indicates the action or actions that the action middleware should apply to. Use `*` at the end for a wildcard; e.g. `user/*` will apply to any actions whose identities begin with `user/`. Use a ! at the beginning to indicate that the action middleware should NOT apply to the actions specified by the expression, e.g. `!user/foo` or `!user/*`. Multiple identity expressions can be specified by separating with a comma, e.g. `pets/count,user/*,!user/tickle` > The `actionIdentities` argument expects the identities to be expressed as if they were [standalone actions](https://sailsjs.com/documentation/concepts/actions-and-controllers#?standalone-actions). To apply action middleware to actions inside of a controller file (e.g. `UserController.js`), simply refer to the lower-cased version of the filename _without "Controller"_ (e.g. `user`). ### Example As an example of action middleware that might be applied in a custom hook, imagine a page view counter (this code might be added to the `initialize` method of the hook): ```javascript // Declare a local var to hold the number of views for each URL. var pageViews = {}; // Register middleware to record each page view. sails.registerActionMiddleware( // First argument is the middleware to run function countPage (req, res, next) { // Initialize the page counter to zero if this is the first time we've seen this URL. pageViews[req.url] = pageViews[req.url] || 0; // Increment the page counter. pageViews[req.url]++; // Add the current page count to the request, so that it can be used in other middleware / actions. req.currentPageCount = pageViews[req.url]; // Continue to the next matching middleware / action next(); }, // Second argument is the actions to apply the middleware to. In this case, we want the // hook to apply to all actions EXCEPT the `show-page-views` action supplied by this hook. '*, !page-view-hook/show-page-views' ); ``` <docmeta name="displayName" value="sails.registerActionMiddleware()"> <docmeta name="pageType" value="method"> <docmeta name="isExperimental" value="true"> ================================================ FILE: docs/reference/application/advanced-usage/sails.reloadActions.md ================================================ # sails.reloadActions() > ##### _**This feature is still experimental.**_ > This method is still under development, and its interface and/or behavior could change at any time. Flush and reload all Sails [actions](https://sailsjs.com/documentation/concepts/actions-and-controllers) ```usage sails.reloadActions(cb); ``` _Or:_ + `sails.reloadActions(options, cb)` This method causes hooks to run their `registerActions()` methods if they have them. After the hooks are finished reloading / re-registering their actions, actions in the `api/controllers` folder (including those stored in [controller files](https://sailsjs.com/documentation/concepts/actions-and-controllers#?controllers)) are reloaded and merged on top of those loaded via hooks. This method is useful primarily in development scenarios. ### Usage |   | Argument | Type | Details |---|--------------------------- | ------------------- |:----------- | 1 | _options_ | ((dictionary?)) | Currently accepts one key, `hooksToSkip`, which if given should be an array of names of hooks that should _not_ call their `reloadActions` method. | 2 | _callback_ | ((function)) | A callback to be called with the virtual response. ### Notes > - Never dynamically replace your Sails.js controller or action files on disk with untrusted code at runtime, regardless of whether you are using `.reloadActions()` in your app or not. Since `reloadActions()` runs the code in your Sails.js app's files, if the files are not safe to run, then using `reloadActions()` would be [a security risk](https://github.com/balderdashy/sails/issues/7209). This risk is only present if your Sails app is deliberately overwriting its own files to replace them with unsafe code. <docmeta name="displayName" value="sails.reloadActions()"> <docmeta name="pageType" value="method"> <docmeta name="isExperimental" value="true"> ================================================ FILE: docs/reference/application/advanced-usage/sails.renderView.md ================================================ # sails.renderView() > ##### _**This feature is still experimental.**_ > This method is still under development, and its interface and/or behavior could change at any time. Compile a view into an HTML template. ```usage sails.renderView(pathToView, templateData); ``` ### Usage |   | Argument | Type | Details |---|--------------------------- | ------------------- |:----------- | 1 | pathToView | ((string)) | The path to the view that will be compiled into HTML. | 2 | _templateData_ | ((dictionary?)) | The dynamic data to pass into the view. ### Example To compile an HTML template with a customized greeting for the recipient: ```javascript var htmlEmailContents = await sails.renderView('emails/signup-welcome', { fullName: inputs.fullName, // Don't include the Sails app's default layout in the rendered template. layout: false }); ``` <docmeta name="displayName" value="sails.renderView()"> <docmeta name="pageType" value="method"> <docmeta name="isExperimental" value="true"> ================================================ FILE: docs/reference/application/advanced-usage/sails.request.md ================================================ # sails.request() > ##### _**This feature is still experimental.**_ > This method is still under development, and its interface and/or behavior could change at any time. Make a virtual request to a running Sails instance. ```usage sails.request(request); ``` _Or:_ + `sails.request(url, body)` + `sails.request(url, callback)` + `sails.request(url, body, callback)` This method can be used on instances that have been started with [`sails.load()`](https://sailsjs.com/documentation/reference/application/sails-load) and that are not actively listening for HTTP requests on a server port. This makes `sails.request()` useful for testing scenarios where running [`sails.lift()`](https://sailsjs.com/documentation/reference/application/sails-lift) is not necessary. However, it should be noted that the data may not be processed in exactly the same way as an HTTP request; in particular, a much simpler body parser will be employed, and Express middleware such as the static asset server will not be used. ### Usage | | Argument | Type | Details |---|--------------------------- | ------------------- |:-----------: | 1 | request (or url) | ((string)) -or- ((dictionary)) | The virtual request to make. If specified as a string, this should be an address containing an optional method and a path, e.g. `/foo` or `PUT /user/friend`. If specified as an object, it should have one or more of the properties described in the "request argument" section below. | 2 | _body_ | ((json?)) | (optional) A JSON-serializable value to use as the request body. This argument will override the `data` property of the `request` argument, if provided. | 3 | _callback_ | ((function?)) | (optional) A callback to be called with the virtual response. #### Request object If the `request` argument is specified as an object, it can have the following properties: | Property | Type | Example | Details |--------------------------- | ------------------- | ------- | :-----------: | url | ((string)) | `"/foo"`, `"PUT /user/friend"` | (required) The route in the Sails app to make a request to, with an optional HTTP method prefix | method | ((string)) | `"GET"`, `"POST"` | (optional) The HTTP method to use in the request. This will override any method supplied as part of the `url` property. | headers | ((dictionary)) | `{'content-type': 'application/json'}` | (optional) Dictionary of headers to use in the virtual request. | data | ((json)) | `{foo:'bar'}`, `12345` | ((optional)) Data to send along with the request. For `GET`, `HEAD` and `DELETE` requests, the data will be serialized into a querystring and added to the URL. Otherwise, it will be sent as-is as the request body. #### Callback | | Argument | Type | Details |---|--------------------------- | ------------------- |:----------- | 1 | _err_ | ((Error?)) | If the response was unsuccessful (status code was not in the 200-399 range) this will be an object containing `status` and `body` properties. If the response was successful, this will be `null`. | 2 | response | ((dictionary)) | If the response was successful, this will be an object containing the full server response. | 3 | body | ((json)) | If the response was successful, this will be the value of `response.body`. #### Returns **Type:** ((stream)) The full virtual request stream object. This is a readable stream. <docmeta name="displayName" value="sails.request()"> <docmeta name="pageType" value="method"> <docmeta name="isExperimental" value="true"> ================================================ FILE: docs/reference/application/application.md ================================================ # Application (`sails`) The Sails application object contains all relevant runtime state for a Sails application. By default, it is exposed globally as `sails` and accessible almost anywhere in your code. > Most users of the framework will only need to know about the `sails` application object in order to access a few basic methods and their custom configuration. Less commonly used methods can be found in the [advanced usage](https://sailsjs.com/documentation/reference/application/advanced-usage) section. ### Properties The application object has a number of useful methods and properties. The officially supported methods on the `sails` object are covered by the other pages in this section. Here are a few of its most useful properties: ##### sails.models A dictionary of all loaded [Sails models](https://sailsjs.com/documentation/concepts/models-and-orm/models), indexed by their _identity_. By default, a model's identity is the lowercased version of its filename, without the **.js** extension. For example, the default identity for a model loaded from `api/models/PowerPuff.js` would be `powerpuff`, and the model would be accessible via `sails.models.powerpuff`. A model's identity can be customized by setting an `identity` property in its module file. ##### sails.helpers A dictionary of all accessible [helpers](https://sailsjs.com/documentation/concepts/helpers), including organics. ##### sails.config The full set of configuration options for the Sails instance, loaded from a combination of environment variables, `.sailsrc` files, user-configuration files, and defaults. See the [configuration concepts section](https://sailsjs.com/documentation/concepts/configuration) for a full overview of configuring Sails, and the [configuration reference](https://sailsjs.com/documentation/reference/configuration) for details on individual options. ##### sails.sockets A set of convenience methods for low-level interaction with connected websockets. See the [`sails.sockets.*` reference section](https://sailsjs.com/documentation/reference/web-sockets/sails-sockets) for details. ### Advanced usage For more options and implementation details (including instructions for programmatic usage) see [Advanced usage](https://sailsjs.com/documentation/reference/application/advanced-usage). <docmeta name="displayName" value="Application"> ================================================ FILE: docs/reference/application/sails.config.custom.md ================================================ # sails.config.custom The runtime values of your app's [custom configuration settings](https://sailsjs.com/documentation/reference/configuration/sails-config-custom). ### Usage ```usage sails.config.custom; ``` ### Example In an action or helper: ```javascript sails.config.custom.mailgunApiKey; // -> "key-testkeyb183848139913858e8abd9a3" ``` ### Notes > + For information on how to set custom configuration in the first place, see [Reference > Config > sails.config.custom](https://sailsjs.com/documentation/reference/configuration/sails-config-custom). <docmeta name="displayName" value="sails.config.custom"> <docmeta name="pageType" value="property"> ================================================ FILE: docs/reference/application/sails.getDatastore.md ================================================ # sails.getDatastore() Access a particular [datastore](https://sailsjs.com/documentation/concepts/models-and-orm#?datastores), or the default datastore. ```usage sails.getDatastore(datastoreName); ``` ### Usage | | Argument | Type | Details |---|---------------------------- | ------------------- |:----------- | 1 | datastoreName | ((string?)) | If specified, this is the name of the datastore to look up. Otherwise, if you leave this blank, this `getDatastore()` will return the default datastore for your app. #### Returns **Type:** ((Dictionary)) A [datastore instance](https://sailsjs.com/documentation/reference/waterline-orm/datastores). <docmeta name="displayName" value="sails.getDatastore()"> <docmeta name="pageType" value="method"> ================================================ FILE: docs/reference/application/sails.getUrlFor.md ================================================ # sails.getUrlFor() Look up the first route pointing at the specified target (e.g. `entrance/view-login`) and return its URL. ```usage sails.getUrlFor(target); ``` ### Usage | | Argument | Type | Details |---|---------------------------- | ------------------- |:----------- | 1 | target | ((string)) | The route target string; e.g. `entrance/view-login` or `PageController.login` ##### Returns **Type:** ((string)) ```javascript '/login' ``` ### Example In a view... ```ejs <a href="<%= sails.getUrlFor('entrance/view-login') %>">Login</a> <a href="<%= sails.getUrlFor('entrance/view-signup') %>">Signup</a> ``` Or, if you're using traditional controllers: ```ejs <a href="<%= sails.getUrlFor('PageController.login') %>">Login</a> <a href="<%= sails.getUrlFor('PageController.signup') %>">Signup</a> ``` ### Notes > - This function searches the Sails app's explicitly configured routes, [`sails.config.routes`](https://sailsjs.com/documentation/reference/configuration/sails-config-routes). Shadow routes bound by hooks (including [blueprint routes](https://sailsjs.com/documentation/reference/blueprint-api#?blueprint-routes)) will not be matched. > - If a matching target cannot be found, this function throws an `E_NOT_FOUND` error (i.e. if you catch the error and check its `code` property, it will be the string `E_NOT_FOUND`). > - If more than one route matches the specified target, the first match is returned. > - The HTTP method (or "verb") from the route address is ignored, if relevant. <docmeta name="displayName" value="sails.getUrlFor()"> <docmeta name="pageType" value="method"> ================================================ FILE: docs/reference/application/sails.log.md ================================================ # sails.log() Log a message or some data at the "debug" [log level](https://sailsjs.com/documentation/reference/configuration/sails-config-log) using Sails' [built-in logger](https://sailsjs.com/documentation/concepts/logging). ```usage sails.log(...); ``` ### Usage This function's usage is purposely very similar to Node's [`console.log()`](https://nodejs.org/api/console.html#console_console_log_data), but with a handful of extra features—namely support for multiple log levels with colorized, prefixed console output. Note that standard `console.log()` conventions from Node.js apply: - takes an [unlimited number](https://en.wikipedia.org/wiki/Variadic_function) of arguments, separated by commas - printf-style parameterization (à la [`util.format()`](https://nodejs.org/api/util.html#util_util_format_format)) - objects, dates, arrays, and most other data types are pretty-printed using the built-in logic in [`util.inspect()`](https://nodejs.org/api/util.html#util_util_inspect_object_options) (e.g. you see `{ pet: { name: 'Hamlet' } }` instead of `[object Object]`.) - if you log an object with a custom `inspect()` method, that method will run automatically, and the string that it returns will be written to the console. ### Example ```javascript var sum = +req.param('x') + +req.param('y'); sails.log(); sails.log('Hey %s, did you know that the sum of %d and %d is %d?', req.param('name'), +req.param('x'), +req.param('y'), sum); sails.log('Bet you didn\'t know robots could do math, huh?'); sails.log(); sails.log('Anyways, here is a dictionary containing all the parameters I received in this request:', req.allParams()); sails.log('Until next time!'); return res.ok(); ``` ### Notes > - For a deeper conceptual exploration of logging in Sails, see [concepts/logging](https://sailsjs.com/documentation/concepts/logging). > - Remember that, in addition to being exposed as an alternative to calling `console.log` directly, the built-in logger in Sails is called internally by the framework. The Sails logger can be configured, or completely overridden, using built-in log configuration settings ([`sails.config.log`](https://sailsjs.com/documentation/reference/configuration/sails-config-log)). > - Keep in mind that, like any part of Sails, `sails.log` is completely optional. Most—but not all—Sails apps take advantage of the built-in logger: some users prefer to stick with `console.log()`, while others `require()` more feature-rich libraries like [Winston](https://www.npmjs.com/package/winston). If you aren't sure what your app needs yet, start with the built-in logger and go from there. <docmeta name="displayName" value="sails.log()"> <docmeta name="pageType" value="method"> ================================================ FILE: docs/reference/blueprint-api/Add.md ================================================ # Add (blueprint) Add a foreign record (e.g. a comment) to one of this record's collections (e.g. "comments"). ```usage PUT /:model/:id/:association/:fk ``` This action adds a reference to some other record (the "foreign", or "child" record) onto a particular collection of this record (the "primary", or "parent" record). + If the specified `:id` does not correspond with a primary record that exists in the database, this responds using `res.notFound()`. + If the specified `:fk` does not correspond with a foreign record that exists in the database, this responds using `res.notFound()`. + If the primary record is already associated with this foreign record, this action will not modify any records. (Note that currently, in the case of a many-to-many association, it _will_ add duplicate junction records! To resolve this, add a multi-column index at the database layer, if possible. We are currently working on a friendlier solution/default for users of MongoDB, sails-disk, and other NoSQL databases.) + Note that if the association is "2-way" (meaning it has `via`), then the foreign key or collection it points to with that `via` will also be updated on the foreign record. ### Parameters Parameter | Type | Details :-----------------------------------| --------------------------------------- |:--------------------------------- model | ((string)) | The [identity](https://sailsjs.com/documentation/concepts/models-and-orm/model-settings#?identity) of the containing model for the parent record.<br/><br/>e.g. `'employee'` (in `/employee/7/involvedinPurchases/47`) id | ((string)) | The desired parent record's primary key value.<br/><br/>e.g. `'7'` (in `/employee/7/involvedInPurchases/47`) association | ((string)) | The name of the collection attribute.<br/><br/>e.g. `'involvedInPurchases'` fk | ((string)) | The primary key value (usually id) of the child record to add to this collection.<br/><br/>e.g. `'47'` ### Example Add purchase #47 to the list of purchases that Dolly (employee #7) has been involved in: ``` PUT /employee/7/involvedInPurchases/47 ``` [![Run in Postman](https://s3.amazonaws.com/postman-static/run-button.png)](https://www.getpostman.com/run-collection/96217d0d747e536e49a4) ##### Expected response This returns "Dolly", the parent record. Notice she is now involved in purchase #47: ```json { "id": 7, "name": "Dolly", "createdAt": 1485462079725, "updatedAt": 1485476060873, "involvedInPurchases": [ { "amount": 10000, "createdAt": 1485476060873, "updatedAt": 1485476060873, "id": 47, "cashier": 7 } ] } ``` ##### Using jQuery ```javascript $.put('/employee/7/involvedInPurchases/47', function (purchases) { console.log(purchases); }); ``` ##### Using Angular ```javascript $http.put('/employee/7/involvedInPurchases/47') .then(function (purchases) { console.log(purchases); }); ``` ##### Using sails.io.js ```javascript io.socket.put('/employee/7/involvedInPurchases/47', function (purchases) { console.log(purchases); }); ``` ##### Using [cURL](http://en.wikipedia.org/wiki/CURL) ```bash curl http://localhost:1337/employee/7/involvedInPurchases/47 -X "PUT" ``` ### Socket notifications If you have WebSockets enabled for your app, then every client [subscribed](https://sailsjs.com/documentation/reference/web-sockets/resourceful-pub-sub) to the primary record will receive a notification in which the notification event name is the primary model identity (e.g. `'employee'`), and the message has the following format: ```usage id: <the parent record primary key value>, verb: 'addedTo', attribute: <the parent record collection attribute name>, addedIds: <the now-added child records' primary key values> ``` For instance, continuing the example above, all clients subscribed to Dolly, aka employee #7, (_except_ for the client making the request) would receive the following message: ```javascript { id: 7, verb: 'addedTo', attribute: 'involvedInPurchases', addedIds: [ 47 ] } ``` **Clients subscribed to the child record receive an additional notification:** Assuming `involvedInPurchases` had a `via`, then either `updated` or `addedTo` notifications would also be sent to any clients who were [subscribed](https://sailsjs.com/documentation/reference/web-sockets/resourceful-pub-sub) to purchase #47, the child record we just added. > If the `via`-linked attribute on the other side is [also plural](https://sailsjs.com/documentation/concepts/models-and-orm/associations/many-to-many) (e.g. `cashiers`), then another `addedTo` notification will be sent. Otherwise, if the `via` [points at a singular attribute](https://sailsjs.com/documentation/concepts/models-and-orm/associations/one-to-many) (e.g. `cashier`) then the [`updated` notification](https://sailsjs.com/documentation/reference/blueprint-api/update#?socket-notifications) will be sent. **Finally, a third notification might be sent:** If adding this purchase to Dolly's collection would "steal" it from another employee's `involvedInPurchases`, then any clients subscribed to that other, stolen-from employee record (e.g. Motoki, employee #12) would receive a `removedFrom` notification (see [**Blueprints > remove from**](https://sailsjs.com/documentation/reference/blueprint-api/remove-from#?socket-notifications). ### Notes > + If you'd like to spend some more time with Dolly, a more detailed walkthrough related to the example above is available [here](https://gist.github.com/mikermcneil/e5a20b03be5aa4e0459b). > + This action is for dealing with _plural_ ("collection") attributes. If you want to set or unset a _singular_ ("model") attribute, just use [update](https://sailsjs.com/documentation/reference/blueprint-api/update) and set the foreign key to the id of the new foreign record (or `null` to clear the association). > If you want to completely _replace_ the set of records in the collection with another set, use the [replace](https://sailsjs.com/documentation/reference/blueprint-api/replace) blueprint. > + The example above assumes "rest" blueprints are enabled, and that your project contains at least an 'Employee' model with attribute: `involvedInPurchases: {collection: 'Purchase', via: 'cashier'}` as well as a `Purchase` model with attribute: `cashier: {model: 'Employee'}`. You can quickly achieve this by running: > > ```shell > $ sails new foo > $ cd foo > $ sails generate model purchase > $ sails generate model employee > ``` > > ...then editing `api/models/Purchase.js` and `api/models/Employee.js`. <docmeta name="displayName" value="add to"> <docmeta name="pageType" value="endpoint"> ================================================ FILE: docs/reference/blueprint-api/Create.md ================================================ # Create (blueprint) Create a new record in your database. ```usage POST /:model ``` Responds with a JSON dictionary representing the newly created instance. If a validation error occurred, a JSON response with the invalid attributes and a `400` status code will be returned instead. Additionally, if the [`autoWatch` setting](https://sailsjs.com/documentation/reference/configuration/sails-config-blueprints?properties) is on (which it is by default), then a "created" notification will be published to all client sockets which are _watching_ this model; that is, client sockets who have previously sent a request to the "Find" blueprint action. Those same sockets will also be subscribed to hear about subsequent changes to the new record. Finally, if this blueprint action is triggered via a socket request, then the requesting socket will ALSO be subscribed to the newly created record. In other words, if the record is subsequently updated or deleted using blueprints, a message will be sent to that client socket informing them of the change. See [`.subscribe()`](https://sailsjs.com/documentation/reference/web-sockets/resourceful-pub-sub/subscribe) for more info. ### Parameters Parameters should be sent in the [request body](https://www.getpostman.com/docs/requests#body). By default, Sails understands the most common types of encodings for body parameters, including url-encoding, form-encoding, and JSON. Parameter | Type | Details -------------- | --------------------------------------------------------- |:--------------------------------- model | ((string)) | The [identity](https://sailsjs.com/documentation/concepts/models-and-orm/model-settings#?identity) of the model in which the new record should be created.<br/><br/>e.g. `'purchase'` (in `POST /purchase`) _*_ | ((json?)) | Send [body parameters](https://www.getpostman.com/docs/requests#body) with the same names as the attribute defined on your model to set those values on your new record. <br/> <br/>These values are handled the same way as if they were passed into the model's <a href="https://sailsjs.com/documentation/reference/waterline-orm/models/create">.create()</a> method. ### Example Create a new user named "Applejack" with a hobby of "pickin", who is involved in purchases #13 and #25: `POST /pony` ```json { "name": "Applejack", "hobby": "pickin", "involvedInPurchases": [13,25] } ``` [![Run in Postman](https://s3.amazonaws.com/postman-static/run-button.png)](https://www.getpostman.com/run-collection/96217d0d747e536e49a4) ##### Example response ```json { "id": 47, "name": "Applejack", "hobby": "pickin", "createdAt": 1485550575626, "updatedAt": 1485550603847, "involvedInPurchases": [ { "id": 13, "amount": 10000, "createdAt": 1485550525451, "updatedAt": 1485550544901 }, { "id": 25, "amount": 4.50, "createdAt": 1485550561340, "updatedAt": 1485550561340 } ] } ``` ### Socket notifications If you have WebSockets enabled for your app, then every socket client who is "watching" this model (has sent a request to the model's ["find where" blueprint action](https://sailsjs.com/documentation/reference/blueprint-api/find-where)) will receive a "created" notification where the event name is the model identity (e.g. `user`), and the message has the following format: ``` verb: 'created', data: <a dictionary of the attribute values of the new record (without associations)> id: <the new record primary key>, ``` For instance, continuing the example above, all clients who are watching the `User` model (_except_ for the client making the request) would receive the following message: ```js id: 47, verb: 'created', data: { id: 47, name: 'Applejack', hobby: 'pickin', createdAt: 1485550575626, updatedAt: 1485550603847 } ``` **Clients subscribed to newly-associated child records will receive a notification, too:** Since the new record in our example included an initial value for `involvedInPurchases`, an association pointed at by `via` on the other side, then `addedTo` notifications would also be sent to any clients who are [subscribed](https://sailsjs.com/documentation/reference/web-sockets/resourceful-pub-sub) to those now-associated child records on the other side of the relationship—in this case, purchases 13 and 25. See [**Blueprints > add to**](https://sailsjs.com/documentation/reference/blueprint-api/add-to) for more info about the structure of those notifications. <docmeta name="displayName" value="create"> <docmeta name="pageType" value="endpoint"> ================================================ FILE: docs/reference/blueprint-api/Destroy.md ================================================ # Destroy (blueprint) Delete the record specified by `id` from the database forever and notify subscribed sockets. ```usage DELETE /:model/:id ``` This destroys the record that matches the **id** parameter and responds with a JSON dictionary representing the destroyed instance. If no model instance exists matching the specified **id**, a `404` is returned. Additionally, a `destroy` event will be published to all sockets subscribed to the record room, and all sockets currently subscribed to the record will be unsubscribed from it. ### Parameters Parameter | Type | Details ---------------------------------- | --------------------------------------- |:--------------------------------- model | ((string)) | The [identity](https://sailsjs.com/documentation/concepts/models-and-orm/model-settings#?identity) of the containing model.<br/><br/>e.g. `'purchase'` (in `/purchase/7`) id<br/>*(required)* | ((string)) | The primary key value of the record to destroy, specified in the path. <br/>e.g. `'7'` (in `/purchase/7`) . ### Example Delete Pinkie Pie: `DELETE /user/4` [![Run in Postman](https://s3.amazonaws.com/postman-static/run-button.png)](https://www.getpostman.com/run-collection/96217d0d747e536e49a4) ##### Expected response ```json { "name": "Pinkie Pie", "hobby": "kickin", "id": 4, "createdAt": 1485550644076, "updatedAt": 1485550644076 } ``` ### Socket notifications If you have WebSockets enabled for your app, then every client [subscribed](https://sailsjs.com/documentation/reference/web-sockets/resourceful-pub-sub) to the destroyed record will receive a notification where the event name is that of the model identity (e.g. `user`), and the “message” has the following format: ``` verb: 'destroyed', id: <the record primary key>, previous: <a dictionary of the attribute values of the destroyed record (including populated associations)> ``` For instance, continuing the example above, all clients subscribed to `User` #4 (_except_ for the client making the request) might receive the following message: ```js id: 4, verb: 'destroyed', previous: { name: 'Pinkie Pie', hobby: 'kickin', createdAt: 1485550644076, updatedAt: 1485550644076 } ``` **If the destroyed record had any links to other records, there might be some additional notifications:** Assuming the record being destroyed in our example had an association with a `via`, then either `updated` or `removedFrom` notifications would also be sent to any clients who are [subscribed](https://sailsjs.com/documentation/reference/web-sockets/resourceful-pub-sub) to those child records on the other side of the relationship. See [**Blueprints > remove from**](https://sailsjs.com/documentation/reference/blueprint-api/remove-from) and [**Blueprints > update**](https://sailsjs.com/documentation/reference/blueprint-api/update) for more info about the structure of those notifications. > If the association pointed at by the `via` is plural (e.g. `cashiers`), then the `removedFrom` notification will be sent. Otherwise, if the `via` points at a singular association (e.g. `cashier`) then the `updated` notification will be sent. <docmeta name="displayName" value="destroy"> <docmeta name="pageType" value="endpoint"> ================================================ FILE: docs/reference/blueprint-api/Find.md ================================================ # Find (blueprint) Find a list of records that match the specified criteria and (if possible) subscribe to each of them. ```usage GET /:model ``` Results may be filtered, paginated, and sorted based on the blueprint configuration and/or parameters sent in the request. If the action was triggered via a socket request, the requesting socket will be "subscribed" to all records returned. If any of the returned records are subsequently updated or deleted, a message will be sent to that socket's client informing them of the change. See the [docs for Model.subscribe()](https://sailsjs.com/documentation/reference/web-sockets/resourceful-pub-sub/subscribe) for details. ### Parameters Parameter | Type | Details -------------- | ------------ |:--------------------------------- model | ((string)) | The [identity](https://sailsjs.com/documentation/concepts/models-and-orm/model-settings#?identity) of the containing model.<br/><br/>e.g. `'purchase'` (in `GET /purchase`) _*_ | ((string?)) | To filter results based on a particular attribute, specify a query parameter with the same name as the attribute defined on your model. <br/> <br/> For instance, if our `Purchase` model has an **amount** attribute, we could send `GET /purchase?amount=99.99` to return a list of $99.99 purchases. _where_ | ((string?)) | Instead of filtering based on a specific attribute, you may instead choose to provide a `where` parameter with the WHERE piece of a [Waterline criteria](https://sailsjs.com/documentation/concepts/models-and-orm/query-language), _encoded as a JSON string_. This allows you to take advantage of `contains`, `startsWith`, and other sub-attribute criteria modifiers for more powerful `find()` queries. <br/> <br/> e.g. `?where={"name":{"contains":"theodore"}}` _limit_ | ((number?)) | The maximum number of records to send back (useful for pagination). Defaults to 30. <br/> <br/> e.g. `?limit=100` _skip_ | ((number?)) | The number of records to skip (useful for pagination). <br/> <br/> e.g. `?skip=30` _sort_ | ((string?)) | The sort order. By default, returned records are sorted by primary key value in ascending order. <br/> <br/> e.g. `?sort=lastName%20ASC` _select_ | ((string?)) | The attributes to include each record in the result, specified as a comma-delimited list. By default, all attributes are selected. Not valid for plural (“collection”) association attributes.<br/> <br/> e.g. `?select=name,age`. _omit_ | ((string?)) | The attributes to exclude from each record in the result, specified as a comma-delimited list. Cannot be used in conjuction with `select`. Not valid for plural (“collection”) association attributes.<br/> <br/> e.g. `?omit=favoriteColor,address`. _populate_ | ((string)) | If specified, overide the default automatic population process. Accepts a comma-separated list of attribute names for which to populate record values, or specify `false` to have no attributes populated. See [here](https://sailsjs.com/documentation/concepts/models-and-orm/records#?populated-values) for more information on how the population process fills out attributes in the returned list of records according to the model's defined associations. ### Example Find up to 30 of the newest purchases in our database: ```text GET /purchase?sort=createdAt DESC&limit=30 ``` [![Run in Postman](https://s3.amazonaws.com/postman-static/run-button.png)](https://www.getpostman.com/run-collection/96217d0d747e536e49a4) ##### Expected response e.g. ```json [ { "amount": 49.99, "id": 1, "createdAt": 1485551132315, "updatedAt": 1485551132315 }, { "amount": 99.99, "id": 47, "createdAt": 1485551158349, "updatedAt": 1485551158349 } ] ``` ##### Using jQuery > See [jquery.com](http://jquery.com/) for more documentation. ```javascript $.get('/purchase?sort=createdAt DESC', function (purchases) { console.log(purchases); }); ``` ##### Using sails.io.js > See [sails.io.js](https://sailsjs.com/documentation/reference/web-sockets/socket-client) for more documentation. ```javascript io.socket.get('/purchase?sort=createdAt DESC', function (purchases) { console.log(purchases); }); ``` ##### Using Angular > See [Angular](https://angularjs.org/) for more documentation. ```javascript $http.get('/purchase?sort=createdAt DESC') .then(function (res) { var purchases = res.data; console.log(purchases); }); ``` ##### Using cURL > You can read more about [cURL on Wikipedia](http://en.wikipedia.org/wiki/CURL). ```bash curl http://localhost:1337/purchase?sort=createdAt%20DESC ``` ### Notes > + The example above assumes "rest" blueprints are enabled, and that your project contains a `Purchase` model. You can quickly achieve this by running: > > ```bash > $ sails new foo > $ cd foo > $ sails generate model purchase > $ sails lift > # You will see a prompt about database auto-migration settings. > # Just choose 1 (alter) and press <ENTER>. > ``` <docmeta name="displayName" value="find where"> <docmeta name="pageType" value="endpoint"> ================================================ FILE: docs/reference/blueprint-api/FindOne.md ================================================ # Find one (blueprint) Look up the record with the specified `id` from the database, and (if possible) subscribe to the record in order to hear about any future changes. ```usage GET /:model/:id ``` The **findOne()** blueprint action returns a single record from the model (given by `:model`) as a JSON object. The specified `id` is the [primary key](http://en.wikipedia.org/wiki/Unique_key) of the desired record. If the action was triggered via a socket request, the requesting socket will be "subscribed" to the returned record. If the record is subsequently updated or deleted, a message will be sent to that socket's client informing them of the change. See the [.subscribe()](https://sailsjs.com/documentation/reference/web-sockets/resourceful-pub-sub/subscribe) docs for more info. ### Parameters Parameter | Type | Details ---------------------------------- | --------------------------------------- |:--------------------------------- model | ((string)) | The [identity](https://sailsjs.com/documentation/concepts/models-and-orm/model-settings#?identity) of the containing model.<br/><br/>e.g. `'purchase'` (in `/purchase/7`) id | ((string)) | The desired target record's primary key value<br/><br/>e.g. `'7'` (in `/purchase/7`). _populate_ | ((string?)) | If specified, overide the default automatic population process. Accepts a comma-separated list of attribute names for which to populate record values, or specify `false` to have no attributes populated. See [here](https://sailsjs.com/documentation/concepts/models-and-orm/records#?populated-values) for more information on how the population process fills out attributes in the returned record according to the model's defined associations. _select_ | ((string?)) | The attributes to include in the result, specified as a comma-delimited list. By default, all attributes are selected. Not valid for plural (“collection”) association attributes.<br/> <br/> e.g. `?select=name,age`. _omit_ | ((string?)) | The attributes to exclude from the result, specified as a comma-delimited list. Cannot be used in conjuction with `select`. Not valid for plural (“collection”) association attributes.<br/> <br/> e.g. `?omit=favoriteColor,address`. ### Example Find the purchase with id #1: ```text GET /purchase/1 ``` [![Run in Postman](https://s3.amazonaws.com/postman-static/run-button.png)](https://www.getpostman.com/run-collection/96217d0d747e536e49a4) ##### Expected Response ```json { "amount": 49.99, "id": 1, "createdAt": 1485551132315, "updatedAt": 1485551132315 } ``` <docmeta name="displayName" value="find one"> <docmeta name="pageType" value="endpoint"> ================================================ FILE: docs/reference/blueprint-api/Populate.md ================================================ # Populate (blueprint) Populate and return foreign record(s) for the given association of this record. ```usage GET /:model/:id/:association ``` If the specified association is plural ("collection"), this action returns the list of associated records as a JSON-encoded array of dictionaries (plain JavaScript objects). If the specified association is singular ("model"), this action returns the associated record as a JSON-encoded dictionary. Parameter | Type | Details :-------------- | ------------ |:--------------------------------- model | ((string)) | The [identity](https://sailsjs.com/documentation/concepts/models-and-orm/model-settings#?identity) of the containing model.<br/><br/>e.g. `'purchase'` (in `GET /purchase/47/cashier`) id | ((string)) | The primary key of the parent record.<br/><br/>e.g. `'47'` (in `GET /purchase/47/cashier`) association | ((string)) | The name of the association.<br/><br/>e.g. `'cashier'` (in `GET /purchase/47/cashier`) or `'products'` (in `GET /purchase/47/products`) _where_ | ((string?)) | Instead of filtering based on a specific attribute, you may instead choose to provide a `where` parameter with the WHERE piece of a [Waterline criteria](https://sailsjs.com/documentation/concepts/models-and-orm/query-language), _encoded as a JSON string_. This allows you to take advantage of `contains`, `startsWith`, and other sub-attribute criteria modifiers for more powerful `find()` queries. <br/> <br/> e.g. `?where={"name":{"contains":"theodore"}}` _limit_ | ((number?)) | The maximum number of records to send back (useful for pagination). Defaults to 30. <br/> <br/> e.g. `?limit=100` _skip_ | ((number?)) | The number of records to skip (useful for pagination). <br/> <br/> e.g. `?skip=30` _sort_ | ((string?)) | The sort order. By default, returned records are sorted by primary key value in ascending order. <br/> <br/> e.g. `?sort=lastName%20ASC` _select_ | ((string?)) | The attributes to include in each record in the result, specified as a comma-delimited list. By default, all attributes are selected. Not valid for plural (“collection”) association attributes.<br/> <br/> e.g. `?select=name,age`. _omit_ | ((string?)) | The attributes to exclude from each record in the result, specified as a comma-delimited list. Cannot be used in conjuction with `select`. Not valid for plural (“collection”) association attributes.<br/> <br/> e.g. `?omit=favoriteColor,address`. ### Example Populate the `cashier` who conducted purchase #47: ```text `GET /purchase/47/cashier` ``` [![Run in Postman](https://s3.amazonaws.com/postman-static/run-button.png)](https://www.getpostman.com/run-collection/96217d0d747e536e49a4) ##### Expected response ```json { "name": "Dolly", "id": 7, "createdAt": 1485462079725, "updatedAt": 1485476060873, } ``` **Using [jQuery](http://jquery.com/):** ```javascript $.get('/purchase/47/cashier', function (cashier) { console.log(cashier); }); ``` **Using [Angular](https://angularjs.org/):** ```javascript $http.get('/purchase/47/cashier') .then(function (cashier) { console.log(cashier); }); ``` **Using [sails.io.js](https://sailsjs.com/documentation/reference/web-sockets/socket-client):** ```javascript io.socket.get('/purchase/47/cashier', function (cashier) { console.log(cashier); }); ``` **Using [cURL](http://en.wikipedia.org/wiki/CURL):** ```bash curl http://localhost:1337/purchase/47/cashier ``` ### Populating a collection You can also populate a collection. For example, to populate the `involvedInPurchases` of employee #7: `GET /employee/7/involvedInPurchases` ##### Expected response ```json [ { "amount": 10000, "createdAt": 1485476060873, "updatedAt": 1485476060873, "id": 47, "cashier": 7 }, { "amount": 50, "createdAt": 1487015460792, "updatedAt": 1487015476357, "id": 52, "cashier": 7 } ] ``` ### Notes > + In the first example above, if purchase #47 did not have a `cashier` (i.e. `null`), then this action would respond with a 404 status code. > + The examples above assume "rest" blueprint routing is enabled (or that you've bound this blueprint action as a comparable [custom route](https://sailsjs.com/documentation/concepts/routes/custom-routes)), and that your project contains at least an empty `Employee` model as well as a `Purchase` model, and that `Employee` has the association attribute: `involvedInPurchases: {model: 'Purchase'}` and that `Purchase` has `cashier: {model: 'Employee'}`. You can quickly achieve this by running: > > ```shell > $ sails new foo > $ cd foo > $ sails generate model purchase > $ sails generate model employee > ``` > ...then editing `api/models/Employee.js` and `api/models/Purchase.js`. <docmeta name="displayName" value="populate where"> <docmeta name="pageType" value="endpoint"> ================================================ FILE: docs/reference/blueprint-api/Remove.md ================================================ # Remove (blueprint) Remove a foreign record (e.g. a comment) from one of this record's collections (e.g. "comments"). ```usage DELETE /:model/:id/:association/:fk ``` This action removes a reference to some other record (the "foreign" or "child" record) from a collection of this record (the "primary" or "parent" record). Note that this does not actually destroy the foreign record, it just unlinks it. + If the primary record does not exist, this responds using `res.notFound()`. + If the foreign record does not exist, this responds using `res.notFound()`. + If the collection doesn't contain a reference to the foreign record, this action will not modify any records. + If the association is "2-way" (meaning it has `via`), then the foreign key or collection it points to with that `via` will also be updated on the foreign record. ### Parameters Parameter | Type | Details :---------------------------------- | --------------------------------------- |:--------------------------------- model | ((string)) | The [identity](https://sailsjs.com/documentation/concepts/models-and-orm/model-settings#?identity) of the containing model for the parent record.<br/><br/>e.g. `'store'` (in `/store/16/employeesOfTheMonth/7`) id | ((string)) | The desired parent record's primary key value.<br/><br/>e.g. `'16'` (in `/store/16/employeesOfTheMonth/7`) association | ((string)) | The name of the collection attribute.<br/><br/>e.g. `'employeesOfTheMonth'` fk | ((string)) | The primary key value (usually id) of the child record to remove from the collection.<br/><br/>e.g. `'7'` ### Example Say you're building an app for a small chain of grocery stores. Each store has a giant television screen that displays the current "Employees of the Month" at that store, so that customers and team members see it when they walk in the door. In order to be sure it is up to date, you build a scheduled job (e.g. using [cron](https://en.wikipedia.org/wiki/Cron)) that runs on the first day of every month to change the "Employees of the Month" for each store in their system. Let's say that, as a part of this scheduled job, we send a request to remove Dolly (employee #7) from store #16's `employeesOfTheMonth`: ```text DELETE /store/16/employeesOfTheMonth/7 ``` [![Run in Postman](https://s3.amazonaws.com/postman-static/run-button.png)](https://www.getpostman.com/run-collection/96217d0d747e536e49a4) ##### Expected response ```json { "id": 16, "name": "Parmer and N. Lamar", "createdAt": 1485552033435, "updatedAt": 1485552048794, "employeesOfTheMonth": [ { "id": 12, "name": "Motoki", "createdAt": 1485462079725, "updatedAt": 1485476060873 }, { "id": 4, "name": "Timothy", "createdAt": 1485462079727, "updatedAt": 1485476090874 } ] } ``` ### Socket notifications If you have WebSockets enabled for your app, then every client [subscribed](https://sailsjs.com/documentation/reference/web-sockets/resourceful-pub-sub) to the parent record will receive a notification about the removed child, where the notification event name is that of the parent model identity (e.g. `store`), and the “message” has the following format: ``` id: <the parent record's primary key value>, verb: 'removedFrom', attribute: <the parent record collection attribute name>, removedIds: <the now-removed child records' primary key values> ``` For instance, continuing the example above, all clients subscribed to employee #7 (_except_ for the client making the request) would receive the following message: ```javascript { id: 16, verb: 'removedFrom', attribute: 'employeesOfTheMonth', removedIds: [ 7 ] } ``` **Clients subscribed to the child record receive an additional notification:** Assuming `employeesOfTheMonth` was defined with a `via`, then either `updated` or `removedFrom` notifications would also be sent to any clients who were [subscribed](https://sailsjs.com/documentation/reference/web-sockets/resourceful-pub-sub) to Dolly, the child record we removed. > If the `via`-linked attribute on the other side is [also plural](https://sailsjs.com/documentation/concepts/models-and-orm/associations/many-to-many) (e.g. `employeeOfTheMonthAtStores`), then another `removedFrom` notification will be sent. Otherwise, if the `via` [points at a singular attribute](https://sailsjs.com/documentation/concepts/models-and-orm/associations/one-to-many) (e.g. `employeeOfTheMonthAtStore`) then the [`updated` notification](https://sailsjs.com/documentation/reference/blueprint-api/update#?socket-notifications) will be sent. ### Notes > + If you'd like to spend some more time with Dolly, a more detailed walkthrough for the example above is available [here](https://gist.github.com/mikermcneil/e5a20b03be5aa4e0459b). > + This action is for dealing with _plural_ ("collection") attributes. If you want to set or unset a _singular_ ("model") attribute, just use [update](https://sailsjs.com/documentation/reference/blueprint-api/update) and set the foreign key to the id of the new foreign record (or `null` to clear the association). > + If you want to completely _replace_ the set of records in the collection with another set, use the [replace](https://sailsjs.com/documentation/reference/blueprint-api/replace) blueprint. <docmeta name="displayName" value="remove from"> <docmeta name="pageType" value="endpoint"> ================================================ FILE: docs/reference/blueprint-api/Replace.md ================================================ # Replace (blueprint) Replace all of the foreign records in one of this record's collections (e.g. "comments"). ```usage PUT /:model/:id/:association ``` This action resets references to "foreign", or "child" records that are members of a particular collection of _this_ record (the "primary", or "parent" record), replacing any existing references in the collection. + If the specified `:id` does not correspond with a primary record that exists in the database, this responds using `res.notFound()`. + Note that, if the association is "2-way" (meaning it has `via`), then the foreign key or collection on the foreign record(s) will also be updated. ### Parameters Parameter | Type | Details :-----------------------------------| --------------------------------------- |:--------------------------------- model | ((string)) | The [identity](https://sailsjs.com/documentation/concepts/models-and-orm/model-settings#?identity) of the containing model for the parent record.<br/><br/>e.g. `'employee'` (in `/employee/7/involvedinPurchases`) id | ((string)) | The desired parent record's primary key value.<br/><br/>e.g. `'7'` (in `/employee/7/involvedInPurchases`) association | ((string)) | The name of the collection attribute.<br/><br/>e.g. `'involvedInPurchases'` fks | ((array)) | The primary key values (usually ids) of the child records to use as the new members of this collection.<br/><br/>e.g. `[47, 65]` > _The `fks` parameter should be sent in the PUT request body, unless you are making this request using a development-only [shortcut blueprint route](https://sailsjs.com/documentation/concepts/blueprints/blueprint-routes#?shortcut-routes), in which case you can simply include it in the query string as `?fks=[47,65]`._ ### Example Suppose you are in charge of keeping records for a large chain of grocery stores, and Dolly the cashier (employee #7) had been taking credit for being involved in a large number of purchases, when really she had only checked out two customers. Since the owner of the grocery store chain is very forgiving, Dolly gets to keep her job, but now you have to update Dolly's `involvedInPurchases` collection so that it _only_ contains purchases #47 and #65: `PUT /employee/7/involvedInPurchases` ```json [47, 65] ``` [![Run in Postman](https://s3.amazonaws.com/postman-static/run-button.png)](https://www.getpostman.com/run-collection/96217d0d747e536e49a4) ##### Expected response This returns Dolly, the parent record. Notice that her record only shows her being involved in purchases #47 and #65: ```json { "id": 7, "name": "Dolly", "createdAt": 1485462079725, "updatedAt": 1485476060873, "involvedInPurchases": [ { "amount": 10000, "createdAt": 1485551132315, "updatedAt": 1486355134239, "id": 47, "cashier": 7 }, { "amount": 5667, "createdAt": 1483551158349, "updatedAt": 1485355134284, "id": 65, "cashier": 7 } ] } ``` ### Socket notifications If you have WebSockets enabled for your app, then every client [subscribed](https://sailsjs.com/documentation/reference/web-sockets/resourceful-pub-sub) to the parent record will receive one [`addedTo` notification](https://sailsjs.com/documentation/reference/blueprint-api/add-to#?socket-notifications) for each child record in the new collection (if any). For instance, continuing the example above, let's assume that Dolly's previous `involvedInPurchases` included purchases #65, #42, and #33. All clients subscribed to Dolly's employee record (_except_ for the client making the request) would receive two kinds of notifications: `addedTo` for the purchase she was not previously involved in (#47), and `removedFrom` for the purchases she is no longer involved in (#42 and #33). ```javascript { id: 7, verb: 'addedTo', attribute: 'involvedInPurchases', addedIds: [ 47 ] } ``` and ```javascript { id: 7, verb: 'removedFrom', attribute: 'involvedInPurchases', removedIds: [ 42, 33 ] } ``` > Note that purchase #65 is not included in the `addedTo` notification, since it was in Dolly's previous list of `involvedInPurchases`. **Clients subscribed to the child records receive additional notifications:** Assuming `involvedInPurchases` had a `via`, then either `updated` or `addedTo`/`removedFrom` notifications would also be sent to clients who were [subscribed](https://sailsjs.com/documentation/reference/web-sockets/resourceful-pub-sub) to any of the purchases we just linked or unlinked. > If the `via`-linked attribute on the other side (Purchase) is [also plural](https://sailsjs.com/documentation/concepts/models-and-orm/associations/many-to-many) (e.g. `cashiers`), then an `addedTo` or `removedFrom` notification will be sent. Otherwise, if the `via` [points at a singular attribute](https://sailsjs.com/documentation/concepts/models-and-orm/associations/one-to-many) (e.g. `cashier`) then the [`updated` notification](https://sailsjs.com/documentation/reference/blueprint-api/update#?socket-notifications) will be sent. **Finally, a third kind of notification might be sent:** If giving Dolly this new collection of Purchases would "steal" any of them from other employees' `involvedInPurchases`, then any clients subscribed to those other, stolen-from employee records (e.g. Motoki, employee #12 and Timothy, employee #4) would receive `removedFrom` notifications. (See [**Blueprints > remove from**](https://sailsjs.com/documentation/reference/blueprint-api/remove-from#?socket-notifications)). ### Notes > + Remember, this blueprint replaces the _entire_ set of associated records for the given attribute. To add or remove a single associated record from the collection, leaving the rest of the collection unchanged, use the "add" or "remove" blueprint actions. (See [**Blueprints > add to**](https://sailsjs.com/documentation/reference/blueprint-api/add-to) and [**Blueprints > remove from**](https://sailsjs.com/documentation/reference/blueprint-api/remove-from)). <docmeta name="displayName" value="replace"> <docmeta name="pageType" value="endpoint"> ================================================ FILE: docs/reference/blueprint-api/Update.md ================================================ # Update (blueprint) Update an existing record in the database and notify subscribed sockets that it has changed. ```usage PATCH /:model/:id ``` This updates the record in the model which matches the **id** parameter and responds with the newly updated record as a JSON dictionary. If a validation error occurred, a JSON response with the invalid attributes and a `400` status code will be returned instead. If no model instance exists matching the specified **id**, a `404` is returned. ### Parameters _Attributes to change should be sent in the HTTP body as form-encoded values or JSON._ Parameter | Type | Details ---------------------------------- | ------------------------------------------------------- |:--------------------------------- model | ((string)) | The [identity](https://sailsjs.com/documentation/concepts/models-and-orm/model-settings#?identity) of the containing model.<br/><br/>e.g. `'product'` (in `PATCH /product/5`) id | ((string)) | The primary key value of the record to update.<br/><br/>e.g. `'5'` (in `PATCH /product/5`) * | ((json)) | For `PATCH` (RESTful) requests, pass in body parameters with the same name as the attributes defined on your model to set those values on the desired record. For `GET` (shortcut) requests, add the parameters to the query string. ### Example Change Applejack's hobby to "kickin": `PATCH /user/47` ```json { "hobby": "kickin" } ``` [![Run in Postman](https://s3.amazonaws.com/postman-static/run-button.png)](https://www.getpostman.com/run-collection/96217d0d747e536e49a4) ##### Expected response ```json { "hobby": "kickin", "id": 47, "name": "Applejack", "createdAt": 1485462079725, "updatedAt": 1485476060873 } ``` ### Socket notifications If you have WebSockets enabled for your app, then every client [subscribed](https://sailsjs.com/documentation/reference/web-sockets/resourceful-pub-sub) to the updated record will receive a notification where the event name is that of the model identity (e.g. `user`), and the data “payload” has the following format: ``` verb: 'updated', id: <the record primary key>, data: <a dictionary of changes made to the record>, previous: <the record prior to the update> ``` For instance, continuing the example above, all clients subscribed to `User` #47 (_except_ for the client making the request) would receive the following message: ```js { id: 47, verb: 'updated', data: { id: 47, hobby: 'kickin' updatedAt: 1485476060873 }, previous: { hobby: 'pickin', id: 47, name: 'Applejack', createdAt: 1485462079725, updatedAt: 1485462079725 } } ``` **If the update changed any links to other records, there might be some additional notifications:** If we were reassigning user #47 to store #25, we'd update `store`, which represents the “one” side of a [one-to-many association](https://sailsjs.com/documentation/concepts/models-and-orm/associations/one-to-many). For instance: `PATCH /user/47` ```json { "store": 25 } ``` Clients subscribed to the new store (25) would receive an `addedTo` notification, and a `removedFrom` notification would be sent to any clients subscribed to the old store. See the [add blueprint reference](https://sailsjs.com/documentation/reference/blueprint-api/add-to) and the [remove blueprint reference](https://sailsjs.com/documentation/reference/blueprint-api/remove-from) for more info about those notifications. ### Notes > + This action can be used to replace an entire collection association (for example, to replace a user’s list of friends), achieving the same result as the [`replace` blueprint action](https://sailsjs.com/documentation/reference/blueprint-api/replace). To modify items in a collection individually, use the [add](https://sailsjs.com/documentation/reference/blueprint-api/add-to) or [remove](https://sailsjs.com/documentation/reference/blueprint-api/remove-from) actions. > + In previous Sails versions, this action was bound to the `PUT /:model/:id` route. <docmeta name="displayName" value="update"> <docmeta name="pageType" value="endpoint"> ================================================ FILE: docs/reference/blueprint-api/blueprint-api.md ================================================ # Blueprint API ### Overview For a conceptual overview of blueprints, see [Concepts > Blueprints](https://sailsjs.com/documentation/concepts/blueprints). ### Activating/deactivating blueprint routes in your app The process for activating/deactivating blueprints varies slightly with the kind of blueprint route you are concerned with (RESTful routes, shortcut routes, or action routes). See the [Blueprint Routes documentation section](https://sailsjs.com/documentation/concepts/blueprints?blueprint-routes) for a discussion of the different blueprint types. ### Overriding blueprints To change a blueprint route, we recommend [explicitly configuring a custom route](https://sailsjs.com/documentation/concepts/routes/custom-routes). Similarly, if you want to override a blueprint action, we recommend writing your own [custom action](https://sailsjs.com/documentation/concepts/actions-and-controllers). But if you really know what you're doing, then read on: ##### RESTful / shortcut routes and actions To override a RESTful blueprint route for a single model, simply create an action in the relevant controller file (or a [standalone action](https://sailsjs.com/documentation/concepts/actions-and-controllers#?standalone-actions) in the relevant folder) with the appropriate name: [_find_](https://sailsjs.com/documentation/reference/blueprint-api/find-where), [_findOne_](https://sailsjs.com/documentation/reference/blueprint-api/find-one), [_create_](https://sailsjs.com/documentation/reference/blueprint-api/create), [_update_](https://sailsjs.com/documentation/reference/blueprint-api/update), [_destroy_](https://sailsjs.com/documentation/reference/blueprint-api/destroy), [_populate_](https://sailsjs.com/documentation/reference/blueprint-api/populate), [_add_](https://sailsjs.com/documentation/reference/blueprint-api/add) or [_remove_](https://sailsjs.com/documentation/reference/blueprint-api/remove). > If you’d like to override a particular blueprint for _all_ models, check out the <a href="https://www.npmjs.com/package/sails-hook-custom-blueprints" target="_blank">sails-hook-custom-blueprints plugin</a>. > It's important to realize that, even if you haven't defined these yourself, Sails will respond with built-in CRUD logic for each model in the form of a JSON API (including support for sort, pagination, and filtering) as long as action or shortcut blueprints are enabled in your [blueprints configuration](https://sailsjs.com/documentation/reference/configuration/sails-config-blueprints). ### Blueprints and resourceful PubSub The blueprint API is compatible with WebSockets (as are any of your custom actions and policies), thanks to the virtual request interpreter. Check out the reference section on the browser SDK ([Reference > WebSockets > sails.io.js](https://sailsjs.com/documentation/reference/web-sockets/socket-client)) for example usage. ##### Blueprints and `.subscribe()` By default, the **Find** and **Find One** blueprint actions will call [`.subscribe()`](https://sailsjs.com/documentation/reference/web-sockets/resourceful-pub-sub/subscribe) automatically when a socket request is used. This subscribes the requesting socket to each of the returned records. However, if the _same_ socket sends a request to the **Update** or **Destroy** actions with `io.socket.put()` (for example) this will *not* by default cause a message to be sent to the requesting socket, but to the *other* connected, subscribed sockets. This is intended to allow UI code to use the client-side SDK's callback to handle the server response separately, e.g. to replace a loading spinner. ##### Blueprints and "auto-watch" By default, the **find** blueprint action (when triggered via a WebSocket request) will subscribe the requesting socket to notifications about _new_ instances of that model being created. This behavior can be changed for all models by setting [`sails.config.blueprints.autoWatch`](https://sailsjs.com/documentation/reference/configuration/sails-config-blueprints) to `false`. ##### Disabling blueprint routes on a per-controller basis > The following technique is only supported for compatibility reasons. Please just use custom routes, whether or not you are using blueprint actions! If you are using controllers, rather than standalone action files, it is possible to **disable** certain settings from [`config/blueprints.js`](https://sailsjs.com/documentation/anatomy/my-app/config/blueprints-js) on a per-controller basis by defining a `_config` key in your controller definition: ```javascript // In /api/controllers/PetController.js module.exports = { _config: { actions: false, shortcuts: false, rest: false } } ``` > Disabling `shortcuts`-style automatic routes on a per-controller basis is not supported. This is never necessary, because you should never use `shortcuts: true` in production. <docmeta name="displayName" value="Blueprint API"> ================================================ FILE: docs/reference/cli/cli.md ================================================ # Command-line interface (CLI) Sails comes with a convenient command-line tool to quickly get your app scaffolded and running. The CLI has commands for creating, starting, and debugging your Sails applications, as well as for getting your version info. For information about each command's usage, see the reference pages in this section. <docmeta name="displayName" value="Command-line interface"> ================================================ FILE: docs/reference/cli/sailsconsole.md ================================================ # `sails console` Lift your Node.js/Sails.js app in interactive mode, and enter the [REPL](http://nodejs.org/api/repl.html). This means you can access and use all of your models, helpers, configuration, services, and the `sails` app instance. Useful for trying out Waterline queries, quickly managing your data, and checking out your project's runtime configuration. ```usage sails console ``` By default, this still lifts the server, so your routes will be accessible via HTTP and sockets (e.g. in a browser). ### Usage `sails console` takes the following options: * `--dontLift`: start `sails console` without lifting the server ### Example ```text $ sails console info: Starting app in interactive mode... info: Welcome to the Sails console. info: ( to exit, type <CTRL>+<C> ) sails> ``` ### Global variables in `sails console` Sails exposes [the same global variables](https://sailsjs.com/documentation/reference/Globals) in the REPL as it does in your app code. By default, you have access to the `sails` app instance and your models, as well as any of your other configured globals (for example, lodash (`_`) and async (`async`)). > **Warning** > > In Node versions earlier than v6, using `_` as a variable in the REPL will cause unexpected behavior. As an alternative, simply import the Lodash module as a variable: > > ```bash > sails> var lodash = require('lodash'); > sails> console.log(lodash.range(1, 5)); > ``` ### More examples ##### Waterline The format `Model.action(query).exec(console.log)` console.log is good for seeing the results. ```text sails> User.create({name: 'Brian', password: 'sailsRules'}).fetch().exec(console.log) undefined sails> undefined { name: 'Brian', password: 'sailsRules', createdAt: "2014-08-07T04:29:21.447Z", updatedAt: "2014-08-07T04:29:21.447Z", id: 1 } ``` It inserts it into the database, which is pretty cool. However, you might be noticing the `undefined` and `null`—don't worry about those. Remember that the .exec() returns errors and data for values, so `.exec(console.log)` has the same effect as `.exec(console.log(err, data))`. The second method will remove the undefined message, but add null on a new line. Whether you want to type more is up to you. > Note that starting with Node 6, an object’s constructor name is displayed next to it in the console. For example, when using the [`sails-mysql` adapter](https://sailsjs.com/documentation/concepts/extending-sails/adapters/available-adapters#?sailsmysql), the `create` query mentioned above would output: > > ```text > sails> undefined RowDataPacket { name: 'Brian', > password: 'sailsRules', > createdAt: "2014-08-07T04:29:21.447Z", > updatedAt: "2014-08-07T04:29:21.447Z", > id: 1 } > ``` ##### Exposing Sails In `sails console`, type `sails` to view a list of Sails properties. You can use this to learn more about Sails, override properties, or check to see if you disabled globals. ```text sails> sails |> [a lifted Sails app on port 1337] \___/ For help, see: https://sailsjs.com/documentation/concepts/ Tip: Use `sails.config` to access your app's runtime configuration. 1 Models: User 1 Controllers: UserController 20 Hooks: moduleloader,logger,request,orm,views,blueprints,responses,controllers,sockets,p ubsub,policies,services,csrf,cors,i18n,userconfig,session,grunt,http,projecthooks sails> ``` <docmeta name="displayName" value="sails console"> <docmeta name="pageType" value="command"> ================================================ FILE: docs/reference/cli/sailsdebug.md ================================================ # `sails debug` > ##### _**This command should only be used with older versions of Node. For Node v6 and above, use [`sails inspect`](https://sailsjs.com/documentation/reference/command-line-interface/sails-inspect).**_ Attach the node debugger and lift the Sails app (similar to running `node --debug app.js`). You can then use [node-inspector](https://github.com/node-inspector/node-inspector) to debug your app as it runs. ```usage sails debug ``` ### Usage Takes the same options as [`sails lift`](https://sailsjs.com/documentation/reference/command-line-interface/sails-lift), listed [here](https://sailsjs.com/documentation/reference/command-line-interface/sails-lift#?usage). ### Example ```text $ sails debug info: Running node-inspector on this app... info: If you don't know what to do next, type `help` info: Or check out the docs: info: http://nodejs.org/api/debugger.html info: ( to exit, type <CTRL>+<C> ) debugger listening on port 5858 ``` > To use the standard (command-line) Node debugger with Sails, you can always just run `node debug app.js`. ### Using Node Inspector To debug your Sails app using Node Inspector, first install it over npm: ```bash $ npm install -g node-inspector ``` Then, launch it with the `node-inspector` command: ```bash $ node-inspector ``` Now, you can lift your Sails app in debug mode: ```bash $ sails debug ``` Once the application is launched, visit http://127.0.0.1:8080?port=5858 in Opera or Chrome (Sorry, other browsers!). Now you can request your app as usual on port 1337 and debug your code from the browser. > **How it works** > Node.js includes a TCP-based debugger. When you start your application using `sails debug`, Node.js lifts your app and opens a socket on port `5858`. This socket allows external tools to interact with and control the debugger. Node Inspector, accessible via the port `8080`, is this kind of tool. > If you don't see your files in the browser at http://127.0.0.1:8080?port=5858 or if it's very slow to load, try running Node Inspector with the `--no-preload` argument. [See the Node Inspector repo](https://github.com/node-inspector/node-inspector) for more details. <docmeta name="displayName" value="sails debug"> <docmeta name="pageType" value="command"> ================================================ FILE: docs/reference/cli/sailsgenerate.md ================================================ # Sails generate Generate a code file (or multiple files) in a Sails app. ```usage sails generate <generator> ``` Sails ships with several _generators_ to help you scaffold new projects, spit out boilerplate code for common files, and automate your development process. ### Core generators The following _core generators_ are bundled with Sails: | Command | Details | |:--------------------------------|:----------------------| | sails generate page | Generate four pages: .ejs, .less, page script, and view action. You must add your .less file to the importer and you must set your route for your new page to work. **Note**: `sails generate page` is intended for use with projects generated with the "Web app" template. You can still use this command if you're not using the web app template, but you'll need to delete the `assets/js/pages/page-name.page.js` file that's been generated, as it relies on dependencies that don't come bundled with an "Empty" Sails app. | sails generate model | Generate **api/models/Foo.js**, including attributes with the specified types if provided.<br /> For example, `sails generate model User username isAdmin:boolean` will generate a User model with a `username` string attribute and an `isAdmin` boolean attribute. | sails generate action | Generate a standalone [action](https://sailsjs.com/documentation/concepts/actions-and-controllers/generating-actions-and-controllers#?generating-standalone-actions). | sails generate helper | Generate a [helper](https://sailsjs.com/documentation/concepts/helpers) at **api/helpers/foo.js**. | sails generate controller | Generate **api/controllers/FooController.js**, including actions with the specified names if provided. | sails generate hook | Generate a [project hook](https://sailsjs.com/documentation/concepts/extending-sails/hooks/project-hooks) in **api/hooks/foo/**. | sails generate generator | Generate a **foo** folder containing the files necessary for building a new generator. | sails generate response | Generate a [custom response](https://sailsjs.com/documentation/concepts/extending-sails/custom-responses) at **api/responses/foo.js** | sails generate adapter | Generate a **api/adapters/foo/** folder containing the files necessary for building a new adapter. | sails generate sails.io.js | Generate a sails.io.js file at the specified location, overwriting the default sails.io.js if applicable. | _sails generate api_ | _Generate **api/models/Foo.js** and **api/controllers/FooController.js**._ | _sails generate new_ | _Alias for [`sails new`](https://sailsjs.com/documentation/reference/command-line-interface/sails-new)._ | _sails generate etc_ | **Experimental.** Adds the following files to your app:<br/>• .gitignore <br/>• .jshintrc <br/>• .editorconfig <br/>• .npmignore <br/>• .travis.yml <br/>• .appveyor.yml ### Custom generators [Custom / third party generators](https://sailsjs.com/documentation/concepts/extending-sails/generators) allow you to extend or override the default functionality of `sails generate` (for example, by creating a generator that outputs view files for your favorite [view engine](https://sailsjs.com/documentation/concepts/views/view-engines)). You can also use custom generators to automate frequent tasks or generate app-specific files. For example, if you are using React, you might wire up a quick custom generator to allow you to generate [React components](https://facebook.github.io/react/docs/react-component.html) in the appropriate folder in your project (`sails generate react component`). <docmeta name="displayName" value="sails generate"> <docmeta name="pageType" value="command"> ================================================ FILE: docs/reference/cli/sailsinspect.md ================================================ # Sails inspect > ##### _**This command should only be used with modern versions of Node. For Node v5 and below, use [`sails debug`](https://sailsjs.com/documentation/reference/command-line-interface/sails-debug).**_ Attach the Node debugger and lift the Sails app (similar to running `node --inspect app.js`). You can then use a tool like Chrome DevTools to interactively debug your apps (see the [Node Inspector docs](https://nodejs.org/en/docs/inspector/) for more information). ```usage sails inspect ``` ### Usage Takes the same options as [`sails lift`](https://sailsjs.com/documentation/reference/command-line-interface/sails-lift), listed [here](https://sailsjs.com/documentation/reference/command-line-interface/sails-lift#?usage). ### Example ```text $ sails inspect info: Running app in inspect mode... info: In Google Chrome, go to chrome://inspect for interactive debugging. info: For other options, see the link below. info: ( to exit, type <CTRL>+<C> ) Debugger listening on ws://127.0.0.1:9229/7f984b04-b070-4497-bd15-056261a37f7c For help see https://nodejs.org/en/docs/inspector ``` > To use the standard (command-line) Node debugger with Sails, you can always just run `node inspect app.js`. > If you don't see your files in the Chrome DevTools, try clicking the "Filesystem" tab and adding your project folder to the workspace. <docmeta name="displayName" value="sails inspect"> <docmeta name="pageType" value="command"> ================================================ FILE: docs/reference/cli/sailslift.md ================================================ # `sails lift` Run the Sails app in the current dir (if `node_modules/sails` exists, it will be used instead of the globally installed Sails). ```usage sails lift ``` By default, Sails lifts your app in development mode. In the development environment, Sails uses [Grunt](https://gruntjs.com/) to keep an eye on your files in `/assets`. If you change something (for example in one of your `.css` or `.less` files) and reload your browser, you'll notice that your changes are reflected automatically. Also note that, in development mode, your view templates won't be cached in memory. So, like assets, you can also change your view files without restarting Sails. > Any changes to back-end logic or configuration (e.g. the files in `config/`, `api/`, or `node_modules/`) _will not take effect_ unless you kill and restart the server (CTRL+C + `sails lift`). ### Usage: `sails lift` takes the following options: * `--prod` - in production environment * `--port <portNum>` - on the port specified by `portNum` instead of the default (1337) * `--verbose` - with verbose logging enabled * `--silly` - with insane logging enabled ### Example ```text $ sails lift info: Starting app... info: info: info: Sails <| info: v1.0.0 |\ info: /|.\ info: / || \ info: ,' |' \ info: .-'.-==|/_--' info: `--'-------' info: __---___--___---___--___---___--___ info: ____---___--___---___--___---___--___-__ info: info: Server lifted in `/Users/mikermcneil/code/sandbox/second` info: To see your app, visit http://localhost:1337 info: To shut down Sails, press <CTRL> + C at any time. debug: -------------------------------------------------------- debug: :: Sat Apr 05 2014 17:03:39 GMT-0500 (CDT) debug: Environment : development debug: Port : 1337 debug: -------------------------------------------------------- ``` <docmeta name="displayName" value="sails lift"> <docmeta name="pageType" value="command"> ================================================ FILE: docs/reference/cli/sailsnew.md ================================================ # `sails new` Create a new Sails project. ```usage sails new your-app-name ``` ### Usage: Most Sails apps should be generated simply by running `sails new your-app-name`, without any additional customization. But `sails new` also accepts the following options: * `--no-frontend`: useful when generating a new Sails app that will not be used to serve any front-end assets. Disables the generation of the `assets/` folder, `tasks/` folder, and related files. * `--minimal`: generates an extremely minimal Sails app. This disables the same things as `--no-frontend`, along with i18n, Waterline, Grunt, Lodash, Async, sessions, and views. * `--without`: used to generate a Sails app without the specified feature(s). The supported "without" options are: `'lodash'`, `'async'`, `'orm'`, `'sockets'`, `'grunt'`, `'i18n'`, `'session'`, and `'views'`. To disable multiple features at once, you can include the options as a comma-separated list, e.g. `sails new your-app-name --without=grunt,views`. ### Example To create a project called "test-project" in `code/testProject/`: ```text $ sails new code/testProject info: Installing dependencies... Press CTRL+C to skip. (but if you do that, you'll need to cd in and run `npm install`) info: Created a new Sails app `test-project`! ``` To create a Sails project in an existing `myProject/` folder: ```text $ cd myProject $ sails new . info: Installing dependencies... Press CTRL+C to skip. (but if you do that, you'll need to cd in and run `npm install`) info: Created a new Sails app `my-project`! ``` > Creating a new Sails app in an existing folder will only work if the folder is empty. ### Notes: > + `sails new` is really just a special [generator](https://sailsjs.com/documentation/concepts/extending-sails/Generators) which runs [`sails-generate-new`](http://github.com/balderdashy/sails-generate-new). In other words, running `sails new foo` is an alias for running `sails generate new foo`, and like any Sails generator, the actual generator module which gets run can be overridden in your global `~/.sailsrc` file. <docmeta name="displayName" value="sails new"> <docmeta name="pageType" value="command"> ================================================ FILE: docs/reference/cli/sailsversion.md ================================================ # `sails --version` Get the version of your computer's _globally_ installed Sails command-line tool (i.e. the version you installed with `npm install -g sails`). ```usage sails --version ``` ### Example ```text $ sails --version 1.0.0 ``` ### Notes > + Different Sails apps can have different local Sails installs at different versions, since each project encapsulates its dependencies in its `node_modules/` folder. To get the _locally_ installed version of Sails from within a particular project, run `npm ls sails`. <docmeta name="displayName" value="sails --version"> <docmeta name="pageType" value="command"> ================================================ FILE: docs/reference/reference.md ================================================ # Sails.js Documentation > API Reference > The contents of this file are overridden automatically during compilation (please do not edit manually!) <docmeta name="displayName" value="API Reference: Table of Contents"> <docmeta name="isTableOfContents" value="true"> ================================================ FILE: docs/reference/req/req._startTime.md ================================================ # `req._startTime` The moment that Sails started processing the request, as a [Javascript Date object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date). > This property is not added when your app is in [production mode](https://sailsjs.com/documentation/concepts/deployment#?set-the-nodeenv-environment-variable-to-production). ### Usage ```usage req._startTime; ``` <docmeta name="displayName" value="req._startTime"> <docmeta name="pageType" value="property"> ================================================ FILE: docs/reference/req/req.accepts.md ================================================ # `req.accepts()` Return whether this request (`req`) advertises that it understands the specified media type. > If none of the media types are considered acceptable, this returns `false`. Otherwise, it returns truthy (the media type). ### Usage ```usage req.accepts(mediaType); ``` ### Example If a request is sent with an `"Accept: application/json"` header: ```javascript req.accepts('application/json'); // -> 'application/json' req.accepts('json'); // -> 'json' req.accepts('image/png'); // -> false ``` If a request is sent with an `"Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8"` header: ```javascript req.accepts('html'); // -> 'html' req.accepts('text/html'); // -> 'text/html' req.accepts('json'); // -> false ``` ### Notes > + The specified media type may be provided as either a [MIME type string](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types) such as "application/json", or an extension name such as "json". > + This is implemented by examining the request's ["Accept" header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept). > + See the [`accepts` package](https://www.npmjs.com/package/accepts) for the finer details of the header-parsing algorithm used in Sails/Express. <docmeta name="displayName" value="req.accepts()"> <docmeta name="pageType" value="method"> ================================================ FILE: docs/reference/req/req.acceptsCharsets.md ================================================ # `req.acceptsCharsets()` Return whether this request (`req`) advertises that it is able to handle any of the specified character set(s), and if so, which one. > If _more than one_ of the character sets passed in to this method are considered acceptable, then the first one will be returned. If none of the character sets are considered acceptable, this returns `false`. ### Usage ```usage req.acceptsCharsets(charset); ``` or: + `req.acceptsCharsets(charset1, charset2, …);` ### Details Useful for advanced content negotiation where a client may or may not support certain character sets, such as Unicode (UTF-8). ### Example If a request is sent with a `"Accept-Charset: utf-8"` header: ```js req.acceptsCharsets('utf-8'); // -> 'utf-8' req.acceptsCharsets('iso-8859-1', 'utf-16', 'utf-8'); // -> 'utf-8' req.acceptsCharsets('utf-16'); // -> false ``` ### Notes > + This is implemented by examining the request's [`Accept-Charset`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Charset) header (see [RFC-2616](http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.2)). > + See the [`accepts` module](https://www.npmjs.com/package/accepts) for the finer details of the header-parsing algorithm used in Sails/Express. <docmeta name="displayName" value="req.acceptsCharsets()"> <docmeta name="pageType" value="method"> ================================================ FILE: docs/reference/req/req.acceptsLanguages.md ================================================ # `req.acceptsLanguages()` Return whether this request (`req`) advertises that it understands any of the specified language(s), and if so, which one. > If _more than one_ of the languages passed in to this method are considered acceptable, then the first one will be returned. If none of the languages are considered acceptable, this returns `false`. > (By languages, we mean natural languages, like English or Japanese, not programming languages.) ### Usage ```usage req.acceptsLanguages(language); ``` _Or:_ + `req.acceptsLanguages(language1, language2, …);` ### Details This method can be useful as a complement to built-in [internationalization and localization](https://sailsjs.com/documentation/concepts/Internationalization), which allows for automatically serving different content to different locales based on the request. ### Example If a request is sent with `"Accept-Language: da, en, en-gb, en-us;"`: ```js req.acceptsLanguages('en'); // -> 'en' req.acceptsLanguages('es'); // -> false req.acceptsLanguages('en-us', 'en', 'en-gb'); // -> 'en-us' req.acceptsLanguages('en-gb', 'en', 'en-us'); // -> 'en-gb' req.acceptsLanguages('es', 'fr'); // -> false ``` ### Notes > + You can expect the ["Accept-Language" header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Language) to exist in most requests that originate in web browsers (see [RFC-2616](http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.4)). > + Browsers send the "Accept-Language" header automatically, based on the user's language settings. > + See the [`accepts` package](https://www.npmjs.com/package/accepts) for the finer details of the header-parsing algorithm used in Sails/Express. <docmeta name="displayName" value="req.acceptsLanguages()"> <docmeta name="pageType" value="method"> ================================================ FILE: docs/reference/req/req.allParams.md ================================================ # `req.allParams()` Returns the value of _all_ parameters sent in the request, merged into a single dictionary (plain JavaScript object). Includes parameters parsed from the URL path, the request body, and the query string, _in that order_. See [`req.param()`](https://sailsjs.com/documentation/reference/request-req/req-param) for details. ### Usage ```usage req.allParams(); ``` ### Example Update the product with the specified `sku`, setting new values using the parameters that were passed in: ```javascript var values = req.allParams(); // Don't allow `price` or `isAvailable` to be edited. delete values.price; delete values.isAvailable; // At this point, `values` might look something like this: // values ==> { displayName: 'Bubble Trouble Bubble Bath' } Product.update({sku: sku}) .set(values) .exec(function (err, newProduct) { // ... }); ``` ### Notes >+ The order of precedence means that URL path params override request body params, which will override query string params. >+ In past versions of Sails, this method was known as `req.params.all()`, but this could be confusing—what if you had a route path parameter named "all"? In apps built on Sails v1 or later, you should use `req.allParams()` in favor of `req.params.all()` to avoid such a situation. <docmeta name="displayName" value="req.allParams()"> <docmeta name="pageType" value="method"> ================================================ FILE: docs/reference/req/req.body.md ================================================ # `req.body` An object containing text parameters from the parsed request body, defaulting to `{}`. By default, the request body can be URL-encoded or stringified as JSON. Support for other formats, such as serialized XML, is possible using the [middleware](https://sailsjs.com/documentation/concepts/Middleware) configuration. ### Usage ```usage req.body; ``` ### Notes >+ If a request contains one or more file uploads, only the text parameters sent _**before**_ the first file parameter will be available in `req.body`. >+ When using [Skipper](https://github.com/balderdashy/skipper), the default body parser, this property will be `undefined` for GET requests. <docmeta name="displayName" value="req.body"> <docmeta name="pageType" value="property"> ================================================ FILE: docs/reference/req/req.cookies.md ================================================ # `req.cookies` An object containing all of the [**unsigned cookies**](https://github.com/balderdashy/sails/blob/master/docs/PAGE_NEEDED.md) from this request (`req`). ### Usage ```usage req.cookies; ``` ### Example Assuming the request contained a cookie named "chocolatechip" with value "Yummy: ```javascript req.cookies.chocolatechip; // "Yummy" ``` <docmeta name="displayName" value="req.cookies"> <docmeta name="pageType" value="property"> ================================================ FILE: docs/reference/req/req.file.md ================================================ # `req.file()` Build and return a [Skipper Upstream](https://github.com/balderdashy/skipper/tree/b0f99c526b6664a2e867e3ef0bafcfff35e6fba2#what-are-upstreams) representing an incoming multipart file upload from the specified `field`. ```usage req.file(field); ``` ### Usage | | Argument | Type | Details | |---|-----------------------------|:-------------------:|------------------------------------------------------------------------------| | 1 | `field` | ((string)) | The name of the file parameter to listen on for uploads; e.g. `avatar`. | ### Details `req.file()` comes from [Skipper](https://github.com/balderdashy/skipper), an opinionated variant of the original Connect body parser. Skipper allows you to take advantage of high-performance, streaming file uploads without any dramatic changes in your application logic. This simplifcation comes with a minor caveat: **text parameters must be included before files in the request body.** Typically these text parameters contain string metadata that provide additional information about the file upload. Multipart requests to Sails should send all of their **text parameters**. before sending _any_ **file parameters**. For instance, if you're building a web front end that communicates with Sails, you should include text parameters _first_ in any form upload or AJAX file upload requests. The term "text parameters" refers to the metadata parameters you might send with the file(s) providing additional information about the upload. ### How it works Skipper treats all file uploads as streams. This allows users to upload monolithic files with minimal performance impact and no disk footprint, all the while protecting your app against nasty denial-of-service attacks involving TMP files. When a multipart request hits your server, instead of writing temporary files to disk, Skipper buffers the request just long enough to run your app code, allowing you to "plug in" to a compatible blob receiver. If you don't "plug in" the data from a particular field, the Upstream hits its "high water mark", the buffer is flushed, and subsequent incoming bytes on that field are ignored. ### Example In a controller action or policy: ```javascript // See the Skipper README on GitHub for usage documentation for `.upload()`, including // a complete list of options. req.file('avatar').upload(function (err, uploadedFiles){ if (err) return res.serverError(err); return res.json({ message: uploadedFiles.length + ' file(s) uploaded successfully!', files: uploadedFiles }); }); ``` ### Notes > + Remember that the client request's text parameters must be sent before the file parameters! > + `req.file()` supports multiple files sent over the same field, but it's important to realize that, as a consequence, the Upstream it returns is actually a stream (buffered event emitter) of potential binary streams (files). Specifically, an [`Upstream`](https://github.com/balderdashy/skipper/tree/b0f99c526b6664a2e867e3ef0bafcfff35e6fba2#what-are-upstreams) is a [Node.js Readable stream](http://nodejs.org/api/stream.html#stream_class_stream_readable) in "object mode", where each object is itself an incoming multipart file upload stream. > + If you prefer to work directly with the Upstream as a stream of streams, you can omit the `.upload()` method and bind "finish" and "error" events (or use `.pipe()`) instead. [Under the covers](https://github.com/balderdashy/skipper/blob/b0f99c526b6664a2e867e3ef0bafcfff35e6fba2/standalone/Upstream/prototype.upload.js), all `.upload()` is doing is piping the **Upstream** into the specified receiver instance, then running the specified callback when the Upstream emits either a `finish` or `error` event. <docmeta name="displayName" value="req.file()"> <docmeta name="pageType" value="method"> ================================================ FILE: docs/reference/req/req.fresh.md ================================================ # `req.fresh` A flag indicating that the user-agent sending this request (`req`) wants "fresh" data (as indicated by the "[if-none-match](http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.26)", "[cache-control](http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9)", and/or "[if-modified-since](http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.25)" request headers.) If the request wants "fresh" data, usually you'll want to `.find()` fresh data from your models and send it back to the client. ### Usage ```usage req.fresh; ``` ### Example ```js if (req.fresh) { // The user-agent is asking for a more up-to-date version of the requested resource. // Let's hit the database to get some stuff and send it back. } ``` ### Notes > + See the [`node-fresh`](https://github.com/visionmedia/node-fresh) module for details specific to the implementation in Sails/Express/Koa/Connect. <docmeta name="displayName" value="req.fresh"> <docmeta name="pageType" value="property"> ================================================ FILE: docs/reference/req/req.get.md ================================================ # `req.get()` Returns the value of the specified `header` field in this request (`req`). Note that header names are case-_insensitive_. ### Usage ```usage req.get(header); ``` ### Example Assuming `req` contains a header named 'myField' with value 'cat': ```javascript req.get('myField'); // -> cat ``` ### Notes >+ The `header` argument is case-insensitive. >+ The `header` argument treats both "referrer" and "referer" as synonyms, because sp3ll1n9. <docmeta name="displayName" value="req.get()"> <docmeta name="pageType" value="method"> ================================================ FILE: docs/reference/req/req.headers.md ================================================ # `req.headers` An object containing the predefined/custom header given in the current request. ### Usage ```usage req.headers; ``` ### Details Often we want to check the headers of the current request. This can be done easily in Sails. ### Example Sample output of the `req.headers` object: ```javascript console.log(req.headers); { host: 'localhost:1337', connection: 'keep-alive', 'cache-control': 'no-cache', 'user-agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.89 Safari/537.36', accept: '*/*', 'accept-encoding': 'gzip, deflate, sdch', 'accept-language': 'en-US,en;q=0.8,hi;q=0.6', cookie: 'sdfkslddklfk; sails.sid=s%3skdlfjkj1231lsdfnsc,m' } ``` ### Note If you want to access any specific, custom, or predefined header, it can be done with bracket notation: ```javascript req.headers['custom-header']; ``` or dot notation: ```javascript req.headers.host; ``` <docmeta name="displayName" value="req.headers"> <docmeta name="pageType" value="property"> ================================================ FILE: docs/reference/req/req.host.md ================================================ # `req.host` > ##### **This method is deprecated and will likely be removed or changed in an upcoming release.** > Instead, use [req.hostname](https://sailsjs.com/documentation/reference/request-req/req-hostname). The hostname of this request, without the port number, as specified by its "Host" header. ### Usage ```usage req.host; ``` ### Example If this request's "Host" header was "ww3.staging.ibm.com:1492": ```javascript req.host; // -> "ww3.staging.ibm.com" ``` <docmeta name="displayName" value="req.host"> <docmeta name="pageType" value="property"> <docmeta name="isDeprecated" value="true"> ================================================ FILE: docs/reference/req/req.hostname.md ================================================ # `req.hostname` Returns the hostname supplied in the host HTTP header. This header may be set either by the client or by the proxy. ### Usage ```usage req.hostname; ``` ### Example If this request's "Host" header was "ww3.staging.ibm.com:1492": ```javascript req.hostname; // -> "ww3.staging.ibm.com" ``` <docmeta name="displayName" value="req.hostname"> <docmeta name="pageType" value="property"> ================================================ FILE: docs/reference/req/req.ip.md ================================================ # `req.ip` The IP address of the client who sent this request (`req`). > **Note:** > > If your Sails app is deployed behind a proxy (on Heroku, for example), then you'll need to do a bit of additional configuration. Normally, `req.ip` is simply the "remote address"—the IP address of the requesting user agent. But if the [sails.config.http.trustProxy](https://sailsjs.com/documentation/reference/configuration/sails-config-http) option is enabled, this is the "[upstream address](https://en.wikipedia.org/wiki/X-Forwarded-For)". ### Usage ```usage req.ip; ``` ### Example ```javascript req.ip; // -> "127.0.0.1" ``` <docmeta name="displayName" value="req.ip"> <docmeta name="pageType" value="property"> ================================================ FILE: docs/reference/req/req.ips.md ================================================ # `req.ips` If [sails.config.http.trustProxy](https://sailsjs.com/documentation/reference/configuration/sails-config-http) is enabled, this variable contains the IP addresses in this request's "X-Forwarded-For" header as an array of the IP address strings. Otherwise an empty array is returned. ### Usage ```usage req.ips; ``` ### Example If a request contains a header, "X-Forwarded-For: client, proxy1, proxy2": ```js req.ips; // -> ["client", "proxy1", "proxy2"]` // ("proxy2" is the furthest "down-stream" IP address) ``` <docmeta name="displayName" value="req.ips"> <docmeta name="pageType" value="property"> ================================================ FILE: docs/reference/req/req.is.md ================================================ # `req.is()` Returns true if this request's declared "Content-Type" matches the specified media/mime `type`. Specifically, this method matches the given `type` against this request's "Content-Type" header. ### Usage ```usage req.is(type); ``` ### Example Assuming the request contains a "Content-Type" header, "text/html; charset=utf-8": ```javascript req.is('html'); // -> true req.is('text/html'); // -> true req.is('text/*'); // -> true ``` <docmeta name="displayName" value="req.is()"> <docmeta name="pageType" value="method"> ================================================ FILE: docs/reference/req/req.isSocket.md ================================================ # `req.isSocket` A flag indicating whether or not this request (`req`) originated from a Socket.io connection. ### Usage ```usage req.isSocket; ``` ### Example ```javascript if (req.isSocket){ // You're a socket. Do cool socket stuff like subscribing. User.subscribe(req, [req.session.userId]); } else { // Just another HTTP request. // (`req.isSocket` is undefined) } ``` ### Notes > + Useful for allowing HTTP requests to skip calls to PubSub or WebSocket-centric methods like `subscribe()` or `watch()` that depend on an actual Socket.io request. This allows you to reuse backend code for both WebSocket and HTTP clients. > + As you might expect, `req.isSocket` doesn't need to be checked before running methods that **publish to other** connected sockets. Those methods don't depend on the request, so they work either way. <docmeta name="displayName" value="req.isSocket"> <docmeta name="pageType" value="property"> ================================================ FILE: docs/reference/req/req.md ================================================ # Request (`req`) Sails is built on [Express](https://github.com/balderdashy/sails/blob/master/docs/PAGE_NEEDED.md), and uses [Node's HTTP server](http://nodejs.org/api/http.html) conventions. Because of this, you can access all of the Node and Express methods and properties on the `req` object wherever it is accessible (in your controllers, policies, and custom responses). A nice side effect of this compatibility is that, in many cases, you can paste existing Node.js code into a Sails app and it will work. And since Sails implements a transport-agnostic request interpreter, the code in your Sails app is WebSocket-compatible as well. Sails adds a few methods and properties of its own to the `req` object, like [`req.wantsJSON`](https://sailsjs.com/documentation/reference/request-req/req-wants-json) and [`req.allParams()`](https://sailsjs.com/documentation/reference/request-req/req-all-params). These features are syntactic sugar on top of the underlying implementation, and also support both HTTP and WebSockets. <!-- ### Protocol Support The chart below describes support for the methods and properties on [`req`](https://sailsjs.com/documentation/reference/request-req), the Sails request object (`req`), across HTTP and WebSockets: | | HTTP | WebSockets | |--------------------------|---------|------------| | req.file() | :white_check_mark: | :white_large_square: | | req.param() | :white_check_mark: | :white_check_mark: | | req.route | :white_check_mark: | :white_check_mark: | | req.cookies | :white_check_mark: | :white_large_square: | | req.signedCookies | :white_check_mark: | :white_large_square: | | req.get() | :white_check_mark: | :white_large_square: | | req.accepts() | :white_check_mark: | :white_large_square: | | req.accepted | :white_check_mark: | :white_large_square: | | req.is() | :white_check_mark: | :white_large_square: | | req.ip | :white_check_mark: | :white_check_mark: | | req.ips | :white_check_mark: | :white_large_square: | | req.path | :white_check_mark: | :white_large_square: | | req.host | :white_check_mark: | :white_large_square: | | req.fresh | :white_check_mark: | :white_large_square: | | req.stale | :white_check_mark: | :white_large_square: | | req.xhr | :white_check_mark: | :white_large_square: | | req.protocol | :white_check_mark: | :white_check_mark: | | req.secure | :white_check_mark: | :white_large_square: | | req.session | :white_check_mark: | :white_check_mark: | | req.subdomains | :white_check_mark: | :white_large_square: | | req.method | :white_check_mark: | :white_check_mark: | | req.originalUrl | :white_check_mark: | :white_large_square: | | req.acceptedLanguages | :white_check_mark: | :white_large_square: | | req.acceptedCharsets | :white_check_mark: | :white_large_square: | | req.acceptsCharset() | :white_check_mark: | :white_large_square: | | req.acceptsLanguage() | :white_check_mark: | :white_large_square: | | req.isSocket | :white_check_mark: | :white_check_mark: | | req.allParams() | :white_check_mark: | :white_check_mark: | | req.transport | :white_large_square: | :white_check_mark: | | req.url | :white_check_mark: | :white_check_mark: | | req.wantsJSON | :white_check_mark: | :white_check_mark: | ### Legend - :white_check_mark: - fully supported - :white_large_square: - feature not yet implemented - :heavy_multiplication_x: - unsupported due to protocol restrictions --> <docmeta name="displayName" value="Request (`req`)"> <docmeta name="stabilityIndex" value="3"> ================================================ FILE: docs/reference/req/req.method.md ================================================ # `req.method` The request method (aka "verb"). ### Usage ```usage req.method; ``` ### Example If a client sends a POST request to `/product`: ```js req.method; // -> "POST" ``` ### Notes > + All requests to a Sails server have a "method", even via WebSockets (this is thanks to the request interpreter). <docmeta name="displayName" value="req.method"> <docmeta name="pageType" value="property"> ================================================ FILE: docs/reference/req/req.options/req.options.md ================================================ # req.options `req.options` is a dictionary (plain JavaScript object) of request-agnostic settings available in your app's actions. The purpose of `req.options` is to allow an action's code to access its configured route options, if there are any. (Simply put, "route options" are just any additional properties provided in a [route target](https://sailsjs.com/documentation/concepts/routes/custom-routes#?route-target).) <!-- FUTURE: pull out the rest of the content below to a new, separate page under **Concepts > Routes > Route options** and just link to it from in here rather than having all this exist inline. (Also be sure to consolidate any additional useful content from https://sailsjs.com/documentation/concepts/routes/custom-routes#?route-target-options into the new page, and replace the content under that heading with a sentence that links to the new "Route options" page.) -m Feb 23, 2017 --> ### With the blueprint API Route options in Sails were originally devised as a more flexible way to configure built-in blueprint actions. Some special settings must always be provided to [certain blueprint actions](https://sailsjs.com/documentation/reference/blueprint-api). This provides a way for your app to communicate which model/association a blueprint action should target. For example, `req.options.model` is the identity of the model that a particular blueprint action should target. And for blueprint actions that directly involve an association, `req.options.alias` indicates the name of the associating attribute. You can take advantage of this in your app order to bind a blueprint action to an arbitrary custom route. For example, consider the following custom route in [`config/routes.js`](https://sailsjs.com/documentation/anatomy/config/routes-js): ```js 'GET /foo/bar': { action: 'user/find', model: 'user' } ``` Whenever a GET request to /foo/bar arrives, the `find` blueprint action will run, and `req.options.model` will be available as `user`. (This is how the built-in, generic "find" blueprint action knows that it should communicate with the User model.) > Need to customize blueprint actions further? In most cases, the easiest (and most maintainable) way to do this is to write a custom action. If you're making the transition between the blueprint API and writing your own custom actions for the first time, you might start by checking out [Concepts > Actions & Controllers](https://sailsjs.com/documentation/concepts/actions-and-controllers). > > Note that there is a middle ground that allows you to programmatically modify some additional aspects of a blueprint action's behavior without overriding it completely (for example, examining the request to determine the criteria that a blueprint action uses when accessing models.) See [**Reference > sails.config.blueprints > Using parseBlueprintOptions**](https://sailsjs.com/documentation/reference/configuration/sails-config-blueprints#?using-parseblueprintoptions) for more on that. ### Custom route options it is also possible to configure and consume your own _custom_ route options. For example, imagine you're building a GitHub plugin for Sails. In order to provide support for handling webhook requests from GitHub, your plugin could register a generic, configurable action like `github/receive-event` that allows any user of your plugin to easily bind it to any route in their app: ```js 'POST /my-cool-webhooks/github/doings-and-things/incoming': { action: 'github/receive-event', } ``` But now, imagine that one of the purposes for your plugin's generic `receive-event` action is to save a record representing the incoming GitHub event to the app's database (e.g. to track it for future use). In order to do that, your generic action needs to know which model to use. So, using a simple approach that is consistent with Sails' built-in blueprint actions, your plugin could support usage like the following: ```js 'POST /my-cool-webhooks/github/doings-and-things/incoming': { action: 'github/receive-event', model: 'repoactivity' } ``` Meanwhile, in your plugin, the action you register might look something like this: ```js module.exports = function receiveEvent(req, res) { if (_.isUndefined(req.options.model) || !sails.models[req.options.model]) { return res.serverError(new Error('Invalid configuration: To use `github/receive-event`, please set this route's `model` to the identity of one of your app\'s models. (Currently, it is `'+req.options.model+'`, which cannot be used.)')); } var GitHubEventModel = sails.models[req.options.model]; GitHubEventModel.create({ raw: req.allParams(), githubId: req.param('id'), // ... // ... etc. (see https://developer.github.com/webhooks/#events) }).exec(function(err) { if (err) { return res.serverError(err); } return res.ok(); }); }; ``` > For more about creating this type of plugin, see [Concepts > Extending Sails > Hooks](TODO). <docmeta name="displayName" value="req.options"> <docmeta name="pageType" value="property"> ================================================ FILE: docs/reference/req/req.originalUrl.md ================================================ # `req.originalUrl` From the [Express docs](https://expressjs.com/en/4x/api.html#req.originalUrl): > This property is much like req.url; however, it retains the original request URL, allowing you to rewrite req.url freely for internal routing purposes. In almost all cases, you’ll want to use [`req.url`](https://sailsjs.com/documentation/reference/request-req/req-url) instead. In the rare cases where `req.url` is modified (for example, inside of a policy or middleware in order to redirect to an internal route), `req.originalUrl` will give you the URL that was originally requested. ```usage req.originalUrl; // => "/search" ``` <docmeta name="displayName" value="req.originalUrl"> <docmeta name="pageType" value="property"> ================================================ FILE: docs/reference/req/req.param.md ================================================ # `req.param()` Returns the value of the parameter with the specified name. ### Usage ```usage req.param(name[, defaultValue]); ``` ### Details `req.param()` searches the URL path, body, and query string of the request (_in that order_) for the specified parameter. If no parameter value exists anywhere in the request with the given `name`, it returns `undefined` or the optional `defaultValue` if specified. + URL path parameters ([`req.params`](https://sailsjs.com/documentation/reference/request-req/req-params)) + e.g. a request "/foo/4" to route `/foo/:id` has URL path params `{ id: 4 }` + body parameters ([`req.body`](https://sailsjs.com/documentation/reference/request-req/req-body)) + e.g. a request with a parseable body (e.g. JSON, URL-encoded, or XML) has body parameters equal to its parsed value + query string parameters ([`req.query`](https://sailsjs.com/documentation/reference/request-req/req-query)) + e.g. a request "/foo?email=5" has query params `{ email: 5 }` ### Example Consider a route (`POST /product/:sku`) that points to a custom action or policy that has the following code: ```javascript req.param('sku'); // -> 123 ``` We can get the expected result by sending the `sku` parameter any of the following ways: + `POST /product/123` + `POST /product?sku=123` + `POST /product` + with a JSON request body: `{ "sku": 123 }` ### Notes >+ The order of precedence means that URL path params will override request body params, which will override query string params. > + If you'd like to get ALL parameters from ALL sources (including the URL path, query string, and parsed request body) you can use [`req.allParams()`](https://sailsjs.com/documentation/reference/request-req/req-all-params). <docmeta name="displayName" value="req.param()"> <docmeta name="pageType" value="method"> ================================================ FILE: docs/reference/req/req.params.md ================================================ # `req.params` An object containing parameter values parsed from the URL path. For example if you have the route `/user/:name`, then the "name" from the URL path wil be available as `req.params.name`. This object defaults to `{}`. ### Usage ```usage req.params; ``` ### Notes > + When a route address is defined using a regular expression, each capture group match from the regex is available as `req.params[0]`, `req.params[1]`, etc. This strategy is also applied to unnamed wild-card matches in string routes such as `/file/*`. <docmeta name="displayName" value="req.params"> <docmeta name="pageType" value="property"> ================================================ FILE: docs/reference/req/req.path.md ================================================ # `req.path` The URL pathname from the [request URL string](http://nodejs.org/api/http.html#http_message_url) of the current request (`req`). Note that this is the part of the URL after and including the leading slash (e.g. `/foo/bar`), but without the query string (e.g. `?name=foo`) or fragment (e.g. `#foobar`.) ### Usage ```usage req.path; ``` ### Example Assuming a client sends the following request: > http://localhost:1337/donor/37?name=foo#foobar `req.path` will be defined as follows: ```js req.path; // -> "/donor/37" ``` ### Notes > + If you would like the URL query string _as well as_ the path, see [`req.url`](https://sailsjs.com/documentation/reference/request-req/req-url). <docmeta name="displayName" value="req.path"> <docmeta name="pageType" value="property"> ================================================ FILE: docs/reference/req/req.protocol.md ================================================ # `req.protocol` The protocol used to send this request (`req`). ### Usage ```usage req.protocol; ``` ### Example ```js switch (req.protocol) { case 'http': // this is an HTTP request break; case 'https': // this is a secure HTTPS request break; } ``` <docmeta name="displayName" value="req.protocol"> <docmeta name="pageType" value="property"> ================================================ FILE: docs/reference/req/req.query.md ================================================ # `req.query` A dictionary containing the parsed query-string, defaulting to `{}`. ### Usage ```usage req.query; ``` ### Example If the request is `GET /search?q=mudslide`: ```js req.query.q // -> "mudslide" ``` <docmeta name="displayName" value="req.query"> <docmeta name="pageType" value="property"> ================================================ FILE: docs/reference/req/req.secure.md ================================================ # `req.secure` Indicates whether or not the request was sent over a secure [TLS](http://en.wikipedia.org/wiki/Transport_Layer_Security) connection (i.e. `https://` or `wss://`). ### Usage ```usage req.secure; ``` <docmeta name="displayName" value="req.secure"> <docmeta name="pageType" value="property"> ================================================ FILE: docs/reference/req/req.setLocale.md ================================================ # `req.setLocale()` Override the inferred locale for this request. Normally, the locale is determined on a per-request basis based on incoming request headers (i.e. a user's browser or device language settings). This command overrides that setting for a particular request. ### Usage ```usage req.setLocale(override); ``` ### Example To allow users to specify their own language settings: ```js if (this.req.me.preferredLocale) { this.req.setLocale(this.req.me.preferredLocale); } return exits.success(); ``` Or, if you are not using the "Web app" template and/or actions2: ```js var me = await User.findOne({ id: req.session.userId }); if (me.preferredLocale) { req.setLocale(me.preferredLocale); } return res.view('pages/homepage'); ``` <docmeta name="displayName" value="req.setLocale()"> <docmeta name="pageType" value="method"> ================================================ FILE: docs/reference/req/req.setTimeout.md ================================================ # `req.setTimeout()` Time out this request if a response is not sent within the specified number of milliseconds. ### Usage ```usage req.setTimeout(numMilliseconds); ``` ### Example To cause requests to a particular action to time out after 4 minutes: ```js req.setTimeout(240000); ``` ### Notes + By default, normal HTTP requests to Node.js/Express/Sails.js apps time out [after 2 minutes](https://nodejs.org/dist/latest/docs/api/http.html#http_server_settimeout_msecs_callback) (120000 milliseconds) if a response is not sent. <docmeta name="displayName" value="req.setTimeout()"> <docmeta name="pageType" value="method"> ================================================ FILE: docs/reference/req/req.signedCookies.md ================================================ # `req.signedCookies` A dictionary containing all the signed cookies from the request object, where a signed cookie is one that is protected against modification by the client. This protection is provided by a Base64-encoded HMAC of the cookie value. When retrieving the cookie, if the HMAC signature does not match based on the cookie's value, then the cookie is not available as a member of the `req.signedCookies` object. ### Purpose A dictionary containing all of the signed cookies from this request (`req`). ### Usage ```usage req.signedCookies; ``` ### Example Adding a signed cookie named "chocolatechip" with value "Yummy: ```javascript res.cookie('chocolatechip', 'Yummy', {signed:true}); ``` Retrieving the cookie: ```javascript req.signedCookies.chocolatechip; // "Yummy" ``` <docmeta name="displayName" value="req.signedCookies"> <docmeta name="pageType" value="property"> ================================================ FILE: docs/reference/req/req.socket.md ================================================ # `req.socket` If the current request (`req`) originated from a connected Socket.IO client, `req.socket` refers to the raw Socket.IO socket instance. ### Usage ```usage req.socket; ``` ### Details > **Warning:** > > `req.socket` may be deprecated in a future release of Sails. You should use the [`sails.sockets.*`](https://sailsjs.com/documentation/reference/web-sockets/sails-sockets) methods instead. If the current request (`req`) did NOT originate from a Socket.IO client, `req.socket` does not have the same meaning. In the most common scenario—HTTP requests—`req.socket` _exists_, but it refers instead to the underlying TCP socket. Before using `req.socket`, you should check the [`req.isSocket`](https://sailsjs.com/documentation/reference/request-req/req-is-socket) flag to ensure the request arrived via a connected Socket.IO client. `req.socket.id` is a unique identifier representing the current socket. This is generated by the Socket.IO server when a client first connects and is a valid unique identifier until the socket is disconnected (if the client is a web browser, for example, `req.socket.id` would be valid until the user closes their browser tab). Sails also provides direct, low-level access to all other methods and properties of a Socket.IO `Socket`, including `req.socket` and its methods `req.socket.join`, `req.socket.leave`, `req.socket.broadcast`, etc. Check out the relevant [Socket.IO docs](https://socket.io/docs/rooms-and-namespaces/#Rooms) for more information. ### Example ```js if (req.isSocket) { // Low-level Socket.io methods and properties accessible on req.socket. // ... } else { // This is not a request from a Socket.io client, so req.socket // may or may not exist. If this is an HTTP request, req.socket is actually // the underlying TCP socket. // ... } ``` <docmeta name="displayName" value="req.socket"> <docmeta name="pageType" value="property"> ================================================ FILE: docs/reference/req/req.subdomains.md ================================================ # `req.subdomains` An array of all the subdomains in this request's URL. ### Usage ```usage req.subdomains; ``` ### Example If the requested URL was "https://ww3.staging.ibm.com": ```javascript req.subdomains; // -> ['ww3', 'staging'] ``` <docmeta name="displayName" value="req.subdomains"> <docmeta name="pageType" value="property"> ================================================ FILE: docs/reference/req/req.url.md ================================================ # `req.url` Like [`req.path`](https://sailsjs.com/documentation/reference/request-req/req-path), but it also includes the query string suffix. ```usage req.url; // => "/search?q=worlds%20largest%20dogs" ``` ### Notes > + It is worth mentioning that the URL fragment/hash (e.g. "#some/clientside/route") part of the URL is [not available on the server](https://github.com/strongloop/express/issues/1083#issuecomment-5179035). This is an [open issue with the current HTTP specification](http://stackoverflow.com/a/2305927/486547). As a result, if you write an action to redirect from one subdomain to another, for instance, you won't be able to peek at the URL fragment in that action. > + However, if you respond with a 302 redirect (i.e. `res.redirect()`), the user agent on the other end will preserve the URL fragment/hash and tack it on to the end of the new redirected URL. In many cases, this is exactly what you want! <docmeta name="displayName" value="req.url"> <docmeta name="pageType" value="property"> ================================================ FILE: docs/reference/req/req.wantsJSON.md ================================================ # `req.wantsJSON` A flag indicating whether the requesting client would prefer a JSON response (as opposed to some other format, like XML or HTML.) `req.wantsJSON` is used by all of the [built-in custom responses](https://sailsjs.com/documentation/anatomy/api/responses) in Sails. ### Usage ```usage req.wantsJSON; ``` ### Details The intended purpose of `req.wantsJSON` is to provide a clean, reusable indication of whether the server should respond with JSON or send back something else. It's not the right answer for _every_ content negotiation problem, but it is a simple, go-to solution for most use cases. For instance, all major browsers set an "Accept: text/plain;" request header for requests typed in the URL field. In this case, `req.wantsJSON` is false. For many other cases, though, the distinction is less clear. In those scenarios, Sails uses heuristics to determine the best value for `req.wantsJSON`. Technically, `req.wantsJSON` inspects the request's `"Content-type"`, `"Accepts"`, and `"X-Requested-With"` headers to determine whether the request expects a JSON response. If the information in these headers is too scanty, Sails errs on the side of JSON, and `req.wantsJSON` will be set to `true`. The benefit of `req.wantsJSON` is that it future-proofs your app and makes it less brittle. As best practices for content negotiation change over time (e.g. a new type of consumer device or enterprise user agent introduces a new header), Sails can patch `req.wantsJSON` at the framework level and modify the heuristics accordingly. It also reduces code duplication and saves you the annoyance of manually inspecting the headers in each of your routes. ### Example ```javascript if (req.wantsJSON) { sails.log('This request wants JSON!'); } else { // `req.wantsJSON` is falsy (undefined), to this request must not want JSON. } ``` ### Details Here is the specific order in which `req.wantsJSON` inspects the request. **If any of the following match, subsequent checks are ignored.** A request "wantsJSON" if: + it looks like an AJAX request + it's a virtual request from a socket + the request DOESN'T explicitly want HTML + the request has a "json" content type AND has its "Accept" header set + `req.options.wantsJSON` is truthy ### Notes > + Lower-level content negotiation is, of course, still possible using `req.is()`, `req.accepts()`, `req.xhr`, and `req.get()`. > + As of Sails v0.10, requests originating from a WebSocket client always want JSON. <docmeta name="displayName" value="req.wantsJSON"> <docmeta name="pageType" value="property"> ================================================ FILE: docs/reference/req/req.xhr.md ================================================ # `req.xhr` A flag indicating whether the current request (`req`) appears to be an AJAX request (i.e. it was issued with its "X-Requested-With" header set to "XMLHttpRequest"). ### Usage ```usage req.xhr; ``` ### Example ```javascript if (req.xhr) { // Yup, it's AJAX alright. } ``` ### Notes > + Whenever possible, you should prefer the `req.wantsJSON` flag. Avoid writing custom content negotiation logic into your app, as it makes your code more brittle and verbose. <docmeta name="displayName" value="req.xhr"> <docmeta name="pageType" value="property"> ================================================ FILE: docs/reference/res/res.attachment.md ================================================ # `res.attachment()` Indicate to a web browser or other user agent that an outgoing file download sent in this response should be "Saved as..." rather than "Opened", and optionally specify the name for the newly downloaded file on disk. Specifically, this sets the "Content-Disposition" header of the current response to "attachment". If a `filename` is given, then the "Content-Type" will be automatically set based on the extension of the file (e.g. `.jpg` or `.html`), and the "Content-Disposition" header will be set to "filename=`filename`". ### Usage ```usage res.attachment([filename]); ``` ### Example This method should be called prior to streaming down the bytes of your file. For example, if you're using the [uploads hook](https://www.npmjs.com/package/sails-hook-uploads) with [actions2](https://sailsjs.com/documentation/concepts/actions-and-controllers#?actions-2): ```js fn: async function({id}, exits) { var file = await LegalDoc.findOne({ id }); if(!file) { throw 'notFound'; } this.res.attachment(file.downloadName); var downloading = await sails.startDownload(file.uploadFd); return exits.success(downloading); } ``` That's it! When accessed in a browser, the file downloaded by this action will be saved as a new file (e.g. "Tax Return (Lerangis, 2019)") instead of being directly opened in the browser itself. Under the covers, `res.attachment()` isn't doing anything fancy, it just sets response headers: ```javascript res.attachment(); // -> response header will contain: // Content-Disposition: attachment ``` ```javascript res.attachment('Tax Return (Lerangis, 2019).pdf'); // -> response header will contain: // Content-Disposition: attachment; filename="Tax Return (Lerangis, 2019).pdf" // Content-Type: application/pdf ``` <docmeta name="displayName" value="res.attachment()"> <docmeta name="pageType" value="method"> ================================================ FILE: docs/reference/res/res.badRequest.md ================================================ # `res.badRequest()` This method is used to send a <a href="http://en.wikipedia.org/wiki/List_of_HTTP_status_codes#4xx_Client_Error" target="_blank">400</a> ("Bad Request") response back down to the client, indicating that the request is invalid. This usually means that the request contained invalid parameters or headers, or that it tried to do something not supported by your app logic. ### Usage ```usage return res.badRequest(); ``` _Or:_ + `return res.badRequest(data);` ### Details Like the other built-in custom response modules, the behavior of this method is customizable. By default, it works as follows: + The status code of the response is set to 400. + Sails sends any provided error `data` as JSON. If no `data` is provided, a default response body will be sent (the string `"Bad Request"`). ### Example ```javascript if ( req.param('amount') > 123 ) return res.badRequest( 'Transaction limit exceeded. Please try again with an amount less than $123.' ); } ``` ### Notes > + This method is **terminal**, meaning it is generally the last line of code your app should run for a given request (hence the advisory usage of `return` throughout these docs). >+ `res.badRequest()` (like other userland response methods) can be overridden or modified. It runs the response method defined in `api/responses/badRequest.js`. If a `badRequest.js` response method does not exist in your app, Sails will implicitly use the default behavior. >+ This method is called automatically by the [Blueprint Actions](https://sailsjs.com/documentation/concepts/blueprints/blueprint-actions) when bad parameters are sent with a request. <docmeta name="displayName" value="res.badRequest()"> <docmeta name="pageType" value="method"> ================================================ FILE: docs/reference/res/res.clearCookie.md ================================================ # `res.clearCookie()` Clears cookie (`name`) in the response. ### Usage ```usage res.clearCookie(name [,options]); ``` ### Details The path option defaults to "/". ### Example ```javascript res.cookie('name', 'tobi', { path: '/admin' }); res.clearCookie('name', { path: '/admin' }); ``` <docmeta name="displayName" value="res.clearCookie()"> <docmeta name="pageType" value="method"> ================================================ FILE: docs/reference/res/res.cookie.md ================================================ # `res.cookie()` Sets a cookie with name (`name`) and value (`value`) to be sent along with the response. ### Usage ```usage res.cookie(name, value [,options]); ``` ### Details The `path` option defaults to "/". `maxAge` is a convenience option that sets `expires` relative to the current time in milliseconds. ```javascript res.cookie('rememberme', '1', { maxAge: 900000, httpOnly: true }); ``` An object that is passed is then serialized as JSON, which is automatically parsed by the Express body-parser middleware. ```javascript res.cookie('cart', { items: [1,2,3] }); res.cookie('cart', { items: [1,2,3] }, { maxAge: 900000 }); ``` Signed cookies are also supported through this method—just pass the `signed` option, set to `true`. `res.cookie()` will then use the secret passed into `express.cookieParser(secret)` to sign the value. ```javascript res.cookie('name', 'tobi', { signed: true }); ``` ### Example ```javascript res.cookie('name', 'tobi', { domain: '.example.com', path: '/admin', secure: true }); res.cookie('rememberme', '1', { expires: new Date(Date.now() + 900000), httpOnly: true }); ``` <docmeta name="displayName" value="res.cookie()"> <docmeta name="pageType" value="method"> ================================================ FILE: docs/reference/res/res.forbidden.md ================================================ # `res.forbidden()` This method is used to send a <a href="http://en.wikipedia.org/wiki/List_of_HTTP_status_codes#4xx_Client_Error" target="_blank">403</a> ("Forbidden") response back down to the client, indicating that a request is not allowed. This usually means the user agent tried to do something it was not allowed to do, like change the password of another user. ### Usage ```usage return res.forbidden(); ``` ### Details Like the other built-in custom response modules, the behavior of this method is customizable. By default, it works as follows: + The status code of the response is set to 403. + A response body is sent with the string `"Forbidden"`. ### Example ```javascript if ( !req.session.userId ) { return res.forbidden(); } ``` ### Notes > + This method is **terminal**, meaning that it is generally the last line of code your app should run for a given request (hence the advisory usage of `return` throughout these docs). >+ `res.forbidden()` (like other userland response methods) can be overridden or modified. It runs the response method defined in `api/responses/forbidden.js`. If a `forbidden.js` response method does not exist in your app, Sails will use the default behavior. <docmeta name="displayName" value="res.forbidden()"> <docmeta name="pageType" value="method"> ================================================ FILE: docs/reference/res/res.get.md ================================================ # `res.get()` Returns the current value of the specified response header (`header`). ### Usage ```usage res.get(header); ``` ### Example ```javascript res.get('Content-Type'); // -> "text/plain" ``` ### Notes >+ The `header` argument is case-insensitive. >+ Response headers can be changed up until the response is sent. See [`res.set()`](https://sailsjs.com/documentation/reference/response-res/res-set) for details. <docmeta name="displayName" value="res.get()"> <docmeta name="pageType" value="method"> ================================================ FILE: docs/reference/res/res.json.md ================================================ # `res.json()` Sends a JSON response composed of the specified `data`. ### Usage ```usage return res.json(data); ``` ### Details When an object or array is passed to it, this method is identical to `res.send()`. Unlike `res.send()`, however, `res.json()` may also be used for explicit JSON conversion of non-objects (null, undefined, etc.), even though these are technically not valid JSON. ### Examples ```javascript return res.json({ firstName: 'Tobi' }); ``` ```javascript return res.status(201).json({ id: 201721 }); ``` ```javascript var leena = await User.findOne({ firstName: 'Leena' }); if (!leena) { return res.notFound(); } return res.json(leena.id);//« you can send down primitives, like numbers ``` ### Notes > + Don't forget that this method's name is all lowercase. > + This method is **terminal**, meaning that it is generally the last line of code your app should run for a given request (hence the advisory usage of `return` throughout these docs). <docmeta name="displayName" value="res.json()"> <docmeta name="pageType" value="method"> ================================================ FILE: docs/reference/res/res.jsonp.md ================================================ # `res.jsonp()` Send a JSON or JSONP response. Identical to [`res.json()`](https://sailsjs.com/documentation/reference/response-res/res-json) except that, if a request parameter named "callback" was provided in the query string, then Sails will send the response data as [JSONP](http://en.wikipedia.org/wiki/JSONP) instead of JSON. The value of the "callback" request parameter will be used as the name of the JSONP function call wrapper in the response. ### Usage ```usage return res.jsonp(data); ``` ### Example In an action: ```js return res.jsonp([ { name: 'Thelma', id: 1 }, { name: 'Leonardo' id: 2 } ]); ``` Given `?callback=gotStuff`, the code above would send back a response body like: ```javascript gotStuff([{name: 'Thelma', id: 1}, {name: 'Louise', id: 2}]) ``` ### Notes > + Don't forget that this method's name is all lowercase. > + If no "callback" request parameter was provided, this method works exactly like `res.json()`. > + This method is **terminal**, meaning that it is generally the last line of code your app should run for a given request (hence the advisory usage of `return` throughout these docs). <docmeta name="displayName" value="res.jsonp()"> <docmeta name="pageType" value="method"> ================================================ FILE: docs/reference/res/res.location.md ================================================ # `res.location()` Sets the "Location" response header to the specified URL expression (`url`). ### Usage ```usage res.location(url); ``` ### Example ```javascript res.location('/foo/bar'); res.location('foo/bar'); res.location('http://example.com'); res.location('../login'); res.location('back'); ``` ### Notes >+ You can use the same kind of URL expressions as in `res.redirect()`. <docmeta name="displayName" value="res.location()"> <docmeta name="pageType" value="method"> ================================================ FILE: docs/reference/res/res.md ================================================ # Response (`res`) ### Overview Sails is built on [Express](https://github.com/expressjs/) and uses [Node's HTTP server](http://nodejs.org/api/http.html#http_http_createserver_requestlistener) conventions. As a result, you can access all of the Node and Express methods and properties on the `res` object wherever it is accessible (i.e. in your actions, helpers, and policies). One of the benefits of this compatibility is that, in many cases, you can paste existing Node.js code into a Sails app and it will work. And since Sails implements a transport-agnostic request interpreter, the code in your Sails app is WebSocket-compatible as well. Sails adds a few methods of its own to the `res` object, like [`res.badRequest()`](https://sailsjs.com/documentation/reference/response-res/res-bad-request), [`res.serverError()`](https://sailsjs.com/documentation/reference/response-res/res-server-error), [`res.view()`](https://sailsjs.com/documentation/reference/response-res/res-view). These features are syntactic sugar on top of the underlying implementation, and support both HTTP _and_ (in many cases) WebSockets. <!-- ### Protocol Support The chart below describes support for the methods and properties on the Sails Response object (`res`) across multiple transports: | | HTTP | WebSockets | |----------------|---------|------------| | res.status() | :white_check_mark: | :white_check_mark: | | res.set() | :white_check_mark: | :white_large_square: | | res.get() | :white_check_mark: | :white_large_square: | | res.cookie() | :white_check_mark: | :white_large_square: | | res.clearCookie() | :white_check_mark: | :white_large_square: | | res.redirect() | :white_check_mark: | :white_check_mark: | | res.location() | :white_check_mark: | :white_large_square: | | res.charset | :white_check_mark: | :white_check_mark: | | res.send() | :white_check_mark: | :white_check_mark: | | res.json() | :white_check_mark: | :white_check_mark: | | res.jsonp() | :white_check_mark: | :white_check_mark: | | res.type() | :white_check_mark: | :white_large_square: | | res.format() | :white_check_mark: | :white_large_square: | | res.attachment() | :white_check_mark: | :white_large_square: | | res.sendfile() | :white_check_mark: | :white_large_square: | | res.download() | :white_check_mark: | :white_large_square: | | res.links() | :white_check_mark: | :white_large_square: | | res.locals | :white_check_mark: | :white_check_mark: | | res.render() | :white_check_mark: | :white_large_square: | | res.view() | :white_check_mark: | :white_large_square: | ### Legend - :white_check_mark: - fully supported - :white_large_square: - feature not yet implemented - :heavy_multiplication_x: - unsupported due to protocol restrictions --> <docmeta name="displayName" value="Response (`res`)"> <docmeta name="stabilityIndex" value="3"> ================================================ FILE: docs/reference/res/res.negotiate.md ================================================ # `res.negotiate()` > _**This method is deprecated**._ > > You should use a [custom response](https://sailsjs.com/documentation/concepts/extending-sails/custom-responses) instead. > > To handle errors from [Waterline model methods](https://sailsjs.com/documentation/reference/waterline-orm/models), check the `name` property of the error (see the [Waterline error reference](https://sailsjs.com/documentation/concepts/models-and-orm/errors) for more details). Given an error (`err`), attempt to guess which error response should be called (`badRequest`, `forbidden`, `notFound`, or `serverError`) by inspecting the `status` property. If `err` is not a dictionary, or the `status` property does not match a known HTTP status code, then default to `serverError`. Especially handy for handling potential validation errors from [Model.create()](https://sailsjs.com/documentation/reference/waterline-orm/models/create) or [Model.update()](https://sailsjs.com/documentation/reference/waterline-orm/models/update). ### Usage ```usage return res.negotiate(err); ``` ### Details Like the other built-in custom response modules, the behavior of this method is customizable. `res.negotiate()` examines the provided error (`err`) and determines the appropriate error-handling behavior from one of the following methods: + [`res.badRequest()`](https://sailsjs.com/documentation/reference/response-res/res-bad-request) (400) + [`res.forbidden()`](https://sailsjs.com/documentation/reference/response-res/res-forbidden) (403) + [`res.notFound()`](https://sailsjs.com/documentation/reference/response-res/res-not-found) (404) + [`res.serverError()`](https://sailsjs.com/documentation/reference/response-res/res-server-error) (500) The determination is made based on `err`'s "status" property. If a more specific diagnosis cannot be determined (e.g. `err` doesn't have a "status" property, or it's a string), Sails will default to `res.serverError()`. ### Example ```javascript // Add Fido's birthday to the database: Pet.update({name: 'fido'}) .set({birthday: new Date('01/01/2010')}) .exec(function (err, fido) { if (err) return res.negotiate(err); return res.ok(fido); }); ``` ### Notes > + This method is **terminal**, meaning it is generally the last line of code your app should run for a given request (hence the advisory usage of `return` throughout these docs). >+ `res.negotiate()` (like other userland response methods) can be overridden - just define a response module (`/responses/negotiate.js`) and export a function definition. >+ This method is used as the default handler for uncaught errors in Sails. That means it is called automatically if an error is thrown in _any_ request handling code, _but only within the initial step of the event loop_. You should always specifically handle errors that might arise in callbacks/promises from asynchronous code. <docmeta name="isDeprecated" value="true"> <docmeta name="displayName" value="res.negotiate()"> <docmeta name="pageType" value="method"> ================================================ FILE: docs/reference/res/res.notFound.md ================================================ # res.notFound() This method is used to send a <a href="http://en.wikipedia.org/wiki/List_of_HTTP_status_codes#4xx_Client_Error" target="_blank">404</a> ("Not Found") response using either [res.json()](https://sailsjs.com/documentation/reference/response-res/res-json) or [res.view()](https://sailsjs.com/documentation/reference/response-res/res-view). It is called automatically when Sails receives a request that doesn't match any of its explicit routes or route blueprints (i.e. serves the 404 page). When called manually from your app code, this method is normally used to indicate that the user agent tried to find, update, or delete something that doesn't exist. ### Usage ```usage return res.notFound(); ``` ### Details Like the other built-in custom response modules, the behavior of this method is customizable. By default, it works as follows: + The status code of the response will be set to 404. + If the request "[wants JSON](https://sailsjs.com/documentation/reference/request-req/req-wants-json)" (e.g. the request originated from AJAX, WebSockets, or a REST client like cURL), Sails will send a response body with the string `"Not Found"`. + If the request _does not_ "want JSON" (e.g. a URL typed into a web browser), Sails will attempt to serve the view located at `views/404.ejs` (assuming the default EJS [view engine](https://sailsjs.com/documentation/concepts/views/view-engines)). If no such view is found, or an error occurs attempting to serve it, a default response body will be sent with the string `"Not Found"`. ### Example ```javascript Pet.findOne() .where({ name: 'fido' }) .exec(function(err, fido) { if (err) return res.serverError(err); if (!fido) return res.notFound(); // ... }) ``` ### Notes > + This method is **terminal**, meaning that it is generally the last line of code your app should run for a given request (hence the advisory usage of `return` throughout these docs). >+ `res.notFound()` (like other userland response methods) can be overridden or modified. It runs the response method defined in `api/responses/notFound.js`. If a `notFound.js` response method does not exist in your app, Sails will use the default behavior. <docmeta name="displayName" value="res.notFound()"> <docmeta name="pageType" value="method"> ================================================ FILE: docs/reference/res/res.ok.md ================================================ # `res.ok()` This method is used to send a <a href="https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#2xx_Success" target="_blank">200</a> ("OK") response back down to the client. ### Usage ```usage return res.ok(); ``` _Or:_ + `return res.ok(data);` ### Details Like the other built-in custom response modules, the behavior of this method is customizable. By default, it works as follows: + The status code of the response will be set to 200. + Sails will send any provided error `data` as JSON. If no `data` is provided, a default response body will be sent (the string `"OK"`). ### Example ```javascript return res.ok(); ``` ### Notes > + This method is **terminal**, meaning that it is generally the last line of code your app should run for a given request (hence the advisory usage of `return` throughout these docs). >+ `res.ok()` (like other userland response methods) can be overridden or modified. It runs the response method defined in `api/responses/ok.js`. If an `ok.js` response method does not exist in your app, Sails will use the default behavior. <docmeta name="displayName" value="res.ok()"> <docmeta name="pageType" value="method"> ================================================ FILE: docs/reference/res/res.redirect.md ================================================ # `res.redirect()` Redirect the requesting user agent to the given absolute or relative URL. ### Usage ```usage return res.redirect(url); ``` _Or:_ + `return res.redirect(statusCode, url);` ### Arguments | | Argument | Type | Details | |---|----------------|:-----------:|---------| | 1 | _statusCode_ | ((number?)) | An optional status code (e.g. 301). (If omitted, a status code of 302 will be assumed.) | 2 | url | ((string)) | A URL expression (see below for complete specification).<br/> e.g. `"http://google.com"` or `"/login"` ### Details Sails/Express support a few forms of redirection: + A fully qualified URI for redirecting to a different domain: ```javascript return res.redirect('http://google.com'); ``` + The domain-relative redirect. For example, if you were on http://example.com/admin/post/new, the following redirect to `/checkout` would land you at http://example.com/checkout: ```javascript return res.redirect('/checkout'); ``` + Pathname-relative redirects. If you were on http://example.com/admin/post/new, the following redirect would land you at http//example.com/admin/post: ```javascript return res.redirect('..'); ``` + A back redirect, which allows you to redirect a request back from whence it came from using the "Referer" (or "Referrer") header (if omitted, redirects to `/` by default): ```javascript return res.redirect('back'); ``` ### Notes > + This method is **terminal**, meaning that it is generally the last line of code your app should run for a given request (hence the advisory usage of `return` throughout these docs). > + As of Sails v1.x, for HTTP requests, `res.redirect()` [does not respect the status code established by `res.status()`](https://github.com/balderdashy/sails-docs/pull/796#issuecomment-284224746). Thanks [@Guillaume-Duval](https://github.com/Guillaume-Duval) and [@oshatrk](https://github.com/oshatrk)! > + When your app calls `res.redirect()`, Sails sends a response with status code [302](http://en.wikipedia.org/wiki/List_of_HTTP_status_codes#3xx_Redirection), indicating a temporary redirect. This instructs the user agent to send a new request to the indicated URL. There is no way to _force_ a user agent to follow redirects, but most clients play nicely. > + In general, you should not need to use `res.redirect()` if a request "wants JSON" (i.e. [`req.wantsJSON`](https://sailsjs.com/documentation/reference/request-req/req-wants-json)). > + If a request originated from the Sails socket client, it always "wants JSON", so the [Sails socket client](https://sailsjs.com/documentation/reference/web-sockets/socket-client) does _not_ follow redirects. For this reason, if an action is called via a WebSocket request using (for example) [`io.socket.get()`](https://sailsjs.com/documentation/reference/web-sockets/socket-client/io-socket-get), it will simply receive the appropriate status code and a "Location" header indicating the location of the desired resource. It’s up to the client-side code to decide how to handle redirects for WebSocket requests. <docmeta name="displayName" value="res.redirect()"> <docmeta name="pageType" value="method"> ================================================ FILE: docs/reference/res/res.send.md ================================================ # `res.send()` Send a string response in a format other than JSON (XML, CSV, plain text, etc.). This method is used in the underlying implementation of most of the other terminal response methods. ### Usage ```usage return res.send([string]); ``` ### Details This method can be used to send a string of XML. If no argument is provided, no response body is sent back—just the status code. ### Examples To allow users to export their own data, while complying with Europe's GDPR regulations, you might send back some dynamic CSV-formatted data, like this: ```javascript // Send back some dynamic CSV-formatted data. return res.set('text/csv').send(` some,csv,like,this or,,like,this `); ``` Or, to respond with XML (e.g. for a sitemap): ```javascript // Send down some dynamic XML-formatted data. return res.set('application/xml').send(`<?xml version="1.0" encoding="UTF-8"?> <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"> <url> <loc>http://sailsjs.com</loc> <lastmod>2018-03-28T17:02:23.688Z</lastmod> <changefreq>monthly</changefreq> </url> </urlset> `); ``` You can also send arbitrary plain text and use any status code you like: ```javascript // You can use any status code you like. // (Defaults to 200 unless you specify something else.) return res.status(420).send('Hello world!'); ``` ### Notes > + This method is **terminal**, meaning that it's generally the last line of code your app should run for a given request (hence the advisory usage of `return` throughout these docs). > + If you want to send a dictionary or JSON, use [`res.json()`](https://sailsjs.com/documentation/reference/response-res/res-json). > + If you want to send a stream, use [actions2](https://sailsjs.com/documentation/concepts/actions-and-controllers)(preferably) or `.pipe(res)` (if you absolutely must). > + If you want to send a custom status code, call [`req.status()`](https://sailsjs.com/documentation/reference/response-res/res-status) first. <docmeta name="displayName" value="res.send()"> <docmeta name="pageType" value="method"> ================================================ FILE: docs/reference/res/res.serverError.md ================================================ # `res.serverError()` This method is used to send a <a href="http://en.wikipedia.org/wiki/List_of_HTTP_status_codes#5xx_Server_Error" target="_blank">500</a> ("Server Error") response back down to the client, indicating that some kind of server error occurred (i.e. the error is not the requesting user agent's fault). ### Usage ```usage return res.serverError(err); ``` _Or:_ + `return res.serverError();` ### Details Like the other built-in custom response modules, the behavior of this method is customizable. By default, it works as follows: + The status code of the response will be set to 500. + If the request "[wants JSON](https://sailsjs.com/documentation/reference/request-req/req-wants-json)" (e.g. the request originated from AJAX, WebSockets, or a REST client like cURL), Sails will send the provided error `data` as JSON. If no `data` is provided, a default response body will be sent (the string `"Internal Server Error"`). + If the request _does not_ "want JSON" (e.g. a URL typed into a web browser), Sails will attempt to serve the view located at `views/500.ejs` (assuming the default EJS [view engine](https://sailsjs.com/documentation/concepts/views/view-engines)). If no such view is found, or an error occurs attempting to serve it, a default response body will be sent with the string `"Internal Server Error"`. ### Example ```javascript return res.serverError('Salesforce could not be reached'); ``` ### Notes > + This method is **terminal**, meaning that it is generally the last line of code your app should run for a given request (hence the advisory usage of `return` throughout these docs). >+ `res.serverError()` (like other userland response methods) can be overridden or modified. It runs the response method defined in `api/responses/serverError.js`. If a `serverError.js` response method does not exist in your app, Sails will use the default behavior. >+ The specified `data` **will be excluded from the JSON response and view locals** if the app is running in the "production" environment (i.e. `process.env.NODE_ENV === 'production'`). <docmeta name="displayName" value="res.serverError()"> <docmeta name="pageType" value="method"> ================================================ FILE: docs/reference/res/res.set.md ================================================ # `res.set()` Sets specified response header (`header`) to the specified value (`value`). Alternatively, you can pass in a single object argument (`headers`) to set multiple header fields at once, where the keys are the header field names and the corresponding values are the desired values. ### Usage ```usage res.set(header, value); ``` -or- ```usage res.set(headers); ``` ### Example ```javascript res.set('Content-Type', 'text/plain'); res.set({ 'Content-Type': 'text/plain', 'Content-Length': '123', 'ETag': '12345' }) ``` <docmeta name="displayName" value="res.set()"> <docmeta name="pageType" value="method"> ================================================ FILE: docs/reference/res/res.status.md ================================================ # `res.status()` Set the status code of this response. ### Usage ```usage res.status(statusCode); ``` ### Example ```javascript res.status(418); res.send('I am a teapot'); ``` ### Notes >+ The status code may be set up until the response is sent. >+ `res.status()` is effectively just a chainable alias of Node's `res.statusCode = …;`. <docmeta name="displayName" value="res.status()"> <docmeta name="pageType" value="method"> ================================================ FILE: docs/reference/res/res.type.md ================================================ # `res.type()` Sets the "Content-Type" response header to the specified `type`. This method is pretty forgiving (see examples below), but note that if `type` contains a `"/"`, `res.type()` assumes it is a MIME type and interprets it literally. ### Usage ```usage res.type(type); ``` ### Example ```javascript res.type('.html'); res.type('html'); res.type('json'); res.type('application/json'); res.type('png'); ``` <docmeta name="displayName" value="res.type()"> <docmeta name="pageType" value="method"> ================================================ FILE: docs/reference/res/res.view.md ================================================ # `res.view()` Respond with an HTML page. ### Usage ```usage return res.view(pathToView, locals); ``` _Or:_ + `return res.view(pathToView);` + `return res.view(locals);` + `return res.view();` Uses the [configured view engine](https://sailsjs.com/documentation/concepts/views/view-engines) to compile the [view template](https://sailsjs.com/documentation/concepts/views/partials) at `pathToView` into HTML. If `pathToView` is not provided, serves the conventional view based on the current controller and action. The specified [`locals`](https://sailsjs.com/documentation/concepts/views/locals) are merged with your configured app-wide locals, as well as certain built-in locals from Sails and/or your view engine, then passed to the view engine as data. ### Arguments | | Argument | Type | Details | |---|----------------|:-----------:|---------| | 1 | pathToView | ((string)) | The path to the desired view file relative to your app's [`views` folder](https://sailsjs.com/documentation/anatomy/views) (usually `views/`), without the file extension (e.g. `.ejs`), and with no trailing slash.<br/>Defaults to "identityOfController/nameOfAction". | 2 | locals | ((dictionary)) | Data to pass to the view template. These explicitly specified locals will be merged in to Sails' [built-in locals](https://sailsjs.com/documentation/concepts/views/locals) and your [configured app-wide locals](https://github.com/balderdashy/sails/blob/master/docs/PAGE_NEEDED.md).<br/>Defaults to `{}`. ### Example Consider a conventionally configured Sails app with a call to `res.view()` in the `cook()` action of its `OvenController.js`. With no `pathToView` argument, `res.view()` will decide the path by combining the identity of the controller (`oven`) and the name of the action (`cook`): ```js return res.view(); // -> responds with `views/oven/cook.ejs` ``` Here's how you would load the same view using an explicit `pathToView`: ```js return res.view('oven/cook'); // -> responds with `views/oven/cook.ejs` ``` Finally, here's a more involved example demonstrating how `res.view` can be combined with Waterline queries: ```js // Find the 5 hottest oven brands on the market Oven.find().sort('heat ASC').exec(function (err, ovens){ if (err) return res.serverError(err); return res.view('oven/top5', { hottestOvens: ovens }); // -> responds using the view at `views/oven/top5.ejs`, // and with the oven data we looked up as view locals. // // e.g. in the view, we might have something like: // ... // <% _.each(hottestOvens, function (aHotOven) { %> // <li><%= aHotOven.name %></li> // <% }) %> // ... }); ``` ### Notes > + This method is **terminal**, meaning that it is generally the last line of code your app should run for a given request (hence the advisory usage of `return` throughout these docs). > + `res.view()` reads a view file from disk, compiles it into HTML, then streams it back to the client. If you already have the view in memory, or don't want to stream the compiled HTML directly back to the client, use `sails.hooks.views.render()` instead. > + `res.view()` always looks for the _lowercased_ version of a view filename. For example, if your controller is `FooBarController` and your action is `Baz`, `res.view()` will attempt to find `views/foobar/baz.ejs`. On _case-sensitive_ filesystems (e.g. Ubuntu Linux), this can lead to unexpected errors when locating views if they are saved with capital letters. For this reason, it is recommended that you always save your views and view folders in lowercase. <docmeta name="displayName" value="res.view()"> <docmeta name="pageType" value="method"> ================================================ FILE: docs/reference/sails.config/miscellaneous.md ================================================ # Miscellaneous (`sails.config.*`) For a conceptual overview of configuration in Sails, see https://sailsjs.com/documentation/concepts/Configuration. This page is a quick reference of assorted configuration topics that don't fit elsewhere, namely top-level properties on the sails.config object. Many of these properties are best set on a [per-environment basis](https://sailsjs.com/documentation/anatomy/my-app/config/env), or in your [config/local.js](https://sailsjs.com/documentation/concepts/configuration/the-local-js-file). To set them globally for your app, create a new file in the `config` folder (e.g. `config/misc.js`) and add them there. ### `sails.config.port` The `port` setting determines which <a href="http://en.wikipedia.org/wiki/Port_(computer_networking)">TCP port</a> your Sails app will use to listen for incoming requests. Ports are a [transport-layer](https://en.wikipedia.org/wiki/Transport_layer) concept designed to allow many different networking applications to run at the same time on a single computer. By default, if it’s set, Sails uses the port configured in your app (`sails.config.port`). If not, it checks to see if the `PORT` environment variable is set, and uses that if possible. Otherwise it falls back to port 1337. > In production, you will probably want Sails to listen on port 80 (or 443, if you have an SSL certificate and are serving your site via `https://`), but depending on where your app is deployed, you may or may not need to actually modify this setting. For example, if you are deploying behind a proxy, or to a PaaS like [Heroku](http://heroku.com), [Azure App Service](https://azure.microsoft.com/en-us/services/app-service/), or [Deis](http://deis.io/), you probably won't need to configure `sails.config.port`, since in most cases that's handled automatically. For more guidance and tips related to deploying, scaling, and maintaining Sails in production, see [Concepts > Deployment](https://sailsjs.com/documentation/concepts/deployment). ### `sails.config.explicitHost` By default, Sails will assume `localhost` as the host that will be listening for incoming requests. This will work in the majority of hosting environments you encounter, but in some cases ([OpenShift](http://www.openshift.com) being one example) you'll need to explicitly declare the host name of your Sails app. Setting `explicitHost` tells Sails to listen for requests on that host instead of `localhost`. ### `sails.config.environment` The runtime “environment” of your Sails app is usually either `development` or `production`. In development, your Sails app will go out of its way to help you (for instance you will receive more descriptive error and debugging output). In production, Sails configures itself (and its dependencies) to optimize performance. You should always put your app in production mode before you deploy it to a server; this helps ensure that your Sails app remains stable, performant, and scalable. #### Using the "production" environment By default, Sails determines its environment using the `NODE_ENV` environment variable. If `NODE_ENV` is not set, Sails will look to see if you provided a `sails.config.environment` setting, and use it if possible. Otherwise, it runs in the development environment. When you lift your app with the `NODE_ENV` environment variable set to `production`, Sails automatically sets `sails.config.environment` to `production` too. This is the recommended way of switching to production mode. We don't usually recommend configuring `sails.config.environment` manually, since some of Sails’ dependencies rely on the `NODE_ENV` environment variable, and it is automatically set by most Sails/Node.js hosting services. If you attempt to lift a Sails app in the production environment _without_ setting `NODE_ENV` to `production` (for example, by running `sails lift --prod`), Sails automatically sets `NODE_ENV` to `production` for you. If you attempt to lift a Sails app in production while `NODE_ENV` is set to a _different_ value (for example `NODE_ENV=development sails lift --prod`), the app fails to start. > For more background on configuring your Sails app for production, see [Concepts > Deployment](https://sailsjs.com/documentation/concepts/deployment). Note that it is perfectly valid to set `sails.config.environment` to something else entirely, like "staging", while still setting `NODE_ENV=production`. This causes Sails to load a different environment-specific configuration file (e.g. `config/env/staging.js`) and Grunt task (e.g. `tasks/register/staging.js`), while still otherwise acting like it's in production. ### `sails.config.hookTimeout` A time limit, in milliseconds, imposed on all hooks in your app. Sails will give up if any hook takes longer than this to load. Defaults to `20000` (20 seconds). > The most common reason to change this setting is to tolerate slow production Grunt tasks. For example, if your app is using uglify, and you have lots and lots of client-side JavaScript files in your assets folder, then you might need Sails to wait longer than 20 seconds to compile all of those client-side assets. For more tips about the production asset pipeline, see [Concepts > Deployment](https://sailsjs.com/documentation/concepts/deployment). ### `sails.config.ssl` SSL/TLS (transport-layer security) is critical for preventing potential man-in-the-middle attacks. Without a protocol like SSL/TLS, web basics like securely transmitting login credentials and credit card numbers would be much more complicated and troublesome. SSL/TLS is not only important for HTTP requests (`https://`), it's also necessary for WebSockets (over `wss://`). Fortunately, you only need to worry about configuring SSL settings in one place: `sails.config.ssl`. > ##### SSL and load balancers > > The `sails.config.ssl` setting is only relevant if you want your _Sails process_ to manage SSL. This isn't always the case. For example, if you expect your Sails app to get more traffic over time, it will need to scale to multiple servers, necessitating a load balancer. Most of the time, for performance and simplicity, it is a good idea to terminate SSL at your load balancer. If you do that, then since SSL/TLS will have already been dealt with _before packets reach your Sails app_, you won't need to use the `sails.config.ssl` setting at all. (This is also true if you're using a PaaS like Heroku, or almost any other host with a built-in load balancer.) > > If you're satisfied that this configuration setting applies to your app, then please continue below for more details. Use `sails.config.ssl` to set up basic SSL server options, or to indicate that you will be specifying more advanced options in [sails.config.http.serverOptions](https://sailsjs.com/documentation/reference/configuration/sails-config-http#?properties). If you specify a dictionary, it should contain both `key` _and_ `cert` keys, _or_ a `pfx` key. The presence of those options indicates to Sails that your app should be lifted with an HTTPS server. If your app requires a more complex SSL setup (for example by using [SNICallback](https://nodejs.org/api/tls.html#tls_tls_createserver_options_secureconnectionlistener)), set `sails.config.ssl` to `true` and specify your advanced options in [sails.config.http.serverOptions](https://sailsjs.com/documentation/reference/configuration/sails-config-http#?properties). #### SSL configuration example For this example, we'll assume you created a folder in your project, `config/ssl/` and dumped your certificate/key files inside. Then, in one of your config files, include the following: ```javascript // Assuming this is in `config/env/production.js`, and your folder of SSL cert/key files is in `config/ssl/`: ssl: { ca: require('fs').readFileSync(require('path').resolve(__dirname,'../ssl/my-gd-bundle.crt')), key: require('fs').readFileSync(require('path').resolve(__dirname,'../ssl/my-ssl.key')), cert: require('fs').readFileSync(require('path').resolve(__dirname,'../ssl/my-ssl.crt')) } ``` <docmeta name="displayName" value="sails.config.*"> ================================================ FILE: docs/reference/sails.config/sails.config.blueprints.md ================================================ # `sails.config.blueprints` These configurable settings allow you to configure the blueprint API in Sails. Some settings (like `sails.config.blueprints.autoWatch`) control the behavior of built-in [blueprint actions](https://sailsjs.com/documentation/concepts/blueprints/blueprint-actions), whereas others (like `sails.config.blueprints.shortcuts`) tweak the behavior of implicit [blueprint routing](https://sailsjs.com/documentation/concepts/blueprints/blueprint-actions) and/or determine whether Sails automatically binds certain kinds of blueprint routes at all. > Remember, blueprint actions can be attached to your custom routes _regardless of whether or not_ you have any kind of implicit blueprint routing enabled. ### Properties ##### Route-related settings | Property | Type | Default | Details | |:------------|:----------:|:----------|:--------| | `actions`| ((boolean))|`false`| Whether implicit blueprint ("shadow") routes are automatically generated for every action in your app. e.g. having an `api/controllers/foo/bar.js` file or a `bar` function in `api/controllers/FooController.js` would automatically route incoming requests to `/foo/bar` to that action, as long as it is not overridden by a [custom route](https://sailsjs.com/documentation/concepts/routes/custom-routes). When enabled, this setting _also_ binds additional, special implicit ("shadow") routes to any actions named `index`, and for the relative "root" URL for your app and each of its controllers. For example, a `/foo` shadow route for `api/controllers/foo/index.js`, or a `/` shadow route for `api/controllers/index.js`. |`rest`|((boolean))|`true`|Automatic REST blueprints enabled? e.g. `'get /:model/:id?'` `'post /:model'` `'put /:model/:id'` `'delete /:model/:id'`. |`shortcuts`|((boolean))|`true`|These CRUD shortcuts exist for your convenience during development, but you'll want to disable them in production.: `'/:model/find/:id?'`, `'/:model/create'`, `'/:model/update/:id'`, and `'/:model/destroy/:id'`. | `prefix`     | ((string))| `''`     | Optional mount path prefix (e.g. '/api/v2') for all [blueprint routes](https://sailsjs.com/documentation/concepts/blueprints/blueprint-routes), including `rest`, `actions`, and `shortcuts`. This only applies to implicit blueprint ("shadow") routes, not your [custom routes](https://sailsjs.com/documentation/concepts/routes/custom-routes). | `restPrefix` | ((string))| `''` | Optional mount path prefix for all REST blueprint routes on a controller, e.g. '/api/v2'. (Does not include `actions` and `shortcuts` routes.) This allows you to take advantage of REST blueprint routing, even if you need to namespace your RESTful API methods. Will be joined to your `prefix` config, e.g. `prefix: '/api'` and `restPrefix: '/rest'`. RESTful actions will be available under `/api/rest`. |`pluralize`|((boolean))|false| Whether to use plural model names in blueprint routes, e.g. `/users` for the `User` model. (This only applies to blueprint autoroutes, not manual routes from `sails.config.routes`.) ##### Action-related settings | Property | Type | Default | Details | |:------------|:----------:|:----------|:--------| |`autoWatch`|((boolean))|`true`| Whether to subscribe the requesting socket in the `find` and `findOne` blueprint action to notifications about newly _created_ records via the blueprint API. |`parseBlueprintOptions`|((function))|(See below)|Provide this function in order to override the default behavior for blueprint actions (including search criteria, skip, limit, sort and population). ##### Using `parseBlueprintOptions` Each blueprint action includes, at its core, a Waterline model method call. For instance, the `find` blueprint, when run for the `User` model, runs `User.find()` in order to retrieve some user records. The options that are passed to these Waterline methods are determined by a call to `parseBlueprintOptions()`. The default version of this method (available via `sails.hooks.blueprints.parseBlueprintOptions()`) determines the default behaviors for blueprints. You can override `parseBlueprintOptions` in your [blueprints config](https://sailsjs.com/documentation/reference/configuration/sails-config-blueprints) (in [`config/blueprints.js`](https://sailsjs.com/documentation/anatomy/config/blueprints.js)) to customize the behavior for _all_ blueprint actions, or on a [per-route basis](https://sailsjs.com/documentation/concepts/routes/custom-routes#?route-target-options) to customize the behavior for a single route. The `parseBlueprintOptions()` method takes a single argument (the [request object](https://sailsjs.com/documentation/reference/request-req)) and is expected to return a dictionary of Waterline query options. (You can review an unrealistically-expanded example of a such a dictionary [here](https://gist.github.com/mikermcneil/1b87af6b6a8458254deb83a6d1cf264f), but keep in mind that not all keys apply to all blueprint actions. See [source code in Sails code](https://github.com/balderdashy/sails/tree/v1.2.2/lib/hooks/blueprints/actions) for complete details). Adding your own `parseBlueprintOptions()` is an advanced concept, and it is recommended that you first familiarize yourself with the [default method code](https://github.com/balderdashy/sails/blob/v1.2.2/lib/hooks/blueprints/parse-blueprint-options.js) and use it as a starting point. For small modifications to blueprint behavior, it is best to first call the default method inside your override and then make changes to the returned query options: ```js parseBlueprintOptions: function(req) { // Get the default query options. var queryOptions = req._sails.hooks.blueprints.parseBlueprintOptions(req); // If this is the "find" or "populate" blueprint action, and the normal query options // indicate that the request is attempting to set an exceedingly high `limit` clause, // then prevent it (we'll say `limit` must not exceed 100). if (req.options.blueprintAction === 'find' || req.options.blueprintAction === 'populate') { if (queryOptions.criteria.limit > 100) { queryOptions.criteria.limit = 100; } } return queryOptions; } ``` <docmeta name="displayName" value="sails.config.blueprints"> <docmeta name="pageType" value="property"> ================================================ FILE: docs/reference/sails.config/sails.config.bootstrap.md ================================================ # `sails.config.bootstrap` ### What is this? `sails.config.bootstrap` is a customizable seed function that runs before your Sails app is lifted (i.e. starts up). By convention, this function is used for: + setting up baseline data + _e.g. find or create an admin user_ + running sanity checks on the status of your database + _e.g. count hand records that don't have any fingers. If any are found, then refuse to lift until the database is fixed_ + seeding your database with stub data + _e.g. create & associate a few fake "Clinic", "Pet", and "Veterinarian" records to make it easier to test your animal adoption app_ For an example bootstrap function, generate a new Sails app and have a look at [`config/bootstrap.js`](https://sailsjs.com/documentation/anatomy/config/bootstrap.js). ### Notes > - Sails will log a warning if the bootstrap function is "taking too long". If your bootstrap function is taking longer to run than the default timeout of 30 seconds and you would like to prevent the warning from being displayed, you can stall it by configuring `sails.config.bootstrapTimeout` to a larger number of milliseconds. (For example, you can increase the timeout to one minute by using `60000`.) <docmeta name="displayName" value="sails.config.bootstrap()"> <docmeta name="pageType" value="property"> ================================================ FILE: docs/reference/sails.config/sails.config.connections.md ================================================ # `sails.config.datastores` ### What is this? Datastore configurations (or simply datastores) are like "saved settings" for your adapters. In Sails, [database adapters](https://sailsjs.com/documentation/concepts/extending-sails/adapters) are the middleman between your app and some kind of structured data storage (typically a database). But in order for an adapter to communicate between your Sails app and a particular database, it needs some additional information. That's where datastores come in. Datastores are dictionaries (plain JavaScript objects) that specify an `adapter`, as well as other necessary configuration information, like `url`, or `host`, `port`, `user`, and `password`. While this [can be overridden](https://sailsjs.com/documentation/concepts/orm/model-settings) on a per-model basis, out of the box, every model in your app uses a datastore named "default". ### The default datastore ##### The default development database As a convenience during development, Sails provides a built-in database adapter called `sails-disk`. This adapter simulates a real database by reading and writing database records to a JSON file on your computer's hard drive. And while `sails-disk` makes it easy to run your Sails/Node.js app in almost any environment with minimal setup, it is not designed for production use. Before deploying your app and exposing it to real users, you'll want to choose a proper database such as PostgreSQL, MySQL, MongoDB, etc. To do that, you'll need to customize your app's default datastore. ##### Using a local MySQL database in development Unsurprisingly, the default datastore shared by all of your app's models is named "default". So to hook up a different database, that's the key you'll want to change. For example, imagine you want to develop against a MySQL server installed locally on your laptop: First, install the [MySQL adapter](http://npmjs.com/package/sails-mysql) for Sails and Waterline: ```bash npm install sails-mysql --save --save-exact ``` Then edit your default datastore configuration in `config/datastores.js` so that it looks something like this: ```javascript // config/datastores.js module.exports.datastores = { default: { adapter: require('sails-mysql'), url: 'mysql://root:squ1ddy@localhost:3306/my_dev_db_name', } }; ``` That's it! The next time you lift your app, all of your models will communicate with the specified MySQL database whenever your code executes built-in model methods like `.create()` or `.find()`. > Want to use a different database? Don't worry, MySQL is just an example. You can use any [supported database adapter](https://sailsjs.com/documentation/concepts/extending-sails/adapters/available-adapters) in your Sails app. ### The connection URL You might have noticed that we used `url` here, instead of specifying individual settings like `host`, `port`, `user`, `password`, and `database`. This is called a _connection URL_ (or "connection string"), and it's just another, more concise way, to tell Sails and Waterline about your datastore configuration. One major benefit to this style of configuration is that the format of a connection URL is the same across various types of databases. In other words, whether you're using MySQL, PostgreSQL, MongoDB, or almost any other common database technology, you can specify basic configuration using a URL that looks roughly the same: ``` protocol://user:password@host:port/database ``` The `protocol://` chunk of the URL is always based on the adapter you're using (`mysql://`, `mongodb://`, etc.), and the rest of the URL is composed of the credentials and network information that your app needs to locate and connect to the database. Here's a deconstructed version of the `url` from the MySQL example above that shows what each section is called: ``` mysql:// root : squ1ddy @ localhost : 3306 / my_dev_db_name | | | | | | | | | | | | protocol user password host port database ``` In production, if you are using a cloud-hosted database, you'll probably be given a connection URL (e.g. `mysql://lkjdsf4:kw8sd@us-west-2.64-8.amazonaws.com:3306/4e843g`). If not, it's usually a good idea to build one yourself from the individual pieces of information. For more information about how to configure your particular database, check out the [database adapter reference](https://sailsjs.com/documentation/concepts/extending-sails/adapters/available-adapters). ##### Building your own connection URL If you have all of the pieces of information mentioned above, building a connection URL is easy: you just stick them together. But sometimes, you may not want to specify _all_ of those details (if you want to use the default port, or if you're using a local database that does not require a username and password, for example). Fortunately, since database connection URLs are more or less just normal URLs, you can omit various pieces of information in the same way you might already be familiar with. For example, here are a few common mashups, all of which are potentially valid connection URLs: + `protocol://user:password@host:port/databaseName` + `protocol://user:password@host/databaseName` _(no port)_ + `protocol://user@host:port/databaseName` _(no password)_ + `protocol://host:port/databaseName` _(neither a username nor a password)_ > Connection URLs are the recommended approach for configuring your Sails app's database(s), so it's best to stick with them if possible. But technically, _some adapters_ also support configuration of individual settings (`user`, `password`, `host`, `port`, and `database`) as an alternative. In that scenario, if both the `url` notation and individual settings are used, the non-url configuration options should always take precedence. You should, however, always use one approach or the other: either the `url` or the individual properties. Mixing the two configuration strategies may confuse the adapter, or cause the underlying database driver to reject your configuration. ### Production datastore configuration When configuring your app for a production deployment, you won't actually use the `config/datastores.js` file. Instead, you can take advantage of `config/env/production.js`, a special file of configuration overrides that only get applied in a production environment. This allows you to override the `url` and `adapter` (or just the `url`) that you set in `config/datastores.js`: ```javascript // config/env/production.js module.exports = { // ... // Override the default datastore settings in production. datastores: { default: { // No need to set `adapter` again, because we already configured it in `config/datastores.js`. url: 'mysql://lkjdsf4a23d9xf4:kkwer4l8adsfasd@u23jrsdfsdf0sad.aasdfsdfsafd.us-west-2.ere.amazonaws.com:3306/ke9944a4x23423g', } }, // ... }; ``` Connection URLs really shine in production, because you can change them by swapping out a single config key. Not only does this make your production settings easier to understand, it also allows you to swap out your production database credentials simply by setting an [environment variable](https://sailsjs.com/documentation/concepts/configuration#?setting-sailsconfig-values-directly-using-environment-variables) (`sails_datastores__default__url`). This is a handy way to avoid immortalizing sensitive database credentials as commits in your version control system. ### Supported databases Sails's ORM, [Waterline](https://sailsjs.com/documentation/concepts/models-and-orm), has a well-defined adapter system for supporting all kinds of datastores. The Sails core team maintains official adapters for [MySQL](http://npmjs.com/package/sails-mysql), [PostgreSQL](http://npmjs.com/package/sails-postgresql), [MongoDB](http://npmjs.com/package/sails-mongo), and [local disk](http://npmjs.com/package/sails-disk); and community adapters exist for databases like Oracle, DB2, MSSQL, OrientDB, and many more. You can find an up-to-date list of supported database adapters [here](https://sailsjs.com/documentation/concepts/extending-sails/adapters/available-adapters). > Still can't find the adapter for your database? You can also create a [custom adapter](https://sailsjs.com/documentation/concepts/extending-sails/adapters/custom-adapters). Or if you'd like to modify/update an existing adapter, get in touch with its maintainer. (Need help? Click [here](https://sailsjs.com/support) for additional resources.) ### Multiple datastores You can set up more than one datastore pointed at the same adapter, or at different adapters. For example, you might be using MySQL as your primary database but also need to integrate with a _second_ MySQL database that contains data from an existing Java or PHP app. Meanwhile, you might need to integrate with a _third_ MongoDB database that was left over from a promotional campaign a few months ago. You could set up `config/datastores.js` as follows: ```javascript // config/datastores.js module.exports.datastores = { default: { adapter: require('sails-mysql'), url: 'mysql://root@localhost:3306/dev', }, existingEcommerceDb: { adapter: require('sails-mysql'), url: 'mysql://djbluegrass:0ldy3ll3r@legacy.example.com:3306/store', }, q3PromoDb: { adapter: require('sails-mongo'), url: 'mongodb://djbluegrass:0ldy3ll3r@seasonal-pet-sweaters-promo.example.com:27017/promotional', } }; ``` > **Note:** If a datastore is using a particular adapter, then _all_ datastores that share that adapter will be loaded on `sails lift`, whether or not models are actually using them. In the example above, if a model was defined with `datastore: 'existingEcommerceDb'`, then at runtime Waterline would create two MySQL connection pools: one for `existingEcommerceDb` and one for `default`. Because of this behavior, we recommend commenting out or removing any "aspirational" datastore configurations that you're not actually using from `config/datastores.js`. ### Best practices Some general rules of thumb: + To change the datastore you're using _during development_, edit the `default` key in `config/datastores.js` (or use `config/local.js` if you'd rather not check in your credentials). + To configure your default _production_ datastore, use `config/env/production.js` (or set environment variables if you'd rather not check in your credentials). + To override the datastore for a particular model, [set its `datastore`](https://sailsjs.com/documentation/concepts/models-and-orm/model-settings#?datastore). + Besides the `config/datastores.js` and `config/env/production.js` files, you can configure datastores in [the same way you configure anything else in Sails](https://sailsjs.com/documentation/concepts/configuration), including environment variables, command-line options, and more. <docmeta name="displayName" value="sails.config.datastores"> <docmeta name="pageType" value="property"> ================================================ FILE: docs/reference/sails.config/sails.config.custom.md ================================================ # Custom configuration ### What is this? The custom configuration for your app. This is useful for one-off settings specific to your application, like the domain to use when sending emails, or third-party API keys for Stripe, Mailgun, Twitter, Facebook, etc. These values are usually set in the [`config/custom.js`](https://sailsjs.com/documentation/anatomy/config/custom-js) file and may be overridden in production using `config/env/production.js`, environment variables, or any of the other [configuration mechanisms](https://sailsjs.com/documentation/concepts/configuration) provided by Sails. ### Example First, to set custom configuration: ```javascript // config/custom.js module.exports.custom = { mailgunDomain: 'transactional-mail.example.com', mailgunApiKey: 'key-testkeyb183848139913858e8abd9a3' }; ``` Then, to access these values from your actions and helpers, use `sails.config.custom`: ```javascript sails.config.custom.mailgunApiKey; // -> "key-testkeyb183848139913858e8abd9a3" ``` <docmeta name="displayName" value="sails.config.custom"> ================================================ FILE: docs/reference/sails.config/sails.config.globals.md ================================================ # `sails.config.globals` Configuration for the [global variables](https://developer.mozilla.org/en-US/docs/Glossary/Global_variable) that Sails exposes by default. The globals configuration in Sails is only for controlling global variables introduced by Sails. The options are conventionally specified in the [`config/globals.js`](https://sailsjs.com/anatomy/config/globals-js) configuration file. ### Properties | Property | Type | Convention | Details | |:-----------|:----------:|:----------|:--------| | `_` _(underscore)_ | ((ref))<br/>_or_<br/>((boolean)) | `require('lodash')` | Expose the specified `lodash` as a global variable (`_`). Or set this to `false` to disable the `_` global altogether. _(More on that below.)_ | `async` | ((ref))<br/>_or_<br/>((boolean)) | `require('async')` | Expose the specified `async` as a global variable (`async`). Or set this to `false` to disable the `async` global altogether. _(More on that below.)_ | `models` | ((boolean)) | `true` | Expose each of your app's models as a global variable (using its "globalId"). For example, a model defined in `api/models/User.js` would have a "globalId" of `User`. If this is disabled, then you can still access all of your models by identity in the [`sails.models`](https://sailsjs.com/documentation/reference/application#?sailsmodels) dictionary. | `sails` | ((boolean)) | `true` | Expose the `sails` instance representing your app. Even if this is disabled, you can still get access to it in your actions via `env.sails`, or in your policies via `req._sails`. | `services` | ((boolean)) | `true` | Expose each of your app's services as global variables (using their "globalId"). E.g. a service defined in `api/services/NaturalLanguage.js` would have a globalId of `NaturalLanguage` by default. If this is disabled, you can still access your services via `sails.services.*`. ### Using global Lodash (`_`) and Async libraries Newly-generated Sails 1.0 apps have Lodash v3.10.1 and Async v2.0.1 installed by default and enabled globally so that you can reference `_` and `async` in your app code without needing to `require()`. This is effected with the following default configuration in `config/globals.js`: ``` { _: require('lodash'), async: require('async') } ``` You can disable access by setting the properties to `false`. Prior to `Sails v1.0` you could set the properties to `true`; this has been deprecated and replaced by the syntax above. To use your own version of Lodash or Async, you just need to `npm install` the version you want. For example, to install the latest version of Lodash 4.x.x: ```sh npm install lodash@^4.x.x --save --save-exact ``` ### Using Lodash (`_`) and Async without globals If you have to disable globals, but would still like to use Lodash and/or Async, you're in luck! With Node.js and NPM, importing packages is very straightforward. To use your own version of Lodash or Async without relying on globals, first modify the relevant settings in `config/globals.js`: ```js // Disable `_` and `async` globals. _: false, async: false, ``` Then install your own Lodash: ```sh npm install lodash --save --save-exact ``` Or Async: ```sh npm install async --save --save-exact ``` Finally, just like you'd import [any other Node.js module](https://soundcloud.com/marak/marak-the-node-js-rap), include `var _ = require('lodash');` or `var async = require('async')` at the top of any file where you need them. ### Notes > + As a shortcut to disable _all_ of the above global variables, you can set `sails.config.globals` itself to `false`. This does the same thing as if you had manually disabled each of the settings above. <docmeta name="displayName" value="sails.config.globals"> <docmeta name="pageType" value="property"> ================================================ FILE: docs/reference/sails.config/sails.config.http.md ================================================ # `sails.config.http` Configuration for your app's underlying HTTP server. These properties are conventionally specified in the [`config/http.js`](https://sailsjs.com/documentation/anatomy/config/http.js) configuration file. ### Properties Property | Type | Default | Details :------------------ |:----------:| --------- |:------- `middleware` | ((dictionary)) | See [conventional defaults for HTTP middleware](https://sailsjs.com/documentation/concepts/Middleware?q=conventional-defaults) | A dictionary of all HTTP middleware functions your app will run on every incoming HTTP request.<br/>[Example](https://gist.github.com/mikermcneil/9cbd68c95839da480e97) `middleware.order` | ((array)) | See [conventional defaults for HTTP middleware order](https://github.com/balderdashy/sails/blob/master/lib/hooks/http/index.js#l51-66) | An array of middleware names (strings) indicating the order in which middleware should be run for all incoming HTTP requests. `cache` | ((number)) | `31557600000` _(1 year)_ | The number of milliseconds to cache [static assets](https://sailsjs.com/documentation/concepts/assets) when your app is running in a ['production' environment](https://sailsjs.com/documentation/reference/configuration/sails-config#?sailsconfigenvironment).<br/>More specifically, this is the "max-age" that will be included in the "Cache-Control" header when responding to requests for static assets—i.e. any flat files like images, scripts, stylesheets, etc. that are served by Express' static middleware. `serverOptions` | ((dictionary)) | `{}` | _SSL only_: advanced options to send directly to the [Node `https` module](https://nodejs.org/dist/latest/docs/api/https.html) when creating the server. These will be merged with your [SSL settings](https://sailsjs.com/documentation/reference/configuration/sails-config#?sailsconfigssl), if any. See the [createServer docs](https://nodejs.org/dist/latest/docs/api/https.html#https_https_createserver_options_requestlistener) for more info. `trustProxy` | ((boolean)) _or_ ((function)) | `undefined` | This tells Sails/Express how it should interpret "X-Forwarded" headers. Only use this setting if you are using HTTPS _and_ if you are deploying behind a proxy (for example, a PaaS like Heroku). If your app does not fit that description, then leave this as undefined. Otherwise, you might start by setting this to `true`, which works for many deployments. If that doesn't work, see [here](https://expressjs.com/en/guide/behind-proxies.html) for all available options. ### Customizing the body parser The _body parser_ is what Sails/Express apps use to read and understand the body of incoming HTTP requests. Many different body parsers are available, each with their own strengths and weaknesses. By default, Sails apps use [Skipper](http://github.com/balderdashy/skipper), a general-purpose solution that knows how to parse most kinds of HTTP request bodies and provides support for streaming, multipart file uploads. > You can specify a different body parser or a custom function with `req`, `res`, and `next` parameters (just like any other [HTTP middleware function](https://sailsjs.com/documentation/concepts/middleware).) ##### Configuring Skipper To customize Skipper, first make sure to `npm install skipper --save` in your app. Next, uncomment the following code in your `config/http.js` file: ```javascript bodyParser: (function _configureBodyParser(){ var skipper = require('skipper'); var middlewareFn = skipper({ strict: true, // ... more Skipper options here ... }); return middlewareFn; })(), ``` Then pass in any of the following options from the table below. Property | Type | Default | Details :--------------------------------------- |:-----------:|:--------- |:------- `maxWaitTimeBeforePassingControlToApp` | ((number)) | `500` | The maximum number of milliseconds to wait when processing an incoming multipart request before passing control to your app's policies and controllers. If this number of milliseconds elapses without any incoming file uploads, and the request hasn't finished sending other data like text parameters (i.e. the form emits "close"), then control will be passed without further delay. For apps running behind particular combinations of load balancers, proxies, and/or SSL, it may be necessary to increase this delay (see https://github.com/balderdashy/skipper/issues/71#issuecomment-217556631). `maxTimeToWaitForFirstFile` | ((number)) | `10000` | The maximum number of milliseconds to wait for the first file upload to arrive in any given upstream before triggering `.upload()`'s callback. If the first file upload on a given upstream does not arrive before this number of milliseconds have elapsed, then an `ETIMEOUT` error will fire. `maxTimeToBuffer` | ((number)) | `4500` | The maximum number of milliseconds to wait for any given live [upstream](https://github.com/balderdashy/skipper#what-are-upstreams) to be plugged in to a receiver after it begins receiving an incoming file upload. Skipper pauses upstreams to allow custom code in your app's policies and controller actions to run (e.g. doing database lookups) before you "plug in" the incoming file uploads (e.g. `req.file('avatar').upload(...)`) to your desired upload target (local disk, S3, gridfs, etc). Incoming bytes are managed using [a combination of buffering and TCP backpressure](https://howtonode.org/streams-explained) built into Node.js streams. The max buffer time is a configurable layer of defense to protect against denial of service attacks that attempt to flood servers with pending file uploads. If the timeout is exceeded, an EMAXBUFFER error will fire. The best defense against these types of attacks is to plug incoming file uploads into receivers as early as possible at the top of your controller actions. `strict` | ((boolean)) | `true` | When enabled, the body of incoming HTTP requests will only be parsed as JSON if it appears to be an array or dictionary (i.e. plain JavaScript object). Otherwise, if _disabled_, the body parser will accept anything `JSON.parse()` accepts (including `null`, `true`, `false`, numbers, and double-quote-wrapped strings). While these other types of data are uncommon in practice, they are technically JSON compatible; therefore, this setting is enabled by default. `extended` | ((boolean)) | `true` | Whether or not to understand multiple text parameters in square bracket notation in the URL-encoded request body (e.g. `courseId[]=ARY%20301&courseId[]=PSY%20420`) encoded the HTTP body as an array (e.g. `courseId: ['ARY 301', 'PSY 420'], ...`). Enabled by default. See https://github.com/expressjs/body-parser#extended for more details. `onBodyParserError` | ((function)) | (see details) | An optional function to be called if Skipper encounters an error while parsing the request body (for example, if it encounters malformed JSON). The function accepts four arguments: `err`, `req`, `res` and `next`. Sails provides a default implementation that responds to the request with a 400 status and a message detailing the error encountered. If no `onBodyParserError` function is provided, parser errors will be passed to `next()` and handled by the next available [error-handling middleware](https://expressjs.com/en/guide/error-handling.html). > Note that, to allow for performance tuning and other advanced configuration, the options you pass in to Skipper this way are also passed through to the underlying Express body parser. See the [body-parser repo](https://github.com/expressjs/body-parser) for a full list of lower-level options. ### Compatibility Most middleware compatible with [Express](https://github.com/expressjs/), [Connect](https://github.com/senchalabs/connect), [Kraken](http://krakenjs.com/), [Loopback](https://github.com/strongloop/loopback), or [Pillar](https://pillarjs.github.io/) can also be used in a Sails app. ### Notes > + Note that this HTTP middleware stack configured in `sails.config.http.middleware` is only applied to true HTTP requests—it is ignored when handling virtual requests (e.g. sockets). > + The middleware named `router` is what handles all of your app's explicit routes (i.e. `sails.config.routes`), as well as shadow routes that are injected for blueprints, policies, etc. > + You cannot define a custom middleware function with the key `order` (since `sails.config.http.middleware.order` has special meaning). <docmeta name="displayName" value="sails.config.http"> <docmeta name="pageType" value="property"> ================================================ FILE: docs/reference/sails.config/sails.config.i18n.md ================================================ # `sails.config.i18n` Configuration for Sails' built-in internationalization and localization features. By convention, this is set in `config/i18n.js`, from which you can set your supported locales. For more information see the [concepts section on internationalization](https://sailsjs.com/documentation/concepts/Internationalization). ### Properties | Property | Type | Default | Details | |:-------------------|:-----------:|:----------------------|:--------| | `locales` | ((array)) | `['en','es','fr','de']` | List of supported [locale codes](http://en.wikipedia.org/wiki/BCP_47). Note that these values and the name of their corresponding translation files must be lowercase. | `localesDirectory` | ((string)) | `'config/locales'` | The app-relative path to the folder containing your locale translations (i.e. stringfiles). Alternatively, an absolute path maybe provided. | `defaultLocale` | ((string)) | `'en'` | The default locale for the site. Note that this setting will be overridden for any request that sends an "Accept-Language" header (i.e. most browsers), but it's still useful if you need to localize the response for requests made by non-browser clients (e.g. mobile devices, IoT, cURL, Postman, etc.). <docmeta name="displayName" value="sails.config.i18n"> <docmeta name="pageType" value="property"> ================================================ FILE: docs/reference/sails.config/sails.config.log.md ================================================ # `sails.config.log` Configuration for the [logger](https://sailsjs.com/documentation/concepts/logging) in your Sails app. These settings apply whenever you call functions like `sails.log.debug()` or `sails.log.error()` in your app code, as well as when Sails logs a message to the console automatically. The options here are conventionally specified in the [config/log.js](https://sailsjs.com/documentation/anatomy/config/log.js) configuration file. ### Properties | Property | Type | Default | Details | |:----------|-------------|:------------|:------------------------------------------------------------------------------------| | level | ((string)) | `'info'` | Set the level of detail to be shown in your app's log. | inspect | ((boolean)) | `true` | Set to false to disable captain's log's handling of logging, logs will instead be passed to the configured custom logger. | | custom | ((ref)) | `undefined` | Specify a reference to an instance of a custom logger (such as [Winston](https://github.com/winstonjs/winston)). If provided, instead of logging directly to the console, the functions exposed by the custom logger will be called, and log messages from Sails will be passed through. For more information, see [captains-log](https://github.com/balderdashy/captains-log/blob/master/README.md#why-use-a-custom-logger). ### Using a custom logger It is sometimes useful to configure a custom logger, particularly for regulatory compliance and organizational requirements (e.g. if your company is using a particular logger in other apps). In the context of Sails, configuring a custom logger also allows you to intercept all log messages automatically created by the framework, which is handy for setting up email notifications about errors and warnings. > Don't feel like you _have_ to use a custom logger if you want these sorts of notifications! In fact, there are usually more straightforward ways to implement features like automated Slack, SMS, or email notifications when errors occur. One approach is to customize your app's default server error response ([`responses/serverError.js`](https://sailsjs.com/documentation/anatomy/my-app/api/responses/server-error-js)). Another popular option is to use a product like [Papertrail](https://papertrailapp.com/), or a monitoring service like [AppDynamics](https://www.appdynamics.com/nodejs/sails/) or [NewRelic](https://discuss.newrelic.com/t/using-newrelic-with-sails-js/3338/8). Here's an example configuring [Winston](https://github.com/winstonjs/winston) as a custom logger, defining both a console transport and file transport. First of all, add `winston` as a dependency of your project: ```bash npm install winston ``` Then, replace the content of `config/log.js` with the following: ```javascript // config/log.js const { version } = require('../package'); const { createLogger, format, transports } = require('winston'); const { combine, timestamp, colorize, label, printf, align } = format; const { SPLAT } = require('triple-beam'); const { isObject } = require('lodash'); function formatObject(param) { if (isObject(param)) { return JSON.stringify(param); } return param; } // Ignore log messages if they have { private: true } const all = format((info) => { const splat = info[SPLAT] || []; const message = formatObject(info.message); const rest = splat.map(formatObject).join(' '); info.message = `${message} ${rest}`; return info; }); const customLogger = createLogger({ format: combine( all(), label({ label: version }), timestamp(), colorize(), align(), printf(info => `${info.timestamp} [${info.label}] ${info.level}: ${formatObject(info.message)}`) ), transports: [new transports.Console()] }); module.exports.log = { custom: customLogger, inspect: false // level: 'info' }; ``` <docmeta name="displayName" value="sails.config.log"> <docmeta name="pageType" value="property"> ================================================ FILE: docs/reference/sails.config/sails.config.md ================================================ # Configuration (`sails.config`) The `sails.config` object contains the runtime values of [your app's configuration](https://sailsjs.com/documentation/concepts/configuration). It is assembled automatically when Sails loads your app; merging together command-line arguments, environment variables, your `.sailsrc` file, and the configuration objects exported from any and all modules in your app's [`config/`](https://sailsjs.com/documentation/anatomy/config) directory. For more general info about how to configure your Sails app, see the [configuration concepts guide](https://sailsjs.com/documentation/concepts/configuration). See the other pages in this reference section for details on the configuration files that come with every new Sails app, or read about [custom configuration](https://sailsjs.com/documentation/reference/configuration/sails-config-custom). <docmeta name="displayName" value="Configuration"> ================================================ FILE: docs/reference/sails.config/sails.config.models.md ================================================ # `sails.config.models` Your default, project-wide **model settings**, conventionally specified in the [config/models.js](https://sailsjs.com/documentation/anatomy/config/models-js) configuration file. Most of the settings below can also be overridden on a per-model basis—just edit the appropriate model definition file. There are some additional model settings, too, which are not listed below; these can _only_ be specified on a per-model basis. For more details, see [Concepts > Model Settings](https://sailsjs.com/documentation/concepts/orm/model-settings). ### Properties Property | Type | Default | Details :---------------------|:---------------:|:------------------------------- |:-------- `attributes` | ((dictionary)) | _see [Attributes](https://sailsjs.com/documentation/concepts/models-and-orm/attributes)_ | Default [attributes](https://sailsjs.com/documentation/concepts/models-and-orm/attributes) to implicitly include in all of your app's model definitions. (Can be overridden on an attribute-by-attribute basis.) `migrate` | ((string)) | _see [Model Settings](https://sailsjs.com/documentation/concepts/orm/model-settings)_ | The [auto-migration strategy](https://sailsjs.com/documentation/concepts/models-and-orm/model-settings#?migrate) for your Sails app. How & whether Sails will attempt to automatically rebuild the tables/collections/etc. in your schema every time it lifts. `schema` | ((boolean)) | `false` | Only relevant for models hooked up to a schemaless database like MongoDB. If set to `true`, then the ORM will switch into "schemaful" mode. For example, if properties passed in to `.create()`, `.createEach()`, or `.update()` do not correspond to recognized attributes, then they will be stripped out before saving. `datastore` | ((string)) | `'default'` | The default [datastore configuration](https://sailsjs.com/documentation/reference/configuration/sails-config-datastores) any given model will use without a configured override. Avoid changing this. `primaryKey` | ((string)) | `'id'` | The name of the attribute that every model in your app should use as its primary key by default. Can be overridden here or on a per-model basis, but there's [usually a better way](https://sailsjs.com/documentation/concepts/models-and-orm/model-settings#?primarykey). `archiveModelIdentity` | ((string)) _or_ ((boolean)) | `'archive'` | The identity of the model to use when calling [`.archive()`](https://sailsjs.com/documentation/reference/waterline-orm/models/archive). By default this is the Archive model, an implicit model automatically defined by Sails/Waterline. Set to `false` to disable built-in support for soft-deletes. <docmeta name="displayName" value="sails.config.models"> <docmeta name="pageType" value="property"> ================================================ FILE: docs/reference/sails.config/sails.config.policies.md ================================================ # `sails.config.policies` <!-- > FUTURE: > > Merge most of the contents of this file into the main reference section on policies. > Include a simple config reference table (with only one row with property: `*`) explaining how > this particular config module is read. But don't worry about trying to explain what policies are here-- instead, link to the full docs on the subject (again, to reduce duplicate content and make this all more maintainable) --> This configuration is a dictionary that maps [policies](https://sailsjs.com/documentation/concepts/policies) to an app’s [actions](https://sailsjs.com/documentation/concepts/actions-and-controllers). See [Concepts > Policies](https://sailsjs.com/documentation/concepts/policies#?using-policies-with-blueprint-actions) for more info. ### Properties | Property | Type | Default | Details | |:-----------|:----------:|:----------|:--------| | (any string) | ((string))<br/>_or_<br/>((dictionary)) | n/a | Any properties added to `sails.config.policies` will be interpreted as a mapping of policies to a controller or a set of standalone actions. ### Example ```js module.exports.policies = { '*': 'isLoggedIn', // Require user to be logged in to access any action not otherwise mapped in this config 'UserController': { 'login': true // Always allow access to the user login action } } ``` <docmeta name="displayName" value="sails.config.policies"> <docmeta name="pageType" value="property"> ================================================ FILE: docs/reference/sails.config/sails.config.routes.md ================================================ # `sails.config.routes` Configuration for custom (aka "explicit") routes. `sails.config.routes` is a dictionary whose keys are URL paths (the "route address") and whose values are one of several types of route handler configurations (called the "route target"). For example: ``` module.exports.routes = { 'GET /': { view: 'pages/homepage' }, 'POST /foo/bar': { action: 'foo/bar' } } ``` Please see the [routes concept overview](https://sailsjs.com/documentation/concepts/routes) for a full discussion of Sails routes, and the [custom routes documentation](https://sailsjs.com/documentation/concepts/routes/custom-routes) for a detailed description of the available configurations for both the route address and route target. <docmeta name="displayName" value="sails.config.routes"> <docmeta name="pageType" value="property"> ================================================ FILE: docs/reference/sails.config/sails.config.security.md ================================================ # `sails.config.security` Configuration for your app's security settings, including how it deals with cross-origin requests (CORS), and which routes require a CSRF token to be included with the request. For an overview of how Sails handles security, see [Concepts > Security](https://sailsjs.com/documentation/concepts/security). ## `sails.config.security.cors` Configuration for Sails' [built-in support for Cross-Origin Resource Sharing](https://sailsjs.com/documentation/concepts/security/cors). CORS specifies how HTTP requests to your app originating from foreign domains should be treated. It is primarily used to allow third-party sites to make AJAX requests to your app, which are normally blocked by browsers following the <a href="http://en.wikipedia.org/wiki/Same-origin_policy" target="_blank">same-origin policy</a>. These options are conventionally set in the **config/security.js** configuration file. Note that these settings (with the exception of `allRoutes`) can be changed on a per-route basis in the [**config/routes.js** file](https://sailsjs.com/documentation/concepts/routes/custom-routes#?route-target-options). ### Properties | Property | Type | Default | Details | |:------------|:----------:|:----------|:--------| | allRoutes | ((boolean))| false | Indicates whether the other CORS configuration settings should apply to every route in the app by default. | allowOrigins | ((array)) or ((string)) | `'*'` | Array of default hosts (beginning with http:// or https://) to grant cross-domain browser access (e.g. AJAX over CORS). Alternatively, if this is the string `*`, then AJAX requests from _any_ domain will be allowed.<br/><br/>**Warning**: If your CORS settings specify `allRoutes: true` AND `allowOrigins: '*'`, then your app will be fully accessible to sites hosted on foreign domains (except for routes which have their own CORS settings). If `allowCredentials` is also `true`, you will _probably want to set this to an array of explicit hosts!_ If you don't, then the app will fail to lift for security reasons, unless you circumvent that precaution by enabling the `allowAnyOriginWithCredentialsUnsafe: true` flag. | allowRequestMethods |((string))| `'GET, POST, PUT, DELETE, OPTIONS, HEAD'` |Comma-delimited list of HTTP methods that are allowed to be used in CORS requests. This is only used in response to [preflight requests](https://developer.mozilla.org/en-US/docs/HTTP/Access_control_CORS#Preflighted_requests), so the inclusion of GET, POST, OPTIONS and HEAD, although customary, is not necessary. | allowRequestHeaders |((string))| `'content-type'` |Comma-delimited list of headers that are allowed to be sent with CORS requests. This is only used in response to [preflight requests](https://developer.mozilla.org/en-US/docs/HTTP/Access_control_CORS#Preflighted_requests). _(For example, if you want cross-origin AJAX requests to be able to include their CSRF token as a request header, you might change this to `'content-type,x-csrf-token'`.)_ | allowResponseHeaders |((string))|`''`| List of response headers that browsers will be allowed to access. See [access-control-expose-headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS#Access-Control-Expose-Headers). | allowCredentials |((boolean)) | false | Whether or not cookies can be shared in CORS requests. _(For example, if `allowCredentials` is not enabled, then when Sails receives an AJAX request from a webpage on some other domain, it won't be able to provide `req.session` when the backend code runs.)_ | | allowAnyOriginWithCredentialsUnsafe |((boolean))|false| A safety precaution. This flag must be enabled in order to use `allowOrigins: '*'` and `allowCredentials: true` _at the same time_. This essentially negates the security benefits of browsers' cross-origin policy and should be used very carefully. ### Custom route config example The following will allow cross-origin AJAX GET, PUT and POST requests to `/foo/bar` from sites hosted `http://foobar.com` and `https://owlhoot.com`. DELETE requests, or requests from sites on any other domains, will be blocked by the browser. ```javascript '/foo/bar': { action: 'foo/bar', cors: { allowOrigins: ['http://foobar.com','https://owlhoot.com'], allowRequestMethods: 'GET,PUT,POST,OPTIONS,HEAD' } } ``` ## `sails.config.security.csrf` Configuration for Sails' built-in [CSRF](http://en.wikipedia.org/wiki/Cross-site_request_forgery) protection middleware. CSRF options are conventionally set in the [`config/security.js`](https://sailsjs.com/documentation/anatomy/config/security.js) configuration file. For detailed usage instructions, see [Concepts > Security > Cross-Site Request Forgery](https://sailsjs.com/documentation/concepts/security/csrf). This setting protects your Sails app against cross-site request forgery (or CSRF) attacks. In addition to the user's session cookie, a would-be attacker also needs this timestamped, secret CSRF token, which is refreshed/granted when the user visits a URL on your app's domain. This allows you to have certainty that your users' requests haven't been hijacked, and that the requests they're making are intentional and legitimate. ### Properties | Property | Type | Default | Details | |:------------|:----------:|:----------|:--------| | `csrf` | ((boolean)) or ((dictionary))| false | CSRF protection is disabled by default to facilitate development. To turn it on, just set `sails.config.security.csrf` to `true`, or for more flexibility, specify `csrf: true` or `csrf: false` in any route in your [`config/routes.js`](https://sailsjs.com/anatomy/config/routes-js) file. ### Notes > + In Sails v1.0, `sails.config.csrf.grantTokenViaAjax` and `sails.config.csrf.origin` were removed in favor of the [built-in `security/grant-csrf-token`](https://sailsjs.com/docs/concepts/security/csrf) action. <docmeta name="displayName" value="sails.config.security"> <docmeta name="pageType" value="property"> ================================================ FILE: docs/reference/sails.config/sails.config.session.md ================================================ # `sails.config.session` Configuration for Sails' built-in session support. Sails' default session integration leans heavily on the great work already done by Express and Connect, but also adds a bit of its own special sauce by hooking into the request interpreter. This allows Sails to access and auto-save any changes your code makes to `req.session` when handling a virtual request from Socket.IO. Most importantly, it means you can just write code that uses `req.session` in the way you might be used to from Express or Connect, whether your controller actions are designed to handle HTTP requests, WebSocket messages, or both. ### Properties | Property | Type | Default | Details | |:------------|:----------:|:----------|:--------| | `adapter` | ((string)) | `undefined` | If left unspecified, Sails will use the default memory store bundled in the underlying session middleware. This is fine for development, but in production, you _must_ pass in the name of an installed scalable session store module instead (e.g. `@sailshq/connect-redis`). See [Production config](https://sailsjs.com/documentation/reference/configuration/sails-config-session#?production-config) below for details. | `name` | ((string)) | `sails.sid` | The name of the session ID cookie to set in the response (and read from in the request) when sessions are enabled (which is the case by default for Sails apps). If you are running multiple different Sails apps from the same shared cookie namespace (i.e. the top-level DNS domain, like `frog-enthusiasts.net`), you must be especially careful to configure separate unique keys for each separate app, otherwise the wrong cookie could be used. | `secret` | ((string))| _n/a_ | This session secret is automatically generated when your new app is created. Care should be taken any time this secret is changed in production, as doing so will invalidate the session cookies of your users, forcing them to log in again. Note that this is also used as the "cookie secret" for signed cookies. | `cookie` | ((dictionary)) | _see [below](https://sailsjs.com/documentation/reference/configuration/sails-config-session#?the-session-id-cookie)_ | Configuration for the session ID cookie, including `maxAge`, `secure`, and more. See [below](https://sailsjs.com/documentation/reference/configuration/sails-config-session#?the-session-id-cookie) for more info. | `isSessionDisabled` | ((function)) | (see details) | A function to be run for every request which, if it returns a <a href="https://developer.mozilla.org/en-US/docs/Glossary/Truthy" target="_blank">“truthy” value</a>, will cause session support to be disabled for the request (i.e. `req.session` will not exist). By default, this function will check the request path against the [sails.LOOKS_LIKE_ASSET_RX](https://sailsjs.com/documentation/reference/application/advanced-usage/sails-looks-like-asset-rx) regular expression, effectively disabling session support when requesting [assets](https://sailsjs.com/documentation/concepts/assets). ### Advanced session config If you are using Redis as a session store in development, additional configuration options are available. Most apps can use Sails' default Redis support as described [here](https://sailsjs.com/documentation/concepts/sessions#?using-redis-as-the-session-store), but some advanced use cases may include the following optional config: | Property | Type | Default | Details | |:--------------|------------|:---------|:--------| | `url` | ((string)) | `undefined` | The URL of the Redis instance to connect to. This may include one or more of the other settings below, e.g. `redis://:mypass@myredishost.com:1234/5` would indicate a `host` of `myredishost.com`, a `port` of `1234`, a `pass` of `mypass` and a `db` of `5`. In general, you should use either `url` or a combination of the settings below, to avoid confusion. | `host` | ((string)) |`'127.0.0.1'` | Hostname of your Redis instance. If a `url` setting is configured, this setting will be ignored. | `port` | ((number)) |`6379` | Port of your Redis instance. If a `url` setting is configured, this setting will be ignored. | `pass` | ((string)) | `undefined` | The password for your Redis instance. Leave blank if you are not using a password. If a `url` setting is configured that includes a password, this setting will override the password in `url`. | `db` | ((number)) |`undefined` | The index of the database to use within your Redis instance. If specified, must be an integer. _(On typical Redis setups, this will be a number between 0 and 15.)_ If a `url` setting is configured that includes a db, this setting will override the db in `url`. | `client` | ((ref)) | `undefined` | An already-connected Redis client to use. If provided, any `url`, `host` and `port` settings will be ignored. This setting is useful if you have a Redis Sentinel setup and need to connect using a module like <a href="https://www.npmjs.com/package/ioredis" target="_blank">`ioredis`</a> | `onRedisDisconnect` | ((function)) | `undefined` | An optional function for Sails to call if the Redis connection is dropped. Useful for placing your site in a temporary maintenance mode or "panic mode" (see [sails-hook-panic-mode](https://www.npmjs.com/package/sails-hook-panic-mode) for an example). | `onRedisReconnect` | ((function)) | `undefined` | An optional function for Sails to call if a previously-dropped Redis connection is restored (see `onDisconnect` above). | `handleConstructingSessionStore` | ((function)) | `undefined` | An optional override function for Sails to call instead of the standard session store construction behavior. To use this setting, please first read and understand the [relevant source code](https://github.com/balderdashy/sails/blob/master/lib/hooks/session/index.js#L415). > Note: `onRedisDisconnect` and `onRedisReconnect` will only be called for Redis clients that are created by Sails for you; if you provide your own Redis client (see the `client` option above), these functions will _not_ be called automatically in the case of a disconnect or reconnect. ##### Using other session stores Any session adapter written for Connect/Express works in Sails, as long as you use a compatible version. The recommended production session store for Sails.js is Redis... but we realize that, for some apps, that isn't an option. Fortunately, Sails.js supports almost any Connect/Express-compatible session store-- meaning you can store your sessions almost anywhere, whether that's Mongo, on the local filesystem, or even in a relational database. Check out the community session stores for Sails.js, Express, and Connect [available on NPM](https://www.npmjs.com/search?q=connect%20session-). ### The session ID cookie The built-in session integration in Sails works by using a session ID cookie. This cookie is [HTTP-only](https://www.owasp.org/index.php/HttpOnly) (as safeguard against [XSS exploits](https://sailsjs.com/documentation/concepts/security/xss)), and by default, is set with the name "sails.sid". ##### The "__Host-" prefix. By default, cookies have no integrity against same-site attackers. In production enviroments, we recommend that you prefix the "name" of your cookie (`sails.config.session.name`) with "__Host-" to limit the scope of your cookie to a single origin. You can read more about the "__Host-" prefix [here](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#attributes). ```js session: { name: '__Host-sails.sid' } ``` > Note: Adding this prefix requires the ["secure" flag](#the-secure-flag) to be set to `true`. ##### Expiration The maximum age / expiration of your app's session ID cookie can be set as a number of milliseconds. For example, to log users out after 24 hours: ```js session: { cookie: { maxAge: 24 * 60 * 60 * 1000 } } ``` Otherwise, by default, this option is set as `null`, meaning that session ID cookies will not send any kind of ["Expires" or "Max Age" header](https://en.wikipedia.org/wiki/HTTP_cookie) and will last only for as long as a user's web browser is open. ##### The "secure" flag Whether to set the ["Secure" flag](https://www.owasp.org/index.php/SecureFlag) on the session ID cookie. ```js session: { cookie: { secure: true } } ``` During development, when you are not using HTTPS, you should leave `sails.config.session.cookie.secure` as undefined (the default). But in production, you'll want to set it to `true`. This instructs web browsers that they should refuse to send back the session ID cookie _except_ over a secure protocol (`https://`). > **Note:** If you are using HTTPS behind a proxy/load balancer—for example, on a PaaS like Heroku—then you should still set `secure: true`. But note that, in order for sessions to work with `secure` enabled, you will _also_ need to set another option called [`sails.config.http.trustProxy`](https://sailsjs.com/documentation/reference/configuration/sails-config-http). ##### Do I need an SSL certificate? In production? Yes. If you are relying on Sails's built-in session integration, please **always use an SSL certificate in production.** Otherwise, the session ID cookie (or any other secure data) could be transmitted in plain-text, which would make it possible for an attacker in a coffee shop to eavesdrop on one of your authenticated user's HTTP requests, intercept their session ID cookie, then masquerade as them to wreak havoc. Also realize that, even if you have an SSL certificate, and you always redirect `http://` to `https://`, for _all_ of your subdomains, it is still important to set `secure: true`. (Because without it, even if you redirect all HTTP traffic immediately, that _very first request_ will still have been made over `http://`, and thus would have transmitted the session ID cookie in plain text.) ##### Advanced options To see other available options (like "[domain](https://stackoverflow.com/a/7887384/486547)") for configuring the session ID cookie in Sails, see [express-session#cookie](https://github.com/expressjs/session/blob/v1.15.6/README.md#cookie). ### Disabling sessions Sessions are enabled by default in Sails. To disable sessions in your app, disable the `session` hook by changing your `.sailsrc` file. The process for disabling `session` is identical to the process for [disabling the Grunt hook](https://sailsjs.com/documentation/concepts/assets/disabling-grunt) (just type `session: false` instead of `grunt: false`). > **Note:** > If the session hook is disabled, the session secret configured as `sails.config.session.secret` will still be used to support signed cookies, if relevant. If the session hook is disabled _AND_ no session secret configuration exists for your app (e.g. because you deleted `config/session.js`), then signed cookies will not be usable in your application. To make more advanced changes to this behavior, you can customize any of your app's HTTP middleware manually using [`sails.config.http`](https://sailsjs.com/documentation/reference/configuration/sails-config-http). <docmeta name="displayName" value="sails.config.session"> <docmeta name="pageType" value="property"> ================================================ FILE: docs/reference/sails.config/sails.config.sockets.md ================================================ # `sails.config.sockets` ### What is this? These configuration options provide transparent access to Socket.IO, the WebSocket/PubSub server encapsulated by Sails. ### Commonly used options | Property | Type | Default | Details | |:--------------|------------|:---------|:--------| | `adapter` |((string)) |`'memory'`| The queue Socket.IO will use to deliver messages. Can be set to either `'memory'` or `'@sailshq/socket.io-redis'`. If `'@sailshq/socket.io-redis'` is specified, you should be sure `@sailshq/socket.io-redis` is amongst your app's dependencies. | | `transports` |((array)) | `['websocket']` | An array of allowed transport strategies that Sails/Socket.IO will use when connecting clients. This should _always_ match the [configuration in your socket client (i.e. `sails.io.js`)](https://sailsjs.com/documentation/reference/web-sockets/socket-client#?configuring-the-sailsiojs-library)—if you change transports here, you need to configure them there, and vice versa.<br/><br/> <em>Note that if you opt to modify the default transports, then you may need to do additional configuration in production. (For example, if you add the `polling` transport, and your app is running on multiple servers behind a load balancer like Nginx, then you will need to configure that load balancer to support TCP sticky sessions. However, that _should not_ be necessary out of the box with only the `websocket` transport enabled.) See [Deployment > Scaling](https://sailsjs.com/documentation/concepts/deployment/scaling) for more tips and best practices.</em> | | `onlyAllowOrigins` | ((array)) | `undefined` | Array of hosts (beginning with http:// or https://) from which sockets will be allowed to connect. By default (i.e. while this is `undefined`) Sails/Socket.IO will allow sockets from _any_ origin to connect, which is useful for testing. But in production mode, as of Sails v1.0, the framework forces you to configure this option to prevent [cross-site WebSocket hijacking (CSWSH) attacks](https://sailsjs.com/documentation/concepts/security/socket-hijacking). Consequently, there's a conventional place to configure this setting in [config/env/production.js](https://sailsjs.com/documentation/anatomy/config/env/production-js), or using environment variables. For example, if you plan on serving web pages from a local Node.js/Sails.js server running in production mode while testing, you’ll probably want to add `http://localhost:1337` to this array.<br/><br/>Note that as the name implies (and in contrast to the similar [CORS setting](https://sailsjs.com/documentation/reference/configuration/sails-config-security-cors)), _only_ the origins listed will be allowed to connect. Also note that this **setting is ignored** if a connecting socket doesn't declare an "origin" header in its upgrade request (e.g. a non-browser environment like a native iOS app, command-line script, or custom hardware). And if you are using a pseudo-browser development platform like Electron, Ionic, React Native, or Cordova/PhoneGap, you'll need to determine what (if any) "origin" header your tool is attaching to initial socket connection requests. For example, Ionic, Cordova, and PhoneGap all send `file://` as their origin.<br/><br/>Finally, note that if you want to override this behavior altogether with your own custom implementation, you can opt to use the `beforeConnect` setting instead. ### Redis configuration If you are configuring your Sails app for production and plan to [scale to more than one server](https://sailsjs.com/documentation/concepts/deployment/scaling), then you should set `sails.config.sockets.adapter` to `'@sailshq/socket.io-redis'`, set up your Redis instance, and then use the following config to point at it from your app: | Property | Type | Default | Details | |:--------------|------------|:---------|:--------| | `url` | ((string)) | `undefined` | The connection URL for the Redis instance to connect to. This may include one or more of the other settings below, e.g. `redis://:mypass@myredishost.com:1234/5` would indicate a `host` of `myredishost.com`, a `port` of `1234`, a `pass` of `mypass` and a `db` of `5`. In general, you should use either `url` _or_ a combination of the settings below, to avoid confusion (the `url` setting will override all of the settings below). | `db` | ((number)) |`undefined` | The index of the database to use within your redis instance. If specified, must be an integer. _(On most Redis setups, this will be a number between 0 and 15.)_ | `host` |((string)) |`'127.0.0.1'` | Hostname of your Redis instance. | `pass` | ((string)) | `undefined` | Password for your Redis instance. | `port` |((number)) |`6379` | Port of your Redis instance. ### Advanced configuration These configuration options provide lower-level access to the underlying Socket.IO server settings for complete customizability. | Property | Type | Default | Details | |:-----------|:---------:|:---------|:--------| | `beforeConnect`|((boolean)), ((function)) | `undefined` | A function that runs every time a new client-side socket attempts to connect to the server, and which can be used to reject or allow the incoming connection. Useful for tweaking your production environment to prevent [DoS](https://sailsjs.com/documentation/concepts/security/ddos) attacks or reject Socket.IO connections based on business-specific heuristics. See [beforeConnect](https://sailsjs.com/documentation/reference/configuration/sails-config-sockets#?beforeconnect) below for more info. | | `afterDisconnect`| ((function)) | `undefined` | A function to run when a client-side socket disconnects from the server. To define your own custom logic, specify a function like `afterDisconnect: function (session, socket, cb) {}`. | `allowUpgrades` | ((boolean)) | `true` | This is a raw configuration option exposed from Engine.io. It indicates whether to allow Socket.io clients to upgrade the transport that they are using (e.g. start with polling, then upgrade to a true WebSocket connection). | | `cookie` | ((string)), ((boolean)) | `false` | This is a raw configuration option exposed from Engine.io. It indicates the name of the HTTP cookie that contains the connecting Socket.IO client's socket id. The cookie will be set when responding to the initial Socket.IO "handshake". Alternatively, may be set to `false` to disable the cookie altogether. Note that the `sails.io.js` client does not rely on this cookie, so it is disabled (set to `false`) by default for enhanced security. If you are using Socket.IO directly and need to re-enable this cookie, keep in mind that the conventional setting is `"io"`. | | `grant3rdPartyCookie` | ((boolean)) | `true` | Whether to expose a `GET /__getcookie` route that sets an HTTP-only session cookie. By default, if it detects that it is about to connect to a cross-origin server, the Sails socket client (`sails.io.js`) sends a JSONP request to this endpoint before it begins connecting. For user agents where 3rd party cookies are possible, this allows `sails.io.js` to connect the socket to the cross-origin Sails server using a user's existing session cookie, if they have one (for example, if they were already logged in). Without this, virtual requests you make from the socket will not be able to access the same session and will need to reauthenticate using some other mechanism. | | `maxHttpBufferSize` | ((number)) | `10E7` | This is a raw configuration option exposed from Engine.io. It reflects the maximum number of bytes or characters in a message when polling before automatically closing the socket (to avoid [DoS]((https://sailsjs.com/documentation/concepts/security/ddos)). | | `path` | ((string)) | `/socket.io` | Path that client-side sockets should connect to on the server. See http://socket.io/docs/server-api/#server(opts:object). | `pingInterval` | ((number)) | `25000` | This is a raw configuration option exposed from Engine.io. It reflects the number of milliseconds to wait between "ping packets" (this is what "heartbeats" has become, more or less). | | `pingTimeout` | ((number)) | `60000` | This is a raw configuration option exposed from Engine.io. It reflects how many milliseconds without a pong packet to wait before considering a Socket.IO connection closed. | | `sendResponseHeaders`|((boolean)) | `true` | Whether to include response headers in the JWR (JSON WebSocket Response) originated for each socket request (e.g. `io.socket.get()` in the browser). This doesn't affect direct Socket.IO usage, unless you're communicating with Sails via the request interpreter (e.g. making normal calls with the sails.io.js browser SDK). This can be useful for squeezing out more performance when tuning high-traffic apps, since it reduces total bandwidth usage. However, as of Sails v0.10, response headers are trimmed whenever possible, so this option should almost never need to be used, even in extremely high-scale applications. | | `serveClient`|((boolean)) | `false` | Whether to serve the default Socket.IO client at `/socket.io/socket.io.js`. Occasionally useful for advanced debugging. | | `onRedisDisconnect` | ((function)) | `undefined` | An optional function for Sails to call if the Redis connection is dropped. Useful for placing your site in a temporary maintenance mode or "panic mode" (see [sails-hook-panic-mode](https://www.npmjs.com/package/sails-hook-panic-mode) for an example). | `onRedisReconnect` | ((function)) | `undefined` | An optional function for Sails to call if a previously-dropped Redis connection is restored (see `onDisconnect` above). > Note: `onRedisDisconnect` and `onRedisReconnect` will only be called for Redis clients that are created by Sails for you; if you provide your own Redis clients (see below), these functions will _not_ be called automatically in the case of a disconnect or reconnect. ### `beforeConnect` During development, when a socket tries to connect, Sails allows it every time (much in the same way any HTTP request is allowed to reach your routes). Then, in production, the `onlyAllowOrigins` array ensures that only incoming socket connections that originate from the base URLs on the whitelist will be permitted to connect to your app. If your app needs more flexibility, as an additional precaution you can define your own custom logic to allow or deny socket connections. To do so, specify a `beforeConnect` function: ```javascript beforeConnect: function(handshake, proceed) { // Send back `true` to allow the socket to connect. // (Or send back `false` to reject the attempt.) return proceed(undefined, true); }, ``` > Note that if `beforeConnect` is used, then the `onlyAllowOrigins` setting will be ignored. This allows you to accept socket connections from non-traditional clients (for example, in an [Electron app](electron.atom.io)) that may not set an `origin` header. ### Sockets & sessions When client sockets connect to a Sails app, they authenticate using a session cookie by default (with the session hook enabled). This allows Sails to associate the virtual requests made from the socket with an existing user session, similar to how normal HTTP requests work. > A note for browser clients: The user's session cookie is NOT (and will never be) accessible from client-side JavaScript. Using HTTP-only cookies is crucial for your app's security. ##### Cross-origin sockets The sails.io.js client is usually initiated from an HTML page that was already fetched via HTTP, which means that sockets connecting from this sort of browser environment will usually provide a valid session cookie automatically. As a result, everything will behave normally and `req.session` will be available. However, in the case of cross-origin sockets, it is possible to receive a connection upgrade request _without a cookie_ (for certain transports, anyway). In this case, there is no way to keep track of the requesting user between virtual requests, since there is no identifying information to link them with a session. The sails.io.js client solves this by sending an HTTP request to a CORS+JSONP endpoint first, in order to get a 3rd party cookie. This cookie is then used when opening the socket connection. ##### Non-browser clients Similarly, if a socket connects _without_ providing a session cookie or provides a corrupted cookie, then a temporary, throwaway session entry will be created for it. The same thing happens if the provided session cookie doesn't match any known session entry. You can also configure sails.io.js to pass along an override for the session cookie in the form of a `?cookie` query parameter in the [url when connecting the socket](https://sailsjs.com/documentation/reference/web-sockets/socket-client/io-sails). Sails will use this instead of the actual session cookie that may or may not have been sent in the initial connection upgrade request. For example, if you were building a standalone Electron app, and you disabled `autoConnect` in favor of connecting a socket manually, you might do: ```javascript var hotSocket = io.sails.connect('http://localhost:1337?cookie=smokeybear'); ``` ### Providing your own Redis clients By default, Sails will create new Redis clients in the background when using the `@sailshq/socket.io-redis` adapter. In some cases, you may instead need to create your own Redis clients for PubSub (typically using the <a href="https://www.npmjs.com/package/node-redis" target="_blank">node-redis</a> or <a href="https://www.npmjs.com/package/ioredis">ioredis</a> modules) and provide them to Sails for use in PubSub. This often comes up when using a <a href="https://redis.io/topics/sentinel" target="_blank">Redis Sentinel</a> setup, which requires that clients connect using a module like <a href="https://www.npmjs.com/package/ioredis" target="_blank">ioredis</a>. The following advanced configuration options allow you to pass already-connected Redis clients and related config info to Sails. | Property | Type | Default | Details | |:-----------|:---------:|:---------|:--------| | `pubClient` | ((ref)) | `undefined` | A custom Redis client used for _publishing_ on channels used by Socket.IO. If unspecified, Sails will create a client for you. | | `subClient` | ((ref)) | `undefined` | A custom Redis client used for _subscribing_ to channels used by Socket.IO. If unspecified, Sails will create a client for you. | | `adminPubClient`| ((ref)) | `undefined` | A custom Redis client for _publishing_ on the internal Sails admin bus, which allows for inter-server communication. If you provide a client for `pubClient`, you'll likely need to provide a client for this setting as well. | `adminSubClient`| ((ref)) | `undefined` | A custom Redis client for _subscribing_ to the internal Sails admin bus, which allows for inter-server communication. If you provide a client for `subClient`, you'll likely need to provide a client for this setting as well. | `subEvent` | ((string)) | `message` | The Redis client event name to subscribe to. When using clients created with `ioredis`, you’ll likely need to set this to `messageBuffer`. | ### Notes > + In older versions of Sails (<v0.11) and Socket.IO (<v1.0), the `beforeConnect` setting was called `authorization`. <docmeta name="displayName" value="sails.config.sockets"> <docmeta name="pageType" value="property"> ================================================ FILE: docs/reference/sails.config/sails.config.views.md ================================================ # `sails.config.views` Configuration for your app's server-side [views](https://sailsjs.com/documentation/concepts/Views). The options are conventionally specified in the [`config/views.js`](https://sailsjs.com/documentation/anatomy/config/views.js) configuration file. ### Properties | Property | Type | Default | Details | |:------------|:----------:|:----------|:--------| | `layout` | ((string)) -or- ((boolean)) | `"layout"` | Set the default [layout](https://sailsjs.com/documentation/concepts/views/layouts) for your app by specifying the relative path to the desired layout file from your views folder (i.e. `views/`), or disable layout support altogether with `false`. Built-in support for layouts is only relevant when using `ejs` (see below). | `extension` | ((string)) | "ejs" | The file extension for view files. | | `getRenderFn` | ((function)) | none | A function that Sails will call to get the rendering function for your desired view engine. See the [view engine documentation](http://sailsjs.com/documentation/concepts/views/view-engines) for more info about specifying a `getRenderFn` value. If this setting is undefined, Sails will use the built-in EJS renderer. | `locals` | ((dictionary)) | `{}` | Default data to be included as [view locals](http://sailsjs.com/documentation/concepts/views/locals) every time a server-side view is compiled anywhere in this app. If an optional `locals` argument was passed in directly via `res.view()`, its properties take precedence when both dictionaries are merged and provided to the view (more on that below). | ### Notes > + If your app is NOT using `ejs` (the default view engine) Sails will function as if the `layout` option was set to `false`. To take advantage of layouts when using a custom view engine like Jade or Handlebars, check out [that view engine's documentation](https://sailsjs.com/documentation/concepts/views/view-engines) to find the appropriate syntax. > + As of Sails 0.12.0, app-wide locals from `sails.config.views.locals` are combined with any one-off locals you use with `res.view()` using a **shallow merge strategy**. That is, if your app-wide locals configuration is `{foo: 3, bar: { baz: 'beep' } }`, and then you use `res.view({bar: 'boop'})`, your view will have access to `foo` (`3`) and `bar` (`'boop'`). <docmeta name="displayName" value="sails.config.views"> <docmeta name="pageType" value="property"> ================================================ FILE: docs/reference/waterline/datastores/datastores.md ================================================ # Working with datastores **Datastores** represent the data sources configured for your app. A datastore usually represents a particular database, whether that's a database running within a locally installed MySQL server, a remote PostgreSQL database running in your company's data center, or a remote MongoDB database hosted by a cloud provider. ### Configuring datastores Datastores are configured in [`sails.config.datastores`](https://sailsjs.com/documentation/reference/configuration/sails-config-datastores). Sails apps start out with an implicit datastore which is used by all of your models by default. For many apps, this is sufficient, but if you are building an app that needs to work with multiple databases, you may also find it helpful to configure additional, named datastores like `legacyProductDb`. ### Using datastores without a model Every [model](https://sailsjs.com/documentation/concepts/models-and-orm/models) in a Sails app is wired up to a particular datastore, so every time you call a built-in model method, the model communicates with its configured datastore implicitly. Even so, it's sometimes useful to be able to communicate with a datastore _outside_ of the context of any particular model. So, when your app lifts, Sails automatically instantiates objects called _registered datastore instances_ for each of your configured datastores. To access one of these at runtime, call either [`sails.getDatastore()`](https://sailsjs.com/documentation/reference/application/sails-get-datastore) or the [`.getDatastore()` model method](https://sailsjs.com/documentation/reference/waterline-orm/models/get-datastore). Registered datastores expose some methods and properties of their own, like `.leaseConnection()` and `.manager`, which provide an easy way to talk directly to the underlying database. (The rest of the pages in this section of the documentation are devoted to covering these datastore methods and properties in detail.) <docmeta name="displayName" value="Datastores"> ================================================ FILE: docs/reference/waterline/datastores/driver.md ================================================ # `.driver` The generic, stateless, low-level driver for this datastore (if supported by the adapter). ```usage datastore.driver; ``` > This property is not guaranteed to exist for all database adapters. If the datastore's underlying adapter does not support the [standardized driver interface](https://github.com/node-machine/driver-interface), then `driver` will not exist. ### Example Imagine you're building your own structured data visualizer (e.g. phpMyAdmin). You might want to connect to any number of different databases dynamically. ```javascript // Get the generic, stateless driver for our database (e.g. MySQL). var Driver = sails.getDatastore().driver; // Create our own dynamic connection manager (e.g. connection pool) var manager = ( await Driver.createManager({ connectionString: req.param('connectionUrl') }) ).manager; var db; try { db = ( await Driver.getConnection({ manager: managerReport.manager }) ).connection; } catch (err) { await Driver.destroyManager({ manager: managerReport.manager }); throw err; } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - // Do some stuff here... // e.g. // await Driver.sendNativeQuery({ // connection: db, // nativeQuery: '...' // }); // - - - - - - - - - - - - - - - - - - - - - - - - - - - - // Finally, before we continue, tear down the dynamic connection manager. // (this also takes care of releasing the active connection we acquired above) await Driver.destroyManager({ manager: managerReport.manager }); return res.ok(); ``` <docmeta name="displayName" value=".driver"> <docmeta name="pageType" value="property"> ================================================ FILE: docs/reference/waterline/datastores/leaseConnection.md ================================================ # `.leaseConnection()` Lease a new connection from the datastore for use in running multiple queries on the same connection (i.e. so that the logic provided in `during` can reuse the db connection). ```usage await datastore.leaseConnection(during); ``` _Or_ + `var result = await datastore.leaseConnection(during);` ### Usage | | Argument | Type | Details |---|---------------------|---------------------|:------------| | 1 | during | ((function)) | A [procedural parameter](https://en.wikipedia.org/wiki/Procedural_parameter) that Sails will call automatically when a connection has been obtained and made ready for you. It will receive the arguments specified in the "During" usage table below. | ##### During | | Argument | Type | Details |---|---------------------|---------------------|:------------| | 1 | db | ((ref)) | Your newly-leased database connection. (See [`.usingConnection()`](https://sailsjs.com/documentation/reference/waterline-orm/models/using-connection) for more information on what to do with this.) | > Note that prior to Sails 1.1.0, the recommended usage of `.leaseConnection()` expected your "during" code to call a callback (`proceed`) when it finished. This is no longer necessary as long as you do not actually include a second argument in the function signature of your "during" code. ##### Result | Type | Details | |---------------------|:---------------------------------------------------------------------------------| | ((Ref?)) | The optional result data sent back from `during`. In other words, if, in your `during` function, you did `return 'foo';`, then this will be `'foo'`. | ##### Errors | Name | Type | When? | |:----------------|---------------------|:---------------------------------------------------------------------------------| | UsageError | ((Error)) | Thrown if something invalid was passed in. | AdapterError | ((Error)) | Thrown if something went wrong in the database adapter. | Error | ((Error)) | Thrown if anything else unexpected happens. See [Concepts > Models and ORM > Errors](https://sailsjs.com/documentation/concepts/models-and-orm/errors) for examples of negotiating errors in Sails and Waterline. ### Example Lease a database connection from the default datastore, then use it to send two queries before releasing it back to the pool. ```javascript var inventory = await sails.getDatastore() .leaseConnection(async (db)=> { var location = await Location.findOne({ id: inputs.locationId }) .usingConnection(db); if (!location) { let err = new Error('Cannot find location with that id (`'+inputs.locationId+'`)'); err.code = 'E_NO_SUCH_LOCATION'; throw err; } // Get all products at the location var productOfferings = await ProductOffering.find({ location: inputs.locationId }) .populate('productType') .usingConnection(db); return productOfferings; }) .intercept('E_NO_SUCH_LOCATION', 'notFound'); // All done! Whatever we were doing with that database connection worked. // Now we can proceed with our business. ``` <docmeta name="displayName" value=".leaseConnection()"> <docmeta name="pageType" value="method"> ================================================ FILE: docs/reference/waterline/datastores/manager.md ================================================ # `.manager` The live connection manager for this datastore. ```usage datastore.manager ``` > Depending on the adapter, this might represent a connection pool, a single connection, or just a reference to a preconfigured client library instance. ### Example Access a raw Mongo collection instance representing a model `Pet`. ```javascript // Since the db connection manager exposed by `sails-mongo` is actually // the same as the Mongo client's `db` instance, we can treat it as such. var db = Pet.getDatastore().manager; // Now we can do anything we could do with a Mongo `db` instance: var rawMongoCollection = db.collection(Pet.tableName); ``` <docmeta name="displayName" value=".manager"> <docmeta name="pageType" value="property"> ================================================ FILE: docs/reference/waterline/datastores/sendNativeQuery.md ================================================ # `.sendNativeQuery()` Execute a raw SQL query using this datastore. ```usage var rawResult = await datastore.sendNativeQuery(sql, valuesToEscape); ``` > `.sendNativeQuery()` is only available on Sails/Waterline [datastores](https://sailsjs.com/documentation/reference/waterline-orm/datastores) that are configured to use a SQL database (e.g. MySQL, SQL Server, or PostgreSQL). Note that exact SQL and result format varies between databases, so you'll need to refer to the documentation for your underlying database adapter. (See below for a simple example to help get you started.) ### Usage | | Argument | Type | Details |---|---------------------|---------------------|:------------| | 1 | sql | ((string)) | A SQL string written in the appropriate dialect for this database. Allows template syntax like `$1`, `$2`, etc. (See example below.) If you are using custom table names or column names, be sure to reference those directly (rather than model identities and attribute names). | | 2 | valuesToEscape | ((array?)) | An array of dynamic, untrusted strings to SQL-escape and inject within `sql`. _(If you have no dynamic values to inject, then just omit this argument or pass in an empty array here.)_ ##### Result | Type | Details | |:--------------------|:---------------------------------------------------------------------------------| | ((Ref?)) | The raw result from the database adapter, if any. _(The exact format of this raw result data varies depending on the SQL query you passed in, as well as the adapter/dialect you're using. See example below for links to relevant documentation.)_ | ##### Errors | Name | Type | When? | |:----------------|---------------------|:---------------------------------------------------------------------------------| | UsageError | ((Error)) | Thrown if something invalid was passed in. | AdapterError | ((Error)) | Thrown if something went wrong in the database adapter. | Error | ((Error)) | Thrown if anything else unexpected happens. See [Concepts > Models and ORM > Errors](https://sailsjs.com/documentation/concepts/models-and-orm/errors) for examples of negotiating errors in Sails and Waterline. ### Example > Below, you'll find a generic example that works with just about any relational database. **But remember**: usage and result data vary depending on the SQL query you send, as well as on the adapter/dialect you're using. The standard [MySQL adapter](https://sailsjs.com/documentation/concepts/extending-sails/adapters/available-adapters#?sailsmysql) for Sails and Waterline uses the [`mysql`](http://npmjs.com/package/mysql) NPM package. The [PostgreSQL adapter](https://sailsjs.com/documentation/concepts/extending-sails/adapters/available-adapters#?sailspostgresql) uses [`pg`](http://npmjs.com/package/pg). ```js // Build our SQL query template. var NAMES_OF_PETS_SQL = ` SELECT pet.name FROM pet WHERE pet.species_label = $1 OR pet.species_label = $2`; // Send it to the database. var rawResult = await sails.sendNativeQuery(NAMES_OF_PETS_SQL, [ 'dog', 'cat' ]); sails.log(rawResult); // (result format depends on the SQL query that was passed in, and the adapter/dialect you're using) // Then parse the raw result and do whatever you like with it. return exits.success(); ``` ### Custom table/column names The SQL query you write should refer to table names and column names, not model identities and attribute names. If your models are defined with custom table names, or if their attributes are defined with custom column names, you'll want to be sure you're using those custom names in your native SQL queries. Are you using custom table/column names and concerned about scattering them throughout your code, because they might change? Fortunately, there's a way to work around this. By using the underlying references to `tableName` and `columnName` available on your Waterline model, you can build your SQL query templates without directly referencing column name and table names. For example: ```js var NAMES_OF_PETS_SQL = ` SELECT ${Pet.tableName}.${Pet.schema.name.columnName} FROM ${Pet.tableName} WHERE ${Pet.tableName}.${Pet.schema.speciesLabel.columnName} = $1 OR ${Pet.tableName}.${Pet.schema.speciesLabel.columnName} = $2 `; ``` Be aware that you still have to deal with custom column names on the way out! The `rawResult` you get back from `.sendNativeQuery()` is inherently database-specific and tied to the physical layer, thus it will inherit any complexity you've set up there (including custom table/column names from your model definitions). ### Notes > + This method only works with SQL databases. If you are using another database like MongoDB, use [`.manager`](https://sailsjs.com/documentation/reference/waterline-orm/datastores/manager) to get access to the raw MongoDB client, or [`.driver`](https://sailsjs.com/documentation/reference/waterline-orm/datastores/driver) to get access to the static, underlying db library (e.g. `mysql`, `pg`, etc.). > + Depending on the adapter you are using, the `valuesToEscape` may be mutated. This was a deliberate decision that was made for performance reasons, but may change in a future major version of Sails. For now if you are passing in a variable for `valuesToEscape` and you're using that variable later on in your code, clone it first. <docmeta name="displayName" value=".sendNativeQuery()"> <docmeta name="pageType" value="method"> ================================================ FILE: docs/reference/waterline/datastores/transaction.md ================================================ # `.transaction()` Fetch a preconfigured, deferred object hooked up to the sails-mysql or sails-postgresql adapter (and consequently the appropriate driver). ```usage await datastore.transaction(during); ``` or + `var result = await datastore.transaction(during);` ### Usage | | Argument | Type | Details |---|---------------------|---------------------|:------------| | 1 | during | ((function)) | See parameters in the "`during` usage" table below. | ##### During | | Argument | Type | Details |---|---------------------|---------------------|:------------| | 1 | db | ((ref)) | The leased (transactional) database connection. (See [`.usingConnection()`](https://sailsjs.com/documentation/reference/waterline-orm/queries/using-connection) for more information on what to do with this.) | > Note that prior to Sails 1.1.0, the recommended usage of `.transaction()` expected your "during" code to call a callback (`proceed`) when it finished. This is no longer necessary as long as you do not actually include a second argument in the function signature of your "during" code. ##### Result | Type | Details | |---------------------|:---------------------------------------------------------------------------------| | ((Ref?)) | The optional result data sent back from `during`. In other words, if in your `during` function you did `return 'foo';`, then this will be `'foo'`. | ##### Errors | Name | Type | When? | |:----------------|---------------------|:---------------------------------------------------------------------------------| | UsageError | ((Error)) | Thrown if something invalid was passed in. | AdapterError | ((Error)) | Thrown if something went wrong in the database adapter. | Error | ((Error)) | Thrown if anything else unexpected happens. See [Concepts > Models and ORM > Errors](https://sailsjs.com/documentation/concepts/models-and-orm/errors) for examples of negotiating errors in Sails and Waterline. ### Example Subtract the specified amount from one user's balance and add it to another's. ```javascript // e.g. in an action: var flaverr = require('flaverr'); await sails.getDatastore() .transaction(async (db)=> { var myAccount = await BankAccount.findOne({ owner: this.req.session.userId }) .usingConnection(db); if (!myAccount) { throw new Error('Consistency violation: Database is corrupted-- logged in user record has gone missing'); } var recipientAccount = await BankAccount.findOne({ owner: inputs.recipientId }).usingConnection(db) if (!recipientAccount) { throw flaverr('E_NO_SUCH_RECIPIENT', new Error('There is no recipient with that id')); } // Do the math to subtract from the logged-in user's account balance, // and add to the recipient's bank account balance. var myNewBalance = myAccount.balance - inputs.amount; // If this would put the logged-in user's account balance below zero, // then abort. (The transaction will be rolled back automatically.) if (myNewBalance < 0) { throw flaverr('E_INSUFFICIENT_FUNDS', new Error('Insufficient funds')); } // Update the current user's bank account await BankAccount.update({ owner: this.req.session.userId }) .set({ balance: myNewBalance }) .usingConnection(db); // Update the recipient's bank account await BankAccount.update({ owner: inputs.recipientId }) .set({ balance: recipientAccount.balance + inputs.amount }) .usingConnection(db); }) .intercept('E_INSUFFICIENT_FUNDS', ()=>'badRequest') .intercept('E_NO_SUCH_RECIPIENT', ()=>'notFound'); ``` > Note that the example above is just a demonstration; in practice, this kind of increment/decrement logic should also include row-level locking. [Unsure?](https://sailsjs.com/support). <docmeta name="displayName" value=".transaction()"> <docmeta name="pageType" value="method"> ================================================ FILE: docs/reference/waterline/models/addToCollection.md ================================================ # `.addToCollection()` Add one or more existing child records to the specified collection (e.g. the `comments` of BlogPost #4). ```usage await Something.addToCollection(parentId, association) .members(childIds); ``` ### Usage | | Argument | Type | Details | |---|:--------------------|----------------------------------------------|:-----------------------------------| | 1 | parentId | ((number)) or ((string)) | The primary key value(s) (i.e. ids) for the parent record(s). <br/>Must be a number or string (e.g. `'507f191e810c19729de860ea'` or `49`). <br/>Alternatively, an array of numbers or strings may be specified (e.g. `['507f191e810c19729de860ea', '14832ace0c179de897']` or `[49, 32, 37]`). In this case, _all_ of the child records will be added to the appropriate collection of each parent record. | 2 | association | ((string)) | The name of the plural ("collection") association (e.g. "pets"). | 3 | childIds | ((array)) | The primary key values (i.e. ids) of the child records to add. _Note that this does not [create](https://sailsjs.com/documentation/reference/waterline-orm/models/create) these child records, it just links them to the specified parent(s)._ ##### Errors | Name | Type | When? | |:----------------|---------------------|:---------------------------------------------------------------------------------| | UsageError | ((Error)) | Thrown if something invalid was passed in. | AdapterError | ((Error)) | Thrown if something went wrong in the database adapter. | Error | ((Error)) | Thrown if anything else unexpected happens. See [Concepts > Models and ORM > Errors](https://sailsjs.com/documentation/concepts/models-and-orm/errors) for examples of negotiating errors in Sails and Waterline. ### Example For user 3, add pets 99 and 98 to the "pets" collection: ```javascript await User.addToCollection(3, 'pets') .members([99,98]); ``` > If either user record already has one of those pets in its "pets", then we just silently skip over it. ### Edge cases + If an empty array of child ids is provided, then this is a <a href="https://en.wikipedia.org/wiki/NOP_(code)" target="_blank">no-op</a>. + If an empty array of parent ids is provided, then this is a <a href="https://en.wikipedia.org/wiki/NOP_(code)" target="_blank">no-op</a>. + If the parent id (or any _one_ of the parent ids, if specified as an array) does not actually correspond with an existing, persisted record, the exact behavior depends on what kind of association this is: + If this collection is a 1-way association, or a 2-way association where the other side is plural ([many-to-many](https://sailsjs.com/documentation/concepts/models-and-orm/associations/many-to-many)), then Waterline **pretends like the parent record(s) exist anyways**, tracking their relationships as prearranged, "aspirational" junction records in the database. + If this is a 2-way association where the other side is singular ([one-to-many](https://sailsjs.com/documentation/concepts/models-and-orm/associations/one-to-many)), then the missing parent records are simply ignored. + Along the same lines, if one of the child ids does not actually correspond with an existing, persisted record, then: + If this is a 1-way association, or a 2-way association where the other side is plural ([many-to-many](https://sailsjs.com/documentation/concepts/models-and-orm/associations/many-to-many)), then Waterline **pretends like these hypothetical child record(s) exist anyways**, tracking their relationships as prearranged, "aspirational" junction records in the database. + If this is a 2-way association where the other side is singular ([one-to-many](https://sailsjs.com/documentation/concepts/models-and-orm/associations/one-to-many)), then the missing child records are simply ignored. + If a parent record's collection _already has_ one or more of these children as members, then, for performance reasons, those memberships might be tracked again (e.g. stored in your database's join table multiple times). In most cases, that's OK, since it usually doesn't affect future queries (for example, when populating the relevant parent record's collection, the double-tracked relationship will not result in the child being listed more than once). If you do need to prevent duplicate join table records, there's an easy way to work around this—assuming you are using a relational database like MySQL or PostgreSQL, then you can create a multi-column index on your join table. Doing so will cause queries like this to result in an AdapterError with `code: 'E_UNIQUE'`. ### Notes > + This method can be used with [`await`](https://github.com/mikermcneil/parley/tree/49c06ee9ed32d9c55c24e8a0e767666a6b60b7e8#usage), promise chaining, or [traditional Node callbacks](https://sailsjs.com/documentation/reference/waterline-orm/queries/exec). > + If the association is "2-way" (meaning it has `via`) then the child records will be modified accordingly. If the attribute on the other side is singular, then each child record's foreign key will be changed. If it's plural, then each child record's collection will be modified accordingly. > + In addition, if the `via` points at a singular ("model") attribute on the other side, then `.addToCollection()` will "steal" these child records if necessary. For example, imagine you have an Employee model with this plural ("collection") attribute: `involvedInPurchases: { collection: 'Purchase', via: 'cashier' }`. If you executed `Employee.addToCollection(7, 'involvedInPurchases', [47])` to assign this purchase to employee #7 (Dolly), but purchase #47 was already associated with a different employee (e.g. #12, Motoki), then this would "steal" the purchase from Motoki and give it to Dolly. In other words, if you executed `Employee.find([7, 12]).populate('involvedInPurchases')`, Dolly's `involvedInPurchases` array would contain purchase #47 and Motoki's would not. <docmeta name="displayName" value=".addToCollection()"> <docmeta name="pageType" value="method"> ================================================ FILE: docs/reference/waterline/models/archive.md ================================================ # `.archive()` Archive ("soft-delete") records that match the specified criteria, saving them as new records in the built-in Archive model, then destroying the originals. ```usage await Something.archive(criteria); ``` #### Usage | | Argument | Type | Details | |---|:--------------------|----------------------------------------------|:-----------------------------------| | 1 | criteria | ((dictionary)) | Records that match this [Waterline criteria](https://github.com/balderdashy/waterline-docs/blob/master/queries/query-language.md) will be archived. Be warned, if you specify an empty dictionary (`{}`) as your criteria, _all records will be destroyed!_ | ##### Callback | | Argument | Type | Details | |---|:--------------------|---------------------|:-----------------------------------------------------------------------------| | 1 | err | ((Error?)) | The error that occurred, or `null` if there were no errors. | 2 | _archivedRecords_ | ((array?)) of ((dictionary)) | For improved performance, the archived records are not provided to this callback by default. But if you chain `.fetch()`, then the recently archived records will be sent back. (Be aware that this requires an extra database query in some adapters.) ### Example To archive a particular user in the the database, use [`.archiveOne()`](https://sailsjs.com/documentation/reference/waterline/archive-one). Or to archive multiple records in the the database: ```javascript await Pet.archive({ lastActiveAt: { '<': Date.now()-1000*60*60*24*365 } }); ``` ### Accessing archived records If you need to access archived records in the future, you can do so by searching the Archive model. For example, you might pass in the original record's primary key and [model identity](https://sailsjs.com/documentation/reference/waterline-orm/models#?sailsmodels) as constraints in a query. For example, to retrieve the archive describing the user we got rid of above: ```javascript var archive = await Archive.findOne({ fromModel: 'user', originalRecordId: 1 }); // The data from the original record is stored as `archive.originalRecord`. ``` ### Notes > This method is best used in situations where you would otherwise use [`.destroy()`](https://sailsjs.com/documentation/reference/waterline-orm/models/destroy), but you still need to keep the deleted data somewhere (e.g. for compliance reasons). If you anticipate needing to access the data again in your app (e.g. if you allow un-deleting), you may want to consider using an `isDeleted` flag instead, since archived records are more difficult to work with programmatically. (There is no built-in "unarchive".) <docmeta name="displayName" value=".archive()"> <docmeta name="pageType" value="method"> ================================================ FILE: docs/reference/waterline/models/archiveOne.md ================================================ # `.archiveOne()` Archive ("soft-delete") the record that matches the specified criteria, saving it (if it exists) as a new record in the built-in Archive model, then destroying the original. ```usage var originalRecord = await Something.archiveOne(criteria); ``` > Before attempting to modify the database, Waterline will check to see if the given criteria would match more than one record and, if so, it will throw an error instead of proceeding. ### Usage | | Argument | Type | Details | |---|:--------------------|-------------------|:-----------------------------------| | 1 | criteria | ((dictionary)) | The [Waterline criteria](https://sailsjs.com/documentation/concepts/models-and-orm/query-language) to use for matching the record in the database. ##### Result | Type | Description | |:--------------------|:-----------------| | ((dictionary?)) | Since this method never archives more than one record, if a record is archived then it is always provided as a result. Otherwise, this returns `undefined`. ##### Errors See [Concepts > Models and ORM > Errors](https://sailsjs.com/documentation/concepts/models-and-orm/errors) for examples of negotiating errors in Sails and Waterline. ### Example ```javascript var finn = await User.archiveOne({ firstName: 'Finn' }); if (finn) { sails.log('Archived the user named "Finn".'); } else { sails.log('The database does not have a user named "Finn".'); } ``` ### Notes > This method is best used in situations where you would otherwise use [`.destroyOne()`](https://sailsjs.com/documentation/reference/waterline-orm/models/destroy-one), but you still need to keep the deleted data somewhere (for compliance reasons, maybe). If you anticipate needing to access the data again in your app (if you allow un-deleting, for example), you may want to consider using an `isDeleted` flag instead, since archived records are more difficult to work with programmatically. (There is no built-in "unarchive".) > + This method **does not support .fetch()**, because it _always_ returns the archived record, if one was matched. <docmeta name="displayName" value=".archiveOne()"> <docmeta name="pageType" value="method"> ================================================ FILE: docs/reference/waterline/models/avg.md ================================================ # `.avg()` Get the aggregate mean of the specified attribute across all matching records. ```usage var average = await Something.avg(numericAttrName, criteria); ``` ### Usage | | Argument | Type | Details | |---|:--------------------|----------------------------------------------|:-----------------------------------| | 1 | numericAttrName | ((string)) | The name of the numeric attribute whose mean will be calculated. | 2 | _criteria_ | ((dictionary?)) | The [Waterline criteria](https://sailsjs.com/documentation/concepts/models-and-orm/query-language) to use for matching records in the database. If no criteria is specified, the average will be computed across _all_ of this model's records. `avg` queries do not support pagination using `skip` and `limit` or projections using `select`. ##### Result | Type | Description | |---------------------|:-----------------| | ((number)) | The aggregate mean of the specified attribute across all matching records. ##### Errors | Name | Type | When? | |:----------------|---------------------|:---------------------------------------------------------------------------------| | UsageError | ((Error)) | Thrown if something invalid was passed in. | AdapterError | ((Error)) | Thrown if something went wrong in the database adapter. | Error | ((Error)) | Thrown if anything else unexpected happens. See [Concepts > Models and ORM > Errors](https://sailsjs.com/documentation/concepts/models-and-orm/errors) for examples of negotiating errors in Sails and Waterline. ### Example Get the average balance of bank accounts owned by people between the ages of 35 and 45. ```javascript var averageBalance = await BankAccount.avg('balance') .where({ ownerAge: { '>=': 35, '<=': 45 } }); ``` ### Notes > + This method can be used with [`await`](https://github.com/mikermcneil/parley/tree/49c06ee9ed32d9c55c24e8a0e767666a6b60b7e8#usage), promise chaining, or [traditional Node callbacks](https://sailsjs.com/documentation/reference/waterline-orm/queries/exec). > + Some databases like MySQL may return `null` for this kind of query, however it's best practice for Sails/Waterline adapter authors to return `0` for consistency and type safety in app-level code. <docmeta name="displayName" value=".avg()"> <docmeta name="pageType" value="method"> ================================================ FILE: docs/reference/waterline/models/count.md ================================================ # `.count()` Get the total number of records matching the specified criteria. ```usage var numRecords = await Model.count(criteria); ``` ### Usage | # | Argument | Type | Details | |---|---------------|:----------------------|:-----------| | 1 | _criteria_ | ((dictionary?)) | The [Waterline criteria](https://sailsjs.com/documentation/concepts/models-and-orm/query-language) to use for matching records in the database. Note that `count` queries do not support pagination using `skip` and `limit` or projections using `select`. ##### Result | Type | Description | |---------------------|:-----------------| | ((number)) | The number of records from your database that match the given criteria. ##### Errors | Name | Type | When? | |:--------------------|---------------------|:-------------------------------------------------------------| | UsageError | ((Error)) | Thrown if something invalid was passed in. | AdapterError | ((Error)) | Thrown if something went wrong in the database adapter. | Error | ((Error)) | Thrown if anything else unexpected happens. See [Concepts > Models and ORM > Errors](https://sailsjs.com/documentation/concepts/models-and-orm/errors) for examples of negotiating errors in Sails and Waterline. ### Example ```javascript var total = await User.count({name:'Flynn'}); sails.log(`There ${total===1?'is':'are'} ${total} user${total===1?'':'s'} named "Flynn".`); ``` ### Notes > + This method can be used with [`await`](https://github.com/mikermcneil/parley/tree/49c06ee9ed32d9c55c24e8a0e767666a6b60b7e8#usage), promise chaining, or [traditional Node callbacks](https://sailsjs.com/documentation/reference/waterline-orm/queries/exec). <docmeta name="displayName" value=".count()"> <docmeta name="pageType" value="method"> ================================================ FILE: docs/reference/waterline/models/create.md ================================================ # `.create()` Create a record in the database. ```usage await Something.create(initialValues); ``` or + `var createdRecord = await Something.create(initialValues).fetch();` ### Usage | | Argument | Type | Details | |---|:--------------------|------------------------------|:--------------------------------------| | 1 | initialValues | ((dictionary)) | The initial values for the new record. _(Note that, if this model is in ["schemaful" mode](https://sailsjs.com/documentation/concepts/models-and-orm/model-settings#?schema), then any extraneous keys will be silently omitted.)_ > **Note**: For performance reasons, as of Sails v1.0 / Waterline 0.13, the `initialValues` dictionary passed into this model method will be mutated in-place in most situations (whereas in Sails/Waterline v0.12, this was not necessarily the case). ##### Result | Type | Description | |---------------------|:-----------------| | ((dictionary?)) | For improved performance, the created record is not provided as a result by default. But if you chain `.fetch()`, then the newly-created record will be sent back. (Be aware that this requires an extra database query in some adapters.) ##### Errors | Name | Type | When? | |--------------------|---------------------|:---------------------------------------------------------------------------------| | UsageError | ((Error)) | Thrown if something invalid was passed in. | AdapterError | ((Error)) | Thrown if something went wrong in the database adapter. See [Concepts > Models and ORM > Errors](https://sailsjs.com/documentation/concepts/models-and-orm/errors) for an example of how to negotiate a uniqueness error (i.e. from attempting to create a record with a duplicate that would violate a uniqueness constraint). | Error | ((Error)) | Thrown if anything else unexpected happens. See [Concepts > Models and ORM > Errors](https://sailsjs.com/documentation/concepts/models-and-orm/errors) for examples of negotiating errors in Sails and Waterline. ##### Meta keys | Key | Type | Details | |:--------------------|-------------------|:---------------------------------------------------------------| | fetch | ((boolean)) | If set to `true`, then the created record will be sent back.<br/><br/>Defaults to `false`. > For more information on meta keys, see [.meta()](https://sailsjs.com/documentation/reference/waterline-orm/queries/meta). ### Example To create a user named Finn in the database: ```javascript await User.create({name:'Finn'}); return res.ok(); ``` ##### Fetching the newly-created record ```javascript var createdUser = await User.create({name:'Finn'}).fetch(); sails.log('Finn\'s id is:', createdUser.id); ``` ### Negotiating errors It's important to always handle errors from model methods. But sometimes, you need to look at errors in a more granular way. To learn more about the kinds of errors Waterline returns, and for examples of how to handle them, see [Concepts > Models and ORM > Errors](https://sailsjs.com/documentation/concepts/models-and-orm/errors). ### Notes > + This method can be used with [`await`](https://github.com/mikermcneil/parley/tree/49c06ee9ed32d9c55c24e8a0e767666a6b60b7e8#usage), promise chaining, or [traditional Node callbacks](https://sailsjs.com/documentation/reference/waterline-orm/queries/exec). <docmeta name="displayName" value=".create()"> <docmeta name="pageType" value="method"> ================================================ FILE: docs/reference/waterline/models/createEach.md ================================================ # `.createEach()` Create a set of records in the database. ```usage await Something.createEach(initialValues); ``` or + `var createdRecords = await Something.createEach(initialValues).fetch();` ### Usage | | Argument | Type | Details | |---|:--------------------|----------------------------------------------|:-----------------------------------| | 1 | initialValues | ((array?)) | An array of dictionaries with attributes for the new records. > **Note**: For performance reasons, as of Sails v1.0 / Waterline 0.13, the dictionaries in the `initialValues` array passed into this model method will be mutated in-place in most situations (whereas in Sails/Waterline v0.12, this was not necessarily the case). ##### Result | Type | Description | |---------------------|:-----------------| | ((array?)) of ((dictionary)) | The created records are not provided as a result by default, in order to optimize for performance. To override the default setting, chain `.fetch()` and the newly created records will be sent back. (Be aware that this requires an extra database query in some adapters.) ##### Errors | Name | Type | When? | |--------------------|---------------------|:---------------------------------------------------------------------------------| | UsageError | ((Error)) | Thrown if something invalid was passed in. | AdapterError | ((Error)) | Thrown if something went wrong in the database adapter. See [Concepts > Models and ORM > Errors](https://sailsjs.com/documentation/concepts/models-and-orm/errors) for an example of how to negotiate a uniqueness error (arising from an attempt to create a record with a duplicate value that would violate a uniqueness constraint). | Error | ((Error)) | Thrown if anything else unexpected happens. See [Concepts > Models and ORM > Errors](https://sailsjs.com/documentation/concepts/models-and-orm/errors) for examples of negotiating errors in Sails and Waterline. ##### Meta keys | Key | Type | Details | |:--------------------|-------------------|:---------------------------------------------------------------| | fetch | ((boolean)) | If set to `true`, then the created records will be sent back.<br/><br/>Defaults to `false`. > For more information on meta keys, see [.meta()](https://sailsjs.com/documentation/reference/waterline-orm/queries/meta). ### Example To create users named Finn and Jake in the database: ```javascript await User.createEach([{name:'Finn'}, {name: 'Jake'}]); ``` ##### Fetching newly created records ```javascript var createdUsers = User.createEach([{name:'Finn'}, {name: 'Jake'}]).fetch(); sails.log(`Created ${createdUsers.length} user${createdUsers.length===1?'':'s'}.`); ``` ### Notes > + This method can be used with [`await`](https://github.com/mikermcneil/parley/tree/49c06ee9ed32d9c55c24e8a0e767666a6b60b7e8#usage), promise chaining, or [traditional Node callbacks](https://sailsjs.com/documentation/reference/waterline-orm/queries/exec). > + The number of records you can add with `.createEach` is limited by the maximum query size of the particular database you’re using. MySQL has a 4MB limit by default, but this can be changed via the [`max_allowed_packet` setting](https://dev.mysql.com/doc/refman/5.7/en/server-system-variables.html#sysvar_max_allowed_packet). MongoDB imposes a 16MB limit on single documents, but essentially has no limit on the number of documents that can be created at once. PostgreSQL has a very large (around 1GB) maximum size. Consult your database’s documentation for more information about query limitations. > + Another thing to watch out for when doing very large bulk inserts is the maximum number of bound variables. This varies per databases but refers to the number of values being substituted in a query. See [maxmimum allowable parameters](http://stackoverflow.com/questions/6581573/what-are-the-max-number-of-allowable-parameters-per-database-provider-type) for more details. > + When using `.fetch()` and manually specifying primary key values for new records, the sort order of returned records is not guaranteed (it varies depending on the database adapter in use). <docmeta name="displayName" value=".createEach()"> <docmeta name="pageType" value="method"> ================================================ FILE: docs/reference/waterline/models/destroy.md ================================================ # `.destroy()` Destroy records in your database that match the given criteria. ```usage await Something.destroy(criteria); ``` or + `var destroyedRecords = await Something.destroy(criteria).fetch();` ### Usage | | Argument | Type | Details | |---|:--------------------|----------------------------------------------|:-----------------------------------| | 1 | criteria | ((dictionary)) | Records matching this [Waterline criteria](https://sailsjs.com/documentation/concepts/models-and-orm/query-language) will be destroyed. Be warned, if you specify an empty dictionary (`{}`) as your criteria, _all records will be destroyed!_ `destroy` queries do not support pagination using `skip` and `limit` or projections using `select`. | ##### Result | Type | Description | |---------------------|:-----------------| | ((array?)) of ((dictionary)) | The destroyed records are not provided as a result by default in order to optimize for performance. To override the default setting, chain `.fetch()` and the newly destroyed records will be sent back. (Be aware that this requires an extra database query in some adapters.) ##### Errors | Name | Type | When? | |-----------------|---------------------|:---------------------------------------------------------------------------------| | UsageError | ((Error)) | Thrown if something invalid was passed in. | AdapterError | ((Error)) | Thrown if something went wrong in the database adapter. | Error | ((Error)) | Thrown if anything else unexpected happens. See [Concepts > Models and ORM > Errors](https://sailsjs.com/documentation/concepts/models-and-orm/errors) for examples of negotiating errors in Sails and Waterline. ##### Meta keys | Key | Type | Details | |:--------------------|-------------------|:---------------------------------------------------------------| | fetch | ((boolean)) | If set to `true`, then the array of destroyed records will be sent back.<br/><br/>Defaults to `false`. > For more information on meta keys, see [.meta()](https://sailsjs.com/documentation/reference/waterline-orm/queries/meta). ### Example To delete any users named Finn from the database: ```javascript await User.destroy({name:'Finn'}); sails.log('Any users named Finn have now been deleted, if there were any.'); ``` To delete two particular users who have been causing trouble: ```javascript await User.destroy({ id: { in: [ 3, 97 ] } }); sails.log('The records for troublesome users (3 and 97) have been deleted, if they still existed.'); ``` ##### Fetching destroyed records To delete a particular book and fetch the destroyed record, use [.destroyOne()](https://sailsjs.com/documentation/reference/waterline/destroy-one). To delete multiple books and fetch all destroyed records: ```javascript var burnedBooks = await Book.destroy({ controversiality: { '>': 0.9 } }).fetch(); sails.log('Deleted books:', burnedBooks); ``` ### Notes > + This method can be used with [`await`](https://github.com/mikermcneil/parley/tree/49c06ee9ed32d9c55c24e8a0e767666a6b60b7e8#usage), promise chaining, or [traditional Node callbacks](https://sailsjs.com/documentation/reference/waterline-orm/queries/exec). > + If you want to confirm that one or more records exist before destroying them, you should first perform a `find()`. However, it is generally a good idea to _try to do things_ rather than _checking first_, lest you end up with a [race condition](http://people.cs.umass.edu/~emery/classes/cmpsci377/f07/scribe/scribe8-1.pdf). <docmeta name="displayName" value=".destroy()"> <docmeta name="pageType" value="method"> ================================================ FILE: docs/reference/waterline/models/destroyOne.md ================================================ # `.destroyOne()` Destroy the record in your database that matches the given criteria, if it exists. ```usage var destroyedRecord = await Something.destroyOne(criteria); ``` > Before attempting to modify the database, Waterline will check to see if more than one record matches the given criteria. If so, it will throw an error instead of proceeding. ### Usage | | Argument | Type | Details | |---|:--------------------|-------------------|:-----------------------------------| | 1 | criteria | ((dictionary)) | The [Waterline criteria](https://sailsjs.com/documentation/concepts/models-and-orm/query-language) to use for matching the record in the database. ##### Result | Type | Description | |:--------------------|:-----------------| | ((dictionary?)) | Since `.destroyOne()` never destroys more than one record, if a record is destroyed then it is always provided as a result. Otherwise, `undefined` is returned. ##### Errors See [Concepts > Models and ORM > Errors](https://sailsjs.com/documentation/concepts/models-and-orm/errors) for examples of negotiating errors in Sails and Waterline. ### Example ```javascript var burnedBook = await User.destroyOne({id: 4}) if (burnedBook) { sails.log('Deleted book with `id: 4`.'); } else { sails.log('The database does not have a book with `id: 4`.'); } ``` ### Notes > + Because it _always_ returns the destroyed record, if one was matched, this method **does not support .fetch()**. > + This method can be used with [`await`](https://github.com/mikermcneil/parley/tree/49c06ee9ed32d9c55c24e8a0e767666a6b60b7e8#usage), promise chaining, or [traditional Node callbacks](https://sailsjs.com/documentation/reference/waterline-orm/queries/exec). <docmeta name="displayName" value=".destroyOne()"> <docmeta name="pageType" value="method"> ================================================ FILE: docs/reference/waterline/models/find.md ================================================ # `.find()` Find records in your database that match the given criteria. ```usage var records = await Something.find(criteria); ``` ### Usage | | Argument | Type | Details | |---|:--------------------|-------------------|:-----------------------------------| | 1 | criteria | ((dictionary)) | The [Waterline criteria](https://sailsjs.com/documentation/concepts/models-and-orm/query-language) to use for matching records in the database. ##### Result | Type | Description | |---------------------|:-----------------| | ((array)) of ((dictionary)) | The array of records from your database that match the given criteria. ##### Errors | Name | Type | When? | |:----------------|---------------------|:---------------------------------------------------------------------------------| | UsageError | ((Error)) | Thrown if something invalid was passed in. | AdapterError | ((Error)) | Thrown if something went wrong in the database adapter. | Error | ((Error)) | Thrown if anything else unexpected happens. See [Concepts > Models and ORM > Errors](https://sailsjs.com/documentation/concepts/models-and-orm/errors) for examples of negotiating errors in Sails and Waterline. ### Example ##### A basic find query To find any users named Finn in the database: ```javascript var usersNamedFinn = await User.find({name:'Finn'}); sails.log('Wow, there are %d users named Finn. Check it out:', usersNamedFinn.length, usersNamedFinn); ``` ##### Using projection Projection selectively omits the fields returned on found records. This is useful for achieving faster performance and greater security when passing found records to the client. The select clause in a [Waterline criteria](https://sailsjs.com/documentation/concepts/models-and-orm/query-language) takes an array of strings that correspond with attribute names. The record ID is always returned. ```javascript var usersNamedFinn = await User.find({ where: {name:'Finn'}, select: ['name', 'email'] }); ``` might yield: ```javascript [ { id: 7392, name: 'Finn', email: 'finn_2017@gmail.com' }, { id: 4427, name: 'Finn', email: 'walkingfinn@outlook.com' } // ...more users named Finn and their email addresses ] ``` ### Notes > + This method can be used with [`await`](https://github.com/mikermcneil/parley/tree/49c06ee9ed32d9c55c24e8a0e767666a6b60b7e8#usage), promise chaining, or [traditional Node callbacks](https://sailsjs.com/documentation/reference/waterline-orm/queries/exec). <docmeta name="importance" value="10"> <docmeta name="displayName" value=".find()"> <docmeta name="pageType" value="method"> ================================================ FILE: docs/reference/waterline/models/findOne.md ================================================ # `.findOne()` Attempt to find a particular record in your database that matches the given criteria. ```usage var record = await Something.findOne(criteria); ``` ### Usage | | Argument | Type | Details | |---|:--------------------|----------------------------------------------|:-----------------------------------| | 1 | criteria | ((dictionary)) | The [Waterline criteria](https://sailsjs.com/documentation/concepts/models-and-orm/query-language) to use for matching this record in the database. (This criteria must never match more than one record.) `findOne` queries do not support pagination using `skip` or `limit`. ##### Result | Type | Description | |---------------------|:-----------------| | ((dictionary?)) | The record that was found, or `undefined` if no such record could be located. ##### Errors | Name | Type | When? | |:----------------|---------------------|:---------------------------------------------------------------------------------| | UsageError | ((Error)) | Thrown if something invalid was passed in. | AdapterError | ((Error)) | Thrown if something went wrong in the database adapter. | Error | ((Error)) | Thrown if anything else unexpected happens. See [Concepts > Models and ORM > Errors](https://sailsjs.com/documentation/concepts/models-and-orm/errors) for examples of negotiating errors in Sails and Waterline. ### Example To locate the user whose username is "finn" in your database: ```javascript var finn = await Users.findOne({ username: 'finn' }); if (!finn) { sails.log('Could not find Finn, sorry.'); } else { sails.log('Found "%s"', finn.fullName); } ``` ### Notes > + This method can be used with [`await`](https://github.com/mikermcneil/parley/tree/49c06ee9ed32d9c55c24e8a0e767666a6b60b7e8#usage), promise chaining, or [traditional Node callbacks](https://sailsjs.com/documentation/reference/waterline-orm/queries/exec). > + Being unable to find a record with the given criteria does **not** constitute an error for `findOne()`. If no matching record is found, the result will be `undefined`. <docmeta name="importance" value="10"> <docmeta name="displayName" value=".findOne()"> <docmeta name="pageType" value="method"> ================================================ FILE: docs/reference/waterline/models/findOrCreate.md ================================================ # `.findOrCreate()` Find the record matching the specified criteria. If no such record exists, create one using the provided initial values. ```usage var newOrExistingRecord = await Something.findOrCreate(criteria, initialValues); ``` or, if you need to know whether a new record was created, ```usage Something.findOrCreate(criteria, initialValues) .exec(function(err, newOrExistingRecord, wasCreated) { }); ``` #### Usage | # | Argument | Type | Details | |---|---------------|:----------------------|:-----------| | 1 | _criteria_ | ((dictionary?)) | The [Waterline criteria](https://sailsjs.com/documentation/concepts/models-and-orm/query-language) to use for matching records in the database. **This particular criteria should always match exactly zero or one records in the database.** | 2 | initialValues | ((dictionary)) | The initial values for the new record, if one is created. #### Callback | | Argument | Type | Details | |---|:------------------------|---------------------|:---------------------------------------------------------------------------------| | 1 | _err_ | ((Error?)) | The error that occurred, or `undefined` if there were no errors. | 2 | _newOrExistingRecord_ | ((dictionary?)) | The record that was found, or `undefined` if no such record could be located. | 3 | wasCreated | ((boolean)) | Whether a new record was created. ##### Errors | Name | Type | When? | |:----------------|---------------------|:---------------------------------------------------------------------------------| | UsageError | ((Error)) | Thrown if something invalid was passed in. | AdapterError | ((Error)) | Thrown if something went wrong in the database adapter. | Error | ((Error)) | Thrown if anything else unexpected happens. See [Concepts > Models and ORM > Errors](https://sailsjs.com/documentation/concepts/models-and-orm/errors) for examples of negotiating errors in Sails and Waterline. ### Example Let's make sure our test user, Finn, exists: ```javascript User.findOrCreate({ name: 'Finn' }, { name: 'Finn' }) .exec(async(err, user, wasCreated)=> { if (err) { return res.serverError(err); } if(wasCreated) { sails.log('Created a new user: ' + user.name); } else { sails.log('Found existing user: ' + user.name); } }); ``` ### Notes > + This method can be used with [`await`](https://github.com/mikermcneil/parley/tree/49c06ee9ed32d9c55c24e8a0e767666a6b60b7e8#usage), promise chaining, or [traditional Node callbacks](https://sailsjs.com/documentation/reference/waterline-orm/queries/exec). If you use `await`, be aware that the result will be the record only—you will not have access to `wasCreated`. > + Behind the scenes, this uses `.findOne()`, so if more than one record in the database matches the provided criteria, there will be an error explaining so. <docmeta name="displayName" value=".findOrCreate()"> <docmeta name="pageType" value="method"> ================================================ FILE: docs/reference/waterline/models/getDatastore.md ================================================ # `.getDatastore()` Access the [datastore](https://sailsjs.com/documentation/concepts/models-and-orm#?datastores) for a particular model. ```usage Something.getDatastore(); ``` ### Usage ##### Returns **Type:** ((Dictionary)) A [datastore instance](https://sailsjs.com/documentation/reference/waterline-orm/datastores). ### Notes > + This is a synchronous method, so you don't need to use `await`, promise chaining, or traditional Node callbacks. <docmeta name="displayName" value=".getDatastore()"> <docmeta name="pageType" value="method"> ================================================ FILE: docs/reference/waterline/models/models.md ================================================ # Working with models This section of the documentation focuses on the model methods provided by Waterline out of the box. In addition to these, additional methods can come from hooks (like the [resourceful PubSub methods](https://sailsjs.com/documentation/reference/web-sockets/resourceful-pub-sub)) or be manually written in your app to wrap reusable custom code. > + For an in-depth introduction to models in Sails/Waterline, see [Concepts > Models and ORM > Models](https://sailsjs.com/documentation/concepts/models-and-orm/models). > + You can find an example of how to define a model [here](https://gist.github.com/rachaelshaw/f5bf442b2171154aa6021846d1a250f8). ### Built-in model methods In general, model methods are _asynchronous_, meaning you cannot just call them and use the return value. Instead, you must use callbacks, promises or async/await. Most built-in model methods accept a callback as an optional final argument. If the callback is not supplied, a chainable Query object is returned, which has methods like `.fetch()`, `.decrypt()`, and `.where()`. See [Working with Queries](https://sailsjs.com/documentation/reference/waterline-orm/queries) for more on that. Here are some of the most common model methods you will encounter building Node.js apps in Sails: Method | Summary --------------------- | ------------------------------------------------------------------------ `.find()` | Get an array of records which match the specified criteria. `.findOne()` | Get the record which matches the specified criteria, or `undefined` if there isn't one. `.updateOne()` | Update the record that matches the specified criteria, if there is one, using the specified `attrName:value` pairs. `.archiveOne()` | Archive ("soft-delete") the record that matches the specified criteria, if there is one. `.destroyOne()` | Permanently and irreversibly destroy the record that matches the specified criteria, if there is one. `.create()` | Create a new record consisting of the specified values. `.createEach()` | Create multiple new records at the same time. `.count()` | Count the total number of records that match certain criteria. `.sum()` | Compute the sum for a given attribute, totalled across all records that match certain criteria. `.avg()` | Compute the arithmetic mean for an attribute, averaged over all records that match certain criteria. `.addToCollection()` | Add existing records from an associated model to one of your collections. `.removeFromCollection()` | Remove record(s) from one of your collections. These methods are just the beginning. To read more about available model methods in Sails, check out the complete reference in the sidebar. <!-- Not actually all that common: `.replaceCollection()` | Replace all the members in one of your collections with a new set of records from its associated model. `.update()` | Update records matching the specified criteria, setting the specified `attrName:value` pairs. `.archive()` | Archive ("soft-delete") all records that match the specified criteria. `.stream()` | Get records that meet the specified criteria one at a time (or batch at a time). `.native()`/`query()` | Make a direct call to the underlying database using a native query. `.findOrCreate()` | Lookup a single record which matches the specified criteria, or create it if it doesn't. `.destroy()` | Destroy records matching the specified criteria. --> <!-- ![screenshot of the api/models/ folder in a text editor](http://i.imgur.com/xdTZpKT.png) --> ### `sails.models` If you need to disable global variables in Sails, you can still use `sails.models.<model_identity>` to access your models. > Not sure of your model's `identity`? Check out [Concepts > Models and ORM > Model settings](https://sailsjs.com/documentation/concepts/models-and-orm/model-settings#?identity). <docmeta name="displayName" value="Models"> ================================================ FILE: docs/reference/waterline/models/native.md ================================================ # `.native()` > **As of Sails v1.x, this method is deprecated.** > Instead, please change your code to use [`Model.getDatastore().manager`](https://sailsjs.com/documentation/reference/waterline-orm/datastores/manager), which offers a cleaner, simpler API. `.native()` is only available when using Sails/Waterline with MongoDB. Returns a raw Mongo collection instance representing the specified model, allowing you to perform raw Mongo queries. For full documentation and usage examples, check out the [native Node Mongo driver](https://github.com/mongodb/node-mongodb-native#introduction). Note that `sails-mongo` maintains a single Mongo connection for each of your configured datastores. Consequently, when using `.native()`, you don't need to close or open `db` manually. For lower-level usage, you can `require('mongodb')` directly. ### Example ```js Pet.native(function(err, collection) { if (err) return res.serverError(err); collection.find({}, { name: true }).toArray(function (err, results) { if (err) return res.serverError(err); return res.ok(results); }); }); ``` Source: https://gist.github.com/mikermcneil/483987369d54512b6104 ### Notes > + This method only works with Mongo! For raw functionality in SQL databases, use [`.query()`](https://sailsjs.com/documentation/reference/waterline-orm/models/query). <docmeta name="displayName" value=".native()"> <docmeta name="pageType" value="method"> <docmeta name="isDeprecated" value="true"> ================================================ FILE: docs/reference/waterline/models/query.md ================================================ # `.query()` > **As of Sails v1.0, this method is deprecated.** > Instead, please use [`Model.getDatastore().sendNativeQuery()`](https://sailsjs.com/documentation/reference/waterline-orm/datastores/send-native-query), the new version of this method that standardizes the format of SQL escape bindings, as well as fully supporting `.exec()` and promise-based usage. Execute a raw SQL query using the specified model's datastore. ```usage SomeModel.query(sql, valuesToEscape, function(err, rawResult) { }); ``` > **WARNING:** Unlike other Waterine model methods, `.query()` supports neither promise-based usage nor the use of `.exec()`. In other words, it does not utilize Waterline's normal deferred object mechanism. Instead, it provides raw access directly to the underlying database driver. ### Usage `.query()` is only available on Sails/Waterline models that are configured to use a SQL database (e.g. PostgreSQL or MySQL). Its purpose is to perform raw SQL queries. Note that exact usage and result format varies between adapters, so you'll need to refer to the documentation for the underlying database driver. (See below for a couple of simple examples to help get you started.) | | Argument | Type | Details | |---|:--------------------|-------------------|:-----------------------------------| | 1 | sql | ((string)) | A SQL string written in the appropriate dialect for this model's database. Allows template syntax, (e.g. `?`, `$1`) the exact style of which depends on the underlying database adapter. _(See examples below.)_ | 2 | valuesToEscape | ((array)) | An array of dynamic, untrusted strings to SQL-escape and inject within the SQL string using the appropriate template syntax for this model's database. _(If you have no dynamic values to inject, then just use an empty array here.)_ | 3 | done | ((function)) | A callback function that will be triggered when the query completes successfully, or if the adapter encounters an error. ##### Callback | | Argument | Type | Details | |---|:--------------------|---------------------|:---------------------------------------------------------------------------------| | 1 | _err_ | ((Error?)) | The error that occurred, or a falsy value if there were no errors. _(The exact format of this error varies depending on the SQL query you passed in and the database adapter you're using. See examples below for links to relevant documentation.)_ | 2 | _rawResult_ | ((Ref?)) | The raw result from the adapter. _(The exact format of this raw result data varies depending on the SQL query you passed in and the database adapter you're using. See examples below for links to relevant documentation.)_ ### Example Remember that usage and result data vary depending on the SQL query you send and the adapter you're using. Below, you'll find two examples: one for PostgreSQL and one for MySQL. ##### PostgreSQL example Communicate directly with [`pg`](http://npmjs.com/package/pg), an NPM package used for communicating with PostgreSQL databases: ```js Pet.query('SELECT pet.name FROM pet WHERE pet.name = $1', [ 'dog' ] ,function(err, rawResult) { if (err) { return res.serverError(err); } sails.log(rawResult); // (result format depends on the SQL query that was passed in, and the adapter you're using) // Then parse the raw result and do whatever you like with it. return res.ok(); }); ``` ##### MySQL example Assuming the `Pet` model is configured to use the `sails-mysql` adapter, the following code will communicate directly with [`mysql`](http://npmjs.com/package/mysql), an NPM package used for communicating with MySQL databases: ```js Pet.query('SELECT pet.name FROM pet WHERE pet.name = ?', [ 'dog' ] ,function(err, rawResult) { if (err) { return res.serverError(err); } sails.log(rawResult); // ...grab appropriate data... // (result format depends on the SQL query that was passed in, and the adapter you're using) // Then parse the raw result and do whatever you like with it. return res.ok(); }); ``` ### Notes > + This method only works with SQL databases. To get access to the raw MongoDB collection, use [`.native()`](https://sailsjs.com/documentation/reference/waterline-orm/models/native). > + This method **does not** support `.exec()` or `.then()`, and it **does not** return a promise. If you want to "promisify" `.query()`, have a look at [this](http://stackoverflow.com/questions/21886630/how-to-use-model-query-with-promises-in-sailsjs-waterline). <docmeta name="displayName" value=".query()"> <docmeta name="pageType" value="method"> <docmeta name="isDeprecated" value="true"> ================================================ FILE: docs/reference/waterline/models/removeFromCollection.md ================================================ # `.removeFromCollection()` Remove one or more members (e.g. a comment) from the specified collection (e.g. the `comments` of BlogPost #4). ```usage await Something.removeFromCollection(parentId, association) .members(childIds); ``` ### Usage | | Argument | Type | Details | |---|:--------------------|----------------------------------------------|:-----------------------------------| | 1 | parentId | ((number)) or ((string)) | The primary key value(s) (i.e. ids) for the parent record(s). <br/>Must be a number or string (e.g. `'507f191e810c19729de860ea'` or `49`). <br/>Alternatively, an array of numbers or strings may be specified (e.g. `['507f191e810c19729de860ea', '14832ace0c179de897']` or `[49, 32, 37]`). In this case, _all_ of the child records will be removed from the appropriate collection of each parent record. | 2 | association | ((string)) | The name of the plural ("collection") association (e.g. "pets") | 3 | childIds | ((array)) | The primary key values (i.e. ids) of the child records to remove. _Note that this does not [destroy](https://sailsjs.com/documentation/reference/waterline-orm/models/destroy) these records, it just detaches them from the specified parent(s)._ ##### Errors | Name | Type | When? | |:----------------|---------------------|:---------------------------------------------------------------------------------| | UsageError | ((Error)) | Thrown if something invalid was passed in. | AdapterError | ((Error)) | Thrown if something went wrong in the database adapter. | Error | ((Error)) | Thrown if anything else unexpected happens. See [Concepts > Models and ORM > Errors](https://sailsjs.com/documentation/concepts/models-and-orm/errors) for examples of negotiating errors in Sails and Waterline. ### Example For user 3, remove pets 99 and 98 from the "pets" collection: ```javascript await User.removeFromCollection(3, 'pets') .members([99,98]); ``` ### Edge cases + If the parent id (or any _one_ of the parent ids, if specified as an array) does not actually correspond with an existing, persisted record, then this will modify the existing records and ignore the non-existent ones. + If one of the child ids does not actually correspond with an existing, persisted record, then that child id will be ignored, and only those members that correspond with the other provided child ids will be removed from the collection. + If a parent record's collection _does not have_ one or more of these child ids as members, then the ids of those non-members will be ignored. ((TODO: test with one-to-many)) + If an empty array of child ids is provided, then this is a [no-op](https://en.wikipedia.org/wiki/NOP#Code). + If an empty array of parent ids is provided, then this is a [no-op](https://en.wikipedia.org/wiki/NOP#Code). ### Notes > + This method can be used with [`await`](https://github.com/mikermcneil/parley/tree/49c06ee9ed32d9c55c24e8a0e767666a6b60b7e8#usage), promise chaining, or [traditional Node callbacks](https://sailsjs.com/documentation/reference/waterline-orm/queries/exec). > + If the association is "two-way" (meaning it has `via`) then the child records will be modified accordingly. If the attribute on the other (e.g. "Pet") side is singular, the each child record's foreign key ("owner") will be set to `null`. If it's plural, then each child record's collection will be modified accordingly. <docmeta name="displayName" value=".removeFromCollection()"> <docmeta name="pageType" value="method"> ================================================ FILE: docs/reference/waterline/models/replaceCollection.md ================================================ # `.replaceCollection()` Replace all members of the specified collection (e.g. the `comments` of BlogPost #4). ```usage await Something.replaceCollection(parentId, association) .members(childIds); ``` ### Usage | | Argument | Type | Details | |---|:--------------------|----------------------------------------------|:-----------------------------------| | 1 | parentId | ((number)) or ((string)) | The primary key value(s) (i.e. ids) for the parent record(s). <br/>Must be a number or string (e.g. `'507f191e810c19729de860ea'` or `49`). <br/>Alternatively, an array of numbers or strings may be specified (e.g. `['507f191e810c19729de860ea', '14832ace0c179de897']` or `[49, 32, 37]`). In this case, the child records will be replaced in each parent record. | 2 | association | ((string)) | The name of the plural ("collection") association (e.g. "pets") | 3 | childIds | ((array)) | The primary key values (i.e. ids) for the child records that will be the new members of the association. _Note that this does not [create](https://sailsjs.com/documentation/reference/waterline-orm/models/create) these records or [destroy](https://sailsjs.com/documentation/reference/waterline-orm/models/destroy) the old ones, it just attaches/detaches records to/from the specified parent(s)._ ##### Errors | Name | Type | When? | |:----------------|---------------------|:---------------------------------------------------------------------------------| | UsageError | ((Error)) | Thrown if something invalid was passed in. | AdapterError | ((Error)) | Thrown if something went wrong in the database adapter. | Error | ((Error)) | Thrown if anything else unexpected happens. See [Concepts > Models and ORM > Errors](https://sailsjs.com/documentation/concepts/models-and-orm/errors) for examples of negotiating errors in Sails and Waterline. ### Example For user 3, replace all pets in the "pets" collection with pets 99 and 98: ```javascript await User.replaceCollection(3, 'pets') .members([99,98]); ``` ### Edge cases + If the parent id does not actually correspond with an existing, persisted record, then this will do nothing. + If one of the child ids does not actually correspond with an existing, persisted record, then that child id will be ignored, and only those members that correspond with the other provided child ids will be included in the replacement collection. + If an empty array of child ids is provided, or if none of the provided child ids correspond to existing records, then this will detach _all_ child records from the parent. ### Notes > + This method can be used with [`await`](https://github.com/mikermcneil/parley/tree/49c06ee9ed32d9c55c24e8a0e767666a6b60b7e8#usage), promise chaining, or [traditional Node callbacks](https://sailsjs.com/documentation/reference/waterline-orm/queries/exec). > + If the association is "2-way" (meaning it has `via`) then the child records will be modified accordingly. If the attribute on the other side is singular, the each newly-linked-or-unlinked child record's foreign key will be changed. If it's plural, then each child record's collection will be modified accordingly. > + In addition, if the `via` points at a singular ("model") attribute on the other side, then `.addToCollection()` will "steal" these child records if necessary. For example, imagine you have an Employee model with this plural ("collection") attribute: `involvedInPurchases: { collection: 'Purchase', via: 'cashier' }`. If you executed `Employee.addToCollection(7, 'involvedInPurchases', [47])` to assign this purchase to employee #7 (Dolly), but purchase #47 was already associated with a different employee (e.g. #12, Motoki), then this would "steal" the purchase from Motoki and give it to Dolly. In other words, if you executed `Employee.find([7, 12]).populate('involvedInPurchases')`, Dolly's `involvedInPurchases` array would contain purchase #47 and Motoki's would not. <docmeta name="displayName" value=".replaceCollection()"> <docmeta name="pageType" value="method"> ================================================ FILE: docs/reference/waterline/models/stream.md ================================================ # `.stream()` Stream records from your database to be consumed one at a time or in batches, without first having to buffer the entire result set in memory. ```usage await Something.stream(criteria) .eachRecord(async (record)=>{ }); ``` ### Usage | | Argument | Type | Details | |---|:--------------------|-------------------|:-----------------------------------| | 1 | _criteria_ | ((dictionary)) | The [Waterline criteria](https://sailsjs.com/documentation/concepts/models-and-orm/query-language) to use for matching records in the database. ##### Iteratee _Use one of the following:_ + `.eachRecord(async (record)=>{ ... })` + `.eachBatch(async (records)=>{ ... })` _The custom function you provide to `eachRecord()` or `eachBatch()` will receive the following arguments:_ <br/> | | Argument | Type | Details | |---|:--------------------|---------------------|:---------------------------------------------------------------------------------| | 1 | record or records | ((dictionary)) or ((array)) | The current record, or the current batch of records. _A batch array will always contain at least one record, and it will never contain more records than the batch size (thirty by default)._ ##### Errors | Name | Type | When? | |:----------------|---------------------|:---------------------------------------------------------------------------------| | UsageError | ((Error)) | Thrown if something invalid was passed in. | AdapterError | ((Error)) | Thrown if something went wrong in the database adapter. | Error | ((Error)) | Thrown if anything else unexpected happens. See [Concepts > Models and ORM > Errors](https://sailsjs.com/documentation/concepts/models-and-orm/errors) for examples of negotiating errors in Sails and Waterline. ### When should I use this? The `.stream()` method is almost exactly like [`.find()`](https://sailsjs.com/documentation/reference/waterline-orm/models/find), except that it fetches records one batch at a time. Every time a batch of records is loaded, the iteratee function you provided is called one or more times. If you used `.eachRecord()`, your per-record function will be called once for each record in the batch. Otherwise, using `.eachBatch()`, your per-batch function will be called once with the entire batch. This is useful for working with very large result sets, the kind that might overflow your server's available RAM if you tried to hold the entire set in memory at the same time. You can use Waterline's `.stream()` method to do the kinds of things you might already be familiar with from Mongo cursors: preparing reports, looping over and modifying database records in a shell script, moving large amounts of data from one place to another, performing complex transformations, or even orchestrating map/reduce jobs. ### Examples We explore four example situations below: ##### Basic usage An action that iterates over users named Finn in the database, one at a time: ```javascript await User.stream({name:'Finn'}) .eachRecord(async (user)=>{ if (Math.random() > 0.5) { throw new Error('Oops! This is a simulated error.'); } sails.log(`Found a user ${user.id} named Finn.`); }); ``` ##### Generating a dynamic sitemap An action that responds with a dynamically generated sitemap: ```javascript // e.g. in an action that handles `GET /sitemap.xml`: var sitemapXml = '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">'; await BlogPost.stream() .limit(50000) .sort('title ASC') .eachRecord((blogPost)=>{ sitemapXml += ( '<url>\n'+ ' <loc>https://blog.example.com/' + _.escape(encodeURIComponent(blogPost.slug))+'</loc>\n'+ ' <lastmod>'+_.escape(blogPost.updatedAt)+'</lastmod>\n'+ '<changefreq>monthly</changefreq>\n'+ '</url>' ); }); sitemapXml += '</urlset>'; ``` ##### With `.populate()` A snippet of a command-line script that searches for creepy comments from someone named "Bailey Bitterbumps" and reports them to the authorities: ```js // e.g. in a shell script var numReported = 0; await Comment.stream({ author: 'Bailey Bitterbumps' }) .limit(1000) .skip(40) .sort('title ASC') .populate('attachedFiles', { limit: 3, sort: 'updatedAt' }) .populate('fromBlogPost') .eachRecord(async (comment)=>{ var isCreepyEnoughToWorryAbout = comment.rawMessage.match(/creepy/) && comment.attachedFiles.length > 1; if (!isCreepyEnoughToWorryAbout) { return; } await sails.helpers.sendTemplateEmail.with({ template: 'email-creepy-comment-notification', templateData: { url: `https://blog.example.com/${comment.fromBlogPost.slug}/comments/${comment.slug}.` }, to: 'authorities@cannedmeat.gov', subject: 'Creepy comment alert' }); numReported++; }); sails.log(`Successfully reported ${numReported} creepy comments.`); ``` ##### Batch-at-a-time If we ran the code in the previous example, we'd be sending one email per creepy comment... which could be a lot, knowing Bailey Bitterbumps. Not only would this be slow, it could mean sending _thousands_ of individual API requests to our [transactional email provider](https://documentation.mailgun.com/faqs.html#why-not-just-use-sendmail-postfix-courier-imap), quickly overwhelming our API rate limit. For this case, we could use `.eachBatch()` to grab the entire batch of records being fetched, rather than processing individual records one at a time, dramatically reducing the number of necessary API requests. ##### Configuring batch size By default, `.stream()` uses a batch size of 30. That means it will load up to 30 records per batch; thus, if you are using `.eachBatch()`, your custom function will receive between 1 and 30 records each time it is called. To increase or decrease the batch size, pass an additional argument to `.eachBatch()`: ```javascript .eachBatch(100, async (records)=>{ console.log(`Got ${records.length} records.`); }) ``` > Using `.eachBatch()` in your code is not necessarily more or less efficient than using `.eachRecord()`. That's because, regardless which iterator you use, Waterline asks the database for more than one record at a time (30, by default). With `.eachBatch()`, you can easily configure this batch size using the extra argument described above. It's also possible to customize the batch size while using `.eachRecord` (for example, to avoid getting rate-limited by a 3rd party API you are using). Just use [`.meta()`](https://sailsjs.com/documentation/reference/waterline-orm/queries/meta). For example, `.meta({batchSize: 100})`. ### Notes > + This method can be used with [`await`](https://github.com/mikermcneil/parley/tree/49c06ee9ed32d9c55c24e8a0e767666a6b60b7e8#usage), promise chaining, or [traditional Node callbacks](https://sailsjs.com/documentation/reference/waterline-orm/queries/exec). > + `.stream()` bails and throws an error _immediately_ upon receiving the first error from any iteratee. > + `.stream()` runs the provided iteratee function on each record or batch, one at a time, in series. > Prior to Sails 1.1.0, the recommended usage of `.stream()` expected the iteratee to invoke a callback (`next`), which is provided as the second argument. This is no longer necessary as long as you do not actually include a second argument in the function signature. > + Prior to Sails v1.0 / Waterline 0.13, this method had a lower-level interface, exposing a [Readable "object stream"](http://nodejs.org/api/stream.html). This was powerful, but tended to be error-prone. The new, adapter-agnostic `.stream()` does not rely on emitters or any particular flavor of Node streams. (Need to get it working the old way? Don't worry, with a little code, you can still easily build a streams2/streams3-compatible Readable "object stream" using the new interface.) > + Read more background about the impetus for creating `.stream()` [here](https://gist.githubusercontent.com/mikermcneil/d1e612cd1a8564a79f61e1f556fc49a6/raw/094d49a670e70cc38ae11a9419314542e8e4e5c9/streaming-records-in-sails-v1.md), including additional examples, background information, and implementation details. <docmeta name="displayName" value=".stream()"> <docmeta name="pageType" value="method"> ================================================ FILE: docs/reference/waterline/models/sum.md ================================================ # `.sum()` Get the aggregate sum of the specified attribute across all matching records. ```usage var total = await Something.sum(numericAttrName, criteria); ``` ### Usage | | Argument | Type | Details | |---|:--------------------|----------------------------------------------|:-----------------------------------| | 1 | numericAttrName | ((string)) | The name of the numeric attribute to be summed. | 2 | _criteria_ | ((dictionary?)) | The [Waterline criteria](https://sailsjs.com/documentation/concepts/models-and-orm/query-language) to use for matching records in the database. If no criteria is specified, the sum will be computed across _all_ of this model's records. `sum` queries do not support pagination using `skip` and `limit` or projections using `select`. ##### Result | Type | Description | |---------------------|:-----------------| | ((number)) | The aggregate sum of the specified attribute across all matching records. ##### Errors | Name | Type | When? | |:----------------|---------------------|:---------------------------------------------------------------------------------| | UsageError | ((Error)) | Thrown if something invalid was passed in. | AdapterError | ((Error)) | Thrown if something went wrong in the database adapter. | Error | ((Error)) | Thrown if anything else unexpected happens. See [Concepts > Models and ORM > Errors](https://sailsjs.com/documentation/concepts/models-and-orm/errors) for examples of negotiating errors in Sails and Waterline. ### Example Get the cumulative account balance of all bank accounts that have less than $32,000 or are flagged as "suspended". ```javascript var total = await BankAccount.sum('balance') .where({ or: [ { balance: { '<': 32000 } }, { suspended: true } ] }); ``` ### Notes > + This method can be used with [`await`](https://github.com/mikermcneil/parley/tree/49c06ee9ed32d9c55c24e8a0e767666a6b60b7e8#usage), promise chaining, or [traditional Node callbacks](https://sailsjs.com/documentation/reference/waterline-orm/queries/exec). > + Some databases, like MySQL, may return `null` for this kind of query; however, it's best practice for Sails/Waterline adapter authors to return `0` for consistency and type safety in app-level code. <docmeta name="displayName" value=".sum()"> <docmeta name="pageType" value="method"> ================================================ FILE: docs/reference/waterline/models/update.md ================================================ # `.update()` Update all records matching criteria. ```usage await Something.update(criteria) .set(valuesToSet); ``` or + `var updatedRecords = await Something.update(criteria).set(valuesToSet).fetch();` ### Usage | | Argument | Type | Details | |---|:--------------------|-------------------|:-----------------------------------| | 1 | criteria | ((dictionary)) | The [Waterline criteria](https://sailsjs.com/documentation/concepts/models-and-orm/query-language) to use for matching records in the database. `update` queries do not support pagination using `skip` and `limit`, or projections using `select`. | 2 | valuesToSet | ((dictionary)) | A dictionary (plain JavaScript object) of values that all matching records should be updated to have. _(Note that, if this model is in ["schemaful" mode](https://sailsjs.com/documentation/concepts/models-and-orm/model-settings#?schema), then any extraneous keys will be silently omitted.)_ > **Note**: For performance reasons, as of Sails v1.0 / Waterline 0.13, the `valuesToSet` object passed into this model method will be mutated in-place in most situations (whereas in Sails/Waterline v0.12, this was not necessarily the case). ##### Result | Type | Description | |:--------------------|:-----------------| | ((array?)) | The updated records are not provided as a result by default, in order to optimize for performance. To override the default setting, chain `.fetch()` and an array of the updated records will be sent back. (Be aware that this requires an extra database query in some adapters.) ##### Errors | Name | Type | When? | |:-------------------|---------------------|:---------------------------------------------------------------------------------| | UsageError | ((Error)) | Thrown if something invalid was passed in. | AdapterError | ((Error)) | Thrown if something went wrong in the database adapter. See [Concepts > Models and ORM > Errors](https://sailsjs.com/documentation/concepts/models-and-orm/errors) for an example of how to negotiate a uniqueness error (i.e. from attempting to update one or more records so that they violate a uniqueness constraint). | Error | ((Error)) | Thrown if anything else unexpected happens. See [Concepts > Models and ORM > Errors](https://sailsjs.com/documentation/concepts/models-and-orm/errors) for examples of negotiating errors in Sails and Waterline. ##### Meta keys | Key | Type | Details | |:--------------------|-------------------|:---------------------------------------------------------------| | fetch | ((boolean)) | If set to `true`, then the array of updated records will be sent back.<br/><br/>Defaults to `false`. > For more information on meta keys, see [.meta()](https://sailsjs.com/documentation/reference/waterline-orm/queries/meta). ### Example To update a particular record, use [`.updateOne()`](https://sailsjs.com/documentation/reference/waterline-orm/models/update-one). Or to update one or more records at the same time: ```javascript await User.update({ name:'Pen' }) .set({ name:'Finn' }); sails.log('Updated all users named Pen so that their new name is "Finn". I hope they like it.'); ``` ##### Fetching updated records To fetch updated records, use enable the `fetch` meta key: ```javascript var updatedUsers = await User.update({name:'Finn'}) .set({ name:'Jake' }) .fetch(); sails.log(`Updated all ${updatedUsers.length} user${updatedUsers.length===1?'':'s'} named "Finn" to have the name "Jake". Here they are now:`); sails.log(updatedUsers); ``` ### Notes > + This method can be used with [`await`](https://github.com/mikermcneil/parley/tree/49c06ee9ed32d9c55c24e8a0e767666a6b60b7e8#usage), promise chaining, or [traditional Node callbacks](https://sailsjs.com/documentation/reference/waterline-orm/queries/exec). > + This method can be used to replace an entire collection association (for example, a user’s list of friends), achieving the same result as the [`replaceCollection` method](https://sailsjs.com/documentation/reference/waterline-orm/models/replace-collection). To modify items in a collection individually, use the [`addToCollection`](https://sailsjs.com/documentation/reference/waterline-orm/models/add-to-collection) or [removeFromCollection](https://sailsjs.com/documentation/reference/waterline-orm/models/remove-from-collection) methods. <docmeta name="displayName" value=".update()"> <docmeta name="pageType" value="method"> ================================================ FILE: docs/reference/waterline/models/updateOne.md ================================================ # `.updateOne()` Update the record that matches the given criteria, if such a record exists. ```usage var updatedRecord = await Something.updateOne(criteria) .set(valuesToSet); ``` > Before attempting to modify the database, Waterline will check to see if more than one record matches the given criteria; if so, it will throw an error instead of proceeding. ### Usage | | Argument | Type | Details | |---|:--------------------|-------------------|:-----------------------------------| | 1 | criteria | ((dictionary)) | The [Waterline criteria](https://sailsjs.com/documentation/concepts/models-and-orm/query-language) to use for matching the record in the database. | 2 | valuesToSet | ((dictionary)) | A dictionary (plain JavaScript object) of values that all matching records should be updated to have. _(Note that if this model is in ["schemaful" mode](https://sailsjs.com/documentation/concepts/models-and-orm/model-settings#?schema), then any extraneous keys will be silently omitted.)_ > **Note**: For performance reasons, as of Sails v1.0 / Waterline 0.13, the `valuesToSet` object passed into this model method will be mutated in-place in most situations (whereas in Sails/Waterline v0.12, this was not necessarily the case). ##### Result | Type | Description | |:--------------------|:-----------------| | ((dictionary?)) | `updateOne()` never updates more than one record, so if a record is updated, then that record is provided as a result. Otherwise, `undefined` is returned. ##### Errors See [Concepts > Models and ORM > Errors](https://sailsjs.com/documentation/concepts/models-and-orm/errors) for examples of negotiating errors in Sails and Waterline. ### Example ```javascript var updatedUser = await User.updateOne({ firstName:'Pen' }) .set({ firstName:'Finn' }); if (updatedUser) { sails.log('Updated the user named "Pen" so that their new name is "Finn".'); } else { sails.log('The database does not contain a user named "Pen".'); } ``` ### Notes > + This method **does not support .fetch()**, because it _always_ returns the modified record if one was matched. > + This method can be used with [`await`](https://github.com/mikermcneil/parley/tree/49c06ee9ed32d9c55c24e8a0e767666a6b60b7e8#usage), promise chaining, or [traditional Node callbacks](https://sailsjs.com/documentation/reference/waterline-orm/queries/exec). > + This method can be used to replace an entire collection association (for example, a user’s list of friends), achieving the same result as the [`replaceCollection` method](https://sailsjs.com/documentation/reference/waterline-orm/models/replace-collection). To modify items in a collection individually, use the [`addToCollection`](https://sailsjs.com/documentation/reference/waterline-orm/models/add-to-collection) or [removeFromCollection](https://sailsjs.com/documentation/reference/waterline-orm/models/remove-from-collection) methods. <docmeta name="displayName" value=".updateOne()"> <docmeta name="pageType" value="method"> ================================================ FILE: docs/reference/waterline/models/validate.md ================================================ # `.validate()` Verify that a value would be valid for a given attribute, then return it, loosely coerced. ```usage Something.validate(attrName, value); ``` > This validates (and potentially coerces) the provided data as if it was one of the values passed into [`.update()`](https://sailsjs.com/documentation/reference/waterline-orm/models/update). You might think about it like a "dry run". ### Usage | # | Description | Accepted Data Types | Required ? | |---|---------------|------------------------------|:-----------| | 1 | attrName | ((string)) | The name of the attribute to validate against. | | 2 | value | ((ref)) | The value to validate/normalize. | ### Example Check the given string and return a normalized version. > Note that if normalization is not possible, this throws an error. **Be careful: You must manually handle any error thrown from within an asynchronous callback.** ```javascript User.validate('emailAddress', req.param('email')); User.validate('password', req.param('password')); ``` ##### Negotiating errors The `.validate()` method can throw any of the usage errors you might see when calling `.update()`. For example: ```javascript try { var normalizedBalance = BankAccount.validate('balance', '$349.86'); } catch (err) { switch (err.code) { case 'E_VALIDATION': // => '[Error: Invalid `bankAccount`]' _.each(e.all, function(woe){ sails.log(woe.attrName+': '+woe.message); }); break; default: throw err; } } ``` ### Notes > + This is a synchronous method, so you don't need to use `await`, promise chaining, or traditional Node callbacks. > + `.validate()` is exposed as a separate method for convenience. You can always simply call `.create()` or `.update()`, _instead_ of calling `.validate()` first, since those model methods apply the same checks automatically. > + `.validate()` is useful when implementing use cases where it is beneficial or more aesthetically pleasing (/[DRY](https://en.wikipedia.org/wiki/Don't_repeat_yourself)) to reuse your model validations for other purposes. For example, you might want to validate some untrusted data before communicating with a 3rd party API like Mailgun or Stripe, or you might just want to run certain model validations initially to make some code easier to reason about. > + `.validate()` does not communicate with the database, and thus it only detects _logical failures_ such as type safety errors and high-level validation rule violations. It cannot detect problems with _physical-layer_ constraints like uniqueness, since those constraints are checked by the underlying database, not by Sails or Waterline. <docmeta name="displayName" value=".validate()"> <docmeta name="pageType" value="method"> ================================================ FILE: docs/reference/waterline/queries/catch.md ================================================ # `.catch()` Execute a Waterline [query instance](https://sailsjs.com/documentation/reference/waterline-orm/queries) using promises. ```usage .catch(callback) ``` > As of Sails v1 and Node.js v8, you can take advantage of [`await`](https://sailsjs.com/documentation/reference/waterline-orm/queries) instead of using this method. ### Usage | | Argument | Type | Details | |---|:--------------------|----------------------------------------------|:-----------------------------------| | 1 | filter | ((dictionary?)) | An optional dictionary whose properties will be checked against the error. If they all match, then the callback will run. Otherwise, it won't. | 2 | callback | ((function)) | A function that runs if the query fails.<br/><br/> Takes the error as its argument. ##### Callback | | Argument | Type | Details | |---|:--------------------|---------------------|:---------------------------------------------------------------------------------| | 1 | _err_ | ((Error?)) | The Error that occurred, or `undefined` if there were no errors. ### Example To look up the user with the specified email address: ```javascript User.findOne({ email: req.param('email') }) .then(function (user){ if(!user) { return res.notFound(); } return res.json(user); }) // If there was some kind of usage / validation error .catch({ name: 'UsageError' }, function (err) { return res.badRequest(err); }) // If something completely unexpected happened. .catch(function (err) { return res.serverError(err); }); ``` ### Notes > + Whenever possible, it is recommended that you use `await` instead of calling this method. > + This is an alternative to `.exec()`. When combined with `.then()`, it provides the same functionality. > + The `.catch()` function also returns a promise to allow for chaining. This is not recommended for any but the most advanced users of promises due to the complex (and arguably non-intuitive) behavior of chained `.catch()` calls. > + For more information, see the [bluebird `.catch()` api docs](http://bluebirdjs.com/docs/api/catch). <docmeta name="displayName" value=".catch()"> <docmeta name="pageType" value="method"> ================================================ FILE: docs/reference/waterline/queries/decrypt.md ================================================ # `.decrypt()` Decrypt any auto-encrypted attributes in the records returned for this particular query. ```usage query.decrypt() ``` ### Usage This method doesn't accept any arguments. ### Example To retrieve user records with `ssn` decrypted: ```javascript await User.find({fullName: 'Finn Mertens'}).decrypt(); // => // [ { id: 4, fullName: 'Finn Mertens', ssn: '555-55-5555' } ] ``` If the records were retrieved without `.decrypt()`, you would get: ```javascript await User.find({fullName: 'Finn Mertens'}); // => // [ { id: 4, fullName: 'Finn Mertens', ssn: 'YWVzLTI1Ni1nY20kJGRlZmF1bHQ=$F4Du3CAHtmUNk1pn$hMBezK3lwJ2BhOjZ$6as+eXnJDfBS54XVJgmPsg' } ] ``` ### Notes > * This is just a shortcut for [`.meta({decrypt: true})`](https://sailsjs.com/documentation/reference/waterline-orm/queries/meta) <docmeta name="displayName" value=".decrypt()"> <docmeta name="pageType" value="method"> ================================================ FILE: docs/reference/waterline/queries/exec.md ================================================ # `.exec()` Execute a Waterline [query instance](https://sailsjs.com/documentation/reference/waterline-orm/queries). ```usage .exec(function (err, result) { }) ``` > As of Sails v1 and Node.js v8, you can take advantage of [`await`](https://sailsjs.com/documentation/reference/waterline-orm/queries) instead of using this method. ### Usage | | Argument | Type | Details | |---|:--------------------|----------------------------------------------|:-----------------------------------| | 1 | callback | ((function)) | The Node-style callback that will be called when the query completes, successfully or otherwise. ##### Callback | | Argument | Type | Details | |---|:--------------------|---------------------|:---------------------------------------------------------------------------------| | 1 | _err_ | ((Error?)) | The Error that occurred, or `undefined` if there were no errors. | 2 | _result_ | ((Ref?)) | The result from the database, if any. Exact data type depends on the query. If an error occurred (i.e. `err` is truthy), then this result argument should be ignored. ### Example ```javascript Zookeeper.find().exec((err, zookeepers)=>{ if (err) { return res.serverError(err); } // would you look at all those zookeepers? return res.json(zookeepers); }); // // (don't put code out here) ``` ### Notes > + If you don't run `.exec()` or use promises, your query will not execute. For help using `.exec()` with model methods like `.find()`, read more about the [chainable query object](https://sailsjs.com/documentation/reference/waterline-orm/queries). <docmeta name="displayName" value=".exec()"> <docmeta name="pageType" value="method"> ================================================ FILE: docs/reference/waterline/queries/fetch.md ================================================ # `.fetch()` Tell Waterline (and the underlying database adapter) to send back records that were updated/destroyed/created when performing an [`.update()`](https://sailsjs.com/documentation/reference/waterline-orm/models/update), [`.create()`](https://sailsjs.com/documentation/reference/waterline-orm/models/create), [`.createEach()`](https://sailsjs.com/documentation/reference/waterline-orm/models/create-each) or [`.destroy()`](https://sailsjs.com/documentation/reference/waterline-orm/models/destroy) query. Otherwise, no data will be returned (or if you are using callbacks, the second argument to the `.exec()` callback will be `undefined`). > Warning: This is not recommended for update/destroy queries that affect large numbers of records. ```usage .fetch() ``` ### Usage This method doesn't accept any arguments. ### Example ```javascript var newUser = await User.create({ fullName: 'Alice McBailey' }).fetch(); sails.log(`Hi, ${newUser.fullName}! Your id is ${newUser.id}.`); ``` ### Notes > * This is just a shortcut for [`.meta({fetch: true})`](https://sailsjs.com/documentation/reference/waterline-orm/queries/meta) <docmeta name="displayName" value=".fetch()"> <docmeta name="pageType" value="method"> ================================================ FILE: docs/reference/waterline/queries/intercept.md ================================================ # `.intercept()` Capture and intercept the specified error, automatically modifying and re-throwing it, or specifying a new error to be thrown instead. (Still throws.) ```usage .intercept(filter, handler) ``` or + `.intercept(handler)` _(to intercept all errors)_ ### Usage | | Argument | Type | Details | |---|-----------------|---------------------|:-----------| | 1 | _filter_ | ((string)) or ((dictionary)) | The code of the error that you want to intercept, or a dictionary of criteria for identifying the error to intercept. (If not provided, ALL errors will be intercepted.) | | 2 | handler | ((function)) or ((string)) | A [procedural parameter](https://en.wikipedia.org/wiki/Procedural_parameter) which Sails calls automatically if the anticipated error is thrown. It will receive the argument specified in the "Handler" usage table below. The handler should return the modified Error, a new Error, or (if applicable) a [special exit signal](https://sailsjs.com/documentation/concepts/actions-and-controllers#?exit-signals). <br/><br/> Alternatively, instead of a function, a string may be provided. This amounts to the same thing as passing in a handler function that simply returns the string. (Convenient when using actions2.) | ##### Handler | | Argument | Type | Details |---|---------------------|---------------------|:------------------------| | 1 | err | ((Error)) | The anticipated Error being intercepted. | Return an Error instance or (if applicable) a [special exit signal](https://sailsjs.com/documentation/concepts/actions-and-controllers#?exit-signals) that will be thrown from the original logic instead of throwing the intercepted error. > .intercept() is for intercepting a certain kind of error (or all errors). If you chain on .intercept(), and it matches the error that occurs, then the underlying logic will throw. But what it throws is determined by what your handler function returns. ### Example If every user record in an app needs to have a unique email address, you may want to ensure that error is formatted in a such a way that the appropriate message will be displayed to the end user. To intercept that error: ```javascript var newUserRecord = await User.create({ emailAddress: inputs.emailAddress, fullName: inputs.fullName, }) .intercept('E_UNIQUE', ()=>{ return new Error('There is already an account using that email address!') }) .fetch(); ``` Or, to handle the same error inside of an [actions2 action](https://sailsjs.com/documentation/concepts/actions-and-controllers#?actions-2), using a [special exit signal](https://sailsjs.com/documentation/concepts/actions-and-controllers#?exit-signals); instead of an Error instance: ```javascript var newUserRecord = await User.create({ emailAddress: inputs.emailAddress, fullName: inputs.fullName, }) .intercept('E_UNIQUE', ()=>'emailAlreadyInUse') .fetch(); ``` ### Notes > Note that the usage in our example above could have also been written more concisely as: > > ```js > .intercept('E_UNIQUE', 'emailAlreadyInUse') > ``` > > Or less concisely as: > > ```js > .intercept({ code: 'E_UNIQUE' }, ()=>{ return 'emailAlreadyInUse'; }) > ``` > > For more examples and further explanation of how `.intercept()` works, check out [this related conversation](https://gitter.im/balderdashy/sails?at=5ab44f512b9dfdbc3a113e2f). <docmeta name="displayName" value=".intercept()"> <docmeta name="pageType" value="method"> ================================================ FILE: docs/reference/waterline/queries/limit.md ================================================ # `.limit()` Set the maximum number of records to retrieve when executing a [query instance](https://sailsjs.com/documentation/reference/waterline-orm/queries). ```usage .limit(maximum) ``` ### Usage | | Argument | Type | Details | |---|:--------------------|--------------|------------| | 1 | maximum | ((number)) | The maximum number of records to retrieve. | ### Example To retrieve records for up to 10 users named Jake: ```javascript var jakes = await User.find({ name: 'Jake' }).limit(10); return res.json(jakes); ``` ### Notes > * If you set the limit to 0, the query will always return an empty array. > * If the limit is greater than the number of records matching the query criteria, all of the matching records will be returned. > * The .find() method returns a chainable object if you don't supply a callback. This method can be chained to .find() to further filter your results. <docmeta name="displayName" value=".limit()"> <docmeta name="pageType" value="method"> ================================================ FILE: docs/reference/waterline/queries/meta.md ================================================ # `.meta()` Provide additional options to Waterline when executing a [query instance](https://sailsjs.com/documentation/reference/waterline-orm/queries). ```usage .meta(options) ``` ### Usage | | Argument | Type | Details | |---|-----------------|---------------------|:-----------| | 1 | options | ((dictionary)) | A dictionary (plain JS object) of options. See all supported options (aka “meta keys”) in the table below. | ##### Supported options Option | Type | Default | Details :------------------------------------ |-------------|:---------| :------------------------------ fetch | ((boolean)) | false | When performing [`.update()`](https://sailsjs.com/documentation/reference/waterline-orm/models/update), [`.create()`](https://sailsjs.com/documentation/reference/waterline-orm/models/create), [`.createEach()`](https://sailsjs.com/documentation/reference/waterline-orm/models/create-each), or [`.destroy()`](https://sailsjs.com/documentation/reference/waterline-orm/models/destroy) queries, set this to `true` to tell the database adapter to send back all records that were updated/destroyed. Otherwise, the second argument to the `.exec()` callback is `undefined`. Warning: Enabling this key may cause performance issues for update/destroy queries that affect large numbers of records. cascade | ((boolean)) | false | If set to `true` on a [`.destroy()`](https://sailsjs.com/documentation/reference/waterline-orm/models/destroy), this tells Waterline to perform a _"virtual cascade"_ for every deleted record. Thus, deleting a record with a 2-way, _plural association_ ([one-to-many](https://sailsjs.com/documentation/concepts/models-and-orm/associations/one-to-many) or [many-to-many](https://sailsjs.com/documentation/concepts/models-and-orm/associations/many-to-many)) will also cleanly remove all links to other records (by removing join table rows or setting foreign key values to `null`).<br/><br/>This may be desirable if database size is a concern, or if primary keys may be reused for records, but it can negatively impact performance on `.destroy()` calls since it involves executing more queries.<br/><br/>**The `cascade` meta key should only be used with databases like MongoDB** that [don't support](http://stackoverflow.com/questions/20370791/what-is-the-recommended-equivalent-of-cascaded-delete-in-mongodb-for-nm-relatio) cascading delete as a native feature. If you need cascading delete and your database supports it natively (e.g. MySQL or PostgreSQL), you'll enjoy improved performance by simply adding a [CASCADE constraint](https://dev.mysql.com/doc/refman/5.7/en/create-table-foreign-keys.html) at the physical layer (e.g. phpMyAdmin, Sequel Pro, mySQL prompt, etc.), rather than relying on Waterline's virtual cascade to take effect at runtime. skipAllLifecycleCallbacks | ((boolean)) | false | Set to `true` to prevent [lifecycle callbacks](https://sailsjs.com/documentation/concepts/models-and-orm/lifecycle-callbacks) from running during the execution of the query. skipRecordVerification | ((boolean)) | false | Set to `true` to skip Waterline's post-query verification pass of any records returned from the adapter(s). Useful for tools like sails-hook-orm's automigrations, or to disable warnings for use cases where you know that pre-existing records in the database do not match your model definitions. skipExpandingDefaultSelectClause | ((boolean)) | false | Set to `true` to force Waterline to skip expanding the `select` clause in criteria when it forges stage 3 queries (i.e. the queries that get passed in to adapter methods). Normally, if a model declares `schema: true`, then the S3Q `select` clause is expanded to an array of column names, even if the S2Q had factory default `select`/`omit` clauses (which is also what it would have if no explicit `select` or `omit` clauses were included in the original query). Useful for tools like sails-hook-orm's automigrations, where you want temporary access to properties that aren't necessarily in the current set of attribute definitions. **Warning: Do not use this flag in your web application backend, or at least [ask for help](https://sailsjs.com/support) first.** decrypt | ((boolean)) | false | Set to `true` to decrypt any [auto-encrypted](https://sailsjs.com/documentation/concepts/models-and-orm/attributes#?encrypt) data in the records. encryptWith | ((string)) | 'default' | The id of a custom key to use for encryption for this particular query. (For decryption, the appropriate key is always used based on the data being decrypted.) makeLikeModifierCaseInsensitive | ((boolean)) | false | Set to `true` to make your query case-insensitive (only for use with the MongoDB adapter). | ### Example ```javascript var newUser = await User.create({name: 'alice'}) .meta({fetch: true}); return res.json(newUser); ``` ### Notes > * The [`.fetch()` method](https://sailsjs.com/documentation/reference/waterline-orm/queries/fetch) is a shorthand for `.meta({fetch: true})`. > * In order for `cascade` to work when the `fetch` meta key is _not_ also `true`, Waterline must do an extra `.find().select('id')` before actually performing the `.destroy()` in order to get the IDs of the records that would be destroyed. > * Rather than using the `.meta()` query method, you can also set meta keys for a query by passing in a dictionary after the explicit callback. For example: `User.create({name: 'alice'}, function(err, newUser){/*...*/}, { fetch: true })`. <docmeta name="displayName" value=".meta()"> <docmeta name="pageType" value="method"> ================================================ FILE: docs/reference/waterline/queries/populate.md ================================================ # `.populate()` Modify a [query instance](https://sailsjs.com/documentation/reference/waterline-orm/queries) so that, when executed, it will populate child records for the specified collection, optionally filtering by `subcriteria`. Populate may be called more than once on the same query, as long as each call is for a different association. ```usage .populate(association, subcriteria) ``` ### Usage | | Argument | Type | Details | |---|:-----------------------|----------------------------------------------|:-----------------------------------| | 1 | association | ((string)) | The name of the association to populate. e.g. `snacks`. | 2 | _subcriteria_ | ((dictionary?)) | Optional. When populating `collection` associations between two models which reside in the same database, a [Waterline criteria](https://sailsjs.com/documentation/concepts/models-and-orm/query-language) may be specified as a second argument to populate. This will be used for filtering, sorting, and limiting the array of associated records (e.g. snacks) associated with each primary record. > **Important:** Both the basic join polyfill (cross-datastore populate, or populate between models whose configured adapter does not provide a `.join()` implementation) and the subcriteria argument to `.populate()` are fully supported in Sails **individually**. However, using the subcriteria argument to `.populate()` at the same time as the join polyfill is experimental. This means that, if an association spans multiple datastores or its datastore's configured adapter does not support a physical layer join, then you should not rely on the subcriteria argument to `.populate()`. If you try that in production, you will see a warning logged to the console. SQL adapters such as [sails-postgresql](https://github.com/balderdashy/sails-postgresql) and [sails-mysql](https://github.com/balderdashy/sails-mysql) support native joins and should be okay to use the subcriteria argument. > **Note:** If you are using `schema: false`, only defined attributes will be populated. ### Example ##### Populating a model association The following finds any users named Finn in the database and, for each one, also populates their dad: ```javascript var usersNamedFinn = await User.find({name:'Finn'}).populate('dad'); sails.log('Wow, there are %d users named Finn.', usersNamedFinn.length); sails.log('Check it out, some of them probably have a dad named Joshua or Martin:', usersNamedFinn); return res.json(usersNamedFinn); ``` This might yield: ```javascript [ { id: 7392, age: 13, name: 'Finn', createdAt: 1451088000000, updatedAt: 1545782400000, dad: { id: 108, age: 47, name: 'Joshua', createdAt: 1072396800000, updatedAt: 1356480000000, dad: null } }, // ...more users ] ``` ##### Populating a collection association > This example uses the optional subcriteria argument. The following finds any users named Finn in the database and, for each one, also populates their three hippest purple swords, in descending order of hipness: ```javascript // Warning: This is only safe to use on large datasets if both models are in the same database, // and the adapter supports optimized populates. // (e.g. cannot do this with the `User` model in PostgreSQL and the `Sword` model in MongoDB) var usersNamedFinn = await User.find({ name:'Finn' }) .populate('currentSwords', { where: { color: 'purple' }, limit: 3, sort: 'hipness DESC' }); // Note that Finns without any swords are still included -- their `currentSwords` arrays will just be empty. sails.log('Wow, there are %d users named Finn.', usersNamedFinn.length); sails.log('Check it out, some of them probably have non-empty arrays of purple swords:', usersNamedFinn); return res.json(usersNamedFinn); ``` This might yield: ```javascript [ { id: 7392, age: 13, name: 'Finn', createdAt: 1451088000000, updatedAt: 1545782400000, dad: 108,//<< not populated swords: [//<< populated { id: 9, title: 'Grape Soda Sword', color: 'purple', createdAt: 1540944000000, updatedAt: 1540944000000 }, // ...more swords ] }, // ...more users ] ``` <docmeta name="displayName" value=".populate()"> <docmeta name="pageType" value="method"> ================================================ FILE: docs/reference/waterline/queries/queries.md ================================================ # Working with queries **Queries** (aka _query instances_) are the chainable deferred objects returned from model methods like `.find()` and `.create()`. They represent a not-quite-yet-fulfilled intent to fetch or modify records from the database. ```usage var query = Zookeeper.find(); ``` The purpose of query instances is to provide a convenient, chainable syntax for working with your models. Methods like `.populate()`, `.where()`, and `.sort()` allow you to refine database calls _before_ they're sent down the wire. Then, when you're ready to fire the query off to the database, you can just `await` it. > If you are using an older version of Node.js that does not support JavaScript's `await` keyword, you can use `.exec()` or `.then()`+`.catch()`. See the section on "Promises and Callbacks" below for more information. Most of the time, you won't think about query instances as objects _per se_, but as just another part of the syntax for communicating with the database. In fact, you may already be using these objects in your Sails app! If so, the following syntax should look familiar: ```js var zookeepers = await Zookeeper.find(); ``` In this example, the call to `Zookeeper.find()` returns a query instance, but _doesn't actually do anything_ until it is executed using the `await` keyword, and then the result is assigned to the `zookeepers` variable. ### How it works When you **execute** a query using `await`, a lot happens. ```js await query; ``` First, the query is "shaken out" by Waterline core into a [normalized query](https://sailsjs.com/documentation/concepts/models-and-orm/query-language). Then it passes through the relevant Waterline adapter(s) for translation to the raw query syntax of your database(s) (e.g. Redis or Mongo commands, various SQL dialects, etc.) Next, each involved adapter uses its native Node.js database driver to send the query out over the network to the corresponding physical database. When the adapter receives a response, it is marshalled to the Waterline interface spec and passed back up to Waterline core, where it is integrated with any other raw adapter responses into a coherent result set. At that point, it undergoes one last normalization before being passed back to "userland" (i.e. your code) for consumption by your app. ### Error handling You can use a try/catch to handle specific errors, if desired: ```js var zookeepersAtThisZoo; try { zookeepersAtThisZoo = await Zookeeper.find({ zoo: req.param('zoo') }).limit(30); } catch (err) { switch (err.name) { case 'UsageError': return res.badRequest(err); default: throw err; } } return res.json(zookeepersAtThisZoo); ``` The specific kinds of errors you could receive vary based on what kind of query you are executing. See the reference docs for the various query methods for more specific information. ### Promises and callbacks As an alternative to `await`, Sails and Waterline provide support for callbacks and promise-chaining. In general, you should **use `await` whenever possible**; it leads to simpler, easier-to-understand code, and helps prevent DDoS vulnerabilities and stability issues that can arise from throwing uncaught exceptions in asynchronous callbacks. That said, sometimes it is necessary to maintain backwards compatibility with an older version of Node.js. For this reason, all queries in Sails and Waterline expose an [`.exec()`](https://sailsjs.com/documentation/reference/waterline-orm/queries/exec) method. ```js Zookeeper.find().exec(function afterFind(err, zookeepers) { // Careful! Do not throw an error in here without a `try` block! // (Even a simple typo or null pointer exception could crash the process!) if (err) { // uh oh // (handle error; e.g. `return res.serverError(err)`) return; } // would you look at all those zookeepers? // (now let's do the next thing; // e.g. `_.reduce(zookeepers, ...)` and/or `return res.json(zookeepers)`) // … }); // // (don't put code out here) ``` As shown in the example above, the query is not executed right away, but notice that instead of using `await` to execute the query and wait for its result, we use the traditional `.exec()` method with a callback function. With this usage, we cannot rely on try/catch and normal error handling in JavaScript to take care of our errors! Instead, we have to manually handle them in our callback to `.exec()`. This style of error handling is the traditional approach used in Node.js apps prior to ~Summer 2017. Under the covers, Sails and Waterline also provide a minimalist integration with the [Bluebird](https://github.com/petkaantonov/bluebird) promise library, exposing `.then()` and `.catch()` methods. ```js Zookeeper.find() .then(function (zookeepers) {...}) .catch(function (err) {...}); // // (don't put code out here) ``` In this example, the callback passed into `.catch()` is equivalent to the contents of the `if(err) {}` block from the `.exec()` example above (e.g. `res.serverError()`). Similarly, the `.then()` callback is equivalent to the code below the `if(err) {}` and early `return`. If you are a fan of promises and have a reasonable amount of experience with them, you should have no problem working with this interface. However if you are not very familiar with promises, or don't care one way or another, you will probably have an easier time working with `.exec()`, since it uses standard Node.js callback conventions. > If you decide to use traditional promise chaining for a particular query in your app, please make sure that you provide callbacks for both `.then()` _and_ `.catch()`. Otherwise errors could go unhandled, and unpleasant race conditions and memory leaks could ensue. This is not just a Sails or Waterline concept. Rather, it's something to be aware of whenever you implement this type of usage in JavaScript—particularly in Node.js—since unhandled errors in server-side code tend to be more problematic than their client-side counterparts. Omitting `.catch()` is equivalent to ignoring the `err` argument in a conventional Node callback, and it is similarly insidious. In fact, this is hands-down one of the most common sources of bugs for Node.js developers of all skill levels. > > Proper error handling is particularly easy to neglect if you're new to asynchronous code. Once you've been at it for a while, you'll get in the habit of handling your asynchronous errors right after (or even better, right before) you write code that handles the successful case. Habits like this immunize your apps to those common bugs discussed above. > > (Better yet: just use `await`!) ### Notes > + A query instance is not _exactly_ the same thing as a Promise, but it's close enough for our purposes. The difference is that a query instance in Sails and Waterline is actually a Deferred, as implemented by the [parley](https://npmjs.com/package/parley) library. That means it doesn't start executing immediately. Instead, it only begins executing when you kick it off with either `await`, `.exec()`, `.then()`, or `.toPromise()`. > + A Node-style callback can be passed directly as a final argument to model methods (e.g. `.find()`). In this case, the query will be executed immediately, and model methods _will not_ return a query instance (instead, the Node-style callback you provided will be triggered when the query is complete). Unless you are doing something very advanced, you are generally better off sticking to standard usage; i.e. calling `.exec()` or calling `.then()` and `.catch()`. <docmeta name="displayName" value="Queries"> ================================================ FILE: docs/reference/waterline/queries/skip.md ================================================ # `.skip()` Indicate the number of records to skip before returning the results from executing a [query instance](https://sailsjs.com/documentation/reference/waterline-orm/queries). ```usage .skip(numRecordsToSkip) ``` ### Usage | | Argument | Type | Details | |---|:--------------------|-----------------|------------| | 1 | numRecordsToSkip | ((number)) | The number of records to skip. | ### Example To retrieve records for all but the original user named Jake: ```javascript var fakeJakes = await User.find({ name: 'Jake' }); .skip(1); return res.json(fakeJakes); ``` ### Notes > If the “skip” value is greater than the number of records matching the query criteria, the query will return an empty array. > The .find() method returns a chainable object if you don't supply a callback. This method can be chained to .find() to further filter your results. <docmeta name="displayName" value=".skip()"> <docmeta name="pageType" value="method"> ================================================ FILE: docs/reference/waterline/queries/sort.md ================================================ # `.sort()` Set the order in which retrieved records should be returned when executing a [query instance](https://sailsjs.com/documentation/reference/waterline-orm/queries). ```usage .sort(sortClause) ``` ### Usage | | Argument | Type | Details | |---|:-----------------|---------------------|------------| | 1 | sortClause | ((string)) _or_ ((array)) of ((dictionary)) | If specified as a string, this should be formatted as: an attribute name, followed by a space, followed by either `ASC` or `DESC` to indicate an _ascending_ or _descending_ sort (e.g. `name ASC`). <br/>If specified as an array, then each array item should be a dictionary with a single key representing the attribute to sort by, whose value is either `ASC` or `DESC`. The array syntax allows for sorting by multiple attributes, using the array order to establish precedence <br/>(e.g. `[ { name: 'ASC' }, { age: 'DESC'} ]`). | ### Example To sort users named Jake by age, in ascending order: ```javascript var users = await User.find({ name: 'Jake'}) .sort('age ASC'); return res.json(users); ``` To sort users named Finn, first by age, then by when they joined: ```javascript var users = await User.find({ name: 'Finn'}) .sort([ { age: 'ASC' }, { createdAt: 'ASC' }, ]); return res.json(users); ``` ### Notes > The .find() method returns a chainable object if you don't supply a callback. This method can be chained to .find() to further filter your results. <docmeta name="displayName" value=".sort()"> <docmeta name="pageType" value="method"> ================================================ FILE: docs/reference/waterline/queries/then.md ================================================ # `.then()` Execute a Waterline [query instance](https://sailsjs.com/documentation/reference/waterline-orm/queries) using promises. ```usage .then(callback) ``` > As of Sails v1 and Node.js v8, you can take advantage of [`await`](https://sailsjs.com/documentation/reference/waterline-orm/queries) instead of using this method. ### Usage | | Argument | Type | Details | |---|:--------------------|----------------------------------------------|:-----------------------------------| | 1 | callback | ((function)) | A function that runs if the query successfully completes<br/><br/> Takes the result of the query as its argument. ##### Callback | | Argument | Type | Details | |---|:--------------------|---------------------|:---------------------------------------------------------------------------------| | 1 | _result_ | ((Ref?)) | The result from the database, if any. Exact data type depends on the query. ### Example To look up the user with the specified email address: ```javascript User.findOne({ email: req.param('email') }) .then(function (user){ if (!user) { return res.notFound(); } return res.json(user); }) .catch(function (err) { return res.serverError(err); }); ``` ### Notes > + Whenever possible, it is recommended that you use `await` instead of calling this method. > + This is an alternative to `.exec()`. When combined with `.catch()`, it provides the same functionality. > + The `.then()` function returns a promise to allow for chaining. > + For more information, see the [bluebird `.then()` api docs](http://bluebirdjs.com/docs/api/then). <docmeta name="displayName" value=".then()"> <docmeta name="pageType" value="method"> ================================================ FILE: docs/reference/waterline/queries/toPromise.md ================================================ # `.toPromise()` Begin executing a Waterline [query instance](https://sailsjs.com/documentation/reference/waterline-orm/queries) and return a promise. ```usage .toPromise(); ``` > This is an alternative to `.exec()`. ### Notes > + For more information, see the [bluebird `Promise.promisify()` API docs](http://bluebirdjs.com/docs/api/promise.promisify.html). <docmeta name="displayName" value=".toPromise()"> <docmeta name="pageType" value="method"> ================================================ FILE: docs/reference/waterline/queries/tolerate.md ================================================ # `.tolerate()` Tolerate (swallow) the specified error, and return a new result value (or `undefined`) instead. (Don't throw.) ```usage .tolerate(filter, handler) ``` _Or:_ + `.tolerate(filter)` + `.tolerate(handler)` _(to tolerate all errors)_ ### Usage | | Argument | Type | Details | |---|-----------------|---------------------|:-----------| | 1 | filter | ((string)) or ((dictionary)) | The code of the error that you want to intercept, or a dictionary of criteria for identifying the error to intercept. | | 2 | _handler_ | ((function?)) | An optional [procedural parameter](https://en.wikipedia.org/wiki/Procedural_parameter), called automatically by Sails if the anticipated error is thrown. It receives the argument specified in the "Handler" usage table below. If specified, the handler should return a value that will be used as the result. If omitted, the anticipated error will be swallowed and the result of the query will be `undefined`. | ##### Handler | | Argument | Type | Details |---|---------------------|---------------------|:------------------------| | 1 | err | ((Error)) | Your anticipated Error. | > `.tolerate()` is useful for tolerating a kind of error (or all errors). If you chain on `.tolerate()` and it matches the error that occurs, then the underlying logic won't throw. Instead, it returns the return value of the handler function you passed into .tolerate(). ### Example Say you're building an address book that doesn't allow records with duplicate email addresses. To instead swallow the error caused by entering a non-unique email address and update the existing contact: ```javascript let newOrExistingContact = await Contact.create({ emailAddress, fullName }) .fetch() .tolerate('E_UNIQUE'); if(!newOrExistingContact) { newOrExistingContact = await Contact.updateOne({ emailAddress }) .set({ fullName }) .fetch(); } ``` <docmeta name="displayName" value=".tolerate()"> <docmeta name="pageType" value="method"> ================================================ FILE: docs/reference/waterline/queries/usingConnection.md ================================================ # `.usingConnection()` Specify an existing database connection to use for this [query](https://sailsjs.com/documentation/reference/waterline-orm/queries). ```usage .usingConnection(connection); ``` ### Usage | | Argument | Type | Details | |---|:--------------------|----------------------------------------------|:-----------------------------------| | 1 | connection | ((ref)) | An existing database connection obtained using [`.transaction()`](https://sailsjs.com/documentation/reference/waterline-orm/datastores/transaction) or [`.leaseConnection()`](https://sailsjs.com/documentation/reference/waterline-orm/datastores/lease-connection). ### Example An example of `.usingConnection()` usage can be found in the example for [`.transaction()`](https://sailsjs.com/documentation/reference/waterline-orm/datastores/transaction#?example). <docmeta name="displayName" value=".usingConnection()"> <docmeta name="pageType" value="method"> ================================================ FILE: docs/reference/waterline/queries/where.md ================================================ # `.where()` Specify a where clause for filtering a query. ```usage .where(whereClause) ``` ### Usage | | Arguments | Type | Details | |---|:-------------------|---------------------|------------| | 1 | whereClause | ((dictionary)) | The [where clause](https://sailsjs.com/documentation/concepts/models-and-orm/query-language) to use for matching records in the database. | ### Example To find all the users named Finn whose email addresses start with 'f': ```javascript var users = await User.find() .where({ name: 'Finn', 'emailAddress' : { startsWith : 'f' } }); return res.json(users); ``` ### Notes > The criteria provided in the `.where()` method takes precendence over the the criteria provided in `.find()`. > The `.find()` method returns a chainable object if you don't supply a callback. This method can be chained to `.find()` to further filter your results. <docmeta name="displayName" value=".where()"> <docmeta name="pageType" value="method"> ================================================ FILE: docs/reference/waterline/records/records.md ================================================ # Records In Sails, [records](https://sailsjs.com/documentation/concepts/models-and-orm/records) come from model methods like `.find()` and represent data from your database. You can work with records just like you would any other data. ```js var orders = await Order.find(); // `orders` is an array of records ``` ### Working with populated records If a record came from a query that used `.populate()`, it may contain populated values (or "child records") which represent the associated data. To add, remove, or replace these child records, use [model methods](https://sailsjs.com/documentation/reference/waterline-orm/models). <docmeta name="displayName" value="Records"> ================================================ FILE: docs/reference/waterline/records/toJSON.md ================================================ # `.toJSON()` ### Purpose Whenever Waterline retrieves a record, it checks whether or not the record’s model has a [`customToJSON`](https://sailsjs.com/documentation/concepts/models-and-orm/model-settings#?customtojson) method defined; if so, Waterline adds the method to the record as its `toJSON` property. `toJSON` is _**not intended to be called directly in your code**_. Instead, it is used automatically when a record is serialized via a call to <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#toJSON()_behavior" target="_blank">`JSON.stringify()`</a>. The [`res.json()` method](https://sailsjs.com/documentation/reference/response-res/res-json), in particular, stringifies objects in this way. When a [`customToJSON`](https://sailsjs.com/documentation/concepts/models-and-orm/model-settings#?customtojson) is defined for a model, the `.toJSON()` method will be added to records retrieved via [`.find()`](https://sailsjs.com/documentation/reference/waterline-orm/models/find), [`.findOne()`](https://sailsjs.com/documentation/reference/waterline-orm/models/find-one), [`.findOrCreate()`](https://sailsjs.com/documentation/reference/waterline-orm/models/find-or-create) and [`.stream()`](https://sailsjs.com/documentation/reference/waterline-orm/models/stream), as well as those retrieved by setting the [`fetch` meta key](https://sailsjs.com/documentation/reference/waterline-orm/queries/meta) to `true` in calls to [`.create()`](https://sailsjs.com/documentation/reference/waterline-orm/models/create), [`.createEach()`](https://sailsjs.com/documentation/reference/waterline-orm/models/create-each), [`.update()`](https://sailsjs.com/documentation/reference/waterline-orm/models/update) and [`.destroy()`](https://sailsjs.com/documentation/reference/waterline-orm/models/destroy). If any child records are attached via [`.populate()`](https://sailsjs.com/documentation/reference/waterline-orm/queries/populate), and the corresponding models have [`customToJSON`](https://sailsjs.com/documentation/concepts/models-and-orm/model-settings#?customtojson) methods, then the child records will also have `.toJSON()` functions attached. See the [customToJSON documentation](https://sailsjs.com/documentation/concepts/models-and-orm/model-settings#?customtojson) for more info on how to customize the way your records are presented. <docmeta name="displayName" value=".toJSON()"> <docmeta name="pageType" value="method"> ================================================ FILE: docs/reference/waterline/waterline.md ================================================ # Waterline (ORM) By default, new Sails apps are bundled with an ORM called [Waterline](http://waterlinejs.org) (implemented in the [sails-hook-orm](http://npmjs.com/package/sails-hook-orm) dependency). > To learn more about using Waterline, start in [Concepts > Models & ORM](https://sailsjs.com/documentation/concepts/models-and-orm). ### Reference This section of the documentation contains a reference of all of the methods available at runtime on [models](https://sailsjs.com/documentation/reference/waterline-orm/models), [queries](https://sailsjs.com/documentation/reference/waterline-orm/queries), and [datastores](https://sailsjs.com/documentation/reference/waterline-orm/datastores). <docmeta name="displayName" value="Waterline (ORM)"> ================================================ FILE: docs/reference/websockets/resourceful-pubsub/get-room-name.md ================================================ # `.getRoomName()` Retrieve the name of the PubSub “room” for a given record. ```js Something.getRoomName(id); ``` ### Usage | | Argument | Type | Details | |---|:-----------|:------------:|:--------| | 1 | id | ((number)) <br> or <br> ((string)) | The ID (primary key value) of the record to get the PubSub room name for. ### Example ```javascript // On the server: subscribeAllBobWatchersToKaren: function (req, res) { // Look up all users named "bob" or "karen". User.find({name: ['bob', 'karen']}, function(err, users) { if (err) {return res.serverError(err);} // Get Bob's ID. We'll assume there is only one Bob. var bobId = _.find(users, { name: 'bob' }).id; // Get Karen's ID. We'll assume there is only one Karen. var karenId = _.find(users, { name: 'karen' }).id; // Subscribe all of Bob's sockets to Karen. sails.sockets.addRoomMembersToRooms(User.getRoomName(bobId), User.getRoomName(karenId)); return res.send(); }); } ``` <docmeta name="displayName" value=".getRoomName()"> <docmeta name="pageType" value="method"> ================================================ FILE: docs/reference/websockets/resourceful-pubsub/publish.md ================================================ # `.publish()` Broadcast an arbitrary message to socket clients [subscribed](https://sailsjs.com/documentation/reference/web-sockets/resourceful-pub-sub/subscribe) to one or more of this model's [records](https://sailsjs.com/documentation/concepts/models-and-orm). ```js Something.publish(ids, data, req); ``` > The event name for this broadcast is the same as the model's identity. ### Usage | | Argument | Type | Details | |---|:-----------|:------------:|:--------| | 1 | ids | ((array)) | An array of record ids (primary key values). | 2 | data | ((json)) | The data to broadcast. | 3 | _req_ | ((req?)) | Optional. If provided, then the requesting socket will *not* receive the broadcast. ### Example ```javascript // On the server: tellSecretToBobs: function (req, res) { // Get the secret from the request. var secret = req.param('secret'); // Look up all users named "Bob". User.find({name: 'bob'}, function(err, bobs) { if (err) {return res.serverError(err);} // Tell the secret to every client who is subscribed to these users, // except for the client that made this request in the first place. // Note that the secret is wrapped in a dictionary with a `verb` property -- this is not // required, but helpful if you'll also be listening for events from Sails blueprints. User.publish(_.pluck(bobs, 'id'), { verb: 'published', theSecret: secret }, req); return res.send(); }); } ``` ```javascript // On the client: // Subscribe this client socket to Bob-only secrets // > See the `.subscribe()` documentation for more info about subscribing to records: // > https://sailsjs.com/documentation/reference/web-sockets/resourceful-pub-sub/subscribe io.socket.get('/subscribeToBobSecrets'); // Whenever a `user` event is received, do something. io.socket.on('user', function(msg) { if (msg.verb === 'published') { console.log('Got a secret only Bobs can hear:', msg.theSecret); } // else if (msg.verb === 'created') { ... } // else if (msg.verb === 'updated') { ... } }); ``` ### Notes > + Be sure to check that `req.isSocket === true` before passing in `req` to refer to the requesting socket. If used, the provided `req` must be from a socket request, not just any old HTTP request. <docmeta name="displayName" value=".publish()"> <docmeta name="pageType" value="method"> ================================================ FILE: docs/reference/websockets/resourceful-pubsub/resourceful-pubsub.md ================================================ # Resourceful PubSub (RPS) ### Overview For apps that rely heavily on [realtime](https://sailsjs.com/documentation/concepts/realtime) client-server communication—for example, peer-to-peer chat and social networking apps—sending and listening for socket events can quickly become overwhelming. Sails helps smooth away some of the complexity associated with socket events by introducing the concept of **resourceful PubSub** ([Publish / Subscribe](http://en.wikipedia.org/wiki/Publish%E2%80%93subscribe_pattern)). Every model in your app is automatically equipped with resourceful PubSub methods, which provide a conventional, data-centric interface for both _broadcasting notifications_ and _subscribing sockets to notifications_ about individual database records. If your app is currently using the [blueprint API](https://sailsjs.com/documentation/reference/blueprint-api), you are already using resourceful PubSub methods! They are embedded in the default blueprint actions bundled with Sails and are called automatically when those actions run, causing requesting sockets to be subscribed when data is fetched and messages to be broadcasted to already-subscribed sockets when data is changed. (Sockets can be subscribed via a call to [`.subscribe()`](https://sailsjs.com/documentation/reference/web-sockets/resourceful-pub-sub/subscribe) or due to a previous socket request to the [`find`](https://sailsjs.com/documentation/reference/blueprint-api/find) or [`findOne`](https://sailsjs.com/documentation/reference/blueprint-api/find-one) blueprints.) Even when writing custom code, you can manually call the methods described in this section in lieu of using `sails.sockets.*` methods directly. Think of resourceful PubSub methods as a way of standardizing the interface for socket communication across your application—these interface elements might be the names of rooms, the schema for data transmitted as socket messages, or the names of socket events. These methods are designed _exclusively_ for scenarios where one or more user interfaces are listening to socket events in order to stay in sync with the backend. If that does not fit your use case or if you are having trouble deciding, don't worry; just call [`sails.sockets.broadcast()`](https://sailsjs.com/documentation/reference/web-sockets/sails-sockets/broadcast), [`sails.sockets.join()`](https://sailsjs.com/documentation/reference/web-sockets/sails-sockets/join), or [`sails.sockets.leave()`](https://sailsjs.com/documentation/reference/web-sockets/sails-sockets/leave) directly, instead. It is perfectly acceptable to use either approach, or even _both_ approaches in the same app. ### Methods Sails exposes three different resourceful PubSub (RPS) methods: `.publish()`, `.subscribe()`, and `.unsubscribe()`. To get a deeper understanding of resourceful PubSub methods, you may find it useful to familiarize yourself with the underlying [`sails.sockets.*`](https://sailsjs.com/documentation/reference/web-sockets/sails-sockets) methods first. That's because each RPS method is more or less just a contextualized wrapper around one of the simpler `sails.sockets.*` methods: + [`.publish()`](https://sailsjs.com/documentation/reference/web-sockets/resourceful-pub-sub/publish) is like _[`sails.sockets.broadcast()`](https://sailsjs.com/documentation/reference/web-sockets/sails-sockets/broadcast)_ + [`.subscribe()`](https://sailsjs.com/documentation/reference/web-sockets/resourceful-pub-sub/subscribe) is like _[`sails.sockets.join()`](https://sailsjs.com/documentation/reference/web-sockets/sails-sockets/join)_ + [`.unsubscribe()`](https://sailsjs.com/documentation/reference/web-sockets/resourceful-pub-sub/unsubscribe) is like _[`sails.sockets.leave()`](https://sailsjs.com/documentation/reference/web-sockets/sails-sockets/leave)_ The biggest difference between these methods and their counterparts in `sails.sockets.*` is that RPS methods expose a higher-level interface. For example, RPS methods choose room names for you behind the scenes, and they infer a conventional event name based on your model's identity. ### Listening for events on the client While you are free to use any JavaScript library to listen for socket events on the client, Sails provides its own socket client called [`sails.io.js`](https://sailsjs.com/documentation/reference/web-sockets/socket-client) as a convenient way to communicate with the Sails server from any web browser or Node.js process that supports Socket.IO. Using the Sails socket client makes listening for resourceful PubSub events as easy as: ```javascript io.socket.on('<model identity>', function (msg) { }); ``` > The _[model identity](https://sailsjs.com/documentation/concepts/models-and-orm/model-settings#?identity)_ is typically the lowercased version of the model name, unless it has been manually configured in the model file. ### Example Let’s say you have a model named `User` in your app, with a single “name” attribute. First, we’ll add a listener for “user” events: ```javascript io.socket.on('user', function(msg){ console.log(msg); }) ``` This will log any notifications that our client socket receives to the console, so long as those socket notifications have "user" as their event name. However, we won’t actually receive those messages until we *subscribe* this client socket to one or more existing `User` records (in our server-side code). If your app has the blueprint API enabled, then subscribing the client socket to the `User` records is really easy. In addition to fetching data, if the ["Find" blueprint action](https://sailsjs.com/documentation/reference/blueprint-api/find-where) is accessed via a [socket request](https://sailsjs.com/documentation/reference/web-sockets/socket-client/io-socket-get), then it calls [`User.subscribe()`](https://sailsjs.com/documentation/reference/web-sockets/resourceful-pub-sub/subscribe) (a resourceful PubSub method) automatically. For example, imagine you write some client-side code that sends a socket `GET` request to `http://localhost:1337/user`: ```js io.socket.get('/user', function(resData) { console.log(resData); }); ``` When that runs, it will hit the "Find" blueprint action, which returns the current list of users from the Sails server. And if we'd sent a normal HTTP request (like `jQuery.get('/user')`), then that's all that would happen. But because we sent a _socket request_, the server _also_ subscribed our client socket to future notifications (calls to [`.publish()`](https://sailsjs.com/documentation/reference/web-sockets/resourceful-pub-sub/publish)) about the user records that were returned. > See [`io.socket.get()`](https://sailsjs.com/documentation/reference/web-sockets/socket-client/io-socket-get) for more info about using the `sails.io.js` client to send virtual requests. Unlike `.subscribe()`, the RPS `.publish()` method can run from anywhere—a controller action triggered as the result of a socket request, an AJAX request, or even a cURL request from the command line. Alternatively, `.publish()` could be called from a custom helper or in a command-line script. Continuing with the above example, if you were to open an additional browser window and go to the following URL: ``` /user/create?name=joe ``` You would see something like the following in the console of the original window: ```js { verb: 'created', id: 1, data: { id: 1, name: 'joe', createdAt: '2014-08-01T05:50:19.855Z' updatedAt: '2014-08-01T05:50:19.855Z' } } ``` What you're seeing here is a dictionary (aka plain JavaScript object) that was broadcasted by the ["Create" blueprint action](https://sailsjs.com/documentation/reference/blueprint-api/create). In the case of the blueprint API, the format of this data is standardized, but in your app, you can use `.publish()` to broadcast any data you like. <docmeta name="displayName" value="Resourceful PubSub"> ================================================ FILE: docs/reference/websockets/resourceful-pubsub/subscribe.md ================================================ # `.subscribe()` Subscribe the requesting client socket to changes/deletions of one or more database records. ```js Something.subscribe(req, ids); ``` ### Usage | | Argument | Type | Details | |---|:-----------|:------------:|:--------| | 1 | req | ((req)) | The incoming socket request (`req`) containing the socket to subscribe. | 2 | ids | ((array)) | An array of record ids (primary key values). When a client socket is subscribed to a record, it is a member of its dynamic "record room". That means it will receive all messages broadcasted to that room by [`.publish()`](https://sailsjs.com/documentation/reference/web-sockets/resourceful-pub-sub/publish). ### Example On the server, in a controller action: ```javascript // On the server: if (!this.req.isSocket) { throw {badRequest: 'Only a client socket can subscribe to Louies. But you look like an HTTP request to me.'}; } // Let's say our client socket has a problem with people named "louie". // First we'll find all users named "louie" (or "louis" even-- we should be thorough) let usersNamedLouie = await User.find({ or: [{name: 'louie'},{name: 'louis'}] }); // Now we'll subscribe our client socket to each of these records. User.subscribe(this.req, _.pluck(usersNamedLouie, 'id')); // All done! We might send down some data, or just an empty 200 (OK) response. ``` Then, back in our client-side code: ```javascript // On the client: // Send a request to the "subscribeToLouies" action, subscribing this client socket // to all future events that the server publishes about Louies. io.socket.get('/foo/bar/subscribeToLouies', function (data, jwr){ if (jwr.error) { console.error('Could not subscribe to Louie-related notifications: '+jwr.error); return; } console.log('Successfully subscribed.'); }); ``` From now on, as long as our requesting client socket stays connected, it will receive a notification any time our server-side code (e.g. other actions or helpers) calls `User.publish()` for one of the Louies we subscribed to above. In order for our client-side code to handle these future notifications, it must _listen_ for the relevant event with `.on()`. For example: ```js // On the client: // Whenever a `user` event is received, say something. io.socket.on('user', function(msg) { console.log('Got a message about a Louie: ', msg); }); ``` See [Concepts > Realtime](https://sailsjs.com/documentation/concepts/realtime) for more background on the difference between rooms and events in Sails/Socket.IO. ### Multiple rooms per record For some applications, you may find yourself needing to manage two different channels related to the same record. To accomplish this, you can combine [`.getRoomName()`](https://sailsjs.com/documentation/reference/web-sockets/resourceful-pub-sub/get-room-name) and [`sails.sockets.join()`](https://sailsjs.com/documentation/reference/web-sockets/sails-sockets/join): ```js // On the server, in your subscribe action… if (!orgId) { throw 'badRequest'; } if (!this.req.isSocket) { throw {badRequest: 'This action is designed for use with WebSockets.'}; } let me = await User.findOne({ id: this.req.session.userId }) .populate('globalAdminOfOrganizations'); // Subscribe to general notifications. Organization.subscribe(this.req, orgId); // If this user is a global admin of this organization, then also subscribe them to // an additional private room (this is used for additional notifications intended only // for global admins): if (globalAdminOfOrganizations.includes(orgId)) { let privateRoom = Organization.getRoomName(`${orgId}-admins-only`); sails.sockets.join(this.req, privateRoom); } ``` Later, to publish to one of these rooms, just compute the appropriate room name (e.g. "13-admins-only") and use [`sails.sockets.broadcast()`](https://sailsjs.com/documentation/reference/web-sockets/sails-sockets/broadcast) to blast out your notification. ### Notes > + Be sure and check `req.isSocket === true` before passing in `req` to refer to the requesting socket. The provided `req` must be from a socket request, not just any old HTTP request. > + `.subscribe()` will only work with requests made over a Socket.IO connection (e.g. using `io.socket.get()`), *not* over an HTTP connection (e.g. using `jQuery.get()`). See the [`sails.io.js` socket client documentation](https://sailsjs.com/documentation/reference/web-sockets/socket-client) for information on using client sockets to send WebSockets/Socket.IO messages with Sails. > + This function does _not actually talk to the database_! In fact, none of the resourceful PubSub methods do. Rather, these make up a simplified abstraction layer built on top of the lower-level `sails.sockets` methods, designed to make your app cleaner and easier to debug by using conventional names for events/rooms/namespaces etc. <docmeta name="displayName" value=".subscribe()"> <docmeta name="pageType" value="method"> ================================================ FILE: docs/reference/websockets/resourceful-pubsub/unsubscribe.md ================================================ # `.unsubscribe()` Unsubscribe the requesting client socket from one or more database records. ```js Something.unsubscribe(req, ids); ``` ### Usage | | Argument | Type | Details | |---|:-----------|:------------:|:--------| | 1 | req | ((req)) | The incoming socket request (`req`) containing the socket to unsubscribe. | 2 | ids | ((array)) | An array of record ids (primary key values). ### Example On the server: ```javascript unsubscribeFromUsersNamedLenny: function (req, res) { if (!req.isSocket) { return res.badRequest(); } User.find({name: 'Lenny'}).exec(function(err, lennies) { if (err) { return res.serverError(err); } var lennyIds = _.pluck(lennies, 'id'); User.unsubscribe(req, lennyIds); return res.ok(); }); }, ``` ### Notes > + Be sure to check that `req.isSocket === true` before passing in `req` to refer to the requesting socket. The provided `req` must be from a socket request, not just any old HTTP request. > + `unsubscribe` will only work when the request is made over a socket connection (e.g. using `io.socket.get()`), *not* over HTTP (e.g. using `jQuery.get()`). <docmeta name="displayName" value=".unsubscribe()"> <docmeta name="pageType" value="method"> ================================================ FILE: docs/reference/websockets/sails.io.js/SailsSocket/SailsSocket.md ================================================ # SailsSocket By default, [`sails.io.js`](https://sailsjs.com/documentation/reference/web-sockets/socket-client) automatically connects a single socket (`io.socket`) almost immediately after it loads. This allows your client-side code to send socket requests to a particular Sails server and to receive events and data sent from that server. For 99% of apps, this is all you need. However, for certain advanced use cases (including automated tests), it can be helpful to connect additional sockets from the same instance of the socket client (e.g. browser tab). For this reason, Sails exposes the `SailsSocket` class. ### Overview The `sails.io.js` library works by wrapping low-level [Socket.io](http://socket.io) clients in instances of the `SailsSocket` class. This class provides higher-level methods like [`.get()`](https://sailsjs.com/documentation/reference/web-sockets/socket-client/io-socket-get) and [`.post()`](https://sailsjs.com/documentation/reference/web-sockets/socket-client/io-socket-post) to your sockets, allowing you to communicate with your Sails app in a familiar way. ### Creating a SailsSocket instance Any web page that loads the `sails.io.js` will create a new SailsSocket instance on page load unless [`io.sails.autoConnect`](https://sailsjs.com/documentation/reference/web-sockets/socket-client/io-sails#?autoconnect) is set to `false`. This instance is then available as the global variable [`io.socket`](https://sailsjs.com/documentation/reference/web-sockets/socket-client/io-socket). Additional SailsSocket instances can be created via calls to [`io.sails.connect`](https://sailsjs.com/documentation/reference/web-sockets/socket-client/io-sails#?the-connect-method): ```javascript var newSailsSocket = io.sails.connect(); ``` <docmeta name="displayName" value="SailsSocket"> <docmeta name="pageType" value="class"> ================================================ FILE: docs/reference/websockets/sails.io.js/SailsSocket/methods.md ================================================ # SailsSocket methods This section describes the methods available on each SailsSocket instance. Most of these methods can be called before the socket even connects to the server. In the case of request methods like `.get()` and `.request()`, calls will be queued up until the socket connects, at which time they will be executed in order. ### Basic methods The most common methods you will use with a SailsSocket instance are documented in the main Socket Client reference section. These include [`.get()`](https://sailsjs.com/documentation/reference/web-sockets/socket-client/io-socket-get), [`.put()`](https://sailsjs.com/documentation/reference/web-sockets/socket-client/io-socket-put), [`.post()`](https://sailsjs.com/documentation/reference/web-sockets/socket-client/io-socket-post), [`.delete()`](https://sailsjs.com/documentation/reference/web-sockets/socket-client/io-socket-delete), [`.request()`](https://sailsjs.com/documentation/reference/web-sockets/socket-client/io-socket-request), [`.on()`](https://sailsjs.com/documentation/reference/web-sockets/socket-client/io-socket-on) and [`.off()`](https://sailsjs.com/documentation/reference/web-sockets/socket-client/io-socket-off). ### Advanced methods In addition to the basic communication and event-listening methods, each SailsSocket instance (including `io.socket`) exposes several methods for dealing with server connections. ##### `.isConnected()` Determines whether the SailsSocket instance is currently connected to a server; returns `true` if a connection has been established. ```js io.socket.isConnected(); ``` ##### `.isConnecting()` Determines whether the SailsSocket instance is currently in the process of connecting to a server; returns `true` if a connection is being attempted. ```js io.socket.isConnecting(); ``` ##### `.mightBeAboutToAutoConnect()` Detects when the SailsSocket instance has already loaded but is not yet fully configured or has not attempted to autoconnect. The `sails.io.js` library waits one tick of the event loop before checking whether [`autoConnect`](https://sailsjs.com/documentation/reference/web-sockets/socket-client/io-sails#?iosailsautoconnect) is enabled and, if so, trying to connect. This allows you to configure the `SailsSocket` instance (for example, by setting `io.sails.url`) before an attempt is made to estabilish a connection. The `mightBeAboutToAutoConnect()` method returns `true` in the situation where `sails.io.js` has loaded, but the requisite tick of the event loop has not yet elapsed. ```js io.socket.mightBeAboutToAutoConnect(); ``` ##### `.disconnect()` Disconnects a SailsSocket instance from the server; throws an error if the socket is already disconnected. ```js io.socket.disconnect(); ``` ##### `.reconnect()` Reconnects a SailsSocket instance to a server after it's been disconnected (either involuntarily or via a call to [`.disconnect()`](https://sailsjs.com/documentation/reference/web-sockets/socket-client/sails-socket/methods#?disconnect)). The instance connects using its currently configured [properties](https://sailsjs.com/documentation/reference/web-sockets/socket-client/sails-socket/properties). `.reconnect()` throws an error if the socket is already connected to a server. ```js io.socket.reconnect(); ``` > When an instance is in a disconnected state, its properties may be changed. This means that an instance that has been disconnected from one server can be reconnected to another without losing its event bindings or queued requests. ##### `.removeAllListeners()` Stops listening for any server-related events on a SailsSocket instance, including `connect` and `disconnect`. ```js io.socket.removeAllListeners(); ``` <docmeta name="displayName" value="Methods"> ================================================ FILE: docs/reference/websockets/sails.io.js/SailsSocket/properties.md ================================================ # SailsSocket properties ### Overview This page describes the properties available on each [SailsSocket instance](https://sailsjs.com/documentation/reference/web-sockets/socket-client/sails-socket). These properties are set in the initial call to `io.sails.connect`, which creates the SailsSocket and cannot be changed while the socket is connected (with the exception of `headers`). If the socket becomes disconnected (either involuntarily or as a result of a call to [`.disconnect`](https://sailsjs.com/documentation/reference/web-sockets/socket-client/sails-socket/methods#?disconnect)), its properties can be changed until the socket connects again. This means that an instance that has been disconnected from one server can be reconnected to another without losing its event bindings or queued requests. ### Common properties Property | Type | Default | Details :-------------------|------------|:----------|:------------------------ `url` | ((string)) | Value of [`io.sails.url`](https://sailsjs.com/documentation/reference/web-sockets/socket-client/sails-socket/properties#?iosails-defaults) | The URL to which the socket is connected or will attempt to connect. `transports` | ((array)) | Value of [`io.sails.transports`](https://sailsjs.com/documentation/reference/web-sockets/socket-client/sails-socket/properties#?iosails-defaults) | The transports by which the socket will attempt to connect. Transports will be tried in order with upgrades allowed; that is, if you list both "polling" and "websocket", then after establishing a long-polling connection, the server will attempt to upgrade it to a websocket connection. This setting should match the value of `sails.config.sockets.transports` in your Sails app. `headers` | ((dictionary)) | Value of [`io.sails.headers`](https://sailsjs.com/documentation/reference/web-sockets/socket-client/sails-socket/properties#?iosails-defaults) | Dictionary of headers to be sent by default with every request from this socket after it connects. Can be overridden via the `headers` option in [`.request()`](https://sailsjs.com/documentation/reference/web-sockets/socket-client/io-socket-request). See `initialConnectionHeaders` below for information on setting headers for the initial socket handshake request. ### Advanced properties Property | Type | Default | Details :------------------ |----------|:--------- |:------- `query` | ((string)) | Value of [`io.sails.query`](https://sailsjs.com/documentation/reference/web-sockets/socket-client/sails-socket/properties#?iosails-defaults) | Query string to use with the initial connection to the server. In server code, this can be accessed via `req.socket.handshake.query` in controller actions or `handshake._query` in [socket lifecycle callbacks](https://sailsjs.com/documentation/reference/configuration/sails-config-sockets). Note that information about the `sails.io.js` SDK version will be tacked onto whatever query string you specify. A common usage of `query` is to set `nosession=true`, indicating that the Sails app should _not_ associate the connecting socket with a browser session. `initialConnectionHeaders` | ((dictionary)) | Value of [`io.sails.initialConnectionHeaders`](https://sailsjs.com/documentation/reference/web-sockets/socket-client/sails-socket/properties#?iosails-defaults) | _Node.js only—not available in browser._ Dictionary of headers to be sent with the _initial connection to the server_ (as opposed to the `headers` property above, which contains headers to be sent with every socket request made _after_ the initial connection). In server code, the initial connection headers can be accessed via `req.socket.handshake.headers` in controller actions or `socket.handshake.headers` in [socket lifecycle callbacks](https://sailsjs.com/documentation/reference/configuration/sails-config-sockets). This is useful for (for example) sending a `cookie` header with the initial handshake, allowing a socket to connect to a previously-established Sails session. `useCORSRouteToGetCookie` | ((boolean)) or ((string)) | Value of [`io.sails.useCORSRouteToGetCookie`](https://sailsjs.com/documentation/reference/web-sockets/socket-client/sails-socket/properties#?iosails-defaults) | Only relevant in browser environments and if you are relying on the default Sails session + session cookies for authentication. For cross-origin socket connections, use this property to choose a route to send an initial JSONP request in order to retrieve a cookie, so that the right session can be established. The route should respond with the string `'_sailsIoJSConnect();'`, which will allow the connection to continue. If `useCORSRouteToGetCookie` is `true`, the default `/__getcookie` route on the Sails server will be used. If it is `false`, no attempt will be made to contact the remote server before connecting the socket. *Note: this strategy may fail on certain browsers (including certain versions of Safari) which block third-party cookies by default.* ### `io.sails.*` defaults The `io.sails` object can be used to provide default values for new client sockets. For example, setting `io.sails.url = "http://myapp.com:1234"` will cause every new client socket to connect to `http://myapp.com:1234`, unless a `url` value is provided in the call to `io.sails.connect()`. The following are the default values for properties in `io.sails`: Property | Default :------------------|:------- `url` | In browser, the URL of the page that loaded the `sails.io.js` script. In Node.js, no default. `transports` | `['websocket']` `headers` | `{}` `query` | `''` `initialConnectionHeaders` | `{}` `useCORSRouteToGetCookie` | `true` <docmeta name="displayName" value="Properties"> ================================================ FILE: docs/reference/websockets/sails.io.js/io.sails.md ================================================ # The `io.sails` object ### Overview The `io.sails` object is the home of global configuration options for the `sails.io.js` library and any sockets it creates. Most of the properties on `io.sails` are used as settings when connecting a client socket to the server or as top-level configuration for the client library itself. `io.sails` also provides a `.connect()` method used for creating new socket connections manually. See [Socket Client](https://sailsjs.com/documentation/reference/web-sockets/socket-client) for information about your different options for configuring `io.sails`. ### The `.connect()` method If [`io.sails.autoConnect`](https://sailsjs.com/documentation/reference/web-sockets/socket-client/io-sails#?autoconnect) is `false`, or if you need to create more than one socket connection with the `sails.io.js` library, you do so via `io.sails.connect([url], [options])`. Both arguments are optional, and the value of the `io.sails` properties (like `url`, `transports`, etc.) are used as defaults. See the [SailsSocket properties reference](https://sailsjs.com/documentation/reference/web-sockets/socket-client/sails-socket/properties) for options. ### `io.sails.autoConnect` When `io.sails.autoConnect` is set to `true` (the default setting), the library will wait one cycle of the event loop after loading and then attempt to create a new [`SailsSocket`](https://sailsjs.com/documentation/reference/web-sockets/socket-client/sails-socket) and connect it to the URL specified by `io.sails.url`. When used in the browser, the new socket will be exposed as `io.socket`. When used in a Node.js script, the new socket will be attached as the `socket` property of the variable used to initialize the `sails.io.js` library. ### `io.sails.reconnection` When `io.sails.reconnection` is set to `true`, sockets will automatically (and continuously) attempt to reconnect to the server if they become disconnected unexpectedly (that is, _not_ as the result of a call to [`.disconnect()`](https://sailsjs.com/documentation/reference/web-sockets/socket-client/sails-socket/methods#?disconnect)). If set to `false` (the default), no automatic reconnection attempt will be made. Defaults to `false`. ### `io.sails.environment` Use `io.sails.environment` to set an environment for `sails.io.js`, which affects how much information is logged to the console. Valid values are `development` (full logs) and `production` (minimal logs). ### Other properties and defaults The other properties of `io.sails` are used as defaults when creating new sockets (either the eager socket or via [`io.sails.connect()`](https://sailsjs.com/documentation/reference/web-sockets/socket-client/io-sails#?the-connect-method)). See the [SailsSocket properties reference](https://sailsjs.com/documentation/reference/web-sockets/socket-client/sails-socket/properties) for a full list of available options, as well as a table of the default `io.sails` values. Here are the most commonly used properties: Property | Type | Default | Details :------------------ |----------|:--------- |:------- url | ((string)) | Value of [`io.sails.url`](https://sailsjs.com/documentation/reference/web-sockets/socket-client/sails-socket/properties#?iosails-defaults) | The URL that the socket is connected to, or will attempt to connect to. transports | ((array)) | Value of [`io.sails.transports`](https://sailsjs.com/documentation/reference/web-sockets/socket-client/sails-socket/properties#?iosails-defaults) | The transports that the socket will attempt to connect using. Transports will be tried in order, with upgrades allowed: that is, if you list both "polling" and "websocket", then after establishing a long-polling connection the server will attempt to upgrade it to a websocket connection. This setting should match the value of `sails.config.sockets.transports` in your Sails app. headers | ((dictionary)) | Value of [`io.sails.headers`](https://sailsjs.com/documentation/reference/web-sockets/socket-client/sails-socket/properties#?iosails-defaults) | Dictionary of headers to be sent by default with every request from this socket. Can be overridden via the `headers` option in [`.request()`](https://sailsjs.com/documentation/reference/web-sockets/socket-client/io-socket-request). <docmeta name="displayName" value="io.sails"> <docmeta name="pageType" value="property"> ================================================ FILE: docs/reference/websockets/sails.io.js/io.socket.md ================================================ # `io.socket` ### Overview When used in the browser, `sails.io.js` creates a global instance of the [SailsSocket](https://sailsjs.com/documentation/reference/web-sockets/socket-client/sails-socket) class as soon as it loads and attempts to connect it to the server after waiting one event loop cycle (to allow for configuration options to be changed). As with any [SailsSocket](https://sailsjs.com/documentation/reference/web-sockets/socket-client/sails-socket), you can start using its properties and methods even before it connects to the server. Any requests or event bindings will be queued up and replayed once the connection is established. ### Configuration Options Like any [SailsSocket](https://sailsjs.com/documentation/reference/web-sockets/socket-client/sails-socket) instance, `io.socket` is affected by the global [`io.sails`](https://sailsjs.com/documentation/reference/web-sockets/socket-client/io-sails) settings. The `sails.io.js` library waits for one event loop cycle before attempting to connect `io.socket` to the server, giving you a chance to change any settings first. ##### Example Changing the server that `io.socket` connects to ```html <script type="text/javascript" src="/js/dependencies/sails.io.js"></script> <script type="text/javascript"> io.sails.url = "http://somesailsapp.com"; </script> ``` ### Properties See the [SailsSocket properties reference](https://sailsjs.com/documentation/reference/web-sockets/socket-client/sails-socket/properties) for a full list of properties available on `io.socket`. ### Methods For basic server communication and event listening methods, see the other `io.socket.*` pages in this section. For advanced methods involving server connection, see the [SailsSocket advanced methods reference](https://sailsjs.com/documentation/reference/web-sockets/socket-client/sails-socket/methods). <docmeta name="displayName" value="io.socket"> <docmeta name="pageType" value="property"> ================================================ FILE: docs/reference/websockets/sails.io.js/io.socket.off.md ================================================ # `io.socket.off()` Unbind the specified event handler (opposite of [`.on()`](https://sailsjs.com/documentation/reference/web-sockets/socket-client/io-socket-on)). ```js io.socket.off(eventIdentity, handlerFn); ``` > **This method is here for completeness, but most apps should not need to use it.** See below for more information. ### Usage | | Argument | Type | Details | |---|------------|:------------:|:--------| | 1 | eventIdentity | ((string)) | The unique event identity associated with a server-sent message, e.g. "recipe". | 2 | handlerFn | ((function)) | The event handler function to unbind from the specified event. ### Notes > + If you decide to use this method, be careful! `io.socket.off()` does **not** stop the this client-side socket from receiving any server-sent messages, it just prevents the specified event handler from firing. Usually, the desired effect is to prevent messages _from being sent altogether_, which is critical if your server-sent messages contain private data. This happens automatically when a socket disconnects, but there are also less-common use cases where it is necessary to unsubscribe sockets from rooms while they are still connected. For example, consider a scenario where an admin user is banned from your system while viewing a realtime dashboard, and your app needs to prevent them from receiving all subsequent realtime updates. To force a client socket to stop receiving broadcasted messages, **do not use this method**. Instead, unsubscribe the socket in your server-side code: > + If the room was joined using `sails.sockets.join()`, call `sails.sockets.leave()`. > + If the room was joined using resourceful PubSub methods, call `.unsubscribe()` or `.unwatch()` as appropriate. > + In order to use `.off()`, you will need to store the `handlerFn` argument you passed in to [`.on()`](https://sailsjs.com/documentation/reference/web-sockets/socket-client/io-socket-on) in a variable. <docmeta name="displayName" value="io.socket.off()"> <docmeta name="pageType" value="method"> ================================================ FILE: docs/reference/websockets/sails.io.js/io.socket.on.md ================================================ # `io.socket.on()` Start listening for socket events from Sails with the specified `eventName`. Triggers the provided callback function when a matching event is received. ```js io.socket.on(eventName, function (msg) { // ... }); ``` ### Usage | | Argument | Type | Details | |---|-------------|:------------:|:--------| | 1 | eventName | ((string)) | The name of the socket event, e.g. `'recipe'` or `'welcome'`. | 2 | handlerFn | ((function)) | An event handler that will be called when the server broadcasts a notification to this socket. Will only be called if the incoming socket notification matches `eventName`. ##### Event handler | | Argument | Type | Details | |---|:----------|:---------------:|:--------| | 1 | msg | ((json)) | The data from the socket notification. ### When is the event handler called? This event handler is called when the client receives an incoming socket notification that matches the specified event name (e.g. `'welcome'`). This happens when the server broadcasts a message to this socket directly, or to a room of which it is a member. To broadcast a socket notification, you need to either use the [blueprint API](https://sailsjs.com/documentation/concepts/blueprints) or write some server-side code (e.g. in an action, helper, or even in a command-line script). This is typically achieved in one of the following ways: ###### Low-level socket methods (`sails.sockets`) + server blasts out a message to all connected sockets (see [sails.sockets.blast()](https://sailsjs.com/documentation/reference/web-sockets/sails-sockets/blast)) + server broadcasts a message directly to a particular socket using its unique ID or to an entire room full of sockets (see [sails.sockets.broadcast()](https://sailsjs.com/documentation/reference/web-sockets/sails-sockets/broadcast)) ###### Resourceful Pubsub Methods + server broadcasts a message about a record, which multiple sockets might be subscribed to (see [Model.publish()](https://sailsjs.com/documentation/reference/web-sockets/resourceful-pub-sub/publish) + server broadcasts a message as part of the "Create" blueprint action _(only relevant if using [blueprints](https://sailsjs.com/documentation/concepts/blueprints))_ ### Example Listen for the "order" event: ```javascript io.socket.on('order', function onServerSentEvent (msg) { // msg => {...whatever the server broadcasted...} }); ``` ##### Realtime cafeteria Imagine you're building an ordering system for a chain of restaurants: ```javascript // In your frontend code... // (This example uses jQuery and Lodash for simplicity. But you can use any library or framework you like.) var ORDER_IN_LIST = _.template('<li data-id="<%- order.id %>"><p><%- order.summary %></p></li>'); $(function whenDomIsReady(){ // Every time we receive a relevant socket event... io.socket.on('order', function (msg) { // Let's see what the server has to say... switch(msg.verb) { case 'created': (function(){ // Render the new order in the DOM. var newOrderHtml = ORDER_IN_LIST(msg.data); $('#orders').append(newOrderHtml); })(); return; case 'destroyed': (function(){ // Find any existing orders w/ this id in the DOM. // // > Remember: To prevent XSS attacks and bugs, never build DOM selectors // > using untrusted provided by users. (In this case, we know that "id" // > did not come from a user, so we can trust it.) var $deletedOrders = $('#orders').find('[data-id="'+msg.id+'"]'); // Then, if there are any, remove them from the DOM. $deletedOrders.remove(); })(); return; // Ignore any unrecognized messages default: return; }//< / switch > });//< / io.socket.on() > });//< / when DOM is ready > ``` > Note that this example assumes the backend calls [`.publish()`](https://sailsjs.com/documentation/reference/web-sockets/resourceful-pub-sub/publish) or [`.broadcast()`](https://sailsjs.com/documentation/reference/web-sockets/sails-sockets/broadcast) at some point. That might be through custom code, or via the [blueprint API](https://sailsjs.com/documentation/concepts/blueprints). ### The `'connect'` event By default, when the Sails socket client is loaded on a page, it begins connecting a socket for you automatically. When using the default, auto-connecting socket (`io.socket`), you don't have to wait for the socket to connect before using it. In other words, you can listen for other socket events or call methods like [`io.socket.get()`](https://sailsjs.com/documentation/reference/web-sockets/socket-client/io-socket-get) immediately. The Sails socket client will queue up anything you do in the meantime and then replay it automatically once the connection is live. Consequently, direct usage of the `'connect'` event **is not necessary for most apps**. But in the spirit of completeness, it is worth mentioning that you can also bind your own `'connect'` handler: ```javascript io.socket.on('connect', function onConnect(){ console.log('This socket is now connected to the Sails server.'); }); ``` ### The `'disconnect'` event If a socket's connection to the server was interrupted—perhaps because the server was restarted, or the client had some kind of network issue—it is possible to handle `disconnect` events in order to display an error message or even to manually reconnect the socket again. ```javascript io.socket.on('disconnect', function onDisconnect(){ console.log('This socket lost connection to the Sails server'); }); ``` > Sockets can be configured to reconnect automatically. However, as of Sails v1, the Sails socket client disables this behavior by default. In practice, since your user interface might have missed socket notifications while disconnected, you'll almost always want to handle any related custom logic by hand. (For example, a "Check your internet connection" error message). ### Notes >+ Remember that a socket only stays subscribed to a room for as long as it is connected—e.g. as long as the browser tab is open—or until it is manually unsubscribed on the server using [`.unsubscribe()`](https://sailsjs.com/documentation/reference/web-sockets/resourceful-pub-sub/unsubscribe) or [`.leave()`](https://sailsjs.com/documentation/reference/web-sockets/sails-sockets/leave). >+ When listening for socket messages from resourceful PubSub calls and blueprints, the event name is always the same as the identity of the calling model. For example, if you have a model named "UserComment", the model's identity (and therefore the socket event name used by [`UserComment.publish()`](https://sailsjs.com/documentation/reference/web-sockets/resourceful-pub-sub)) is "usercomment". >+ For context, socket notifications are also sometimes referred to as "server-sent events" or "[comet](http://en.wikipedia.org/wiki/Comet_(programming)) messages". <docmeta name="displayName" value="io.socket.on()"> <docmeta name="pageType" value="method"> ================================================ FILE: docs/reference/websockets/sails.io.js/sails.io.js.md ================================================ # Socket client (`sails.io.js`) > This section of the docs is about the Sails socket client SDK for the browser. It is written in JavaScript, and is also usable on the server. > > There are also a handful of community projects implementing Sails/Socket.IO clients for native iOS, Android, and Windows Phone. ### Overview The Sails socket client ([`sails.io.js`](https://github.com/balderdashy/sails.io.js)) is a tiny browser library that is bundled by default in new Sails apps. It is a lightweight wrapper that sits on top of the Socket.IO client and whose purpose is to make sending and receiving messages from your Sails backend as simple as possible. The main responsibility of `sails.io.js` is to provide a familiar, Ajax-like interface for communicating with your Sails app using WebSockets/Socket.IO. That basically means providing `.get()`, `.post()`, `.put()`, and `.delete()` methods that allow you take advantage of realtime features while still reusing the same backend routes you're using for the rest of your app. In other words, running `io.socket.post('/user')` in your browser will be routed within your Sails app in exactly the same way as an HTTP POST request to the same route. ### Basic usage (browser) In the browser, all that is required to use `sails.io.js` is to include the library in a `<SCRIPT>` tag. Sails adds the library to the `assets/js/dependencies` folder of all new apps, so you can write: ```html <!-- This will import the sails.io.js library bundled in your Sails app by default. The bundled version embeds minified code for the Socket.io client as well. One tick of the event loop after importing this script, a new "eager" socket will automatically be created begin connecting unless you configure it not to. --> <script type="text/javascript" src="/js/dependencies/sails.io.js"></script> ``` and then use `io.socket` as a global variable in subsequent inline or external scripts. For detailed instructions and examples of everyday usage, see [`io.socket`](https://sailsjs.com/documentation/reference/web-sockets/socket-client/io-socket). ### Basic usage (Node.js) To use the Sails socket client SDK in a Node.js script, you will need to install and require both the `sails.io.js` and `socket.io-client` libraries: ```javascript // Initialize the sails.io.js library with the socket.io-client module, // which will automatically create and connect a new socket as io.socket // unless you configure it not to. var io = require('sails.io.js')( require('socket.io-client') ); ``` See the [sails.io.js GitHub repo](https://github.com/balderdashy/sails.io.js) for more information on using the Sails socket client from Node.js. ### Configuring the `sails.io.js` library > This section focuses on the most common runtime environment for the JavaScript socket client: the browser. See the [`sails.io.js` GitHub repository](https://github.com/balderdashy/sails.io.js) for help configuring the socket client for use in a Node.js script. There are two ways to configure Sails' socket client in the browser: using HTML attributes on the `<script>` tag or by programmatically modifying the `io.sails` object. ##### Basic configuration using HTML attributes The easiest way to configure the four most common settings for the socket client (`autoConnect`, `environment`, `headers`, and `url`) is by sticking one or more HTML attributes on the script tag: ```html <script src="/js/dependencies/sails.io.js" autoConnect="false" environment="production" headers='{ "x-csrf-token": "<%= typeof _csrf !== 'undefined' ? _csrf : '' %>" }' ></script> ``` This example will disable the eager socket connection, force the client environment to "production" (which disables logs), and set an `x-csrf-token` header that will be sent in every socket request (unless overridden). Note that composite values like the `headers` dictionary are wrapped in a pair of _single-quotes_. That's because composite values specified this way must be _JSON-encoded_, meaning that their key names and value strings must be enclosed in double quotes (for a simlilar reason, the strings within the value string are enclosed in single quotes). Any configuration that can be provided as an HTML attribute can alternately be provided prefixed with `data-` (e.g. `data-autoConnect`, `data-environment`, `data-headers`, `data-url`). This is for folks who need to support browsers that have issues with nonstandard HTML attributes (or if the idea of using nonstandard HTML attributes just creeps you out). If both the standard HTML attribute and the `data-` prefixed HTML attribute are provided, the latter takes precendence. > **Note:** > In order to use this approach for configuring the socket client, if you are using the default Grunt asset pipeline (which automatically injects script tags), you will need to remove `sails.io.js` from your `pipeline.js` file, and instead include an explicit `<script>` tag, which imports it. ##### Programmatic configuration using `io.sails` As of Sails v0.12.x, only the most basic configuration options may be set using HTML attributes. If you want to configure any of the other options not mentioned above, you will need to interact with `io.sails` programmatically. Fortunately, the approach described above is really just a convenient shortcut for doing just that! Heres how it works: When you load it on the page in a `<script>` tag, the `sails.io.js` library waits for one cycle of the event loop before _automatically connecting_ a socket (if `io.sails.autoConnect` is enabled, [see below](https://sailsjs.com/documentation/reference/web-sockets/socket-client/io-sails#?autoconnect)). This allows any properties that you specify on `io.sails` to be set before the socket begins connecting. However, in order to ensure that the `io.sails` properties are read before connection, you should put the code setting those properties immediately after the `<script>` tag that includes `sails.io.js`: ```html <script src="/js/dependencies/sails.io.js"></script> <script type="text/javascript"> io.sails.url = 'https://myapp.com'; </script> <!-- ...other scripts... --> ``` Normally, the socket client always connects to the server where the script is being served. The example above will cause the eager (auto-connecting) socket to attempt a (cross-domain) socket connection to the Sails server running at `https://myapp.com`, instead. > **Note:** > If you are using the default Grunt asset pipeline (which automatically injects script tags), it is a good idea to exclude `sails.io.js` from your `pipeline.js` file, instead explicitly adding a `<script>` tag for it. This ensures that your configuration will be applied _before_ the "eager" auto-connecting socket begins connecting, since it means that the inline `<script>` tag you are using for programmatic configuration (setting `io.sails.url = 'https://myapp.com';`, for example) is executed _immediately after_ the socket client. ### Advanced usage You can also create and connect client sockets manually using [`io.sails.connect`](https://sailsjs.com/documentation/reference/web-sockets/socket-client/io-sails#?the-connect-method). This returns an instance of the `SailsSocket`. For more information about use cases that are less common and more advanced, such as connecting multiple sockets, see [SailsSocket](https://sailsjs.com/documentation/reference/web-sockets/socket-client/sails-socket). ##### Advanced configuration The `sails.io.js` library and its individual client sockets have a handful of configuration options. Global configuration lives in [`io.sails`](https://sailsjs.com/documentation/reference/web-sockets/socket-client/io-sails), which—among other things—allows you to disable the "eager" socket and default settings for new sockets. Individual sockets can also be configured when they are manually connected—see [`io.sails.connect()`](https://sailsjs.com/documentation/reference/web-sockets/socket-client/io-sails#?the-connect-method) for more information on that. ### Frequently asked questions ##### Can I use this with XYZ front-end framework? Yes. The Sails socket client can be used to great effect with any front-end framework, whether it's Angular, React, Ember, Backbone, Knockout, jQuery, [FishBerry](https://mrsharpoblunto.github.io/foswig.js/), etc. ##### Do I have to use this? No. The Sails socket client is extremely helpful when building realtime/chat features in a browser-based UI, but like the rest of the `assets/` directory, it is probably not particularly useful if you are building a [native Android app](https://stackoverflow.com/questions/25081188/sending-socket-request-from-client-ios-android-to-sails-js-server/25081189#25081189) or an API with no user interface at all. Fortunately, like every other boilerplate file and folder in Sails, the socket client is completely optional. To remove it, just delete `assets/js/dependencies/sails.io.js`. ##### How does this work? Under the hood, the socket client (`sails.io.js`) emits Socket.IO messages with reserved names that, when interpreted by Sails, are routed to the appropriate policies/controllers/etc. according to your app's routes and blueprint configuration. ##### How do I tell my Sails app _not_ to connect a socket with the current browser session? By default, a socket connection will be linked to the current browser session (if any) using the `cookie` header that is sent with the initial socket handshake. In order to turn off this behavior, add `nosession=true` to the [`query` property](https://sailsjs.com/documentation/reference/web-sockets/socket-client/sails-socket/properties#?advanced-properties) of the socket before it connects. For example: ``` <script src="/js/dependencies/sails.io.js"></script> <script type="text/javascript">io.sails.query='nosession=true';</script> ``` ##### Can I bypass this client and use Socket.IO directly? It is possible to bypass the request interpreter in your Sails app and communicate with Socket.IO directly. However, it is not reccommended, since it breaks the convention-over-configuration philosophy used elsewhere in the framework. The Sails socket client (`sails.io.js`) is unobtrusive: it works by wrapping the native Socket.IO client and exposing a higher-level API that takes advantage of the virtual request interpreter in Sails to send simulated HTTP requests. This makes your backend code more reusable, reduces the barrier to entry for developers new to using WebSockets/Socket.IO, and keeps your app easier to reason about. > **Note:** > In very rare circumstances (e.g. compatibility with an existing/legacy frontend using Socket.IO directly), bypassing the request interpreter is a _requirement_. If you find yourself in this position, you can use the Socket.IO client, SDK, and then use `sails.io` on the backend to access the raw Socket.IO instance. Please embark on this road only if you have extensive experience working directly with Socket.IO, and only if you have first reviewed the internals of the [`sockets` hook](https://github.com/balderdashy/sails-hook-sockets) (particularly the "admin bus" implementation, a Redis integration that sits on top of @sailshq/socket.io-redis and powers Sails's multi-server support for joining/leaving rooms). <docmeta name="displayName" value="Socket client"> ================================================ FILE: docs/reference/websockets/sails.io.js/socket.delete.md ================================================ # `io.socket.delete()` Send a virtual DELETE request to a Sails server using Socket.IO. ```js io.socket.delete(url, data, function (data, jwres){ // ... }); ``` ### Usage | | Argument | Type | Details | |---|------------|:------------:|---------| | 1 | url | ((string)) | The destination URL path, e.g. "/checkout". | 2 | _data_ | ((json?)) | Optional request data. If provided, it will be URL-encoded and appended to `url` (existing query string params in url will be preserved). | 3 | _callback_ | ((function?)) | Optional callback. If provided, it will be called when the server responds. ##### Callback | | Argument | Type | Details | |---|-----------|:------------:|---------| | 1 | resData | ((json)) | Data received in the response from the Sails server (=== `jwres.body`, equivalent to the HTTP response body). | 2 | jwres | ((dictionary)) | A JSON WebSocket Response object. Has `headers`, a `body`, and a `statusCode`. ### Example ```html <script> io.socket.delete('/users/9', function (resData) { resData; // => {id:9, name: 'Timmy Mendez', occupation: 'psychic'} }); </script> ``` ### Notes > + Remember that you can communicate with _any of your routes_ using socket requests. > + Need to customize request headers? Check out the slightly lower-level [`io.socket.request()`](https://sailsjs.com/documentation/reference/web-sockets/socket-client/io-socket-request) method. To set custom headers for _all_ outgoing requests, check out [`io.sails.headers`](https://sailsjs.com/documentation/reference/web-sockets/socket-client/io-sails). <docmeta name="displayName" value="io.socket.delete()"> <docmeta name="pageType" value="method"> ================================================ FILE: docs/reference/websockets/sails.io.js/socket.get.md ================================================ # `io.socket.get()` Send a socket request (virtual GET) to a Sails server using Socket.IO. ```js io.socket.get(url, data, function (resData, jwres){ // ... }); ``` ### Usage | | Argument | Type | Details | |---|:-----------|:------------:|:--------| | 1 | url | ((string)) | The destination URL path, e.g. "/checkout". | 2 | _data_ | ((json?)) | Optional request data. If provided, it will be URL-encoded and appended to `url` (existing query string params in url will be preserved). | 3 | _callback_ | ((function?)) | Optional callback. If provided, it will be called when the server responds. ##### Callback | | Argument | Type | Details | |---|:----------|:---------------:|:--------| | 1 | resData | ((json)) | Data, if any, sent in the response from the Sails server. This is the same thing as `jwres.body`. | 2 | jwres | ((dictionary)) | A JSON WebSocket response, which consists of `headers` (a ((dictionary))), `body` (((json))), and `statusCode` (a ((number))). ### Example ```html <script> io.socket.get('/users/9', function (resData) { // resData => {id:9, name: 'Timmy Mendez'} }); </script> ``` ### Notes > + Remember that you can communicate with _any of your routes_ using socket requests. > + Need to customize request headers? Check out the slightly lower-level [`io.socket.request()`](https://sailsjs.com/documentation/reference/web-sockets/socket-client/io-socket-request) method. To set custom headers for _all_ outgoing requests, check out [`io.sails.headers`](https://sailsjs.com/documentation/reference/web-sockets/socket-client/io-sails). <docmeta name="displayName" value="io.socket.get()"> <docmeta name="pageType" value="method"> ================================================ FILE: docs/reference/websockets/sails.io.js/socket.patch.md ================================================ # `io.socket.patch()` Send a socket request (virtual PATCH) to a Sails server using Socket.IO. ```js io.socket.patch(url, data, function (resData, jwres){ // ... }); ``` ### Usage | | Argument | Type | Details | |---|------------|:------------:|---------| | 1 | url | ((string)) | The destination URL path, e.g. "/checkout". | 2 | _data_ | ((json?)) | Optional request data. If provided, it will be JSON-encoded and included as the virtual HTTP body. | 3 | _callback_ | ((function?)) | Optional callback. If provided, it will be called when the server responds. ##### Callback | | Argument | Type | Details | |---|-----------|:------------:|---------| | 1 | resData | ((json)) | Data received in the response from the Sails server (=== `jwres.body`, equivalent to the HTTP response body). | 2 | jwres | ((dictionary))| A JSON WebSocket Response object. Has `headers`, a `body`, and a `statusCode`. ### Example ```html <script> io.socket.patch('/users/9', { occupation: 'psychic' }, function (resData, jwr) { resData.statusCode; // => 200 }); </script> ``` ### Notes > + Remember that you can communicate with _any of your routes_ using socket requests. > + Need to customize request headers? Check out the slightly lower-level [`io.socket.request()`](https://sailsjs.com/documentation/reference/web-sockets/socket-client/io-socket-request) method. To set custom headers for _all_ outgoing requests, check out [`io.sails.headers`](https://sailsjs.com/documentation/reference/web-sockets/socket-client/io-sails). <docmeta name="displayName" value="io.socket.patch()"> <docmeta name="pageType" value="method"> ================================================ FILE: docs/reference/websockets/sails.io.js/socket.post.md ================================================ # `io.socket.post()` Send a socket request (virtual POST) to a Sails server using Socket.IO. ```js io.socket.post(url, data, function (resData, jwres){ // ... }); ``` ### Usage | | Argument | Type | Details | |---|------------|:------------:|---------| | 1 | url | ((string)) | The destination URL path, e.g. "/checkout". | 2 | _data_ | ((json?)) | Optional request data. If provided, it will be JSON-encoded and included as the virtual HTTP body. | 3 | _callback_ | ((function?)) | Optional callback. If provided, it will be called when the server responds. ##### Callback | | Argument | Type | Details | |---|-----------|:------------:|---------| | 1 | resData | ((json)) | Data received in the response from the Sails server (=== `jwres.body`, and also equivalent to the HTTP response body). | 2 | jwres | ((dictionary)) | A JSON WebSocket Response object. Has `headers`, a `body`, and a `statusCode`. ### Example ```html <script> io.socket.post('/users', { name: 'Timmy Mendez' }, function (resData, jwRes) { jwRes.statusCode; // => 200 }); </script> ``` ### Notes > + Remember that you can communicate with _any of your routes_ using socket requests. > + Need to customize request headers? Check out the slightly lower-level [`io.socket.request()`](https://sailsjs.com/documentation/reference/web-sockets/socket-client/io-socket-request) method. To set custom headers for _all_ outgoing requests, check out [`io.sails.headers`](https://sailsjs.com/documentation/reference/web-sockets/socket-client/io-sails). <docmeta name="displayName" value="io.socket.post()"> <docmeta name="pageType" value="method"> ================================================ FILE: docs/reference/websockets/sails.io.js/socket.put.md ================================================ # `io.socket.put()` Send a socket request (virtual PUT) to a Sails server using Socket.IO. ```js io.socket.put(url, data, function (resData, jwres){ // ... }); ``` ### Usage | | Argument | Type | Details | |---|------------|:------------:|---------| | 1 | url | ((string)) | The destination URL path, e.g. "/checkout". | 2 | _data_ | ((json?)) | Optional request data. If provided, it will be JSON-encoded and included as the virtual HTTP body. | 3 | _callback_ | ((function?)) | Optional callback. If provided, it will be called when the server responds. ##### Callback | | Argument | Type | Details | |---|-----------|:------------:|---------| | 1 | resData | ((json)) | Data received in the response from the Sails server (=== `jwres.body`, equivalent to the HTTP response body). | 2 | jwres | ((dictionary)) | A JSON WebSocket Response object. Has `headers`, a `body`, and a `statusCode`. ### Example ```html <script> io.socket.put('/users/9', { occupation: 'psychic' }, function (resData, jwr) { resData.statusCode; // => 200 }); </script> ``` ### Notes > + Remember that you can communicate with _any of your routes_ using socket requests. > + Need to customize request headers? Check out the slightly lower-level [`io.socket.request()`](https://sailsjs.com/documentation/reference/web-sockets/socket-client/io-socket-request) method. To set custom headers for _all_ outgoing requests, check out [`io.sails.headers`](https://sailsjs.com/documentation/reference/web-sockets/socket-client/io-sails). <docmeta name="displayName" value="io.socket.put()"> <docmeta name="pageType" value="method"> ================================================ FILE: docs/reference/websockets/sails.io.js/socket.request.md ================================================ # `io.socket.request()` Send a virtual request to a Sails server using Socket.IO. This function is very similar to [`io.socket.get()`](https://sailsjs.com/documentation/reference/web-sockets/socket-client/io-socket-get), [`io.socket.post()`](https://sailsjs.com/documentation/reference/web-sockets/socket-client/io-socket-post), etc. except that it provides lower-level access to the request headers, parameters, method, and URL of the request. Using the automatically-created [`io.socket`](https://sailsjs.com/documentation/reference/web-sockets/socket-client/io-socket) instance: ```js io.socket.request(options, function (resData, jwres)){ // ... // jwres.headers // jwres.statusCode // jwres.body === resData // ... }); ``` ### Usage | Option | Type | Details | |:-----------|:------------:|:--------| | method | ((string)) | The HTTP request method; e.g. `'GET'`. | url | ((string)) | The destination URL path; e.g. "/checkout". | _data_ | ((json?)) | Optional. If provided, this request data will be JSON-encoded and included as the virtual HTTP body. | _headers_ | ((dictionary?)) | Optional. If provided, this dictionary of string headers will be sent as virtual request headers. ##### Callback | | Argument | Type | Details | |---|:----------|:------------:|:--------| | 1 | `resData` | ((json)) | Data received in the response from the Sails server (=== `jwres.body`, and also equivalent to the HTTP response body). | 2 | `jwres` | ((dictionary)) | A JSON WebSocket Response object. Has `headers`, a `body`, and a `statusCode`. ### Example ```javascript io.socket.request({ method: 'get', url: '/user/3/friends', data: { limit: 15 }, headers: { 'x-csrf-token': 'ji4brixbiub3' } }, function (resData, jwres) { if (jwres.error) { console.log(jwres.statusCode); // => e.g. 403 return; } console.log(jwres.statusCode); // => e.g. 200 }); ``` ### Notes > + A helpful analogy might be to think of the difference between `io.socket.get` and this method as the difference between JQuery's `$.get` and `$.ajax`. > + Remember that you can communicate with _any of your routes_ using socket requests. > + Need to set custom headers for _all_ outgoing requests? Check out [`io.sails.headers`](https://sailsjs.com/documentation/reference/web-sockets/socket-client/io-sails). <docmeta name="displayName" value="io.socket.request()"> <docmeta name="pageType" value="method"> ================================================ FILE: docs/reference/websockets/sails.sockets/sails.sockets.addRoomMembersToRooms.md ================================================ # `.addRoomMembersToRooms()` Subscribe all members of a room to one or more additional rooms. ```js sails.sockets.addRoomMembersToRooms(sourceRoom, destRooms, cb); ``` ### Usage | | Argument | Type | Details | |---|------------|:-----------:|:--------| | 1 | sourceRoom | ((string)) | The room from which to retrieve members. | 2 | destRooms | ((string)), ((array)) | The room or rooms to which to subscribe the members of `sourceRoom`. | 3 | cb | ((function?))| An optional callback, which will be called when the operation is complete on the current server (see notes below for more information) or if fatal errors were encountered. In the case of errors, it will be called with a single argument (`err`). ### Example In a controller action: ```javascript subscribeFunRoomMembersToFunnerRooms: function(req, res) { sails.sockets.addRoomMembersToRooms('funRoom', ['greatRoom', 'awesomeRoom'], function(err) { if (err) {return res.serverError(err);} res.json({ message: 'Subscribed all members of `funRoom` to `greatRoom` and `awesomeRoom`!' }); }); } ``` ### Notes > + In a multi-server environment, the callback function (`cb`) will be executed when the `.addRoomMembersToRooms()` call completes _on the current server_. This does not guarantee that other servers in the cluster have already finished running the operation. <docmeta name="displayName" value=".addRoomMembersToRooms()"> <docmeta name="pageType" value="method"> ================================================ FILE: docs/reference/websockets/sails.sockets/sails.sockets.blast.md ================================================ # `.blast()` Broadcast a message to all sockets connected to the server (or any server in the cluster, if you have a multi-server deployment using Redis). ```javascript sails.sockets.blast(data); ``` or: + `sails.sockets.blast(eventName, data);` + `sails.sockets.blast(data, socketToOmit);` + `sails.sockets.blast(eventName, data, socketToOmit);` ### Usage | | Argument | Type | Details | |---|:-------------------------- | ------------------- |:----------------------------------------------------------------- | | 1 | _eventName_ | ((string?)) | Optional. Defaults to `'message'`. | 2 | data | ((json)) | The data to send in the message. | 3 | _socketToOmit_ | ((req?)) | Optional. If provided, the socket associated with this socket request will **not** receive the message blasted out to everyone else. Useful when the broadcast-worthy event is triggered by a requesting user who doesn't need to hear about it again. ### Example In a controller action... ```javascript sails.sockets.blast('user_logged_in', { msg: 'User #' + user.id + ' just logged in.', user: { id: user.id, username: user.username } }, req); ``` ### Notes > + Be sure to check that `req.isSocket === true` before passing in `req` to this method. For the socket to be omitted, the current `req` must be from a socket request, not just any HTTP request. <docmeta name="displayName" value=".blast()"> <docmeta name="pageType" value="method"> ================================================ FILE: docs/reference/websockets/sails.sockets/sails.sockets.broadcast.md ================================================ # `.broadcast()` Broadcast a message to all sockets in a room (or to a particular socket). ```javascript sails.sockets.broadcast(roomNames, data); ``` _Or:_ + `sails.sockets.broadcast(roomNames, eventName, data);` + `sails.sockets.broadcast(roomNames, data, socketToOmit);` + `sails.sockets.broadcast(roomNames, eventName, data, socketToOmit);` ### Usage | | Argument | Type | Details |---|:--------------------------- | ------------------- |:----------- | 1 | roomNames | ((string)), ((Array)) | The name of one or more rooms in which to broadcast a message (see [sails.sockets.join](https://sailsjs.com/documentation/reference/web-sockets/sails-sockets/join)). To broadcast to individual sockets, use their IDs as room names. | 2 | _eventName_ | ((string?)) | Optional. The unique name of the event used by the client to identify this message. Defaults to `'message'`. | 3 | data | ((json)) | The data to send in the message. | 4 | _socketToOmit_ | ((req?)) | Optional. If provided, the socket belonging to the specified socket request will *not* receive the message. This is useful if you trigger the broadcast from a client, but don't want that client to receive the message itself (for example, sending a message to everybody else in a chat room). ### Example In an action, service, or arbitrary script on the server: ```javascript sails.sockets.broadcast('artsAndEntertainment', { greeting: 'Hola!' }); ``` On the client: ```javascript io.socket.on('message', function (data){ console.log(data.greeting); }); ``` ##### Additional Examples More examples of `sails.sockets.brodcast()` usage are [available here](https://gist.github.com/mikermcneil/0a4d05750768a99b4fcb), including broadcasting to multiple rooms, using a custom event name, and omitting the requesting socket. ### Notes > + `sails.sockets.broadcast()` is more or less equivalent to the functionality of `.emit()` and `.broadcast()` in Socket.IO. > + Every socket is automatically subscribed to a room with its ID as the name, allowing direct messaging to a socket via [`sails.sockets.broadcast()`](https://sailsjs.com/documentation/reference/web-sockets/sails-sockets/sails-sockets-broadcast) > + Be sure to check that `req.isSocket === true` before passing in `req` as `socketToOmit`. For the requesting socket to be omitted, the request (`req`) must be from a socket request, not just any old HTTP request. > + `data` must be JSON-serializable; i.e. it's best to use plain dictionaries/arrays, and make sure your data does not contain any circular references. If you aren't sure, build your broadcast `data` manually, or call something like [`rttc.dehydrate(data,true,true)`](https://github.com/node-machine/rttc/blob/master/README.md#dehydratevalue-allownullfalse-dontstringifyfunctionsfalse) on it first. <docmeta name="displayName" value=".broadcast()"> <docmeta name="pageType" value="method"> ================================================ FILE: docs/reference/websockets/sails.sockets/sails.sockets.getid.md ================================================ # `.getId()` Parse the socket ID from an incoming socket request (`req`). ```javascript sails.sockets.getId(req); ``` ### Usage | | Argument | Type | Details |---| --------------------------- | ------------------- | ----------- | 1 | req | ((req)) | A socket request (`req`). Once acquired, the socket object's ID can be used to send direct messages to that socket (see [sails.sockets.broadcast](https://sailsjs.com/documentation/reference/web-sockets/sails-sockets/broadcast)). ### Example ```javascript // Controller action getSocketID: function(req, res) { if (!req.isSocket) { return res.badRequest(); } var socketId = sails.sockets.getId(req); // => "BetX2G-2889Bg22xi-jy" sails.log('My socket ID is: ' + socketId); return res.json(socketId); } ``` ### Notes > + Be sure to check that `req.isSocket === true` before passing in `req`. This method does not work for HTTP requests! <docmeta name="displayName" value=".getId()"> <docmeta name="pageType" value="method"> ================================================ FILE: docs/reference/websockets/sails.sockets/sails.sockets.id.md ================================================ # sails.sockets.id() This method is an alias for [sails.sockets.getId()](https://sailsjs.com/documentation/reference/web-sockets/sails-sockets/get-id) deprecated in Sails v0.12. Please see the [v0.12 migration guide](https://sailsjs.com/documentation/concepts/upgrading/to-v-0-12) for more information. <docmeta name="displayName" value="sails.sockets.id()"> <docmeta name="isDeprecated" value="true"> ================================================ FILE: docs/reference/websockets/sails.sockets/sails.sockets.join.md ================================================ # `.join()` Subscribe a socket to a room. ```js sails.sockets.join(socket, roomName); ``` or: + `sails.sockets.join(socket, roomName, cb);` ### Usage | | Argument | Type | Details | |---|------------|:-----------:|:--------| | 1 | socket | ((string)), ((req)) | The socket to be subscribed. May be specified by the socket's ID or an incoming socket request (`req`). | 2 | roomName | ((string)) | The name of the room to which the socket will be subscribed. If the room does not exist yet, it will be created. | 3 | _cb_ | ((function?))| An optional callback, which will be called when the operation is complete on the current server (see notes below for more information), or if fatal errors were encountered. In the case of errors, it will be called with a single argument (`err`). ### Example In a controller action: ```javascript subscribeToFunRoom: function(req, res) { if (!req.isSocket) { return res.badRequest(); } var roomName = req.param('roomName'); sails.sockets.join(req, roomName, function(err) { if (err) { return res.serverError(err); } return res.json({ message: 'Subscribed to a fun room called '+roomName+'!' }); }); } ``` ### Notes > + `sails.sockets.join()` is more or less equivalent to the functionality of `.join()` in Socket.IO, but with additional built-in support for multi-server deployments. With [recommended production settings](https://sailsjs.com/documentation/concepts/deployment/scaling), `sails.sockets.join()` works as documented, no matter what server the code happens to be running on or the server to which the target socket is connected. > + In a multi-server environment, when calling `.join()` with a socket ID argument, the callback function (`cb`) will be executed when the `.join()` call completes _on the current server_. This does not guarantee that other servers in the cluster have already finished running the operation. > + Every socket is automatically subscribed to a room with its ID as the name, allowing direct messaging to a socket via [`sails.sockets.broadcast()`](https://sailsjs.com/documentation/reference/web-sockets/sails-sockets/sails-sockets-broadcast) > + Be sure to check that `req.isSocket === true` before passing in `req` as the target socket. For that to work, the provided `req` must be from a socket request, not just any old HTTP request. <docmeta name="displayName" value=".join()"> <docmeta name="pageType" value="method"> ================================================ FILE: docs/reference/websockets/sails.sockets/sails.sockets.leave.md ================================================ # `.leave()` Unsubscribe a socket from a room. ```js sails.sockets.leave(socket, roomName); ``` or: + `sails.sockets.leave(socket, roomName, cb);` ### Usage | | Argument | Type | Details | |---|------------|:-----------:|:--------| | 1 | socket | ((string)), ((req)) | The socket to be unsubscribed. May be either the incoming socket request (req) or the ID of another socket. | 2 | roomName | ((string)) | The name of the room to which the socket will be unsubscribed. | 3 | _cb_ | ((function?))| An optional callback, which will be called when the operation is complete on the current server (see notes below for more information), or if fatal errors were encountered. In the case of errors, it will be called with a single argument (`err`). ### Example In a controller action, unsubscribe the requesting socket from the specified room: ```javascript leaveFunRoom: function(req, res) { if ( _.isUndefined(req.param('roomName')) ) { return res.badRequest('`roomName` is required.'); } if (!req.isSocket) { return res.badRequest('This endpoints only supports socket requests.'); } var roomName = req.param('roomName'); sails.sockets.leave(req, roomName, function(err) { if (err) {return res.serverError(err);} return res.json({ message: 'Left a fun room called '+roomName+'!' }); }); } ``` ##### Additional Examples More examples of `sails.sockets.leave()` usage are [available here](https://gist.github.com/mikermcneil/971b4e92d833211a0243), including unsubscribing other sockets by ID, deeper integration with the database, usage within a service, and usage with the `async` library. ### Notes > + `sails.sockets.leave()` is more or less equivalent to the functionality of `.leave()` in Socket.IO, but with additional built-in support for multi-server deployments. With [recommended production settings](https://sailsjs.com/documentation/concepts/deployment/scaling), `sails.sockets.leave()` works as documented no matter what server the code happens to be running on or the server to which the target socket is connected. > + In a multi-server environment, when calling `.leave()` with a socket ID argument, the callback function (`cb`) will be executed when the `.leave()` call completes _on the current server_. This does not guarantee that other servers in the cluster have already finished running the operation. > + Be sure to check that `req.isSocket === true` before passing in `req` as the socket to be unsubscribed. For that to work, the provided `req` must be from a socket request, not just any old HTTP request. <docmeta name="displayName" value=".leave()"> <docmeta name="pageType" value="method"> ================================================ FILE: docs/reference/websockets/sails.sockets/sails.sockets.leaveAll.md ================================================ # `.leaveAll()` Unsubscribe all members of a room (e.g. `chatroom7`) from that room _and_ every other room they are currently subscribed to, except the automatic room associated with their socket ID. ```javascript sails.sockets.leaveAll(roomName, cb); ``` ### Usage | | Argument | Type | Details | |---|:-----------|:-----------:|:--------| | 1 | roomName | ((string)) | The room to evactuate. Note that this room's members will be forced to leave _all of their rooms_, not just this one. | 2 | cb | ((function?))| An optional callback, which will be called when the operation is complete _on the current server_ (see notes below for more information), or if fatal errors were encountered. In the case of errors, it will be called with a single argument (`err`). ### Example In a controller action: ```javascript unsubscribeFunRoomMembersFromEverything: function(req, res) { sails.sockets.leaveAll('funRoom', function(err) { if (err) { return res.serverError(err); } // Unsubscribed all sockets in "funRoom" from "funRoom". // And... from every other room too. return res.ok(); }); } ``` ### Notes > + In a multi-server environment, the callback function (`cb`) will be executed when the `.leaveAll()` call completes _on the current server_. This does not guarantee that other servers in the cluster have already finished running the operation. <docmeta name="displayName" value=".leaveAll()"> <docmeta name="pageType" value="method"> ================================================ FILE: docs/reference/websockets/sails.sockets/sails.sockets.md ================================================ # Sockets (`sails.sockets`) ### Overview Sails exposes several methods (`sails.sockets.*`) that provide a simple interface for [realtime communication](https://sailsjs.com/documentation/concepts/realtime) with connected socket clients. These are useful for pushing events and data to connected clients in realtime, rather than waiting for their HTTP requests. These methods are available regardless of whether a client socket was connected from a browser tab, an iOS app, or your favorite household IoT appliance. These methods are implemented using a built-in instance of [Socket.IO](http://socket.io), which is available directly as [`sails.io`](https://sailsjs.com/documentation/reference/application/advanced-usage#?sailsio). However, you should _almost never_ use `sails.io` directly. Instead, you should call the methods available on `sails.sockets.*`. In addition, for certain use cases, you might also want to take advantage of [resourceful PubSub methods](https://sailsjs.com/documentation/reference/web-sockets/resourceful-pub-sub), which access a higher level of abstraction and are used by Sails' built-in [blueprint API](https://sailsjs.com/documentation/reference/blueprint-api). ### Methods | Method | Description | |:-----------------------------------|:---------------------------------------------------------| | [`.addRoomMembersToRooms()`](https://sailsjs.com/documentation/reference/web-sockets/sails-sockets/add-room-members-to-rooms) | Subscribe all members of a room to one or more additional rooms. | [`.blast()`](https://sailsjs.com/documentation/reference/web-sockets/sails-sockets/blast) | Broadcast a message to all sockets connected to the server. | [`.broadcast()`](https://sailsjs.com/documentation/reference/web-sockets/sails-sockets/broadcast) | Broadcast a message to all sockets in a room. | [`.getId()`](https://sailsjs.com/documentation/reference/web-sockets/sails-sockets/get-id) | Parse the socket ID from an incoming socket request (`req`). | [`.join()`](https://sailsjs.com/documentation/reference/web-sockets/sails-sockets/join) | Subscribe a socket to a room. | [`.leave()`](https://sailsjs.com/documentation/reference/web-sockets/sails-sockets/leave) | Unsubscribe a socket from a room. | [`.leaveAll()`](https://sailsjs.com/documentation/reference/web-sockets/sails-sockets/leave-all) | Unsubscribe all members of one room from that room _and_ from every other room they are currently subscribed to, except the automatic room with the same name as each socket ID. | [`.removeRoomMembersFromRooms()`](https://sailsjs.com/documentation/reference/web-sockets/sails-sockets/remove-room-members-from-rooms) | Unsubscribe all members of a room from one or more other rooms. > Don't see a method you're looking for above? A number of `sails.sockets` methods were deprecated in Sails v0.12, either because a more performant alias was already available, or for performance and scalability reasons. Please see the [v0.12 migration guide](https://sailsjs.com/documentation/concepts/upgrading/to-v-0-12) for more information. <docmeta name="displayName" value="sails.sockets"> <docmeta name="pageType" value="property"> ================================================ FILE: docs/reference/websockets/sails.sockets/sails.sockets.removeRoomMembersFromRoom.md ================================================ # `.removeRoomMembersFromRooms()` Unsubscribe all members of a room from one or more other rooms. ```js sails.sockets.removeRoomMembersFromRooms(sourceRoom, destRooms, cb); ``` ### Usage | | Argument | Type | Details | |---|----------------|:----------------------------:|:--------| | 1 | sourceRoom | ((string)) | The room from which to retrieve members. | 2 | destRooms | ((string)), ((array)) | The room or rooms from which to unsubscribe the members of `sourceRoom`. | 3 | cb | ((function?)) | An optional callback, which will be called when the operation is complete _on the current server_ (see notes below for more information), or if fatal errors were encountered. In the case of errors, it will be called with a single argument (`err`). ### Example In a controller action: ```javascript unsubscribeFunRoomMembersFromFunnerRooms: function(req, res) { sails.sockets.removeRoomMembersFromRooms('funRoom', ['greatRoom', 'awesomeRoom'], function(err) { if (err) {return res.serverError(err);} res.json({ message: 'Unsubscribed all members of `funRoom` from `greatRoom` and `awesomeRoom`!' }); }); } ``` ### Notes > + In a multi-server environment, the callback function (`cb`) will be executed when the `.removeRoomMembersFromRooms()` call completes _on the current server_. This does not guarantee that other servers in the cluster have already finished running the operation. <docmeta name="displayName" value=".removeRoomMembersFromRooms()"> <docmeta name="pageType" value="method"> ================================================ FILE: docs/reference/websockets/websockets.md ================================================ # WebSockets For a full discussion of realtime concepts in Sails, see the [Realtime concept documentation](https://sailsjs.com/documentation/concepts/realtime). For information on client-to-server socket communication, see the [Socket Client (sails.io.js)](https://sailsjs.com/documentation/reference/web-sockets/socket-client). For information on server-to-client socket communication, see the [sails.sockets](https://sailsjs.com/documentation/reference/web-sockets/sails-sockets). For information on using realtime messages to communicate changes in Sails models, see the [Resourceful PubSub reference](https://sailsjs.com/documentation/reference/web-sockets/resourceful-pub-sub). Sails uses [socket.io](http://socket.io) as the underlying engine for realtime communication. Every Sails app has a Socket.IO instance available as `sails.io`. However, most `socket.io` functionality is wrapped for convenience (and safety) by a `sails.sockets` method. <docmeta name="displayName" value="WebSockets"> ================================================ FILE: docs/security/README.md ================================================ # docs/security This section contains the official policy for disclosing security vulnerabilities in Sails or our dependencies. It is made available at https://sailsjs.com/security-policy. ### Notes > - This README file **is not compiled to HTML** for the website. It is just here to explain what you're looking at. > - Depending on what branch of `sails` you are currently viewing, the domain may vary. See the top-level documentation README file for information about working with the markdown files in this repo, and to understand the branching/versioning strategy. <docmeta name="notShownOnWebsite" value="true"> ================================================ FILE: docs/security/SAILS-SECURITY-POLICY.md ================================================ # Security policy Sails is committed to providing a secure framework, and quickly responding to any suspected security vulnerabilities. Contributors work carefully to ensure best practices, but we also rely heavily on the community when it comes to discovering, reporting, and remediating security issues. ### Reporting a security issue in Sails If you believe you've found a security vulnerability in Sails, Waterline, or one of the other modules maintained by the Sails core team, please send an email to **critical at sailsjs dot com**. In the spirit of [responsible disclosure](https://en.wikipedia.org/wiki/Responsible_disclosure), we ask that you privately report any security vulnerability at that email address, and give us time to patch the issue before publishing the details</em>. ### What is a security vulnerability? A security vulnerability is any major bug or unintended consequence that could compromise a Sails.js app in production. For example, an issue where Sails crashes in a development environment when using non-standard Grunt tasks is _not a security vulnerability_. On the other hand, if it was possible to perform a trivial DoS attack on a Sails cluster running in a production environment and using documented best-practices (a la the [Express/Connect body parser issue](http://expressjs-book.com/index.html%3Fp=140.html)), that _is a security vulnerability_ and we want to know about it. > Note that this definition includes any such vulnerability that exists due to one of our dependencies. In this case, an upgrade to a different version of the dependency is not always necessary: for example, when Express 3 deprecated multipart upload support in core, Sails.js dealt with the feature mismatch by implementing a wrapper around the `multiparty` module called [Skipper](https://github.com/balderdashy/skipper#history). ### What should be included in the email? - The name and NPM version string of the module where you found the security vulnerability (e.g. Sails, Waterline, other core module). - A summary of the vulnerability - The code you used when you discovered the vulnerability or a code example of the vulnerability (whichever is shorter). - Whether you want us to make your involvement public. If you want such a reference the name and link you wish to be referred (e.g. Jane Doe's link to her GitHub account) > Please respect the core team's privacy and do not send bugs resulting from undocumented usage, questions, or feature requests to this email address. ### The process When you report a vulnerability, one of the project members will respond to you within a maximum of 72 hours. This response will most likely be an acknowledgement that we've received the report and will be investigating it immediately. Our target patching timeframe for most security vulnerabilities is 14 days. Based upon the nature of the vulnerability, and the amount of time it would take to fix, we'll either send out a patch that disables the broken feature, provide an estimate of the time it will take to fix, and/or document best practices to follow to avoid production issues. You can expect follow-up emails outlining the progression of a solution to the vulnerability along with any other questions we may have regarding your experience. ##### When a solution is achieved we do the following: - notify you - release a patch on NPM - coordinate with [Node Security](http://nodesecurity.io) to issue an [advisory](https://nodesecurity.io/advisories?search=sails), crediting you (unless you expressly asked not to be identified) - publicize the release via our [newsgroup](https://groups.google.com/forum/#!forum/sailsjs) ### Is this an SLA? No. The Sails framework is available under the [MIT license](https://sailsjs.com/license), which does not include a service level agreement. However, the core team and contributors care deeply about Sails, and all of us have websites and APIs running on Sails in production. We will _always_ publish a fix for any serious security vulnerability as soon as possible-- not just out of the kindness of our hearts, but because it could affect our apps (and our customer's apps) too. > For more support options, see https://sailsjs.com/support. ================================================ FILE: docs/tutorials/coffeeScript.md ================================================ # Using CoffeeScript in a Sails app **The recommended language for building Node.js+Sails apps is JavaScript.** But Sails also supports using CoffeeScript to write your custom app code (like [actions](http://www.sailsjs.com/documentation/concepts/actions-and-controllers) and [models](http://www.sailsjs.com/documentation/concepts/core-concepts-table-of-contents/models-and-orm)). You can enable this support in three steps: 1. Run `npm install coffee-script --save` in your app folder. 2. Add the following line at the top of your app's `app.js` file: ```javascript require('coffee-script/register'); ``` 3. Start your app with `node app.js` instead of `sails lift`. ### Using CoffeeScript generators If you want to use CoffeeScript to write your controllers, models or config files, just follow these steps: 1. Install the generators for CoffeeScript (optional): <br/>`npm install --save-dev sails-generate-controller-coffee sails-generate-model-coffee` 2. To generate scaffold code, add `--coffee` when using one of the supported generators from the command-line: ```bash sails generate api <foo> --coffee # Generate api/models/Foo.coffee and api/controllers/FooController.coffee sails generate model <foo> --coffee # Generate api/models/Foo.coffee sails generate controller <foo> --coffee # Generate api/controllers/FooController.coffee ``` <docmeta name="displayName" value="Using CoffeeScript"> ================================================ FILE: docs/tutorials/full-stack-javascript.md ================================================ # Full-stack JavaScript with Sails This video tutorial is an in-depth guide to building your first Node.js/Sails.js app, taught by [the creator of the framework](https://twitter.com/mikermcneil), and following the best practices and conventions our team uses for all our projects. We walk you through setting up your development environment and building our demo app, [Ration](https://ration.io). #### Links + [Take the course](https://platzi.com/cursos/sails-js/) + [Try out the demo app (Ration)](https://ration.io) + [Download the source code](https://github.com/mikermcneil/ration) for the demo app <docmeta name="displayName" value="Full-stack JavaScript with Sails"> ================================================ FILE: docs/tutorials/low-level-mysql-access.md ================================================ # Low-level MySQL usage (advanced) This tutorial steps through how to access the raw MySQL connection instance from the [`mysql` package](https://www.npmjs.com/package/mysql). This is useful for getting access to low level APIs available only in the raw client itself. > Note: Many Node.js / Sails apps using MySQL will never need the kind of low-level usage described here. If you find yourself running up against the limitations of the ORM, there is usually a workaround that does not involve writing code for the underlying database. Even then, if you're just looking to use custom native SQL queries, read no further-- instead, check out [`sendNativeQuery()`](/documentation/reference/waterline-orm/datastores/send-native-query) instead. > > Also, before we proceed, make sure you have a datastore configured to use a functional MySQL database. ### Get access to an active MySQL connection To obtain an active connection from the MySQL package you can call the [`.leaseConnection()`](/documentation/reference/waterline-orm/datastores/lease-connection) method of a registered datastore object (RDI). 1. Get the registered datastore instance for the connection: ```javascript // Get the named datastore var rdi = sails.getDatastore('default'); // Get the datastore configured for a specific model var rdi = Product.getDatastore(); ``` 2. Call the `leaseConnection()` method to obtain an active connection: ```javascript rdi.leaseConnection(function(connection, proceed) { db.query('SELECT * from `user`;', function(err, results, fields) { if (err) { return proceed(err); } proceed(undefined, results); }); }, function(err, results) { // Handle results here after the connection has been closed }) ``` ### Get access to the low-level driver To get access to the low-level driver and MySQL package in a Sails app, you can grab them from the registered datastore object (RDI). 1. Get the registered datastore instance for the connection: ```javascript // Get the named datastore var rdi = sails.getDatastore('default'); // Get the datastore configured for a specific model var rdi = Product.getDatastore(); ``` 2. Get the driver from the datastore instance which contains the MySQL module: ```javascript var mysql = rdi.driver.mysql; ``` 3. You can now use the module to make native requests and call other function native to the MySQL module: ```javascript // Get the named datastore var rdi = sails.getDatastore('default'); // Grab the MySQL module from the datastore instance var mysql = rdi.driver.mysql; // Create a new connection var connection = mysql.createConnection({ host : 'localhost', user : 'root', password : 'password', database: 'example_database' }); // Make a query and pipe the results connection.query('SELECT * FROM posts') .stream({highWaterMark: 5}) .pipe(...); ``` <docmeta name="displayName" value="Low-level MySQL usage (advanced)"> ================================================ FILE: docs/tutorials/mongo.md ================================================ # Using MongoDB with Node.js/Sails.js Sails supports the popular [MongoDB database](https://www.mongodb.com/) via the [sails-mongo adapter](https://www.npmjs.com/package/sails-mongo). > First, make sure you have access to a running MongoDB server, either on your development machine or in the cloud. Below, 'mongodb://root@localhost/foo' refers to a locally-installed MongoDB using "foo" as the database name. Be sure to replace that [connection URL](https://sailsjs.com/documentation/reference/configuration/sails-config-datastores#?the-connection-url) with the appropriate string for your database. ### Developing locally with MongoDB To use MongoDB in your Node.js/Sails app during development: 1. Run `npm install sails-mongo` in your app folder. 2. In your `config/datastores.js` file, edit the `default` datastore configuration: ```js default: { adapter: 'sails-mongo', url: 'mongodb://root@localhost/foo' } ``` 3. In your `config/models.js` file, edit the default `id` attribute to have the appropriate `type` and `columnName` for MongoDB's primary keys: ```js attributes: { id: { type: 'string', columnName: '_id' }, //… } ``` That's it! Lift your app again and you should be good to go. #### Case-sensitivity After configuring your project to use MongoDB, you may notice that your Waterline [queries](https://sailsjs.com/documentation/reference/waterline-orm/queries) are now case-sensitive by default. To do case-insensitive queries, you can use [`.meta({makeLikeModifierCaseInsensitive: true})`](https://sailsjs.com/documentation/reference/waterline-orm/queries/meta). ### Deploying your app with MongoDB To use MongoDB in production, edit your adapter setting in `config/env/production.js`: ```js adapter: 'sails-mongo', ``` You may also configure your [connection URL](https://sailsjs.com/documentation/reference/configuration/sails-config-datastores#?the-connection-url) -- but many developers prefer not to check sensitive credentials into version control. Another option is to use an environment variable: ``` sails_datastores__default__url=mongodb://heroku_12345678:random_password@ds029017.mLab.com:29017/heroku_12345678 ``` > To use MongoDB in your staging environment, edit `config/env/staging.js`. Depending on your application, it may be acceptable to check in your staging database credentials to version control, since they are less of a security risk. ### Low-level MongoDB usage (advanced) As with all of the [Sails database adapters](https://sailsjs.com/documentation/concepts/extending-sails/adapters/available-adapters), you can use any of the [Waterline model methods](https://sailsjs.com/documentation/reference/waterline-orm/models) to interact with your models when using `sails-mongo`. For many apps, that's all you'll need-- from "hello world" to production. Even if you run into limitations, they can usually be worked around without writing Mongo-specific code. However, for situations when there is no alternative, it is possible to use the Mongo driver directly in your Sails app. To access the lower-level “native” MongoDB client directly, use the [`.manager`](https://sailsjs.com/documentation/reference/waterline-orm/datastores/manager) property of the [datastore instance](https://sailsjs.com/documentation/reference/application/sails-get-datastore). As of `sails-mongo` v2.0.0 and above, you can access the [`MongoClient`](https://mongodb.github.io/node-mongodb-native/3.5/api/MongoClient.html) object via `manager.client`. This gives you access to the latest MongoDB improvements, like [`ClientSession`](https://mongodb.github.io/node-mongodb-native/3.5/api/ClientSession.html), and with it, transactions, [change streams](https://mongodb.github.io/node-mongodb-native/3.5/api/ChangeStream.html), and other new features. ```js var mongoClient = Pet.getDatastore().manager.client; var results = await mongoClient.db('test') .collection('pet') .find({}, { name: 1 }) .toArray(); console.log(results); ``` For a full list of methods available in the native MongoDB client, see the [Node.js MongoDB Driver API reference](https://mongodb.github.io/node-mongodb-native/3.5/api/Collection.html). <docmeta name="displayName" value="Using MongoDB"> ================================================ FILE: docs/tutorials/tutorials.md ================================================ # Tutorials Here you can find step-by-step guides for dealing with a few specific Sails use cases. There's a wealth of great information and tutorials about Sails across the web; these are just a few that have been personally vetted by members of the core team. Currently, the objective of this section of the docs is to cover some less common scenarios that users have asked about, but that would add unnecessary complexity to the reference & conceptual documentation if they were included there. In the future, we hope to also address some common _"How do I ...?"_ questions that we hear from newcomers to the framework. If you have a use case you would like to see addressed here, and/or are interested in writing a tutorial yourself, we appreciate your input! Just take a look at our [contribution guide](https://sailsjs.com/documentation/contributing) if you haven't already, then open an issue in the [`sails`](https://github.com/balderdashy/sails/issues/new) repo with a proposal for the tutorial you'd like to see/write. <docmeta name="displayName" value="Tutorials"> <docmeta name="isOverviewPage" value="true"> ================================================ FILE: docs/tutorials/typeScript.md ================================================ # Using TypeScript in a Sails app **The recommended language for building Node.js+Sails apps is JavaScript.** But Sails also supports using TypeScript to write your custom app code (like [actions](http://www.sailsjs.com/documentation/concepts/actions-and-controllers) and [models](https://sailsjs.com/documentation/concepts/models-and-orm)). You can enable this support in just a few steps: 1. Run `npm install typescript ts-node --save` in your app folder. 2. Install the necessary typings for your app. At the very least you'll probably want to: ``` npm install @types/node --save npm install @types/express --save ``` 3. Add the following line at the top of your app's `app.js` file: ```javascript require('ts-node/register'); ``` 4. Start your app with `node app.js` instead of `sails lift`. To get you started, here's an example of a traditional Sails [controller](https://sailsjs.com/documentation/concepts/actions-and-controllers) written in Typescript, courtesy of [@oshatrk](https://github.com/oshatrk): ```typescript // api/controllers/SomeController.ts declare var sails: any; export function hello(req:any, res:any, next: Function):any { res.status(200).send('Hello from Typescript!'); } ``` To try that example out, configure a route so that its target points at `SomeController.hello`, relift, and then visit the route in your browser or with a tool like Postman. <docmeta name="displayName" value="Using TypeScript"> ================================================ FILE: docs/upgrading/To0.10.md ================================================ # Upgrading to Sails v0.10 For the most part, running sails lift in an existing v0.9 project should just work. The core contributors have taken a number of steps to make the upgrade as easy as possible, and if you follow the deprecation messages in the console, you should do just fine. Sails v0.10 comes with some big changes. The sections below provide a high level overview of what's changed, major bug fixes, enhancements and new features, as well as a basic tutorial on how to upgrade your v0.9.x Sails app to v0.10. ## File uploads The Connect multipart middleware [will soon be officially deprecated](http://www.senchalabs.org/connect/multipart.html). But since this module was used as the built-in HTTP body parser in Sails v0.9 and Express v3, this is a breaking change for v0.9 Sails projects relying on `req.files`. By default in v0.10, Sails includes [skipper](https://github.com/balderdashy/skipper), a body parser which allows for streaming file uploads without buffering tmp files to disk. For run-of-the-mill file upload use cases, Skipper comes with bundled support for uploads to local disk (via skipper-disk), but streaming uploads can be plugged in to any of its supported adapters. For examples/documentation, please see the Skipper repository as well as the Sails documentation on `req.file()`. ### Why? A body parser's job is to parse the "body" of incoming multipart HTTP requests. Sometimes, that "body" includes text parameters, but sometimes, it includes file uploads. Connect multipart is great code, and it supports both file uploads AND text parameters in multipart requests. But like most modules of its kind, it accomplishes this by buffering file uploads to disk. This can quickly overwhelm a server's available disk space, and in many cases exposes a serious DoS attack vulnerability. Skipper is unique in that it supports **streaming** file uploads, but also maintains support for metadata in the request body (i.e. JSON/XML/urlencoded request body parameters). It uses a handful of heuristics to make sure only the files you're expecting get plugged in and received by the blob adapter, and other (potentially malicous) file fields are ignored. > #### ** Important!** > For Skipper to work, you _must include all text parameters BEFORE file parameters_ in file upload requests to the server. Once Skipper sees the first file field, it stops waiting for text parameters (this is to avoid unnecessary/unsafe buffering of file data). ### Configuring a different body parser As with most things in Sails, you can use any Connect/Express/Sails-compatible bodyparser you like. To switch back to **connect-multipart**, or any other body parser (like **formidable** or **busboy**), change your app's http configuration. ## Blueprints A new blueprint action (`findOne`) has been added. For instance, if you have a `FooController` and `Foo` model, then send a request to `/foo/5`, the `findOne` action in your `FooController` will run. If you don't have a `findOne` action, the `findOne` blueprint action will be used in its stead. Requests sent to `/foo` will still run the find controller/blueprint action. ## Policies Policies work exactly as they did in v0.9- however there is a new consideration you should take into account: Due to the introduction of the more specific `findOne()` blueprint action mentioned above, you will want to make sure you're handling it explicitly in your policy mapping configuration. For example, let's say you have a v0.9 app whose `policies.js` configuration prevents access to the `find` action in your `DoveController`: ``` module.exports.policies = { '*': true, DoveController: { find: false } }; ``` Assuming rest blueprint routes are enabled, this would prevent access to requests like both `/dove` and `/dove/14`. But now in v0.10, since `/dove/14` will actually run the `findOne` action, we must handle it explicitly: ``` module.exports.policies = { '*': true, DoveController: { find: false, findOne: false } }; ``` ## Pubsub ### Summary + `message` socket (i.e. "comment") event on client is now `modelIdentity` (where "modelIdentity" is different depending on the model that the `publish*()` method was called from. + Clients are no longer subscribed to model-creation events by the blueprint routes. To listen for creation events, use `Model.watch()`. + The events that were formerly `create`, `update`, and `destroy` are now `created`, `updated`, and `destroyed`. ### Details The biggest change to pubsub is that Socket.io events are emitted under the name of the model emitting them. Previously, your client listened for the `message` event and then had to determine which model it came from based on the included data: ``` socket.on('message', function(cometEvent) { if (cometEvent.model == 'user') { // Handle inbound messages related to a user record } else if (cometEvent.model === 'product') { // Handle inbound messages related to a product record } // ... } ``` Now, you subscribe to the identity of the model: ``` socket.on('user', function(cometEvent) { // Handle inbound messages related to a user record }); socket.on('product', function (cometEvent) { // Handle inbound messages related to a product record }); ``` This helps to structure your front end code. The way you subscribe clients to models has also changed. Previously, you specified whether you were subscribing to the model class (class room) or one or more model instances based on the parameters that you passed to `Model.subscribe`. It was effectively one method to do two very different things. Now, you use `Model.subscribe()` to subscribe only to model instances (records). You can also specify event "contexts", or types, that you'd like to hear about. For example, if you only wanted to get messages about updates to an instance, you would call `User.subscribe(req, myUser, 'update')`. If no context is given in a call to `.subscribe()`, then all contexts specified by the model class's autosubscribe property will be used. To subscribe to model creation events, you can now use `Model.watch()`. Upon subscription, your clients will receive messages every time a new record is created on that model using the blueprint routes, and will automatically be subscribed to the new instance as well. Remember, when working with blueprints, clients are no longer auto subscribed to the class room. This must be done manually. Finally, if you want to see all pubsub messages from all models, you can access the `firehose`, a development-only tool that broadcasts messages about _everything_ that happens to your models. You can subscribe to the firehose using `sails.sockets.subscribeToFirehose(socket)`, or on the front end by making a socket request to `/firehose`. The firehose will broadcast a `firehose` event whenever a model is created, updated, destroyed, added to, removed from or messaged. This effectively replaces the `message` event used in previous Sails versions. To see examples of the new pubsub methods in action, see [SailsChat](https://github.com/balderdashy/sailschat). ## Arguments to lifecycle callbacks are now typecasted Previously, with `schema: true`, if you sent an attribute value to a `.create()` or `.update()` that did not match the expected type declared in the model's attributes, the value you passed in would still be accessible in your model's lifecycle callbacks. In Sails/Waterline v0.10, this is no longer the case. Values passed to `.create()` and `.update()` are type-casted before your lifecycle callbacks run. Affected lifecycle callbacks include `beforeUpdate()`, `beforeCreate()`, and `beforeValidate()`. ## beforeValidation() is now beforeValidate() If you were using the `beforeValidation` or `afterValidation` model lifecycle callbacks in any of your models, you should change them to `beforeValidate` or `afterValidate`. This change was made in Waterline to match the style of the other lifecycle callbacks (e.g. `beforeCreate`, `afterUpdate`, etc.). ## .done() vs. .exec() ** The old (/confusing?) meaning of `.done()` has been deprecated.** In Sails <= v0.8, the syntax for executing an ORM query was `Model. [ … ] .done( cb )`. In v0.9, when promise support was added, the `Model. [ … ] .exec( cb )` became the recommended replacement, since `.done()` has a special meaning in the promise spec. However, the original usage of `.done()` was left untouched to make upgrading from v0.8 to v0.9 easier. But as of Sails/Waterline v0.10, the original meaning of `.done()` has been officially deprecated to allow for a more robust promise implementation going forward, and pluggable promise library support (e.g. choose `Q` or `Bluebird` etc.). ## Associations Sails v0.10 introduces associations between data models. Since the work we've done on associations is largely additive, your existing models should still just work. That said, this is a powerful new feature that allows you to write less code and makes your app more maintainable, so we suggest taking advantage of it! To learn about how to use associations in Sails, check out the docs. Associations (or "relations") are really just special attributes. Instead of string or integer values, you can specify an instance of a model or a collection of model instances. You can think about this kind of like an object (`{...}`) or an array (`[{...}, {...}]`) you might store as JSON in a NoSQL database. The difference is, in Sails, this works with any of the supported databases, and even allows you to populate (i.e. join) across different databases and types of databases. ## Generators Sails has had support for generating code for a while now (e.g. `sails generate controller foo`) but in v0.10, we wanted to make this feature more extensible, open, and accessible to everybody in the Sails community. With that in mind, v0.10 comes with a complete rewrite of the command-line tool, and pluggable generators. Want to be able to run `sails generate blog foo` to make a new blog built on Sails? Create a `blog` generator (run sails `generate generator blog`), add your templates, and configure the generator to copy the new templates over. Then you can release it to the community by publishing an npm module called `sails-generate-blog`. Compatibility with Yeoman generators is also in our roadmap. ## Command-line tool The big change here is how you create a new api. In the past you called `sails generate new_api`. This would generate a new controller and model called `new_api` in the appropriate places. This is now done using `sails generate api new_api`. You can still generate models and controllers seperately using the same CLI Commands. Also, `--linker` switch is no longer available. In previous version, if `--linker` switch was provided, it created a `myApp/assets/linker folder`, with `js`, `styles` and `templates` folders inside. In this new version, the `myApp/assets/linker` folder is not created. Compiling CoffeeScript and Less is the default behavior now, right from the `myApp/assets/js` and `myApp/assets/scripts` folders. ## Custom server responses In v0.10, you can now generate your own custom server responses. Like before, there are a few that we automatically create for you. Instead of generating `myApp/config/500.js` and other `.js` responses in the config directory, they are now generated in `myApp/api/responses/`. To migrate, you will need to create a new v0.10 project and copy the `myApp/api/responses` directory into your existing app. You will then modify the appropriate .js file to reflect any customization you made in your response logic files (500.js,etc). ## Legacy data stored in the temporary sails-disk database `sails-disk`, used by default in new Sails projects, now stores data a bit differently. If you have some temporary data stored in a 0.9.x project, you'll want to wipe it out and start fresh. To do this: From your project's root directory: ``` $ rm .tmp/disk.db ``` ## Adapter/Database Configuration `config.adapters` (in `myApp/config/adapters.js`) is now config.connections (in new projects, this is generated in `myApp/config/connections.js`). Also, `config.model` is now `config.models`. Your app's default `connection` (i.e. database) should now be configured as a string `config.models.connection` used by default for model. New projects are generated with a `/config/models.js` file that includes the default connection. To configure a model to use specific adapters, you must now specify them in the `connection` key instead of `adapters`. For example: ``` module.exports = { connection: ['someMongoDatabase'], attributes: { name:{ type : 'string', required : true } } }; ``` ## Blueprints/Controller configuration The object literal describing controller configuration overrides for controller blueprints should change from: ``` ... _config: { blueprints: { rest: true, ... } } ``` to: ``` ... _config: { rest: true, ... } ``` ## Layout paths: In Sails v0.9, you could use the following syntax to specify `auth/someLayout.ejs` as a custom layout when rendering a view: ``` return res.view('auth/login',{ layout: 'someLayout' }); ``` However in Sails v0.10, all layout paths are relative to your app's views path. In other words, the relative path of the layout is no longer resolved from the view's own path-- it is now always resolved from the views path. This makes it easier to understand which file is being used, particularly when layout files have similar names: ``` return res.view('auth/login', { layout: 'auth/someLayout' }); ``` <docmeta name="displayName" value="To v.0.10"> <docmeta name="version" value="0.10.0"> ================================================ FILE: docs/upgrading/To0.11.md ================================================ # Upgrading to Sails v0.11 **tldr;** v0.11 comes with many minor improvements, as well as some internal cleanup in core. The biggest change is that Sails core is now using Socket.io v1. Almost none of this should affect the existing code in project, but there are a few important differences and new features to be aware of. We've listed them below. ## Differences #### Upgrade the Socket.io / Sails.io browser client Old v0.9 socket.io client will no longer work, so consequently you'll need to upgrade your sails.io.js client from v0.9 or v0.10 to v0.11. To do this, just remove your sails.io.js client and install the new one. We've bundled a new generator that will do this for you, assuming your sails.io.js client is in the conventional location at `assets/js/dependencies/sails.io.js` (i.e. if you haven't moved or renamed it): ```sh sails generate sails.io.js --force ``` #### `onConnect` lifecycle callback > **tldr;** > > Remove your `onConnect` function from `config/sockets.js`. The `onConnect` lifecycle callback has been deprecated. Instead, if you need to do something when a new socket is connected, send a request from the newly-connected client to do so. The purpose of `onConnect` was always for optimizing performance (eliminating the need to do this initial extra round-trip with the server), yet its use can lead to confusion and race conditions. If you desperately need to eliminate the server roundtrip, you can bind a handler directly on `sails.io.on('connect', function (newlyConnectedSocket){})` in your bootstrap function (`config/bootstrap.js`). However, note that this is discouraged. Unless you're facing _true_ production performance issues, you should use the strategy mentioned above for your "on connection" logic (i.e. send an initial request from the client after the socket connects). Socket requests are lightweight, so this doesn't add any tangible overhead to your application, and it will help make your code more predictable. #### `onDisconnect` lifecycle callback The `onDisconnect` lifecycle callback has been deprecated in favor of `afterDisconnect`. If you were using `onDisconnect` previously, you might have had to change the `session`, then call `session.save()` manually. In v0.11, this works in almost exactly the same way, except that `afterDisconnect` receives an additional 3rd argument: a callback function. This way, you can just call the provided callback when your `afterDisconnect` logic has finished, so that Sails can persist any changes you've made to the session automatically. Finally, as you might expect, you won't need to call `session.save()` manually anymore- it is now taken care of for you (just like `req.session` in a normal route, action, or policy.) > **tldr;** > Rename your `onDisconnect` function in `config/sockets.js` with the following: > ``` afterDisconnect: function (session, socket, cb) { // Be sure to call the callback return cb(); } ``` #### Other configuration in `config/sockets.js` Many of the configuration options in Socket.io v1 have changed, so you'll want to update your `config/sockets.js` file accordingly. + if you haven’t customized any of the options in `config/sockets.js` for your app, you can safely remove or comment out the entire file and let the Sails defaults do their magic. Otherwise, refer to the new [Sails sockets documentation](https://sailsjs.com/documentation/reference/configuration/sails-config-sockets) to ensure that your configuration is still valid and avoid unwanted hair loss. + if you are scaling to multiple servers in an environment that does *not support sticky sessions* (this includes Heroku), you'll need to set your `transports` to `['websocket']` in both `config/socket.js` and your client--see [our Scaling doc](https://sailsjs.com/documentation/concepts/deployment/scaling#?preparing-your-app-for-a-clustered-deployment) for more info. + if you were using a custom `authorization` function to restrict socket connections, you'll now want to use `beforeConnect`. `authorization` was deprecated by Socket.io v1, but `beforeConnect` (which maps to the `allowRequest` option from Engine.io) works just the same way. + if you were using other low-level socket configuration that was passed directly to socket.io v1, be sure and check out the [reference page on sailsjs.com](https://sailsjs.com/documentation/reference/configuration/sails-config-sockets) where all of the new configuration options are covered in detail. #### The "firehose" The "firehose" feature for testing with sockets has been deprecated. If you don't know what that means, you have nothing to worry about. The basic usage will continue to work for a while, but it will soon be removed from core and should not be relied upon in your app. This also applies to the following methods: + sails.sockets.subscribeToFirehose() + sails.sockets.unsubscribeFromFirehose() + sails.sockets.drink() + sails.sockets.spit() + sails.sockets.squirt() > If you want the "firehose" back, let [Mike know on twitter](http://twitter.com/mikermcneil) (it can be brought back as a separate hook). #### Config files in subfolders It has always been the intention that files in the Sails `config` folder have no precedence over each other, and that the filenames and subfolders (with the exception of `local.js` and the `env` and `locale` subfolders) be used merely for organization. However, in previous Sails versions, saving config files in subfolders would have the effect that the filename would be added as a key in `sails.config`, so that if you saved some config in `config/foo/bar.js`, then that config would be namespaced under `sails.config.bar`. This was unintentional and potentially confusing as 1) the directory name is ignored, and 2) moving the file would change the config key. This has been fixed in v0.11.x: config files in subfolders will be treated the same as those in the root `config` folder. If you are for some reason relying on the old behavior, you may set `dontFlattenConfig` to `true` in your `.sailsrc` file, but we would strongly recommend that you instead just namespace the config yourself by setting the desired key on `module.exports`; for example `module.exports.foo = {...}`. See [issue #2544](https://github.com/balderdashy/sails/issues/2544) for more details. #### Waterline now uses Bluebird As of v0.11, Waterline now supports Bluebird (instead of q) for promises. If you are using `.exec()` you won't be affected-- only if you are using `.then()`. See https://github.com/balderdashy/sails/issues/1186 for more information. ## New features Sails v0.11 also comes with some new stuff that we thought you'd like to know about: #### User-level hooks Hooks can now be installed directly from NPM. This means you can now install hooks with a single command in your terminal. For instance, consider the [`autoreload` hook](https://github.com/sgress454/sails-hook-autoreload) by [@sgress454](https://twitter.com/sgress454), which watches for changes to your backend code so you don't need to kill and re-lift the server every time you change your controllers, routes, models, etc. To install the `autoreload` hook, run: ```sh npm install sails-hook-autoreload ``` This is just one example of what's possible. As you might already know, hooks are the lowest-level pluggable abstraction in Sails. They allow authors to tap into the lift process, listen for events, inject custom "shadow" routes, and, in general, take advantage of raw access to the `sails` runtime. Most of the features you're familiar with in Sails have actually already been implemented as "core" hooks for over a year, including: + `blueprints` _(which provides the blueprint API)_ + `sockets` _(which provides socket.io integration)_ + `grunt` _(which provides Grunt integration)_ + `orm` _(which provides integration with the Waterline ORM, and imports your projects adapters, models, etc.)_ + `http` _(which provides an HTTP server)_ + and 16 others. You can read more about how to write your own hooks in the [new and improved "Extending Sails" documentation](https://sailsjs.com/documentation/concepts/extending-sails) on https://sailsjs.com. #### Socket.io v1.x The upgrade to Socket.io v1.0 shouldn't actually affect your app-level code, provided you are using the layer of abstraction provided by Sails itself; everything from the `sails.sockets.*` wrapper methods and "up" (resourceful pubsub, blueprints) If you are using underlying socket.io methods in your apps, or are just curious about what changed in Socket.io v1.0, be sure and check out the [complete Socket.io 1.0 migration guide](http://socket.io/docs/migrating-from-0-9/) from Guillermo and the socket.io team. #### Ever-increasing modularity As part of the upgrade to Socket.io v1.0, we pulled out the core `sockets` hook into a separate repository. This allowed us to write some modular, hook-specific tests for the socket.io interpreter, which will make things easier to maintain, customize, and override. This also allows the hook to grow at its own pace, and puts related issues in one place. Consider this a test of the pros and cons of pulling other hooks out of the sails core repo over the next few months. This will make Sails core lighter, faster, and more extensible, with fewer core dependencies, shorter "lift" time for most apps, and faster `npm install`s. #### Testing, the "virtual" request interpreter, and the `sails.request()` method In the process of pulling the `sockets` hook _out_ of core, the logic which interprets requests has been normalized and is now located _in_ Sails core. As a result, the `sails.request()` method is much more powerful. This method allows you to communicate directly with the request interpreter in Sails without lifting your server onto a port. It's the same mechanism that Sails uses to map incoming messages from Socket.io to "virtual requests" that have the familiar `req` and `res` streams. The primary use case for `sails.request()` is in writing faster-running unit and integration tests, but it's also handy for proxying to mounted apps (or "sub-apps"). For instance, here is an example (using mocha) of how you might test one of your app's routes: ```js var assert = require('assert'); var Sails = require('sails').Sails; before(function beforeRunningAnyTests (done){ // Load the app (no need to "lift" to a port) sails.load({ log: { level: 'warn' }, hooks: { grunt: false } }, function whenAppIsReady(err){ if (err) return done(err); // At this point, the `sails` global is exposed, although we // could have disabled it above with our config overrides to // `sails.load()`. In fact, you can actually use this technique // to set any configuration setting you like. return done(); }); }); after(function afterTestsFinish (done) { sails.lower(done); }); describe('GET /hotpockets', function (){ it('should respond with a 200 status code', function (done){ sails.request({ method: 'get', url: '/hotpockets', params: { limit: 10, sort: 'price ASC' } }, function (err, clientRes, body) { if (err) return done(err); assert.equal(clientRes.statusCode, 200); return done(); }); }); }); ``` #### `config/env/` subfolders In v0.10.x, we added the `config/env` folder (thanks to [@clarkorz](https://github.com/clarkorz)), where you can add config files that will be loaded only in the appropriate environment (e.g. `config/env/production.js` for production environment, `config/env/development` for development, etc.). In v0.11.x we've added the ability to specify whole subfolders per-environment. For example, *all* config files saved to the `config/env/production` will be loaded and merged on top of other configuration when the environment is set to `production`. Note that if both a `config/env/production` folder and a `config/env/production.js` file are present, the `config/env/production.js` settings will take precedence. And, as always, `local.js` is merged on top of all other files, and `.sailsrc` rules them all. ## Questions? As always, if you run into issues upgrading, or if any of the notes above don't make sense, let us know and we'll do what we can to clarify. Finally, to those of you that have contributed to the project since the v0.10 release in August: we can't stress enough how much we value your continued support and encouragement. There is a pretty massive stream of issues, pull requests, documentation tweaks, and questions, but it always helps to know that we're in this together :) Thanks. -[@mikermcneil](https://github.com/mikermcneil/), [@sgress454](https://github.com/sgress454/) and [@particlebanana](https://github.com/particlebanana/) <docmeta name="displayName" value="To v0.11"> <docmeta name="version" value="0.11.0"> ================================================ FILE: docs/upgrading/To0.12.md ================================================ # Upgrading to Sails v0.12 Sails v0.12 comes with an upgrade to Socket.io and Express, as well as many bug fixes and performance enhancements. While you should find that this version is mostly backwards compatible with Sails v0.11, there are some major changes to `sails.sockets.*` methods which may affect your app. Those changes are addressed in the migration guide below, so if you are upgrading an existing app from v0.11 and are using `sails.sockets` methods, please be sure and carefully read the information below. Aside from those changes, running `sails lift` in an existing project should just work. The sections below provide a high-level overview of what's changed, major bug fixes, enhancements and new features, as well as a basic tutorial on how to upgrade your v0.11.x Sails app to v0.12. ## Installing the update Run the following command from the root of your Sails app: ```bash npm install sails@~0.12.0 --force --save ``` The `--force` flag will override the existing Sails dependency installed in your `node_modules/` folder with the latest patch release of Sails v0.12, and the `--save` flag will update your package.json file so that future npm installs will also use the new version. ## Things to do immediately after upgrading + If your app uses the `socket.io-redis` adapter, upgrade to at least version 1.0.0 (`npm install --save socket.io-redis@^1.0.0`). + If your app is using the Sails socket client (e.g. `assets/js/dependencies/sails.io.js`) on the front end, also install the newest version (`sails generate sails.io.js --force`). ## Overview of changes in v0.12 > For a full list of changes, see the changelog file for [Sails](https://github.com/balderdashy/sails/blob/master/CHANGELOG.md), as well as those for [Waterline](https://github.com/balderdashy/waterline/blob/master/CHANGELOG.md), [sails-hook-sockets](https://github.com/balderdashy/sails-hook-sockets/blob/master/CHANGELOG.md) and [sails.io.js](https://github.com/balderdashy/sails.io.js/blob/master/CHANGELOG.md). + Security enhancements: updated several dependencies with potential vulnerabilities. + Reverse routing functionality is now built into Sails core via the new [`sails.getRouteFor()`](https://sailsjs.com/documentation/reference/application/sails-get-route-for) and [`sails.getUrlFor()`](https://sailsjs.com/documentation/reference/application/sails-get-url-for) methods. + Generally improved multi-node support (and therefore scalability) of low-level `sails.socket.*` methods, and made additional adjustments and improvements related to the latest socket.io upgrade. Added a much tighter Redis integration that sits on top of `socket.io-redis`, using a Redis client to implement cross-server communication rather than an additional socket client. + Cleaned up the API for `sails.socket.*` methods, normalizing overloaded functions and deprecating methods which cause problems in multiserver deployments (more on that below). + Added a few brand new sails.sockets methods: `.leaveAll()`, `.addRoomMembersToRooms()`, and `.removeRoomMembersFromRooms()`. + `sails.sockets.id()` is now `sails.sockets.getId()` (backwards compatible with deprecation message). + New Sails apps are now generated with the updated version of `sails.io.js` (the JavaScript Sails socket client). This upgrade bundles the latest version of `socket.io-client`, as well as some more advanced functionality (including the ability to specify common headers for all virtual socket requests). + Upgraded to latest trusted versions of `grunt-contrib-*` dependencies (eliminates many NPM deprecation warnings and provides better error messages from NPM). + If you are using NPM v3, running `sails new` will now run `npm install` instead of symlinking your new app's initial dependencies. This is slower than you may be used to, but it is a necessary change due to changes in the way NPM handles nested dependencies. The core maintainers are [working on](https://github.com/npm/npm/issues/10013#issuecomment-178238596) a better long-term solution, but in the meantime if you frequently run `sails new` and the slowdown is bugging you, consider temporarily downgrading to an earlier version of NPM (v2.x). If the installed version of NPM is prior to version 3, Sails will continue to take advantage of the classic symlinking strategy. ## Socket Methods Without question, the biggest change in Sails v0.12 is to the API of the low-level `sails.sockets` methods exposed by the `sockets` hook. In order to ensure that Sails apps perform flawlessly in a [multi-server (aka "multi-node" or "clustered") environment](https://sailsjs.com/documentation/concepts/realtime/multi-server-environments), several [low-level methods](https://sailsjs.com/documentation/reference/web-sockets/sails-sockets) have been deprecated and some new ones have been added. The following `sails.sockets` methods have been deprecated: + [`.emit()`](https://0.12.sailsjs.com/documentation/reference/web-sockets/sails-sockets/sails-sockets-emit) + [`.id()`](https://0.12.sailsjs.com/documentation/reference/web-sockets/sails-sockets/sails-sockets-id) (renamed to [`.getId()`](https://sailsjs.com/documentation/reference/web-sockets/sails-sockets/get-id)) + [`.socketRooms()`](https://0.12.sailsjs.com/documentation/reference/web-sockets/sails-sockets/sails-sockets-socket-rooms) + [`.rooms()`](https://0.12.sailsjs.com/documentation/reference/web-sockets/sails-sockets/sails-sockets-rooms) + [`.subscribers()`](https://0.12.sailsjs.com/documentation/reference/web-sockets/sails-sockets/sails-sockets-subscribers) If you are using any of those methods in your app, they will still work in v0.12 but _you should replace them as soon as possible_ as they may be removed from Sails in the next version. See the individual doc pages for each method for more information. ## Resourceful PubSub Methods The [`.subscribers()`](https://sailsjs.com/documentation/reference/web-sockets/resourceful-pub-sub/subscribers) resourceful PubSub method has been deprecated for the same reasons as [`sails.sockets.subscribers()`](https://sailsjs.com/documentation/reference/web-sockets/sails-sockets/sails-sockets-subscribers). Follow the guidelines in the docs for replacing this method if you are using it in your code. ## Waterline (ORM) Updates Sails v0.12 comes with the latest version of the Waterline ORM (v0.11.0). There are two API changes to be aware of: ##### `.save()` no longer provides a second argument to its callback The callback to the `.save()` instance method no longer receives a second argument. While requiring the second argument was convenient, it made `.save()` less performant, especially for apps working with millions of records. This change resolves those issues by eliminating the need to build redundant queries, and preventing your database from having to process them. If there are places in your app where you have code like this: ```javascript sierra.save(function (err, modifiedSierra){ if (err) { /* ... */ return; } // ... }); ``` You should replace it with: ```javascript sierra.save(function (err){ if (err) { /* ... */ return; } // ... }); ``` ##### Custom column/field names for built-in timestamps You can now configure a custom column name (i.e. field name, for Mongo/Redis folks) for the built-in `createdAt` and `updatedAt` attributes. In the past, the top-level `autoCreatedAt` and `autoUpdatedAt` model settings could be specified as `false` to disable the automatic injection of `createdAt` and `updatedAt` altogether. That _still works as it always has_, but now you can also specify string values for one or both of these settings instead. If a string is specified, it will be understood as the custom column (/field) name to use for the automatic timestamp. ```javascript { attributes: {}, autoCreatedAt: 'my_cool_created_when_timestamp', autoUpdatedAt: 'my_cool_updated_at_timestamp' } ``` If you were using the [workaround suggested by @sgress454 here](http://stackoverflow.com/a/24562385/486547), you may want to take advantage of this simpler approach instead. ## SQL Adapter Performance [Sails-PostgreSQL](https://github.com/balderdashy/sails-postgresql) and [Sails-MySQL](https://github.com/balderdashy/sails-mysql) recieved patch updates that significantly improved performance when populating associations. Thanks to [@jianpingw](https://github.com/jianpingw) for digging into the source and finding a bug that was processing database records too many times. If you are using either of these adapters, upgrading to `sails-postgresql@0.11.1` or `sails-mysql@0.11.3` will give you a significant performance boost. ## Contributing While not technically part of the release, Sails v0.12 is accompanied by some major improvements to the tools and resources available to contributors. More core hooks are now fully documented ([controllers](https://github.com/balderdashy/sails/tree/master/lib/hooks/controllers)|[grunt](https://github.com/balderdashy/sails/tree/master/lib/hooks/grunt)|[logger](https://github.com/balderdashy/sails/tree/master/lib/hooks/logger)|[cors](https://github.com/balderdashy/sails/tree/master/lib/hooks/cors)|[responses](https://github.com/balderdashy/sails/tree/master/lib/hooks/responses)|[orm](https://github.com/balderdashy/sails/tree/master/lib/hooks/orm)), and the team has put together a [Code of Conduct](https://github.com/balderdashy/sails/blob/master/CODE-OF-CONDUCT.md) for contributing to the Sails project. The biggest change for contributors is the [updated contribution guide](https://github.com/balderdashy/sails/blob/master/CONTRIBUTING.md), which contains the new, streamlined process for feature/enhancement proposals and for merging features, enhancements, and patches into core. As the Sails framework has grown (both the code base and the user base), it's become necessary to establish clearer processes for how issue contributions, code contributions, and contributions to the documentation are reviewed and merged. ## Documentation This release also comes with a deep clean of the official reference documentation, and some minor usability improvements to the online docs at [https://sailsjs.com/documentation](https://sailsjs.com/documentation). The entire Sails website is now available in [Japanese](http://sailsjs.jp/), and four other [translation projects](https://github.com/balderdashy/sails/tree/master/docs#in-other-languages) are underway for Korean, Brazillian Portugese, Taiwanese Mandarin, and Spanish. In addition, the Sails.js project (finally) has an [official blog](http://blog.sailsjs.com). The Sails.js blog is the new source for all longform updates and announcements about Sails, as well as for our related projects like Waterline, Skipper, and the machine specification. ## Need Help? If you run into an unexpected issue upgrading your Sails app to v0.12.0, please review our contribution guide and [submit an issue in the Sails GitHub repo](https://github.com/balderdashy/sails/blob/master/CONTRIBUTING.md). <docmeta name="displayName" value="To v0.12"> <docmeta name="version" value="0.12.0"> ================================================ FILE: docs/upgrading/To1.0.md ================================================ # Upgrading to Sails v1.0 Sails v1.0 is here! Keep reading for a high-level overview of what's changed in this release, and to learn about some new features you might want to take advantage of in your app. ### A note about breaking changes While working on this version of Sails, a lot of the decisions we made favored a better developer experience over backwards compatibility. Because of this, the upgrade to Sails 1.0 will involve dealing with more breaking changes than previous versions. But when you're finished, there'll be a much better chance that the features you're using in Sails are things that its author and maintainers understand thoroughly and use almost every day. For more about the philosophy behind many of the breaking changes in 1.0, you can read Mike McNeil's in-depth explanation [here](https://gitter.im/balderdashy/sails?at=5a1d8fcd3a80a84b5b907099). ### Upgrading an existing app using the automated tool Ready to upgrade your existing v0.12.x Sails app to version 1.0? To get started, we recommend using the Sails 1.0 upgrade tool, which will help with some of the most common migration tasks. To use the tool, first install Sails 1.0 globally with `npm install -g sails@^1.0.0` and then run `sails upgrade`. After the tool runs, it will create a report for you with a list of remaining items that need to be manually upgraded. ### Upgrading an existing app manually The checklist below covers the changes most likely to affect the majority of apps. If your app still has errors or warnings on startup after following this checklist, or if you're seeing something unexpected, head back to this document and take a look further down the page. (One of the guides for covering various app components will probably be applicable.) > We've done a lot of work to make the upgrade process as seamless as possible, particularly when it comes to the errors and warnings you'll see on the console. But if you're stumped or have lingering questions about any of the changes below, feel free to [drop by the Sails community Gitter channel](https://sailsjs.com/support). (If your company is using Sails Flagship, you can also chat directly with the Sails core team [here](https://flagship.sailsjs.com/ask).) ### tl;dr checklist: things you simply _must_ do when upgrading to version 1.0 The upgrade tool does its best to help with some of these items, but it won’t change your app-specific code for you! + **Step 0**: Check your Node version + **Step 1**: Install hooks & update dependencies + **Step 2**: Update configuration + **Step 3**: Modify client-side code for the new blueprint API + **Step 4**: Adopt the new release of Waterline ORM ##### Step 0: Check your Node version! If your app needs to support Node versions earlier than v4, you will not be able to upgrade to Sails 1.0, as Sails 1.0 no longer supports Node v0.x. The earliest version of Node supported by Sails 1.0 is Node 4.x. ##### Step 1: Install hooks & update dependencies Sails v1 introduces [custom builds](https://github.com/balderdashy/sails/pull/3504). This means that certain core hooks are now installed as direct dependencies of your app, giving you more control over your dependencies and making `npm install sails` run _considerably_ faster. So, the first thing you'll need to do is install the core hooks you're using. (And while you're at it, be sure to update the other dependencies mentioned in the list below.) * **Install the `sails-hook-orm` package** into your app with `npm install --save sails-hook-orm`, unless your app has the ORM hook disabled. * **Install the `sails-hook-sockets` package** into your app with `npm install --save sails-hook-sockets`, unless your app has the sockets hook disabled. * **Install the `sails-hook-grunt` package** into your app with `npm install --save sails-hook-grunt`, unless your app has the Grunt hook disabled. * **Install the latest version of your database adapter**. For example, if you're using `sails-mysql`, do `npm install --save sails-mysql@latest`. * **Upgrade your `sails.io.js` websocket client** with `sails generate sails.io.js`. See the ["Websockets" section below](https://sailsjs.com/documentation/upgrading/to-v-1-0/#?websockets) for more details. ##### Step 2: Update configuration Sails v1 comes with several improvements in app configuration. For example, automatic install of lodash and async can now be customized to any version, and view engine configuration syntax is now consistent with that of Express v4+. The most significant change to configuration, however, is related to one of the most exciting new features in Sails v1: [datastores](https://sailsjs.com/documentation/reference/waterline-orm/datastores). To make sure you correctly upgrade the configuration for your database(s) and other settings, be sure to carefully read through the steps below and apply the necessary changes. * **Update your `config/globals.js` file** (unless your app has `sails.config.globals` set to `false`) + Set `models` and `sails` to have boolean values (`true` or `false`). + Set `async` and `lodash` to either have `require('async')` and `require('lodash')` respectively, or else `false`. You may need to `npm install --save lodash` and `npm install --save async`, as well. * **Comment out any database configuration your aren’t using** in `config/connections.js`. Unlike previous versions, Sails 1.0 will load _all_ database adapters that are referenced in config files, regardless of whether they are actually used by a model. See the [migration guide section on database configuration](https://sailsjs.com/documentation/upgrading/to-v-1-0/#?changes-to-database-configuration) for more info. * **The `/csrfToken` route** is no longer provided to all apps by default when using CSRF. If you're utilizing this route in your app, you'll need to manually add it to `config/routes.js` as `'GET /csrfToken': { action: 'security/grant-csrf-token' }`. * **If your app relies on [action shadow routes](https://sailsjs.com/documentation/concepts/blueprints/blueprint-routes#?action-routes)** (where every custom controller action is automatically mapped to a route), you’ll need to update your `config/blueprints.js` file and set `actions` to `true`. This setting is now `false` by default. * **If your app uses CoffeeScript or TypeScript** see the [CoffeeScript](https://sailsjs.com/documentation/tutorials/using-coffee-script) and [TypeScript](https://sailsjs.com/documentation/tutorials/using-type-script) tutorials for update information. * **If your app uses a view engine other than EJS**, you’ll need to configure it yourself in the `config/views.js` file, and you'll likely need to run `npm install --save consolidate` for your project. See the "Views" section below for more details. * **If your `api` or `config` folders and subfolders contain any non-source files**, they’ll need to be moved. The exception is for Markdown (.md) and text (.txt) files, which will continue to be ignored. Sails will attempt to read all other files in those folders as code, allowing for more flexibility in choosing between Javascript dialects (see the notes about CoffeeScript and TypeScript above). ##### Step 3: Modify client-side code for the new blueprint API As well as having been expanded to include a new endpoint, there also are a couple of minor—but breaking—changes to the blueprint API that may require you to make changes to your client-side code. * **If your app uses blueprint routes**, be aware that a couple of implicit "shadow" routes have had their HTTP method (aka verb) changed: + the RESTful blueprint route address for [**add**](https://sailsjs.com/documentation/reference/blueprint-api/add-to) has changed from `POST` to `PUT`. + the RESTful blueprint route address for [**update**](https://sailsjs.com/documentation/reference/blueprint-api/update) has changed from `PUT` to `PATCH`. * **If your app relies on the default socket notifications from blueprint actions**, be aware that there have been some performance-related upgrades that change the structure of these messages somewhat: + Sails no longer publishes separate `addedTo` notifications, one for each new member of a collection. Those individual notifications are now rolled up into a single notification, and the new message contains an array of ids (`addedIds`) instead of just one. + Sails no longer publishes separate `removedFrom` notifications, one for each former member of a collection. Sails now rolls those up into a single notification, and the new message now contains an array of ids (`removedIds`) instead of just one. ##### Step 4: Adopt the new release of Waterline ORM The new release of Waterline ORM (v0.13) introduces full support for SQL transactions, the ability to include or omit attributes in result sets (aka "projections"), dynamic database connections, and more extensive granular control over query behavior. It also includes a major stability and performance overhaul, which comes with a few breaking changes to usage. The bullet points below cover the most common issues you're likely to run into with the Waterline upgrade. * **If your app relies on getting records back from `.create()`, `.createEach()`, `.update()`, or `.destroy()` calls**, you’ll need to update your model settings to indicate that you want those methods to fetch records (or chain a `.fetch()` to individual calls). See the [migration guide section on `create()`, `.createEach()`, `.update()`, and `.destroy()` results](https://sailsjs.com/documentation/upgrading/to-v-1-0/#?changes-to-create-createeach-update-and-destroy-results) for more info. * **If your app relies on using the `.add()`, `.remove()`, and `.save()` methods to modify collections**, you will need to update them to use the new [.addToCollection](https://sailsjs.com/documentation/reference/waterline-orm/models/add-to-collection), [.removeFromCollection](https://sailsjs.com/documentation/reference/waterline-orm/models/remove-from-collection), and [.replaceCollection](https://sailsjs.com/documentation/reference/waterline-orm/models/replace-collection) model methods. * **Waterline queries will now rely on the database for case sensitivity.** This means that in most adapters your queries will now be case-sensitive, whereas before they were not. This may have unexpected consequences if you are used to having case-insensitive queries. For more information on how to manage this for databases such as MySQL, see the [case sensitivity docs](https://sailsjs.com/documentation/concepts/models-and-orm/models#?case-sensitivity). * **Waterline no longer supports nested creates or updates**, and this change extends to the related blueprints. If your app relies on these features, see the [migration guide section on nested creates and updates](https://sailsjs.com/documentation/upgrading/to-v-1-0/#?nested-creates-and-updates) for more info. * **If your app sets a model attribute to `null`** using `.create()`, `.findOrCreate()` or `.update()`, you’ll need to change the type of that attribute to `json`, or use the base value for the existing attribute type, instead of `null` (e.g. `0` for numbers). See [the validations docs](https://sailsjs.com/documentation/concepts/models-and-orm/validations#?null-and-empty-string) for more info. * **The `create` blueprint response is now fully populated**, just like responses from `find`, `findOne`, `update` and `destroy`. To suppress population of records, add a `parseBlueprintOptions` to your blueprints config or to a specific route. See the [blueprints configuration reference](https://sailsjs.com/documentation/reference/configuration/sails-config-blueprints#?using-parseblueprintoptions) for more information. * **If you're using `createEach`** to insert large numbers of rows into a database, keep in mind that the Sails 1.0-compatible versions of most adapters now optimize the `createEach` method to use a single query, instead of using one query per row. Depending on your database, per-request data size limits may apply. See the [notes at the bottom of the `.createEach()` reference page](https://sailsjs.com/documentation/reference/waterline-orm/models/create-each#?notes) for more information. * **The `size` property for attributes** is no longer supported. Instead, you may indicate column size using [the `columnType` property](https://sailsjs.com/documentation/concepts/models-and-orm/attributes#?columntype). * **The `defaultsTo` property for attributes may no longer be defined as a function.** Instead, you will either need to hard-code a default value, or remove the `defaultsTo` entirely and update your code to determine the appropriate value for the attribute before creating new records. (This can either be handled before calls to `.create()`/`.createEach()` in your actions, or in the model's [`beforeCreate`](https://sailsjs.com/documentation/concepts/models-and-orm/lifecycle-callbacks#?lifecycle-callbacks-on-create)). ### Other breaking changes The upgrade guide above provides for the most common upgrade issues that Sails contributors have encountered when upgrading various apps between version 0.12 and version 1.0. Every app is different, though, so we recommend reading through the points below, as well. Not all of the changes discussed will necessarily apply to your app, but some might. * **Several properties and methods on `req` now work a little differently:** * `req.accepted` has been replaced with [`req.accepts()`](https://sailsjs.com/documentation/reference/request-req/req-accepts) * `req.acceptedLanguages` and `req.acceptsLanguage()` have been replaced with [`req.acceptsLanguages()`](https://sailsjs.com/documentation/reference/request-req/req-accepts-languages) * `req.acceptedCharsets` and `req.acceptsCharset()` have been replaced with [`req.acceptsCharsets()`](https://sailsjs.com/documentation/reference/request-req/req-accepts-charsets) * **Several `req.options` properties related to blueprints are no longer supported.** Instead, the new `parseBlueprintOptions` method can be used to give you complete control over blueprint behavior. See the [blueprints configuration reference](https://sailsjs.com/documentation/reference/configuration/sails-config-blueprints#?using-parseblueprintoptions) for more information. * **The `defaultLimit` and `populate` blueprint configuration options are no longer supported.** Instead, the new `parseBlueprintOptions` method can be used to give you complete control over blueprint behavior. See the [blueprints configuration reference](https://sailsjs.com/documentation/reference/configuration/sails-config-blueprints#?using-parseblueprintoptions) for more information. * **The `.findOne()` query method no longer supports `sort` and `limit` modifiers, and will throw an error if the given criteria match more than one record**. If you want to find a single record using anything besides a `unique` attribute (like the primary key) as criteria, use `.find(<criteria>).limit(1)` instead (keeping in mind that this will return an array of one item). * **`autoPk`, `autoCreatedAt` and `autoUpdatedAt`** are no longer supported as top-level model properties. See the [migration guide section on model config changes](https://sailsjs.com/documentation/upgrading/to-v-1-0/#?changes-to-model-configuration) for more information. * **Dynamic finders** (such as `User.findById()`) are no longer added to your models automatically. You can implement these yourself as [custom model methods](https://sailsjs.com/documentation/concepts/models-and-orm/models#?custom-model-methods). * **Model Instance Methods** are no longer supported. This allows records returned from find queries to be plain JavaScript objects instead of model record instances. * **Custom `.toJSON()`** instance methods are no longer supported. Instead, add a [`customToJSON` method](https://sailsjs.com/documentation/concepts/models-and-orm/model-settings#?customtojson) to the model class (outside of the `attributes` dictionary). See the [model settings documentation](https://sailsjs.com/documentation/concepts/models-and-orm/model-settings) for more information. * **The `.toObject()` instance method** is no longer added to every record. When implementing [`customToJSON`](https://sailsjs.com/documentation/concepts/models-and-orm/model-settings#?customtojson) for a model, be sure to clone the record using `_.omit()`, `_.pick()` or `_.clone()`. * **`autoUpdatedAt` timestamps can now be manually updated** in calls to `.update()` (previously, the passed-in attribute value would be ignored). The previous behavior faciliated the use of `.save()`, which is no longer supported. Now, you can update the `updatedAt` if you need to (but generally you should let Sails do this for you!) * **`beforeValidate` and `afterValidate` lifecycle callbacks no longer exist**. Use one of the [many other lifecycle callbacks](https://sailsjs.com/documentation/concepts/models-and-orm/lifecycle-callbacks) to tap into the query. * **`afterDestroy` lifecycle callback now receives a single record**. It has been normalized to work the same way as the `afterUpdate` callback and call the function once for each record that has been destroyed rather than once with all the destroyed records. * **Many resourceful PubSub methods have changed** (see the PubSub section below for the full list). If your app only uses the automatic RPS functionality provided by blueprints (and doesn’t call RPS methods directly), no updates are required. * **The `.find()` model method no longer automatically coerces constraints that are provided for unrecognized attributes**. For example, if you execute `Purchase.find({ amount: '12' })`, e.g. via blueprints (http://localhost:1337/purchase?amount=12), and there is no such "amount" attribute, then even if the database contains a record with the numeric equivalent (`12`), it will not be matched. (This is only relevant when using MongoDB and sails-disk.) If you are running into problems because of this, either define the attribute as a number or (if you're using blueprints) use an explicit `where` clause instead (e.g. `http://localhost.com:1337/purchase?where={"amount":12}`). * **Custom blueprints and the associated blueprint route syntax have been removed**. This functionality can be replicated using custom actions, helpers, and routes. See the "Replacing custom blueprints" section below for more information. * **Blueprint action shadow routes no longer include `/:id?`** at the end -- that is, if you have a `UserController.js` with a `tickle` action, you will no longer get a `/user/tickle/:id?` route (instead, it will be just `/user/tickle`). Apps relying on those routes should add them manually to their `config/routes.js` file. * **`sails.getBaseUrl`**, deprecated in v0.12.x, has been removed. See the [v0.12 docs for `getBaseUrl`](http://0.12.sailsjs.com/documentation/reference/application/sails-get-base-url) for more information on why it was removed and how you should replace it. * **`req.params.all()`**, deprecated in v0.12.x, has been removed. Use `req.allParams()` instead. * **`sails.config.dontFlattenConfig`**, deprecated in v0.12.x, has been removed. See the [original notes about `dontFlattenConfig`](https://sailsjs.com/documentation/upgrading/to-v-0-11#?config-files-in-subfolders) for details. * **The order of precedence for `req.param()` and `req.allParams()` has changed.** It is now consistently path > body > query (that is, url path params override request body params, which override query string params). * **`req.validate()`** has been removed. Use [`actions2`](https://sailsjs.com/documentation/concepts/actions-and-controllers#?actions-2) instead. * **The default `res.created()` response has been removed.** If you’re calling `res.created()` directly in your app, and you don't have an `api/responses/created.js` file, you’ll need to create one. + On a related note, the [Blueprint create action](https://sailsjs.com/documentation/reference/blueprint-api/create) will now return a 200 status code upon success, instead of 201. * **The default `notFound` and `serverError` responses no longer accept a `pathToView` argument.** They will only attempt to serve a `404` or `500` view. If you need to be able to call these responses with different views, you can customize the responses by adding `api/responses/notFound.js` or `api/responses/serverError.js` files to your app. * **The default `badRequest` or `forbidden` responses no longer display views**. If you don’t already have the `api/responses/badRequest.js` and `api/responses/forbidden.js` files, you’ll need add them yourself and write custom code if you want them to display view files. * **The <a href="https://www.npmjs.com/package/connect-flash" target="_blank">`connect-flash`</a> middleware has been removed** (so `req.flash()` will no longer be available by default). If you wish to continue using `req.flash()`, run `npm install --save connect-flash` in your app folder and [add the middleware manually](https://sailsjs.com/documentation/concepts/middleware). * **The `POST /:model/:id` blueprint RESTful route has been removed.** If your app is relying on this route, you’ll need to add it manually to `config/routes.js` and bind it to a custom action. * **The `handleBodyParserError` middleware has been removed**; in its place, the <a href="https://www.npmjs.com/package/skipper" target="_blank">Skipper body parser</a> now has its own `onBodyParserError` method. + If you have customized the [middleware order](https://sailsjs.com/documentation/concepts/middleware#?adding-or-overriding-http-middleware), you’ll need to remove `handleBodyParserError` from the array. + If you've overridden `handleBodyParserError`, you’ll need to instead override `bodyParser` with your own customized version of Skipper, including your error-handling logic in the `onBodyParserError` option. * **The `methodOverride` middleware has been removed.** If your app utilizes this middleware: + `npm install --save method-override` + Make sure your `sails.config.http.middleware.order` array (in `config/http.js`) includes `methodOverride` somewhere before `router` + Add `methodOverride: require('method-override')()` to `sails.config.http.middleware`. * **The `router` middleware is no longer overrideable.** Instead, the Express 4 router is used for routing both external and internal (aka “virtual”) requests. It’s still important to have a `router` entry in `sails.config.http.middleware.order` to delimit which middleware should be added before and after the router. * **The query modifiers `lessThan`, `lessThanOrEqual`, `greaterThan`, and `greaterThanOrEqual` have been removed**. Use the shorthand versions instead (`<`, `<=`, `>`, `>=`). * **The [`find one`](https://sailsjs.com/documentation/reference/blueprint-api/find-one) and [`find`](https://sailsjs.com/documentation/reference/blueprint-api/find-where) blueprint actions** now accept a `populate=false` rather than `populate=` to specify that no attributes should be populated. * **The [`add`](https://sailsjs.com/documentation/reference/blueprint-api/add-to) and [`remove`](https://sailsjs.com/documentation/reference/blueprint-api/remove-from) blueprint actions** now require that the primary key of the child record to add or remove be supplied as part of the URL, rather than allowing it to be passed on the query string or in the body. * **The [`destroy`](https://sailsjs.com/documentation/reference/blueprint-api/destroy) blueprint action** now requires that the primary key of the record to destroy be supplied as part of the URL, rather than allowing it to be passed on the query string or in the body. * **The `sails.config.session.routesDisabled` setting has changed** to `sails.config.session.isSessionDisabled()`, a function. See the [`config/session.js` docs](https://sailsjs.com/documentation/reference/configuration/sails-config-session) for more information on configuring `isSessionDisabled()`. * **The experimental “switchback-style” usage for Waterline methods is no longer supported**. Only function callbacks may be used with Waterline model methods. * **The experimental `create` auto-migration scheme is no longer supported**. It is highly recommended that you use a migration tool such as [Knex](http://knexjs.org/#Migrations) to handle migrations of your production database. * **The experimental `forceLoadAdapter` datastore setting is no longer supported**. Instead, all adapters referenced in `config/datastores.js` (formerly `config/connections.js`) are automatically loaded whenever Sails lifts. * **The experimental `usage` route option has been removed.** It is recommended that you perform any route parameter validation in your controller code. * **The experimental “associated-item” blueprint shadow routes have been removed.** These were routes like `GET /user/1/pets/2`, whose functionality can be replicated by simply using the much-clearer route `GET /pets/2`. * **The experimental `.validate()` method in model classes** (e.g. `User.validate()`) is now fully supported, but its usage has changed. See the [`.validate()` docs](https://sailsjs.com/documentation/reference/waterline-orm/models/validate) for more information. * **The ordering of attributes** in the internal representation of model classes has changed (association attributes are now sorted at the bottom). This has the effect of causing tables created using `migrate: 'alter'` to have their columns in a different order than in previous versions of Waterline, so be aware of this if column ordering is important in your application. As a reminder, auto-migrations are intended to help you design your schema as you build your app. They are not guaranteed to be consistent regarding any details of your physical database columns besides setting the column name, type (including character set / encoding if specified) and uniqueness. * **Using `_config` to link a controller to a model** will no longer work. This was never a supported feature, but it was used in some projects to change the URLs that were mapped to the blueprint actions for a model. Please use [`restPrefix`](https://sailsjs.com/documentation/reference/configuration/sails-config-blueprints#?properties) instead. * **The `find()`, `destroy()`, and `update()` methods** ignore `undefined` attributes. These methods will strip undefined attributes from their search criteria, e.g. `User.update({id: undefined}).with({ firstName: 'Finn'})` would update **every** user record. Read more about this in [this Github issue](https://github.com/balderdashy/sails/issues/4639#issuecomment-320369193) ### Changes to database configuration * The `sails.config.connections` setting has been deprecated in favor of `sails.config.datastores`. If you lift an app that still has `sails.config.connections` configured, you’ll get a warning which you can avoid by simply changing `module.exports.connections` in `config/connections.js` to `module.exports.datastores`. For your own sanity, it’s recommended that you also change the filename to `config/datastores.js`. * The `sails.config.models.connection` setting has been deprecated in favor of `sails.config.models.datastore`. As above, changing the name of the property in `config/models.js` should be sufficient to turn off any warnings. * Every app now has a default datastore (appropriately named `default`) that is configured to use a built-in version of the [`sails-disk` adapter](https://github.com/balderdashy/sails-disk). In Sails 1.0, the default value of `sails.config.models.datastore` is `default` (rather than `localDiskDb`). The recommended approach to setting the default datastore for your models is to simply to add the desired configuration under the `default` key in `config/datastores.js`, and leave the `datastore` key in `config/models.js` undefined, rather than the previous approach of setting `datastore` to (for example) `myPostgresqlDb` and then adding a `myPostgresqlDb` key to `config/datastores.js`. This makes it a lot easier to change the datastore used by different environments (for instance, by changing the configuration of the `default` datastore in `config/env/production.js`). * _All_ datastores that are configured in an app will be loaded at runtime (rather than only loading datastores that were being used by at least one model). This has the benefit of allowing the use of a datastore outside the context of an individual model, but it does mean that if you don’t want to connect to a certain database when Sails lifts, you should comment out that datastore connection config! ### Nested creates and updates * The [`.create()`](https://sailsjs.com/documentation/reference/waterline-orm/models/create), [`.update()`](https://sailsjs.com/documentation/reference/waterline-orm/models/update) and [`.add()`](https://sailsjs.com/documentation/reference/waterline-orm/models/find) model methods no longer support creating a new “child” record to link immediately to a new or existing parent. For example, given a `User` model with a singular association to an `Animal` model through an attribute called `pet`, it is not possible to set `pet` to a dictionary representing values for a brand new `Animal` (aka a “nested create”). Instead, create the new `Animal` first and use its primary key to set `pet` when creating the new `User`. * Similarly, the [create](https://sailsjs.com/documentation/reference/blueprint-api/create), [update](https://sailsjs.com/documentation/reference/blueprint-api/update) and [add](https://sailsjs.com/documentation/reference/blueprint-api/add-to) blueprint actions no longer support nested creates. * The [`.update()`](https://sailsjs.com/documentation/reference/waterline-orm/models/update) model method and its associated [blueprint action](https://sailsjs.com/documentation/reference/blueprint-api/update) no longer support replacing an entire plural association. If a record is linked to one or more other records via a [“one-to-many”](https://sailsjs.com/documentation/concepts/models-and-orm/associations/one-to-many) or [“many-to-many”](https://sailsjs.com/documentation/concepts/models-and-orm/associations/many-to-many) association and you wish to link it to an entirely different set of records, use the [`.replaceCollection()` model method](https://sailsjs.com/documentation/reference/waterline-orm/models/replace-collection) or the [replace blueprint action](https://sailsjs.com/documentation/reference/blueprint-api/replace). ### Changes to model configuration ##### tl;dr Remove any `autoPK`, `autoCreatedAt` and `autoUpdatedAt` properties from your models, and add the following to your `config/models.js` file: ```javascript attributes: { createdAt: { type: 'number', autoCreatedAt: true, }, updatedAt: { type: 'number', autoUpdatedAt: true, }, id: { type: 'number', autoIncrement: true}, // <-- for SQL databases id: { type: 'string', columnName: '_id'}, // <-- for MongoDB } ``` ##### The `autoPK` top-level property is no longer supported This property was formerly used to indicate whether or not Waterline should create an `id` attribute as the primary key for a model. Starting with Sails v1.0 / Waterline 0.13, Waterline will no longer create any attributes in the background. Instead, the `id` attribute must be defined explicitly. There is also a new top-level model property called `primaryKey`, which can be set to the name of the attribute that should be used as the model's primary key. This value defaults to `id` for every model, so in general you won't have to set it yourself. ##### The `autoUpdatedAt` and `autoCreatedAt` model settings are now attribute-level properties These properties were formerly used to indicate whether or not Waterline should create `createdAt` and `updatedAt` timestamps for a model. Starting with Sails v1.0 / Waterline 0.13, Waterline will no longer create these attributes in the background. Instead, the `createdAt` and `updatedAt` attributes must be defined explicitly if you want to use them. By adding `autoCreatedAt: true` or `autoUpdatedAt: true` to an attribute definition, you can instruct Waterline to set that attribute to the current timestamp whenever a record is created or updated. Depending on the type of these attributes, the timestamps will be generated in one of two formats: + For `type: 'string'`, these timestamps are stored in the same way as they were in Sails 0.12: as timezone-agnostic ISO 8601 JSON timestamp strings (e.g. `'2017-12-30T12:51:10Z'`). So if any of your front-end code is relying on the timestamps as strings it's important to set this to `string`. + For `type: 'number'`, these timestamps are stored as JS timestamps (the number of milliseconds since Jan 1, 1970 at midnight UTC). Furthermore, for any attribute, if you pass `new Date()` as a constraint within a Waterline criteria's `where` clause, or as a new record, or within the values to set in a `.update()` query, then these same rules are applied based on the type of the attribute. If the attribute is `type: 'json'`, it uses the latter approach. <!-- TODO: finish filling in the gaps for this section: ##### Changes to built-in data types As of Sails v1.0 / Waterline 0.13, we've made changes to the way that data types and type safety work in the ORM. This allows us to do more as far as type validation/coercion, which makes your app more future-proof and less error-prone[1]()[2]()[3](). As a result, we've narrowed down the `type` options to the following: + `'string'` + `'number'` + `'boolean'` + `'json'` + _`'ref'`_ _(advanced: do not use unless you have personally inspected the source code of your adapter to understand how it handles data of this type - this is a direct channel between the adapter and your app.)_ This means that the following types are **no longer supported** (but can be simulated in most cases by including `columnType` and/or validation rules in your attribute definition): + `'text'` _(use `type: 'string'` and `columnType: 'TEXT'`)_ + `'integer'` _(use `type: 'number'`, `columnType: 'INT'` and `isInteger: true`)_ + `'float'` _(use `type: 'number'` and `columnType: 'FLOAT'`)_ + `'date'` + `'datetime'` + `'binary'` + `'array'` _(use `type: 'json'`)_ + `'mediumtext'` _(use `type: 'string'` and `columnType: 'MEDIUMTEXT'`)_ + `'longtext'` _(use `type: 'string'` and `columnType: 'LONGTEXT'`)_ + `'objectid'` + `'email'` _(use `type: 'string'` and `isEmail: true`)_ --> ### Changes to `.create()`, `.createEach()`, `.update()`, and `.destroy()` results As of Sails v1.0 / Waterline 0.13, the default result from `.create()`, `.createEach()`, `.update()`, and `.destroy()` has changed. To encourage better performance and easier scalability, `.create()` no longer sends back the created record. Similarly, `.createEach() ` no longer sends back an array of created records, `.update()` no longer sends back an array of _updated_ records, and `.destroy()` no longer sends back _destroyed_ records. Instead, the second argument to the .exec() callback is now `undefined` (or the first argument to `.then()`, if you're using promises). This makes your app more efficient by removing unnecessary `find` queries, and it makes it possible to use `.update()` and `.destroy()` to modify many different records in large datasets, rather than falling back to lower-level native queries. You can still instruct the adapter to send back created or modified records for a single query by using the `fetch` method. For example: ```js Article.update({ category: 'health-and-wellness', status: 'draft' }) .set({ status: 'live' }) .fetch() .exec(function(err, updatedRecords){ //... }); ``` > If the prospect of changing all of your app's queries seems daunting, there is a temporary convenience you might want to take advantage of. > To ease the process of upgrading an existing app, you can tell Sails/Waterline to fetch created/updated/destroyed records for ALL of your app's `.create()`/`.createEach()`/`.update()`/`.destroy()` queries. Just edit your app-wide model settings in `config/models.js`: > > ```js > fetchRecordsOnUpdate: true, > fetchRecordsOnDestroy: true, > fetchRecordsOnCreate: true, > fetchRecordsOnCreateEach: true, > ``` > > That's it! Still, to improve performance and future-proof your app, you should go through all of your `.create()`, `.createEach()`, `.update()`, and `.destroy()` calls and add `.fetch()` when you can. Support for these model settings will eventually be removed in Sails v2. ### Changes to Waterline criteria usage * For performance reasons, as of Sails v1.0 / Waterline 0.13, criteria passed into Waterline's model methods will now be mutated in-place in most situations (whereas in Sails/Waterline v0.12, this was not necessarily the case). * Aggregation clauses (`sum`, `average`, `min`, `max`, and `groupBy`) are no longer supported in criteria. Instead, see new model methods [.sum()](https://sailsjs.com/documentation/reference/waterline-orm/models/sum) and [.avg()](https://sailsjs.com/documentation/reference/waterline-orm/models/avg). * Changes to limit and skip: + `limit: 0` **no longer does the same thing as `limit: undefined`**. Instead of matching ∞ results, it now matches 0 results. + Avoid specifying a limit of < 0. It is still ignored, and acts like `limit: undefined`, but it now logs a deprecation warning to the console. + `skip: -20` **no longer does the same thing as `skip: undefined`**. Instead of skipping zero results, it now refuses to run with an error. + Limit must be < Number.MAX_SAFE_INTEGER (...with one exception: for compatibility/convenience, `Infinity` is tolerated and normalized to `Number.MAX_SAFE_INTEGER` automatically.) + Skip must be < Number.MAX_SAFE_INTEGER ##### Change in support for mixed `where` clauses Criteria dictionaries with a mixed `where` clause are no longer supported. For example, instead of: ```javascript { username: 'santaclaus', limit: 4, select: ['beardLength', 'lat', 'long'] } ``` you should use: ```javascript { where: { username: 'santaclaus' }, limit: 4, select: ['beardLength', 'lat', 'long'] } ``` > Note that you can still do `{ username: 'santaclaus' }` as shorthand for `{ where: { username: 'santaclaus' } }`, you just can't mix other top-level criteria clauses (like `limit`) alongside constraints (e.g. `username`). > > For places where you're using Waterline's chainable deferred object to build criteria, don't worry about this—it's already taken care of for you. ### Security New apps created with Sails 1.0 will contain a **config/security.js** file instead of individual **config/cors.js** and **config/csrf.js** files. Apps migrating from earlier versions can keep their existing files, as long as they perform the following upgrades: * Change `module.exports.cors` to `module.exports.security.cors` in `config/cors.js` * Change CORS config settings names to match the newly documented names in [Reference > Configuration > sails.config.security](https://sailsjs.com/documentation/reference/configuration/sails-config-security#?sailsconfigsecuritycors) * Change `module.exports.csrf` to `module.exports.security.csrf` in `config/csrf.js`. This value is now simply `true` or `false`; no other CSRF options are supported (see below). * `sails.config.csrf.routesDisabled` is no longer supported. Instead, add `csrf: false` to any route in `config/routes.js` that you wish to be unprotected by CSRF, for example: ```js 'POST /some-thing': { action: 'do-a-thing', csrf: false }, ``` * `sails.config.csrf.origin` is no longer supported. Instead, you can add any custom CORS settings directly to your CSRF token route configuration, for example: ```js 'GET /csrfToken': { action: 'security/grant-csrf-token', cors: { allowOrigins: ['http://foobar.com', 'https://owlhoot.com'] } } ``` * `sails.config.csrf.grantTokenViaAjax` is no longer supported. This setting was used to turn the CSRF token-granting route on or off. In Sails 1.0, you add that route manually in your `config/routes.js` file (see above). If you don’t want to grant CSRF tokens via AJAX, just leave that route out of `config/routes.js`. ### Views For maximum flexibility, Consolidate is no longer bundled with Sails. If you are using a view engine besides EJS, you'll probably want to install Consolidate as a direct dependency of your app. You can then configure the view engine in `config/views.js`, like so: ```javascript extension: 'swig', getRenderFn: function() { // Import `consolidate`. var cons = require('consolidate'); // Return the rendering function for Swig. return cons.swig; } ``` Adding custom configuration to your view engine is a lot easier in Sails 1.0: ```javascript extension: 'swig', getRenderFn: function() { // Import `consolidate`. var cons = require('consolidate'); // Import `swig`. var swig = require('swig'); // Configure `swig`. swig.setDefaults({tagControls: ['{?', '?}']}); // Set the module that Consolidate uses for Swig. cons.requires.swig = swig; // Return the rendering function for Swig. return cons.swig; } ``` Note that the [built-in support for layouts](https://sailsjs.com/documentation/concepts/views/layouts) still works for the default EJS views, but layout support for other view engines (e.g. Handlebars or Ractive) is not bundled with Sails 1.0. ### Resourceful PubSub * Removed deprecated `backwardsCompatibilityFor0.9SocketClients` setting. * Removed deprecated `.subscribers()` method. * Removed deprecated "firehose" functionality. * Removed support for 0.9.x socket client API. * The following resourceful pubsub methods have also been removed: * `.publishAdd()` * `.publishCreate()` * `.publishDestroy()` * `.publishRemove()` * `.publishUpdate()` * `.watch()` * `.unwatch()` * `.message()` In place of the removed methods, you should use the new `.publish()` method, or the low-level [sails.sockets](https://sailsjs.com/documentation/reference/web-sockets/sails-sockets) methods. Keep in mind that unlike `.message()`, `.publish()` does _not_ wrap your data in an envelope containing the record ID, so—if it's important—you'll need to include the ID yourself as part of the data. For example, in Sails v0.12.x, `User.message(123, {owl: 'hoot'})` would have resulted in the following notification being broadcasted to clients: ``` { verb: 'messaged', id: 123, data: { owl: 'hoot' } } ``` By contrast, in Sails v1.0, `User.publish(123, {owl: 'hoot'})` will simply broadcast: ``` { owl: 'hoot' } ``` ### Replacing custom blueprints Out of the box, it is no longer possible to add a file to `api/blueprints/` that will automatically be used as a blueprint action for all models. However, this behavior can easily be replicated by installing [`sails-hook-custom-blueprints`](https://www.npmjs.com/package/sails-hook-custom-blueprints). <!-- Another way is to add a route like `'POST /:model': 'SharedController.create'` to the bottom of your `config/routes.js` file, and then add the custom `create` blueprint to a `api/controllers/SharedController.js` file (or a `api/controllers/shared/create.js` standalone action). Yet another option would be to add a `api/helpers/create.js` helper which takes a model name and dictionary of values as inputs (see [Concepts > Helpers](https://sailsjs.com/documentation/concepts/helpers)), and call that helper from the related action for each model (e.g. `UserController.create`). --> ### Express 4 Sails 1.0 comes with an update to the internal Express server from version 3 to version 4 (thanks to some great work by [@josebaseba](http://github.com/josebaseba)). This change is mainly about maintainability for the Sails framework and should be transparent to your app. However, there are a couple of differences worth noting: * The `404`, `500` and `startRequestTimer` middleware are now built-in to every Sails app, and have been removed from the `sails.config.http.middleware.order` array. If your app has an overridden `404` or `500` handler, you should instead override `api/responses/notFound.js` and `api/responses/serverError.js` respectively. * Session middleware that was designed specifically for Express 3 (e.g. very old versions of `connect-redis` or `connect-mongo`) will no longer work, so you’ll need to upgrade to more recent versions. * The `sails.config.http.customMiddleware` feature is deprecated in Sails 1.0. It will still work for now, but may be removed in a later release. Instead of using `customMiddleware` to modify the Express app directly, use regular (`req`, `res`, `next`) middleware instead. For instance, you can replace something like: ``` customMiddleware: function(app) { var passport = require('passport'); app.use(passport.initialize()); app.use(passport.session()); } ``` with something like: ``` var passport = require('passport'); middleware: { passportInit: passport.initialize(), passportSession: passport.session() }, ``` being sure to insert `passportInit` and `passportSession` into your `middleware.order` array in `config/http.js`. ### Response methods * `.jsonx()` is deprecated. If you have files in `api/responses` that you haven't customized at all, you can just delete them and let the Sails default responses work their magic. If you have files in `api/responses` that you’d like to keep, replace any occurences of `res.jsonx()` in those files with `res.json()`. * `res.negotiate()` is deprecated. Use `res.serverError()`, `res.badRequest()`, or a [custom response](https://sailsjs.com/documentation/concepts/extending-sails/custom-responses) instead. ### i18n Sails 1.0 switches from using the [i18n](http://npmjs.org/package/i18n) to the lighter-weight [i18n-2](http://npmjs.org/package/i18n-2) module. The overwhelming majority of users should see no difference in their apps. However, if you’re using the `sails.config.i18n.updateFiles` option, be aware that this is no longer supported; instead, locale files will _always_ be updated in development mode, and _never_ in production mode. If this is a problem or you’re missing some other feature from the i18n module, you can install [sails-hook-i18n](http://npmjs.org/package/sails-hook-i18n) to revert to pre-Sails-1.0 functionality. > If your 0.12 application is running into issues during upgrade due to its use of i18n features, see [#4343](https://github.com/balderdashy/sails/issues/4343) for more troubleshooting tips. ### WebSockets All Sails 1.0 projects that use websockets must install the latest `sails-hook-sockets` dependency (`npm install --save sails-hook-sockets`). This version of `sails-hook-sockets` differs from previous ones in a couple of ways: * The default `transports` setting is simply `['websocket']`. In the majority of production deployments, restricting your app to the `websocket` transport (rather than using `['polling', 'websocket']`) avoids problems with sessions (see the pre-1.0 [scaling guide notes](https://github.com/balderdashy/sails-docs/blob/1038b38cb34fd945086480ee45325a1ac95a0950/concepts/Deployment/Scaling.md#notes) for details). If you’re using the `sails.io.js` websocket client, the easiest way to make your app compatible with the new websocket settings is to install the new `sails.io.js` version with `sails generate sails.io.js`. The latest version of that package also defaults to the “websocket-only” transport strategy. If you’ve customized the `transports` setting in your front-end code and `config/sockets.js` file, then you'll just need to continue to ensure that the values in both places match. * The latest `sails-hook-sockets` hook uses a newer version of Socket.io. See the [Socket.io changelog](https://github.com/socketio/socket.io/blob/master/History.md#150--2016-10-06) for a full update, but keep in mind that socket IDs no longer have `/#` prepended to them by default. ### Grunt The Grunt task-management functionality that was formerly part of the Sails core has now been moved into the separate `sails-hook-grunt` module. Existing apps simply need to `npm install --save sails-hook-grunt` to continue using Grunt. However, with a modification to your app’s `Gruntfile.js`, you can take advantage of the fact that `sails-hook-grunt` includes all of the `grunt-contrib` modules that previously had to be installed at the project level. The new `Gruntfile.js` contains: ``` module.exports = function(grunt) { var loadGruntTasks = require('sails-hook-grunt/accessible/load-grunt-tasks'); // Load Grunt task configurations (from `tasks/config/`) and Grunt // task registrations (from `tasks/register/`). loadGruntTasks(__dirname, grunt); }; ``` Assuming that you haven’t customized the Gruntfile in your app, you can replace `Gruntfile.js` with that code and then safely run: ``` npm uninstall --save grunt-contrib-clean npm uninstall --save grunt-contrib-coffee npm uninstall --save grunt-contrib-concat npm uninstall --save grunt-contrib-copy npm uninstall --save grunt-contrib-cssmin npm uninstall --save grunt-contrib-jst npm uninstall --save grunt-contrib-less npm uninstall --save grunt-contrib-uglify npm uninstall --save grunt-contrib-watch npm uninstall --save grunt-sails-linker npm uninstall --save grunt-sync npm uninstall --save grunt-cli ``` to remove those dependencies from your project. ### Troubleshooting ##### Still displaying v0.12 at launch? Make sure you have `sails` installed locally in your project, and that you're using the v1 version of the command-line tool. To install the v1.0 globally, run `npm install sails@^1.0.0 -g`. To install it for a particular Sails app, cd into that app's directory, then run `npm install sails@^1.0.0 --save`. (After installing locally, be sure to also install the necessary hooks -- see above.) <docmeta name="displayName" value="To v.1.0"> <docmeta name="version" value="1.0.0"> ================================================ FILE: docs/upgrading/upgrading.md ================================================ # Upgrading Like most Node packages, Sails respects [semantic versioning](http://semver.org/). For example, if you are using Sails v0.11.3, and then upgrade to Sails v0.11.4, you shouldn't need to change your application code. This is called a **patch release**. On the other hand, if you upgrade from Sails v0.11.3 to v1.0.0, you can expect some _breaking changes_, meaning that you will need to change your Sails app's code in order to use the new version. With any framework or tool, _some_ breaking changes are inevitable over time, but you can expect to see these kinds of changes less often as the APIs in Node and Sails continue to stabilize. In the meantime, the core maintainers strive to minimize breaking changes and maintain backwards compatibility where possible. ### Version notes For details about changes between versions, as well as a migration guide to assist you in making an necessary changes to your app, please refer to the appropriate page: - [v1.x](https://sailsjs.com/documentation/upgrading/to-v-1-0) - [v0.12.x](https://sailsjs.com/documentation/concepts/upgrading/to-v-0-12) - [v0.11.x](https://sailsjs.com/documentation/concepts/upgrading/to-v-0-11) - [v0.10.x](https://sailsjs.com/documentation/concepts/upgrading/to-v-0-10) ### Notes > - Like Node.js, minor version bumps in Sails versions prior to v1.0 included breaking changes—e.g. upgrading from v0.11.3 to v0.12.0 might force you to make some changes to your code. But from v1.0.0 and on, minor version (the second number) releases should be fully backwards compatible. For example, v1.1.0 to v1.2.0 should not force you to make changes to your code, whereas upgrading to v2.0.0 might. > - If you are more than one version behind the latest release and run into difficulties, consider updating your app one step at a time. The migration guides are written with a particular version diff in mind, and it's best to isolate as many variables as possible. For instance, if you are running Sails v0.11 and trying to upgrade to Sails v1.5.18 but having trouble, try first upgrading to Sails v0.11, then v0.12, _then_ v1.5.18. <docmeta name="displayName" value="Upgrading"> <docmeta name="isOverviewPage" value="true"> ================================================ FILE: docs/version-notes/0.10.x/0.10.x.md ================================================ <docmeta name="displayName" value="0.10.x"> <docmeta name="version" value="0.10.0"> ================================================ FILE: docs/version-notes/0.10.x/Changelog0.10.0-rc9.md ================================================ # 0.10.0-rc9 Changelog + Associations + Adapter-level support for optimized joins (SQL databases and Mongo) + Built-in support for in-memory joins. Allows for cross-database and even cross-adapter joins! (e.g. a User in Mongo has many Messages in a MySQL database called `legacy_messages`, and also a Role in a MySQL database called `myapp`. These can be automatically joined together using the same ORM syntax as normal.) + Better Error Handling in Waterline + Revamped Sails CLI + Generators w/ support for coffeescript + Support for dry runs (`--dry`) for `sails generate` and `sails new` + Experimental support for custom generators + API Blueprints + Blueprints are injected into project, allowing the built-in API to be customized. + Dramatic simplification of how blueprints are injected-- by implicitly including them in the routes file. + Backwards compatibility for blueprints on <=v0.9 apps can be achieved by plugging in a simple config to re-enable the traditional support and configurations. + Blueprint routes automatically take associations into account, e.g.: + `GET /user/2/dogs` -- get dogs belonging to user #2 + `GET /user/2/dad` -- get dad belonging to user #2 + `PUT /user/2/dogs` -- add a dog to user #2 + `DELETE /user/2/dogs/2` -- remove dog #5 from user #2 + PubSub + Simplified dramatically- removed concept of class rooms (most of the time, this isn't exactly what you want anyways) + Blueprints still work the same way by introspecting your app's schema and taking advantage of information about assocations to create logical publish/subscribe dependencies, relying on the global channel in cases where a shared instance doesn't exist. + Reduced to a handful of simple methods: + `SomeModel.publish()` -- publish to model instance + `SomeModel.subscribe()` -- subscribe socket to model instance + `SomeModel.unsubscribe()` -- unsubscribe socket from model instance + `sails.publish()` -- publish to global channel + `sails.subscribe()` -- subscribe socket to global channel + `sails.unsubscribe()` -- unsubscribe socket to global channel + Error Negotiation Shortcuts + Automatically content-negotiate a response-- configurable in `500.js`, `404.js`, `400.js`, `403.js` + `res.serverError( msgOrObj )` + `res.notFound()` + `res.forbidden( msgOrObj )` + `res.badRequest( msgOrObj )` # Deprecated ### Overview The following features are considered deprecated and should at some point be removed from the codebase # Dynamic Finder Methods - .findOneBy`<attribute>`In() - .findOneBy`<attribute>`Like() - .findBy`<attribute>`In() - .findBy`<attribute>`Like() - .countBy`<attribute>`In() - .countBy`<attribute>`Like() - .`<attribute>`Contains() # CRUD Class Methods - .findAll() - .findOneLike() - .findLike() - .contains() - .join() - .select() - .findOrCreateEach() - .join() - .startsWith() - .endsWith() <docmeta name="displayName" value="0.10.0-rc9 Changelog"> <docmeta name="version" value="0.10.0"> ================================================ FILE: docs/version-notes/0.10.x/Changelog0.10x.md ================================================ # Upgrading to v0.10 For the most part, running sails lift in an existing v0.9 project should just work. The core contributors have taken a number of steps to make the upgrade as easy as possible, and if you follow the deprecation messages in the console, you should do just fine. Sails v0.10 comes with some big changes. The sections below provide a high level overview of what's changed, major bug fixes, enhancements and new features, as well as a basic tutorial on how to upgrade your v0.9.x Sails app to v0.10. ## File uploads The Connect multipart middleware [will soon be officially deprecated](http://www.senchalabs.org/connect/multipart.html). But since this module was used as the built-in HTTP body parser in Sails v0.9 and Express v3, this is a breaking change for v0.9 Sails projects relying on `req.files`. By default in v0.10, Sails includes [skipper](https://github.com/balderdashy/skipper), a body parser which allows for streaming file uploads without buffering tmp files to disk. For run-of-the-mill file upload use cases, Skipper comes with bundled support for uploads to local disk (via skipper-disk), but streaming uploads can be plugged in to any of its supported adapters. For examples/documentation, please see the Skipper repository as well as the Sails documentation on `req.file()`. ### Why? A body parser's job is to parse the "body" of incoming multipart HTTP requests. Sometimes, that "body" includes text parameters, but sometimes, it includes file uploads. Connect multipart is great code, and it supports both file uploads AND text parameters in multipart requests. But like most modules of its kind, it accomplishes this by buffering file uploads to disk. This can quickly overwhelm a server's available disk space, and in many cases exposes a serious DoS attack vulnerability. Skipper is unique in that it supports **streaming** file uploads, but also maintains support for metadata in the request body (i.e. JSON/XML/urlencoded request body parameters). It uses a handful of heuristics to make sure only the files you're expecting get plugged in and received by the blob adapter, and other (potentially malicous) file fields are ignored. > #### ** Important!** > For Skipper to work, you _must include all text parameters BEFORE file parameters_ in file upload requests to the server. Once Skipper sees the first file field, it stops waiting for text parameters (this is to avoid unnecessary/unsafe buffering of file data). ### Configuring a different body parser As with most things in Sails, you can use any Connect/Express/Sails-compatible bodyparser you like. To switch back to **connect-multipart**, or any other body parser (like **formidable** or **busboy**), change your app's http configuration. ## Blueprints A new blueprint action (`findOne`) has been added. For instance, if you have a `FooController` and `Foo` model, then send a request to `/foo/5`, the `findOne` action in your `FooController` will run. If you don't have a `findOne` action, the `findOne` blueprint action will be used in its stead. Requests sent to `/foo` will still run the find controller/blueprint action. ## Policies Policies work exactly as they did in v0.9- however there is a new consideration you should take into account: Due to the introduction of the more specific `findOne()` blueprint action mentioned above, you will want to make sure you're handling it explicitly in your policy mapping configuration. For example, let's say you have a v0.9 app whose `policies.js` configuration prevents access to the `find` action in your `DoveController`: ```javascript module.exports.policies = { '*': true, DoveController: { find: false } }; ``` Assuming rest blueprint routes are enabled, this would prevent access to requests like both `/dove` and `/dove/14`. But now in v0.10, since `/dove/14` will actually run the `findOne` action, we must handle it explicitly: ```javascript module.exports.policies = { '*': true, DoveController: { find: false, findOne: false } }; ``` ## Pubsub ### Summary + `message` socket (i.e. "comment") event on client is now `modelIdentity` (where "modelIdentity" is different depending on the model that the `publish*()` method was called from. + Clients are no longer subscribed to model-creation events by the blueprint routes. To listen for creation events, use `Model.watch()`. + The events that were formerly `create`, `update`, and `destroy` are now `created`, `updated`, and `destroyed`. ### Details The biggest change to pubsub is that Socket.io events are emitted under the name of the model emitting them. Previously, your client listened for the `message` event and then had to determine which model it came from based on the included data: ```javascript socket.on('message', function(cometEvent) { if (cometEvent.model == 'user') { // Handle inbound messages related to a user record } else if (cometEvent.model === 'product') { // Handle inbound messages related to a product record } // ... } ``` Now, you subscribe to the identity of the model: ```javascript socket.on('user', function(cometEvent) { // Handle inbound messages related to a user record }); socket.on('product', function (cometEvent) { // Handle inbound messages related to a product record }); ``` This helps to structure your front end code. The way you subscribe clients to models has also changed. Previously, you specified whether you were subscribing to the model class (class room) or one or more model instances based on the parameters that you passed to `Model.subscribe`. It was effectively one method to do two very different things. Now, you use `Model.subscribe()` to subscribe only to model instances (records). You can also specify event "contexts", or types, that you'd like to hear about. For example, if you only wanted to get messages about updates to an instance, you would call `User.subscribe(req, myUser, 'update')`. If no context is given in a call to `.subscribe()`, then all contexts specified by the model class's autosubscribe property will be used. To subscribe to model creation events, you can now use `Model.watch()`. Upon subscription, your clients will receive messages every time a new record is created on that model using the blueprint routes, and will automatically be subscribed to the new instance as well. Remember, when working with blueprints, clients are no longer auto subscribed to the class room. This must be done manually. Finally, if you want to see all pubsub messages from all models, you can access the `firehose`, a development-only tool that broadcasts messages about _everything_ that happens to your models. You can subscribe to the firehose using `sails.sockets.subscribeToFirehose(socket)`, or on the front end by making a socket request to `/firehose`. The firehose will broadcast a `firehose` event whenever a model is created, updated, destroyed, added to, removed from or messaged. This effectively replaces the `message` event used in previous Sails versions. To see examples of the new pubsub methods in action, see [SailsChat](https://github.com/balderdashy/sailschat). ## Arguments to lifecycle callbacks are now typecasted Previously, with `schema: true`, if you sent an attribute value to a `.create()` or `.update()` that did not match the expected type declared in the model's attributes, the value you passed in would still be accessible in your model's lifecycle callbacks. In Sails/Waterline v0.10, this is no longer the case. Values passed to `.create()` and `.update()` are type-casted before your lifecycle callbacks run. Affected lifecycle callbacks include `beforeUpdate()`, `beforeCreate()`, and `beforeValidate()`. ## beforeValidation() is now beforeValidate() If you were using the `beforeValidation` or `afterValidation` model lifecycle callbacks in any of your models, you should change them to `beforeValidate` or `afterValidate`. This change was made in Waterline to match the style of the other lifecycle callbacks (e.g. `beforeCreate`, `afterUpdate`, etc.). ## .done() vs. .exec() ** The old (/confusing?) meaning of `.done()` has been deprecated.** In Sails <= v0.8, the syntax for executing an ORM query was `Model. [ … ] .done( cb )`. In v0.9, when promise support was added, the `Model. [ … ] .exec( cb )` became the recommended replacement, since `.done()` has a special meaning in the promise spec. However, the original usage of `.done()` was left untouched to make upgrading from v0.8 to v0.9 easier. But as of Sails/Waterline v0.10, the original meaning of `.done()` has been officially deprecated to allow for a more robust promise implementation going forward, and pluggable promise library support (e.g. choose `Q` or `Bluebird` etc.). ## Associations Sails v0.10 introduces associations between data models. Since the work we've done on associations is largely additive, your existing models should still just work. That said, this is a powerful new feature that allows you to write less code and makes your app more maintainable, so we suggest taking advantage of it! To learn about how to use associations in Sails, check out the docs. Associations (or "relations") are really just special attributes. Instead of string or integer values, you can specify an instance of a model or a collection of model instances. You can think about this kind of like an object (`{...}`) or an array (`[{...}, {...}]`) you might store as JSON in a NoSQL database. The difference is, in Sails, this works with any of the supported databases, and even allows you to populate (i.e. join) across different databases and types of databases. ## Generators Sails has had support for generating code for a while now (e.g. `sails generate controller foo`) but in v0.10, we wanted to make this feature more extensible, open, and accessible to everybody in the Sails community. With that in mind, v0.10 comes with a complete rewrite of the command-line tool, and pluggable generators. Want to be able to run `sails generate blog foo` to make a new blog built on Sails? Create a `blog` generator (run sails `generate generator blog`), add your templates, and configure the generator to copy the new templates over. Then you can release it to the community by publishing an npm module called `sails-generate-blog`. Compatibility with Yeoman generators is also in our roadmap. ## Command-line tool The big change here is how you create a new api. In the past you called `sails generate new_api`. This would generate a new controller and model called `new_api` in the appropriate places. This is now done using `sails generate api new_api`. You can still generate models and controllers seperately using the same CLI Commands. Also, `--linker` switch is no longer available. In previous version, if `--linker` switch was provided, it created a `myApp/assets/linker folder`, with `js`, `styles` and `templates` folders inside. In this new version, the `myApp/assets/linker` folder is not created. Compiling CoffeeScript and Less is the default behavior now, right from the `myApp/assets/js` and `myApp/assets/scripts` folders. ## Custom server responses In v0.10, you can now generate your own custom server responses. Like before, there are a few that we automatically create for you. Instead of generating `myApp/config/500.js` and other `.js` responses in the config directory, they are now generated in `myApp/api/responses/`. To migrate, you will need to create a new v0.10 project and copy the `myApp/api/responses` directory into your existing app. You will then modify the appropriate .js file to reflect any customization you made in your response logic files (500.js,etc). ## Legacy data stored in the temporary sails-disk database `sails-disk`, used by default in new Sails projects, now stores data a bit differently. If you have some temporary data stored in a 0.9.x project, you'll want to wipe it out and start fresh. To do this: From your project's root directory: ``` $ rm .tmp/disk.db ``` ## Adapter/Database Configuration `config.adapters` (in `myApp/config/adapters.js`) is now config.connections (in new projects, this is generated in `myApp/config/connections.js`). Also, `config.model` is now `config.models`. Your app's default `connection` (i.e. database) should now be configured as a string `config.models.connection` used by default for model. New projects are generated with a `/config/models.js` file that includes the default connection. To configure a model to use specific adapters, you must now specify them in the `connection` key instead of `adapters`. For example: ```javascript module.exports = { connection: ['someMongoDatabase'], attributes: { name:{ type : 'string', required : true } } }; ``` ## Blueprints/Controller configuration The object literal describing controller configuration overrides for controller blueprints should change from: ``` ... _config: { blueprints: { rest: true, ... } } ``` to: ``` ... _config: { rest: true, ... } ``` ## Layout paths: In Sails v0.9, you could use the following syntax to specify `auth/someLayout.ejs` as a custom layout when rendering a view: ```javascript return res.view('auth/login',{ layout: 'someLayout' }); ``` However in Sails v0.10, all layout paths are relative to your app's views path. In other words, the relative path of the layout is no longer resolved from the view's own path-- it is now always resolved from the views path. This makes it easier to understand which file is being used, particularly when layout files have similar names: ```javascript return res.view('auth/login', { layout: 'auth/someLayout' }); ``` <docmeta name="displayName" value="0.10.0 Migration Guide"> <docmeta name="version" value="0.10.0"> ================================================ FILE: docs/version-notes/0.11.x/0.11.x.md ================================================ <docmeta name="displayName" value="0.11.x"> <docmeta name="version" value="0.11.0"> ================================================ FILE: docs/version-notes/0.11.x/MigrationGuide0.11.md ================================================ # v0.11 Migration Guide **tldr;** v0.11 comes with many minor improvements, as well as some internal cleanup in core. The biggest change is that Sails core is now using Socket.io v1. Almost none of this should affect the existing code in project, but there are a few important differences and new features to be aware of. We've listed them below. ## Differences #### Upgrade the Socket.io / Sails.io browser client Old v0.9 socket.io client will no longer work, so consequently you'll need to upgrade your sails.io.js client from v0.9 or v0.10 to v0.11. To do this, just remove your sails.io.js client and install the new one. We've bundled a new generator that will do this for you, assuming your sails.io.js client is in the conventional location at `assets/js/dependencies/sails.io.js` (i.e. if you haven't moved or renamed it): ```sh sails generate sails.io.js --force ``` #### `onConnect` lifecycle callback > **tldr;** > > Remove your `onConnect` function from `config/sockets.js`. The `onConnect` lifecycle callback has been deprecated. Instead, if you need to do something when a new socket is connected, send a request from the newly-connected client to do so. The purpose of `onConnect` was always for optimizing performance (eliminating the need to do this initial extra round-trip with the server), yet its use can lead to confusion and race conditions. If you desperately need to eliminate the server roundtrip, you can bind a handler directly on `sails.io.on('connect', function (newlyConnectedSocket){})` in your bootstrap function (`config/bootstrap.js`). However, note that this is discouraged. Unless you're facing _true_ production performance issues, you should use the strategy mentioned above for your "on connection" logic (i.e. send an initial request from the client after the socket connects). Socket requests are lightweight, so this doesn't add any tangible overhead to your application, and it will help make your code more predictable. #### `onDisconnect` lifecycle callback The `onDisconnect` lifecycle callback has been deprecated in favor of `afterDisconnect`. If you were using `onDisconnect` previously, you might have had to change the `session`, then call `session.save()` manually. In v0.11, this works in almost exactly the same way, except that `afterDisconnect` receives an additional 3rd argument: a callback function. This way, you can just call the provided callback when your `afterDisconnect` logic has finished, so that Sails can persist any changes you've made to the session automatically. Finally, as you might expect, you won't need to call `session.save()` manually anymore- it is now taken care of for you (just like `req.session` in a normal route, action, or policy.) > **tldr;** > Rename your `onDisconnect` function in `config/sockets.js` with the following: > > ``` > afterDisconnect: function (session, socket, cb) { > // Be sure to call the callback > return cb(); > } > ``` #### Other configuration in `config/sockets.js` Many of the configuration options in Socket.io v1 have changed, so you'll want to update your `config/sockets.js` file accordingly. + if you haven’t customized any of the options in `config/sockets.js` for your app, you can safely remove or comment out the entire file and let the Sails defaults do their magic. Otherwise, refer to the new [Sails sockets documentation](https://sailsjs.com/documentation/reference/configuration/sails-config-sockets) to ensure that your configuration is still valid and avoid unwanted hair loss. + if you are scaling to multiple servers in an environment that does *not support sticky sessions*, you'll need to set your `transports` to `['websocket']` in both `config/socket.js` and your client--see [our Scaling doc](https://sailsjs.com/documentation/concepts/deployment/scaling#?preparing-your-app-for-a-clustered-deployment) for more info. + if you were using a custom `authorization` function to restrict socket connections, you'll now want to use `beforeConnect`. `authorization` was deprecated by Socket.io v1, but `beforeConnect` (which maps to the `allowRequest` option from Engine.io) works just the same way. + if you were using other low-level socket configuration that was passed directly to socket.io v1, be sure and check out the [reference page on sailsjs.com](https://sailsjs.com/documentation/reference/configuration/sails-config-sockets) where all of the new configuration options are covered in detail. #### The "firehose" The "firehose" feature for testing with sockets has been deprecated. If you don't know what that means, you have nothing to worry about. The basic usage will continue to work for a while, but it will soon be removed from core and should not be relied upon in your app. This also applies to the following methods: + sails.sockets.subscribeToFirehose() + sails.sockets.unsubscribeFromFirehose() + sails.sockets.drink() + sails.sockets.spit() + sails.sockets.squirt() > If you want the "firehose" back, let [Mike know on twitter](http://twitter.com/mikermcneil) (it can be brought back as a separate hook). #### Config files in subfolders It has always been the intention that files in the Sails `config` folder have no precedence over each other, and that the filenames and subfolders (with the exception of `local.js` and the `env` and `locale` subfolders) be used merely for organization. However, in previous Sails versions, saving config files in subfolders would have the effect that the filename would be added as a key in `sails.config`, so that if you saved some config in `config/foo/bar.js`, then that config would be namespaced under `sails.config.bar`. This was unintentional and potentially confusing as 1) the directory name is ignored, and 2) moving the file would change the config key. This has been fixed in v0.11.x: config files in subfolders will be treated the same as those in the root `config` folder. If you are for some reason relying on the old behavior, you may set `dontFlattenConfig` to `true` in your `.sailsrc` file, but we would strongly recommend that you instead just namespace the config yourself by setting the desired key on `module.exports`; for example `module.exports.foo = {...}`. See [issue #2544](https://github.com/balderdashy/sails/issues/2544) for more details. #### Waterline now uses Bluebird As of v0.11, Waterline now supports Bluebird (instead of q) for promises. If you are using `.exec()` you won't be affected-- only if you are using `.then()`. See https://github.com/balderdashy/sails/issues/1186 for more information. ## New features Sails v0.11 also comes with some new stuff that we thought you'd like to know about: #### User-level hooks Hooks can now be installed directly from NPM. This means you can now install hooks with a single command in your terminal. For instance, consider the [`autoreload` hook](https://github.com/sgress454/sails-hook-autoreload) by [@sgress454](https://twitter.com/sgress454), which watches for changes to your backend code so you don't need to kill and re-lift the server every time you change your controllers, routes, models, etc. To install the `autoreload` hook, run: ```sh npm install sails-hook-autoreload ``` This is just one example of what's possible. As you might already know, hooks are the lowest-level pluggable abstraction in Sails. They allow authors to tap into the lift process, listen for events, inject custom "shadow" routes, and, in general, take advantage of raw access to the `sails` runtime. Most of the features you're familiar with in Sails have actually already been implemented as "core" hooks for over a year, including: + `blueprints` _(which provides the blueprint API)_ + `sockets` _(which provides socket.io integration)_ + `grunt` _(which provides Grunt integration)_ + `orm` _(which provides integration with the Waterline ORM, and imports your projects adapters, models, etc.)_ + `http` _(which provides an HTTP server)_ + and 16 others. You can read more about how to write your own hooks in the [new and improved "Extending Sails" documentation](https://sailsjs.com/documentation/concepts/extending-sails) on https://sailsjs.com. #### Socket.io v1.x The upgrade to Socket.io v1.0 shouldn't actually affect your app-level code, provided you are using the layer of abstraction provided by Sails itself; everything from the `sails.sockets.*` wrapper methods and "up" (resourceful pubsub, blueprints) If you are using underlying socket.io methods in your apps, or are just curious about what changed in Socket.io v1.0, be sure and check out the [complete Socket.io 1.0 migration guide](http://socket.io/docs/migrating-from-0-9/) from Guillermo and the socket.io team. #### Ever-increasing modularity As part of the upgrade to Socket.io v1.0, we pulled out the core `sockets` hook into a separate repository. This allowed us to write some modular, hook-specific tests for the socket.io interpreter, which will make things easier to maintain, customize, and override. This also allows the hook to grow at its own pace, and puts related issues in one place. Consider this a test of the pros and cons of pulling other hooks out of the sails core repo over the next few months. This will make Sails core lighter, faster, and more extensible, with fewer core dependencies, shorter "lift" time for most apps, and faster `npm install`s. #### Testing, the "virtual" request interpreter, and the `sails.request()` method In the process of pulling the `sockets` hook _out_ of core, the logic which interprets requests has been normalized and is now located _in_ Sails core. As a result, the `sails.request()` method is much more powerful. This method allows you to communicate directly with the request interpreter in Sails without lifting your server onto a port. It's the same mechanism that Sails uses to map incoming messages from Socket.io to "virtual requests" that have the familiar `req` and `res` streams. The primary use case for `sails.request()` is in writing faster-running unit and integration tests, but it's also handy for proxying to mounted apps (or "sub-apps"). For instance, here is an example (using mocha) of how you might test one of your app's routes: ```js var assert = require('assert'); var Sails = require('sails').Sails; before(function beforeRunningAnyTests (done){ // Load the app (no need to "lift" to a port) sails.load({ log: { level: 'warn' }, hooks: { grunt: false } }, function whenAppIsReady(err){ if (err) return done(err); // At this point, the `sails` global is exposed, although we // could have disabled it above with our config overrides to // `sails.load()`. In fact, you can actually use this technique // to set any configuration setting you like. return done(); }); }); after(function afterTestsFinish (done) { sails.lower(done); }); describe('GET /hotpockets', function (){ it('should respond with a 200 status code', function (done){ sails.request({ method: 'get', url: '/hotpockets', params: { limit: 10, sort: 'price ASC' } }, function (err, clientRes, body) { if (err) return done(err); assert.equal(clientRes.statusCode, 200); return done(); }); }); }); ``` #### `config/env/` subfolders In v0.10.x, we added the `config/env` folder (thanks to [@clarkorz](https://github.com/clarkorz)), where you can add config files that will be loaded only in the appropriate environment (e.g. `config/env/production.js` for production environment, `config/env/development` for development, etc.). In v0.11.x we've added the ability to specify whole subfolders per-environment. For example, *all* config files saved to the `config/env/production` will be loaded and merged on top of other configuration when the environment is set to `production`. Note that if both a `config/env/production` folder and a `config/env/production.js` file are present, the `config/env/production.js` settings will take precedence. And, as always, `local.js` is merged on top of all other files, and `.sailsrc` rules them all. ## Questions? As always, if you run into issues upgrading, or if any of the notes above don't make sense, let us know and we'll do what we can to clarify. Finally, to those of you that have contributed to the project since the v0.10 release in August: we can't stress enough how much we value your continued support and encouragement. There is a pretty massive stream of issues, pull requests, documentation tweaks, and questions, but it always helps to know that we're in this together :) Thanks. -[@mikermcneil](https://github.com/mikermcneil/), [@sgress454](https://github.com/sgress454/) and [@particlebanana](https://github.com/particlebanana/) <docmeta name="displayName" value="0.10 to 0.11 Migration Guide"> <docmeta name="version" value="0.11.0"> ================================================ FILE: docs/version-notes/0.12.x/0.12.x.md ================================================ <docmeta name="displayName" value="0.12.x"> <docmeta name="version" value="0.12.0"> ================================================ FILE: docs/version-notes/0.12.x/migration-guide-0.12.md ================================================ # Upgrading to Sails v0.12 Sails v0.12 comes with an upgrade to Socket.io and Express, as well as many bug fixes and performance enhancements. You will find that this version is mostly backwards compatible with Sails v0.11, however there are some major changes to `sails.sockets.*` methods which may or may not affect your app. Most of the migration guide below deals with those changes, so if you are upgrading an existing app from v0.11 and are using `sails.sockets` methods, please be sure and carefully read the information below in case it affects your app. Other than that, running `sails lift` in an existing project should just work. The sections below provide a high level overview of what's changed, major bug fixes, enhancements and new features, as well as a basic tutorial on how to upgrade your v0.11.x Sails app to v0.12. ## Installing the update Run the following command from the root of your Sails app: ```bash npm install sails@0.12.0 --force --save ``` The `--force` flag will override the existing Sails dependency installed in your `node_modules/` folder with Sails v0.12, and the `--save` flag will update your package.json file so that future npm installs will also use the new version. ## Things to do immediately after upgrading + If your app uses the `socket.io-redis` adapter, upgrade to at least version 1.0.0 (`npm install --save socket.io-redis@^1.0.0`). + If your app is using the Sails socket client (e.g. `assets/js/dependencies/sails.io.js`) on the front end, also install the newest version (`sails generate sails.io.js --force`) ## Overview of changes in v0.12 > For a full list of changes, see the changelog file for [Sails](https://github.com/balderdashy/sails/blob/master/CHANGELOG.md), as well as those for [Waterline](https://github.com/balderdashy/waterline/blob/master/CHANGELOG.md), [sails-hook-sockets](https://github.com/balderdashy/sails-hook-sockets/blob/master/CHANGELOG.md) and [sails.io.js](https://github.com/balderdashy/sails.io.js/blob/master/CHANGELOG.md). + Security enhancements: updated several dependencies with potential vulnerabilities + Reverse routing functionality is now built in to Sails core via the new [`sails.getRouteFor()`](https://sailsjs.com/documentation/reference/application/sails-get-route-for) and [`sails.getUrlFor()`](https://sailsjs.com/documentation/reference/application/sails-get-url-for) methods + Generally improved multi-node support (and therefore scalability) of low-level `sails.socket.*` methods, and made additional adjustments and improvements related to the latest socket.io upgrade. Added a much tighter Redis integration that sits on top of `socket.io-redis`, using a Redis client to implement cross-server communication rather than an additional socket client. + Cleaned up the API for `sails.socket.*` methods, normalizing overloaded functions and deprecating methods which cause problems in multiserver deployments (more on that below). + Added a few brand new sails.sockets methods: `.leaveAll()`, `.addRoomMembersToRooms()`, and `.removeRoomMembersFromRooms()` + `sails.sockets.id()` is now `sails.sockets.getId()` (backwards compatible w/ deprecation message) + New Sails apps are now generated with the updated version of `sails.io.js` (the JavaScript Sails socket client). This upgrade bundles the latest version of `socket.io-client`, as well as some more advanced functionality (including the ability to specify common headers for all virtual socket requests) + Upgraded to latest trusted versions of `grunt-contrib-*` dependencies (eliminates many NPM deprecation warnings and provides better error messages from NPM). + If you are using NPM v3, running `sails new` will now run `npm install` instead of symlinking your new app's initial dependencies. This is slower than you may be used to, but is a necessary change due to changes in the way NPM handles nested dependencies. The core maintainers are [working on](https://github.com/npm/npm/issues/10013#issuecomment-178238596) a better long-term solution, but in the mean time if you run `sails new` a lot and the slowdown is bugging you, consider temporarily downgrading to an earlier version of NPM (v2.x). If the installed version of NPM is < version 3, Sails will continue to take advantage of the classic symlinking strategy. ## Socket Methods Without question, the biggest change in Sails v0.12 is to the API of the low-level `sails.sockets` methods exposed by the `sockets` hook. In order to ensure that Sails apps perform flawlessly in a [multi-server (aka "multi-node" or "clustered") environment](https://sailsjs.com/documentation/concepts/realtime/multi-server-environments), several [low-level methods](https://sailsjs.com/documentation/reference/web-sockets/sails-sockets) have been deprecated, and some new ones have been added. The following `sails.sockets` methods have been deprecated: + [`.emit()`](https://0.12.sailsjs.com/documentation/reference/web-sockets/sails-sockets/sails-sockets-emit) + [`.id()`](https://0.12.sailsjs.com/documentation/reference/web-sockets/sails-sockets/sails-sockets-id) (renamed to [`.getId()`](https://sailsjs.com/documentation/reference/web-sockets/sails-sockets/get-id)) + [`.socketRooms()`](https://0.12.sailsjs.com/documentation/reference/web-sockets/sails-sockets/sails-sockets-socket-rooms) + [`.rooms()`](https://0.12.sailsjs.com/documentation/reference/web-sockets/sails-sockets/sails-sockets-rooms) + [`.subscribers()`](https://0.12.sailsjs.com/documentation/reference/web-sockets/sails-sockets/sails-sockets-subscribers) If you are using any of those methods in your app, they will still work in v0.12 but _you should replace them as soon as possible_ as they may be removed from Sails in the next version. See the individual doc pages for each method for more information. ## Resourceful PubSub Methods The [`.subscribers()`](https://sailsjs.com/documentation/reference/web-sockets/resourceful-pub-sub/subscribers) resourceful pubsub method has been deprecated for the same reasons as [`sails.sockets.subscribers()`](https://sailsjs.com/documentation/reference/web-sockets/sails-sockets/sails-sockets-subscribers). Follow the guidelines in the docs for replacing this method if you are using it in your code. ## Waterline (ORM) Updates Sails v0.12 comes with the latest version of the Waterline ORM (v0.11.0). There are two API changes to be aware of: ##### `.save()` no longer provides a second argument to its callback The callback to the `.save()` instance method no longer receives a second argument. While convenient, the requirement of providing this second argument made `.save()` less performant, especially for apps working with millions of records. This change resolves those issues by eliminating the need to build redundant queries, and preventing your database from having to process them. If there are places in your app where you have code like this: ```javascript sierra.save(function (err, modifiedSierra){ if (err) { /* ... */ return; } // ... }); ``` You should replace it with: ```javascript sierra.save(function (err){ if (err) { /* ... */ return; } // ... }); ``` ##### Custom column/field names for built-in timestamps You can now configure a custom column name (i.e. field name for Mongo/Redis folks) for the built-in `createdAt` and `updatedAt` attributes. In the past, the top-level `autoCreatedAt` and `autoUpdatedAt` model settings could be specified as `false` to disable the automatic injection of `createdAt` and `updatedAt` altogether. That _still works as it always has_, but now you can also specify string values for one or both of these settings instead. If a string is specified, it will be understood as the custom column (/field) name to use for the automatic timestamp. ```javascript { attributes: {}, autoCreatedAt: 'my_cool_created_when_timestamp', autoUpdatedAt: 'my_cool_updated_at_timestamp' } ``` If you were using the [workaround suggested by @sgress454 here](http://stackoverflow.com/a/24562385/486547), you may want to take advantage of this simpler approach instead. ## SQL Adapter Performance [Sails-PostgreSQL](https://github.com/balderdashy/sails-postgresql) and [Sails-MySQL](https://github.com/balderdashy/sails-mysql) recieved patch updates that significantly improved performance when populating associations. Thanks to [@jianpingw](https://github.com/jianpingw) for digging into the source and finding a bug that was processing database records too many times. If you are using either of these adapters, upgrading to `sails-postgresql@0.11.1` or `sails-mysql@0.11.3` will give you a significant performance boost. ## Contributing While not technically part of the release, Sails v0.12 is accompanied by some major improvements to the tools and resources available to contributors. More core hooks are now fully documented ([controllers](https://github.com/balderdashy/sails/tree/master/lib/hooks/controllers)|[grunt](https://github.com/balderdashy/sails/tree/master/lib/hooks/grunt)|[logger](https://github.com/balderdashy/sails/tree/master/lib/hooks/logger)|[cors](https://github.com/balderdashy/sails/tree/master/lib/hooks/cors)|[responses](https://github.com/balderdashy/sails/tree/master/lib/hooks/responses)|[orm](https://github.com/balderdashy/sails/tree/master/lib/hooks/orm)), and the team has put together a [Code of Conduct](https://github.com/balderdashy/sails/blob/master/CODE-OF-CONDUCT.md) for contributing to the Sails project. The biggest change for contributors is the [updated contribution guide](https://github.com/balderdashy/sails/blob/master/CONTRIBUTING.md), which contains the new, streamlined process for feature/enhancement proposals and for merging features, enhancements, and patches into core. As the Sails framework has grown (both the code base and the user base), it's become necessary to establish clearer processes for how issue contributions, code contributions, and contributions to the documentation are reviewed and merged. ## Documentation This release also comes with a deep clean of the official reference documentation, and some minor usability improvements to the online docs at [https://sailsjs.com/documentation](https://sailsjs.com/documentation). The entire Sails website is now available in [Japanese](http://sailsjs.jp/), and four other [translation projects](https://github.com/balderdashy/sails/tree/master/docs#in-other-languages) are underway for Korean, Brazilian Portugese, Taiwanese Mandarin, and Spanish. In addition, the Sails.js project (finally) has an [official blog](http://blog.sailsjs.com). The Sails.js blog is the new source for all longform updates and announcements about Sails, as well as for our related projects like Waterline, Skipper and the machine specification. ## Need Help? If you run into an unexpected issue upgrading your Sails app to v0.12.0, please review our contribution guide and [submit an issue in the Sails GitHub repo](https://github.com/balderdashy/sails/blob/master/CONTRIBUTING.md). <docmeta name="displayName" value="0.12 Migration Guide"> <docmeta name="version" value="0.12.0"> ================================================ FILE: docs/version-notes/0.8.x/0.8.x.md ================================================ <docmeta name="displayName" value="0.8.x"> <docmeta name="version" value="0.8.0"> ================================================ FILE: docs/version-notes/0.8.x/Changelog0.8.7x.md ================================================ # Changelog 0.8.7x ### 0.8.79 + Adapter definitions are no longer functions-- instead the direct definition object is accepted. This makes it easier, cleaner, and more declarative to create adapters. + Merged waterline into main Sails repo. + Brought in sails-util and sails-moduleloader, moved watelrine tests into top level. + Attribute values in models in result sets from Waterline are now cast to numbers, if they are number-looking strings. + Substantial refactoring of waterline model-augmentation logic. + Added TODO for asynchronous module loading for future. + Upgraded waterline-dirty dep. ### 0.8.77 + Patch updates the waterline-dirty dependency to deal with an issue with that adapter returning objects which map directly to the in-memory database (was causing changes made to found models to be persisted without calling `.save()`) <docmeta name="displayName" value="0.8.7x Changelog"> <docmeta name="version" value="0.8.7"> ================================================ FILE: docs/version-notes/0.8.x/Changelog0.8.8x.md ================================================ # Changelog 0.8.8x ### 0.8.80 + Refactored app layout to make it a bit more straightforward. To check out the the new folder structure, make a new project with `sails new foo` + Added robot.txt in new app generation + Bound all methods in adapter to have the right context. ### 0.8.82 _Sunday, February 24, 2013_ + Bootstrap function fires warning if callback not triggered after a few seconds (thanks [@virpool](https://github.com/virpool)) + Bug fixes w/ pubsub/model convenience methods. ### 0.8.83 _Saturday, March 2, 2013_ + Support for streaming large datasets from models `(e.g. User.stream().pipe(res);)` + Bug fix for chains of multiple policies (thanks [@themouette](https://github.com/themouette)) + Jade template support (thanks [@valinorsgatekeeper](https://github.com/valinorsgatekeeper) + AssetRack integration for more robust css/js/template/LESS management, replaces Rigging (thanks [@techpines](https://github.com/techpines)) + Fixed some docs /refactored (thanks @slantzjr) + Bundled excruciatingly simple "authenticated" policy in new projects + Made "redirect" work in API scaffolds + Renamed waterline-* adapter modules as sails-*. Added backwards compat. + Added .gitkeep in all directories when generating new projects to make sure they get committed + Bootstrap and log config now available in project template + View config now available in new projects as 'config/views.js' + Better error checking in the `sails` CLI + Docs + Added app.js file back in, but this time hidden as '.app.js'. It can be run however you like, or you can use npm debug to debug it. To run daemonized, you can use `forever start .app.js` + Added notion of `sails.explicitHost` to track whether a host was explicitly specified. If it was not, Express takes the approach of accepting `all connections via INADDR_ANY` (see [http://expressjs.com/2x/guide.html#app.listen()](http://expressjs.com/2x/guide.html#app.listen())) Now, if you specify `sails.config.host`, `sails.explicitHost` gets set, and Express will start the server deliberately using the host you specify. In certain PaaS deployments, this is required. For instance, this was causing problems in an Openshift deployment environment (big thanks to @hypereive for figuring that out). ### 0.8.84 _Saturday, March 2, 2013_ + Bug fixes: (explicit hosts, and included an additional file in new app generation) ### 0.8.85 _Sunday, March 3, 2013_ + Check for and warn if port is currently being used on lift, with support for explicit hosts [https://github.com/balderdashy/sails/issues/197](https://github.com/balderdashy/sails/issues/197)) + Model.stream() support over socket.io [https://github.com/balderdashy/sails/issues/196](https://github.com/balderdashy/sails/issues/196)) ### 0.8.86 _Monday, March 4, 2013_ + Patch to allow for easier SSL configuration. ### 0.8.87 _Monday, March 4, 2013_ + Patch fixes updates sails-dirty version which fixes sorting by date ### 0.8.88 + Adds coffeescript support on the front-end in dev and production environments via [asset-rack](https://github.com/techpines/asset-rack) [@techpines](thanks https://github.com/techpines)!) ### 0.8.892 + Front-end CoffeeScript support in AssetRack (thanks [@techpines](https://github.com/techpines)!) + Chained policy support + New styles for default home page (thanks [@egdelwonk](https://github.com/egdelwonk)!) + Windows compat. fix (thanks [@feroc1ty](https://github.com/feroc1ty)!) + Support for string IDs (thanks [@tedkulp](https://github.com/tedkulp)!) + Attribute scaffolding for model generation (thanks [@Tidwell](https://github.com/Tidwell)) + Support for big int string conversion in ID normalization (thanks [@d4mn](https://github.com/d4mn)!) ### 0.8.895 + Policies: Fixed the "*" route for controllers. + Policies: The "*" policy can now be set to false + Collections: Type restrictions are cleaner + Adapters: Default was changed to memory due to an issue with node-dirty + Log: sails.config.log.level is passed to socket.io + Assets: Bug fixed: not calling next when compiling LESS with syntax (thanks vicapow) + Assets: Typescript supported on front-end (thanks Diullei) + Assets: Meaningful LESS errors were added (thanks vicapow) <docmeta name="displayName" value="0.8.8x Changelog"> <docmeta name="version" value="0.8.8"> ================================================ FILE: docs/version-notes/0.8.x/Changelog0.8.9.md ================================================ # Changelog 0.8.9 _April 9, 2013_ + Controllers must now also be generated to use the default API (they can be empty) + Haml template support on back-end for new projects (thanks [@dcbartlett](https://github.com/dcbartlett)) + default values in models (defaultsTo) + Chained policies fixed + Removed all reference to blueprints as "scaffolds". Blueprints are more than temporary placeholders-- they are the preferred method of serving an API from your app. + Refactored most of the code base + Removed CRUD synonyms + Main: Compatibility with Node v0.10.0 (patches node-dirty) + Main: Fixed crash that happened when absolute path was given as appPath + Assets: Added more logging features for LESS. + Assets: Reset.css now in mixins + Assets: LESS assets are deligated to Rack.LessAsset + Assets: LESS assets served from asset-rack will have their extensions changed to css + Policies: Implemented the controller syntax for defining a policy. + Naming: scaffolds is now known as blueprints + Naming: blueprints is now known as boilerplates + Routing: Added controller.action syntax + Routing: Removed CRUD Synonyms-- now you must explicitly use find, findAll, create, destroy, update (can't use `get`, `detail`, `delete`, `edit`, etc. to indicate the same thing. Turns out this was actually annoying, not helpful) + Routing: Fix in API blueprint for regression around PUT/DELETE automatic RESTful routes + Routing: Fix for resourceful routing. /model/[id] didn't work with verbs. It now does. + Config: _ and async no longer have to be global (but they are by default) They are configurable with `sails.config.globals._` and `sails.config.globals.async` (thanks [@particlebanana](https://github.com/particlebanana)!) + New sails project can now be created in the current dir with `sails new .` (thanks [@collinwren](https://github.com/collinwren)!) + More tests (thanks [@collinwren](https://github.com/collinwren) and [@benrudolph](https://github.com/benrudolph)) + Travis CI integration (thanks [@collinwren](https://github.com/collinwren)!) <docmeta name="displayName" value="0.8.9 Changelog"> <docmeta name="version" value="0.8.9"> ================================================ FILE: docs/version-notes/0.8.x/ChangelogPre-0.8.77.md ================================================ # Changelog <0.8.77 + I wasn't keeping good notes, sorry. :( + Check out <a target="_blank" href="https://github.com/balderdashy/sails/commits/master">https://github.com/balderdashy/sails/commits/master</a> if you want to dive in. <docmeta name="displayName" value="Pre-0.8.77 Changelog"> <docmeta name="version" value="0.8.0"> ================================================ FILE: docs/version-notes/0.9.x/0.9.x.md ================================================ <docmeta name="displayName" value="0.9.x"> <docmeta name="version" value="0.9.0"> ================================================ FILE: docs/version-notes/0.9.x/Changelog0.9.0.md ================================================ # Changelog 0.9.0 _July 10, 2013_ ### Sails.js + Main: Express 3.x has been integrated. + Main: CSRF Attack Protection was added as part of the core. Uses express-csrf, plus a token-based approach for SPAs and embedded apps (Chrome extensions, JavaScript plugins). + Main: Most of the core has been refactored for performance, code clarity, and simplicity to make contributions easier. + Main: Most of the core has been pulled into hooks. In a subsequent patch release for 0.9.x, this process will make Socket.io optional. + Controllers: Automatic routing is now disable-able. + Assets: Grunt integration replaces Asset Rack. + Assets: Public folder removed from new projects. + Assets: Temporary 'public' folder is automatically built on lift, using the contents of the assets folder. + Assets: Static assets can be compiled with "sails build" for external hosting of front-end assets + Assets: Grunt ecosystem allows for a [wide variety](https://github.com/gruntjs/grunt-contrib) of front-end template/css/js preprocessor support (sass, hbs, stylus, dust, typescript, etc.) + Routing: Automatic 404 and 500 routing is replaced. + Assets: Asset bundling is now disabled by default, use `sails new foo --linker` to enable it + Config: Most configuration is now also explicit in new projects. Defaults are still provided underneath. + Sockets: Socket.IO can now be configured with the options detailed in config/sockets.js. + Sockets: Built-in support for Redis MQ-- allows you to scale realtime apps to a multi-instance deployment without necessitating sticky sessions at your load balancer. + Views: Express 3 killed support for layouts/view partials. Sails has been extended to maintain support for them with ejs and jade, but otherwise you are limited to what is supported by the engine itself. + Views: Automatic routing to views is now disable-able. + Sessions: Built-in support for Redis and Mongo sessions for scaling your app to multi-instance deployments. ### Waterline + ORM: Waterline has been pulled out of Sails.js... Again. (See [Waterline](https://github.com/balderdashy/waterline)) + ORM: Model attributes now support validations. (See [Anchor](https://github.com/balderdashy/anchor)) + ORM: Custom instance methods can now be defined on models as virtual attributes. + ORM: Lifecycle Callbacks have been added. (See [Lifecycle Callbacks](https://github.com/balderdashy/sails-docs/tree/0.9)) + ORM: findAll() has been replaced with find(). + ORM: find() has been replaced with findOne(). + ORM: .done() promise now works on all ORM methods + ORM: Complete support for the Promise specificiation has been added. ### Anchor + Validations: Too many added to list, see [Validations](https://github.com/balderdashy/sails-docs/tree/0.9) <docmeta name="displayName" value="0.9.0 Changelog"> <docmeta name="version" value="0.9.0"> ================================================ FILE: docs/version-notes/0.9.x/Changelog0.9.16.md ================================================ # Changelog 0.9.16 _December 16, 2013_ + [@sgress454](https://github.com/sgress454) Hotfix for CORS issue when no Origin header is present. … f42da3c + [@mikermcneil](https://github.com/mikermcneil) Update README.md 1bf1d15 + [@bicherele](https://github.com/bicherele) Update es.json … ddb9a07 + [@andyzhau](https://github.com/andyzhau) Fix the join room variable reference error. 57783a3 + [@mikermcneil](https://github.com/mikermcneil) Use npm version of linker 5459119 + [@mikermcneil](https://github.com/mikermcneil) Update CHANGELOG.md aae737f + [@devel-pa](https://github.com/devel-pa) lodash library updated to last version available (2.4.1) c71ce81 + [@mikermcneil](https://github.com/mikermcneil) Hot fix to protect connect cookie parsing. e181656 + [@mikermcneil](https://github.com/mikermcneil) Rebased from #1012 as hotfix for windows view issue. 031ebe1 <docmeta name="displayName" value="0.9.16 Changelog"> <docmeta name="version" value="0.9.16"> ================================================ FILE: docs/version-notes/0.9.x/Changelog0.9.4.md ================================================ # Changelog 0.9.4 _September 5, 2013_ + Improved CSRF prevention support (thanks to [@sgress454](https://github.com/sgress454)) + Support for CORS (thanks to [@sgress454](https://github.com/sgress454)) + CoffeeScript supported client-side by default in gruntfile thanks to @reecelewellen + Improves/fixes internationalization (thanks to [@xdissent](https://github.com/xdissent) and [@silvinci](https://github.com/silvinci)) + Removed vanilla HAML support and tests since it was incomplete (jade is still supported) + Config: Sails core is no longer automatically copied as a dependency during `sails new`. This speeds up the process significantly and avoids occassional recursive copy death spirals. + Config: Added explicit `--port` option to `sails lift`. + Sockets: Added query string parsing to requests. + Sockets: Headers can now be specified in requests (**_This has implications on full compatibility w/ most Express middleware!_**) + Routing: Fixed issues with default 404 and 500 responses. + Other minor bug fixes/inconsistencies and documentation enhancements > And thanks a ton to anybody I left out! Send me a message on twitter and I'll add you. <docmeta name="displayName" value="0.9.4 Changelog"> <docmeta name="version" value="0.9.4"> ================================================ FILE: docs/version-notes/0.9.x/Changelog0.9.7.md ================================================ # Changelog 0.9.7 _October 10, 2013_ + Complete improvement/refactoring of configuration loader (fixes bugs) + Complete improvement/refactoring of ORM loader (fixes bugs) + Continued improvements of tests + Include a modified version of consolidate to better support view engines + Blueprints are now configurable per-controller (thanks [@xdissent](https://github.com/xdissent), and everyone else who helped!) + (waiting to expose this and deprecate the old behavior in the docs until the next minor release to avoid causing any breaking changes) + New `prefix` option in global blueprint config, as well as per-controller. + New `jsonp` option in global controller config, as well as per-controller. + New `pluralize` option in global controller config, as well as per-controller. + Models can now easily use one or more custom named connections which use different adapters + (waiting to expose this and deprecate the old behavior in the docs until the next minor release to avoid causing any breaking changes) + Adds configurable default behavior for 403/404/500/400 HTTP status code error cases. + (waiting to expose this and deprecate the old behavior in the docs until the next minor release to avoid causing any breaking changes) + Properly namespace the io in bundled sails.io.js client in new projects (thanks [@drosen0](https://github.com/drosen0)) + Better handle crash scenario, particularly in nodemon (thanks [@edy](https://github.com/edy)) > Thanks to everyone else I missed, and to everyone else who helped out with this release! <docmeta name="displayName" value="0.9.7 Changelog"> <docmeta name="version" value="0.9.7"> ================================================ FILE: docs/version-notes/1.0.x/migration-guide-1.0.md ================================================ > The 1.0 migration guide now lives in the 'Upgrading' section, [here](https://github.com/balderdashy/sails/blob/master/docs/upgrading/To1.0.md). ================================================ FILE: errors/README.md ================================================ # errors/ > FUTURE: > 1. Fold in messages inline instead of using the stringfile (`sails-stringfile` still isn't worth it, at least not until post-v1) > 2. ~~Ideally we don't call `process.exit()` at all- instead, consistently call sails.lower(). See comment at bottom of `fatal.js`.~~ That's ok sometimes actually (We have process handlers anyway.) The issue is that we really need to inline these, because it all depends on the context. > 3. Inline these errors where they're being used. Then this directory can be deleted. ================================================ FILE: errors/fatal.js ================================================ /** * Module dependencies */ var nodeutil = require('util'); var CaptainsLog = require('captains-log'); // Once per process: // Build logger using best-available information // when this module is initially required. var rconf = require('../lib/app/configuration/rc')(); var log = CaptainsLog(rconf.log); /** * Fatal Errors */ module.exports = { // Lift-time and load-time errors failedToLoadSails: function(err) { log.error(); // If the error is something the user can fix (as opposed to an internal Sails error AKA bug), // just show the error message, not the whole stack trace into Sails core. if (err.name && err.name === 'userError') { log.error(err.message); } else { // Dont log stack trace if this is a recognized load/lift-time error // (& also comes from a core hook) switch (err.code) { case 'include-all:COULD_NOT_REQUIRE': case 'E_COULD_NOT_LOAD_ADAPTER': case 'E_ADAPTER_NOT_INSTALLED': case 'E_BIND_ERR': log.error(err.message); break; default: log.error(err); } } console.error(); log.error('Could not load Sails app.'); log.error(); log.error('Tips:'); log.error(' • First, take a look at the error message above.'); log.error(' • Make sure you\'ve installed dependencies with `npm install`.'); log.error(' • Check that this app was built for a compatible version of Sails.'); log.error(' • Have a question or need help? (http://sailsjs.com/support)'); _terminateProcess(1); }, noPackageJSON: function() { log.error('Cannot read package.json in the current directory (' + process.cwd() + ')'); log.error('Are you sure this is a Sails app?'); _terminateProcess(1); }, notSailsApp: function() { log.error('The package.json in the current directory does not list Sails as a dependency...'); log.error('Are you sure `' + process.cwd() + '` is a Sails app?'); _terminateProcess(1); }, badLocalDependency: function(pathToLocalSails, requiredVersion) { log.error( 'The local Sails dependency installed at `' + pathToLocalSails + '` ' + 'has a corrupted, missing, or un-parsable package.json file.' ); log.error('You may consider running:'); log.error('rm -rf ' + pathToLocalSails + ' && npm install sails@' + requiredVersion); _terminateProcess(1); }, // FUTURE: inline this error // app/loadHooks.js:42 malformedHook: function() { log.error('Malformed hook!'); log.error('Hooks should be a function with one argument (`sails`)'); _terminateProcess(1); }, // FUTURE: inline this error // app/load.js:146 hooksTookTooLong: function() { var hooksTookTooLongErr = 'Hooks are taking way too long to get ready... ' + 'Something might be amiss.\nAre you using any custom hooks?\nIf so, make sure the hook\'s ' + '`initialize()` method is triggering its callback.'; log.error(hooksTookTooLongErr); process.exit(1); }, // Invalid user module errors invalidCustomResponse: function(responseIdentity) { log.error('Cannot define custom response `' + responseIdentity + '`.'); log.error('`res.' + responseIdentity + '` has special meaning in Connect/Express/Sails.'); log.error('Please remove the `' + responseIdentity + '` file from the `responses` directory.'); _terminateProcess(1); }, __UnknownPolicy__: function(policy, source, pathToPolicies) { source = source || 'config.policies'; log.error('Unknown policy, "' + policy + '", referenced in `' + source + '`.'); log.error('Are you sure that policy exists?'); log.error('It would be located at: `' + pathToPolicies + '/' + policy + '.js`'); return _terminateProcess(1); }, __InvalidConnection__: function(connection, sourceModelId) { log.error('In model (' + sourceModelId + '), invalid connection ::', connection); log.error('Must contain an `adapter` key referencing the adapter to use.'); return _terminateProcess(1); }, __UnknownConnection__: function(connectionId, sourceModelId) { log.error('Unknown connection, "' + connectionId + '", referenced in model `' + sourceModelId + '`.'); log.error('Are you sure that connection exists? It should be defined in `sails.config.connections`.'); // var probableAdapterModuleName = connectionId.toLowerCase(); // if ( ! probableAdapterModuleName.match(/^(sails-|waterline-)/) ) { // probableAdapterModuleName = 'sails-' + probableAdapterModuleName; // } // log.error('Otherwise, if you\'re trying to use an adapter named `' + connectionId + '`, please run ' + // '`npm install ' + probableAdapterModuleName + '@' + sails.majorVersion + '.' + sails.minorVersion + '.x`'); return _terminateProcess(1); }, __ModelIsMissingConnection__: function(sourceModelId) { log.error(nodeutil.format('One of your models (%s) doesn\'t have a connection.', sourceModelId)); log.error('Do you have a default `connection` in your `config/models.js` file?'); return _terminateProcess(1); }, __UnknownAdapter__: function(adapterId, sourceModelId /*, sailsMajorV, sailsMinorV */) { log.error('Trying to use unknown adapter, "' + adapterId + '", in model `' + sourceModelId + '`.'); log.error('Are you sure that adapter is installed in this Sails app?'); log.error('If you wrote a custom adapter with identity="' + adapterId + '", it should be in this app\'s adapters directory.'); var probableAdapterModuleName = adapterId.toLowerCase(); if (!probableAdapterModuleName.match(/^(sails-|waterline-)/)) { probableAdapterModuleName = 'sails-' + probableAdapterModuleName; } log.error('Otherwise, if you\'re trying to use an adapter named `' + adapterId + '`, please run ' + '`npm install ' + probableAdapterModuleName + ' --save'/*'@' + sailsMajorV + '.' + sailsMinorV + '.x`'*/); return _terminateProcess(1); }, __InvalidAdapter__: function(attemptedModuleName, supplementalErrMsg) { log.error('There was an error attempting to require("' + attemptedModuleName + '")'); log.error('Is this a valid Sails/Waterline adapter? The following error was encountered ::'); log.error(supplementalErrMsg); return _terminateProcess(1); } }; // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // FUTURE: Make all of this more elegant (see the info in errors/README) // (involves getting rid of this file) // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /** * _terminateProcess * * Terminate the process as elegantly as possible. * If process.env is 'test', throw instead. * * @param {[type]} code [console error code] * @param {[type]} opts [currently unused] */ function _terminateProcess(code /*, opts */) { // FUTURE: get rid of this (actual handling will be inline where the fatal errors are // actually coming from, so we'll be able to handle it there by actually throwing. // That way, it's up to the caller whether it wants to catch the original error and // do a deliberate process.exit and omit the error stack (which can be disorienting // for folks new to SSJ/Node.js)) // // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // TODO: Double-check on this, and then remove it if possible: // (I'm pretty sure we can get rid of this now b/c all tests have been updated. // We should definitely never be checking that NODE_ENV is or isn't anything other // than "production") if (process.env.NODE_ENV === 'test') { throw new Error({ type: 'terminate', code: code, // options: { // todo: 'put the stuff from the original errors in here' // } // ^^ Removed this in Sails v1 since it was useless anyways. ~Mike Dec 11, 2016 }); }//-• // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - return process.exit(code); } ================================================ FILE: errors/index.js ================================================ // Merge together error sub-modules module.exports = { fatal: require('./fatal'), warn: require('./warn') }; ================================================ FILE: errors/warn.js ================================================ /** * Module dependencies */ var nodepath = require('path'); var CaptainsLog = require('captains-log'); // Once per process: // Build logger using best-available information // when this module is initially required. var rconf = require('../lib/app/configuration/rc')(); var log = CaptainsLog(rconf.log); /** * Warnings */ module.exports = { incompatibleLocalSails: function(requiredVersion, localVersion) { log.warn('Trying to lift app using a local copy of `sails`'); log.warn('(located in ' + nodepath.resolve(process.cwd(), 'node_modules/sails') + ')'); log.warn(); log.warn('But the package.json in the current directory indicates a dependency'); log.warn('on Sails `' + requiredVersion + '`, and the locally installed Sails is `' + localVersion + '`!'); log.warn(); log.warn('If you run into compatibility issues, try installing ' + requiredVersion + ' locally:'); log.warn(' $ npm install sails@' + requiredVersion); log.warn(); log.blank(); }, // Verbose-only warnings: noPackageJSON: function() { log.warn('Cannot read package.json in the current directory (' + process.cwd() + ')'); log.warn('Are you sure this is a Sails app?'); log.warn(); }, notSailsApp: function() { log.warn('The package.json in the current directory does not list Sails as a dependency...'); log.warn('Are you sure `' + process.cwd() + '` is a Sails app?'); log.warn(); }, badLocalDependency: function(pathToLocalSails, requiredVersion) { log.warn( 'The local Sails dependency installed at `' + pathToLocalSails + '` ' + 'has a corrupted, missing, or un-parsable package.json file.' ); log.warn('You may consider running:'); log.warn('rm -rf ' + pathToLocalSails + ' && npm install sails@' + requiredVersion); log.warn(); } }; ================================================ FILE: lib/EVENTS.md ================================================ # Core Events ## Status > ##### Stability: [2](http://nodejs.org/api/documentation.html#documentation_stability_index) - Unstable > > The API is in the process of settling, but has not yet had sufficient real-world testing to be considered stable. > > Backwards-compatibility will be maintained if reasonable. ## Purpose The instantiated `sails` object is a Node EventEmitter. > WARNING > `sails.on(*)` events are for contributors to the core or developers building custom hooks. > > **Please do not use these events directly in your app. You have been warned!** ### Background Events have been a feature of the Sails core since v0.9. + Developers needed an easier way to modify the Sails core for their needs. Hooks and events make this possible! + Events themselves originated as a feature to allow hooks to talk to each other during and after the app bootstrapping process. + For posterity, the original `sails.on(*)` event proposal: https://gist.github.com/mikermcneil/5898598 ### Best Practices Although it can be tempting, it's really best not to add new events to `sails` in your app code. In general, consistent conventions, clarity, and simplicity are the best practice for developing apps, because it makes them easier to extend, and makes it easier for you to remember how everything works when you come back to it later (not to mention everyone else on your team!) If you want to add/trigger events to monkeypatch your Sails core, it's best to do this by authoring a hook. More information will show up as we learn more about best practices around that process, but one thing we've definitely learned is that you're better off namespacing your events and firing them on a single object (`sails`), than emitting and listening on different objects. Why? Sometimes objects get deleted or copied, and this can make a big mess. If you need a special event in your hook, you *will* want to namespace it. For instance, if I'm adding a hook called `enforceRestfulSesssions` that limits the actions that can be added to controllers to encourage code consistency, I might have a `hook:enforceRestfulSesssions:checked` event that fires when all of the controllers have been checked. This is so that other hooks that know about `enforceRestfulSesssions` can wait until it has finished its check before proceeding (whether it's just me, or other people on my team, or if I release my hook and it gets popular, other people in the Sails community). In my hook's initialize method, I might have the following: ```javascript // Wait until all the middleware from this app's controllers have loaded sails.after('hook:controllers:loaded', function () { // Do stuff // e.g. prevent any methods called `login`, `logout` or `signup` // since we've opted organizationally for using CRUD on a SessionController instead // .....code here........ // When you're done, fire an event in sails.emit('hook:enforceRestfulSesssions:checked'); }); ``` ## Reference ### Lifecycle ##### `lifted` Called after drawing the sailboat. ##### `ready` Called when all hooks are loaded and the internal router is ready to handle requests. i.e. the HTTP hook listens for `ready` before binding its HTTP server. ##### `lower` Called when `sails.lower()` is called. `sails.lower()` is called automatically when the process is halted. ##### `router:before` Called before any of the app's configured static routes have been bound. i.e. a hook might listen to this event to bind some middleware. ##### `router:after` Called after all of the app's configured static routes have been bound. i.e. a hook might listen to this event to bind a "shadow route" to a blueprint. ##### `router:done` Called when all routes have been bound, including those originating from hooks (i.e. things listening for `router:after`.) ##### `router:reset` Called when the router is flushed (i.e. all routes are unbound). The `http` hook (i.e. Express), and any other attached servers which maintain their own routes should listen for this event so they know to unbind their private routes. ### Lift-time ##### `router:bind` Called when a route is bound. This allows hooks to handle routes directly if they want to- Should receive a single argument, "routeObj", which looks like: ``` { path: 'String', target: function theFnBoundtoTheRoute (req, res, next) {}, verb: 'String', options: 'Object' } ``` ##### `router:unbind` Called when a route is unbound. ### Runtime > NOTE: these events should only be relied on by attached servers without their own routers, or when a hook > implementation prefers to use the built-in Sails router. > > The optimal behavior for the http hook implemented on Express, for instance, is to listen to `router:bind` > from the built-in router and listen for the routes itself using `app.use`. On the other hand, in the `sockets` hook, > Socket.io needs to use the `router:request` event to simulate a connect-style router since it > can't bind dynamic routes ahead of time. ##### `router:request` Called when a request is received by the Sails router. Should receive three arguments, `req`, `res`, and `next`. ##### `router:request:500` Absolute last-resort handler for server errors. Called when a request encounters an error and isn't handled by other means. Should receive three arguments, `err`, `req`, and `res`. ##### `router:request:404` Absolute last-resort handler for requests which don't match any routes. Called when a request doesn't match any routes (or shadow routes, including 404/slug handlers), and this case isn't handled by other means. Should receive two arguments, `req`, and `res`. ##### `router:route` Called every time a request is routed. Compare with `router:request`- e.g.: ``` sails.router.bind('/foo/*', noop); sails.router.bind('/foo/:bar', noop); sails.router.bind('/foo/explicit', noop); // Request to /foo/x will emit `router:request` only once, but `router:route` three times. ``` ## Usage #### `sails.on()` Fires your handler **NEXT TIME** the event is triggered and **EVERY TIME AFTERWARD**. ```javascript sails.on('hook:yourHookID:someEvent', function yourEventHandler ( /* a, b, c, ..., z */ ) { // your implementation }); ``` #### `sails.once()` Fires your handler **NEXT TIME** the specified event is triggered, and then stop listening. ```javascript sails.once('hook:yourHookID:someEvent', function yourEventHandler ( /* a, b, c, ..., z */ ) { // your implementation }); ``` #### `sails.after()` Fires your handler **IF THE SPECIFIED EVENT HAS ALREADY BEEN TRIGGERED** or **WHEN IT IS TRIGGERED**. Kind of like jQuery's `$(document).ready()`, except `document` is whatever you want. Useful for checking whether some state has been achieved yet. ```javascript sails.after('hook:yourHookID:someEvent', function yourEventHandler ( /* a, b, c, ..., z */ ) { // your implementation }); ``` You can actually wait for several events using `.after` as well: ```javascript sails.after(['hook:yourHookID:someEvent', 'hook:someOtherHookID:someOtherEvent'], function yourEventHandler ( /* a, b, c, ..., z */ ) { // your implementation }); ``` <!-- This can be omitted for now- it really shouldn't be used in userspace. May be deprecated, API may change. Please do not use. #### sails.emit Emit the specified event with the specified arguments to all listeners. ```javascript sails.emit('hook:yourHookID:someEvent', 'arbitrary', 'number', {of: 'arguments'}, ['allowed']); ``` --> ## FAQ > If you have a question that isn't covered here, please feel free to send a PR adding it to this section (even if you don't have the answer!) ================================================ FILE: lib/README.md ================================================ # Understanding Core Welcome to the Sails.js Core. The goal of this file is to provide a light overview of the structure and philosophy of Sails.js for core contributors, as well as establish code and documentation conventions. Many of the subdirectories herein contain a `README.md` file with more information about that particular component. ## Overview The Sails.js core runs when an app is fired up with `sails.load` or `sails.lift`. ## Stability Index See the [Sails Project Stability Index](https://github.com/balderdashy/sails/blob/master/docs/contributing/stability-index.md) for more information. We use a slight variation of the [stability index] used by [Node.js core](http://nodejs.org/api/documentation.html#documentation_stability_index); partially out of allegiance, but mostly for consistency. ## FAQ > If you have an unanswered question that isn't covered here, and that you feel would add value for the community, please feel free to send a PR adding it to this section. ================================================ FILE: lib/app/README.md ================================================ # App Lifecycle ## API Status > ##### Stability: [3](http://nodejs.org/api/documentation.html#documentation_stability_index) - Stable ## Purpose The `app` directory contains logic concerned with the lifecycle of the Sails core itself. This includes: + Loading and initializing hooks + Loading the router + Populating middleware library + Teardown and cleanup of the currently-running instance of sails ## Loading Steps The Sails core has been iterated upon several times to make it easier to maintain and extend. As a result, it has a very particular loading order, which its hooks depend on heavily. This process is summarized below. #### Prepare Configuration Object Populate `sails.config` with core (hook-agnostic) implicit defaults. Then apply the initial known set of configuration overrides, including command-line options, environment variables, and programmatic configuration (i.e. options passed to `sails.load` or `sails.lift`.) The most important core implicit default configuration is the set of built-in hooks. #### Load Hooks Load hooks in the proper order. #### Populate Middleware Registry Grab `this.middleware` from each hook and make it available on the `sails` object as `sails.middleware.[HOOK_ID]`. #### Assemble Router Prepare the core Router, then emit multiple events on the `sails` object informing hooks that they can safely bind routes. #### Expose global variables After all hooks have initialized, Sails exposes global variables (by default: `sails` object, models, services, `_`, and `async`) #### Initialize App Runtime > This step does not run when `sails.load()` is used programmatically. > To also run the initialization step, use `sails.lift()` instead. + Start attached servers (by default: Express and Socket.io) + Run the bootstrap function (`sails.config.bootstrap`) ## FAQ + What is the difference between `sails.lift()` and `sails.load()`? + `lift()` === `load()` + `initialize()`. It does everything `load()` does, plus it starts any attached servers (e.g. HTTP) and logs a picture of a boat. > If you have a question that isn't covered here, please feel free to send a PR adding it to this section (even if you don't have the answer!) ================================================ FILE: lib/app/Sails.js ================================================ /** * Module dependencies. */ var util = require('util'); var events = require('events'); var _ = require('@sailshq/lodash'); var CaptainsLog = require('captains-log'); var loadSails = require('./load'); var mixinAfter = require('./private/after'); var __Router = require('../router'); /** * Construct a Sails (app) instance. * * @constructor */ function Sails() { // Inherit methods from EventEmitter events.EventEmitter.call(this); // Remove memory-leak warning about max listeners // See: http://nodejs.org/docs/latest/api/events.html#events_emitter_setmaxlisteners_n this.setMaxListeners(0); // Keep track of spawned child processes this.childProcesses = []; // Ensure CaptainsLog exists this.log = CaptainsLog(); // Keep a hash of loaded actions this._actions = {}; // Keep a hash of loaded action middleware this._actionMiddleware = {}; // Build a Router instance (which will attach itself to the sails object) __Router(this); // Mixin `load()` method to load the pieces // of a Sails app this.load = loadSails(this); // Mixin support for `Sails.prototype.after()` mixinAfter(this); // Bind `this` context for all `Sails.prototype.*` methods this.load = _.bind(this.load, this); this.request = _.bind(this.request, this); this.lift = _.bind(this.lift, this); this.lower = _.bind(this.lower, this); this.initialize = _.bind(this.initialize, this); this.exposeGlobals = _.bind(this.exposeGlobals, this); this.runBootstrap = _.bind(this.runBootstrap, this); this.isLocalSailsValid = _.bind(this.isLocalSailsValid, this); this.isSailsAppSync = _.bind(this.isSailsAppSync, this); this.inspect = _.bind(this.inspect, this); this.toString = _.bind(this.toString, this); this.toJSON = _.bind(this.toJSON, this); this.all = _.bind(this.all, this); this.get = _.bind(this.get, this); this.post = _.bind(this.post, this); this.put = _.bind(this.put, this); this['delete'] = _.bind(this['delete'], this); this.getActions = _.bind(this.getActions, this); this.registerAction = _.bind(this.registerAction, this); this.registerActionMiddleware = _.bind(this.registerActionMiddleware, this); this.reloadActions = _.bind(this.reloadActions, this); } // Extend from EventEmitter to allow hooks to listen to stuff util.inherits(Sails, events.EventEmitter); // Public methods //////////////////////////////////////////////////////// Sails.prototype.lift = require('./lift'); Sails.prototype.lower = require('./lower'); Sails.prototype.getRouteFor = require('./get-route-for'); Sails.prototype.getUrlFor = require('./get-url-for'); Sails.prototype.reloadActions = require('./reload-actions'); Sails.prototype.getActions = require('./get-actions'); Sails.prototype.registerAction = require('./register-action'); Sails.prototype.registerActionMiddleware = require('./register-action-middleware'); // Public properties //////////////////////////////////////////////////////// // Regular expression to match request paths that look like assets. Sails.prototype.LOOKS_LIKE_ASSET_RX = /^[^?]*\/[^?\/]+\.[^?\/]+(\?.*)?$/; // Experimental methods //////////////////////////////////////////////////////// Sails.prototype.request = require('./request'); // Expose Express-esque synonyms for low-level usage of router Sails.prototype.all = function(path, action) { this.router.bind(path, action); return this; }; Sails.prototype.get = function(path, action) { this.router.bind(path, action, 'get'); return this; }; Sails.prototype.post = function(path, action) { this.router.bind(path, action, 'post'); return this; }; Sails.prototype.put = function(path, action) { this.router.bind(path, action, 'put'); return this; }; Sails.prototype.del = Sails.prototype['delete'] = function(path, action) { this.router.bind(path, action, 'delete'); return this; }; /** * .getRc() * * Get a dictionary of config from env vars, CLI opts, and `.sailsrc` file(s). * * @returns {Dictionary} */ Sails.prototype.getRc = require('./configuration/rc'); // FUTURE: expose a flavored version of sails-generate as `.generate()` // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // ``` // Sails.prototype.generate = function (){ /* ... */ }; // ``` // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // Private methods: //////////////////////////////////////////////////////// Sails.prototype.initialize = require('./private/initialize'); Sails.prototype.exposeGlobals = require('./private/exposeGlobals'); Sails.prototype.runBootstrap = require('./private/bootstrap'); Sails.prototype.isLocalSailsValid = require('./private/isLocalSailsValid'); Sails.prototype.isSailsAppSync = require('./private/isSailsAppSync'); // Presentation methods: //////////////////////////////////////////////////////// Sails.prototype.inspect = require('./private/inspect'); Sails.prototype.toString = require('./private/toString'); Sails.prototype.toJSON = require('./private/toJSON'); // Expose Sails constructor: //////////////////////////////////////////////////////// module.exports = Sails; ================================================ FILE: lib/app/configuration/default-hooks.js ================================================ /** * Default hooks * * (order still matters for now for some of these- * but mostly not, due to our use of events... * ...but for a few core hooks, e.g. `moduleloader`, * it still does.) * * * > FUTURE: make sure order does not matter, then once proven/tested, * > document it as such. (This will require adding a test that scrambles * > the order of this array then loads Sails over and over.) */ module.exports = { 'moduleloader': true,//<< FUTURE: absorb into core (i.e. federate its methods out to the places where they are used and remove support for `sails.modules`) 'logger': true,//<< FUTURE: absorb into core (i.e. like what we did w/ the controllers hook -- can live in `lib/app/private/log-ship.js`, and the rest can be inlined) 'request': true, // -•- For posterity, this is where the `orm` hook was formerly inserted (please don't get rid of this until we're a few patch releases into Sails v1, just so it's easier to reference.) 'views': true, 'blueprints': true,//<< FUTURE: pull this out into a standalone hook and have it work like the other core hooks that get installed as peers (unless you do --without=blueprints) 'responses': true, 'helpers': true, // -•- For posterity, this is where the `sockets` hook was formerly inserted (please don't get rid of this until we're a few patch releases into Sails v1, just so it's easier to reference.) 'pubsub': true,//<< FUTURE: **pull the private methods into the blueprints hook, and pull the PUBLIC methods into sails-hook-sockets -- i.e. if orm hook available, then sails-hook-sockets decorates models with RPS methods** 'policies': true, 'services': true, 'security': true, 'i18n': true,//<< FUTURE: pull this out into a standalone hook and have it work like the other core hooks that get installed as peers (unless you do --without=i18n) 'userconfig': true,//<< FUTURE: absorb into core (i.e. like what we did w/ the controllers hook -- can live in `lib/app/configuration`) 'session': true, // -•- For posterity, this is where the `grunt` hook was formerly inserted (please don't get rid of this until we're a few patch releases into Sails v1, just so it's easier to reference.) 'http': true, 'userhooks': true//<< FUTURE: absorb into core (i.e. like what we did w/ the controllers hook -- its logic can live in `lib/app/private`, and be driven by `lib/hooks/index.js`) }; ================================================ FILE: lib/app/configuration/index.js ================================================ /** * Module dependencies. */ var path = require('path'); var _ = require('@sailshq/lodash'); var DEFAULT_HOOKS = require('./default-hooks'); module.exports = function(sails) { /** * Expose new instance of `Configuration` */ return new Configuration(); function Configuration() { /** * Sails default configuration * * @api private */ this.defaults = function defaultConfig(appPath) { var defaultEnv; // If we're not loading the userconfig hook, which normally takes care // of ensuring that we have an environment, then make sure we set one here. if (_.isObject(sails.config.hooks) && sails.config.hooks.userconfig === false || (_.isArray(sails.config.loadHooks) && sails.config.loadHooks.indexOf('userconfig') === -1) ) { defaultEnv = sails.config.environment || 'development'; } // If `appPath` not specified, unfortunately, this is a fatal error, // since reasonable defaults cannot be assumed if (!appPath) { throw new Error('No `appPath` specified!'); } // Set up config defaults return { environment: defaultEnv, // Note: to avoid confusion re: timing, `hooks` configuration may eventually be removed // from `sails.config` in favor of something more flexible / obvious, e.g. the `app` object // itself (i.e. because you can't configure hooks in `userconfig`-- only in `overrides`). // Core (default) hooks hooks: _.reduce(DEFAULT_HOOKS, function (memo, hookBundled, hookIdentity) { // if `true`, then the core hook is bundled in the `lib/hooks/` directory // as `lib/hooks/HOOK_IDENTITY`. if (hookBundled === true) { memo[hookIdentity] = require('../../hooks/'+hookIdentity); } // if it's a string, then the core hook is an NPM dependency of sails, // so require it (which grabs it from `node_modules/`) else if (_.isString(hookBundled)) { var hook; try { hook = require(hookBundled); } catch (unusedErr) { // FUTURE: provide access to error details instead of swallowing throw new Error('Sails internal error: Could not require(\''+hookBundled+'\').'); } memo[hookIdentity] = hook; } // otherwise freak out else { throw new Error('Sails internal error: `'+hookIdentity+'`, a core hook, is invalid!'); } return memo; }, {}) || {}, // Save appPath in implicit defaults // appPath is passed from above in case `sails lift` was used // This is the directory where this Sails process is being initiated from. // ( usually this means `process.cwd()` ) appPath: appPath, // Built-in path defaults paths: { tmp: path.resolve(appPath, '.tmp') }, // Start off `routes` and `middleware` as empty objects routes: {}, middleware: {}, // Set implicit default for sniffing tactic. // Respected by helpers and actions, when running the configured // bootstrap function, and when invoking the `initialize` method // in hooks. implementationSniffingTactic: 'analogOrClassical' }; }; /** * Load the configuration modules * * @api private */ this.load = require('./load')(sails); // Bind the context of all instance methods _.bindAll(this); } }; ================================================ FILE: lib/app/configuration/load.js ================================================ /** * Module dependencies. */ var path = require('path'); var fs = require('fs'); var _ = require('@sailshq/lodash'); var async = require('async'); var CaptainsLog = require('captains-log'); var mergeDictionaries = require('merge-dictionaries'); module.exports = function(sails) { /** * Expose Configuration loader * * Load command-line overrides * * FUTURE: consider merging this into the `app` directory * * For reference, config priority is: * --> implicit defaults * --> environment variables * --> user config files * --> local config file * --> configOverride ( in call to sails.lift() ) * --> --cmdline args */ return function loadConfig(cb) { // Save reference to context for use in closures var self = this; // Commence with loading/validating/defaulting all the rest of the config async.auto({ /** * Until this point, `sails.config` is composed only of * configuration overrides passed into `sails.lift(overrides)` * (or `sails.load(overrides)`-- same thing) * * This step clones this into an "overrides" object, negotiating cmdline * shortcuts into the properly namespaced sails configuration options. */ mapOverrides: function(cb) { // Clone the `overrides` that were passed in. // TODO -- since this code is only called as a result of `sails.load()`, which already // clones the overrides, is this clone necessary? var overrides = _.clone(sails.config || {}); // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // FUTURE: Try bringing the rconf stuff from bin/sails-lift in here // (that way, we don't have to rely on duplicate code in app.js and in bin/sails-lift.js) // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // Map Sails options from overrides, handling a few special "shortcuts" // (i.e. allowing for CLI arguments like `--verbose`, instead of `--log.level=verbose`) try { overrides = _.merge(overrides, { // `--verbose` command-line shortcut // `--silly` command-line shortcut // `--silent` command-line shortcut log: overrides.verbose ? { level: 'verbose' } : overrides.silly ? { level: 'silly' } : overrides.silent ? { level: 'silent' } : undefined, // `--port=?` command-line shortcut port: overrides.port || undefined, // `--safe` command-line shortcut // `--alter` command-line shortcut // `--drop` command-line shortcut models: (function(){ if (overrides.safe) { return { migrate: 'safe' }; } else if (overrides.drop) { return { migrate: 'drop' }; } else if (overrides.alter) { return { migrate: 'alter' }; } else { return undefined; } })(), // `--redis` command-line shortcut session: (function(){ if (overrides.redis) { return { adapter: '@sailshq/connect-redis' }; } return undefined; })(), sockets: (function(){ if (overrides.redis) { return { adapter: '@sailshq/socket.io-redis' }; } return undefined; })(), // `--prod` command-line shortcut // `--staging` command-line shortcut // `--dev` command-line shortcut environment: (function(){ if (overrides.staging) {// --staging return 'staging'; } else if (overrides.prod){// --prod (but it's cleaner to use NODE_ENV=production with no other environment instead) return 'production'; } else if (overrides.dev) {// --dev (deprecated) console.warn('`--dev` option is deprecated: Please do not use it.'); // Note: we use `console.warn` here because we're not guaranteed // to have a working logger yet. return 'development'; } else { return undefined; } })()//† }); } catch (e) { return cb(e); } // Pass on overrides object return cb(undefined, overrides); }, /** * Immediately instantiate the default logger in case a log-worthy event occurs * Even though the app might actually use its own custom logger, we don't know * all of the user configurations yet. * * Makes sails.log accessible for the first time */ logger: ['mapOverrides', function(asyncData, cb) { var logConfigSoFar = asyncData.mapOverrides.log; sails.log = new CaptainsLog(logConfigSoFar); cb(); } ], /** * Expose version/dependency info for the currently-running * Sails on the `sails` object (from its `package.json`) */ versionAndDependencyInfo: function(cb) { var pathToThisVersionOfSails = path.join(__dirname, '../../..'); var json; try { json = JSON.parse(fs.readFileSync(path.resolve(pathToThisVersionOfSails, 'package.json'), 'utf8')); } catch (e) { return cb(e); } sails.version = json.version; sails.majorVersion = sails.version.split('.')[0].replace(/[^0-9]/g, ''); sails.minorVersion = sails.version.split('.')[1].replace(/[^0-9]/g, ''); sails.patchVersion = sails.version.split('.')[2].replace(/[^0-9]/g, ''); sails.dependencies = json.dependencies; cb(); }, /** * Ensure that environment variables are applied to important configs */ mixinDefaults: ['mapOverrides', function(results, cb) { // Get overrides var overrides = results.mapOverrides; // Apply environment variables // (if the config values are not set in overrides) overrides.environment = overrides.environment || process.env.NODE_ENV; overrides.port = overrides.port || process.env.PORT; // Generate implicit, built-in framework defaults for the app var implicitDefaults = self.defaults(overrides.appPath || process.cwd()); // Extend copy of implicit defaults with user config // TODO -- is the _.clone() necessary? var mergedConfig = mergeDictionaries(_.clone(implicitDefaults), overrides); return cb(undefined, mergedConfig); } ] }, function configLoaded(err, results) { if (err) { sails.log.error('Error encountered loading config ::\n', err); return cb(err); } // Override the previous contents of sails.config with the new, validated // config w/ defaults and overrides mixed in the appropriate order. sails.config = results.mixinDefaults; return cb(); }); }; }; ================================================ FILE: lib/app/configuration/rc.js ================================================ /** * Module dependencies */ var _ = require('@sailshq/lodash'); var minimist = require('minimist'); var rc = require('../../util/rc.js'); var rttc = require('rttc'); /** * Load configuration from .rc files and env vars * @param {String} namespace [namespace to look for env vars under (defaults to `sails`)] * @return {Dictionary} A dictionary of config values gathered from .rc files, with env vars and command line options merged on top */ module.exports = function(namespace) { // Default namespace to `sails`. namespace = namespace || 'sails'; // Locate and load .rc files if they exist. var conf = rc(namespace); // Load in overrides from the environment, using `rttc.parseHuman` to // guesstimate the types. // NOTE -- the code below is lifted from the `rc` module, and modified to: // 1. Pass JSHint // 2. Run `parseHuman` on values // If at some point `rc` exposes metadata about which configs came from // the environment, we can simplify our code by just running `parseHuman` // on those values instead of doing the work to pluck them from the env. // Construct the expected env var prefix from the namespace. var prefix = namespace + '_'; // Cache the prefix length so we don't have to keep looking it up. var l = prefix.length; // Loop through the env vars, looking for ones with the right prefix. _.each(process.env, function(val, key) { // If this var's name has the right prefix... if((key.indexOf(prefix)) === 0) { // Replace double-underscores with dots, to work with Lodash _.set(). var keypath = key.substring(l).replace(/__/g,'.'); // Attempt to parse the value as JSON. try { val = rttc.parseHuman(val, 'json'); } // If that doesn't work, humanize the value without providing a schema. catch(unusedErr) { val = rttc.parseHuman(val); } // Override the current value at this keypath in `conf` (which currently contains // the string value of the env var) with the now (possibly) humanized value. _.set(conf, keypath, val); } }); // Load command line arguments, since they need to take precedence over env. var argv = minimist(process.argv.slice(2)); // Merge the command line arguments back on top. Minimist allows nested config (i.e. --log.level=silly), // hence the _.merge(). conf = _.merge(conf, argv); return conf; }; ================================================ FILE: lib/app/get-actions.js ================================================ /** * Module dependencies. */ var _ = require('@sailshq/lodash'); /** * Sails.prototype.getActions() * * Return a shallow clone of the loaded actions dictionary. * * @returns {Dictionary} A shallow clone of all actions, indexed by their unique identifier. * * @this {SailsApp} * ---------------------------------------------------------------------------------------- * * Usage: * * ``` * sails.getActions(); * // => * // { * // 'duck/quack': {...}, * // // ... * // } * ``` */ module.exports = function getActions() { // Return a shallow clone of the actions dictionary, so that the caller // can't modify the actions. return _.clone(this._actions); }; ================================================ FILE: lib/app/get-route-for.js ================================================ /** * Module dependencies */ var util = require('util'); var _ = require('@sailshq/lodash'); var detectVerb = require('../util/detect-verb'); /** * getRouteFor() * * Look up more information about the first explicit route defined in this app * which has the given route target. * * Note that this function _only searches explicit routes_ which have been configured * manually (e.g. in `config/routes.js`). For more info, see: * https://github.com/balderdashy/sails/issues/3402#issuecomment-171633341 * * @this {SailsApp} * ---------------------------------------------------------------------------------------- * * Usage: * * ``` * getRouteFor('DuckController.quack'); * getRouteFor({ target: 'DuckController.quack' }); * // => * // { * // url: '/ducks/:id/quack', * // method: 'post' * // } * ``` */ module.exports = function getRouteFor(routeQuery){ // Get reference to sails app instance. var sails = this; // Get the identity of the action we're trying to look up, based on the route we're querying. var actionToLookup; try { actionToLookup = sails.router.getActionIdentityForTarget(routeQuery); } catch (unusedErr) { // FUTURE: provide access to error details instead of swallowing var invalidUsageErr = new Error(`Usage error: sails.getRouteFor() expects a string route target (e.g. "DuckController.quack") or a dictionary with either a "target" (e.g. {target: "DuckController.quack"}) or an "action" (e.g. {controller: "duck", action: "quack"} or {action: "duck/quack"}). But instead, it received a ${typeof routeQuery}: ${util.inspect(routeQuery, {depth: null})}`); invalidUsageErr.code = 'E_USAGE'; throw invalidUsageErr; } // Now look up the first route with this target (`routeTargetToLookup`). var firstMatchingRouteAddress = _.find(_.keys(sails.router.explicitRoutes), function (address) { var target = sails.router.explicitRoutes[address]; // Attempt to look up the action that corresponds to this route target. var actionIdentity; try { actionIdentity = sails.router.getActionIdentityForTarget(target); } catch (e) { // If the target is not an action (i.e if it is a view, a raw req/res function or something else) // then just return false (it's not a match for the action we're looking up). if (e.code === 'E_NOT_ACTION_TARGET') { return false; } // If some other error occurred looking up the target, then throw it. throw e; } // Ok, we got an action identity. Does it match the identity of the action we're trying to look up? return (actionIdentity === actionToLookup); }); // If no route was found, throw an error. if (!firstMatchingRouteAddress) { var unrecognizedTargetErr = new Error('Route not found: No explicit route could be found in this app with the specified target (`'+util.inspect(routeQuery, {depth: null})+'`).'); unrecognizedTargetErr.code = 'E_NOT_FOUND'; throw unrecognizedTargetErr; } // Now that the raw route address been located, we'll normalize it: // // If route address is '*', it will be automatically corrected to `/*` when bound, so also reflect that here. firstMatchingRouteAddress = firstMatchingRouteAddress === '*' ? '/*' : firstMatchingRouteAddress; // Then we parse it into its HTTP method and URL pattern parts. var parsedAddress = detectVerb(firstMatchingRouteAddress); // At this point we being building the final return value- the route info dictionary. var routeInfo = {}; routeInfo.method = parsedAddress.verb || ''; routeInfo.url = parsedAddress.path; // And finally return the route info. return routeInfo; }; ================================================ FILE: lib/app/get-url-for.js ================================================ /** * Module dependencies */ // N/A /** * getUrlFor() * * Look up the URL of this app's first explicit route with the given route target. * * Note that this function _only searches explicit routes_ which have been configured * manually (e.g. in `config/routes.js`). For more info, see: * https://github.com/balderdashy/sails/issues/3402#issuecomment-171633341 * * * @this {SailsApp} * ---------------------------------------------------------------------------------------- * * Usage: * * ``` * getUrlFor('DuckController.quack'); * // => '/ducks/:id/quack' * * getUrlFor({ target: 'DuckController.quack' }); * // => '/ducks/:id/quack' * ``` */ module.exports = function getUrlFor(routeQuery){ // Get reference to sails app instance. var sails = this; // Now attempt to look up the first route that matches the specified argument // and if it works, then return its URL. return sails.getRouteFor(routeQuery).url; }; ================================================ FILE: lib/app/index.js ================================================ /** * Module dependencies. */ var _ = require('@sailshq/lodash'); var Sails = require('./Sails'); /** * Expose `Sails` factory...thing. * (maintains backwards compatibility w/ constructor usage) */ module.exports = SailsFactory; function SailsFactory() { return new Sails(); } // Backwards compatibility for Sails singleton usage: var singleton = SailsFactory(); SailsFactory.isLocalSailsValid = _.bind(singleton.isLocalSailsValid, singleton); SailsFactory.isSailsAppSync = _.bind(singleton.isSailsAppSync, singleton); ================================================ FILE: lib/app/lift.js ================================================ /** * Module dependencies. */ var _ = require('@sailshq/lodash'); var async = require('async'); var chalk = require('chalk'); /** * Sails.prototype.lift() * * Load the app, then bind process listeners and emit the internal "ready" event. * The "ready" event is listened for by core hooks; for example, the HTTP hook uses * it to start listening for requests. * * > This method also logs the ASCII art for the characteristic ship. * * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * @param {Dictionary?} configOverride * Overrides that will be deep-merged (w/ precedence) on top of existing configuration. * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * @callback {Function?} done * @param {Error?} err * * A Node-style callback that wil be triggered when the lift has completed (one way or another) * > If the `done` callback is omitted, then: * > • If the lift fails, Sails will log the underlying fatal error using `sails.log.error()`. * > • Otherwise, Sails will log "App lifted successfully." using `sails.log.verbose()`. * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * @api public * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ module.exports = function lift(configOverride, done) { var sails = this; // configOverride is optional. if (_.isFunction(configOverride)) { done = configOverride; configOverride = {}; } // Callback is optional (but recommended.) done = done || function defaultCallback(err) { if (err) { sails.log.error('Failed to lift app:',err); if(err.raw) { sails.log.error('More details (raw):', err.raw); } sails.log.silly('(You are seeing the above error message because no custom callback was programmatically provided to `.lift()`.)'); return; } sails.log.verbose('App lifted successfully.'); sails.log.silly('(You are seeing the "App lifted successfully" verbose log message because no custom callback was programmatically provided to `.lift()`.)'); }; async.series([ function (next) { sails.load(configOverride, next); }, function (next){ sails.initialize(next); }, ], function whenSailsIsReady(err) { if (err) { sails.lower(function (additionalErrLoweringSails){ if (additionalErrLoweringSails) { sails.log.error('When trying to lower the app as a result of a failed lift, encountered an error:', additionalErrLoweringSails); }//>- return done(err); });//</sails.lower> return; }//-• // If `config.noShip` is set, skip the startup message. // Otherwise, gather app meta-info and log startup message (the boat). if (!_.isObject(sails.config.log) || !sails.config.log.noShip) { sails.log.ship && sails.log.ship(); sails.log.info(('Server lifted in `' + sails.config.appPath + '`')); sails.log.info(('To shut down Sails, press <CTRL> + C at any time.')); sails.log.info(('Read more at '+chalk.underline('https://sailsjs.com/support')+'.')); sails.log.blank(); sails.log(chalk.grey(Array(56).join('-'))); sails.log(chalk.grey(':: ' + new Date())); sails.log.blank(); sails.log('Environment : ' + sails.config.environment); // Only log the host if an explicit host is set if (!_.isUndefined(sails.config.explicitHost)) { sails.log('Host : ' + sails.config.explicitHost); // 12 - 4 = 8 spaces } sails.log('Port : ' + sails.config.port); // 12 - 4 = 8 spaces // > Note that we don't try to include the "Local: " stuff // > unless we're pretty sure which URL it would be a good idea to try and visit. // > (even then, it's not 100% or anything. But at least with these checks, it's // > not wrong MOST of the time.) if (process.env.NODE_ENV !== 'production' && (!sails.config.ssl || _.isEqual(sails.config.ssl, {})) && !sails.config.http.serverOptions && !sails.config.explicitHost ) { sails.log('Local : ' + chalk.underline('http://localhost:'+sails.config.port)); } sails.log.verbose('NODE_ENV : ' + (process.env.NODE_ENV||chalk.gray('(not set)'))); // 12 - 8 - 2 = 2 spaces sails.log.silly(); sails.log.silly('Version Info:'); sails.log.silly('node : ' + (process.version)); sails.log.silly('engine (v8) : ' + (process.versions.v8)); sails.log.silly('openssl : ' + (process.versions.openssl)); sails.log(chalk.grey(Array(56).join('-'))); }//>- // Emit 'lifted' event. sails.emit('lifted'); // Set `isLifted` (private dignostic flag) sails.isLifted = true; // try {console.timeEnd('core_lift');}catch(e){} return done(undefined, sails); });//</async.series()> }; ================================================ FILE: lib/app/load.js ================================================ /** * Module dependencies */ var util = require('util'); var _ = require('@sailshq/lodash'); var async = require('async'); var flaverr = require('flaverr'); var __Configuration = require('./configuration'); var __initializeHooks = require('./private/loadHooks'); var __loadActionModules = require('./private/controller/load-action-modules'); var __checkGruntConfig = require('./private/checkGruntConfig'); /** * @param {SailsApp} sails * @returns {Function} */ module.exports = function(sails) { var Configuration = __Configuration(sails); var initializeHooks = __initializeHooks(sails); var checkGruntConfig = __checkGruntConfig(sails); /** * Expose loader start point. * (idempotent) * * @api public */ return function load(configOverride, cb) { if (sails._exiting) { return cb(new Error('\n*********\nCannot load or lift an app after it has already been lowered. \nYou can make a new app instance with:\nvar SailsApp = require(\'sails\').Sails;\nvar sails = new SailsApp();\n\nAnd then you can do:\nsails.load([opts,] cb)\n\n')); } // Log a verbose log message about the fact that EVEN MORE verbosity // is available in `silly` mode. sails.log.verbose('• • • • • • • • • • • • • • • • • • • • • • • • • • • • • •'); sails.log.verbose('• Loading Sails with "verbose" logging enabled... •'); sails.log.verbose('• (For even more details, try "silly".) •'); sails.log.silly ('• Actually, looks like you\'re already using "silly"! •'); sails.log.verbose('• •'); sails.log.verbose('• http://sailsjs.com/config/log •'); sails.log.verbose('• • • • • • • • • • • • • • • • • • • • • • • • • • • • • •'); // configOverride is optional if (_.isFunction(configOverride)) { cb = configOverride; configOverride = {}; } // Ensure override is an object and clone it (or make an empty object if it's not). // The shallow clone protects against the caller accidentally adding/removing props // to the config after Sails has loaded (but they could still mess with nested config). configOverride = configOverride || {}; sails.config = _.clone(configOverride); // If host is explicitly specified, set `explicitHost` // (otherwise when host is omitted, Express will accept all connections via INADDR_ANY) if (configOverride.host) { configOverride.explicitHost = configOverride.host; } // Optionally expose services, models, sails, _, async, etc. as globals as soon as the // user config loads. sails.on('hook:userconfig:loaded', sails.exposeGlobals); async.auto({ // Apply core defaults and hook-agnostic configuration, // esp. overrides including command-line options, environment variables, // and options that were passed in programmatically. config: [Configuration.load], // Verify that the combination of Sails environment and NODE_ENV is valid // as early as possible -- that is, as soon as we know for sure what the // Sails environment is. verifyEnv: ['config', function(results, cb) { // If the userconfig hook is active, wait until it's finished to // verify the environment, since it might be set in a config file. if (_.isUndefined(sails.config.hooks) || (sails.config.hooks !== false && sails.config.hooks.userconfig !== false)) { sails.on('hook:userconfig:loaded', verifyEnvironment); } // Otherwise verify it right now. The Sails environment will be // whatever was set on the command line, or via the sails_environment // env var, or defaulted to "development". else { verifyEnvironment(); } return cb(); }], // Check if the current app needs the Grunt hook installed but doesn't have it. grunt: ['config', checkGruntConfig], // Load hooks into memory, with their middleware and routes hooks: ['verifyEnv', 'config', helpLoadHooks], // Load actions from disk and config overrides controller: ['hooks', function(results, cb) { __loadActionModules(sails, cb); }], // Populate the "registry" // Houses "middleware-esque" functions bound by various hooks and/or Sails core itself. // (i.e. `function (req, res [,next]) {}`) // // (Basically, that means we grab an exposed `middleware` object, // full of functions, from each hook, then make it available as // `sails.middleware.[HOOK_ID]`.) // // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // FUTURE: finish refactoring to change "middleware" nomenclature // to avoid confusion with the more specific (and more common) // usage of the term. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - registry: ['hooks', function populateRegistry(results, cb) { // Iterate through hooks and absorb the middleware therein // Save a reference to registry and expose it on // the Sails instance. sails.middleware = sails.registry = // Namespace functions by their source hook's identity _.reduce(sails.hooks, function(registry, hook, identity) { registry[identity] = hook.middleware; return registry; }, {}); sails.emit('middleware:registered'); cb(); } ], // Load the router and bind routes in `sails.config.routes` router: ['registry', sails.router.load] }, ready__(cb)); // Makes `app.load()` chainable return sails; }; // ============================================================================== // < inline function declarations > // ██╗ ██╗███╗ ██╗██╗ ██╗███╗ ██╗███████╗ ███████╗███╗ ██╗ ██████╗ ███████╗███████╗███████╗ ██╗ // ██╔╝ ██║████╗ ██║██║ ██║████╗ ██║██╔════╝ ██╔════╝████╗ ██║ ██╔══██╗██╔════╝██╔════╝██╔════╝ ╚██╗ // ██╔╝ ██║██╔██╗ ██║██║ ██║██╔██╗ ██║█████╗ █████╗ ██╔██╗ ██║ ██║ ██║█████╗ █████╗ ███████╗ ╚██╗ // ╚██╗ ██║██║╚██╗██║██║ ██║██║╚██╗██║██╔══╝ ██╔══╝ ██║╚██╗██║ ██║ ██║██╔══╝ ██╔══╝ ╚════██║ ██╔╝ // ╚██╗ ██║██║ ╚████║███████╗██║██║ ╚████║███████╗ ██║ ██║ ╚████║ ██████╔╝███████╗██║ ███████║ ██╔╝ // ╚═╝ ╚═╝╚═╝ ╚═══╝╚══════╝╚═╝╚═╝ ╚═══╝╚══════╝ ╚═╝ ╚═╝ ╚═══╝ ╚═════╝ ╚══════╝╚═╝ ╚══════╝ ╚═╝ // // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // FUTURE: extrapolate the following inline function definitions into // separate files -- or if appropriate (and if there's no tangible impact // on lift performance) then pull them inline above. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /** * Load hooks in parallel * let them work out dependencies themselves, * taking advantage of events fired from the sails object * * @api private */ function helpLoadHooks(results, cb) { sails.hooks = { }; // If config.hooks is disabled, skip hook loading altogether if (sails.config.hooks === false) { return cb(); } async.series([ function(cb) { loadHookDefinitions(sails.hooks, cb); }, function(cb) { initializeHooks(sails.hooks, cb); } ], function(err) { if (err) { return cb(err); } // Inform any listeners that the initial, built-in hooks // are finished loading sails.emit('hooks:builtIn:ready'); sails.log.silly('Built-in hooks are ready.'); return cb(); }); } /** * Load built-in hook definitions from `sails.config.hooks` * and put them back into `hooks` (probably `sails.hooks`) * * @api private */ function loadHookDefinitions(hooks, cb) { // Mix in user-configured hook definitions _.extend(hooks, sails.config.hooks); // Make sure these changes to the hooks object get applied // to sails.config.hooks to keep logic consistent // (I think we can get away w/o this, but leaving as a stub) // sails.config.hooks = hooks; // If user configured `loadHooks`, only include those. if (sails.config.loadHooks) { if (!_.isArray(sails.config.loadHooks)) { return cb('Invalid `loadHooks` config. ' + 'Please specify an array of string hook names.\n' + 'You specified ::' + util.inspect(sails.config.loadHooks)); } _.each(hooks, function(def, hookName) { if (!_.contains(sails.config.loadHooks, hookName)) { hooks[hookName] = false; } }); sails.log.verbose('Deliberate partial load-- will only initialize hooks ::', sails.config.loadHooks); } return cb(); } function verifyEnvironment() { // At this point, the Sails environment is set to its final value, // whether it came from the command line or a config file. So we // can now compare it to the NODE_ENV environment variable and // act accordingly. This may involve changing NODE_ENV to "production", // which we want to do as early as possible since dependencies might // be relying on that value. // If the Sails environment is production, but NODE_ENV is undefined, // log a warning and change NODE_ENV to "production". if (sails.config.environment === 'production' && process.env.NODE_ENV !== 'production' ) { if (_.isUndefined(process.env.NODE_ENV)) { sails.log.debug('Detected Sails environment is "production", but NODE_ENV is `undefined`.'); sails.log.debug('Automatically setting the NODE_ENV environment variable to "production".'); sails.log.debug(); process.env.NODE_ENV = 'production'; } else { throw flaverr({ name: 'userError', code: 'E_INVALID_NODE_ENV' }, new Error('When the Sails environment is set to "production", NODE_ENV must also be set to "production" (but it was set to "' + process.env.NODE_ENV + '" instead).')); } } } /** * Returns function which is fired when Sails is ready to go * * @api private */ function ready__(cb) { return function(err) { if (err) { return cb && cb(err); } sails.log.silly('The router & all hooks were loaded successfully.'); // If userconfig hook is turned off, still load globals. if (sails.config.hooks && sails.config.hooks.userconfig === false || (sails.config.loadHooks && sails.config.loadHooks.indexOf('userconfig') === -1)) { sails.exposeGlobals(); } // If the Sails environment is set to "production" but the Node environment isn't, // log a warning. if (sails.config.environment === 'production' && process.env.NODE_ENV !== 'production') { sails.log.warn('Detected Sails environment of `production`, but Node environment is `' + process.env.NODE_ENV + '`.\n' + 'It is recommended that in production mode, both the Sails and Node environments be set to `production`.'); } cb && cb(null, sails); }; } // ██╗ ██╗ ██╗███╗ ██╗██╗ ██╗███╗ ██╗███████╗ ███████╗███╗ ██╗ ██████╗ ███████╗███████╗███████╗ ██╗ // ██╔╝ ██╔╝ ██║████╗ ██║██║ ██║████╗ ██║██╔════╝ ██╔════╝████╗ ██║ ██╔══██╗██╔════╝██╔════╝██╔════╝ ╚██╗ // ██╔╝ ██╔╝ ██║██╔██╗ ██║██║ ██║██╔██╗ ██║█████╗ █████╗ ██╔██╗ ██║ ██║ ██║█████╗ █████╗ ███████╗ ╚██╗ // ╚██╗ ██╔╝ ██║██║╚██╗██║██║ ██║██║╚██╗██║██╔══╝ ██╔══╝ ██║╚██╗██║ ██║ ██║██╔══╝ ██╔══╝ ╚════██║ ██╔╝ // ╚██╗██╔╝ ██║██║ ╚████║███████╗██║██║ ╚████║███████╗ ██║ ██║ ╚████║ ██████╔╝███████╗██║ ███████║ ██╔╝ // ╚═╝╚═╝ ╚═╝╚═╝ ╚═══╝╚══════╝╚═╝╚═╝ ╚═══╝╚══════╝ ╚═╝ ╚═╝ ╚═══╝ ╚═════╝ ╚══════╝╚═╝ ╚══════╝ ╚═╝ // // </ inline function declarations (see note above) > // ============================================================================== }; ================================================ FILE: lib/app/lower.js ================================================ /** * Module dependencies. */ var _ = require('@sailshq/lodash'); var async = require('async'); /** * Sails.prototype.lower() * * The inverse of `lift()`, this method * shuts down all attached servers. * * It also unbinds listeners and terminates child processes. * * @api public */ module.exports = function lower(options, cb) { var sails = this; sails.log.verbose('Lowering sails...'); // `options` is optional. if (_.isFunction(options)) { cb = options; options = undefined; } // Callback is optional cb = cb || function(err) { if (err) { return sails.log.error(err); } }; options = options || {}; options.delay = options.delay || 100; // Flag `sails._exiting` as soon as the app has begun to shut down. // This may be used by core hooks and other parts of core // (e.g. to stop handling HTTP requests and prevent ugly error msgs). sails._exiting = true; var beforeShutdown = (sails.config && sails.config.beforeShutdown) || function(cb) { return cb(); }; // Wait until beforeShutdown logic runs beforeShutdown(function(err) { // If an error occurred, don't stop-- still go ahead and take care of other teardown tasks. if (err) { sails.log.error(err); } // Try to kill all child processes _.each(sails.childProcesses, function kill(childProcess) { sails.log.silly('Sent kill signal to child process (' + childProcess.pid + ')...'); try { childProcess.kill('SIGINT'); } catch (e) { sails.log.error('While lowering Sails app: received error killing child process:', e.stack); } }); // Shut down HTTP server sails.emit('lower'); // (Note for future: would be cleaner to provide a way to defer this to the http // and sockets hooks-- i.e. having hooks expose a `teardown(cb)` interceptor. Keep // in mind we'd need a way to distinguish between a graceful shutdown and a force // kill. In a force kill situation, it's never ok for the process to hang.) async.series([ function shutdownSockets(cb) { // If the sockets hook is disabled, skip this. // Also skip if the socket server is piggybacking on the main HTTP server, to avoid // the onClose event possibly being called multiple times (because you can't tell // socket.io to close without it trying to close the http server). If we're piggybacking // we'll call sails.io.close in the main "shutdownHTTP" code below. if (!_.isObject(sails.hooks) || !sails.hooks.sockets || !sails.io || (sails.io && sails.io.httpServer && sails.hooks.http.server === sails.io.httpServer)) { return cb(); } var timeOut; try { sails.log.silly('Shutting down socket server...'); timeOut = setTimeout(function() { sails.io.httpServer.removeListener('close', onClose); return cb(); }, 100); sails.io.httpServer.unref(); sails.io.httpServer.once('close', onClose); sails.io.close(); } catch (e) { sails.log.verbose('Error occurred closing socket server: ', e); clearTimeout(timeOut); return cb(); } function onClose() { sails.log.silly('Socket server shut down successfully.'); clearTimeout(timeOut); cb(); } }, function shutdownHTTP(cb) { if (!_.isObject(sails.hooks) || !sails.hooks.http || !sails.hooks.http.server) { return cb(); } var timeOut; try { sails.log.silly('Shutting down HTTP server...'); // Allow process to exit once this server is closed sails.hooks.http.server.unref(); // If we have a socket server and it's piggybacking on the main HTTP server, tell // socket.io to close now. This may call `.close()` on the HTTP server, which will // happen again below, but the second synchronous call to .close() will have no // additional effect. Leaving this as-is in case future versions of socket.io // DON'T automatically close the http server for you. if (sails.io && sails.io.httpServer && sails.hooks.http.server === sails.io.httpServer) { sails.io.close(); } // If the "hard shutdown" option is on, destroy the server immediately, // severing all connections if (options.hardShutdown) { sails.hooks.http.destroy(); } // Otherwise just stop the server from accepting new connections, // and wait options.delay for the existing connections to close // gracefully before destroying. else { timeOut = setTimeout(sails.hooks.http.destroy, options.delay); sails.hooks.http.server.close(); } // Wait for the existing connections to close sails.hooks.http.server.once('close', function () { sails.log.silly('HTTP server shut down successfully.'); clearTimeout(timeOut); cb(); }); } catch (e) { sails.log.verbose('Error occurred closing HTTP server: ', e); clearTimeout(timeOut); return cb(); } }, function removeListeners(cb) { // Manually remove all event listeners _.each(_.keys(sails._events)||[], function (eventName){ sails.removeAllListeners(eventName); }); var listeners = sails._processListeners; if (listeners) { process.removeListener('SIGUSR2', listeners.sigusr2); process.removeListener('SIGINT', listeners.sigint); process.removeListener('SIGTERM', listeners.sigterm); process.removeListener('exit', listeners.exit); } sails._processListeners = null; // If `sails.config.process.removeAllListeners` is set, do that. // This is no longer necessary due to https://github.com/balderdashy/sails/pull/2693 // Deprecating for v0.12. if (sails.config && sails.config.process && sails.config.process.removeAllListeners) { sails.log.debug('sails.config.process.removeAllListeners is deprecated; please remove listeners individually!'); process.removeAllListeners(); } cb(); }, ], function (err) { if (err) { // This should never happen because `err` is never passed in any of the async // functions above. Still, just to be safe, we set up an error log. sails.log.error('While lowering Sails app: received unexpected error:', err.stack); return cb(err); } return cb(); });//</async.series> });//</beforeShutdown()> }; ================================================ FILE: lib/app/private/after.js ================================================ /** * Module dependencies */ var _ = require('@sailshq/lodash'); var async = require('async'); // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // FUTURE // Pull _most of this_ into a separate module, since it's not specific // to Sails, and has come up in a few different places. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /** * Mix-in an `after` function to an EventEmitter. * * If `events` have already fired, trigger fn immediately (with no args) * Otherwise bind a normal one-time event using `EventEmitter.prototype.once()`. * Useful for checking whether or not something has finished loading, etc. * * This is a lot like jQuery's `$(document).ready()`. * * @param {EventEmitter} emitter The Sails application instance */ module.exports = function mixinAfter(emitter) { /** * { emitter.warmEvents } * * Events which have occurred at least once * (Required to support `emitter.after()`) */ emitter.warmEvents = {}; var _originalEmit = emitter.emit; /** * emitter.emit() * * Synchronously calls each of the listeners registered for the event named eventName, in the order they were registered, passing the supplied arguments to each. * * Override `EventEmitter.prototype.emit` to keep track of all the events that have occurred once. * (Required to support `emitter.after()`) * * @param {String} eventName [name of the event] * @return {Boolean} Returns true if the event had listeners, false otherwise. * @see https://nodejs.org/api/events.html#events_emitter_emit_eventname_args */ emitter.emit = function(eventName) { var args = Array.prototype.slice.call(arguments, 0); emitter.warmEvents[eventName] = true; return _originalEmit.apply(emitter, args); }; /** * `emitter.after()` * * Fires your handler **IF THE SPECIFIED EVENT HAS ALREADY BEEN TRIGGERED** or **WHEN IT IS TRIGGERED**. * * @param {String|Array} events [name of the event(s)] * @param {Function} fn [event handler function] */ emitter.after = function(events, fn) { // Support a single event or an array of events if (!_.isArray(events)) { events = [events]; } // Convert named event dependencies into an array // of async-compatible functions. var dependencies = _.reduce(events, function (dependencies, event) { // Push on the handler function. dependencies.push(function handlerFn(cb) { // If the event has already fired, then just execute our callback. if (emitter.warmEvents[event]) { return cb(); } // But otherwise, bind a one-time-use handler that listens for the // first time this event is fired, and then executes our callback // once it does. else { emitter.once(event, function (){ return cb(); }); } });//</declared and pushed on handler function> return dependencies; }, []);//</_.reduce() :: iterate over each event in order to build `dependencies` (an array of handler functions)> // When all events have fired, call `fn` // (all arguments passed to `emit()` calls are discarded) async.parallel(dependencies, function(err) { if (err) { console.error('Consistency violation: Received `err`, but this should be impossible! Here is the error: '+err.stack); console.error('^^^If you are seeing this message, then please report this error at http://sailsjs.com/bugs. (Continuing anyway...)'); }//>- return fn(); }); }; }; ================================================ FILE: lib/app/private/bootstrap.js ================================================ /** * Module dependencies */ var STRIP_COMMENTS_RX = /(\/\/.*$)|(\/\*[\s\S]*?\*\/)|(\s*=[^,\)]*(('(?:\\'|[^'\r\n])*')|("(?:\\"|[^"\r\n])*"))|(\s*=[^,\)]*))/mg; /** * runBootstrap() * * Run the configured bootstrap function. * * @this {SailsApp} * * @param {Function} done [description] * * @api private */ module.exports = function runBootstrap(done) { // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // > FUTURE: Add tests that verify that the bootstrap function may // > be disabled or set explicitly w/o running, depending on user // > config. (This is almost certainly good to go already, just worth // > an extra test since it was mentioned specifically way back in // > https://github.com/balderdashy/sails/commit/926baaad92dba345db64c2ec9e17d35711dff5a3 // > and thus was a problem that came up when shuffling things around // > w/ hook loading.) // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - var sails = this; // Run bootstrap script if specified // Otherwise, do nothing and continue if (!sails.config.bootstrap) { return done(); } sails.log.verbose('Running the setup logic in `sails.config.bootstrap(done)`...'); // If bootstrap takes too long, display warning message // (just in case user forgot to call THEIR bootstrap's `done` callback, if // they're using that approach) var timeoutMs = sails.config.bootstrapTimeout || 30000; var timer = setTimeout(function bootstrapTookTooLong() { sails.log.warn( 'Bootstrap is taking a while to finish ('+timeoutMs+' milliseconds).\n'+ 'If this is unexpected, and *if* the bootstrap function uses a callback,\n'+ 'maybe double-check to be sure that callback is getting called.\n'+ ' [?] Read more: https://sailsjs.com/config/bootstrap'); }, timeoutMs); var ranBootstrapFn = false; (function(proceed){ try { var seemsToExpectCallback = true; if (sails.config.implementationSniffingTactic === 'analogOrClassical') { var hasParameters = (function(fn){ var fnStr = fn.toString().replace(STRIP_COMMENTS_RX, ''); var parametersAsString = fnStr.slice(fnStr.indexOf('(')+1, fnStr.indexOf(')')); // console.log('::',parametersAsString, parametersAsString.replace(/\s*/g,'').length); return parametersAsString.replace(/\s*/g,'').length !== 0; })(sails.config.bootstrap);//† seemsToExpectCallback = hasParameters; } if (sails.config.bootstrap.constructor.name === 'AsyncFunction') { var promise; if (seemsToExpectCallback) { promise = sails.config.bootstrap(proceed); } else { promise = sails.config.bootstrap(function(unusedErr){ proceed(new Error('Unexpected attempt to invoke callback. Since this "bootstrap" function does not appear to expect a callback parameter, this stub callback was provided instead. Please either explicitly list the callback parameter among the arguments or change this code to no longer use a callback.')); }) .then(function(){ proceed(); }); }//fi promise.catch(function(e) { proceed(e); // (Note that we don't do `return proceed(e)` here. That's on purpose-- // to avoid sending the wrong idea to you, dear reader) }); } else { if (seemsToExpectCallback) { sails.config.bootstrap(proceed); } else { sails.config.bootstrap(function(unusedErr){ proceed(new Error('Unexpected attempt to invoke callback. Since this "bootstrap" function does not appear to expect a callback parameter, this stub callback was provided instead. Please either explicitly list the callback parameter among the arguments or change this code to no longer use a callback.')); }); return proceed(); } } } catch (e) { return proceed(e); } })(function (err){ if (ranBootstrapFn) { if (err) { sails.log.error('The bootstrap function encountered an error *AFTER* it already ran once! Details:',err); } else { sails.log.error('The bootstrap function (`sails.config.bootstrap`) signaled that it is finished, but it already ran once! (*If* it is using a callback, check that the callback is not being called more than once.)'); } return; }//-• ranBootstrapFn = true; clearTimeout(timer); // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // Note that async+await+bluebird+Node 8 errors are not necessarily "true" Error instances, // as per _.isError() anyway (see https://github.com/node-machine/machine/commits/6b9d9590794e33307df1f7ba91e328dd236446a9). // So if we want improve the stack trace here, we'd have to be a bit more relaxed and tolerate // these sorts of "errors" directly as well (by tweezing out the `cause`, which is where the // original Error lives.) // FUTURE: try that out // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - return done(err); });//</ self-calling function > }; ================================================ FILE: lib/app/private/checkGruntConfig.js ================================================ /** * Module dependencies */ var crypto = require('crypto'); var path = require('path'); var fs = require('fs'); var _ = require('@sailshq/lodash'); var GRUNT_FILE_HASHES = [ // v0.10.x - v0.12.x 'LC+ibPOKSr+pnfsUCCqCN6QobNQ=', 'Koim03Z2n89h3BxknLlFfO1rTMQ=', '1gm6DioH6w1qsObN8Riopf98TLE=', 'GwpKmvwrsJNW83hN3DTWdo3cmgM=', '/Fuc2veAiInWAYLHSFFG7CZb1fY=', 'sbGaQEjDsJUaoxBt75vU51k/XAQ=', 'nbNGVMWlWgGp+6w5/hpOctqb3Zg=', '0mC0iC9vKOn1ZsZD3PIXRbiVpeE=', 'uvunqwrU4favfaTz+r+jj9HrCBo=', 'oyQisk4tEr2xmoL0agollwB47BQ=', 'CENhG5uQtyYchMfsSR1hrXip2n8=', 'r8sSfwQd2H5rvWHlOqGVJvVV2fM=', 'gU+eObj1p6i/fBxFMmnT71rD9nY=', 'px4a1ssydoV0oPaS9jUIQcJENQY=', 'VSBY7VlgZQFWjwZnqIuoGqSnUGE=', 'Wlt+xKwDUMonGwG7Nft9CTzELR0=', 'Z0E2pGVhvZ9W4xxnmwZ+fl5v4eM=', 'rwg3LZZpX4NCaKXyZrwLCXwA7do=', 'n0Rgeh72CLkgowuiEnD8kXN1BjU=', '54FhWGvi0cGKWqPcC8lGZObeyDE=', 'lMUqRINbKUqt7dlYpUfFbbaKUec=', '3c4GtS51hOeJgCrzwEnieoRQl+Y=', 'yQF6e3lJEQL1rfcMTzaQfYuHmiY=', '73gW9Db+T+auwjCC1pKy2i5EuLM=', 'bi7qfWlEwCvkWNq2tBByU+UMlrM=', 'aNXJ8DfeOsAcwdZlDos85/STc1g=', 'eLMtNcLVGJj8Ybw3LS2bgHO/I2o=', // v1.0 'H7L2crM/z2r0M0UiHqsagAoDsT0=', ]; module.exports = function(sails) { return function(results, cb) { // If the Grunt hook is explicitly turned off, don't worry about this check. if (sails.config.hooks.grunt === false) { return cb(); } // Load this app's package.json and dependencies var appPackageJSON; try { appPackageJSON = JSON.parse(fs.readFileSync(path.resolve(process.cwd(), 'package.json'))); } catch (unusedErr) { // If there's an error loading the package.json, just ignore it. // This is indicative of a bigger issue that will likely be dealt with elsewhere, // and is not the responsibility of the "check for Grunt hook" code. return cb(); } // If this app has sails-hook-grunt installed, then we're all good. if ( (appPackageJSON.dependencies && appPackageJSON.dependencies['sails-hook-grunt']) || (appPackageJSON.devDependencies && appPackageJSON.devDependencies['sails-hook-grunt']) ) { return cb(); } // Attempt to hash the contents of this app's Gruntfile.js (if it has one). var hash = (function() { try { var Gruntfile = fs.readFileSync(path.resolve(process.cwd(), 'Gruntfile.js')); return crypto.createHash('sha1').update(Gruntfile).digest('base64'); } catch (unusedErr) { return null; } })();//ˆ // If we didn't get a hash, it's probably because the Gruntfile doesn't exist. // If that's the case, then there's nothing to worry about (Grunt won't run), // and if there was some other error, that's weird but not something we need to // deal with in this advisory warning code. if (!hash) { return cb(); } // Check the hash against known Gruntfiles, and if it matches either, log the warning. // Otherwise, it means the file's been customized, and we'll just trust that the user // knows what they're doing. if (_.contains(GRUNT_FILE_HASHES, hash)) { sails.log.debug('Warning: Grunt functionality may not work properly with your current configuration.'); sails.log.debug('Run `npm install sails-hook-grunt --save` to continue using Grunt with your app.'); sails.log.debug(); } return cb(); }; }; ================================================ FILE: lib/app/private/controller/README.md ================================================ # Actions in Sails In Sails, an _action_ is a named request handler that is intended to be bound directly to a route in an app's `config/routes.js` file. Actions may be loaded from disk (typically from the `api/controllers` project folder and subfolders), from runtime configuration (in `sails.config.controllers.moduleDefinitions`) or added by hooks (using `sails.registerAction`). ##### Benefits of actions By using actions to represent the majority of the code that is executed at runtime in user apps, we get the following benefits: * An easy way to reference all of the available request handlers -- calling `sails.getActions()` returns a list of all actions registered by the user _and_ by hooks like the blueprints hook. * Simplified user routing -- instead of hooks adding routes whose address must be configured separately (e.g. the `/csrfToken` route) or referenced using a separate syntax (e.g. the `{response: 'notFound'}` route target syntax), hooks simply provide an action for the app developer to route to in `config/routes.js` using a single, streamlined syntax. * Ability to easily add middleware that applies only to app-level code, and not to all routes. Instead of binding to `/*` in a hook and then excluding assets (or forgetting to), `registerActionMiddleware` can be used to add handlers _only_ to actions. ##### Should my hook register an action, bind a route, or both? If you are creating a hook that adds request handlers to a Sails app, and you want the app developer to be able to bind their own routes to those request handlers, then you should register the handler as an action (the core "blueprints", "responses" and "views" hooks are good examples of this). This doesn't mean you can't also bind routes directly within the hook (this is how blueprint RESTful routes are created, for example), especially if the user may override your hook's action with their own (as may happen with blueprint or response actions). On the other hand, if your hook creates a request handler that is not intended to be bound directly by the app developer, and/or it is not intended to be overridden, you may not want to register that handler as an action. Handlers that _modify_ requests and then call `next()` (for example the `addLocalizationMethod` handler in the core `i18n` hook) generally fall into this category. Somewhere in the middle lie hooks that create a request handler that you may want the app developer to be able to bind directly, but which should _not_ be overridable. A good example of this is the core `CSRF` hook; besides adding CSRF protection to an app's routes, it also provides an action which returns the current CSRF token. It is desirable to allow this action to be bound to a route in the `config/routes.js` file (so that the app developer can choose the address to bind it to), but it is _not_ desirable for the user to override the action with their own code. In this case, a hook can register the action as "not overridable" by beginning namespacing it under `_.` (e.g. `sails.registerAction(myAction, '_.csrf.return-token')`). > Alternate idea: have hooks register actions under UPPERCASED version of their name, e.g. `CSRF.return-token` ##### Action middleware Action middleware are functions that are intended to _modify_ actions (or more accurately, the requests that the actions handle). You may register middleware that affects a single action, a subset of actions, or all actions. Note that action middleware _only_ affects actions; if you need to modify _all_ requests (particularly, if you need to modify requests that return assets), you should still bind a route directly to `/*` in your hook. ## Action-related methods in Sails ### Public methods ##### `sails.registerAction(action, identity)` Registers an action in Sails. It is very similar in function to the private `registerAction` method, with the notable exception that there is no `force` argument: if a call to `registerAction` results in an attempt to overwrite an existing key in the actions dictionary, the call will trigger an error with code `E_CONFLICT`. ##### `sails.getActions()` Returns a shallow clone of the internal Sails actions dictionary. This is a flat (i.e. one-level) dictionary where the keys are the kebab-cased, dash-delimited action identities, and the values are the action functions (all actions in the dictionary will have been converted to `req, res` functions at this point). ##### `sails.registerActionMiddleware(middleware, includeActions, excludeActions)` Registers middleware that will run before the specified actions. Middleware should be `req, res, next` functions. The `includeActions` and `excludeActions` arguments are strings or arrays of strings describing the actions that the middleware should (or should not) be attached to, using `*` as a wildcard to cover multiple actions at once. Examples: **Register middleware that affects all actions**: ``` sails.registerActionMiddleware(mustBeLoggedIn, '*') ```` **Register middleware that affects all `user` actions**: ``` sails.registerActionMiddleware(mustBeLoggedIn, 'user.*') ```` **Register middleware that affects all `user` and `pet` actions**: ``` sails.registerActionMiddleware(mustBeLoggedIn, ['user.*', 'pet.*']) ```` **Register middleware that affects all `user` and `pet` actions _except_ user.hello**: ``` sails.registerActionMiddleware(mustBeLoggedIn, ['user.*', 'pet.*'], 'user.hello') ```` **Register middleware that affects all `user` and `pet` actions _except_ ones namespaced under "public"**: ``` sails.registerActionMiddleware(mustBeLoggedIn, ['user.*', 'pet.*'], ['user.public.*', 'pet.public.*']) ```` ### Private utilities > _Please do not use these in a hook (even a core hook) or in any userland code! They may change at any time, without warning!_ ##### `loadActionModules()` When Sails loads, this method loads all of the files underneath the controllers directory (`api/controllers` by default) and attempts to parse them into actions that can be bound to routes. File in the controllers directory can either be pascal-cased and ending in "Controller" (e.g. MyController.js), in which case they are expected to be _dictionaries_ of actions, or else kebab-cased and lowercased (e.g. my-action.js) in which case they are expected to contain a single action. An action may be a function which accepts `req` and `res` as arguments, or a [node-machine](http://node-machine.org) definition which will be parsed by [machine-as-action](https://github.com/treelinehq/machine-as-action). After actions are loaded from disk, any actions specified under the `sails.config.controllers.moduleDefinitions` config key are merged on top of those actions. This allows Sails apps to be constructed dynamically at runtime. Note that this method is called internally by Sails _after_ hooks have loaded (or in the case of a `Sails.reloadModules` call, after they have _reloaded_). This ensures that user actions always take precedence over those added by hooks. ##### `helpRegisterAction(action, identity, [force])` This method takes an _action_ (in the form of a function or a machine definition, which is transformed into a function via `machine-as-action`) and adds it to the internal `Sails._actions` dictionary under the key specified by `identity`. Keys in the internal dictionary can be overridden by setting the `force` argument to `true`; otherwise any conflict will result in an error being thrown. ================================================ FILE: lib/app/private/controller/help-register-action.js ================================================ /** * Module dependencies */ var util = require('util'); var assert = require('assert'); var _ = require('@sailshq/lodash'); var flaverr = require('flaverr'); var machineAsAction = require('machine-as-action'); /** * helpRegisterAction() * * @param {SailsApp} sails * @param {Function|Dictionary} action [either a req/res function, or an actions2 (i.e. machine-as-action) definition] * @param {String} identity [the identity to register this action as] * @param {Boolean} force * * @throws {Error} If there is a conflicting, previously-registered action * @property {String} code (==='E_CONFLICT') * @property {String} identity [the conflicting identity (always the same as what was passed in)] * * @throws {Error} If the action is invalid * @property {String} code (==='E_INVALID') * @property {String} identity [the action identity (always the same as what was passed in)] * @property {Error} origError [the original (raw/underlying) error from `machine-as-action`] */ module.exports = function helpRegisterAction(sails, action, identity, force) { assert(_.isObject(sails) && _.isObject(sails._actions), new Error('Consistency violation: `sails` (a Sails app instance) should be passed in as the first argument.')); assert(_.isFunction(action) || _.isObject(action), new Error('Consistency violation: `action` (2nd arg) should be provided as either a req/res/next function or a machine def (actions2), but instead, got: '+util.inspect(action,{depth:null}))); assert(_.isString(identity), new Error('Consistency violation: Identity should be provided as a string, but instead, got: '+util.inspect(identity,{depth:null}))); // Get a reference to the Sails private actions hash. var actions = sails._actions; // Make sure identity is lowercased. identity = identity.toLowerCase(); // Identities should only have letters, numbers, dots, dashes and slashes. var IS_VALID_ACTION_IDENTITY_RX = /^[a-z_\$][a-z0-9-_.\$]*(\/[a-z_\$][a-z0-9-_\$.]*)*$/; if (!identity.match(IS_VALID_ACTION_IDENTITY_RX)) { throw flaverr({ name: 'userError', code: 'E_INVALID_ACTION_IDENTITY' }, new Error('Could not register action with invalid identity `' + identity + '`')); } // If we already registered an action with this identity, bail unless `force` is true. if (actions[identity] && !force) { throw flaverr({ name: 'userError', code: 'E_CONFLICT', identity: identity}, new Error('The action `' + identity + '` could not be registered because it conflicts with a previously-registered action.')); } // If the action is already a function, hope it's a req/res function // and save it in our set of actions. if (_.isFunction(action)) { actions[identity] = action; } // Otherwise try to interpret it as an actions2 definition and build a Callable: else { try { actions[identity] = machineAsAction(_.extend({ implementationSniffingTactic: sails.config.implementationSniffingTactic||undefined, }, action)); } catch (e) { throw flaverr({ name: 'userError', code: 'E_INVALID', identity: identity, origError: e}, new Error('The action `' + identity + '` could not be registered. It looks like a machine definition (actions2), but it could not be used to build an action.\nDetails: '+e.stack)); } } // Set the _middlewareType, which is used when the log level is "silly" to // identify what kind of a thing a route address is bound to. actions[identity]._middlewareType = actions[identity]._middlewareType || 'ACTION: ' + identity; }; ================================================ FILE: lib/app/private/controller/load-action-modules.js ================================================ /** * Module dependencies. */ var path = require('path'); var _ = require('@sailshq/lodash'); var includeAll = require('include-all'); var flaverr = require('flaverr'); var helpRegisterAction = require('./help-register-action'); /** * loadActionModules() * * @param {SailsApp} sails * @param {Function} cb */ module.exports = function loadActionModules (sails, cb) { sails.config.paths = sails.config.paths || {}; sails.config.paths.controllers = sails.config.paths.controllers || 'api/controllers'; // Keep track of actions loaded from disk, so we can detect conflicts. var actionsLoadedFromDisk = {}; // Load all files under the controllers folder. includeAll.optional({ dirname: sails.config.paths.controllers, filter: /(^[^.]+\.(?:(?!md|txt).)+$)/, flatten: true, keepDirectoryPath: true }, function(err, files) { if (err) { return cb(err); } try { // Set up a var to hold a list of invalid files. var garbage = []; // Traditional controllers are PascalCased and end with the word "Controller". var traditionalRegex = new RegExp('^((?:(?:.*)/)*([0-9A-Z][0-9a-zA-Z_]*))Controller\\..+$'); // Actions are kebab-cased. var actionRegex = new RegExp('^((?:(?:.*)/)*([a-z][a-z0-9-]*))\\..+$'); // Loop through all of the files returned from include-all. _.each(files, function(moduleDef) { // Get the original filepath of the action or controller. var filePath = moduleDef.globalId; // If the filepath starts with a dot, ignore it. if (filePath[0] === '.') {return;} // If the file is in a subdirectory, transform any dots in the subdirectory // path into slashes. if (path.dirname(filePath) !== '.') { filePath = path.dirname(filePath).replace(/\./g, '/') + '/' + path.basename(filePath); } // Declare a var to hold the eventual action identity. var identity = ''; // Attempt to match the file path to the pattern of a traditional controller file. var match = traditionalRegex.exec(filePath); // Is it a traditional controller? if (match) { // If it looks like a traditional controller, but it's not a dictionary, // throw it in the can. if (!_.isObject(moduleDef) || _.isArray(moduleDef) || _.isFunction(moduleDef)) { return garbage.push(filePath); } // Get the controller identity (e.g. /somefolder/somecontroller) identity = match[1]; // Loop through each action in the controller file's dictionary. _.each(moduleDef, function(action, actionName) { // Ignore strings (this could be the "identity" property of a module). if (_.isString(action)) {return;} // Give the action name `_config` special treatement: just merge it into the blueprint // config instead of trying to load it as an action. if (actionName === '_config') { if (sails.config.blueprints) { sails.config.blueprints._controllers[identity.toLowerCase()] = action; } return; } // The action identity is the controller identity + the action name, // with path separators transformed to dots. // e.g. somefolder.somecontroller.dostuff var actionIdentity = (identity + '/' + actionName).toLowerCase(); // If the action identity matches one we've already loaded from disk, bail. if (actionsLoadedFromDisk[actionIdentity]) { throw flaverr({ name: 'userError', code: 'E_CONFLICT', identity: actionIdentity}, new Error('The action `' + actionName + '` in `' + filePath + '` conflicts with a previously-loaded action.')); } // Attempt to load the action into our set of actions. // Since the following code might throw E_CONFLICT errors, we'll inject a `try` block here // to intercept them and wrap the Error. try { helpRegisterAction(sails, action, actionIdentity, true); } catch (e) { switch (e.code) { case 'E_CONFLICT': // Improve error message with addtl contextual information about where this action came from. // (plus a slightly better stack trace) throw flaverr({ name: 'userError', code: 'E_CONFLICT', identity: actionIdentity }, new Error('Failed to register `' + actionName + '`, an action in the controller loaded from `'+filePath+'` because it conflicts with a previously-registered action.') ); default: throw e; } }//</catch> // Flag that an action with the given identity was successfully loaded from disk. actionsLoadedFromDisk[actionIdentity] = true; }); } // </ is it a traditional controller? > // Okay, it's not a traditional controller. Is it an action? // Attempt to match the file path to the pattern of an action file, // and make sure it is either a function OR a dictionary containing // a function as its `fn` property. else if ((match = actionRegex.exec(filePath)) && (_.isFunction(moduleDef) || !_.isUndefined(moduleDef.machine) || !_.isUndefined(moduleDef.friendlyName) || _.isFunction(moduleDef.fn))) { // The action identity is the same as the module identity // e.g. somefolder/dostuff var actionIdentity = match[1].toLowerCase(); if (actionsLoadedFromDisk[actionIdentity]) { throw flaverr({ name: 'userError', code: 'E_CONFLICT', identity: actionIdentity }, new Error('The action `' + _.last(actionIdentity.split('/')) + '` in `' + filePath + '` conflicts with a previously-loaded action.')); } // Attempt to load the action into our set of actions. // This may throw an error, which will be caught below. try { helpRegisterAction(sails, moduleDef, actionIdentity, true); } catch (e) { switch (e.code) { // Improve Error with addtl contextual information about where this action came from. case 'E_CONFLICT': throw flaverr({ name: 'userError', code: 'E_CONFLICT', identity: actionIdentity }, new Error( 'Failed to register `' + _.last(actionIdentity.split('/')) + '`, an action loaded from `'+filePath+'` because it conflicts with a previously-registered action.' )); default: throw e; } }//</catch> // Flag that an action with the given identity was successfully loaded from disk. actionsLoadedFromDisk[actionIdentity] = true; } // </ is it an action?> // Otherwise give up on this file, it's GARBAGE. // No, no, it's probably a very nice file but it's // no controller as far as we're concerned. else { garbage.push(filePath); } // </ it is garbage> }); // </each(file from includeAll)> // Complain about garbage. if (garbage.length) { sails.log.warn('---------------------------------------------------------------------------'); sails.log.warn('Files in the `controllers` directory may be traditional controllers or \n' + 'action files. Traditional controllers are dictionaries of actions, with \n' + 'pascal-cased filenames ending in "Controller" (e.g. MyGreatController.js).\n' + 'Action files are kebab-cased (e.g. do-stuff.js) and contain a single action.\n'+ 'The following file'+(garbage.length > 1 ? 's were' : ' was')+' ignored for not meeting those criteria:'); _.each(garbage, function(filePath){sails.log.warn('- '+filePath);}); sails.log.warn('----------------------------------------------------------------------------\n'); } // (Shallow) merge stuff from sails.config.controllers.moduleDefinitions on top of any loaded files. // Note that the third argument (force) to `helpRegisterAction` is `true`, so there's no danger // of identity conflicts. Actions defined in `moduleDefinitions` will override anything else. _.each(_.get(sails, 'config.controllers.moduleDefinitions') || {}, function(action, actionIdentity) { helpRegisterAction(sails, action, actionIdentity, true); }); } catch (e) { return cb(e); } // Get a list of the action identities. var actionIdentities = _.keys(sails._actions); // Flag indicating that warnings were raised (for formatting purposes). var raisedWarnings = false; // Now that we have all the actions loaded, loop through the registered action middleware // and raise a warning about any that don't correspond to a registered action. _.each(sails._actionMiddleware, function(fns, target) { // Iterate over the list of action globs (e.g. 'foo', 'foo/bar', 'foo/bar/*', '!baz/boop') that a middleware is targeting. _.each(_.map(target.split(','), _.trim), function(actionGlob) { // Ignore * (it matches everything) and anything starting with '!' // (doesn't matter if a middleware is NOT applied to a non-existent action). if (actionGlob === '*' || actionGlob[0] === '!') { return; } // If the glob doesn't contain a wildcard, and it exactly matches a known action identity, it's ok. if (actionGlob.indexOf('*') === -1) { if (actionIdentities.indexOf(actionGlob) > -1) { return; } } // Otherwise, if one of the known action identities would match against the glob, it's okay. else { var actionGlobWithoutWildcard = actionGlob.replace(/\/\*$/, ''); if (_.find(actionIdentities, function(actionIdentity) { return actionIdentity.indexOf(actionGlobWithoutWildcard) === 0; })) { return; } } // Otherwise, construct a warning using the _middlewareType properties (if available) of the middleware functions // that were mapped to this action glob. var warning = 'Action middleware '; warning += (function(){ var fnDescs = _.reduce(fns, function(memo, fn) { if (fn._middlewareType) { memo.push(fn._middlewareType); } return memo; }, []); if (fnDescs.length) { return '(' + fnDescs.join(', ') + ') '; } return ''; })();//† warning += 'was bound to a target `' + actionGlob + '` that doesn\'t match any registered actions.'; sails.log.warn(warning); raisedWarnings = true; });//∞ });//∞ // If we raised any warnings, add an extra line break afterwards. if (raisedWarnings) { console.log(); } // All done. return cb(); }); // </includeAll> }; ================================================ FILE: lib/app/private/exposeGlobals.js ================================================ /** * Module dependencies. */ var util = require('util'); var _ = require('@sailshq/lodash'); var flaverr = require('flaverr'); /** * exposeGlobals() * * Expose certain global variables * (if config says so) * * @throws E_BAD_GLOBAL_CONFIG * * @this {SailsApp} * @api private */ module.exports = function exposeGlobals() { var sails = this; // Implicit default for globals is `false`, to allow for intuitive programmatic // usage of `sails.lift()`/`sails.load()` in automated tests, command-line scripts, // scheduled jobs, etc. // // > Note that this is not the same as the boilerplate `config/globals.js` settings, // > since the use of certain global variables is still the recommended approach for // > the code you write in your Sails app's controller actions, etc. if (_.isUndefined(sails.config.globals)) { sails.config.globals = false; return; } // If globals config is provided, it must be either `false` or a dictionary. else if (sails.config.globals !== false && (!_.isObject(sails.config.globals) || _.isArray(sails.config.globals) || _.isFunction(sails.config.globals))) { throw flaverr({ name: 'userError', code: 'E_BAD_GLOBAL_CONFIG' }, new Error('As of Sails v1, if `sails.config.globals` is defined, it must either be `false` or a dictionary (plain JavaScript object) or `false`. But instead, got: '+util.inspect(sails.config.globals, {depth:null})+'\n> Note: if no globals config is specified, Sails will now assume `false` (no globals). This is to allow for more intuitive programmatic usage.\nFor more info, see http://sailsjs.com/config/globals')); } // Globals explicitly disabled. if (sails.config.globals === false) { sails.log.verbose('No global variables will be exposed.'); return; } sails.log.verbose('Exposing global variables... (you can customize/disable this by modifying the properties in `sails.config.globals`. Set it to `false` to disable all globals.)'); // `sails.config.globals._` must be false or an object. // (it's probably a function with lots of extra properties, but to future-proof, we'll allow any type of object) if (sails.config.globals._ !== false) { if (!_.isObject(sails.config.globals._)) { throw flaverr({ name: 'userError', code: 'E_BAD_GLOBAL_CONFIG' }, new Error('As of Sails v1, `sails.config.globals._` must be either `false` or a locally-installed version of Lodash (typically `require(\'lodash\')`). For more info, see http://sailsjs.com/config/globals')); } global['_'] = sails.config.globals._; } // `sails.config.globals.async` must be false or an object. // (it's probably a plain object aka dictionary, but to future-proof, we'll allow any type of object) if (sails.config.globals.async !== false) { if (!_.isObject(sails.config.globals.async)) { throw flaverr({ name: 'userError', code: 'E_BAD_GLOBAL_CONFIG' }, new Error('As of Sails v1, `sails.config.globals.async` must be either `false` or a locally-installed version of `async` (typically `require(\'async\')`) For more info, see http://sailsjs.com/config/globals')); } global['async'] = sails.config.globals.async; } // `sails.config.globals.sails` must be a boolean if (sails.config.globals.sails !== false) { if (sails.config.globals.sails !== true) { throw flaverr({ name: 'userError', code: 'E_BAD_GLOBAL_CONFIG' }, new Error('As of Sails v1, `sails.config.globals.sails` must be either `true` or `false` (Tip: you may need to uncomment the `sails` setting in your `config/globals.js` file). For more info, see http://sailsjs.com/config/globals')); } global['sails'] = sails; } // `sails.config.globals.models` must be a boolean. // `orm` hook takes care of actually globalizing models and adapters (if enabled) if (sails.config.globals.models !== false && sails.config.globals.models !== true) { throw flaverr({ name: 'userError', code: 'E_BAD_GLOBAL_CONFIG' }, new Error('As of Sails v1, `sails.config.globals.models` must be either `true` or `false` (you may need to uncomment the `models` setting in your `config/globals.js` file). For more info, see http://sailsjs.com/config/globals')); } // `services` hook takes care of globalizing services (if enabled) // It does this by default for now, so that we don't have to document configuring // services, which we're trying to phase out in favor of helpers. }; ================================================ FILE: lib/app/private/initialize.js ================================================ /** * Module dependencies */ var _ = require('@sailshq/lodash'); var async = require('async'); /** * Sails.prototype.initialize() * * Start the Sails server * NOTE: sails.load() should be run first. * * @param {Function?} callback [optional] * * @api private */ module.exports = function initialize(cb) { var sails = this; // Callback is optional cb = cb || function(err) { if (err) { sails.log.error(err); } }; // Indicate that server is starting sails.log.verbose('Starting app at ' + sails.config.appPath + '...'); var listeners = { sigusr2: function() { sails.lower(function() { process.kill(process.pid, 'SIGUSR2'); }); }, sigint: function() { sails.lower(function (){ process.exit(); }); }, sigterm: function() { sails.lower(function (){ process.exit(); }); }, exit: function() { if (!sails._exiting) { sails.lower(); } } }; // Add "beforeShutdown" events process.once('SIGUSR2', listeners.sigusr2); process.on('SIGINT', listeners.sigint); process.on('SIGTERM', listeners.sigterm); process.on('exit', listeners.exit); sails._processListeners = listeners; // Run the app bootstrap sails.runBootstrap(function afterBootstrap(err) { if (err) { sails.log.error('Bootstrap encountered an error: (see below)'); return cb(err); } // Fire the `ready` event // Since Express 4, the router is built in, so middlewares are divided between // pre-route and post-route. The way to tell when to do the split is via the // ready event // More info in lib/hooks/http/initialize.js:378 sails.emit('ready'); // Now loop over each hook, and if it exposes a `handleLift` function, then run it. // (this is used by attached servers, etc.) if (!_.isObject(sails.hooks)) { return cb(new Error('Consistency violation: `sails.hooks` should be a dictionary.')); } async.each(Object.keys(sails.hooks), function (hookName, next){ if (!_.isFunction(sails.hooks[hookName].handleLift)) { return next(); } return sails.hooks[hookName].handleLift(next); }, function (err){ if (err) { return cb(err); } return cb(null, sails); }); }); }; ================================================ FILE: lib/app/private/inspect.js ================================================ /** * Module dependencies */ var util = require('util'); var _ = require('@sailshq/lodash'); /** * Sails.prototype.inspect() * * The string that should be returned when this `Sails` instance * is passed to `util.inspect()` (i.e. when logged w/ `console.log()`) * * @return {String} */ module.exports = function inspect () { var sails = this; return util.format('\n'+ ' |> %s', this.toString()) + '\n' + '\\___/ For help, see: http://sailsjs.com/documentation/concepts/'+ '\n\n' + 'Tip: Use `sails.config` to access your app\'s runtime configuration.'+ '\n\n' + util.format('%d Models:\n', _(sails.models).toArray().value().length) + _(sails.models).toArray().filter(function (it) {return !it.junctionTable;}).pluck('globalId').value() + '\n\n' + // util.format('%d Actions:\n', Object.keys(sails.getActions()).length)+ // _(sails.getActions()).keys().map(function (it) {return _.camelCase(it.replace(/^.*(\/[^\/]+)$/, '$1'));}).value() + // '\n\n' + // util.format('%d Controllers:\n', _(sails.controllers).toArray().value().length)+ // _(sails.controllers).toArray().pluck('globalId').map(function (it) {return it+'Controller';}).value() + // '\n\n' + // 'Routes:\n'+ // _(sails.routes).toArray().filter(function (it) {return !it.junctionTable;}).pluck('globalId').map(function (it) {return it+'Controller';}).value() + // '\n\n' + util.format('%d Hooks:\n', _(sails.hooks).toArray().value().length)+ _(sails.hooks).toArray().pluck('identity').value() + '\n' + ''; }; ================================================ FILE: lib/app/private/isLocalSailsValid.js ================================================ /** * Module dependencies */ var fs = require('fs'); var path = require('path'); var semver = require('semver'); var CaptainsLog = require('captains-log'); var Err = require('../../../errors'); // FUTURE: change the name of this to `isLocalSailsValidSync()` /** * Check if the specified installation of Sails is valid for the specified project. * * @param sailsPath * @param appPath */ module.exports = function isLocalSailsValid(sailsPath, appPath) { var sails = this; var appPackageJSON; var appDependencies; // Has no package.json file if (!fs.existsSync(appPath + '/package.json')) { Err.warn.noPackageJSON(); } else { // Load this app's package.json and dependencies try { appPackageJSON = JSON.parse(fs.readFileSync(path.resolve(appPath, 'package.json'), 'utf8')); } catch (unusedErr) { Err.warn.notSailsApp(); return; } appDependencies = appPackageJSON.dependencies; // Package.json exists, but doesn't list Sails as a dependency if (!(appDependencies && appDependencies.sails)) { Err.warn.notSailsApp(); return; } } // Ensure the target Sails exists if (!fs.existsSync(sailsPath)) { return false; } // Read the package.json in the local installation of Sails var sailsPackageJSON; try { sailsPackageJSON = JSON.parse(fs.readFileSync(path.resolve(sailsPath, 'package.json'), 'utf8')); } catch (unusedErr) { // Local Sails has a missing or corrupted package.json Err.warn.badLocalDependency(sailsPath, appDependencies.sails); return; } // Lookup sails dependency requirement in app's package.json var requiredSailsVersion = appDependencies.sails; // If you're using a `git://` sails dependency, you probably know // what you're doing, but we'll let you know just in case. var expectsGitVersion = requiredSailsVersion.match(/^git:\/\/.+/); // FUTURE: expand this to check the various other permutations // of extremely loose SVRs (e.g. Github dependencies, `*`, `>=0.0.0`, etc.) if (expectsGitVersion) { var log = sails.log ? sails.log : CaptainsLog(); log.blank(); log.debug('NOTE:'); log.debug('This app depends on an unreleased version of Sails:'); log.debug(requiredSailsVersion); log.blank(); } // Ignore `latest`, `beta` and `edge` // (kind of like how we handle specified git:// deps) var expectsLatest = requiredSailsVersion === 'latest'; // if (expectsLatest) { // // FUTURE: potentially log something here (need to test if it's annoying or not...) // } var expectsBeta = requiredSailsVersion === 'beta'; // if (expectsBeta) { // // FUTURE: potentially log something here (need to test if it's annoying or not...) // } var expectsEdge = requiredSailsVersion === 'edge'; // if (expectsEdge) { // // FUTURE: potentially log something here (need to test if it's annoying or not...) // } // Error out if it has the wrong version in its package.json if (!expectsLatest && !expectsBeta && !expectsEdge && !expectsGitVersion) { // Use semver for version comparison if (!semver.satisfies(sailsPackageJSON.version, requiredSailsVersion)) { Err.warn.incompatibleLocalSails(requiredSailsVersion, sailsPackageJSON.version); } } // If we made it this far, the target Sails installation must be OK return true; }; ================================================ FILE: lib/app/private/isSailsAppSync.js ================================================ /** * Module dependencies */ var fs = require('fs'); var path = require('path'); /** * Check if the specified appPath contains something that looks like a Sails app. * * @param {String} appPath */ module.exports = function isSailsAppSync(appPath) { // Has no package.json file if (!fs.existsSync(path.join(appPath, 'package.json'))) { return false; } // Package.json exists, but doesn't list Sails as a dependency var appPackageJSON; try { appPackageJSON = JSON.parse(fs.readFileSync(path.resolve(appPath, 'package.json'), 'utf8')); } catch (unusedErr) { return false; } var appDependencies = appPackageJSON.dependencies; if (!(appDependencies && appDependencies.sails)) { return false; } return true; }; ================================================ FILE: lib/app/private/loadHooks.js ================================================ /** * Module dependencies */ var util = require('util'); var _ = require('@sailshq/lodash'); var async = require('async'); var defaultsDeep = require('merge-defaults');// « TODO: Get rid of this var __hooks = require('../../hooks'); /** * @param {SailsApp} sails * @returns {Function} */ module.exports = function(sails) { var Hook = __hooks(sails); // Keep an array of all the hook timeouts. // This way if a hook fails to load, we can clear all the timeouts at once. var hookTimeouts = []; // NOTE: There's no particular reason this (^^) is outside of the function being returned below. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // FUTURE: pull it in below to avoid leading to any incorrect assumptions about race conditions, etc.) // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /** * Resolve the hook definitions and then finish loading them * * @api private */ return function initializeHooks(hooks, cb) { // ============================================================================== // < inline function declarations > // ██╗ ██╗███╗ ██╗██╗ ██╗███╗ ██╗███████╗ ███████╗███╗ ██╗ ██████╗ ███████╗███████╗███████╗ ██╗ // ██╔╝ ██║████╗ ██║██║ ██║████╗ ██║██╔════╝ ██╔════╝████╗ ██║ ██╔══██╗██╔════╝██╔════╝██╔════╝ ╚██╗ // ██╔╝ ██║██╔██╗ ██║██║ ██║██╔██╗ ██║█████╗ █████╗ ██╔██╗ ██║ ██║ ██║█████╗ █████╗ ███████╗ ╚██╗ // ╚██╗ ██║██║╚██╗██║██║ ██║██║╚██╗██║██╔══╝ ██╔══╝ ██║╚██╗██║ ██║ ██║██╔══╝ ██╔══╝ ╚════██║ ██╔╝ // ╚██╗ ██║██║ ╚████║███████╗██║██║ ╚████║███████╗ ██║ ██║ ╚████║ ██████╔╝███████╗██║ ███████║ ██╔╝ // ╚═╝ ╚═╝╚═╝ ╚═══╝╚══════╝╚═╝╚═╝ ╚═══╝╚══════╝ ╚═╝ ╚═╝ ╚═══╝ ╚═════╝ ╚══════╝╚═╝ ╚══════╝ ╚═╝ // // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // FUTURE: extrapolate the following three inline function definitions // into separate files. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /** * FUTURE: extrapolate * @param {[type]} id [description] * @return {[type]} [description] */ function prepareHook(id) { var rawHookFn = hooks[id]; // Backwards compatibility: if (rawHookFn === 'false') { // FUTURE: Do not allow the string "false" here (now that all environment variables // are handled via rttc.parseHuman, this is no longer necessary) sails.log.debug('The string "false" was configured for `sails.config.hooks[\''+id+'\']`.'); sails.log.debug('For compatibility\'s sake, automatically changing this to `false` (boolean).'); sails.log.debug('(Note that this backwards-compatibility check will be removed in a future'); sails.log.debug('release of Sails, so be sure to update this app ASAP.)'); rawHookFn = false; }//>- // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // COMPATIBILITY NOTE: // There used to be a check here, to the effect of this: // ``` // // Check if this hook has a dot in the name. // // If so, something is wrong. // var doesHookHaveDotInName = !!id.match(/\./); // if (doesHookHaveDotInName) { // var partBeforeDot = id.split('.')[0]; // hooks[partBeforeDot] = false; // ``` // // But it was removed in Sails v1, since it was no longer relevant. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // Allow disabling of hooks by setting them to `false`. if (rawHookFn === false) { delete hooks[id]; return; } // Check for invalid hook config if (hooks.userconfig && !hooks.moduleloader) { return cb('Invalid configuration:: Cannot use the `userconfig` hook w/o the `moduleloader` hook enabled!'); } // Handle folder-defined modules (default to index.js) // Since a hook definition must be a function if (_.isObject(rawHookFn) && !_.isArray(rawHookFn) && !_.isFunction(rawHookFn)) { rawHookFn = rawHookFn.index; } if (!_.isFunction(rawHookFn)) { sails.log.error('Malformed hook! (' + id + ')'); sails.log.error('Hooks should be a function with one argument (`sails`)'); sails.log.error('But instead, got:', rawHookFn); process.exit(1); } // Instantiate the hook var def = rawHookFn(sails); // Mix in an `identity` property to hook definition def.identity = id.toLowerCase(); // If a config key was defined for this hook when it was loaded, // (probably because a user is overridding the default config key) // set it on the hook definition def.configKey = rawHookFn.configKey || def.identity; // New up an actual Hook instance hooks[id] = new Hook(def); }//ƒ /** * Apply a hook's "defaults" property * * FUTURE: extrapolate * * @param {[type]} hook [description] * @return {[type]} [description] */ function applyDefaults(hook) { // Get the hook defaults var defaults = (_.isFunction(hook.defaults) ? hook.defaults(sails.config) : hook.defaults) || {}; // Replace the special __configKey__ key with the actual config key if (hook.defaults.__configKey__ && hook.configKey) { hook.defaults[hook.configKey] = hook.defaults.__configKey__; delete hook.defaults.__configKey__; } defaultsDeep(sails.config, defaults); }//ƒ /** * Load a hook (bind its routes, load any modules and initialize it) * * FUTURE: extrapolate * * @param {[type]} id [description] * @param {Function} cb [description] * @return {[type]} [description] */ function loadHook(id, cb) { // TODO: refactor this^^^ // (no need for an inline function declaration) // Validate `hookTimeout` setting, if present. if (!_.isUndefined(sails.config.hookTimeout)) { if (!_.isNumber(sails.config.hookTimeout) || sails.config.hookTimeout < 1 || Math.floor(sails.config.hookTimeout) !== sails.config.hookTimeout) { return cb(new Error('Invalid `hookTimeout` config! If set, this should be a positive whole number, but instead got `'+sails.config.hookTimeout+'`. Please change this setting, then try lifting again.')); } } var timestampBeforeLoad = Date.now(); var DEFAULT_HOOK_TIMEOUT = 40000; var timeoutInterval = (sails.config[hooks[id].configKey || id] && sails.config[hooks[id].configKey || id]._hookTimeout) || sails.config.hookTimeout || DEFAULT_HOOK_TIMEOUT; var hookTimeout; if (id !== 'userhooks') { hookTimeout = setTimeout(function tooLong() { // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // FUTURE: sniff hook id here to improve error msg, e.g.: // ``` // ((id === 'grunt') ? 'It looks like Grunt is still compiling your assets.' : '...') // ``` // ^^But note that this would require a bit more work: currently, the id here isn't // necessarily the hook that timed out. (It could be a dependent hook.) // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - var err = new Error( 'Sails is taking too long to load.\n'+ '\n'+ '-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --\n'+ ' Troubleshooting tips:\n'+ ' -• Were you still reading/responding to an interactive prompt?\n'+ ' (Whoops, sorry! Please lift again and try to respond a bit more quickly.)\n'+ '\n'+ ' -• Do you have a lot of stuff in `assets/`? Grunt might still be running.\n'+ ' (Try increasing the hook timeout. Currently it is '+(sails.config.hookTimeout||DEFAULT_HOOK_TIMEOUT)+'.\n'+ ' e.g. `sails lift --hookTimeout='+(Math.max(DEFAULT_HOOK_TIMEOUT, 2*(sails.config.hookTimeout||DEFAULT_HOOK_TIMEOUT)))+'`)\n'+ '\n'+ ' -• Is `'+id+'` a custom or 3rd party hook?\n'+ ' (*If* `initialize()` is using a callback, make sure it\'s being called.)\n'+ '-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --\n' ); err.code = 'E_HOOK_TIMEOUT'; cb(err); }, timeoutInterval); hookTimeouts.push(hookTimeout); } hooks[id].load(function(err) { // Sanity check: (see https://trello.com/c/1jCljHHP for an example of a // potential bug this catches) if (!process.nextTick) { return cb(new Error('Consistency violation: Hmm... it looks like something is wrong with Node\'s `process` global. Check it out:\n'+util.inspect(process))); } if (id !== 'userhooks') { clearTimeout(hookTimeout); } if (err) { // Clear all hook timeouts so that the process doesn't hang because // something is waiting for this failed hook to load. _.each(hookTimeouts, function(hookTimeout) {clearTimeout(hookTimeout);}); if (id !== 'userhooks') { sails.log.error('A hook (`' + id + '`) failed to load!'); } sails.emit('hook:' + id + ':error'); // Defer a tick to allow other stuff to happen process.nextTick(function(){ cb(err); }); return; } sails.log.verbose(id, 'hook loaded successfully. ('+(Date.now() - timestampBeforeLoad)+'ms)'); sails.emit('hook:' + id + ':loaded'); // Defer a tick to allow other stuff to happen process.nextTick(function(){ cb(); }); }); }//ƒ // ██╗ ██╗ ██╗███╗ ██╗██╗ ██╗███╗ ██╗███████╗ ███████╗███╗ ██╗ ██████╗ ███████╗███████╗███████╗ ██╗ // ██╔╝ ██╔╝ ██║████╗ ██║██║ ██║████╗ ██║██╔════╝ ██╔════╝████╗ ██║ ██╔══██╗██╔════╝██╔════╝██╔════╝ ╚██╗ // ██╔╝ ██╔╝ ██║██╔██╗ ██║██║ ██║██╔██╗ ██║█████╗ █████╗ ██╔██╗ ██║ ██║ ██║█████╗ █████╗ ███████╗ ╚██╗ // ╚██╗ ██╔╝ ██║██║╚██╗██║██║ ██║██║╚██╗██║██╔══╝ ██╔══╝ ██║╚██╗██║ ██║ ██║██╔══╝ ██╔══╝ ╚════██║ ██╔╝ // ╚██╗██╔╝ ██║██║ ╚████║███████╗██║██║ ╚████║███████╗ ██║ ██║ ╚████║ ██████╔╝███████╗██║ ███████║ ██╔╝ // ╚═╝╚═╝ ╚═╝╚═╝ ╚═══╝╚══════╝╚═╝╚═╝ ╚═══╝╚══════╝ ╚═╝ ╚═╝ ╚═══╝ ╚═════╝ ╚══════╝╚═╝ ╚══════╝ ╚═╝ // // </ inline function declarations (see note above) > // ============================================================================== // Now do a few things, one after another. async.series( { // First load the moduleloader (if any) moduleloader: function(cb) { if (!hooks.moduleloader) { return cb(); } prepareHook('moduleloader'); applyDefaults(hooks['moduleloader']); hooks['moduleloader'].configure(); loadHook('moduleloader', cb); }, // Next load the user config (if any) userconfig: function(cb) { if (!hooks.userconfig) { return cb(); } prepareHook('userconfig'); applyDefaults(hooks['userconfig']); hooks['userconfig'].configure(); loadHook('userconfig', cb); }, // Next get the user hooks (if any), which will be // added to the list of hooks to load userhooks: function(cb) { if (!hooks.userhooks) { return cb(); } prepareHook('userhooks'); applyDefaults(hooks['userhooks']); hooks['userhooks'].configure(); loadHook('userhooks', cb); }, validate: function(cb) { if (hooks.controllers) { sails.log.debug('================================================================================='); sails.log.debug('Ignoring `controllers` hook:'); sails.log.debug('As of Sails v1, `controllers` can no longer be disabled/enabled as hooks.'); sails.log.debug('Instead, Sails core now understands controller actions as first-class citizens.'); sails.log.debug('See the Sails v1.0 upgrade guide: http://sailsjs.com/upgrading'); sails.log.debug('================================================================================='); delete hooks.controllers; } return cb(); }, // Prepare all other hooks prepare: function(cb) { async.each(_.without(_.keys(hooks), 'userconfig', 'moduleloader', 'userhooks'), function (id, cb) { prepareHook(id); // Defer to next tick to allow other stuff to happen process.nextTick(cb); }, cb); }, // Apply the default config for all other hooks defaults: function(cb) { async.each(_.without(_.keys(hooks), 'userconfig', 'moduleloader', 'userhooks'), function (id, cb) { var hook = hooks[id]; applyDefaults(hook); // Defer to next tick to allow other stuff to happen process.nextTick(cb); }, cb); }, // Run configuration method for all other hooks configure: function(cb) { async.each(_.without(_.keys(hooks), 'userconfig', 'moduleloader', 'userhooks'), function (id, cb) { var hook = hooks[id]; try { hook.configure(); } catch (err) { return process.nextTick(function(){ cb(err); }); } // Defer to next tick to allow other stuff to happen process.nextTick(cb); }, cb); }, // Load all other hooks load: function(cb) { async.each(_.without(_.keys(hooks), 'userconfig', 'moduleloader', 'userhooks'), function (id, cb) { sails.log.silly('Loading hook: ' + id); loadHook(id, cb); }, cb); } }, function afterwards(err) { if (err) { return cb(err); } return cb(); } );//</async.series> }; }; ================================================ FILE: lib/app/private/toJSON.js ================================================ /** * Module dependencies */ var _ = require('@sailshq/lodash'); /** * SailsApp.prototype.toJSON() * * Get a JSON-serializable representation of the current Sails app. * * @this {SailsApp} * @returns {JSON} [a JSON-compatible summary of this Sails app] */ module.exports = function toJSON() { // `this` refers to our Sails app instance. // // > Here we set up a local variable, `sails`. This is for familiarity, // > so that we don't accidentally write code in this file that relies on // > access to the Sails global. var sails = this; // Build JSON serializable dictionary that summarizes the Sails app instance. var sailsAppSummary; try { sailsAppSummary = _.reduce(sails, function (_jsonSerializable, val, key) { // Allow `config` to go straight through as-is. // > (non JSON-serializable things will have to be handled later -- // > we don't want to introduce the slowness of an rttc.dehydrate() here) if (key === 'config') { _jsonSerializable[key] = val; } //‡ // Turn `hooks` into an array of hook identities. else if (key === 'hooks') { _jsonSerializable.hooks = _.reduce(val, function (memo, hook, hookName) { memo.push(hookName); return memo; }, []); } //‡ // Turn `models` into an array of "model summary" dictionaries. else if (key === 'models') { _jsonSerializable[key] = _.reduce(val, function (memo, Model) { // Skip virtual models (i.e. junctions) if (Model.junctionTable) { return memo; } // But otherwise, push on a stripped down version of the model. // > (again, any nested, non-JSON-serializable things will have to be handled // > later -- we don't want to introduce the slowness of an rttc.dehydrate() here) memo.push({ identity: Model.identity, globalId: Model.globalId, datastore: Model.datastore, tableName: Model.tableName, hasSchema: Model.hasSchema, primaryKey: Model.primaryKey, attributes: Model.attributes, }); return memo; }, []); } //‡ // Otherwise, this is some other key on the Sails app instance. else { // (So, we'll just ignore it, omitting it from this JSON-serializable value we're building.) }//>- return _jsonSerializable; }, {});//</_.reduce :: sailsAppSummary> } catch (e) { throw new Error('Consistency violation: Unexpected error when attempting to build a JSON-serializable version of the Sails app instance. Details: '+e.stack); } // Return our JSON serializable summary of this Sails app instance. return sailsAppSummary; }; ================================================ FILE: lib/app/private/toString.js ================================================ /** * Module dependencies */ var util = require('util'); /** * Sails.prototype.toString() * * e.g. * ('This is how `sails` looks when toString()ed: ' + sails) * * @returns {String} */ module.exports = function toString () { return util.format('[a %sSails app%s]', this.isLifted ? 'lifted ' : '', this.isLifted && this.config.port ? ' on port '+this.config.port : ''); }; ================================================ FILE: lib/app/register-action-middleware.js ================================================ /** * Module dependencies */ var _ = require('@sailshq/lodash'); var flaverr = require('flaverr'); /** * Sails.prototype.registerActionMiddleware() * * Register an action middleware with Sails. * * > Action middleware runs before the action or actions specified by the `actionsGlobKey`. * * ------------------------------------------------------------------------------------------- * @param {Function|Array} middleware * The `(req,res,next)` function to register, or an array of such functions. * * @param {String} actionsGlobKey * A special, limited glob expression that indicates the action or actions that * this action middleware should apply to. Use * at the end for a wildcard; * e.g. `user/*` will apply to any actions whose identities begin with `user/`. * Use a ! at the beginning to indicate that the action middleware should NOT * apply to the actions specified by the glob, e.g. `!user/foo` or `!user/*`. * * @context {SailsApp} * * @api public */ module.exports = function registerActionMiddleware(middleware, actionsGlobKey) { var sails = this; // -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- // FUTURE: explore how we might extend machine-as-action or implement // something entirely new (e.g. `machine-as-middleware`) that is kinda // like machine-as-action, but where the success response calls `next`) // This would be so that machine defs can be registered as middleware? // -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- if (!_.isArray(middleware)) { middleware = [middleware]; } if (!_.all(middleware, _.isFunction)) { throw flaverr({ name: 'userError', code: 'E_NON_FN_POLICY' }, new Error('Attempted to register action middleware(s) (aka policies) for `' + actionsGlobKey + '` but one or more provided action middlewares (policies) was not a function.')); } // Get or create the array for this glob key. var existingActionMiddlewareRegisteredForGlobKey = sails._actionMiddleware[actionsGlobKey] || []; // Add these middlewares to the array. existingActionMiddlewareRegisteredForGlobKey = existingActionMiddlewareRegisteredForGlobKey.concat(middleware); // Assign the array back to our `_actionMiddleware` dictionary. sails._actionMiddleware[actionsGlobKey] = existingActionMiddlewareRegisteredForGlobKey; }; ================================================ FILE: lib/app/register-action.js ================================================ /** * Module dependencies */ var helpRegisterAction = require('./private/controller/help-register-action'); /** * Sails.prototype.registerAction() * * Register an action with Sails. * * Registered actions may be subsequently bound to routes. * This method will throw an error if an action with the specified * identity has already been registered. * * @param {Function|Dictionary} action [The action to register] * @param {String} identity [The identity of the action] * * @context {SailsApp} * * @throws {Error} If there is a conflicting, previously-registered action, and `force` is not true * @property {String} code (==='E_CONFLICT') * @property {String} identity [the conflicting identity (always the same as what was passed in)] * * @throws {Error} If the action is invalid * @property {String} code (==='E_INVALID') * @property {String} identity [the action identity (always the same as what was passed in)] * @property {Error} origError [the original (raw/underlying) error from `machine-as-action`] * * @api public */ module.exports = function registerAction(action, identity, force) { var sails = this; // Call the private `helpRegisterAction` method. helpRegisterAction(sails, action, identity, force); }; ================================================ FILE: lib/app/reload-actions.js ================================================ /** * Module dependencies. */ var _ = require('@sailshq/lodash'); var async = require('async'); var loadActionModules = require('./private/controller/load-action-modules'); /** * Sails.prototype.reloadActions() * * Reload actions for any hook that has a `registerActions` method. * * @param {Dictionary} options * @property {Array} skipHooks [an Array of identities of hooks to _not_ reload actions for.] * */ module.exports = function reloadActions(options, cb) { var sails = this; // Allow for options to be left out. if (_.isFunction(options)) { cb = options; options = {}; } // Default options to an empty dictionary. else if (!_.isObject(options)) { options = {}; } // Default `hooksToSkip` to an empty array. var hooksToSkip = options.hooksToSkip || []; // The list of hooks we want to reload is the list of all hooks minus the hooks to skip. var hooksToReload = _.difference(_.keys(sails.hooks), hooksToSkip); // Clear the actions dictionary. sails._actions = {}; // Reload the actions. async.each(hooksToReload, function(hookIdentity, next) { if (_.isFunction(sails.hooks[hookIdentity].registerActions)) { sails.hooks[hookIdentity].registerActions(next); } else { return next(); } }, function doneReloadingActions(err) { if (err) {return cb(err);} // Reload the controller actions. loadActionModules(sails, cb); }); }; ================================================ FILE: lib/app/request.js ================================================ /** * Module dependencies. */ var util = require('util'); var _ = require('@sailshq/lodash'); var Transform = require('stream').Transform; var QS = require('querystring'); var detectVerb = require('../util/detect-verb'); /** * Originate a new client request instance and lob it at this Sails * app at the specified route `address`. * * Particularly useful for running unit/integration tests without * actually having to bind the HTTP and/or WebSocket servers to * a TCP port. * * @param {String} address * @param {Object} body * @param {Function} cb * @return {Stream.Readable} * * @api public */ module.exports = function request( /* address, body, cb */ ) { var sails = this; // // Body params may be passed in to DELETE, HEAD, and GET requests, // even though these types of requests don't normally contain a body. // (this method just serializes them as if they were sent in the querystring) // // Normalize usage var address = arguments[0]; var body; var cb; var method; var headers; var url; // Usage: // sails.request(opts, cb) // • opts.url // • opts.method // • opts.params // • opts.headers // // (`opts.url` is required) if (_.isObject(arguments[0]) && arguments[0].url) { url = detectVerb(arguments[0].url).original; method = arguments[0].method || detectVerb(arguments[0].url).verb; headers = arguments[0].headers || {}; body = arguments[0].params || arguments[0].data || {}; } // console.log('called sails.request() '); // console.log('headers: ',headers); // console.log('method: ',method); // Usage: // sails.request(address, [params], cb) if (arguments[2]) { cb = arguments[2]; body = arguments[1]; } if (_.isFunction(arguments[1])) { cb = arguments[1]; } else if (arguments[1]) { body = arguments[1]; } // If route has an HTTP verb (e.g. `get /foo/bar`, `put /bar/foo`, etc.) parse it out, // (unless method or url was explicitly defined) method = method || detectVerb(address).verb; method = method ? method.toUpperCase() : 'GET'; url = url || detectVerb(address).original; // Parse query string (`req.query`) var queryStringPos = url.indexOf('?'); // If this is a GET, HEAD, or DELETE request, treat the "body" // as parameters which should be serialized into the querystring. if (_.isObject(body) && _.contains(['GET', 'HEAD', 'DELETE'], method)) { var stringifiedParams = QS.stringify(body); if (queryStringPos === -1) { url += '?' + stringifiedParams; } else { url = url.substring(0, queryStringPos) + '?' + stringifiedParams; } } // Build HTTP Client Response stream var clientRes = new MockClientResponse(); clientRes.on('finish', function() { // console.log('clientRes finished. Headers:',clientRes.headers); // Only dump the buffer if a callback was supplied if (cb) { // Attempt to read the response buffer into a string try { clientRes.body = clientRes.read(); clientRes.body = clientRes.body.toString(); } catch (unusedErr) {} // Don't include body if it is empty. if (!clientRes.body) {delete clientRes.body;} // Now, if appropriate, parse the body as JSON. // (Attempt to parse as JSON if the content-type response header indicates it // would be a good idea -- and of course if there's a body.) if (!_.isUndefined(clientRes.body) && clientRes.headers['content-type'] === 'application/json') { clientRes.body = JSON.parse(clientRes.body); } // If status code is indicative of an error, send the // response body or status code as the first error argument. if (clientRes.statusCode < 200 || clientRes.statusCode >= 400) { var error = new Error(util.inspect(clientRes.body || clientRes.statusCode)); if (clientRes.body) {error.body = clientRes.body;} error.status = clientRes.statusCode; return cb(error); } else { return cb(undefined, clientRes, clientRes.body); } } }); clientRes.on('error', function(err) { err = err || new Error('Error on response stream'); if (cb) { return cb(err); } else { return clientRes.emit('error', err); } }); // To kick things off, pass `opts` (as req) and `res` to the Sails router sails.router.route({ method: method, url: url, body: body, headers: headers || {} }, { _clientRes: clientRes }); // Return clientRes stream return clientRes; }; function MockClientResponse() { Transform.call(this); } util.inherits(MockClientResponse, Transform); MockClientResponse.prototype._transform = function(chunk, encoding, next) { this.push(chunk); next(); }; ================================================ FILE: lib/hooks/README.md ================================================ #Hooks ## Status > ##### Stability: [2](https://github.com/balderdashy/sails-docs/blob/master/contributing/stability-index.md) - Stable ## Purpose Most of the non-essential Sails core has been pulled into hooks already. These hooks may eventually be pulled out into separate modules, or they may continue to live in the main Sails repo (like Connect middleware). Hooks were introduced to Sails as part of major refactor designed to make the framework more modular and testable. Their primary purpose was originally to pull all but the most minimal functionality of Sails into independent modules. Today, most of the non-essential Sails core are hooks. These hooks may eventually be pulled out into separate modules, or they may continue to live in the main Sails repo (like Connect middleware). This architecture has allowed for built-in hooks to be overridden or disabled, and even for new hooks to be mixed-in to projects. This gave way to hooks becoming a proper plugin system. Nowadays, the goal of hooks is to provide an API that is flexible and powerful enough for plugin developers or folks who need to hack Sails core, but also predictable, documented, and easy to install for end users. See http://sailsjs.com/documentation/concepts/extending-sails/hooks for more information. > **For historical purposes, here is the original proposal from the v0.9 days:** > https://gist.github.com/mikermcneil/5746660 ## FAQ > If you have a question that isn't covered here, please feel free to send a PR adding it to this section (even if you don't have the answer!) ================================================ FILE: lib/hooks/blueprints/README.md ================================================ # sails-hook-blueprints Implements support for the blueprint API in Sails. > This is a core hook in the Sails.js framework. You can override or disable it using your `.sailsrc` file or environment variables. See [Concepts > Configuration](http://sailsjs.com/docs/concepts/configuration) for more information. ## Purpose This hook's responsibilities are: 1. Use `sails.modules` to read blueprints from the user's app into `self.middleware`. 2. Bind shadow routes to blueprint actions and controller actions. 3. Listen for `route:typeUnknown` on `sails`, interpret route syntax which should match a blueprint action, and bind the appropriate middleware (this happens when the Router is loaded, after all the hooks.) ## Help Have questions or having trouble? Click [here](http://sailsjs.com/support). > For more information on overriding core hooks, check out [Extending Sails > Hooks](http://sailsjs.com/documentation/concepts/extending-sails/hooks). ## Bugs   [![NPM version](https://badge.fury.io/js/sails-hook-blueprints.svg)](http://npmjs.com/package/sails-hook-blueprints) To report a bug, [click here](http://sailsjs.com/bugs). ## Contributing Please observe the guidelines and conventions laid out in the [Sails project contribution guide](http://sailsjs.com/documentation/contributing) when opening issues or submitting pull requests. [![NPM](https://nodei.co/npm/sails-hook-blueprints.png?downloads=true)](http://npmjs.com/package/sails-hook-blueprints) ## License The [Sails framework](http://sailsjs.com) is free and open-source under the [MIT License](http://sailsjs.com/license). ================================================ FILE: lib/hooks/blueprints/actionUtil.js ================================================ /** * Module dependencies */ var util = require('util'); var _ = require('@sailshq/lodash'); var flaverr = require('flaverr'); var mergeDefaults = require('merge-defaults');// « TODO: Get rid of this /** * Utility methods used in built-in blueprint actions. * * @type {Object} */ var actionUtil = { /** * Given a Waterline query and an express request, populate * the appropriate/specified association attributes and * return it so it can be chained further ( i.e. so you can * .exec() it ) * * @param {Query} query [waterline query object] * @param {Request} req * @return {Query} */ populateRequest: function(query, req) { var DEFAULT_POPULATE_LIMIT = req._sails.config.blueprints.defaultLimit || 30; var _options = req.options; var aliasFilter = req.param('populate'); var shouldPopulate = _.isUndefined(_options.populate) ? (req._sails.config.blueprints.populate) : _options.populate; // Convert the string representation of the filter list to an Array. We // need this to provide flexibility in the request param. This way both // list string representations are supported: // /model?populate=alias1,alias2,alias3 // /model?populate=[alias1,alias2,alias3] if (typeof aliasFilter === 'string') { aliasFilter = aliasFilter.replace(/\[|\]/g, ''); aliasFilter = (aliasFilter) ? aliasFilter.split(',') : []; } var associations = []; _.each(_options.associations, function(association) { // If an alias filter was provided, override the blueprint config. if (aliasFilter) { shouldPopulate = _.contains(aliasFilter, association.alias); } // Only populate associations if a population filter has been supplied // with the request or if `populate` is set within the blueprint config. // Population filters will override any value stored in the config. // // Additionally, allow an object to be specified, where the key is the // name of the association attribute, and value is true/false // (true to populate, false to not) if (shouldPopulate) { var populationLimit = _options['populate_' + association.alias + '_limit'] || _options.populate_limit || _options.limit || DEFAULT_POPULATE_LIMIT; associations.push({ alias: association.alias, limit: populationLimit }); } }); return actionUtil.populateQuery(query, associations, req._sails); }, /** * Given a Waterline query and Waterline model, populate the * appropriate/specified association attributes and return it * so it can be chained further ( i.e. so you can .exec() it ) * * @param {Query} query [waterline query object] * @param {Model} model [waterline model object] * @return {Query} */ populateModel: function(query, model) { return actionUtil.populateQuery(query, model.associations); }, /** * Given a Waterline query, populate the appropriate/specified * association attributes and return it so it can be chained * further ( i.e. so you can .exec() it ) * * @param {Query} query [waterline query object] * @param {Array} associations [array of objects with an alias * and (optional) limit key] * @return {Query} */ populateQuery: function(query, associations, sails) { var DEFAULT_POPULATE_LIMIT = (sails && sails.config.blueprints.defaultLimit) || 30; return _.reduce(associations, function(query, association) { var options = {}; if (association.type === 'collection') { options.limit = association.limit || DEFAULT_POPULATE_LIMIT; } return query.populate(association.alias, options); }, query); }, /** * Subscribe deep (associations) * * @param {[type]} associations [description] * @param {[type]} record [description] * @return {[type]} [description] */ subscribeDeep: function ( req, record ) { _.each(req.options.associations, function (assoc) { // Look up identity of associated model var ident = assoc[assoc.type]; var AssociatedModel = req._sails.models[ident]; if (req.options.autoWatch) { AssociatedModel._watch(req); } // Subscribe to each associated model instance in a collection if (assoc.type === 'collection') { _.each(record[assoc.alias], function (associatedInstance) { AssociatedModel.subscribe(req, [associatedInstance[AssociatedModel.primaryKey]]); }); } // If there is an associated to-one model instance, subscribe to it else if (assoc.type === 'model' && _.isObject(record[assoc.alias])) { AssociatedModel.subscribe(req, [record[assoc.alias][AssociatedModel.primaryKey]]); } }); }, /** * Parse primary key value for use in a Waterline criteria * (e.g. for `find`, `update`, or `destroy`) * * @param {Request} req * @return {Integer|String} */ parsePk: function ( req ) { var pk = req.options.id || (req.options.where && req.options.where.id) || req.param('id'); // FUTURE: make this smarter... // (e.g. look for actual primary key of model and look for it // in the absence of `id`.) // exclude criteria on id field pk = _.isPlainObject(pk) ? undefined : pk; return pk; }, /** * Parse primary key value from parameters. * Throw an error if it cannot be retrieved. * * @param {Request} req * @return {Integer|String} */ requirePk: function (req) { var pk = module.exports.parsePk(req); // Validate the required `id` parameter if ( !pk ) { var err = new Error( 'No `id` parameter provided.'+ '(Note: even if the model\'s primary key is not named `id`- '+ '`id` should be used as the name of the parameter- it will be '+ 'mapped to the proper primary key name)' ); err.status = 400; throw err; } return pk; }, /** * Parse `criteria` for a Waterline `find` or `update` from all * request parameters. * * @param {Request} req * * @returns {Dictionary} * The normalized WHERE clause * * @throws {Error} If WHERE clause cannot be parsed... * ...whether that's for syntactic reasons (JSON.parse), * or for semantic reasons (Waterline's `forgeStageTwoQuery()`). * @property {String} `name: 'UsageError'` */ parseCriteria: function ( req ) { // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // FUTURE: this should be renamed to `.parseWhere()` // ("criteria" means the entire dictionary, including // `where` -- but also `skip`, `limit`, etc.) // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // Allow customizable blacklist for params NOT to include as criteria. req.options.criteria = req.options.criteria || {}; req.options.criteria.blacklist = req.options.criteria.blacklist || ['limit', 'skip', 'sort', 'populate']; // Validate blacklist to provide a more helpful error msg. var blacklist = req.options.criteria && req.options.criteria.blacklist; if (blacklist && !_.isArray(blacklist)) { throw new Error('Invalid `req.options.criteria.blacklist`. Should be an array of strings (parameter names.)'); } // Look for explicitly specified `where` parameter. var where = req.allParams().where; // If `where` parameter is a string, try to interpret it as JSON. // (If it cannot be parsed, throw a UsageError.) if (_.isString(where)) { try { where = JSON.parse(where); } catch (e) { throw flaverr({ name: 'UsageError' }, new Error('Could not JSON.parse() the provided `where` clause. Here is the raw error: '+e.stack)); } }//>-• // If `where` has not been specified, but other unbound parameter variables // **ARE** specified, build the `where` option using them. if (!where) { // Prune params which aren't fit to be used as `where` criteria // to build a proper where query where = req.allParams(); // Omit built-in runtime config (like query modifiers) where = _.omit(where, blacklist || ['limit', 'skip', 'sort']); // Omit any params that have `undefined` on the RHS. where = _.omit(where, function(p) { if (_.isUndefined(p)) { return true; } }); }//>- // Deep merge w/ req.options.where. where = _.merge({}, req.options.where || {}, where) || undefined; // Return final `where`. return where; }, /** * Parse `values` for a Waterline `create` or `update` from all * request parameters. * * @param {Request} req * @return {Dictionary} */ parseValues: function (req) { // Allow customizable blacklist for params NOT to include as values. req.options.values = req.options.values || {}; req.options.values.blacklist = req.options.values.blacklist; // Validate blacklist to provide a more helpful error msg. var blacklist = req.options.values.blacklist; if (blacklist && !_.isArray(blacklist)) { throw new Error('Invalid `req.options.values.blacklist`. Should be an array of strings (parameter names.)'); } // Start an array to hold values var values; // Make an array out of the request body data if it wasn't one already; // this allows us to process multiple entities (e.g. for use with a "create" blueprint) the same way // that we process singular entities. var bodyData = _.isArray(req.body) ? req.body : [req.allParams()]; // Process each item in the bodyData array, merging with req.options, omitting blacklisted properties, etc. var valuesArray = _.map(bodyData, function(element){ var values; // Merge properties of the element into req.options.value, omitting the blacklist values = mergeDefaults(element, _.omit(req.options.values, 'blacklist')); // Omit properties that are in the blacklist (like query modifiers) values = _.omit(values, blacklist || []); // Omit any properties w/ undefined values values = _.omit(values, function(p) { if (_.isUndefined(p)) { return true; } }); return values; }); // If req.body is an array, simply return our array of processed values if (_.isArray(req.body)) {return valuesArray;} // Otherwaise grab the first (and only) value from valuesArray values = valuesArray[0]; return values; }, /** * Determine the model class to use w/ this blueprint action. * @param {Request} req * @return {WLCollection} */ parseModel: function (req) { // Ensure a model can be deduced from the request options. var model = req.options.model || req.options.controller; if (!model) { throw new Error(util.format('No "model" specified in route options.')); } var Model = req._sails.models[model]; if ( !Model ) { throw new Error(util.format('Invalid route option, "model".\nI don\'t know about any models named: `%s`',model)); } return Model; }, /** * @param {Request} req */ parseSort: function (req) { var sort = req.param('sort') || req.options.sort; if (_.isUndefined(sort)) {return undefined;} // If `sort` is a string, attempt to JSON.parse() it. // (e.g. `{"name": 1}`) if (_.isString(sort)) { try { sort = JSON.parse(sort); // If it is not valid JSON (e.g. because it's just some other string), // then just fall back to interpreting it as-is (e.g. "name ASC") } catch(unusedErr) {} } return sort; }, /** * @param {Request} req */ parseLimit: function (req) { var DEFAULT_LIMIT = req._sails.config.blueprints.defaultLimit || 30; var limit = req.param('limit') || (typeof req.options.limit !== 'undefined' ? req.options.limit : DEFAULT_LIMIT); if (limit) { limit = +limit; } return limit; }, /** * @param {Request} req */ parseSkip: function (req) { var DEFAULT_SKIP = 0; var skip = req.param('skip') || (typeof req.options.skip !== 'undefined' ? req.options.skip : DEFAULT_SKIP); if (skip) { skip = +skip; } return skip; } }; module.exports = actionUtil; ================================================ FILE: lib/hooks/blueprints/actions/add.js ================================================ /** * Module dependencies */ var _ = require('@sailshq/lodash'); var formatUsageError = require('../formatUsageError'); /** * Add Record To Collection * * http://sailsjs.com/docs/reference/blueprint-api/add-to * * Associate one record with the collection attribute of another. * e.g. add a Horse named "Jimmy" to a Farm's "animals". * */ module.exports = function addToCollection (req, res) { var parseBlueprintOptions = req.options.parseBlueprintOptions || req._sails.config.blueprints.parseBlueprintOptions; // Set the blueprint action for parseBlueprintOptions. req.options.blueprintAction = 'add'; var queryOptions = parseBlueprintOptions(req); var Model = req._sails.models[queryOptions.using]; var relation = queryOptions.alias; // The primary key of the parent record var parentPk = queryOptions.targetRecordId; // Get the model class of the child in order to figure out the name of // the primary key attribute. var associationAttr = _.findWhere(Model.associations, { alias: relation }); var ChildModel = req._sails.models[associationAttr.collection]; // The primary key of the child record; var childPk = queryOptions.associatedIds[0]; // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // FUTURE: Use a database transaction here, if all of the involved models // are using the same datastore, and if that datastore supports transactions. // e.g. // ``` // Model.getDatastore().transaction(function during(db, proceed){ ... }) // .exec(function afterwards(err, result){})); // ``` // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Model.findOne(parentPk).meta(queryOptions.meta).exec(function foundParent(err, parentRecord) { if (err) { return res.serverError(err); } // No such parent record? Bail out with a 404. if (!parentRecord) { return res.notFound(); } // Look up the child record to make sure it exists. ChildModel.findOne(childPk).exec(function foundChild(err, childRecord) { if (err) { return res.serverError(err); } // No such child record? Bail out with a 404. if (!childRecord) {return res.notFound();} // Add the child record to the parent. Model.addToCollection(parentPk, relation, childPk).exec( function(err) { if (err) { switch (err.name) { // Any kind of usage error coming back from Waterline, // (e.g. a bad criteria), is met with a 400 status code. case 'UsageError': return res.badRequest(formatUsageError(err, req)); case 'AdapterError': switch (err.code) { // If this child record is already a member of this collection, // then just continue along to the publishing below-- we'll still // respond w/ a 200 status code. // (see http://sailsjs.com/documentation/reference/blueprint-api/add-to) case 'E_UNIQUE': break; // Any other kind of adapter error is unexpected, so use 500. default: return res.serverError(err); } break; // Otherwise, it's some other unexpected error, so use 500. default: return res.serverError(err); } } // Broadcast updates if pubsub hook is enabled. if (req._sails.hooks.pubsub) { // Subscribe to the model you're adding to, if this was a socket request if (req.isSocket) { Model.subscribe(req, [parentPk]); } // Publish to subscribed sockets Model._publishAdd(parentPk, relation, childPk, !req.options.mirror && req); // If the inverse relationship on the child model is a singular association, and // the association attribute on the child was not `null` before, then notify the // former parent that this child has been "stolen". if (associationAttr.via && ChildModel.attributes[associationAttr.via].model && !_.isNull(childRecord[associationAttr.via])) { Model._publishRemove(childRecord[associationAttr.via], relation, childPk, !req.options.mirror && req, {noReverse: true}); } } // Finally, look up the parent record again and populate the relevant collection. var query = Model.findOne(parentPk, queryOptions.populates).meta(queryOptions.meta); query.exec(function(err, matchingRecord) { if (err) { return res.serverError(err); } if (!matchingRecord) { return res.serverError(); } if (!matchingRecord[relation]) { return res.serverError(); } return res.ok(matchingRecord); }); }); }); // </ ChildModel.findOne(childPk) > }); // </ Model.findOne(parentPk)> }; ================================================ FILE: lib/hooks/blueprints/actions/create.js ================================================ /** * Module dependencies */ var _ = require('@sailshq/lodash'); var async = require('async'); var formatUsageError = require('../formatUsageError'); /** * Create Record * * http://sailsjs.com/docs/reference/blueprint-api/create * * An API call to crete a single model instance using the specified attribute values. * */ module.exports = function createRecord (req, res) { var parseBlueprintOptions = req.options.parseBlueprintOptions || req._sails.config.blueprints.parseBlueprintOptions; // Set the blueprint action for parseBlueprintOptions. req.options.blueprintAction = 'create'; var queryOptions = parseBlueprintOptions(req); var Model = req._sails.models[queryOptions.using]; // Get the new record data. var data = queryOptions.newRecord; // Look for any many-to-one collections that are being set. // For example, User.create({pets: [1, 2, 3]}) where `pets` is a collection of `Pet` // via an `owner` attribute that is `model: 'user'`. // We need to know about these so that, if any of the new children already had parents, // those parents get `removedFrom` notifications. async.reduce(_.keys(Model.attributes), [], function(memo, attrName, nextAttrName) { var attrDef = Model.attributes[attrName]; if ( // Does this attribute represent a plural association. attrDef.collection && // Is this attribute set with a non-empty array? _.isArray(data[attrName]) && data[attrName].length > 0 && // Does this plural association have an inverse attribute on the related model? attrDef.via && // Is that inverse attribute a singular association, making this a many-to-one relationship? req._sails.models[attrDef.collection].attributes[attrDef.via].model ) { // Create an `in` query looking for all child records whose primary keys match // those in the array that the new parent's association attribute (e.g. `pets`) is set to. var criteria = {}; criteria[req._sails.models[attrDef.collection].primaryKey] = data[attrName]; req._sails.models[attrDef.collection].find(criteria).exec(function(err, newChildren) { if (err) {return nextAttrName(err);} // For each child, see if the inverse attribute already has a value, and if so, // push a new `removedFrom` notification onto the list of those to send. _.each(newChildren, function(child) { if (child[attrDef.via]) { memo.push({ id: child[attrDef.via], removedId: child[req._sails.models[attrDef.collection].primaryKey], attribute: attrName }); } }); return nextAttrName(undefined, memo); }); } else { return nextAttrName(undefined, memo); } }, function (err, removedFromNotificationsToSend) { if (err) {return res.serverError(err);} // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // FUTURE: Use a database transaction here, if supported by the datastore. // e.g. // ``` // Model.getDatastore().transaction(function during(db, proceed){ ... }) // .exec(function afterwards(err, result){})); // ``` // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // Create new instance of model using data from params Model.create(data).meta(queryOptions.meta).exec(function created (err, newInstance) { // Differentiate between waterline-originated validation errors // and serious underlying issues. Respond with badRequest if a // validation error is encountered, w/ validation info, or if a // uniqueness constraint is violated. if (err) { switch (err.name) { case 'AdapterError': switch (err.code) { case 'E_UNIQUE': return res.badRequest(err); default: return res.serverError(err); }//• case 'UsageError': return res.badRequest(formatUsageError(err, req)); default: return res.serverError(err); } }//-• // If we didn't fetch the new instance, just return 'OK'. if (!newInstance) { return res.ok(); } // Look up and populate the new record (according to `populate` options in request / config) Model .findOne(newInstance[Model.primaryKey], queryOptions.populates) .exec(function foundAgain(err, populatedRecord) { if (err) { return res.serverError(err); } if (!populatedRecord) { return res.serverError('Could not find record after creating!'); } // If we have the pubsub hook, use the model class's publish method // to notify all subscribers about the created item if (req._sails.hooks.pubsub) { if (req.isSocket) { Model.subscribe(req, [populatedRecord[Model.primaryKey]]); Model._introduce(populatedRecord); } Model._publishCreate(populatedRecord, !req.options.mirror && req); if (removedFromNotificationsToSend.length) { _.each(removedFromNotificationsToSend, function(notification) { Model._publishRemove(notification.id, notification.attribute, notification.removedId, !req.options.mirror && req, {noReverse: true}); }); } }//>- // Send response res.ok(populatedRecord); }); // </foundAgain> }); }); }; ================================================ FILE: lib/hooks/blueprints/actions/destroy.js ================================================ /** * Module dependencies */ var _ = require('@sailshq/lodash'); var formatUsageError = require('../formatUsageError'); /** * Destroy One Record * * http://sailsjs.com/docs/reference/blueprint-api/destroy * * Destroys the single model instance with the specified `id` from * the data adapter for the given model if it exists. * */ module.exports = function destroyOneRecord (req, res) { var parseBlueprintOptions = req.options.parseBlueprintOptions || req._sails.config.blueprints.parseBlueprintOptions; // Set the blueprint action for parseBlueprintOptions. req.options.blueprintAction = 'destroy'; var queryOptions = parseBlueprintOptions(req); var Model = req._sails.models[queryOptions.using]; var criteria = {}; criteria[Model.primaryKey] = queryOptions.criteria.where[Model.primaryKey]; // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // FUTURE: Use a database transaction here, if supported by the datastore. // e.g. // ``` // Model.getDatastore().transaction(function during(db, proceed){ ... }) // .exec(function afterwards(err, result){})); // ``` // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - var query = Model.findOne(_.cloneDeep(criteria), queryOptions.populates).meta(queryOptions.meta); query.exec(function foundRecord (err, record) { if (err) { // If this is a usage error coming back from Waterline, // (e.g. a bad criteria), then respond w/ a 400 status code. // Otherwise, it's something unexpected, so use 500. switch (err.name) { case 'UsageError': return res.badRequest(formatUsageError(err, req)); default: return res.serverError(err); } }//-• if(!record) { return res.notFound('No record found with the specified `id`.'); } // (Note: this could be achieved in a single query, but a separate `findOne` // is used first to provide a better experience for front-end developers // integrating with the blueprint API out of the box. However, we'll also include // the meta query optons for the purpose of enabling the `afterDestroy` // lifecycle callback (which only runs if `.meta({fetch: true})` is included). Model.destroy(_.cloneDeep(criteria)).meta(queryOptions.meta) .exec(function destroyedRecord (err) { if (err) { switch (err.name) { case 'UsageError': return res.badRequest(formatUsageError(err, req)); default: return res.serverError(err); } }//-• if (req._sails.hooks.pubsub) { Model._publishDestroy(criteria[Model.primaryKey], !req._sails.config.blueprints.mirror && req, {previous: record}); if (req.isSocket) { Model.unsubscribe(req, [record[Model.primaryKey]]); Model._retire(record); } } return res.ok(record); }); }); }; ================================================ FILE: lib/hooks/blueprints/actions/find.js ================================================ /** * Module dependencies */ var _ = require('@sailshq/lodash'); var actionUtil = require('../actionUtil'); var formatUsageError = require('../formatUsageError'); /** * Find Records * * http://sailsjs.com/docs/reference/blueprint-api/find * * An API call to find and return model instances from the data adapter * using the specified criteria. If an id was specified, just the instance * with that unique id will be returned. * */ module.exports = function findRecords (req, res) { var parseBlueprintOptions = req.options.parseBlueprintOptions || req._sails.config.blueprints.parseBlueprintOptions; // Set the blueprint action for parseBlueprintOptions. req.options.blueprintAction = 'find'; var queryOptions = parseBlueprintOptions(req); var Model = req._sails.models[queryOptions.using]; Model .find(queryOptions.criteria, queryOptions.populates).meta(queryOptions.meta) .exec(function found(err, matchingRecords) { if (err) { // If this is a usage error coming back from Waterline, // (e.g. a bad criteria), then respond w/ a 400 status code. // Otherwise, it's something unexpected, so use 500. switch (err.name) { case 'UsageError': return res.badRequest(formatUsageError(err, req)); default: return res.serverError(err); } }//-• if (req._sails.hooks.pubsub && req.isSocket) { Model.subscribe(req, _.pluck(matchingRecords, Model.primaryKey)); // Only `._watch()` for new instances of the model if // `autoWatch` is enabled. if (req.options.autoWatch) { Model._watch(req); } // Also subscribe to instances of all associated models _.each(matchingRecords, function (record) { actionUtil.subscribeDeep(req, record); }); }//>- return res.ok(matchingRecords); });//</ .find().exec() > }; ================================================ FILE: lib/hooks/blueprints/actions/findOne.js ================================================ /** * Module dependencies */ var _ = require('@sailshq/lodash'); var actionUtil = require('../actionUtil'); var formatUsageError = require('../formatUsageError'); /** * Find One Record * * http://sailsjs.com/docs/reference/blueprint-api/find-one. * * > Blueprint action to find and return the record with the specified id. * */ module.exports = function findOneRecord (req, res) { var parseBlueprintOptions = req.options.parseBlueprintOptions || req._sails.config.blueprints.parseBlueprintOptions; // Set the blueprint action for parseBlueprintOptions. req.options.blueprintAction = 'findOne'; var queryOptions = parseBlueprintOptions(req); var Model = req._sails.models[queryOptions.using]; // Only use the `where`, `select` or `omit` from the criteria (nothing else is valid for findOne). queryOptions.criteria = _.pick(queryOptions.criteria, ['where', 'select', 'omit']); // Only use the primary key in the `where` clause. queryOptions.criteria.where = _.pick(queryOptions.criteria.where, Model.primaryKey); Model .findOne(queryOptions.criteria, queryOptions.populates).meta(queryOptions.meta) .exec(function found(err, matchingRecord) { if (err) { // If this is a usage error coming back from Waterline, // (e.g. a bad criteria), then respond w/ a 400 status code. // Otherwise, it's something unexpected, so use 500. switch (err.name) { case 'UsageError': return res.badRequest(formatUsageError(err, req)); default: return res.serverError(err); } }//-• if(!matchingRecord) { req._sails.log.verbose('In `findOne` blueprint action: No record found with the specified id (`'+queryOptions.criteria.where[Model.primaryKey]+'`).'); return res.notFound(); } if (req._sails.hooks.pubsub && req.isSocket) { Model.subscribe(req, [matchingRecord[Model.primaryKey]]); actionUtil.subscribeDeep(req, matchingRecord); } return res.ok(matchingRecord); }); }; ================================================ FILE: lib/hooks/blueprints/actions/populate.js ================================================ /** * Module dependencies */ var actionUtil = require('../actionUtil'); var formatUsageError = require('../formatUsageError'); /** * Populate (or "expand") an association * * http://sailsjs.com/docs/reference/blueprint-api/populate * */ module.exports = function populate(req, res) { var sails = req._sails; var parseBlueprintOptions = req.options.parseBlueprintOptions || req._sails.config.blueprints.parseBlueprintOptions; // Set the blueprint action for parseBlueprintOptions. req.options.blueprintAction = 'populate'; var queryOptions = parseBlueprintOptions(req); var Model = req._sails.models[queryOptions.using]; var attrName = queryOptions.alias; if (!attrName || !Model) { return res.serverError(); } // The primary key of the parent record var parentPk = queryOptions.criteria.where[Model.primaryKey]; Model .findOne(parentPk, queryOptions.populates).meta(queryOptions.meta) .exec(function found(err, matchingRecord) { if (err) { // If this is a usage error coming back from Waterline, // (e.g. a bad criteria), then respond w/ a 400 status code. // Otherwise, it's something unexpected, so use 500. switch (err.name) { case 'UsageError': return res.badRequest(formatUsageError(err, req)); default: return res.serverError(err); } }//-• if (!matchingRecord) { sails.log.verbose('In `populate` blueprint action: No parent record found with the specified id (`'+parentPk+'`).'); return res.notFound(); }//-• if (!matchingRecord[attrName]) { sails.log.verbose('In `populate` blueprint action: Specified parent record ('+parentPk+') does not have a `'+attrName+'`.'); return res.notFound(); }//-• // Subcribe to relevant record(s), if appropriate. if (sails.hooks.pubsub && req.isSocket) { Model.subscribe(req, matchingRecord); actionUtil.subscribeDeep(req, matchingRecord); // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // FUTURE: // Only subscribe to the associated record(s) without watching the entire // associated model. (Currently, `.subscribeDeep()` also calls `.watch()`, // if `autoWatch` is enabled.) // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - } return res.ok(matchingRecord[attrName]); }); }; ================================================ FILE: lib/hooks/blueprints/actions/remove.js ================================================ /** * Module dependencies */ var _ = require('@sailshq/lodash'); var formatUsageError = require('../formatUsageError'); /** * Remove a member from an association * * http://sailsjs.com/docs/reference/blueprint-api/remove-from * */ module.exports = function remove(req, res) { var parseBlueprintOptions = req.options.parseBlueprintOptions || req._sails.config.blueprints.parseBlueprintOptions; // Set the blueprint action for parseBlueprintOptions. req.options.blueprintAction = 'remove'; var queryOptions = parseBlueprintOptions(req); var Model = req._sails.models[queryOptions.using]; var relation = queryOptions.alias; // The primary key of the parent record var parentPk = queryOptions.targetRecordId; // Get the model class of the child in order to figure out the name of // the primary key attribute. var associationAttr = _.findWhere(Model.associations, { alias: relation }); var ChildModel = req._sails.models[associationAttr.collection]; // The primary key of the child record; var childPk = queryOptions.associatedIds[0]; // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // FUTURE: Use a database transaction here, if all of the involved models // are using the same datastore, and if that datastore supports transactions. // e.g. // ``` // Model.getDatastore().transaction(function during(db, proceed){ ... }) // .exec(function afterwards(err, result){})); // ``` // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Model.findOne(parentPk).meta(queryOptions.meta).exec(function foundParent(err, parentRecord) { if (err) { return res.serverError(err); } if (!parentRecord) { return res.notFound(); } // Look up the child record to make sure it exists. ChildModel.findOne(childPk).exec(function foundChild(err, childRecord) { if (err) { return res.serverError(err); } // No such child record? Bail out with a 404. if (!childRecord) {return res.notFound();} Model.removeFromCollection(parentPk, relation, childPk).exec(function(err) { if (err) { // If this is a usage error coming back from Waterline, // (e.g. a bad criteria), then respond w/ a 400 status code. // Otherwise, it's something unexpected, so use 500. switch (err.name) { case 'UsageError': return res.badRequest(formatUsageError(err, req)); default: return res.serverError(err); } }//-• // Finally, look up the parent record again and populate the relevant collection. var query = Model.findOne(parentPk, queryOptions.populates).meta(queryOptions.meta); query.exec(function found(err, parentRecord) { if (err) { return res.serverError(err); } if (!parentRecord) { return res.serverError(); } if (!parentRecord[relation]) { return res.serverError(); } if (!parentRecord[Model.primaryKey]) { return res.serverError(); } // If we have the pubsub hook, use the model class's publish method // to notify all subscribers about the removed item if (req._sails.hooks.pubsub) { Model._publishRemove(parentRecord[Model.primaryKey], relation, childPk, !req._sails.config.blueprints.mirror && req); } return res.ok(parentRecord); }); }); }); }); }; ================================================ FILE: lib/hooks/blueprints/actions/replace.js ================================================ /** * Module dependencies */ var _ = require('@sailshq/lodash'); var async = require('async'); var formatUsageError = require('../formatUsageError'); /** * Replace Records in Collection * * http://sailsjs.com/docs/reference/blueprint-api/replace * * Replace the associated records in the given collection with * different records. For example, replace all of a user's pets. * */ module.exports = function replaceCollection (req, res) { var parseBlueprintOptions = req.options.parseBlueprintOptions || req._sails.config.blueprints.parseBlueprintOptions; // Set the blueprint action for parseBlueprintOptions. req.options.blueprintAction = 'replace'; var queryOptions = parseBlueprintOptions(req); var Model = req._sails.models[queryOptions.using]; var relation = queryOptions.alias; // The primary key of the parent record var parentPk = queryOptions.targetRecordId; var childPks = queryOptions.associatedIds; var removedFromNotificationsToSend = []; var existingChildPks = []; // Get the relevant association attribute on the parent model. var attr = Model.attributes[relation]; // Get the related ("child") model. var relatedModel = req._sails.models[attr.model || attr.collection]; // Get the inverse attribute (if any) on the related model. var inverseAttr = attr.via && relatedModel.attributes[attr.via]; async.auto({ // If this is a many-to-one relationship, get all of the existing child PKs so that we can // inform them of their removal (if they're not in the new set), and get all of the parent PKs notificationsForExistingParentsOfReplacementChildren: function(cb) { // If there is no inverse attribute on the related model, then this is a via-less collection // which uses an implicit join table, so there's no "stealing" of children. if (!inverseAttr) { return cb(); } // If the inverse relationship on the related model is a collection, then this is // a many-to-many relationship, so again, no stolen children. if (inverseAttr.collection) { return cb(); } // Ok, this is a many-to-one relationship, so let's find all of the "replacement" children // and add `removedFrom` notifications for each (if the current parent is different from the new parent). var criteria = {}; criteria[relatedModel.primaryKey] = childPks; criteria[attr.via] = {'!=': parentPk}; relatedModel.stream(criteria).select([attr.via]).eachRecord(function(childRecord, nextChild) { if (childRecord[attr.via] !== null) { removedFromNotificationsToSend.push({ id: childRecord[attr.via], removedId: childRecord[relatedModel.primaryKey], attribute: relation, reverse: false }); } return nextChild(); }).exec(cb); }, notificationsForExistingChildrenOfParent: function(cb) { // If this is a many-to-many or a via-less relationship, then we can't query the related model // to find the existing children of our parent. We'll have to just do a find + populate. if (!inverseAttr || inverseAttr.collection) { var parentCriteria = {}; parentCriteria[Model.primaryKey] = parentPk; var populateCriteria = { select: [relatedModel.primaryKey] }; Model.findOne(parentCriteria).populate(relation, populateCriteria).exec(function(err, parentRecord) { if (err) {return cb(err);} _.each(parentRecord[relation], function(child) { existingChildPks.push(child[relatedModel.primaryKey]); if (!_.contains(childPks, child[relatedModel.primaryKey])) { removedFromNotificationsToSend.push({ id: parentPk, removedId: child[relatedModel.primaryKey], attribute: relation, reverse: true }); } }); return cb(); });//_∏_ return; }//-• // Otherwise, this is a many-to-one relationship, and we can query the related model. var criteria = { where: {}, select: [relatedModel.primaryKey] }; criteria.where[attr.via] = parentPk; relatedModel.stream(criteria).eachRecord(function(childRecord, nextChild) { existingChildPks.push(childRecord[relatedModel.primaryKey]); if (!_.contains(childPks, childRecord[relatedModel.primaryKey])) { removedFromNotificationsToSend.push({ id: parentPk, removedId: childRecord[relatedModel.primaryKey], attribute: relation, reverse: true }); } return nextChild(); }).exec(cb); } }, function(err) { if (err) { return res.serverError(err); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // FUTURE: Use a database transaction here, if all of the involved models // are using the same datastore, and if that datastore supports transactions. // e.g. // ``` // Model.getDatastore().transaction(function during(db, proceed){ ... }) // .exec(function afterwards(err, result){})); // ``` // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Model.replaceCollection(parentPk, relation, childPks).exec( function(err) { if (err) { // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // FUTURE: When we support transactions in blueprints, if this request was // able to use a transaction, handle E_UNIQUE by sending back a 409 ("Conflict") // instead of a 5xx error-- since in that case, all of our changes from this // blueprint action would have been rolled back along with the transaction // upon failure. // // For example, we'd call `proceed(err)` like normal, then in the "afterwards" // callback from `.transaction()`, we'd check for E_UNIQUE, and if we see it, call: // ``` // return res.badRequest(err); // ``` // - - - - - - - - - - - - - - - - - - - - - - - - switch (err.name) { // If this is a usage error coming back from Waterline, // (e.g. a bad criteria), then respond w/ a 400 status code. case 'UsageError': return res.badRequest(formatUsageError(err, req)); case 'AdapterError': switch(err.code) { // If there is a uniqueness error, then this collection have been simultaneously // mucked around with by some other query that is editing records at the same time. // (Because we know that uniqueness errors cannot happen because of duplicates // in the array of child ids-- duplicate child ids are ignored.) So for now, // we'll respond with a 500 error in this case, but see the note above about how // this will be handled in the future. case 'E_UNIQUE': return res.serverError(err); // Any other kind of adapter error is unexpected, so use 500. default: return res.serverError(err); }//• // Otherwise, it's some other unexpected error, so use 500. default: return res.serverError(err); } }//-• // Broadcast updates to subscribers of the child records. if (req._sails.hooks.pubsub) { // Subscribe to the model you're adding to, if this was a socket request if (req.isSocket) { Model.subscribe(req, [parentPk]); } // Publish to subscribed sockets _.each(_.difference(childPks, existingChildPks), function(childPk) { Model._publishAdd(parentPk, relation, childPk, !req.options.mirror && req); }); if (removedFromNotificationsToSend.length) { _.each(removedFromNotificationsToSend, function(notification) { Model._publishRemove(notification.id, notification.attribute, notification.removedId, !req.options.mirror && req, {noReverse: !notification.reverse}); }); } } var query = Model.findOne(parentPk, queryOptions.populates).meta(queryOptions.meta); query.exec(function(err, matchingRecord) { if (err) { return res.serverError(err); } if (!matchingRecord) { return res.serverError(); } if (!matchingRecord[relation]) { return res.serverError(); } return res.ok(matchingRecord); }); }); // </ Model.replaceCollection(parentPk)> }); }; ================================================ FILE: lib/hooks/blueprints/actions/update.js ================================================ /** * Module dependencies */ var _ = require('@sailshq/lodash'); var formatUsageError = require('../formatUsageError'); /** * Update One Record * * http://sailsjs.com/docs/reference/blueprint-api/update * * An API call to update a model instance with the specified `id`, * treating the other unbound parameters as attributes. * */ module.exports = function updateOneRecord (req, res) { var parseBlueprintOptions = req.options.parseBlueprintOptions || req._sails.config.blueprints.parseBlueprintOptions; // Set the blueprint action for parseBlueprintOptions. req.options.blueprintAction = 'update'; var queryOptions = parseBlueprintOptions(req); var Model = req._sails.models[queryOptions.using]; var criteria = {}; criteria[Model.primaryKey] = queryOptions.criteria.where[Model.primaryKey]; // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // FUTURE: Use a database transaction here, if supported by the datastore. // e.g. // ``` // Model.getDatastore().transaction(function during(db, proceed){ ... }) // .exec(function afterwards(err, result){})); // ``` // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // Find and update the targeted record. // // (Note: this could be achieved in a single query, but a separate `findOne` // is used first to provide a better experience for front-end developers // integrating with the blueprint API.) Model.findOne( _.cloneDeep(criteria), _.cloneDeep(queryOptions.populates) ) .exec(function (err, matchingRecord) { if (err) { switch (err.name) { case 'UsageError': return res.badRequest(formatUsageError(err, req)); default: return res.serverError(err); } }//-• if (!matchingRecord) { return res.notFound(); }//• // This should only update a single record Model.updateOne(_.cloneDeep(criteria)) .set(queryOptions.valuesToSet) .meta(queryOptions.meta) .exec(function (err, updatedRecord) { // Differentiate between waterline-originated validation errors // and serious underlying issues. Respond with badRequest if a // validation error is encountered, w/ validation info, or if a // uniqueness constraint is violated. if (err) { switch (err.name) { case 'AdapterError': switch (err.code) { case 'E_UNIQUE': return res.badRequest(err); default: return res.serverError(err); }//• case 'UsageError': return res.badRequest(formatUsageError(err, req)); default: return res.serverError(err); } }//• if (!updatedRecord) { return res.notFound(); }//• // If we have the pubsub hook, use the Model's publish method // to notify all subscribers about the update. if (req._sails.hooks.pubsub) { if (req.isSocket) { Model.subscribe(req, [updatedRecord[Model.primaryKey]]); }//fi // The _.cloneDeep()s ensure that only plain dictionaries are broadcast. // > TODO: why is that important? var pk = updatedRecord[Model.primaryKey]; Model._publishUpdate(pk, _.cloneDeep(queryOptions.valuesToSet), !req.options.mirror && req, { previous: _.cloneDeep(matchingRecord) }); }//fi // Do a final query to populate the associations of the record. // // (Note: again, this extra query could be eliminated, but it is // included by default to provide a better interface for integrating // front-end developers.) Model.findOne( _.cloneDeep(criteria), _.cloneDeep(queryOptions.populates) ) .exec(function foundAgain(err, populatedRecord) { if (err) { return res.serverError(err); } if (!populatedRecord) { return res.serverError('Could not find record after updating!'); } res.ok(populatedRecord); }); // </.findOne() (for populating the updated record)> });// </.updateOne()> }); // </.findOne() to get the ORIGINAL populated record> }; ================================================ FILE: lib/hooks/blueprints/formatUsageError.js ================================================ /** * Module dependencies */ var _ = require('@sailshq/lodash'); /** * Give Waterline UsageErrors from blueprints a toJSON function for nicer output. */ module.exports = function(err, req) { err.toJSON = function (){ // Include the error code and the array of RTTC validation errors // for easy programmatic parsing. var jsonReadyErrDictionary = _.pick(err, ['code', 'details']); // And also include a more front-end-friendly version of the error message. var preamble = 'The server could not fulfill this request (`'+req.method+' '+req.path+'`) '+ 'due to a problem with the parameters that were sent. See the `details` for more info.'; // If NOT running in production, then provide additional details and tips. if (process.env.NODE_ENV !== 'production') { jsonReadyErrDictionary.message = preamble+' '+ '**The following additional tip will not be shown in production**: '+ 'Tip: Check your client-side code to make sure that the request data it '+ 'sends matches the expectations of the corresponding attributes in your '+ 'model. Also check that your client-side code sends data for every required attribute.'; } // If running in production, use a message that is more terse. else { jsonReadyErrDictionary.message = preamble; } //>- return jsonReadyErrDictionary; };//</define :: err.toJSON()> return err; }; ================================================ FILE: lib/hooks/blueprints/index.js ================================================ /** * Module dependencies */ var _ = require('@sailshq/lodash'); var util = require('util'); var pluralize = require('pluralize'); var STRINGFILE = require('sails-stringfile'); var flaverr = require('flaverr'); var BlueprintController = { create: require('./actions/create'), find: require('./actions/find'), findone: require('./actions/findOne'), update: require('./actions/update'), destroy: require('./actions/destroy'), populate: require('./actions/populate'), add: require('./actions/add'), remove: require('./actions/remove'), replace: require('./actions/replace'), }; /** * Blueprints (Core Hook) * * Stability: 1 - Experimental * (see http://nodejs.org/api/documentation.html#documentation_stability_index) */ module.exports = function(sails) { /** * Private dependencies. * (need access to `sails`) */ var onRoute = require('./onRoute')(sails); var hook; /** * Expose blueprint hook definition */ return { /** * Default configuration to merge w/ top-level `sails.config` * @type {Object} */ defaults: { // These config options are mixed into the route options (req.options) // and made accessible from the blueprint actions. Most of them currently // relate to the shadow (i.e. implicit) routes which are created, and are // interpreted by this hook. blueprints: { // Blueprint/Shadow-Routes Enabled // // e.g. '/frog/jump': 'FrogController.jump' actions: false, // e.g. '/frog/find/:id?': 'FrogController.find' shortcuts: true, // e.g. 'get /frog/:id?': 'FrogController.find' rest: true, // Blueprint/Shadow-Route Modifiers // // e.g. 'get /api/v2/frog/:id?': 'FrogController.find' prefix: '', // Blueprint/REST-Route Modifiers // Will work only for REST and will extend `prefix` option // // e.g. 'get /api/v2/frog/:id?': 'FrogController.find' restPrefix: '', // e.g. 'get /frogs': 'FrogController.find' pluralize: false, // Configuration of the blueprint actions themselves: // Whether to run `Model.watch()` in the `find` blueprint action. autoWatch: true, // Private per-controller config. _controllers: {}, parseBlueprintOptions: function(req) { return req._sails.hooks.blueprints.parseBlueprintOptions(req); } } }, configure: function() { if (sails.config.blueprints.jsonp) { throw flaverr({ name: 'userError', code: 'E_JSONP_UNSUPPORTED' }, new Error('JSONP support was removed from the blueprints API in Sails 1.0 (detected sails.config.blueprints.jsonp === ' + sails.config.blueprints.jsonp + ')')); } if (!_.isUndefined(sails.config.blueprints.defaultLimit)) { sails.log.debug('The `sails.config.blueprints.defaultLimit` option is no longer supported in Sails 1.0.'); sails.log.debug('Instead, you can use a `parseBlueprintOptions` function to fully customize blueprint behavior.'); sails.log.debug('See http://sailsjs.com/docs/reference/configuration/sails-config-blueprints#?using-parseblueprintoptions.'); sails.log.debug('(Setting the default limit to 30 in the meantime.)'); sails.log.debug(); } if (!_.isUndefined(sails.config.blueprints.populate)) { sails.log.debug('The `sails.config.blueprints.populate` option is no longer supported in Sails 1.0.'); sails.log.debug('Instead, you can use a `parseBlueprintOptions` function to fully customize blueprint behavior.'); sails.log.debug('See http://sailsjs.com/docs/reference/configuration/sails-config-blueprints#?using-parseblueprintoptions.'); sails.log.debug('(Will populate all associations in blueprints in the meantime.)'); sails.log.debug(); } }, parseBlueprintOptions: require('./parse-blueprint-options'), /** * Internal list of action functions that may be bound via shadow routes. * @type {Object} */ _actions: {}, /** * Initialize is fired first thing when the hook is loaded. * * @param {Function} cb */ initialize: function (cb) { // Provide hook context to closures hook = this; // Set the _middlewareType of each blueprint action to 'BLUEPRINT: <action>'. _.each(BlueprintController, function(fn, key) { fn._middlewareType = 'BLUEPRINT: ' + key; }); // Register route syntax for binding blueprints directly. // This is deprecated, so onRoute currently just logs a warning. sails.on('route:typeUnknown', onRoute); // Wait until after user routes have been bound to bind our // own "shadow routes" (action routes, RESTful routes, // shortcut routes and index routes). sails.on('router:after', hook.bindShadowRoutes); // If the ORM hook is active, wait for it to load, then create actions // for each model. if (sails.hooks.orm) { sails.after('hook:orm:loaded', function() { hook.registerActions(cb); }); } // Otherwise we're done! else { return cb(); } }, bindShadowRoutes: function() { var logWarns = function(warns) { sails.log.blank(); _.each(warns, function (warn) { sails.log.warn(warn); }); STRINGFILE.logMoreInfoLink(STRINGFILE.get('links.docs.config.blueprints'), sails.log.warn); }; // Local reference to the sails blueprints config. var config = sails.config.blueprints; // Get a copy of the Sails actions dictionary. var actions = sails.getActions(); // Determine whether any model is using the default archive model. var defaultArchiveInUse = _.any(sails.models, function(model) { return model.archiveModelIdentity === 'archive'; }); // ┬ ┬┌─┐┬ ┬┌┬┐┌─┐┌┬┐┌─┐ ┌─┐┬─┐┌─┐┌─┐┬─┐ ┬┌─┐┌─┐ // └┐┌┘├─┤│ │ ││├─┤ │ ├┤ ├─┘├┬┘├┤ ├┤ │┌┴┬┘├┤ └─┐ // └┘ ┴ ┴┴─┘┴─┴┘┴ ┴ ┴ └─┘ ┴ ┴└─└─┘└ ┴┴ └─└─┘└─┘ // Validate prefix for generated routes. if ( config.prefix ) { if ( !_(config.prefix).isString() ) { sails.after('lifted', function () { logWarns([ 'Ignoring invalid blueprint prefix configured for controllers.', '`prefix` should be a string, e.g. "/api/v1".' ]); }); return; } if ( !config.prefix.match(/^\//) ) { var originalPrefix = config.prefix; sails.after('lifted', function () { logWarns([ util.format('Invalid blueprint prefix ("%s") configured for controllers.', originalPrefix), util.format('For now, assuming you meant: "%s".', config.prefix) ]); }); config.prefix = '/' + config.prefix; } } // Validate prefix for RESTful routes. if ( config.restPrefix ) { if ( !_(config.restPrefix).isString() ) { sails.after('lifted', function () { logWarns([ 'Ignoring invalid blueprint rest prefix configured for controllers', '`restPrefix` should be a string, e.g. "/api/v1".' ]); }); return; } if ( !config.restPrefix.match(/^\//) ) { var originalRestPrefix = config.restPrefix; sails.after('lifted', function () { logWarns([ util.format('Invalid blueprint restPrefix ("%s") configured for controllers (should start with a `/`).', originalRestPrefix), util.format('For now, assuming you meant: "%s".', config.restPrefix) ]); }); config.restPrefix = '/' + config.restPrefix; } } // ╔═╗╔═╗╔╦╗╦╔═╗╔╗╔ ┬─┐┌─┐┬ ┬┌┬┐┌─┐┌─┐ // ╠═╣║ ║ ║║ ║║║║ ├┬┘│ ││ │ │ ├┤ └─┐ // ╩ ╩╚═╝ ╩ ╩╚═╝╝╚╝ ┴└─└─┘└─┘ ┴ └─┘└─┘ // If action routing is turned on, bind a route pointing // at each action in the Sails actions dictionary if ( config.actions ) { // Loop through each action in the dictionary _.each(actions, function(action, key) { // If this is a blueprint action, only skip it. // It'll be handled in the "shortcut routes" section, // if those routes are enabled. if (action._middlewareType && action._middlewareType.indexOf('BLUEPRINT') === 0) { return; } // If this action belongs to a controller with blueprint action routes turned off, skip it. if (_.any(config._controllers, function(config, controllerIdentity) { return config.actions === false && key.indexOf(controllerIdentity) === 0; })) { return; } // Add the route prefix (if any) and bind the route to that URL. var url = config.prefix + '/' + key; sails.router.bind(url, key); }); } // ╔═╗╦ ╦╔═╗╦═╗╔╦╗╔═╗╦ ╦╔╦╗ ┬─┐┌─┐┬ ┬┌┬┐┌─┐┌─┐ // ╚═╗╠═╣║ ║╠╦╝ ║ ║ ║ ║ ║ ├┬┘│ ││ │ │ ├┤ └─┐ // ╚═╝╩ ╩╚═╝╩╚═ ╩ ╚═╝╚═╝ ╩ ┴└─└─┘└─┘ ┴ └─┘└─┘ // If shortcut blueprint routing is turned on, bind CRUD routes // for each model using GET-only urls. if ( config.shortcuts ) { // Loop through each model. _.each(sails.models, function(Model, identity) { if (identity === 'archive' && defaultArchiveInUse) { return; } // If this there is a matching controller with blueprint shortcut routes turned off, skip it. if (_.any(config._controllers, function(config, controllerIdentity) { return config.shortcuts === false && identity === controllerIdentity; })) { return; } // Determine the base route for the model. var baseShortcutRoute = (function() { // Start with the model identity. var baseRouteName = identity; // Pluralize it if plurization option is on. if (config.pluralize) { baseRouteName = pluralize(baseRouteName); } // Add the route prefix and base route name together. return config.prefix + '/' + baseRouteName; })(); _bindShortcutRoute('get %s/find', 'find'); _bindShortcutRoute('get %s/find/:id', 'findOne'); _bindShortcutRoute('get %s/create', 'create'); _bindShortcutRoute('get %s/update/:id', 'update'); _bindShortcutRoute('get %s/destroy/:id', 'destroy'); // Bind "rest" blueprint/shadow routes based on known associations in our model's schema // Bind add/remove for each `collection` associations _.each(_.where(Model.associations, {type: 'collection'}), function (association) { var alias = association.alias; _bindAssocRoute('get %s/:parentid/%s/add/:childid', 'add', alias); _bindAssocRoute('get %s/:parentid/%s/replace', 'replace', alias); _bindAssocRoute('get %s/:parentid/%s/remove/:childid', 'remove', alias); }); // and populate for both `collection` and `model` associations, // if we didn't already do it above for RESTful routes if ( !config.rest ) { _.each(Model.associations, function (association) { var alias = association.alias; _bindAssocRoute('get %s/:parentid/%s', 'populate', alias ); }); } function _bindShortcutRoute(template, blueprintActionName) { // Get the route URL for this shortcut var shortcutRoute = util.format(template, baseShortcutRoute); // Bind it to the appropriate action, adding in some route options including a deep clone of the model associations. // The clone prevents the blueprint action from accidentally altering the model definition in any way. sails.router.bind(shortcutRoute, identity + '/' + blueprintActionName, null, { model: identity, associations: _.cloneDeep(Model.associations), autoWatch: sails.config.blueprints.autoWatch }); } function _bindAssocRoute(template, blueprintActionName, alias) { // Get the route URL for this shortcut var assocRoute = util.format(template, baseShortcutRoute, alias); // Bind it to the appropriate action, adding in some route options including a deep clone of the model associations. // The clone prevents the blueprint action from accidentally altering the model definition in any way. sails.router.bind(assocRoute, identity + '/' + blueprintActionName, null, { model: identity, alias: alias, associations: _.cloneDeep(Model.associations), autoWatch: sails.config.blueprints.autoWatch }); } }); } // ╦═╗╔═╗╔═╗╔╦╗ ┬─┐┌─┐┬ ┬┌┬┐┌─┐┌─┐ // ╠╦╝║╣ ╚═╗ ║ ├┬┘│ ││ │ │ ├┤ └─┐ // ╩╚═╚═╝╚═╝ ╩ ┴└─└─┘└─┘ ┴ └─┘└─┘ // If RESTful blueprint routing is turned on, bind CRUD routes // for each model. if ( config.rest ) { // Loop throug each model. _.each(sails.models, function(Model, identity) { if (identity === 'archive' && defaultArchiveInUse) { return; } // If this there is a matching controller with blueprint shortcut routes turned off, skip it. if (_.any(config._controllers, function(config, controllerIdentity) { return config.rest === false && identity === controllerIdentity; })) { return; } // Determine the base REST route for the model. var baseRestRoute = (function() { // Start with the model identity. var baseRouteName = identity; // Pluralize it if plurization option is on. if (config.pluralize) { baseRouteName = pluralize(baseRouteName); } // Add the route prefix, RESTful route prefix and base route name together. return config.prefix + config.restPrefix + '/' + baseRouteName; })(); _bindRestRoute('get %s', 'find'); _bindRestRoute('get %s/:id', 'findOne'); _bindRestRoute('post %s', 'create'); _bindRestRoute('patch %s/:id', 'update'); _bindRestRoute('delete %s/:id?', 'destroy'); // Bind the `put :model/:id` route to the update action, first bind a route that // logs a warning about using `PUT` instead of `PATCH`. // Some route options are set as well, including a deep clone of the model associations. // The clone prevents the blueprint action from accidentally altering the model definition in any way. sails.router.bind( util.format('put %s/:id', baseRestRoute), function (req, res, next) { sails.log.debug('Using `PUT` to update a record is deprecated in Sails 1.0. Use `PATCH` instead!'); return next(); } ); _bindRestRoute('put %s/:id', 'update'); // Bind "rest" blueprint/shadow routes based on known associations in our model's schema // Bind add/remove for each `collection` associations _.each(_.where(Model.associations, {type: 'collection'}), function (association) { var alias = association.alias; _bindAssocRoute('put %s/:parentid/%s/:childid', 'add', alias); _bindAssocRoute('put %s/:parentid/%s', 'replace', alias); _bindAssocRoute('delete %s/:parentid/%s/:childid', 'remove', alias); }); // and populate for both `collection` and `model` associations _.each(Model.associations, function (association) { var alias = association.alias; _bindAssocRoute('get %s/:parentid/%s', 'populate', alias ); }); function _bindRestRoute(template, blueprintActionName) { // Get the URL for the RESTful route var restRoute = util.format(template, baseRestRoute); // Bind it to the appropriate action, adding in some route options including a deep clone of the model associations. // The clone prevents the blueprint action from accidentally altering the model definition in any way. sails.router.bind(restRoute, identity + '/' + blueprintActionName, null, { model: identity, associations: _.cloneDeep(Model.associations), autoWatch: sails.config.blueprints.autoWatch }); } function _bindAssocRoute(template, blueprintActionName, alias) { // Get the URL for the RESTful route var assocRoute = util.format(template, baseRestRoute, alias); // Bind it to the appropriate action, adding in some route options including a deep clone of the model associations. // The clone prevents the blueprint action from accidentally altering the model definition in any way. sails.router.bind(assocRoute, identity + '/' + blueprintActionName, null, { model: identity, alias: alias, associations: _.cloneDeep(Model.associations), autoWatch: sails.config.blueprints.autoWatch }); } }); } // ╦╔╗╔╔╦╗╔═╗═╗ ╦ ┬─┐┌─┐┬ ┬┌┬┐┌─┐┌─┐ // ║║║║ ║║║╣ ╔╩╦╝ ├┬┘│ ││ │ │ ├┤ └─┐ // ╩╝╚╝═╩╝╚═╝╩ ╚═ ┴└─└─┘└─┘ ┴ └─┘└─┘ // // If action routing is turned on, bind a route pointing // any action ending in `/index` to the base of that // action's path, e.g. 'user.index' => '/user' if ( config.actions ) { // Loop through each action in the dictionary _.each(actions, function(action, key) { // Does the key end in `/index` (or is it === `index`)? if (key === 'index' || key.match(/\/index$/)) { // If this action belongs to a controller with blueprint action routes turned off, skip it. if (_.any(config._controllers, function(config, controllerIdentity) { return config.actions === false && key.indexOf(controllerIdentity) === 0; })) { return; } // Strip the `.index` off the end. var index = key.replace(/\/?index$/,''); // Replace any remaining dots with slashes. var url = '/' + index; // Bind the url to the action. sails.router.bind(url, key); } }); } }, registerActions: function(cb) { // Determine whether or not any model is using the default archive. var defaultArchiveInUse = _.any(sails.models, function(model) { return model.archiveModelIdentity === 'archive'; }); // Loop through all of the loaded models and add actions for each. // Even though we're adding the same exact actions for each model, // (e.g. user/find and pet/find are the same), it's important that // each model gets its own set so that they can have different // action middleware (e.g. policies) applied to them. _.each(_.keys(sails.models), function(modelIdentity) { if (modelIdentity === 'archive' && defaultArchiveInUse) { return; } sails.registerAction(BlueprintController.create, modelIdentity + '/create'); sails.registerAction(BlueprintController.find, modelIdentity + '/find'); sails.registerAction(BlueprintController.findone, modelIdentity + '/findOne'); sails.registerAction(BlueprintController.update, modelIdentity + '/update'); sails.registerAction(BlueprintController.destroy, modelIdentity + '/destroy'); sails.registerAction(BlueprintController.populate, modelIdentity + '/populate'); sails.registerAction(BlueprintController.add, modelIdentity + '/add'); sails.registerAction(BlueprintController.remove, modelIdentity + '/remove'); sails.registerAction(BlueprintController.replace, modelIdentity + '/replace'); }); return cb(); } }; }; ================================================ FILE: lib/hooks/blueprints/onRoute.js ================================================ /** * Module dependencies. */ var _ = require('@sailshq/lodash'); // NOTE: // Since controllers load blueprint actions by default anyways, this route syntax handler // can be replaced with `{action: 'find'}, {action: 'create'}, ...` etc. /** * Expose route parser. * @type {Function} */ module.exports = function(sails) { /** * interpretRouteSyntax * * "Teach" router to understand direct references to blueprints * as a target to sails.router.bind() * (i.e. in the `routes.js` file) * * @param {[type]} route [description] * @return {[type]} [description] * @api private */ return function interpretRouteSyntax(route) { var target = route.target; if (_.isFunction(target)) { throw new Error('Consistency violation: route target is a function, but is being handled by blueprint hook instead of Sails router!'); } if (_.isArray(target)) { throw new Error('Consistency violation: route target is an array, but is being handled by blueprint hook instead of Sails router!'); } if (!_.isObject(target)) { throw new Error('Consistency violation: route target is a ' + typeof(target) + ', but is being handled by blueprint hook instead of Sails router!'); } // Support referencing blueprints in explicit routes // (`{ blueprint: 'create' }` et. al.) if (!_.isUndefined(target.blueprint)) { var errMsg = 'The `blueprint` route target syntax is no longer supported.'; if (_.isString(target.blueprint) && _.isString(target.model)) { errMsg = ' Use {action: \'' + target.model.toLowerCase() + '.' + target.blueprint + '\'} instead!'; } sails.log.error(errMsg); return; } }; }; ================================================ FILE: lib/hooks/blueprints/parse-blueprint-options.js ================================================ /** * Module dependencies */ var util = require('util'); var _ = require('@sailshq/lodash'); var flaverr = require('flaverr'); /** * parseBlueprintOptions() * * Parse information from the request for use in a blueprint action. * * > This is just the default implementation -- it can be overridden. * > See http://sailsjs.com/config/blueprints for more information. * * | Term | Meaning * |:----------------------|:----------------------------------------------------------------------------------------| * | route option | e.g. `model`, `alias`, `parseBlueprintOptions`, `action`, etc. (+ non-standard options) * | query key | e.g. `criteria`, `newRecord`, `valuesToSet`, `meta`, `using`, etc. (fully standardized) * | blueprint option | e.g. `criteria`, `newRecord`, `valuesToSet`, `meta`, `using`, etc. (fully standardized) * * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * @param {Request} req * * @returns {Dictionary} * The final dict of "blueprint options"; special settings that * tell a blueprint action what to do when it runs. (They are * roughly equivalent to Waterline query keys.) * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ module.exports = function parseBlueprintOptions(req) { // ███████╗███████╗████████╗██╗ ██╗██████╗ // ██╔════╝██╔════╝╚══██╔══╝██║ ██║██╔══██╗ // ███████╗█████╗ ██║ ██║ ██║██████╔╝ // ╚════██║██╔══╝ ██║ ██║ ██║██╔═══╝ // ███████║███████╗ ██║ ╚██████╔╝██║ // ╚══════╝╚══════╝ ╚═╝ ╚═════╝ ╚═╝ // If you're copying code from one of the sections in the switch statement below, // you'll probably also want to copy this setup code. // Set some defaults. var DEFAULT_LIMIT = 30; var DEFAULT_POPULATE_LIMIT = 30; // Get the name of the blueprint action being run. var blueprint = req.options.blueprintAction; // ┌─┐┌─┐┬─┐┌─┐┌─┐ ┌┬┐┌─┐┌┬┐┌─┐┬ // ├─┘├─┤├┬┘└─┐├┤ ││││ │ ││├┤ │ // ┴ ┴ ┴┴└─└─┘└─┘ ┴ ┴└─┘─┴┘└─┘┴─┘ // Get the model identity from the action name (e.g. 'user/find'). var model = req.options.action.split('/')[0]; if (!model) { throw new Error(util.format('No "model" specified in route options.')); } // Get the model class. var Model = req._sails.models[model]; if ( !Model ) { throw new Error(util.format('Invalid route option, "model".\nI don\'t know about any models named: `%s`',model)); } // ┌┬┐┌─┐┌─┐┌─┐┬ ┬┬ ┌┬┐ ┌─┐┌─┐┌─┐┬ ┬┬ ┌─┐┌┬┐┌─┐┌─┐ // ││├┤ ├┤ ├─┤│ ││ │ ├─┘│ │├─┘│ ││ ├─┤ │ ├┤ └─┐ // ─┴┘└─┘└ ┴ ┴└─┘┴─┘┴ ┴ └─┘┴ └─┘┴─┘┴ ┴ ┴ └─┘└─┘ // Get the default populates array var defaultPopulates = _.reduce(Model.associations, function(memo, association) { if (association.type === 'collection') { memo[association.alias] = { where: {}, limit: DEFAULT_POPULATE_LIMIT, skip: 0, select: [ '*' ], omit: [] }; } else { memo[association.alias] = {}; } return memo; }, {}); // Initialize the queryOptions dictionary we'll be returning. var queryOptions = { using: model, populates: defaultPopulates }; switch (blueprint) { // ███████╗██╗███╗ ██╗██████╗ ██╗ // ██╔════╝██║████╗ ██║██╔══██╗ ██╔╝ // █████╗ ██║██╔██╗ ██║██║ ██║ ██╔╝ // ██╔══╝ ██║██║╚██╗██║██║ ██║ ██╔╝ // ██║ ██║██║ ╚████║██████╔╝ ██╔╝ // ╚═╝ ╚═╝╚═╝ ╚═══╝╚═════╝ ╚═╝ // // ███████╗██╗███╗ ██╗██████╗ ██████╗ ███╗ ██╗███████╗ // ██╔════╝██║████╗ ██║██╔══██╗██╔═══██╗████╗ ██║██╔════╝ // █████╗ ██║██╔██╗ ██║██║ ██║██║ ██║██╔██╗ ██║█████╗ // ██╔══╝ ██║██║╚██╗██║██║ ██║██║ ██║██║╚██╗██║██╔══╝ // ██║ ██║██║ ╚████║██████╔╝╚██████╔╝██║ ╚████║███████╗ // ╚═╝ ╚═╝╚═╝ ╚═══╝╚═════╝ ╚═════╝ ╚═╝ ╚═══╝╚══════╝ case 'find': case 'findOne': queryOptions.criteria = {}; // ┌─┐┌─┐┬─┐┌─┐┌─┐ ╔═╗╦═╗╦╔╦╗╔═╗╦═╗╦╔═╗ // ├─┘├─┤├┬┘└─┐├┤ ║ ╠╦╝║ ║ ║╣ ╠╦╝║╠═╣ // ┴ ┴ ┴┴└─└─┘└─┘ ╚═╝╩╚═╩ ╩ ╚═╝╩╚═╩╩ ╩ queryOptions.criteria.where = (function getWhereCriteria(){ var where = {}; // For `findOne`, set "where" to just look at the primary key. if (blueprint === 'findOne') { where[Model.primaryKey] = req.param('id'); return where; } // Look for explicitly specified `where` parameter. where = req.allParams().where; // If `where` parameter is a string, try to interpret it as JSON. // (If it cannot be parsed, throw a UsageError.) if (_.isString(where)) { try { where = JSON.parse(where); } catch (e) { throw flaverr({ name: 'UsageError' }, new Error('Could not JSON.parse() the provided `where` clause. Here is the raw error: '+e.stack)); } }//>-• // If `where` has not been specified, but other unbound parameter variables // **ARE** specified, build the `where` option using them. if (!where) { // Prune params which aren't fit to be used as `where` criteria // to build a proper where query where = req.allParams(); // Omit built-in runtime config (like query modifiers) where = _.omit(where, ['limit', 'skip', 'sort', 'populate', 'select', 'omit']); // Omit any params that have `undefined` on the RHS. where = _.omit(where, function(p) { if (_.isUndefined(p)) { return true; } }); }//>- // Return final `where`. return where; })(); // ┌─┐┌─┐┬─┐┌─┐┌─┐ ┌─┐┌─┐┬ ┌─┐┌─┐┌┬┐ // ├─┘├─┤├┬┘└─┐├┤ └─┐├┤ │ ├┤ │ │ // ┴ ┴ ┴┴└─└─┘└─┘ └─┘└─┘┴─┘└─┘└─┘ ┴ if (!_.isUndefined(req.param('select'))) { queryOptions.criteria.select = req.param('select').split(',').map(function(attribute) {return attribute.trim();}); } // ┌─┐┌─┐┬─┐┌─┐┌─┐ ┌─┐┌┬┐┬┌┬┐ // ├─┘├─┤├┬┘└─┐├┤ │ │││││ │ // ┴ ┴ ┴┴└─└─┘└─┘ └─┘┴ ┴┴ ┴ else if (!_.isUndefined(req.param('omit'))) { queryOptions.criteria.omit = req.param('omit').split(',').map(function(attribute) {return attribute.trim();}); } // ┌─┐┌─┐┬─┐┌─┐┌─┐ ┬ ┬┌┬┐┬┌┬┐ // ├─┘├─┤├┬┘└─┐├┤ │ │││││ │ // ┴ ┴ ┴┴└─└─┘└─┘ ┴─┘┴┴ ┴┴ ┴ if (!_.isUndefined(req.param('limit'))) { queryOptions.criteria.limit = req.param('limit'); } else { queryOptions.criteria.limit = DEFAULT_LIMIT; } // ┌─┐┌─┐┬─┐┌─┐┌─┐ ┌─┐┬┌─┬┌─┐ // ├─┘├─┤├┬┘└─┐├┤ └─┐├┴┐│├─┘ // ┴ ┴ ┴┴└─└─┘└─┘ └─┘┴ ┴┴┴ if (!_.isUndefined(req.param('skip'))) { queryOptions.criteria.skip = req.param('skip'); } // ┌─┐┌─┐┬─┐┌─┐┌─┐ ┌─┐┌─┐┬─┐┌┬┐ // ├─┘├─┤├┬┘└─┐├┤ └─┐│ │├┬┘ │ // ┴ ┴ ┴┴└─└─┘└─┘ └─┘└─┘┴└─ ┴ if (!_.isUndefined(req.param('sort'))) { queryOptions.criteria.sort = (function getSortCriteria() { var sort = req.param('sort'); if (_.isUndefined(sort)) {return undefined;} // If `sort` is a string, attempt to JSON.parse() it. // (e.g. `{"name": 1}`) if (_.isString(sort)) { try { sort = JSON.parse(sort); // If it is not valid JSON (e.g. because it's just some other string), // then just fall back to interpreting it as-is (e.g. "name ASC") } catch(unusedErr) {} } return sort; })(); } // ┌─┐┌─┐┬─┐┌─┐┌─┐ ┌─┐┌─┐┌─┐┬ ┬┬ ┌─┐┌┬┐┌─┐ // ├─┘├─┤├┬┘└─┐├┤ ├─┘│ │├─┘│ ││ ├─┤ │ ├┤ // ┴ ┴ ┴┴└─└─┘└─┘ ┴ └─┘┴ └─┘┴─┘┴ ┴ ┴ └─┘ // If a `populate` param was sent, filter the attributes to populate // against that value. // e.g.: // /model?populate=alias1,alias2,alias3 // /model?populate=[alias1,alias2,alias3] if (req.param('populate')) { queryOptions.populates = (function getPopulates() { // Get the request param. var attributes = req.param('populate'); // If it's `false`, populate nothing. if (attributes === 'false') { return {}; } // Split the list on commas. attributes = attributes.split(','); // Trim whitespace off of the attributes. attributes = _.reduce(attributes, function(memo, attribute) { memo[attribute.trim()] = {}; return memo; }, {}); return attributes; })(); } break; // ██████╗██████╗ ███████╗ █████╗ ████████╗███████╗ // ██╔════╝██╔══██╗██╔════╝██╔══██╗╚══██╔══╝██╔════╝ // ██║ ██████╔╝█████╗ ███████║ ██║ █████╗ // ██║ ██╔══██╗██╔══╝ ██╔══██║ ██║ ██╔══╝ // ╚██████╗██║ ██║███████╗██║ ██║ ██║ ███████╗ // ╚═════╝╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝ ╚═╝ ╚══════╝ case 'create': // Set `fetch: true` queryOptions.meta = { fetch: true }; // ┌─┐┌─┐┬─┐┌─┐┌─┐ ┬ ┬┌─┐┬ ┬ ┬┌─┐┌─┐ // ├─┘├─┤├┬┘└─┐├┤ └┐┌┘├─┤│ │ │├┤ └─┐ // ┴ ┴ ┴┴└─└─┘└─┘ └┘ ┴ ┴┴─┘└─┘└─┘└─┘ queryOptions.newRecord = (function getNewRecord(){ // Use all of the request params as values for the new record. var values = req.allParams(); // Attempt to JSON parse any collection attributes into arrays. This is to allow // setting collections using the shortcut routes. _.each(Model.attributes, function(attrDef, attrName) { if (attrDef.collection && (!req.body || !req.body[attrName]) && (req.query && _.isString(req.query[attrName]))) { try { values[attrName] = JSON.parse(req.query[attrName]); // If it is not valid JSON (e.g. because it's just a normal string), // then fall back to interpreting it as-is } catch(unusedErr) {} } }); return values; })(); break; // ██╗ ██╗██████╗ ██████╗ █████╗ ████████╗███████╗ // ██║ ██║██╔══██╗██╔══██╗██╔══██╗╚══██╔══╝██╔════╝ // ██║ ██║██████╔╝██║ ██║███████║ ██║ █████╗ // ██║ ██║██╔═══╝ ██║ ██║██╔══██║ ██║ ██╔══╝ // ╚██████╔╝██║ ██████╔╝██║ ██║ ██║ ███████╗ // ╚═════╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚══════╝ case 'update': queryOptions.criteria = { where: {} }; queryOptions.criteria.where[Model.primaryKey] = req.param('id'); // Note that we do NOT set `fetch: true`, because if we do so, some versions // of Waterline complain that `fetch` need not be included with .updateOne(). // (Now that we take advantage of .updateOne() in blueprints, this is a thing.) queryOptions.meta = {}; // ┌─┐┌─┐┬─┐┌─┐┌─┐ ┬ ┬┌─┐┬ ┬ ┬┌─┐┌─┐ // ├─┘├─┤├┬┘└─┐├┤ └┐┌┘├─┤│ │ │├┤ └─┐ // ┴ ┴ ┴┴└─└─┘└─┘ └┘ ┴ ┴┴─┘└─┘└─┘└─┘ queryOptions.valuesToSet = (function getValuesToSet(){ // Use all of the request params as values for the new record, _except_ `id`. var values = _.omit(req.allParams(), 'id'); // No matter what, don't allow changing the PK via the update blueprint // (you should just drop and re-add the record if that's what you really want) if (typeof values[Model.primaryKey] !== 'undefined' && values[Model.primaryKey] !== queryOptions.criteria.where[Model.primaryKey]) { req._sails.log.warn('Cannot change primary key via update blueprint; ignoring value sent for `' + Model.primaryKey + '`'); } // Make sure the primary key is unchanged values[Model.primaryKey] = queryOptions.criteria.where[Model.primaryKey]; return values; })(); break; // ██████╗ ███████╗███████╗████████╗██████╗ ██████╗ ██╗ ██╗ // ██╔══██╗██╔════╝██╔════╝╚══██╔══╝██╔══██╗██╔═══██╗╚██╗ ██╔╝ // ██║ ██║█████╗ ███████╗ ██║ ██████╔╝██║ ██║ ╚████╔╝ // ██║ ██║██╔══╝ ╚════██║ ██║ ██╔══██╗██║ ██║ ╚██╔╝ // ██████╔╝███████╗███████║ ██║ ██║ ██║╚██████╔╝ ██║ // ╚═════╝ ╚══════╝╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ case 'destroy': queryOptions.criteria = {}; queryOptions.criteria = { where: {} }; queryOptions.criteria.where[Model.primaryKey] = req.param('id'); // Set `fetch: true` queryOptions.meta = { fetch: true }; break; // █████╗ ██████╗ ██████╗ // ██╔══██╗██╔══██╗██╔══██╗ // ███████║██║ ██║██║ ██║ // ██╔══██║██║ ██║██║ ██║ // ██║ ██║██████╔╝██████╔╝ // ╚═╝ ╚═╝╚═════╝ ╚═════╝ case 'add': if (!req.options.alias) { throw new Error('Missing required route option, `req.options.alias`.'); } queryOptions.alias = req.options.alias; queryOptions.targetRecordId = req.param('parentid'); queryOptions.associatedIds = [req.param('childid')]; break; // ██████╗ ███████╗███╗ ███╗ ██████╗ ██╗ ██╗███████╗ // ██╔══██╗██╔════╝████╗ ████║██╔═══██╗██║ ██║██╔════╝ // ██████╔╝█████╗ ██╔████╔██║██║ ██║██║ ██║█████╗ // ██╔══██╗██╔══╝ ██║╚██╔╝██║██║ ██║╚██╗ ██╔╝██╔══╝ // ██║ ██║███████╗██║ ╚═╝ ██║╚██████╔╝ ╚████╔╝ ███████╗ // ╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝ ╚═════╝ ╚═══╝ ╚══════╝ case 'remove': if (!req.options.alias) { throw new Error('Missing required route option, `req.options.alias`.'); } queryOptions.alias = req.options.alias; queryOptions.targetRecordId = req.param('parentid'); queryOptions.associatedIds = [req.param('childid')]; break; // ██████╗ ███████╗██████╗ ██╗ █████╗ ██████╗███████╗ // ██╔══██╗██╔════╝██╔══██╗██║ ██╔══██╗██╔════╝██╔════╝ // ██████╔╝█████╗ ██████╔╝██║ ███████║██║ █████╗ // ██╔══██╗██╔══╝ ██╔═══╝ ██║ ██╔══██║██║ ██╔══╝ // ██║ ██║███████╗██║ ███████╗██║ ██║╚██████╗███████╗ // ╚═╝ ╚═╝╚══════╝╚═╝ ╚══════╝╚═╝ ╚═╝ ╚═════╝╚══════╝ case 'replace': if (!req.options.alias) { throw new Error('Missing required route option, `req.options.alias`.'); } queryOptions.alias = req.options.alias; queryOptions.criteria = {}; queryOptions.criteria = { where: {} }; queryOptions.targetRecordId = req.param('parentid'); queryOptions.associatedIds = _.isArray(req.body) ? req.body : req.query[req.options.alias]; if (_.isString(queryOptions.associatedIds)) { try { queryOptions.associatedIds = JSON.parse(queryOptions.associatedIds); } catch (e) { throw flaverr({ name: 'UsageError', raw: e }, new Error( 'The associated ids provided in this request (for the `' + req.options.alias + '` collection) are not valid. '+ 'If specified as a string, the associated ids provided to the "replace" blueprint action must be parseable as '+ 'a JSON array, e.g. `[1, 2]`.' // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // FUTURE: Use smart example depending on the expected pk type (e.g. if string, show mongo ids instead) // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - )); }//</catch> } break; // ██████╗ ██████╗ ██████╗ ██╗ ██╗██╗ █████╗ ████████╗███████╗ // ██╔══██╗██╔═══██╗██╔══██╗██║ ██║██║ ██╔══██╗╚══██╔══╝██╔════╝ // ██████╔╝██║ ██║██████╔╝██║ ██║██║ ███████║ ██║ █████╗ // ██╔═══╝ ██║ ██║██╔═══╝ ██║ ██║██║ ██╔══██║ ██║ ██╔══╝ // ██║ ╚██████╔╝██║ ╚██████╔╝███████╗██║ ██║ ██║ ███████╗ // ╚═╝ ╚═════╝ ╚═╝ ╚═════╝ ╚══════╝╚═╝ ╚═╝ ╚═╝ ╚══════╝ case 'populate': if (!req.options.alias) { throw new Error('Missing required route option, `req.options.alias`.'); } var association = _.find(Model.associations, {alias: req.options.alias}); if (!association) { throw new Error('Consistency violation: `populate` blueprint could not find association `' + req.options.alias + '` in model `' + Model.globalId + '`.'); } queryOptions.alias = req.options.alias; queryOptions.criteria = {}; // ┌─┐┌─┐┬─┐┌─┐┌─┐ ╔═╗╦═╗╦╔╦╗╔═╗╦═╗╦╔═╗ // ├─┘├─┤├┬┘└─┐├┤ ║ ╠╦╝║ ║ ║╣ ╠╦╝║╠═╣ // ┴ ┴ ┴┴└─└─┘└─┘ ╚═╝╩╚═╩ ╩ ╚═╝╩╚═╩╩ ╩ queryOptions.criteria = {}; queryOptions.criteria = { where: {} }; queryOptions.criteria.where[Model.primaryKey] = req.param('parentid'); queryOptions.populates = {}; queryOptions.populates[req.options.alias] = {}; // If this is a to-many association, add a `where` clause. if (association.collection) { queryOptions.populates[req.options.alias].where = (function getPopulateCriteria(){ var where = req.allParams().where; // If `where` parameter is a string, try to interpret it as JSON. // (If it cannot be parsed, throw a UsageError.) if (_.isString(where)) { try { where = JSON.parse(where); } catch (e) { throw flaverr({ name: 'UsageError' }, new Error('Could not JSON.parse() the provided `where` clause. Here is the raw error: '+e.stack)); } }//>-• // If `where` has not been specified, but other unbound parameter variables // **ARE** specified, build the `where` option using them. if (!where) { // Prune params which aren't fit to be used as `where` criteria // to build a proper where query where = req.allParams(); // Omit built-in runtime config (like top-level criteria clauses) where = _.omit(where, ['limit', 'skip', 'sort', 'populate', 'select', 'omit', 'parentid']); // - - - - - - - - - - - - - - - - - - - - - // ^^TODO: what about `where` itself? // - - - - - - - - - - - - - - - - - - - - - // Omit any params that have `undefined` on the RHS. where = _.omit(where, function(p) { if (_.isUndefined(p)) { return true; } }); }//>- // Return final `where`. return where; })(); } // ┌─┐┌─┐┬─┐┌─┐┌─┐ ┌─┐┌─┐┬ ┌─┐┌─┐┌┬┐ // ├─┘├─┤├┬┘└─┐├┤ └─┐├┤ │ ├┤ │ │ // ┴ ┴ ┴┴└─└─┘└─┘ └─┘└─┘┴─┘└─┘└─┘ ┴ if (!_.isUndefined(req.param('select'))) { queryOptions.populates[req.options.alias].select = req.param('select').split(',').map(function(attribute) {return attribute.trim();}); } // ┌─┐┌─┐┬─┐┌─┐┌─┐ ┌─┐┌┬┐┬┌┬┐ // ├─┘├─┤├┬┘└─┐├┤ │ │││││ │ // ┴ ┴ ┴┴└─└─┘└─┘ └─┘┴ ┴┴ ┴ else if (!_.isUndefined(req.param('omit'))) { queryOptions.populates[req.options.alias].omit = req.param('omit').split(',').map(function(attribute) {return attribute.trim();}); } // // ┌─┐┌─┐┬─┐┌─┐┌─┐ ┬ ┬┌┬┐┬┌┬┐ // ├─┘├─┤├┬┘└─┐├┤ │ │││││ │ // ┴ ┴ ┴┴└─└─┘└─┘ ┴─┘┴┴ ┴┴ ┴ if (!_.isUndefined(req.param('limit'))) { queryOptions.populates[req.options.alias].limit = req.param('limit'); } // If this is a to-many association, use the default limit if not was provided. else if (association.collection) { queryOptions.populates[req.options.alias].limit = DEFAULT_LIMIT; } // ┌─┐┌─┐┬─┐┌─┐┌─┐ ┌─┐┬┌─┬┌─┐ // ├─┘├─┤├┬┘└─┐├┤ └─┐├┴┐│├─┘ // ┴ ┴ ┴┴└─└─┘└─┘ └─┘┴ ┴┴┴ if (!_.isUndefined(req.param('skip'))) { queryOptions.populates[req.options.alias].skip = req.param('skip'); } // ┌─┐┌─┐┬─┐┌─┐┌─┐ ┌─┐┌─┐┬─┐┌┬┐ // ├─┘├─┤├┬┘└─┐├┤ └─┐│ │├┬┘ │ // ┴ ┴ ┴┴└─└─┘└─┘ └─┘└─┘┴└─ ┴ if (!_.isUndefined(req.param('sort'))) { queryOptions.populates[req.options.alias].sort = (function getSortCriteria() { var sort = req.param('sort'); if (_.isUndefined(sort)) {return undefined;} // If `sort` is a string, attempt to JSON.parse() it. // (e.g. `{"name": 1}`) if (_.isString(sort)) { try { sort = JSON.parse(sort); // If it is not valid JSON (e.g. because it's just a normal string), // then fall back to interpreting it as-is (e.g. "fullName ASC") } catch(unusedErr) {} } return sort; })();//ˆ } break; } return queryOptions; }; ================================================ FILE: lib/hooks/helpers/index.js ================================================ /** * Module dependencies */ var fs = require('fs'); var path = require('path'); var _ = require('@sailshq/lodash'); var chalk = require('chalk'); var machine = require('machine'); var loadHelpers = require('./private/load-helpers'); var iterateHelpers = require('./private/iterate-helpers'); /** * Helpers hook */ module.exports = function(sails) { // Private variable used below to keep track of whether a compatibility // warning message might need to be displayed. var _prereleaseCompatWarning; return { defaults: { helpers: { // Custom usage/miscellaneous options: usageOpts: { arginStyle: 'serial', execStyle: 'natural' }, // Experimental: Programmatically provide a dictionary of helpers. moduleDefinitions: undefined, } }, configure: function() { // Check SVR of the `sails` dep in this app's package.json file. // // If it points at a 1.0 prerelease less than `1.0.0-44`, then log a warning // explaining what's going on with helpers (we needed to make a breaking // change in a prerelease), and that the old way can be achieved through // configuration. Also mention that, to avoid breaking this app, we've // applied the old configuration automatically, but that if the SVR in the // package.json is changed, then this protection will disappear, and this // app will potentially break. // // To resolve this, either: // • (A) configure `sails.config.helpers.usageOpts` as `{arginStyle: 'named', execStyle: 'deferred'}` // • or (B) change any code in this app that invokes helpers to take advantage of the new default style // of usage, then upgrade to the latest version of Sails v1.0 and update // your package.json file accordingly to make this warning go away. if (sails.config.helpers.usageOpts.arginStyle !== 'named' || sails.config.helpers.usageOpts.execStyle !== 'deferred') { var localSailsSVR = (function _gettingSVROfLocalSailsDep(){ var pjPath = path.resolve(sails.config.appPath, 'package.json'); var rawPjText; try { rawPjText = fs.readFileSync(pjPath, 'utf8'); } catch (unusedErr) {} var appPj; try { appPj = JSON.parse(rawPjText); } catch (unusedErr) {} return appPj && appPj.dependencies && appPj.dependencies.sails; })();//† var prerelease = localSailsSVR && localSailsSVR.match(/1\.0\.0\-(.+)$/); prerelease = prerelease && prerelease[1]; var isMinSVRPointedAtSensitiveV1Prerelease = prerelease && Number(prerelease) < 44; if (isMinSVRPointedAtSensitiveV1Prerelease) { sails.config.helpers.usageOpts.arginStyle = 'named'; sails.config.helpers.usageOpts.execStyle = 'deferred'; // Note that we don't actually log the warning right now-- we won't do that // until a bit later, in .initialize(). Even then, we'll only actually log // the warning if there are helpers defined in the app. (Because if there // aren't any helpers, logging a warning would just be annoying!) _prereleaseCompatWarning = '---------------------------------------------------------------------\n'+ 'Based on its package.json file, it looks like this app was built\n'+ 'using the Sails beta, but with a version prior to v1.0.0-44.\n'+ '(This app depends on sails@'+localSailsSVR+'.)\n'+ '\n'+ 'In the 1.0.0-44 prerelease of Sails, changes were introduced. By\n'+ 'default, helpers now expect serial arguments instead of a dictionary\n'+ 'of named parameters. In other words, you\'d now call:\n'+ ' await sails.helpers.passwords.changePassword(\'abc123\')\n'+ 'Instead of:\n'+ ' await sails.helpers.passwords.changePassword({password:\'abc123\'})\n'+ '\n'+ 'Additionally, it is no longer necessary to call .now() or .execSync()\n'+ 'for synchronous helpers-- by default they are invoked automatically.\n'+ '(Not a fan? Sorry about the inconvenience! And don\'t worry, it\'s\n'+ 'easy to change.)\n'+ '\n'+ 'To avoid breaking this app, some special settings that make Sails\n'+ 'backwards-compatible have been set automatically for you. But please\n'+ 'be sure to take the steps below to resolve this as soon as possible.\n'+ '(What if you forgot about this and changed your package.json file?\n'+ 'You might inadvertently remove this compatibility check... And if\n'+ 'that were to happen, the next time you tried to lift your app, your\n'+ 'helpers would no longer work!)\n'+ '\n'+ 'To resolve this, use one of the following solutions:\n'+ '\n'+ ' (A) <<<<Quick & dirty>>>>\n'+ ' If you need a quick fix, or you just prefer to call helpers the old\n'+ ' way, no problem: just nestle this in your .sailsrc file:\n'+ ' "helpers": {\n'+ ' "usageOpts": {\n'+ ' "arginStyle": "named",\n'+ ' "execStyle": "deferred"\n'+ ' }\n'+ ' }\n'+ ' ^^That will make helpers behave exactly like they did before.\n'+ '\n'+ ' (B) <<<<Recommended>>>>\n'+ ' Change any relevant code in this app (e.g. `sails.helpers.x({…})`)\n'+ ' to take advantage of serial usage, or chain on .with({…}). Then, \n'+ ' update the `sails` dependency in your package.json file so that it\n'+ ' satisfies ^1.0.0-44 or higher.\n'+ '\n'+ ' Note: If you go with this approach, it\'s not all or nothing. You\n'+ ' you can always use .with() to call a helper with named parameters\n'+ ' on a one-off basis. For example:\n'+ ' await sails.helpers.changePassword.with({password:\'abc123\'});\n'+ '\n'+ '\n'+ '(To hide this message, apply one of the solutions suggested above.)\n'+ '\n'+ ' [?] If you\'re unsure, visit https://sailsjs.com/support\n'+ '---------------------------------------------------------------------\n'; }//fi }//fi // Define `sails.helpers` here so that it can potentially be used by other hooks. // > NOTE: This is NOT `sails.config.helpers`-- this is `sails.helpers`! // > (As for sails.config.helpers, it's set automatically based on our `defaults above) sails.helpers = {}; Object.defineProperty(sails.helpers, Symbol.for('nodejs.util.inspect.custom'), { enumerable: false, configurable: false, writable: true, value: function inspect(){ // Tree diagram: // ``` // . // ├── … // │ ├── … // │ │ └── … // │ ├── … // │ └── … // │ // ├── … // │ ├── … // │ │ └── … // │ ├── … // │ │ ├── … // │ │ ├── … // │ │ └── … // │ ├── … // │ └── … // │ // ├── … // │ └── … // │ // ├── … // ├── … // └── … // ``` var treeDiagram = (function(){ var OFFSET = ' '; var TAB = ' '; var SYMBOL_INITIAL = '. '; var SYMBOL_NO_BRANCH = '│ '; var SYMBOL_MID_BRANCH = '├── '; var SYMBOL_LAST_BRANCH = '└── '; var treeDiagram = ''; treeDiagram += OFFSET + SYMBOL_INITIAL + '\n'; iterateHelpers( sails.helpers, function _onBeforeStartingPack(pack, key, depth, isFirst, isLast, lastnessPerAncestor){ var indentation = _.reduce(lastnessPerAncestor, function(indentation, wasLast){ if (wasLast) { indentation += TAB; } else { indentation += SYMBOL_NO_BRANCH; } return indentation; }, ''); if (isLast) { treeDiagram += OFFSET + indentation + SYMBOL_LAST_BRANCH + key + '\n'; } else { treeDiagram += OFFSET + indentation + SYMBOL_MID_BRANCH + key + '\n'; } }, undefined,// « no need for an _onAfterFinishingPack notifier here, so we omit it function _onHelper(callable, methodName, depth, isFirst, isLast, lastnessPerAncestor){ var indentation = _.reduce(lastnessPerAncestor, function(indentation, wasLast){ if (wasLast) { indentation += TAB; } else { indentation += SYMBOL_NO_BRANCH; } return indentation; }, ''); if (isLast) { treeDiagram += OFFSET + indentation + SYMBOL_LAST_BRANCH + (callable.toJSON()._fromLocalSailsApp ? chalk.bold.cyan(methodName) : chalk.italic(methodName)) + chalk.gray('()')+'\n'; if (depth === 2) { treeDiagram += OFFSET + indentation + '\n'; } } else { treeDiagram += OFFSET + indentation + SYMBOL_MID_BRANCH + (callable.toJSON()._fromLocalSailsApp ? chalk.bold.cyan(methodName) : chalk.italic(methodName)) + chalk.gray('()')+'\n'; } } ); return treeDiagram; })();//† // Examples (asynchronous and synchronous) var example1 = (function(){ var exampleArginPhrase = ''; if (sails.config.helpers.usageOpts.arginStyle === 'named') { exampleArginPhrase = '{dir: \'./colorado/\'}'; } else if (sails.config.helpers.usageOpts.arginStyle === 'serial') { exampleArginPhrase = '\'./colorado/\''; } return 'var contents = await sails.helpers.fs.ls('+exampleArginPhrase+');'; })();//† var example2 = (function(){ var exampleArginPhrase = ''; if (sails.config.helpers.usageOpts.arginStyle === 'named') { exampleArginPhrase = '{style: \'url-friendly\'}'; } else if (sails.config.helpers.usageOpts.arginStyle === 'serial') { exampleArginPhrase = '\'url-friendly\''; } if (sails.config.helpers.usageOpts.execStyle === 'deferred') { return 'var name = sails.helpers.strings.random('+exampleArginPhrase+').now();'; } else if (sails.config.helpers.usageOpts.execStyle === 'immediate' || sails.config.helpers.usageOpts.execStyle === 'natural') { return 'var name = sails.helpers.strings.random('+exampleArginPhrase+');'; } throw new Error('Consistency violation: Unrecognized arginStyle/execStyle in sails.config.helpers.usageOpts (This should never happen, since it should have already been validated and prevented from being built- please report at https://sailsjs.com/bugs)'); })();//† return ''+ '-------------------------------------------------------\n'+ ' sails.helpers\n'+ '\n'+ ' Available methods:\n'+ treeDiagram+'\n'+ '\n'+ ' Example usage:\n'+ ' '+example1+'\n'+ ' '+example2+'\n'+ '\n'+ ' More info:\n'+ ' https://sailsjs.com/support\n'+ '-------------------------------------------------------\n'; }//ƒ });//…) }, initialize: function(done) { // Load helpers from the appropriate folder. loadHelpers(sails, function(err) { if (err) { return done(err); } // If deemed relevant, log the prerelease compatibility warning that // we built above. (Then clear it out, since we don't want to ever // display it again during this "lift" cycle-- even if the experimental // .reload() method is in use.) if (_prereleaseCompatWarning && _.keys(sails.helpers).length > 0) { sails.log.warn(_prereleaseCompatWarning); _prereleaseCompatWarning = ''; } return done(); });//_∏_ }, /** * @experimental * (This might change at any time, without a major version release!) */ furnishPack: function(slug, packInfo){ packInfo = packInfo || {}; slug = _.map(slug.split('.'), _.kebabCase).join('.'); var slugKeyPath = _.map(slug.split('.'), _.camelCase).join('.'); var chunks = slugKeyPath.split('.'); if (chunks.length > 1) { sails.log.verbose( 'Watch out! Nesting helpers more than one sub-folder deep can be a liability. '+ 'It also means that you\'ll need to type more every time you want to use '+ 'your helper. Instead, try keeping your directory structure as flat as possible; '+ 'i.e. in general, having more explicit filenames is better than having deep, '+ 'complicated folder hierarchies.' ); } // If pack already exists, avast. if (_.get(sails.helpers, slugKeyPath)) { return; } // Ancestor packs: var thisKeyPath; var theseChunks; var parentKeyPath; var parentPackOrRoot; for (var i = 0; i < chunks.length - 1; i++) { theseChunks = chunks.slice(0,i+1); thisKeyPath = theseChunks.join('.'); parentKeyPath = theseChunks.slice(0, -1).join('.'); if (!_.get(sails.helpers, thisKeyPath)) { parentPackOrRoot = parentKeyPath ? _.get(sails.helpers, parentKeyPath) : sails.helpers; parentPackOrRoot[chunks[i]] = machine.pack({ name: 'sails.helpers.'+chunks.slice(0,i+1).join('.'), defs: {}, customize: _.extend({}, sails.config.helpers.usageOpts, { implementationSniffingTactic: sails.config.implementationSniffingTactic||undefined }) }); } }//∞ // Main pack: parentKeyPath = chunks.slice(0, -1).join('.'); parentPackOrRoot = parentKeyPath ? _.get(sails.helpers, parentKeyPath) : sails.helpers; parentPackOrRoot[chunks[chunks.length - 1]] = machine.pack(_.extend({}, packInfo, { name: 'sails.helpers.'+slugKeyPath, customize: _.extend({}, sails.config.helpers.usageOpts, { implementationSniffingTactic: sails.config.implementationSniffingTactic||undefined }) })); }, /** * @experimental * (This might change at any time, without a major version release!) */ furnishHelper: function(identityPlusMaybeSlug, nmDef){ // Ensure we're starting off with dot-delimited, kebab-cased hops. identityPlusMaybeSlug = _.map(identityPlusMaybeSlug.split('.'), _.kebabCase).join('.'); var chunks = identityPlusMaybeSlug.split('.'); // slug ('foo-bar.baz-bing.beep.boop') // identity ('do-something') var slug = chunks.length >= 2 ? chunks.slice(0, -1).join('.') : undefined; var identity = _.last(chunks); // Camel-case every part of the file path, and join with dots // e.g. admin-stuff.foo.do-something => adminStuff.foo.doSomething var slugKeyPath = slug ? _.map(slug.split('.'), _.camelCase).join('.') : undefined; var fullKeyPath = slug ? slugKeyPath + '.' + machine.getMethodName(identity) : machine.getMethodName(identity); if (!_.get(sails.helpers, fullKeyPath)) { // Work our way down if (slug && !_.get(sails.helpers, slugKeyPath)) { this.furnishPack(slug, { name: 'sails.helpers.'+slugKeyPath, defs: {} }); }//fi // And then build the helper last // > (can't do it first! We'd confuse `_.get()`!) // Use provided `identity` if no explicit identity was set. // (Otherwise, as of machine@v15, this could fail with an ImplementationError.) if (!nmDef.identity) { nmDef.identity = identity; } // Attach new method to the appropriate pack. // e.g. sails.helpers.userHelpers.foo.myHelper if (slug) { var parentPack = _.get(sails.helpers, slugKeyPath); parentPack.registerDefs( (function(){ var defs = {}; defs[identity] = nmDef; return defs; })()//† ); } else { sails.helpers[machine.getMethodName(identity)] = machine.buildWithCustomUsage(_.extend( {}, sails.config.helpers.usageOpts, { def: nmDef, implementationSniffingTactic: sails.config.implementationSniffingTactic } )); } }//fi }, /** * sails.hooks.helpers.reload() * * @param {Dictionary?} helpers [if specified, these helpers will replace all existing helpers. Otherwise, if omitted, helpers will be freshly reloaded from disk, and old helpers will be thrown away.] * @param {Function} done [optional callback] * * @experimental * (This might change at any time, without a major version release!) */ reload: function(helpers, done) { // Handle variadic usage if (typeof helpers === 'function') { done = helpers; helpers = undefined; } // Handle optional callback done = done || function _noopCb(err){ if (err) { sails.log.error('Could not reload helpers due to an error:', err, '\n(continuing anyway...)'); } };//ƒ // If we received an explicit set of helpers to load, use them. // Otherwise reload helpers from the appropriate folder. if (helpers) { sails.helpers = helpers; return done(); } else { return loadHelpers(sails, done); } }//ƒ }; }; ================================================ FILE: lib/hooks/helpers/private/iterate-helpers.js ================================================ /** * Module dependencies */ var _ = require('@sailshq/lodash'); /** * iterateHelpers() * * @private * --------------------------------------------------------------- * @param {Dictionary} root * the initial pack, or `sails.helpers` * * @param {Function?} onBeforeRecurse(branch,key,depth,isFirst,isLast,lastnessPerAncestor) * * @param {Function?} onAfterRecurse(branch,key,depth,isFirst,isLast,lastnessPerAncestor) * * @param {Function?} onLeaf(leaf,key,depth,isFirst,isLast,lastnessPerAncestor) * --------------------------------------------------------------- */ module.exports = function iterateHelpers(initialPackOrRoot, onBeforeRecurse, onAfterRecurse, onLeaf){ (function $recurse(parentPackOrRoot, lastnessPerAncestor, depth){ // Build an array of keys, sorted by: // • packs first, then helpers // • alphabetical by key after that var keys = _.sortByAll(_.keys(parentPackOrRoot), function(key){ var branch = parentPackOrRoot[key]; if (!_.isFunction(branch) && branch.toJSON && branch.toJSON().defs) { //(pack) return '________'+key; } else if (_.isFunction(branch) && branch.toJSON && branch.toJSON().identity) { //(helper) return key; } else { //(mystery meat?) return Infinity; } }); _.each(keys, function(key, keyIdx){ var branch = parentPackOrRoot[key]; var isFirst = keyIdx === 0; var isLast = keyIdx === keys.length - 1; // Duck-type this branch and handle it accordingly. if (!_.isFunction(branch) && branch.toJSON && branch.toJSON().defs) { // (pack) if (onBeforeRecurse) { onBeforeRecurse(branch, key, depth + 1, isFirst, isLast, lastnessPerAncestor); } $recurse(branch, lastnessPerAncestor.concat([isLast]), depth + 1); if (onAfterRecurse) { onAfterRecurse(branch, key, depth + 1, isFirst, isLast, lastnessPerAncestor); } } else if (_.isFunction(branch) && branch.toJSON && branch.toJSON().identity) { // (helper) if (onLeaf) { onLeaf(branch, key, depth + 1, isFirst, isLast, lastnessPerAncestor); } } else { // (mystery meat?) // ignore it. } });//∞ })(initialPackOrRoot, [], 0);//‰ };//ƒ ================================================ FILE: lib/hooks/helpers/private/load-helpers.js ================================================ /** * Module dependencies */ var _ = require('@sailshq/lodash'); var flaverr = require('flaverr'); var includeAll = require('include-all'); /** * loadHelpers() * * Load helper definitions from disk, build them into Callables, then attach * them to the `sails.helpers` dictionary. * * @param {SailsApp} sails * @param {Function} done * @param {Error?} err */ module.exports = function loadHelpers(sails, done) { // Load helper defs out of the specified folder includeAll.optional({ dirname: sails.config.paths.helpers, filter: /^([^.]+)\.(?:(?!md|txt).)+$/, flatten: true, keepDirectoryPath: true }, function(err, helperDefs) { if (err) { return done(err); } // If any helpers were specified when loading Sails, add those on // top of the ones loaded from disk. (Experimental) if (sails.config.helpers.moduleDefinitions) { // Note that this is a shallow merge! _.extend(helperDefs, sails.config.helpers.moduleDefinitions); } try { // Loop through each helper def, attempting to build each one as // a Callable (a.k.a. "wet machine") _.each(helperDefs, function(helperDef, identity) { try { // Camel-case every part of the file path, and join with dots // e.g. /user-helpers/foo/my-helper => userHelpers.foo.myHelper var keyPath = _.map(identity.split('/'), _.camelCase).join('.'); // Save _loadedFrom property for debugging purposes. // (e.g. `financial/calculate-mortgage-series`) helperDef._loadedFrom = identity; // Save _fromLocalSailsApp for internal use. helperDef._fromLocalSailsApp = true; // Use filename-derived `identity` REGARDLESS if an explicit identity // was set. (And exclude any extra hierarchy.) Otherwise, as of // machine@v15, this could fail with an ImplementationError. helperDef.identity = identity.match(/\//) ? _.last(identity.split('/')) : identity; // Check helper def to make sure it doesn't include any obvious signs // of confusion with actions -- e.g. no "responseType". If anything // like that is detected, log a warning. if (helperDef.files) { sails.log.warn( 'Ignoring unexpected `files` property in helper definition loaded '+ 'from '+helperDef._loadedFrom+'. This feature can only be used '+ 'by actions, not by helpers!' ); } var hasAnyConfusingExitProps = ( _.isObject(helperDef.exits) && _.any(helperDef.exits, function(exitDef){ return ( _.isObject(exitDef) && ( exitDef.responseType !== undefined || exitDef.viewTemplatePath !== undefined || exitDef.statusCode !== undefined ) ); }) ); if (hasAnyConfusingExitProps) { sails.log.warn( 'Ignoring unexpected property in one of the exits of the helper '+ 'definition loaded from '+helperDef._loadedFrom+'. Features like '+ '`responseType`, `viewTemplatePath`, and `statusCode` can only be '+ 'used by actions, not by helpers!' ); } // Build & expose helper on `sails.helpers` // > e.g. sails.helpers.userHelpers.foo.myHelper sails.hooks.helpers.furnishHelper(keyPath, helperDef); } catch (err) { // If an error occurs building the callable, throw here to bust // out of the _.each loop early throw flaverr({ code: 'E_FAILED_TO_BUILD_CALLABLE', identity: helperDef.identity, loadedFrom: identity, raw: err }, err); } });//∞ } catch (err) { // Handle any errors building Callables for our helpers by sending the // errors through the hook callback, which will cause Sails to halt lifting. if (flaverr.taste('E_FAILED_TO_BUILD_CALLABLE', err)) { return done(flaverr({ message: 'Failed to load helper `' + err.loadedFrom +'` into a Callable! '+err.message }, err)); } else { return done(err); } }//</ caught > // --• Everthing worked! return done(); }); }; ================================================ FILE: lib/hooks/http/README.md ================================================ # http (Core Hook) ## Status > ##### Stability: [2](https://github.com/balderdashy/sails-docs/blob/master/contributing/stability-index.md) - Stable ## Dependencies In order for this hook to load, the following other hooks must have already finished loading: - moduleloader ## Peers If the following other core hooks are enabled, the behavior of this hook will change: - session - views ## Dependents If this hook is disabled, in order for Sails to load, the following other core hooks must also be disabled: - views ## Purpose This hook's responsibilities are: ##### Start an HTTP server and handle requests This hook starts an http or https server and listens for incoming requests when Sails core emits an event letting us know it's time to "lift" (rather than just "load"). ##### Bind the configured middleware, along with built-in defaults This hook binds built-in HTTP middleware, in addition to custom middleware functions (`sails.config.http.middleware`). The order in which middleware can be bound is configurable in `sails.config.http.middleware.order`. > Note that it is possible for the configured HTTP middleware stack to be shared with the > core router built into Sails-- this would make the same stack take effect for all virtual requests > including sockets. Currently, an abbreviated version of this stack is built-in to `lib/router/` > in an imperative way (rather than the declarative approach used here: a sorted array of named middleware). > > In Sails core, this has been explored in a number of different ways in the past. > In the future, it would be possible to add a separate middleware stack configuration for virtual > requests (including socket requests). However, while this would certainly be more consistent, in practice, > this would have an unwanted impact on performance. ## Implicit Defaults This hook sets the following implicit default configuration on `sails.config`: > **TODO: document** > > _(if you'd like to help, please send a pull request expanding this section. See `hooks/logger/README.md` for an example)_ ## Events ##### `hook:http:loaded` Emitted when this hook has been automatically loaded by Sails core, and triggered the callback in its `initialize` function. ## Methods > **TODO: document** > > _(if you'd like to help, please send a pull request expanding this section. See `hooks/responses/README.md` for an example)_ ## FAQ > If you have a question about this hook that isn't covered here, please feel free to send a PR adding it to this section (even if you don't have the answer, a core maintainer will merge your PR and add an answer as soon as possible) ================================================ FILE: lib/hooks/http/get-configured-http-middleware-fns.js ================================================ /** * Module dependencies */ var Path = require('path'); var util = require('util'); var flaverr = require('flaverr'); var _ = require('@sailshq/lodash'); /** * getBuiltInHttpMiddleware() * * Return a dictionary containing all built-in middleware in Sails, * applying configuration along the way. * * @param {Router} expressRouterMiddleware [i.e. `app.router`] * @param {SailsApp} sails * @return {Dictionary} * @property {Function} * * @param {Request} req * @param {Response} res * @param {Function} next */ module.exports = function getBuiltInHttpMiddleware (expressRouterMiddleware, sails) { // Note that the environment of a Sails app is officially determined by // `sails.config.environment`. Normally, that is identical to what you'll // find inside `process.env.NODE_ENV`. // // However it is possible for NODE_ENV and `sails.config.environment to vary // (e.g. `sails.config.environment==='staging'` and `process.env.NODE_ENV==='production'`). // // Some middleware _depends on the NODE_ENV environment variable_ to determine // its behavior. Since NODE_ENV may have been set automatically, this is why the // relevant requires are included _within_ this function, rather than up top. // // This is also why the NODE_ENV environment variable is used here to determine // whether or not to consider the app "in production". This way, if you set // `NODE_ENV=production` explicitly, you can still use something like "staging" // or "sandbox" for your `sails.config.environment` in order to take advantage // of env-specific config files; while still having dependencies work like they // will in production (since NODE_ENV is set). // var IS_NODE_ENV_PRODUCTION = (process.env.NODE_ENV === 'production'); return _.defaults(sails.config.http.middleware || {}, { // Configure flat file server to serve static files // (By default, all explicit+shadow routes take precedence over flat files) www: (function() { var flatFileMiddleware = require('serve-static')(sails.config.paths['public'], { maxAge: sails.config.http.cache }); return flatFileMiddleware; })(), // If a Connect session store is configured, hook it up to Express session: (function() { // Silently do nothing if there's no session hook. // You can still have session middleware without the session hook enabled, // you just have to provide it yourself by configuring sails.config.http.middleware.session. if (!sails.hooks.session) { sails.log.verbose('Cannot load default HTTP session middleware when Sails session hook is disabled. Skipping...'); return; } // Complain a bit louder if the session hook is enabled, but not configured. if (!sails.config.session) { sails.log.error('Cannot load default HTTP session middleware without `sails.config.session` configured. Skipping...'); return; } var configuredSessionMiddleware = sails._privateSessionMiddleware; return function session(req, res, next){ // --• // Run the session middleware. configuredSessionMiddleware(req,res,function (err) { if (!err) { return next(); } var errMsg = 'Error occurred in session middleware :: ' + util.inspect((err&&err.stack)?err.stack:err, false, null); sails.log.error(errMsg); // If headers have already been sent (e.g. because of timing issues in application-level code), // then don't attempt to send another response. // (but still log a warning) if (res.headersSent) { sails.log.warn('The session middleware encountered an error and triggered its callback, but response headers have already been sent. Rather than attempting to send another response, failing silently...'); return; } // --• // Otherwise, we can go ahead and send a response. return res.status(400).send(errMsg); }); }; })(), // Build configured favicon mwr function. favicon: (function (){ var toServeFavicon = require('serve-favicon'); var pathToDefaultFavicon = Path.resolve(__dirname,'./default-favicon.ico'); var serveFaviconMwr = toServeFavicon(pathToDefaultFavicon); return serveFaviconMwr; })(), cookieParser: (function() { var cookieParser = sails.config.http.middleware.cookieParser; if (!cookieParser) { cookieParser = require('cookie-parser'); } var sessionSecret = sails.config.session && sails.config.session.secret; // If available, Sails uses the configured session secret for signing cookies. if (sessionSecret) { // Ensure secret is a string. This check happens in the session hook as well, // but sails.config.session.secret may still be provided even if the session hook // is turned off, so to be extra anal we'll check here as well. if (!_.isString(sessionSecret)) { throw flaverr({ name: 'userError', code: 'E_INVALID_SESSION_SECRET' }, new Error('If provided, sails.config.session.secret should be a string.')); } return cookieParser(sessionSecret); } // If no session secret was provided in config // (e.g. if session hook is disabled and config/session.js is removed) // then we do not enable signed cookies by providing a cookie secret. // (note that of course signed cookies can still be enabled in a Sails app: // see conceptual docs on disabling the session hook for info) else { return cookieParser(); } })(), compress: IS_NODE_ENV_PRODUCTION && require('compression')(), // Configures the middleware function used for parsing the HTTP request body, if enabled. bodyParser: (function() { var opts = {}; var fn; opts.onBodyParserError = function (err, req, res, next) {// eslint-disable-line no-unused-vars // Note that we _need_ all four arguments in order for this function // to have special meaning as an error handler (i.e. to Express) var bodyParserFailureErrorMsg = 'Unable to parse HTTP body- error occurred :: ' + util.inspect((err&&err.stack)?err.stack:err, false, null); sails.log.error(bodyParserFailureErrorMsg); if (IS_NODE_ENV_PRODUCTION) { return res.status(400).send(); } return res.status(400).send(bodyParserFailureErrorMsg); }; // Handle original bodyParser config: //////////////////////////////////////////////////////// // If a body parser was configured, use it if (sails.config.http.bodyParser) { fn = sails.config.http.bodyParser; return fn(opts); } else if (sails.config.http.bodyParser === false) { // Allow for explicit disabling of bodyParser using traditional // `express.bodyParser` conf return undefined; } // Default to built-in bodyParser: fn = require('skipper'); return fn(opts); })(), // Add powered-by Sails header poweredBy: function xPoweredBy(req, res, next) { res.header('X-Powered-By', 'Sails <sailsjs.com>'); next(); } }); }; ================================================ FILE: lib/hooks/http/index.js ================================================ /** * Module dependencies. */ var path = require('path'); var util = require('util'); var _ = require('@sailshq/lodash'); var flaverr = require('flaverr'); var toStartServer = require('./start'); var toInitializeHttpHook = require('./initialize'); module.exports = function(sails) { /** * Expose `http` hook definition */ return { defaults: { // Self-awareness: the host the server *thinks it is* // (this is necessary for some production environments-- only set it if you _absolutely_ need it) explicitHost: undefined, // Port to run this app on port: 1337, // SSL cert settings end up here ssl: {}, // Path static files will be served from // Uses `path.resolve()` to accept either: // • an absolute path // • a relative path from the app root (sails.config.appPath) paths: { public: '.tmp/public' }, // New http-only middleware config // (provides default middleware) http: { middleware: { order: [ 'cookieParser', 'session', 'bodyParser', 'compress', 'poweredBy', 'router', 'www', 'favicon', ], // Built-in HTTP middleware functions are injected after the express // app instance has been created (i.e. `app`). See `./initialize.js` // and `./get-configured-http-middleware-fns.js` in this hook for details. }, // HTTP cache configuration // // > Implicit default in production is 365.25 days (in dev: 1 milisecond). // FUTURE: remove implicit production default, and if this is production // and no cache was set, log a warning (in `configure`) cache: process.env.NODE_ENV !== 'production' ? 1 : 31557600000, // Extra options to pass directly into the Express server // when it is instantiated // (or false to disable) // // This is the options object for the `createServer` method, as discussed here: // • http://nodejs.org/docs/v4.0.0/api/https.html#https_class_https_server // • http://nodejs.org/docs/v6.0.0/api/https.html#https_class_https_server // • http://nodejs.org/docs/v7.0.0/api/https.html#https_class_https_server serverOptions: undefined, // Custom express middleware function to use. // (FUTURE: add deprecation message if this is attempted-- instead recommend using an arbitrary middleware) customMiddleware: undefined, // Should be left false unless behind a proxy. // (this is passed in to Express as the "trust proxy" setting) trustProxy: false, }//< .http> },//< / defaults > configure: function() { // If one piece of the ssl config is specified, ensure the other required piece is there if (sails.config.ssl && ( sails.config.ssl.cert && !sails.config.ssl.key ) || (!sails.config.ssl.cert && sails.config.ssl.key)) { throw flaverr({ name: 'userError', code: 'E_INVALID_SSL_CONFIG' }, new Error('Invalid SSL configuration in `sails.config.ssl`! Must include `cert` and `key` properties!')); } // Deprecate `customMiddlware` option. if (sails.config.http.customMiddleware) { sails.log.debug('Warning: use of `customMiddleware` is deprecated in Sails 1.0.'); sails.log.debug('Instead, use an Express 4-compatible middleware (res, res, next) function.'); sails.log.debug('See http://sailsjs.com/docs/upgrading/to-v-1-0#?express-4 for more info.'); sails.log.debug(); } // Path static files will be served from // // Uses `path.resolve()` to accept either: // • an absolute path // • a relative path from the app root (sails.config.appPath) sails.config.paths.public = path.resolve(sails.config.appPath, sails.config.paths.public); // If no _explicit_ middleware order is specified, make sure the implicit default order // will be used. This allows overriding built-in middleware functions (like `www`) // without having to explicitly configure the `sails.config.http.middleware.order` array. sails.config.http.middleware.order = sails.config.http.middleware.order || sails.hooks.http.defaults(sails.config).http.middleware.order; // Note that this (^^) is probably not necessary anymore. // ┌┐ ┌─┐┌─┐┬┌─┬ ┬┌─┐┬─┐┌┬┐┌─┐ ┌─┐┌─┐┌┬┐┌─┐┌─┐┌┬┐┬┌┐ ┬┬ ┬┌┬┐┬ ┬ // ├┴┐├─┤│ ├┴┐│││├─┤├┬┘ ││└─┐ │ │ ││││├─┘├─┤ │ │├┴┐││ │ │ └┬┘ // └─┘┴ ┴└─┘┴ ┴└┴┘┴ ┴┴└──┴┘└─┘ └─┘└─┘┴ ┴┴ ┴ ┴ ┴ ┴└─┘┴┴─┘┴ ┴ ┴ // ┬ ┌┬┐┌─┐┌─┐┬─┐┌─┐┌─┐┌─┐┌┬┐┬┌─┐┌┐┌ ┬ ┬┌─┐┬─┐┌┐┌┬┌┐┌┌─┐┌─┐ // ┌┼─ ││├┤ ├─┘├┬┘├┤ │ ├─┤ │ ││ ││││ │││├─┤├┬┘││││││││ ┬└─┐ // └┘ ─┴┘└─┘┴ ┴└─└─┘└─┘┴ ┴ ┴ ┴└─┘┘└┘ └┴┘┴ ┴┴└─┘└┘┴┘└┘└─┘└─┘ // Backwards compatibility and/or deprecation messages for: // • `sails.config.host` => `sails.config.explicitHost`. // • `sails.config.express` => `sails.config.http`. // • `sails.config.express.loadMiddleware` => `sails.config.http`. // • `sails.config.cache.maxAge` => `sails.config.http.cache`. if (sails.config.host) { sails.log.debug('The `sails.config.host` setting is deprecated in Sails 1.0.'); sails.log.debug('Please use `sails.config.explicitHost` instead.\n'); sails.config.explicitHost = sails.config.host; } if (sails.config.express) { throw flaverr({ name: 'userError', code: 'E_INVALID_HTTP_CONFIG' }, new Error( '\n-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-\n'+ 'The `sails.config.express` setting is no longer available in Sails 1.0.\n'+ 'Please use `sails.config.http.js` instead (available in `config/http.js` in new apps).\n'+ '-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-\n' )); } if (sails.config.http.loadMiddleware) { throw flaverr({ name: 'userError', code: 'E_INVALID_HTTP_CONFIG' }, new Error( '\n-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-\n'+ 'The `sails.config.http.loadMiddleware` setting is no longer available in Sails 1.0.\n'+ 'Please use `sails.config.http.middleware.order` instead.\n'+ '-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-\n' )); } if (sails.config.cache) { throw flaverr({ name: 'userError', code: 'E_INVALID_HTTP_CONFIG' }, new Error( '\n-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-\n'+ 'The `sails.config.cache` setting is no longer available in Sails 1.0.\n'+ 'Please use `sails.config.http.cache` instead.\n'+ '-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-\n' )); } if (sails.config.http.trustProxy === 0 || sails.config.http.trustProxy === '' || sails.config.http.trustProxy === null || _.isNaN(sails.config.http.trustProxy)) { throw flaverr({ name: 'userError', code: 'E_HTTP_BAD_TRUSTPROXY' }, new Error( '\n-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-\n'+ 'The `sails.config.http.trustProxy` property cannot be zero, empty string, null or NaN.\n'+ 'The property is currently set to: `' + util.inspect(sails.config.http.trustProxy) + '`.\n'+ 'To indicate that your app is directly facing the internet, set `trustProxy` to `false`.\n'+ '-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-\n')); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // BACKWARDS COMPATIBILITY: // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - if (sails.config.http.bodyParser) { sails.log.debug('The `sails.config.http.bodyParser` setting is deprecated in Sails 1.0.'); sails.log.debug('Please use `sails.config.http.middleware.bodyParser` instead.'); sails.log.debug('See http://sailsjs.com/docs/concepts/middleware for more details.\n'); if (!sails.config.http.middleware.bodyParser) { sails.config.http.middleware.bodyParser = sails.config.http.bodyParser; } } if (sails.config.http.methodOverride) { sails.log.debug('The `sails.config.http.methodOverride` setting is deprecated in Sails 1.0.'); sails.log.debug('Please use `sails.config.http.middleware.methodOverride` instead.'); sails.log.debug('Also note that in Sails 1.0, the `methodOverride` module is no longer'); sails.log.debug('included by default -- you\'ll need to `npm install method-override --save`'); sails.log.debug('and add `methodOverride` to the `sails.config.http.middleware.order` array.'); sails.log.debug('See http://sailsjs.com/docs/concepts/middleware for more details.\n'); if (!sails.config.http.middleware.methodOverride) { sails.config.http.middleware.methodOverride = sails.config.http.methodOverride; } } if (sails.config.http.cookieParser) { sails.log.debug('The `sails.config.http.cookieParser` setting is deprecated in Sails 1.0.'); sails.log.debug('Please use `sails.config.http.middleware.cookieParser` instead.'); sails.log.debug('See http://sailsjs.com/docs/concepts/middleware for more details.\n'); if (!sails.config.http.middleware.cookieParser) { sails.config.http.middleware.cookieParser = sails.config.http.cookieParser; } } // ┬ ┬┌─┐┬─┐┬┌─┐┬ ┬ ┌┬┐┬┌┬┐┌┬┐┬ ┌─┐┬ ┬┌─┐┬─┐┌─┐ // └┐┌┘├┤ ├┬┘│├┤ └┬┘ ││││ ││ │││ ├┤ │││├─┤├┬┘├┤ // └┘ └─┘┴└─┴└ ┴ ┴ ┴┴─┴┘─┴┘┴─┘└─┘└┴┘┴ ┴┴└─└─┘ // Make sure that middleware in the order exists, and that every // custom middleware is present in the order. // Loop through all of the middleware in `sails.config.http.middleware`, and verify that it's // in the order (skipping the `order` key itself). _.each(_.without(_.keys(sails.config.http.middleware), 'order'), function (middlewareName) { // If the custom middleware isn't in the middleware order, bail. // Make an exception for 404, 500 and startRequest timer, which we'll handle more // gently when initializing the hook. if (!_.contains(sails.config.http.middleware.order.concat(['404', '500', 'startRequestTimer']), middlewareName)) { throw flaverr({ name: 'userError', code: 'E_INVALID_HTTP_CONFIG' }, new Error( '\n-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-\n'+ 'Detected a custom middleware `' + middlewareName + '` that does not appear in the\n'+ 'middleware order. Please add `' + middlewareName + '` to `sails.config.http.middleware.order`.\n'+ 'See http://sailsjs.com/docs/concepts/middleware for more info.\n'+ '-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-\n' )); } }); // Now loop through all of the middleware names in `sails.config.http.middleware.order` (ignoring // the built-in ones) and verify that there's a matching custom middleware. // Make an exception for the middleware that was removed from the order in Sails 1.0, which we'll // handle more gently when initializing the hook. _.each(_.difference(sails.config.http.middleware.order, sails.hooks.http.defaults.http.middleware.order.concat(['404', '500', 'startRequestTimer', 'handleBodyParserError', 'methodOverride', '$custom'])), function(middlewareName) { if (!_.isFunction(sails.config.http.middleware[middlewareName])) { throw flaverr({ name: 'userError', code: 'E_INVALID_HTTP_CONFIG' }, new Error( '\n-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-\n'+ 'Detected an entry for `' + middlewareName + '` in `sails.config.http.middleware.order`,\n'+ 'but `sails.config.http.middleware[\'' + middlewareName + '\']` is undefined or not a function.\n'+ 'Please provide a custom `req, res, next` middleware function for `' + middlewareName + '`,\n'+ 'or remove it from the order. See http://sailsjs.com/docs/concepts/middleware for more info.\n'+ '-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-\n' )); } }); }, /** * Initialize is fired first thing when the hook is loaded * but after waiting for user config (if applicable). */ initialize: toInitializeHttpHook(sails), /** * `handleLift` is fired when sails is ready for HTTP requests to * start coming in. * * @param {Function} done */ handleLift: function(done){ // In order for `sails.config` to be correct, this needs to happen in here. var startServer = toStartServer(sails); // Now that Sails is ready, start listening for requests on // the express server. startServer(done); } }; }; ================================================ FILE: lib/hooks/http/initialize.js ================================================ /** * Module dependencies. */ var _ = require('@sailshq/lodash'); var getConfiguredHttpMiddlewareFns = require('./get-configured-http-middleware-fns'); module.exports = function(sails) { /** * initialize() * * Configure the encapsulated Express server that will be used to serve actual HTTP requests */ return function initialize(cb) { // Before proceeding, wait for the session hook-- // or if it is disabled, then go ahead and proceed // (but change the middleware order config so we don't // attempt to handle sessions). (function _waitForSessionHookIfApplicable(next){ // If the session hook is available... if (sails.hooks.session) { // Then wait until after session hook has initialized // so that the proper session config is available for use // in the built-in "session" middleware. sails.after('hook:session:loaded', function () { return next(); }); } // Otherwise, the session hook is NOT available. else { // Then, if it present, rip out "session" from the configured // middleware order so we don't try to use the built-in session // middleware. _.pull(sails.config.http.middleware.order, 'session'); return next(); } })(function _afterLoadingSessionHookIfApplicable(err) { if (err) { return cb(err); } try { // Required to be here due to dynamic NODE_ENV settings via command line args // (i.e. if we `require` this above w/ everything else, the NODE_ENV might not be set properly yet) var express = require('express'); // Create express app instance. var expressApp = express(); // Create a new router object to handle routes bound in Sails apps. // We do this because Express doesn't provide a public API to return // its built-in router (expressApp._router), and we need direct access // to the router object in order to do `unbind` and `reset`. // // Note that we don't add this router to the express app until right // before its time to add any "post-router" middleware (i.e. after // the "ready" event is received). var internalExpressRouter = express.Router(); // Expose express app as `sails.hooks.http.app` for use in other files // in this hook, and in other core hooks. sails.hooks.http.app = expressApp; // Disable the default powered-by header (required by Express 3.x). expressApp.disable('x-powered-by'); // Determine whether or not to create an HTTPS server var isUsingSSL = (sails.config.ssl === true) || (sails.config.ssl.key && sails.config.ssl.cert) || sails.config.ssl.pfx; // Merge SSL into server options var serverOptions = sails.config.http.serverOptions || {}; _.extend(serverOptions, sails.config.ssl); // Lodash 3's _.merge attempts to transform buffers into arrays; // so if we detect an array, then transform it back into a buffer. _.each(['key', 'cert', 'pfx'], function _eachSSLOption(sslOption) { if (_.isArray(serverOptions[sslOption])) { serverOptions[sslOption] = Buffer.from(serverOptions[sslOption]); } }); // ^^^ The following is probably not relevant anymore, because `_.merge()` // is not being used above. Leaving for compatibility reasons (just to be safe). // Get the appropriate server creation method for the protocol var createServer = isUsingSSL ? require('https').createServer : require('http').createServer; // Use serverOptions if they were specified // Manually create http server using Express app instance if(process.version.match(/^v(\d+\.\d+)/)[1] < 10.12){ if (sails.config.http.serverOptions || isUsingSSL) { sails.hooks.http.server = createServer(serverOptions, expressApp); } else { sails.hooks.http.server = createServer(expressApp); } } else { sails.hooks.http.server = createServer(serverOptions, expressApp); } // Keep track of all openTcpConnections that come in, // so we can destroy them later if we want to. var openTcpConnections = {}; // Listen for `connection` events on the raw HTTP server. sails.hooks.http.server.on('connection', function _onNewTCPConnection(tcpConnection) { var key = tcpConnection.remoteAddress + ':' + tcpConnection.remotePort; openTcpConnections[key] = tcpConnection; tcpConnection.on('close', function() { delete openTcpConnections[key]; }); }); // Create a `destroy` method we can use to do a hard shutdown of the server. sails.hooks.http.destroy = function(done) { sails.log.verbose('Destroying http server...'); sails.hooks.http.server.close(done); // FUTURE: consider moving this loop ABOVE `sails.hooks.http.server.close(done)` // for clarity (since at this point we've passed control via `done`) for (var key in openTcpConnections) { openTcpConnections[key].destroy(); } };//</define `sails.hooks.http.destroy()`> // Configure views if hook enabled if (sails.hooks.views) { // FUTURE: explore handling this differently to avoid potential // timing issues with view engine configuration sails.after('hook:views:loaded', function() { var View = require('./view'); // Use View subclass to allow case-insensitive view lookups expressApp.set('view', View); // Set up location of server-side views and their engine expressApp.set('views', sails.config.paths.views); // Teach Express how to render templates w/ our configured view extension expressApp.engine(sails.config.views.extension, sails.hooks.views._renderFn); // Set default view engine sails.log.silly('Setting default Express view engine to ' + sails.config.views.extension + '...'); expressApp.set('view engine', sails.config.views.extension); });//</ after hook:views:loaded > }//>- // In non-production environments, format `res.json()` output nicely. // > https://expressjs.com/en/4x/api.html#app.set if (process.env.NODE_ENV !== 'production') { expressApp.set('json spaces', 2); } // Set Express "trust proxy" if appropriate. // > https://expressjs.com/en/guide/behind-proxies.html if (sails.config.http.trustProxy) { expressApp.set('trust proxy', sails.config.http.trustProxy); } // Whenever Sails binds a route, bind it to the internal Express router. sails.on('router:bind', function(route) { // Clone the route so that if a route handler messes with the options, the changes // don't get persisted and used in subsequent requests. route = _.cloneDeep(route); internalExpressRouter[route.verb || 'all'](route.path || '/*', route.target); }); // Whenever Sails unbinds a route, remove it from the internal Express router. sails.on('router:unbind', function(routeToRemove) { // Remove any route which matches the path and verb of the argument _.remove(internalExpressRouter.stack, function(layer) { return (layer.route.path === routeToRemove.path && layer.route.methods[routeToRemove.verb] === true); }); }); // Whenever Sails resets its router, clear out the internal Express router. sails.on('router:reset', function() { internalExpressRouter.stack = []; }); // Now expressApp.use() an initial piece of middleware to bind // _core, mandatory properties_ to the incoming `req`. // This middleware cannot be disabled in userland configuration-- // and that's done on purpose. expressApp.use(function _exposeSailsOnReq (req, res, next){ // Expose req._sails on incoming HTTP request instances. // // This is also handled separately for virtual requests in `lib/router/`: // (see https://github.com/balderdashy/sails/pull/3599#issuecomment-195665040) req._sails = sails; // Wrap `req.param()` in a shim that normalizes the behavior of `req.param('length')`. // (see https://github.com/balderdashy/sails/issues/3738#issue-156095626) var origReqParam = req.param; req.param = function getValForParam (name){ if (name === 'length') { // If `req.params.length` is a string, instead of a number, then we know this request // must have matched a route address like `/foo/bar/:length/baz`, so in that case, we'll // allow `req.param('length')` to return the runtime value of `length` as a string. if (_.isString(req.params.length)) { return req.params.length; } else if (!_.isArray(req.body) && _.isObject(req.body) && !_.isUndefined(req.body.length) && !_.isNull(req.body.length)) { // > In future versions of Sails, this shim will likely be modified to allow the `null` literal to be received // > as a value for a body parameter and accessed in `req.param()`. // > (This is because `null` and `undefined` are distinct and lossless when serializing and deserializing to // > and from JSON-- so it's really specifically for standard JSON response bodies.) // > // > However, this is a departure from the behavior of Express, and a breaking change- so it will // > not happen until the release of Sails v1 (or possibly in a pre v1.0 minor version.) return req.body.length; } else if (_.isObject(req.query) && !_.isUndefined(req.query.length) && !_.isNull(req.query.length)) { return req.query.length; } else { return undefined; } } return origReqParam.apply(req, Array.prototype.slice.call(arguments)); }; return next(); }); // If there's a `handleBodyParserError` middleware in the order, and there isn't // a custom definition for it (i.e. it's trying to use the default) log a // deprecation warning. if (_.contains(sails.config.http.middleware.order, 'handleBodyParserError') && !_.isFunction(sails.config.http.middleware.handleBodyParserError)) { sails.log.debug('The `handleBodyParserError` middleware has been removed in Sails 1.0.'); sails.log.debug('To avoid this message, remove `handleBodyParserError` from '); sails.log.debug('the `order` array in `config/http.js`.'); sails.log.debug('See http://sailsjs.com/upgrading for more info.\n'); } // If there's a `methodOverride` middleware in the order, and there isn't // a custom definition for it (i.e. it's trying to use the default) log a // deprecation warning. if (_.contains(sails.config.http.middleware.order, 'methodOverride') && !_.isFunction(sails.config.http.middleware.methodOverride)) { sails.log.debug('The `methodOverride` middleware has been removed in Sails 1.0.'); sails.log.debug('To avoid this message, remove `methodOverride` from '); sails.log.debug('the `order` array in `config/http.js`.'); sails.log.debug('See http://sailsjs.com/upgrading for more info.\n'); } // Ignore explicit declarations of `startRequestTimer`, `404` and `500` middleware. var removedHttpMiddleware = _.remove(sails.config.http.middleware.order, function(middleware) { return _.contains(['handleBodyParserError', 'startRequestTimer', '404', '500'], middleware); }); // Warn about an explicit `startRequestTimer` in the order, or a custom middleware implementation of it. if (_.contains(removedHttpMiddleware, 'startRequestTimer') || sails.config.http.middleware.startRequestTimer) { sails.log.debug('The `startRequestTimer` middleware is added to your app automatically in Sails 1.0.'); if (sails.config.http.middleware.startRequestTimer) { sails.log.debug('(ignoring custom implementation in `sails.config.http.middleware.startRequestTimer)'); } else { sails.log.debug('(ignoring entry in the `sails.config.http.middleware.order` list)'); } sails.log.debug('See http://sailsjs.com/documentation/reference/request-req/req-start-time for more info.\n'); } // Warn about an explicit `404` in the order, or a custom middleware implementation of it. if (_.contains(removedHttpMiddleware, '404') || sails.config.http.middleware['404']) { sails.log.debug('The `404` middleware is added to your app automatically in Sails 1.0.'); if (sails.config.http.middleware['404']) { sails.log.debug('(ignoring custom implementation in `sails.config.http.middleware[\'404\']`)'); } else { sails.log.debug('(ignoring entry in the `sails.config.http.middleware.order` list)'); } sails.log.debug('If you wish to customize the 404 functionality for your app, you can'); sails.log.debug('do so by creating a custom `notFound` response as `api/responses/notFound.js`.'); sails.log.debug('See http://sailsjs.com/documentation/concepts/custom-responses for more info.\n'); } // Warn about an explicit `500` in the order. if (_.contains(removedHttpMiddleware, '500') || sails.config.http.middleware['500']) { sails.log.debug('The `500` middleware is added to your app automatically in Sails 1.0.'); if (sails.config.http.middleware['500']) { sails.log.debug('(ignoring custom implementation in `sails.config.http.middleware[\'500\']`)'); } else { sails.log.debug('(ignoring entry in the `sails.config.http.middleware.order` list)'); } sails.log.debug('If you wish to customize the 500 functionality for your app, you can'); sails.log.debug('do so by creating a custom `serverError` response as `api/responses/serverError.js`.'); sails.log.debug('See http://sailsjs.com/documentation/concepts/custom-responses for more info.\n'); } // Then build a dictionary of configured middleware functions, including // built-in middleware as well as any middleware provided in // `sails.config.http.middleware`. var configuredHttpMiddlewareFns = getConfiguredHttpMiddlewareFns(expressApp, sails); // Add in the middleware to record the request start time. expressApp.use(function startRequestTimer(req, res, next) { req._startTime = new Date(); next(); }); // Split the middleware order into "pre-router" and "post-router" middleware. // The internal "startRequestTimer" always comes first. var preRouterMiddleware = []; var postRouterMiddleware = null; _.each(sails.config.http.middleware.order, function(middlewareKey) { if (middlewareKey === 'router') { postRouterMiddleware = []; } else if ( _.isArray(postRouterMiddleware) ) { postRouterMiddleware.push(middlewareKey); } else { preRouterMiddleware.push(middlewareKey); } }); // If a custom `loadMiddleware` function was configured, then call it to "use" // the configured middleware (instead of doing it automatically with the more // modern `sails.config.http.middleware.order` configuration). // // This is primarily for backwards compatibility for the undocumented // `express.loadMiddleware` config that is still in use in legacy apps // from the 2013-early 2014 time frame. // // It is no longer relevant in most cases thanks to `sails.config.http.middleware`, // and may be removed in an upcoming release. if (sails.config.http.loadMiddleware) { sails.config.http.loadMiddleware(expressApp, configuredHttpMiddlewareFns, sails); } // Otherwise (i.e. the normal case) we `.use()` each of the configured // middleware functions in the configured order (`sails.config.http.middleware.order`). else { _.each(preRouterMiddleware, function (middlewareKey) { // `$custom` is a special entry in the middleware order config that exists // purely for compatibility. When procesing `$custom`, we check to see if // `sails.config.http.customMiddleware`, was provided and if so, call it // with the express app instance as an argument (rather than calling // `sails.config.http.middleware.$custom`). // If `customMiddleware` is not being used, we just ignore `$custom` altogether. if (middlewareKey === '$custom') { if (sails.config.http.customMiddleware) { // Allows for injecting a custom function to attach middleware. // (This is here for compatibility, and for situations where the raw Express // app instance is necessary for configuring middleware). sails.config.http.customMiddleware(expressApp); } // Either way, bail at this point (we don't want to do anything further with $custom) return; } // Look up the referenced middleware function. var referencedMwr = configuredHttpMiddlewareFns[middlewareKey]; // If a middleware fn by this name is not configured (i.e. `undefined`), // then skip this entry & write a verbose log message. if (_.isUndefined(referencedMwr)) { sails.log.verbose('An entry (`%s`) in `sails.config.http.middleware.order` references an unrecognized middleware function-- that is, it was not provided as a key in the `sails.config.http.middleware` dictionary. Skipping...', middlewareKey); return; } // On the other hand, if the referenced middleware appears to be disabled // _on purpose_, or because _it is not compatible_, then just skip it and // don't log anything. (i.e. it is `null` or `false`) if (!referencedMwr) { return; } // Otherwise, we're good to go, so go ahead and use the referenced // middleware function. expressApp.use(referencedMwr); });//</each item in `sails.config.http.middleware.order`> } // www, favicon, 404 and 500 middleware should be attached at the very end. // In previous Sails versions (that used Express <4), the router was added // as part of the middleware stack in sails.config.http.middleware.order, // so we could just put these 4 middleware after `router` in that list. // In Express 4, the router is built in, so we have to wait until the // server is fully initialized and then add the post-router middleware after. sails.once('ready', function addPostRouterMiddleware() { expressApp.use(internalExpressRouter); _.each(postRouterMiddleware, function (middlewareKey) { // Look up the referenced middleware function. var referencedMwr = configuredHttpMiddlewareFns[middlewareKey]; // If a middleware fn by this name is not configured (i.e. `undefined`), // then skip this entry & write a verbose log message. if (_.isUndefined(referencedMwr)) { sails.log.verbose('An entry (`%s`) in `sails.config.http.middleware.order` references an unrecognized middleware function-- that is, it was not provided as a key in the `sails.config.http.middleware` dictionary. Skipping...', middlewareKey); return; } // On the other hand, if the referenced middleware appears to be disabled // _on purpose_, or because _it is not compatible_, then just skip it and // don't log anything. (i.e. it is `null` or `false`) if (!referencedMwr) { return; } // Otherwise, we're good to go, so go ahead and use the referenced // middleware function. expressApp.use(referencedMwr); }); // Add the default 404 middleware. expressApp.use(function handleUnmatchedRequest(req, res) { // Explicitly ignore error arg to avoid inadvertently // turning this into an error handler sails.emit('router:request:404', req, res); }); // Add the default 500 middleware. expressApp.use(function handleError(err, req, res, next) {// eslint-disable-line no-unused-vars // Note that we _need_ all four arguments in order for this function // to have special meaning as an error handler (i.e. to Express) sails.emit('router:request:500', err, req, res); }); }); // All done! return cb(); } catch (e) { return cb(e); } // ^ for improving the readability of any bugs in the above code, // or for unhandled errors from our dependencies, or for unexpected // configuration. });//</_afterLoadingSessionHookIfApplicable> };//</initialize()> }; ================================================ FILE: lib/hooks/http/start.js ================================================ /** * Module dependencies. */ var async = require('async'); var flaverr = require('flaverr'); module.exports = function (sails) { return function startServer (cb) { // Used to warn about possible issues if starting the server is taking a long time var liftAbortTimer; var liftTimeout = sails.config.liftTimeout || 4000; // TODO: pull this defaulting into `defaults` // and also ensure this config is properly documented. async.auto({ // Start Express server start: function (next) { var explicitHost = sails.config.explicitHost; // If host is explicitly declared, include it in express's listen() call if (explicitHost) { sails.log.verbose('Restricting access to explicit host: '+explicitHost); sails.hooks.http.server.listen(sails.config.port, explicitHost, next); } else { // Listen for error events that may be emitted as the server attempts to start sails.hooks.http.server.on('error', failedToStart); sails.hooks.http.server.listen(sails.config.port, function(err) { // Remove the error listener so future error events emitted by the server // don't get handled by the "failedToStart" function below. sails.hooks.http.server.removeListener('error', failedToStart); return next(err); }); } // Start timer in case this takes suspiciously long... liftAbortTimer = setTimeout(failedToStart, liftTimeout); // If the server fails to start because of an error, or if it's just taking // too long, show some troubleshooting notes and bail out. function failedToStart(err) { // If this was called because of an actual error, clear the timeout // so failedToStart doesn't get called again. if (err) { clearTimeout(liftAbortTimer); } // If sails is exiting already, don't worry about the timer going off. if (sails._exiting) {return;} // Figure out if this user is on Windows var isWin = !!process.platform.match(/^win/); // If server isn't starting, provide general troubleshooting information, // sharpened with a few simple heuristics: console.log(''); if (err) { sails.log.error('Server failed to start.'); if (err.code) { sails.log.error('(received error: ' + err.code + ')'); } } else { sails.log.error('Server is taking a while to start up (it\'s been ' + (liftTimeout / 1000) + ' seconds).'); } sails.log.error(); sails.log.error('Troubleshooting tips:'); sails.log.error(); // 0. Just a slow Grunt task if (sails.hooks.grunt && ! (err && err.code === 'EADDRINUSE')) { if (process.env.NODE_ENV === 'production') { sails.log.error( ' -> Do you have a slow Grunt task? You are running in production mode where, by default, tasks are configured to minify the JavaScript and CSS/LESS files in your assets/ directory. Sometimes, these processes can be slow, particularly if you have lots of these types of files.' ); } else { sails.log.error( ' -> Do you have a slow Grunt task, or lots of assets?' ); } sails.log.error(); } // 1. Unauthorized if (sails.config.port < 1024) { sails.log.error( ' -> Do you have permission to use port ' + sails.config.port + ' on this system?', // Don't mention `sudo` to Windows users-- I hear you guys get touchy about that sort of thing :) (isWin) ? '' : '(you might try `sudo`)' ); sails.log.error(); } // 2. Invalid or unauthorized explicitHost configuration. if (explicitHost) { sails.log.error( ' -> You might remove your explicit host configuration and try lifting again (you specified', '`'+explicitHost+'`', '.)'); sails.log.error(); } // 3. Something else is running on this port sails.log.error( ' -> Is something else already running on port', sails.config.port, (explicitHost ? (' with hostname ' + explicitHost) : '') + '?' ); sails.log.error(); // 4. invalid explicitHost if (!explicitHost) { sails.log.error( ' -> Are you deploying on a platform that requires an explicit hostname,', 'like OpenShift?'); sails.log.error( ' (Try setting the `explicitHost` config to the hostname where the server will be accessible.)' ); sails.log.error( ' (e.g. `mydomain.com` or `183.24.244.42`)' ); } console.log(''); // Lower Sails to do any necessary cleanup sails.lower(function(){ // Exit with a non-zero value to indicate an error process.exit(1); // TODO: For a more graceful shutdown, we should instead consider: // return next(new Error('blah blah')); }); }//</failedToStart> }, verify: ['start', function (results, next) { var explicitHost = sails.config.explicitHost; // Check for port conflicts // Ignore this check if explicit host is set, since other more complicated things might be going on. if( !explicitHost && !sails.hooks.http.server.address() ) { var portBusyErrorMsg = ''; portBusyErrorMsg += 'Trying to start server on port ' + sails.config.port + ' but can\'t...'; portBusyErrorMsg += 'Something else is probably running on that port!' + '\n'; portBusyErrorMsg += 'Please disable the other server, or choose a different port and try again.'; sails.log.error(portBusyErrorMsg); throw flaverr({ name: 'userError', code: 'E_PORT_BUSY' }, new Error(portBusyErrorMsg)); // TODO: For a more graceful shutdown, we should instead consider: // return next(new Error(portBusyErrorMsg)); } next(); }] }, function expressListening (err) { clearTimeout(liftAbortTimer); if (err) { return cb(err); } // Announce that express is now listening on a port sails.emit('hook:http:listening'); return cb(); }); }; }; ================================================ FILE: lib/hooks/http/view.js ================================================ /** * Module dependencies */ var util = require('util'); var path = require('path'); var glob = require('glob'); var ExpressView = require('express/lib/view'); var expressUtils = require('express/lib/utils'); var globPath = function(viewPath) { // return glob.sync(path, { // nocase: true // }); return glob.sync(path.basename(viewPath), { cwd: path.dirname(viewPath), nocase: true }); }; /** * `exists()` * * Helper function to check existence of the specified path amongst the app's views. * @param {String} viewPath * @return {Boolean} */ var exists = function(viewPath) { return globPath(viewPath).length > 0; }; /** * @constructs {SailsView} */ function SailsView (name, options) { ExpressView.call(this, name, options); } util.inherits(SailsView, ExpressView); SailsView.prototype.lookup = function(viewPath) { var viewExt = this.ext; var rootPath = this.root; // <path>.<engine> if (!expressUtils.isAbsolute(viewPath)) { viewPath = path.join(rootPath, viewPath); } if (exists(viewPath)) { return viewPath; //return globPath(viewPath)[0]; } // <path>/index.<engine> viewPath = path.join(path.dirname(viewPath), path.basename(viewPath, viewExt), 'index' + viewExt); if (exists(viewPath)) { return viewPath; //return globPath(path)[0]; } }; module.exports = SailsView; ================================================ FILE: lib/hooks/i18n/index.js ================================================ module.exports = function(sails) { /** * Module dependencies. */ var util = require('util'); var path = require('path'); var _ = require('@sailshq/lodash'); var i18nFactory = require('i18n-2'); // The file extension for locales files (e.g. in config/locales/) var I18N_LOCALES_FILE_EXTENSION = '.json'; // Declare a var to hold the hook's singleton i18n instance. // (if we're unable to initialize this hook properly and we skip it, // then this will still be set to `undefined`) var i18n; // Hold the resolved absolute path to the configured locales dir. // (if we're unable to initialize this hook properly and we skip it, // then this will still be set to `undefined`) var resolvedLocalesDirectory; /** * Expose hook definition */ return { defaults: { // i18n i18n: { locales: [], // ^^this is just the implicit default (is overridden by boilerplate settings in `config/i18n.js`) defaultLocale: 'en', localesDirectory: 'config/locales/' } }, configure: function() { if (!_.isArray(sails.config.i18n.locales)) { throw new Error('`sails.config.i18n.locales` must be an array of strings like [\'en\', \'es\'], but instead got: '+util.inspect(sails.config.i18n.locales, {depth:null})+''); } if (!sails.config.i18n.localesDirectory || !_.isString(sails.config.i18n.localesDirectory)) { throw new Error('`sails.config.i18n.localesDirectory` must be a string like "config/locales/". But, instead got: '+util.inspect(sails.config.i18n.localesDirectory, {depth:null})+''); } // If we have a default locale config, and it exists in the list of configured locales, // move it to the top of the list. This is a workaround for https://github.com/jeresig/i18n-node-2/issues/90 if (sails.config.i18n.defaultLocale && sails.config.i18n.locales && _.contains(sails.config.i18n.locales, sails.config.i18n.defaultLocale)) { sails.config.i18n.locales = [sails.config.i18n.defaultLocale].concat(_.without(sails.config.i18n.locales, sails.config.i18n.defaultLocale)); } }, initialize: function(cb) { var self = this; // If the array of locales is empty, then bail out now. // (There's nothing for us to do in this hook.) if (sails.config.i18n.locales.length === 0) { sails.log.verbose('Skipping i18n hook because configured locales (`sails.config.i18n.locales`) is an empty array.'); // Delete sails.hooks.i18n so that checks like `if (sails.hooks.i18n)` come back falsey. delete sails.hooks.i18n; // Add __ and i18n functions that just pass through the string (and return a warning). sails.__ = sails.i18n = function(str) { sails.log.warn('i18n hook has disabled itself due to misconfiguration -- returning input string as-is. Set `sails.config.i18n.locales` to activate i18n.'); return str; }; // Add passthru __ and i18n to res.locals. sails.on('router:before', function () { sails.router.bind('all /*', function addLocalizationMethod (req, res, next) { res.locals.__ = res.locals.i18n = sails.__; return next(); }); });//</sails.on> return cb(); }//-• // Determine the abs path to the locales dir, resolving relative from the appPath. // (supports absolute or relative path) resolvedLocalesDirectory = path.resolve(sails.config.appPath, sails.config.i18n.localesDirectory); // Override logger while initializing i18n-2, since it uses console function directly. // We'll just buffer any messages and replay them when i18n-2 is done (or fails) initializing. var logs = []; var warns = []; var errors = []; var origLog = console.log; var origWarn = console.warn; var origError = console.error; console.log = function() {logs.push(Array.prototype.slice.call(arguments));}; console.warn = function() {warns.push(Array.prototype.slice.call(arguments));}; console.error = function() {errors.push(Array.prototype.slice.call(arguments));}; // Attempt to initialize i18n. This will fail if there's no `config/locales` directory. try { i18n = new i18nFactory({ locales: sails.config.i18n.locales, defaultLocale: sails.config.i18n.defaultLocale, directory: resolvedLocalesDirectory, extension: I18N_LOCALES_FILE_EXTENSION }); // Add all of the i18n prototype methods into this hook. _.each(i18nFactory.prototype, function(val, key) { if (_.isFunction(val)) { self[key] = i18n[key].bind(i18n); } }); // Expose global access to locale strings sails.__ = self.__; sails.i18n = self.__; } catch (e) { sails.log.error('Failed to initialize i18n hook. (Tip: Does the ' + resolvedLocalesDirectory + ' directory exist?)'); sails.log.error(e.stack); return cb(e); } // Restore the original console logger functions, and then // replay any logs that were generated while trying to init i18n-2. console.log = origLog; console.warn = origWarn; console.error = origError; _.each(logs, function(log) {sails.log.verbose.apply(this, log);}); _.each(warns, function(warn) {sails.log.warn.apply(this, warn);}); _.each(errors, function(error) {sails.log.error.apply(this, error);}); // When Sails is ready to bind "before" shadow routes, bind a shadow route that // adds the localization methods and locals. sails.on('router:before', function (){ sails.router.bind('all /*', function addLocalizationMethod (req, res, next) { return sails.hooks.i18n.expressMiddleware(req, res, next); }); });//</sails.on> // Finally, call the callback to indicate that the hook is done initializing. return cb(); }, // Express middleware that adds translation capabilities (e.g. the __ function) // to the `res` object. Useful mainly for doing internationalization in views. expressMiddleware: function (req, res, next) { // If we don't have res.locals, we don't have anything to mix i18n options onto. if (!res.locals) { return next(new Error('Provided `res` must contain `res.locals`.')); } // If we weren't able to initialize the singleton i18n module, then we // should never have made it here. if (!i18n) { return next(new Error('Consistency violation: This should never happen -- sails.hooks.i18n.expressMiddleware should never be available if i18n could not be initialized.')); } // Try to create a new i18n instance. This is necessary because // locale is set on a per-instance basis, and the request header // may change the locale for a given instance (but we wouldn't // want it to change for the instance connected to `sails.__()` ) try { req.i18n = new i18nFactory({ locales: sails.config.i18n.locales, defaultLocale: sails.config.i18n.defaultLocale, directory: resolvedLocalesDirectory, extension: I18N_LOCALES_FILE_EXTENSION, request: req }); // Mix translation capabilities into res.locals. i18nFactory.registerMethods(res.locals, req); // Add `setLocale()` method for convenience. // (This is documented and fully supported as of Sails v1.0 and beyond.) req.setLocale = req.i18n.setLocale.bind(req.i18n); // For backwards compatibility: // • Add `i18n` method as alias to `__` // • Add `getLocale()` method to `req` res.locals.i18n = res.locals.__; req.getLocale = req.i18n.getLocale.bind(req.i18n); } catch (e) { // Seeing as we have a valid i18n singleton already, the // initialization failing now seems more serious, but we // still don't want to crash because of it. // We should at least log the error though. sails.log.error('Error attaching i18n to response:'); sails.log.error(e); } // Continue processing the request. return next(); }, }; }; ================================================ FILE: lib/hooks/index.js ================================================ /** * Module dependencies. */ var _ = require('@sailshq/lodash'); var flaverr = require('flaverr'); var async = require('async'); var STRIP_COMMENTS_RX = /(\/\/.*$)|(\/\*[\s\S]*?\*\/)|(\s*=[^,\)]*(('(?:\\'|[^'\r\n])*')|("(?:\\"|[^"\r\n])*"))|(\s*=[^,\)]*))/mg; module.exports = function(sails) { /** * Expose hook constructor * * @api private */ return function Hook(definition) { // Flags to indicate whether or not this hook's `initialize` function is asynchronous (i.e. declared with `async`) // and whether or not it has any parameters. var hasAsyncInit; var initSeemsToExpectParameters; // A few sanity checks to make sure te provided definition does not contain any reserved properties. if (!_.isObject(definition)) { // This particular behavior can be made a bit less genteel in future versions (it is currently // forgiving for backwards compatibility) definition = definition || {}; } if (_.isFunction(definition.config)) { throw flaverr({ name: 'userError', code: 'E_INVALID_HOOK_CONFIG' }, new Error('Error defining hook: `config` is a reserved property and cannot be used as a custom hook method.')); } if (_.isFunction(definition.middleware)) { throw flaverr({ name: 'userError', code: 'E_INVALID_HOOK_CONFIG' }, new Error('Error defining hook: `middleware` is a reserved property and cannot be used as a custom hook method.')); } /** * Load the hook asynchronously * * @api private */ this.load = function(cb) { var self = this; // TODO: refactor this: (no need for an inline function declaration) var routeCallbacks = function(routes) { _.each(routes, function(middleware, route) { middleware._middlewareType = self.identity.toUpperCase() + ' HOOK' + (middleware.name ? (': ' + middleware.name) : ''); sails.router.bind(route, middleware); }); };//ƒ // Determine if this hook should load based on Sails environment & hook config if (this.config.envs && this.config.envs.length > 0 && this.config.envs.indexOf(sails.config.environment) === -1) { return cb(); } // Convenience config to bind routes before any of the static app routes sails.on('router:before', function() { routeCallbacks(self.routes.before); }); // Convenience config to bind routes after the static app routes sails.on('router:after', function() { routeCallbacks(self.routes.after); }); // Run loadModules method if moduleloader is loaded async.auto({ modules: function(cb) { if (sails.config.hooks.moduleloader) { return self.loadModules(cb); } return cb(); } }, function(err) { if (err) { return cb(err); } // console.log(self.identity, self.initialize.toString()); try { var seemsToExpectCallback = true; if (sails.config.implementationSniffingTactic === 'analogOrClassical') { seemsToExpectCallback = initSeemsToExpectParameters; // (TODO: also locate and update relevant error messages) } if (hasAsyncInit) { var promise; if (seemsToExpectCallback) { promise = self.initialize(cb); } else { promise = self.initialize(function(unusedErr){ cb(new Error('Unexpected attempt to invoke callback. Since this "initialize" function does not appear to expect a callback parameter, this stub callback was provided instead. Please either explicitly list the callback parameter among the arguments or change this code to no longer use a callback.')); }) .then(function(){ cb(); }); }//fi promise.catch(function(e) { cb(e); // (Note that we don't do `return proceed(e)` here. That's on purpose-- // to avoid sending the wrong idea to you, dear reader) }); } else { if (seemsToExpectCallback) { self.initialize(cb); } else { self.initialize(function(unusedErr){ cb(new Error('Unexpected attempt to invoke callback. Since this "initialize" function does not appear to expect a callback parameter, this stub callback was provided instead. Please either explicitly list the callback parameter among the arguments or change this code to no longer use a callback.')); }); return cb(); } } } catch (e) { return cb(e); } }); }; /** * `defaults` * * Default configuration for this hook. * * Hooks may override this function, or use a dictionary instead. * * @type {Function|Dictionary} * @returns {Dictionary} [default configuration for this hook to be merged into sails.config] */ this.defaults = function() { return {}; }; /** * `configure` * * If this hook provides this function, the provided implementation should * normalize and validate configuration related to this hook. That config is * already in `sails.config` at the time this function is called. Any modifications * should be made in place on `sails.config` * * Hooks may override this function. * * @type {Function} */ this.configure = function() { }; /** * `loadModules` * * Load any modules as a dictionary and pass the loaded modules to the callback when finished. * * Hooks may override this function (This runs before `initialize()`!) * * @type {Function} * @async */ this.loadModules = function(cb) { return cb(); }; /** * `initialize` * * If provided, this implementation should prepare the hook, then trigger the callback. * * Hooks may override this function. * * @type {Function} * @async */ this.initialize = function(cb) { return cb(); }; // Ensure that the hook definition has valid properties _normalize(this); definition = _normalize(definition); // Merge default definition with overrides in the definition passed in _.extend(definition.config, this.config, definition.config); _.extend(definition.middleware, this.middleware, definition.middleware); _.extend(definition.routes.before, this.routes.before, definition.routes.before); _.extend(definition.routes.after, this.routes.after, definition.routes.after); _.extend(this, definition); // Set a flag if this hook has an async `initialize` function, and // whether or not that function seems to be expecting any parameters. hasAsyncInit = this.initialize.constructor.name === 'AsyncFunction'; initSeemsToExpectParameters = (function(fn){ var fnStr = fn.toString().replace(STRIP_COMMENTS_RX, ''); var parametersAsString = fnStr.slice(fnStr.indexOf('(')+1, fnStr.indexOf(')')); // console.log('::',parametersAsString, parametersAsString.replace(/\s*/g,'').length); return parametersAsString.replace(/\s*/g,'').length !== 0; })(this.initialize);//† // Bind context of new methods from definition _.bindAll(this); /** * Ensure that a hook definition has the required properties. * * @returns {Dictionary} [coerced hook definition] * @api private */ function _normalize(def) { def = def || {}; // Default hook config def.config = def.config || {}; // list of environments to run in, if empty defaults to all def.config.envs = def.config.envs || []; def.middleware = def.middleware || {}; // Default hook routes def.routes = def.routes || {}; def.routes.before = def.routes.before || {}; def.routes.after = def.routes.after || {}; return def; } }; }; ================================================ FILE: lib/hooks/logger/README.md ================================================ # logger (Core Hook) ## Status > ##### Stability: [0](https://github.com/balderdashy/sails-docs/blob/master/contributing/stability-index.md) - Deprecated > > This hook will almost certainly be merged into core (see FAQ below). ## Dependencies In order for this hook to load, the following other hooks must have already finished loading: - moduleloader - userconfig ## Dependents If this hook is disabled, in order for Sails to load, the following other core hooks must also be disabled: _N/A_ ## Purpose This hook's responsibilities are: ##### Set up CaptainsLog Instantiate a [CaptainsLog](https://github.com/balderdashy/captains-log) logger instance. ##### Expose `sails.log` function Publicly expose `sails.log()` function (see http://sailsjs.com/documentation/concepts/logging) ##### Add `sails.log.ship()` method Add an extra method to the logger which teaches it how to draw a ship in ASCII. ## Implicit Defaults This hook sets the following implicit default configuration on `sails.config`: | Property | Type | Default | |------------------------------------------------|:-------------:|-----------------| | `sails.config.log.level` | ((string)) | `'info'` | ## Events ##### `hook:logger:loaded` Emitted when this hook has been automatically loaded by Sails core, and triggered the callback in its `initialize` function. ## FAQ + Why is this a hook and not part of core? + Originally, it was as a way of separating concerns. But realistically, this particular hook _could_ be merged into core (under `app`) in a future release. But realistically since the core configuration process does everything this hook does anyways, this hook _might as well_ be merged into core (under `app`). Look for this to happen in a future release. > If you have a question that isn't covered here, please feel free to send a PR adding it to this section (even if you don't have the answer!) ================================================ FILE: lib/hooks/logger/index.js ================================================ /** * Module dependencies. */ var CaptainsLog = require('captains-log'); var buildShipFn = require('./ship'); module.exports = function(sails) { /** * Expose `logger` hook definition */ return { defaults: { log: { level: 'info' } }, configure: function() { }, /** * Initialize is fired when the hook is loaded, * but after waiting for user config. */ initialize: function(cb) { // Get basic log functions var log = CaptainsLog(sails.config.log); // Mix in log.ship() method log.ship = buildShipFn( sails.version ? ('v' + sails.version) : '', log.info ); // Expose log on sails object sails.log = log; return cb(); } }; }; ================================================ FILE: lib/hooks/logger/ship.js ================================================ /** * Draw an ASCII image of a ship */ module.exports = function _drawShip(message, log) { log = log || console.log; // There are 20 characters before the ship's mast on the 2nd line, // starting from the 'v' (inclusive) var mesageLen = message.length; var numSpaces = 19 - mesageLen; for (var i = 0; i < numSpaces; i++) { message += ' '; } return function() { log(''); log(' .-..-.'); log(''); log(' ' + 'Sails ' + ' ' + '<' + '|' + ' .-..-.'); log(' ' + message + ' |\\'); log(' /|.\\'); log(' / || \\'); log(' ,\' |\' \\'); log(' .-\'.-==|/_--\''); log(' `--\'-------\' '); log(' __---___--___---___--___---___--___'); log(' ____---___--___---___--___---___--___-__'); log(''); }; }; ================================================ FILE: lib/hooks/moduleloader/README.md ================================================ # `moduleloader` (Core Hook) This hook exposes `sails.modules`, a set of functions which other core hooks call to load modules from an app's configured directories in `sails.config.paths`. The moduleloader hook is always the first core hook to load; even before `userconfig`. Consequently, in order to customize `sails.config.paths`, you need to inject configuration into the load process using env variables, the .sailsrc file, or by passing in an option to the programmatic call to sails.lift (i.e. in app.js). Otherwise, by the time your user configuration files in config/* have loaded, it is too late (this hook has already run using the default paths). ================================================ FILE: lib/hooks/moduleloader/index.js ================================================ module.exports = function(sails) { /** * Module dependencies */ var path = require('path'); var fs = require('fs'); var async = require('async'); var _ = require('@sailshq/lodash'); var includeAll = require('include-all'); var mergeDictionaries = require('merge-dictionaries'); var COMMON_JS_FILE_EXTENSIONS = require('common-js-file-extensions'); /** * Module constants */ // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // Supported file extensions for imperative code files such as hooks: // • 'js' (.js) // • 'ts' (.ts) // • 'es6' (.es6) // • ...etc. // // > For full list, see: // > https://github.com/luislobo/common-js-file-extensions/blob/210fd15d89690c7aaa35dba35478cb91c693dfa8/README.md#code-file-extensions // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - var BASIC_SUPPORTED_FILE_EXTENSIONS = COMMON_JS_FILE_EXTENSIONS.code; // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // Supported file extensions, ONLY for configuration files: // • All of the normal supported extensions like 'js', plus // • 'json' (.json) // • 'json5' (.json5) // • 'json.ls' (.json.ls) // • ...etc. // // > For full list, see: // > https://github.com/luislobo/common-js-file-extensions/blob/210fd15d89690c7aaa35dba35478cb91c693dfa8/README.md#configobject-file-extensions // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - var SUPPORTED_FILE_EXTENSIONS_FOR_CONFIG = COMMON_JS_FILE_EXTENSIONS.config.concat(BASIC_SUPPORTED_FILE_EXTENSIONS); /** * Module loader * * Load code files from a Sails app into memory; modules like controllers, * models, services, config, etc. */ return { defaults: function (config) { var localConfig = { // The path to the application appPath: config.appPath ? path.resolve(config.appPath) : process.cwd(), // Paths for application modules and key files // If `paths.app` not specified, use process.cwd() // (the directory where this Sails process is being initiated from) paths: { // Configuration // // For `userconfig` hook config: path.resolve(config.appPath, 'config'), // Server-Side Code // // For `controllers` hook controllers: path.resolve(config.appPath, 'api/controllers'), // For `policies` hook policies: path.resolve(config.appPath, 'api/policies'), // For `services` hook services: path.resolve(config.appPath, 'api/services'), // For `orm` hook adapters: path.resolve(config.appPath, 'api/adapters'), models: path.resolve(config.appPath, 'api/models'), // For `userhooks` hook hooks: path.resolve(config.appPath, 'api/hooks'), // For `blueprints` hook blueprints: path.resolve(config.appPath, 'api/blueprints'), // For `responses` hook responses: path.resolve(config.appPath, 'api/responses'), // For `helpers` hook helpers: path.resolve(config.appPath, 'api/helpers'), // Server-Side View templates // // For `views` hook views: path.resolve(config.appPath, 'views'), layout: path.resolve(config.appPath, 'views/layout.ejs') } }; return localConfig; }, initialize: function(cb) { // Expose self as `sails.modules`. sails.modules = sails.hooks.moduleloader; // | // |_Note that, in the future, the moduleloader's methods will be federated // | out to the places where they're being used, instead of relying on // | having those other modules call the appropriate method on `sails.modules.*()`. return cb(); }, configure: function() { // Default to process.cwd() sails.config.appPath = sails.config.appPath ? path.resolve(sails.config.appPath) : process.cwd(); _.extend(sails.config.paths, { // Configuration // // For `userconfig` hook config: path.resolve(sails.config.appPath, sails.config.paths.config), // Server-Side Code // // For `controllers` hook controllers: path.resolve(sails.config.appPath, sails.config.paths.controllers), // For `policies` hook policies: path.resolve(sails.config.appPath, sails.config.paths.policies), // For `services` hook services: path.resolve(sails.config.appPath, sails.config.paths.services), // For `orm` hook adapters: path.resolve(sails.config.appPath, sails.config.paths.adapters), models: path.resolve(sails.config.appPath, sails.config.paths.models), // For `userhooks` hook hooks: path.resolve(sails.config.appPath, sails.config.paths.hooks), // For `blueprints` hook blueprints: path.resolve(sails.config.appPath, sails.config.paths.blueprints), // For `responses` hook responses: path.resolve(sails.config.appPath, sails.config.paths.responses), // Server-Side HTML // // For `views` hook views: path.resolve(sails.config.appPath, sails.config.paths.views), layout: path.resolve(sails.config.appPath, sails.config.paths.layout) }); }, /** * Load user config from app * * @param {Object} options * @param {Function} cb */ loadUserConfig: function (cb) { async.auto({ 'config/*': function loadOtherConfigFiles (cb) { includeAll.aggregate({ dirname : sails.config.paths.config, exclude : ['locales', /local\..+/], excludeDirs: /(locales|env)$/, filter : new RegExp('^(.+)\\.(' + SUPPORTED_FILE_EXTENSIONS_FOR_CONFIG.join('|') + ')$'), flatten : true, keepDirectoryPath: true, identity : false }, cb); }, 'config/local' : function loadLocalOverrideFile (cb) { includeAll.aggregate({ dirname : sails.config.paths.config, filter : new RegExp('^local\\.(' + SUPPORTED_FILE_EXTENSIONS_FOR_CONFIG.join('|') + ')$'), identity : false }, cb); }, // Load environment-specific config folder, e.g. config/env/development/* 'config/env/**': ['config/local', function loadEnvConfigFolder (asyncData, cb) { // If there's an environment already set in sails.config, then it came from the environment // or the command line, so that takes precedence. Otherwise, check the config/local.js file // for an environment setting. Lastly, default to development. var env = sails.config.environment || asyncData['config/local'].environment || 'development'; includeAll.aggregate({ dirname : path.resolve( sails.config.paths.config, 'env', env ), filter : new RegExp('^(.+)\\.(' + SUPPORTED_FILE_EXTENSIONS_FOR_CONFIG.join('|') + ')$'), optional : true, flatten : true, keepDirectoryPath: true, identity : false }, cb); }], // Load environment-specific config file, e.g. config/env/development.js 'config/env/*' : ['config/local', function loadEnvConfigFile (asyncData, cb) { // If there's an environment already set in sails.config, then it came from the environment // or the command line, so that takes precedence. Otherwise, check the config/local.js file // for an environment setting. Lastly, default to development. var env = sails.config.environment || asyncData['config/local'].environment || 'development'; includeAll.aggregate({ dirname : path.resolve( sails.config.paths.config, 'env' ), filter : new RegExp('^' + _.escapeRegExp(env) + '\\.(' + SUPPORTED_FILE_EXTENSIONS_FOR_CONFIG.join('|') + ')$'), optional : true, flatten : true, keepDirectoryPath: true, identity : false }, cb); }] }, function (err, asyncData) { if (err) { return cb(err); } // Save the environment override, if any. var env = sails.config.environment; // Merge the configs, with env/*.js files taking precedence over others, and local.js // taking precedence over everything. var config = mergeDictionaries( asyncData['config/*'], asyncData['config/env/**'], asyncData['config/env/*'], asyncData['config/local'] ); // Set the environment, but don't allow env/* files to change it; that'd be weird. config.environment = env || asyncData['config/local'].environment || 'development'; // Return the user config cb(undefined, config); }); }, /** * Load adapters * * @param {Object} options * @param {Function} cb */ loadAdapters: function (cb) { // Load things like `api/adapters/FooAdapter.js` includeAll.optional({ dirname: sails.config.paths.adapters, filter: /^(.+Adapter)\..+$/, replaceExpr: /Adapter/, flatten: true, depth: 1 }, function(err, classicStyleAdapters) { if (err) { return cb(err); } // Load things like `api/adapters/foo/index.js` fs.readdir(sails.config.paths.adapters, function(err, contents) { if (err) { if (err.code === 'ENOENT') { return cb(undefined, classicStyleAdapters); } return cb(err); } var folderStyleAdapters = {}; try { _.each(contents, function(filename) { var absPath = path.join(sails.config.paths.adapters, filename); // Exclude things that aren't directories, and directories that start with dots. if (_.startsWith(filename, '.')) { return; } var stats = fs.statSync(absPath); if (!stats.isDirectory()) { return; } // But otherwise, if we see a directory in here, try to require it. // (this follows the rules of the package.json file if there is one-- // or otherwise uses index.js by convention) var adapterDef = require(absPath); // Use the name of the folder as the identity. folderStyleAdapters[filename] = adapterDef; }); //</_.each()> } catch (e) { return cb(e); } // Finally, send back the merged-together set of adapters. return cb(undefined, _.extend(classicStyleAdapters, folderStyleAdapters)); }); //</fs.readdir> }); //</includeall.optional> }, /** * Load app's model definitions * * @param {Object} options * @param {Function} cb */ loadModels: function (cb) { // Get the main model files includeAll.optional({ dirname : sails.config.paths.models, filter : /^(.+)\.(?:(?!md|txt).)+$/, replaceExpr : /^.*\//, flatten: true }, function(err, models) { if (err) { return cb(err); } // --------------------------------------------------------- // Get any supplemental files (BACKWARDS-COMPAT.) includeAll.optional({ dirname : sails.config.paths.models, filter : /(.+)\.attributes.json$/, replaceExpr : /^.*\//, flatten: true }, bindToSails(function(err, supplements) { if (err) { return cb(err); } if (_.keys(supplements).length > 0) { sails.log.debug('The use of `.attributes.json` files is deprecated, and support will be removed in a future release of Sails.'); } return cb(undefined, _.merge(models, supplements)); })); // --------------------------------------------------------- }); }, /** * Load app services * * @param {Object} options * @param {Function} cb */ loadServices: function (cb) { includeAll.optional({ dirname : sails.config.paths.services, filter : /^(.+)\.(?:(?!md|txt).)+$/, depth : 1, caseSensitive : true }, bindToSails(cb)); }, /** * Check for the existence of views in the app * * @param {Object} options * @param {Function} cb */ statViews: function (cb) { includeAll.optional({ dirname: sails.config.paths.views, filter: /^(.+)\.(?:(?!md|txt).)+$/, replaceExpr: null, dontLoad: true }, cb); }, /** * Load app policies * * @param {Object} options * @param {Function} cb */ loadPolicies: function (cb) { includeAll.optional({ dirname: sails.config.paths.policies, filter: /^(.+)\.(?:(?!md|txt).)+$/, replaceExpr: null, flatten: true, keepDirectoryPath: true }, bindToSails(cb)); }, /** * Load app hooks * * > Note that, while `sails.config.hooks` is respected here in this * > function, the `sails.config.loadHooks` setting in regards to * > user hooks is taken care of in the initialize() method of the * > userhooks hook itself. * * @param {Object} options * @param {Function} cb */ loadUserHooks: function (cb) { var defaultInstalledHooks = _.filter(_.values(require('../../app/configuration/default-hooks')), function(val) {return val !== true;}); // Get the current app's package.json file (defaulting to an empty dictionary) var appPackageJson; try { appPackageJson = require(path.resolve(sails.config.appPath, 'package.json')); } catch (unusedErr) { appPackageJson = {}; } async.auto({ // Load user hooks from the "api/hooks" folder hooksFolder: function(cb) { includeAll.optional({ dirname: sails.config.paths.hooks, filter: new RegExp('^(.+)\\.(' + BASIC_SUPPORTED_FILE_EXTENSIONS.join('|') + ')$'), // Hooks should be defined as either single files as a function // OR (better yet) a subfolder with an index.js file // (like a standard node module) depth: 2 }, cb); }, // Load package.json files from node_modules to check for hooks nodeModulesFolder: function(cb) { includeAll.optional({ dirname: path.resolve(sails.config.appPath, 'node_modules'), filter: /^(package\.json)$/, excludeDirs: /^\./, // Look inside namespaced folders e.g. node_modules/@sailsjs/sails-hook-foo depth: 3, // Don't actually load the files, since malformed ones would cause a crash. // Just keep track of where they are and we'll load them carefully below. dontLoad: true }, function(err, modules) { if (err) { return cb(err); } // Now that we have a map of where the package.json files are, flatten that // map and load the files carefully. Map might look something like: // { angular2: // { animate: {}, // bundles: { web_worker: undefined }, // es6: { dev: undefined, prod: undefined }, // examples: { router: undefined }, // http: {}, // 'package.json': true, // etc... modules = (function _flatten(modules, installedHooks, currentPath, level) { installedHooks = installedHooks || {}; currentPath = currentPath || ''; level = level || 0; // Loop through the keys in the current map object Object.keys(modules).forEach(function(identity) { // If it represents a package.json file, attempt to load it and, if // successful, save it in our set of found files. If unsuccessful, // just ignore it. if (identity === 'package.json' && modules[identity] === true) { var filePath = path.resolve(sails.config.appPath, 'node_modules', currentPath, identity); try { // Attempt to load the package.json file var packageJson = require(filePath); // If the module isn't declared as a Sails hook, ignore it. if (!packageJson.sails || !packageJson.sails.isHook) { return; } // If the module isn't saved in this app's package.json, ignore it. if (!_.get(appPackageJson, 'dependencies.' + packageJson.name) && !_.get(appPackageJson, 'devDependencies.' + packageJson.name) && !_.get(appPackageJson, 'optionalDependencies.' + packageJson.name)) { sails.log.debug('Ignoring hook `' + packageJson.name + '` because it isn\'t saved as any kind of dependency in your package.json file.'); sails.log.debug('(You could try installing it with `npm install ' + packageJson.name +' --save`. Or if you aren\'t using this hook,'); sails.log.debug('just remove it from the node_modules/ folder and this message will stop appearing.)'); sails.log.debug(); return; } // If it's one of our default hooks, ignore it so that it can be safely overridden. if (_.contains(defaultInstalledHooks, packageJson.name)) { return; } // Save a reference to this installed hook, which we'll use to require // the full module below. installedHooks[currentPath] = packageJson; } catch(e) { sails.log.verbose('While searching for installable hooks, found invalid package.json file at `'+filePath+'`. Details:',e.stack); return; } } // If the key represents an object, recursively search within it, but only if it's directly // under node_modules or under a node_modules/@something (namespaced) folder if (_.isObject(modules[identity]) && level === 0 || (level === 1 && currentPath[0] === '@')) { var nextPath; if (currentPath) { nextPath = path.join(currentPath,identity); } else { nextPath = identity; } _flatten(modules[identity], installedHooks, nextPath, level + 1 ); } });//</forEach() :: key in `modules`> // Return the dictionary of installed hooks we found. return installedHooks; })(modules);//</ invoked self-calling recursive function :: _flatten()> return cb(undefined, modules); });//</includeAll.optional() :: loading package.json files from the node_modules folder to check for hooks> } }, function(err, results) { if (err) {return cb(err);} // Marshall the hooks by checking that they are valid. The ones from the // api/hooks folder are assumed to be okay, as long as they aren't explicitly turned off. var hooks = _.reduce(results.hooksFolder, function(memo, module, identity) { if (sails.config.hooks[identity] !== false && sails.config.hooks[identity] !== 'false') { memo[identity] = module; } return memo; }, {}); try { // Loop through the package.json files of the hooks we found in the node_modules folder. _.extend(hooks, _.reduce(results.nodeModulesFolder, function(memo, modulePackageJson, identity) { // Any special config for this hook will be under the `sails` key in the package.json file. var hookConfig = modulePackageJson.sails; // Determine the name the hook should be added as var hookName; if (!_.isEmpty(hookConfig.hookName)) { hookName = hookConfig.hookName; } // If an identity was specified in sails.config.installedHooks, use that else if (sails.config.installedHooks && sails.config.installedHooks[identity] && sails.config.installedHooks[identity].name) { hookName = sails.config.installedHooks[identity].name; } // Otherwise use the module name, with namespacing and initial "sails-hook-" stripped off if it exists else { // Strip off any NPM namespacing and/or sails-hook- prefix hookName = identity.replace(/^(@.+?[\/\\])?(sails-hook-)?/, ''); } if (sails.config.hooks[hookName] === false || sails.config.hooks[hookName] === 'false') { return memo; } // Allow overriding core hooks if (sails.hooks[hookName]) { sails.log.verbose('Found hook: `'+hookName+'` in `node_modules/`. Overriding core hook w/ the same identity...'); } // If we have a hook in api/hooks with this name, throw an error if (hooks[hookName]) { var err = (function (){ var msg = 'Found hook: `' + hookName + '`, in `node_modules/`, but a hook with that identity already exists in `api/hooks/`. '+ 'The hook defined in your `api/hooks/` folder will take precedence.'; var err = new Error(msg); err.code = 'E_INVALID_HOOK_NAME'; return err; })(); sails.log.warn(err); return memo; } // Load the hook code var hook = require(path.resolve(sails.config.appPath, 'node_modules', identity)); // Set its config key (defaults to the hook name) hook.configKey = (sails.config.installedHooks && sails.config.installedHooks[identity] && sails.config.installedHooks[identity].configKey) || hookName; // Add this to the list of hooks to load memo[hookName] = hook; return memo; }, {}));//</_.reduce() + _.extend()> return bindToSails(cb)(null, hooks); } catch (e) { return cb(e); } });//</after async.auto> },//<loadUserHooks> /** * Load custom blueprint actions. * * @param {Object} options * @param {Function} cb */ loadBlueprints: function (cb) { includeAll.optional({ dirname: sails.config.paths.blueprints, filter: /^(.+)\.(?:(?!md|txt).)+$/, useGlobalIdForKeyName: true }, cb); }, /** * Load custom API responses. * * @param {Object} options * @param {Function} cb */ loadResponses: function (cb) { includeAll.optional({ dirname: sails.config.paths.responses, filter: /^(.+)\.(?:(?!md|txt).)+$/, useGlobalIdForKeyName: true }, bindToSails(cb)); }, optional: includeAll.optional, required: includeAll.required, aggregate: includeAll.aggregate, exists: includeAll.exists }; /** * Private helper function used above. * * @param {Function} cb [description] * @return {Function} * @param {Error?} err * @param {Dictionary} modules */ function bindToSails(cb) { return function(err, modules) { if (err) {return cb(err);} _.each(modules, function(moduleDef) { // Add a reference to the Sails app that loaded the module moduleDef.sails = sails; // Bind all methods to the module context _.bindAll(moduleDef); }); return cb(undefined, modules); }; }//</bindToSails definition (private helper function)> }; ================================================ FILE: lib/hooks/policies/README.md ================================================ # Policies (Core Hook) ## Status > ##### Stability: [2](https://github.com/balderdashy/sails-docs/blob/master/contributing/stability-index.md) - Stable ## Purpose This hook's responsibilities are: 1. Use `sails.modules` to read policies from the user's app into `self.middleware`. 2. Normalize the policy mapping config (`sails.config.policies`) 3. Listen for `route:typeUnknown` and bind a policy if the route requests it. 4. Listen for `router:before` and when it fires, transform loaded middleware that match the policy mapping config (i.e. controller actions) to arrays of functions, where the original middleware is "protected" by one or more relevant policy middleware. ## FAQ > No frequently asked questions yet... > > If you have a question, please feel free to send a PR adding it to this section (even if you don't have the answer!) ================================================ FILE: lib/hooks/policies/index.js ================================================ /** * Module dependencies */ var _ = require('@sailshq/lodash'); var flaverr = require('flaverr'); var Err = require('../../../errors'); /** * Policies hook * @param {SailsApp} sails */ module.exports = function(sails) { /** * Expose `policies` hook definition */ var policyHookDef = { defaults: { // Default policy mappings (allow all) policies: { } }, configure: function () { this.middleware || (this.middleware = { }); }, /** * Initialize is fired first thing when the hook is loaded * * @api public */ initialize: function(cb) { var self = this; // Grab policies config & policy modules and trigger callback self.loadMiddleware(function (err) { if (err) { return cb(err); } sails.log.silly('Finished loading policy middleware functions. Preparing to bind policies based on config...'); try { self.bindPolicies(); } catch (e) { return cb(e); } return cb(); }); }, /** * Wipe everything and (re)load middleware from policies * (policies.js config is already loaded at this point) * * @api private */ loadMiddleware: function(cb) { var self = this; // Load policy modules from disk. sails.log.silly('Loading policy modules from app...'); sails.modules.loadPolicies(function modulesLoaded (err, modules) { if (err) { return cb(err); } // Add the loaded policies to our internal dictionary. _.extend(self.middleware, modules); // If any policies were specified when loading Sails, add those on // top of the ones loaded from disk. if (sails.config.policies && sails.config.policies.moduleDefinitions) { _.extend(self.middleware, sails.config.policies.moduleDefinitions); } // Validate that all policies are functions. try { _.each(_.keys(self.middleware), function(policyName) { // If we find a bad'n, bail out. if (!_.isFunction(sails.hooks.policies.middleware[policyName])) { throw flaverr({ name: 'userError', code: 'E_INVALID_POLICY' }, new Error('Failed loading invalid policy `' + policyName + '` (expected a function, but got a `' + typeof(sails.hooks.policies.middleware[policyName]) + '`)' )); } }); } catch (e) { return cb(e); } // Set the _middlewareType property on each policy. _.each(self.middleware, function(policyFn, policyName) { policyFn._middlewareType = 'POLICY: '+policyName; }); return cb(); }); }, /** * Curry the policy chains into the appropriate controller functions * * @api private */ bindPolicies: function() { // Build / normalize policy config this.mapping = this.buildPolicyMap(); // Register action middleware for each item in the map _.each(this.mapping, function(policies, targets) { sails.registerActionMiddleware(policies, targets); }); // Emit event to let other hooks know we're ready to go sails.log.silly('Policy-controller bindings complete!'); sails.emit('hook:policies:bound'); }, /** * Build normalized, hook-internal representation of policy mapping * by performing a non-destructive parse of `sails.config.policies` * * @returns {Object} mapping * @api private */ buildPolicyMap: function () { // Loop through the keys looking for the old-style "controller-based" policy config, // and if we find it then expand it out to the new style. _.each(_.without(_.keys(sails.config.policies), 'moduleDefinitions'), function(key) { // Is this a plain dictionary, e.g. UserController: { '*': true } ? if (_.isPlainObject(sails.config.policies[key])) { // Get the controller name by stripping off the (optional) trailing "Controller" var controller = key.replace(/Controller$/,'').toLowerCase(); // For each item (i.e. action) in the dictionary, add an entry to the config. _.each(_.keys(sails.config.policies[key]), function(action) { // Get the policies to attach to this action. var policies = sails.config.policies[key][action]; // Add the target/policies mapping to sails.config.policies sails.config.policies[controller + '/' + action.toLowerCase()] = policies; }); // Remove the deprecated config key. delete sails.config.policies[key]; } // Make sure all standalone action glob keys are lowercased. else if (key !== key.toLowerCase()) { sails.config.policies[key.toLowerCase()] = sails.config.policies[key]; delete sails.config.policies[key]; } }); // Sort the policy keys alphabetically, ensuring that more restrictive // keys (e.g. user/foo) come after less restrictive (e.g. user/*). // Ignore `moduleDefinitions` since it is a special key used to allow // programmatic setting of policy functions. var actionsToProtect = _.without(_.keys(sails.config.policies), 'moduleDefinitions').sort(); // Declare a "never allow" function to use when a policy of `false` is encountered. var neverAllow = function neverAllow (req, res) { return res.forbidden(); }; neverAllow._middlewareType = 'POLICY: false (neverAllow)'; // Declare a "never allow" function to use when a policy of `false` is encountered. var alwaysAllow = function alwaysAllow (req, res, next) { return next(); }; alwaysAllow._middlewareType = 'POLICY: true (alwaysAllow)'; // Loop through the keys and create the map. var mapping = _.reduce(actionsToProtect, function (memo, target, index) { // Allow bald `true` and `false` policies by wrapping them in an array. if (sails.config.policies[target] === true || sails.config.policies[target] === false) { sails.config.policies[target] = [sails.config.policies[target]]; } // Make sure policies are contained in an array. if (!_.isArray(sails.config.policies[target])) { sails.config.policies[target] = [sails.config.policies[target]]; } // Get the policies the user wants to add to this set of actions. // Note the use of _.compact to transform [undefined] into []. var policies = _.compact(_.map(sails.config.policies[target], function(policy) { // If the policy is `true`, make sure it's the only one for this target. if (policy === true) { if (sails.config.policies[target].length > 1) { throw flaverr({ name: 'userError', code: 'E_INVALID_POLICY_CONFIG' }, new Error('Invalid policy setting for `' + target + '`: if `true` is specified, it must be the only policy in the array.')); } // Map `true` to the "always allow" policy. return alwaysAllow; } // If the policy is `false`, make sure it's the only one for this target. if (policy === false) { if (sails.config.policies[target].length > 1) { throw flaverr({ name: 'userError', code: 'E_INVALID_POLICY_CONFIG' }, new Error('Invalid policy setting for `' + target + '`: if `false` is specified, it must be the only policy in the array.')); } // Map `false` to the "never allow" policy. return neverAllow; } // If the policy is a string, make sure it corresponds to one of the policies we loaded. if (_.isString(policy)) { if (!sails.hooks.policies.middleware[policy.toLowerCase()]) { throw flaverr({ name: 'userError', code: 'E_INVALID_POLICY_CONFIG' }, new Error('Invalid policy setting for `' + target + '`: `' + policy + '` does not correspond to any of the loaded policies.')); } return sails.hooks.policies.middleware[policy.toLowerCase()]; } // If the policy is a function, return it. if (_.isFunction(policy)) { policy._middlewareType = 'POLICY: ' + (policy.name || 'anonymous'); return policy; } // Otherwise just bail. throw flaverr({ name: 'userError', code: 'E_INVALID_POLICY_CONFIG' }, new Error('Invalid policy setting for `' + target + '`: a policy must be a string, a function or `false`.')); })); // Start an array of targets that this set of policies will be applied to or ignored for. var allowDenyList = [target]; // If this is the global target, loop through the rest of the targets and exclude them // from this one. We may change this behavior / make it optional in the future, // but for now policies are NOT cumulative. if (target === '*') { (function() { for (var i = index + 1; i < actionsToProtect.length; i++) { var nextTarget = actionsToProtect[i]; allowDenyList.push('!' + nextTarget); } })(); } // If this target is a wildcard, then any other target that matches it will // override it. We may change this behavior / make it optional in the future, // but for now policies are NOT cumulative. else if (target.slice(-2) === '/*') { (function() { // Get a version of the target without the /* var nakedTarget = target.slice(0,-2); // Get a version of the target without the . var slashTarget = target.slice(0,-1); // If we already bound a policy to the naked target, then flag that the // current policy should _not_ be applied to it. if (memo[nakedTarget]) { allowDenyList.push('!' + nakedTarget); } // Now run through the rest of the targets in the list, and if any of them // start with the "slashTarget", make sure this policy does _not_ apply to them. // So if our target is `user/foo/*`, and we see `user/foo/bar` in the list, // we will add that to the blacklist for this policy. for (var i = index + 1; i < actionsToProtect.length; i++) { var nextTarget = actionsToProtect[i]; if (nextTarget.indexOf(slashTarget) === 0) { allowDenyList.push('!' + nextTarget); } // As soon as we find a non-matching target, we're done (because they're // arranged in alphabetical order). else { break; } } })(); } // Transform the allow/deny list into a comma-delimited string that can be // understood by `registerActionMiddleware`. memo[allowDenyList.join(',')] = policies; return memo; }, {}); return mapping; }, }; /** * Bind `route:typeUnknown` event handler in order to support * the `policy: '...` route option. * * > This allows for manually mapping policies directly on top * > of explicit routes; e.g. in `config/routes.js` */ sails.on('route:typeUnknown', function bindDirectlyToRoute (event) { // Only pay attention to delegated route events // if `policy` is declared in event.target if ( !event.target || !event.target.policy ) { return; } var policyId = event.target.policy.toLowerCase(); // Policy doesn't exist if (!sails.hooks.policies.middleware[policyId] ) { var routeAddrToDisplay = event.verb ? event.verb+' '+event.path : event.path; Err.fatal.__UnknownPolicy__ (policyId, routeAddrToDisplay, sails.config.paths.policies); // ^^That begins terminating the process. return; }//-• // Bind policy function to route. // Make sure to merge the target and options together, so that route options like `skipRegex` // are still applied to requests before running the policy. var fn = sails.hooks.policies.middleware[policyId]; sails.router.bind(event.path, fn, event.verb, _.merge(event.options, event.target)); }); return policyHookDef; }; ================================================ FILE: lib/hooks/pubsub/README.md ================================================ # Pubsub (core hook) See http://sailsjs.com/documentation/reference/web-sockets/resourceful-pub-sub for more info. ================================================ FILE: lib/hooks/pubsub/index.js ================================================ /** * Module dependencies. */ var util = require('util'); var _ = require('@sailshq/lodash'); /** * Module errors */ var Err = { dependency: function (dependent, dependency) { return new Error( '\n' + 'Cannot use `' + dependent + '` hook ' + 'without the `' + dependency + '` hook enabled!' ); } }; // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // TODO: Remove this hook altogether, instead splitting its contents between // the `blueprints` and `sockets` hooks. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /** * pubsub hook * * > Implements public resourceful pubsub (RPS) methods, as well as some * > private methods used by the blueprints hook. */ module.exports = function(sails) { // Private function for parsing a potential instance ID. var parseId = function (id) { if(!_.isObject(this.attributes, this.primaryKey)) { return id; } var pkAttrDef = this.attributes[this.primaryKey]; if(_.isPlainObject(pkAttrDef)) { if (pkAttrDef.type === 'number') { return parseInt(id); } else if (pkAttrDef.type === 'string') { return new String(id).toString(); //jshint ignore:line } } return id; }; /** * Check that records are a list, if not, make them a list * Also if they are ids, make them dummy objects with an `id` property * * @param {Object|Array|String|Finite} records * @returns {Array} array of things that have an `id` property * * @api private * @synchronous */ var pluralize = function (records) { // If `records` is a non-array object, // turn it into a single-item array ("pluralize" it) // e.g. { id: 7 } -----> [ { id: 7 } ] if ( !_.isArray(records) ) { var record = records; records = [record]; } // If a list of ids things look ids (finite numbers or strings), // wrap them up as dummy objects; e.g. [1,2] ---> [ {id: 1}, {id: 2} ] var self = this; return _.map(records, function (record) { if ( _.isString(record) || _.isFinite(record) ) { var id = record; var data = {}; data[self.primaryKey] = id; return data; } if (_.isNull(record) || _.isUndefined(record)) { throw new Error('Could not coerce value into an array of records!'); } return record; }); }; /** * Expose Hook definition */ return { initialize: function(cb) { var self = this; // If `views` or `orm` hook is not enabled, complain and disable the hook. if (!sails.hooks.sockets || !sails.hooks.orm) { sails.log.verbose('Cannot use `pubsub` hook without the `sockets` and `orm` hooks enabled! (Skipping...)'); delete sails.hooks.pubsub; return cb(); } // If `views` or `orm` hook is not enabled, complain and respond w/ error if (!sails.hooks.sockets) { return cb( Err.dependency('pubsub', 'sockets') ); } if (!sails.hooks.orm) { return cb( Err.dependency('pubsub', 'orm') ); } // Wait for `hook:orm:loaded` sails.on('hook:orm:loaded', function() { // Do the heavy lifting self.augmentModels(); // Indicate that the hook is fully loaded cb(); }); // When the orm is reloaded, re-apply all of the pubsub methods to the // models sails.on('hook:orm:reloaded', function() { self.augmentModels(); // Trigger an event in case something needs to respond to the pubsub reload sails.emit('hook:pubsub:reloaded'); }); }, augmentModels: function() { // Augment models with room/socket logic (& bind context) for (var identity in sails.models) { var AugmentedModel = _.defaults(sails.models[identity], getPubsubMethods(), {autosubscribe: true} ); _.bindAll(AugmentedModel, 'subscribe', 'unsubscribe', 'publish', '_watch', '_room', '_introduce', '_retire', '_publishCreate', '_publishUpdate', '_publishDestroy', '_publishAdd', '_publishRemove' ); sails.models[identity] = AugmentedModel; } } }; /** * These methods get appended to the Model class objects * Some take req.socket as an argument to get access * to user('s|s') socket object(s) */ function getPubsubMethods () { return { // ██████╗ ██╗ ██╗██████╗ ██╗ ██╗ ██████╗ // ██╔══██╗██║ ██║██╔══██╗██║ ██║██╔════╝ // ██████╔╝██║ ██║██████╔╝██║ ██║██║ // ██╔═══╝ ██║ ██║██╔══██╗██║ ██║██║ // ██║ ╚██████╔╝██████╔╝███████╗██║╚██████╗ // ╚═╝ ╚═════╝ ╚═════╝ ╚══════╝╚═╝ ╚═════╝ // /** * Broadcast a custom message to sockets connected to the specified records * @param {Object|String|Finite} records -- record or ID of record whose subscribers should receive the message * @param {Object|Array|String|Finite} data -- the message payload * @param {Request|Socket} req - if specified, broadcast using this * socket (effectively omitting it) * */ publish: function(ids, data, req) { var self = this; // ids is required. if (!ids) { sails.log.error('`' + self.identity + '.publish` : missing or empty second argument `ids`. API is `.publish(ids, data [, req])`.'); return; } // ids must be an array of primary keys -- we'll coerce it (with a warning) if it's not. if (!_.isArray(ids) || _.any(ids, function(id) {return !_.isString(id) && !_.isNumber(id);})) { sails.log.debug('The first argument passed to `' + self.identity + '.publish()` must be an array of ids. To subscribe to a single record, wrap the id in an array.'); try { ids = _.pluck(pluralize.apply(this, [ids]), this.primaryKey); } catch (err) { throw new Error('We tried to transform `' + util.inspect(ids, {depth: 2}) + '` into an array of IDs, but there was a problem (could some values have been `null` or `undefined`?) Details: '+err.stack); } sails.log.debug('For example: `[' + ids[0] + ']`'); sails.log.debug('Wrapping it in an array for you this time...'); } // If a request object was sent, get its socket, otherwise assume a socket was sent. var socketToOmit = (req && req.socket ? req.socket : req); // Ensure that we're working with a clean, unencumbered object data = _.cloneDeep(data); // Loop through the record IDs to broadcast to. _.each(ids, function(id) { var room = self._room(id); sails.sockets.broadcast( room, self.identity, data, socketToOmit ); }); }, /** * Subscribe a socket to a handful of records in this model * * Usage: * Model.subscribe(req, ids) * * @param {Request|Socket} req - request containing the socket to subscribe, or the socket itself * @param {Array} ids - array of ids of instances to subscribe to * * // Subscribe to User.update() and User.destroy() * // for the specified instances (or user.save() / user.destroy()): * User.subscribe(req.socket, users) * * @api public */ subscribe: function (req, ids) { var self = this; // If a request object was sent, get its socket, otherwise assume a socket was sent. var socket = sails.sockets.parseSocket(req); // Request must originate from a socket. if (!socket) { sails.log.debug('`Model.subscribe()` called by a non-socket request. Only requests originating from a connected socket may be subscribed. Ignoring...'); return; } if (!ids) { sails.log.error('`' + self.identity + '.subscribe` : missing or empty second argument `ids`. API is `.subscribe(request, ids)`.'); return; } if (!_.isArray(ids) || _.any(ids, function(id) {return !_.isString(id) && !_.isNumber(id);})) { sails.log.debug('The second argument passed to `' + self.identity + '.subscribe()` must be an array of ids. To subscribe to a single record, wrap the id in an array.'); try { ids = _.pluck(pluralize.apply(this, [ids]), this.primaryKey); } catch (err) { throw new Error('We tried to transform `' + util.inspect(ids, {depth: 2}) + '` into an array of IDs, but there was a problem (could some values have been `null` or `undefined`?) Details: '+err.stack); } sails.log.debug('For example: `[' + ids[0] + ']`'); sails.log.debug('Wrapping it in an array for you this time...'); } for (let id of ids) { // Attempt to join the room for the specified instance. sails.sockets.join( socket, self._room(id) ); sails.log.silly( 'Subscribed to the ' + self.globalId + ' with id=' + id + '\t(room :: ' + self._room(id) + ')' ); }//∞ }, /** * Unsubscribe a socket from some records * * Usage: * Model.unsubscribe(req, ids) * * @param {Request|Socket} req - request containing the socket to unsubscribe, or the socket itself * @param {Array} ids - array of ids of instances to unsubscribe from */ unsubscribe: function (req, ids) { var self = this; // If a request object was sent, get its socket, otherwise assume a socket was sent. var socket = sails.sockets.parseSocket(req); if (!socket) { sails.log.debug('`Model.unsubscribe()` called by a non-socket request. Only requests originating from a connected socket may be subscribed. Ignoring...'); return; } // If no ids provided, unsubscribe from the class room if (!ids) { sails.log.error('`' + self.identity + '.unsubscribe` : missing or empty second argument `ids`. API is `.subscribe(request, ids)`.'); return; } // ids must be an array of primary keys -- we'll coerce it (with a warning) if it's not. if (!_.isArray(ids) || _.any(ids, function(id) {return !_.isString(id) && !_.isNumber(id);})) { sails.log.debug('The second argument passed to `' + self.identity + '.unsubscribe()` must be an array of ids. To subscribe to a single record, wrap the id in an array.'); try { ids = _.pluck(pluralize.apply(this, [ids]), this.primaryKey); } catch (err) { throw new Error('We tried to transform `' + util.inspect(ids, {depth: 2}) + '` into an array of IDs, but there was a problem (could some values have been `null` or `undefined`?) Details: '+err.stack); } sails.log.debug('For example: `[' + ids[0] + ']`'); sails.log.debug('Wrapping it in an array for you this time...'); } for (let id of ids) { // Attempt to leave the room for the specified instance. sails.sockets.leave( socket, self._room(id)); sails.log.silly( 'Unsubscribed from the ' + self.globalId + ' with id=' + id + '\t(room :: ' + self._room(id) + ')' ); }//∞ }, /** * Get the socket room name for a model instance. * * Usage: * Model.getRoomName(id) * * @param {Number|String} id - the ID of the instance to get the room name for. */ getRoomName: function(id) { return this._room(id); }, // ██████╗ ██████╗ ██╗██╗ ██╗ █████╗ ████████╗███████╗ // ██╔══██╗██╔══██╗██║██║ ██║██╔══██╗╚══██╔══╝██╔════╝ // ██████╔╝██████╔╝██║██║ ██║███████║ ██║ █████╗ // ██╔═══╝ ██╔══██╗██║╚██╗ ██╔╝██╔══██║ ██║ ██╔══╝ // ██║ ██║ ██║██║ ╚████╔╝ ██║ ██║ ██║ ███████╗ // ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═══╝ ╚═╝ ╚═╝ ╚═╝ ╚══════╝ // /** * Broadcast a resourceful pubsub message to sockets connected to the specified records * (or null to broadcast to the entire class room) * * @param {Object|Array|String|Finite} records -- records whose subscribers should receive the message * @param {Object|Array|String|Finite} data -- the message payload * socket (effectively omitting it) * * @api private */ _publishRPS: function (records, data, req) { var self = this; records = pluralize.apply(this, [records]); var ids = _.pluck(records, this.primaryKey); if ( ids.length === 0 ) { sails.log.debug('Can\'t publish a message to an empty list of instances-- ignoring...'); } // If a request object was sent, get its socket, otherwise assume a socket was sent. var socketToOmit = (req && req.socket ? req.socket : req); // Ensure that we're working with a clean, unencumbered object data = _.cloneDeep(data); // Loop through the record IDs to broadcast to. _.each(ids, function(id) { sails.sockets.broadcast( self._room(id), self.identity, data, socketToOmit ); }); }, /** * @param {String|Number} id Unique ID (i.e. primary key) of the record to get the room for * @param {String} name Name of the room to get the identifier for * @return {String} name of the instance room for an instance of this model w/ given id * @synchronous */ _room: function (id) { if (!id) { sails.log.error('Must specify an `id` when calling `Model._room(id)`'); return; } return 'sails_model_'+this.identity+'_'+id+':'+this.identity; }, /** * @return {String} name of this model's global class room * @synchronous * @api private */ _classRoom: function() { return 'sails_model_create_'+this.identity; }, /** * Publish an update on a particular model * * @param {String|Finite} id * - primary key of the instance we're referring to * * @param {Object} changes * - an object of changes to this instance that will be broadcasted * * @param {Request|Socket} req - if specified, broadcast using this socket (effectively omitting it) * * @api public */ _publishUpdate: function (id, changes, req, options) { var reverseAssociation; // Make sure there's an options object options = options || {}; // Ensure that we're working with a clean, unencumbered object changes = _.cloneDeep(changes); // Enforce valid usage var validId = _.isString(id) || _.isFinite(id); if ( !validId ) { return sails.log.error( 'Invalid usage of `' + this.identity + '._publishUpdate(id, changes, [socketToOmit])`' ); } if (_.isFunction(this._beforePublishUpdate)) { this._beforePublishUpdate(id, changes, req, options); } // Coerce id to match the attribute type of the primary key of the model id = parseId.apply(this,[id]); var data = { model: this.identity, verb: 'update', data: changes, id: id }; if (options.previous && !options.noReverse) { var previous = options.previous; // If any of the changes were to association attributes, publish add or remove messages. _.each(changes, function(val, key) { // If value wasn't changed, do nothing if (val === previous[key]) { return; } // Find an association matching this attribute var association = _.find(this.associations, {alias: key}); // If the attribute isn't an assoctiation, return if (!association) { return; } // Get the associated model class var ReferencedModel = sails.models[association.type === 'model' ? association.model : association.collection]; // Bail if this attribute isn't in the model's schema if (association.type === 'model') { var previousPK = _.isObject(previous[key]) ? previous[key][ReferencedModel.primaryKey] : previous[key]; var newPK = _.isObject(val) ? val[this.primaryKey] : val; if (previousPK === newPK) { return; } // Get the inverse association definition, if any reverseAssociation = _.find(ReferencedModel.associations, {collection: this.identity, via: key}) || _.find(ReferencedModel.associations, {model: this.identity, via: key}); if (!reverseAssociation) {return;} // If this is a to-many association, do _publishAdd or _publishRemove as necessary // on the other side if (reverseAssociation.type === 'collection') { // If there was a previous value, alert the previously associated model if (previous[key]) { ReferencedModel._publishRemove(previousPK, reverseAssociation.alias, id, req, {noReverse:true}); } // If there's a new value (i.e. it's not null), alert the newly associated model if (val) { ReferencedModel._publishAdd(newPK, reverseAssociation.alias, id, req, {noReverse:true}); } } // Otherwise do a _publishUpdate else { var pubData = {}; // If there was a previous association, notify it that it has been nullified if (previous[key]) { pubData[reverseAssociation.alias] = null; ReferencedModel._publishUpdate(previousPK, pubData, req, {noReverse:true}); } // If there's a new association, notify it that it has been linked if (val) { pubData[reverseAssociation.alias] = id; ReferencedModel._publishUpdate(newPK, pubData, req, {noReverse:true}); } } } else { // Get the reverse association definition, if any reverseAssociation = _.find(ReferencedModel.associations, {collection: this.identity, via: key}) || _.find(ReferencedModel.associations, {model: this.identity, alias: association.via}); if (!reverseAssociation) {return;} // If we can't get the previous PKs (b/c previous isn't populated), bail if (_.isUndefined(previous[key])) { return; } // Get the previous set of IDs var previousPKs = _.pluck(previous[key], ReferencedModel.primaryKey); // Get the current set of IDs var updatedPKs = _.map(val, function(_val) { if (_.isObject(_val)) { return _val[ReferencedModel.primaryKey]; } else { return _val; } }); // Find any values that were added to the collection var addedPKs = _.difference(updatedPKs, previousPKs); // Find any values that were removed from the collection var removedPKs = _.difference(previousPKs, updatedPKs); // If this is a to-many association, do _publishAdd or _publishRemove as necessary // on the other side if (reverseAssociation.type === 'collection') { // Alert any removed models _.each(removedPKs, function(pk) { ReferencedModel._publishRemove(pk, reverseAssociation.alias, id, req, {noReverse:true}); }); // Alert any added models _.each(addedPKs, function(pk) { ReferencedModel._publishAdd(pk, reverseAssociation.alias, id, req, {noReverse:true}); }); } // Otherwise do a _publishUpdate else { // Alert any removed models _.each(removedPKs, function(pk) { var pubData = {}; pubData[reverseAssociation.alias] = null; ReferencedModel._publishUpdate(pk, pubData, req, {noReverse:true}); }); // Alert any added models _.each(addedPKs, function(pk) { var pubData = {}; pubData[reverseAssociation.alias] = id; ReferencedModel._publishUpdate(pk, pubData, req, {noReverse:true}); }); }//</else> }//</else> }, this);//</_.each()> }//</ if `previous` and `!noReverse` > // If a request object was sent, get its socket, otherwise assume a socket was sent. var socketToOmit = (req && req.socket ? req.socket : req); data.verb = 'updated'; data.previous = options.previous; delete data.model; // Broadcast to the model instance room this._publishRPS(id, data, socketToOmit); if (_.isFunction(this._afterPublishUpdate)) { this._afterPublishUpdate(id, changes, req, options); } }, /** * Publish the destruction of a particular model * * @param {String|Finite} id * - primary key of the instance we're referring to * * @param {Request|Socket} req - if specified, broadcast using this socket (effectively omitting it) * */ _publishDestroy: function (id, req, options) { var reverseAssociation; options = options || {}; // Enforce valid usage var invalidId = !id || _.isObject(id); if ( invalidId ) { return sails.log.error( 'Invalid usage of `' + this.identity + '._publishDestroy(id, [socketToOmit])`' ); } if (_.isFunction(this._beforePublishDestroy)) { this._beforePublishDestroy(id, req, options); } // Coerce id to match the attribute type of the primary key of the model id = parseId.apply(this,[id]); var data = { model: this.identity, verb: 'destroy', id: id, previous: options.previous }; // If a request object was sent, get its socket, otherwise assume a socket was sent. var socketToOmit = (req && req.socket ? req.socket : req); data.verb = 'destroyed'; delete data.model; // Broadcast to the model instance room this._publishRPS(id, data, socketToOmit); // Unsubscribe everyone from the model instance this._retire(id); if (options.previous) { var previous = options.previous; // Loop through associations and alert as necessary _.each(this.associations, function(association) { var ReferencedModel; // If it's a to-one association, and it wasn't falsy, alert // the reverse side if (association.type === 'model' && association.alias && previous[association.alias]) { ReferencedModel = sails.models[association.model]; // Get the inverse association definition, if any reverseAssociation = _.find(ReferencedModel.associations, {collection: this.identity}) || _.find(ReferencedModel.associations, {model: this.identity}); if (reverseAssociation) { // If it's a to-one, publish a simple update alert var referencedModelId = _.isObject(previous[association.alias]) ? previous[association.alias][ReferencedModel.primaryKey] : previous[association.alias]; if (reverseAssociation.type === 'model') { var pubData = {}; pubData[reverseAssociation.alias] = null; ReferencedModel._publishUpdate(referencedModelId, pubData, req, {noReverse:true}); } // If it's a to-many, publish a "removed" alert else { ReferencedModel._publishRemove(referencedModelId, reverseAssociation.alias, id, req, {noReverse:true}); } } } else if (association.type === 'collection' && association.via && previous[association.alias] && previous[association.alias].length) { ReferencedModel = sails.models[association.collection]; // Get the inverse association definition, if any var reverseAttribute = ReferencedModel.attributes[association.via]; _.each(previous[association.alias], function(associatedModel) { // If it's a to-one, publish a simple update alert if (reverseAttribute.model) { var pubData = {}; pubData[association.via] = null; ReferencedModel._publishUpdate(associatedModel[ReferencedModel.primaryKey], pubData, req, {noReverse:true}); } // If it's a to-many, publish a "removed" alert else { ReferencedModel._publishRemove(associatedModel[ReferencedModel.primaryKey], association.via, id, req, {noReverse:true}); } }); } }, this); } if (_.isFunction(this._afterPublishDestroy)) { this._afterPublishDestroy(id, req, options); } }, /** * _publishAdd * * @param {[type]} id [description] * @param {[type]} alias [description] * @param {[type]} idAdded [description] * @param {[type]} socketToOmit [description] */ _publishAdd: function(id, alias, added, req, options) { var reverseAssociation; // Make sure there's an options object options = options || {}; // Enforce valid usage var invalidId = !id || _.isObject(id); var invalidAlias = !alias || !_.isString(alias); var invalidAddedId = !added || _.isArray(added); if ( invalidId || invalidAlias || invalidAddedId ) { return sails.log.error( 'Invalid usage of `' + this.identity + '._publishAdd(id, alias, idAdded|recordAdded, [socketToOmit])`' ); } // Get the model on the opposite side of the association var reverseModel = sails.models[_.find(this.associations, {alias: alias}).collection]; // Determine whether `added` was provided as a pk value or an object var idAdded; // If it is a pk value, we'll turn it into `idAdded`: if (!_.isObject(added)) { idAdded = added; added = undefined; } // Otherwise we'll leave it as `added` for use below, and determine `idAdded` by examining the object // using our knowledge of what the name of the primary key attribute is. else { idAdded = added[reverseModel.primaryKey]; // If we don't find a primary key value, we'll log an error and return early. if (!_.isString(idAdded) && !_.isNumber(idAdded)) { sails.log.error( 'Invalid usage of _publishAdd(): expected object provided '+ 'for `recordAdded` to have a `%s` attribute', reverseModel.primaryKey ); return; } } // Coerce id to match the attribute type of the primary key of the model id = parseId.apply(this,[id]); // Coerce idAdded to match the attribute type of the primary key of the reverse model idAdded = parseId.apply(reverseModel,[idAdded]); // Lifecycle event if (_.isFunction(this._beforePublishAdd)) { this._beforePublishAdd(id, alias, idAdded, req); } // If a request object was sent, get its socket, otherwise assume a socket was sent. var socketToOmit = (req && req.socket ? req.socket : req); this._publishRPS(id, (function (){ var event = { id: id, verb: 'addedTo', attribute: alias, addedId: idAdded }; if (added) { event.added = added; } return event; })(), socketToOmit); if (!options.noReverse) { var data; // Subscribe to the model you're adding if (req && req.isSocket) { data = {}; data[reverseModel.primaryKey] = idAdded; reverseModel.subscribe(req, [idAdded]); } // Find the reverse association, if any reverseAssociation = _.find(reverseModel.associations, {alias: _.find(this.associations, {alias: alias}).via}); if (reverseAssociation) { // If this is a many-to-many association, do a _publishAdd for the // other side. if (reverseAssociation.type === 'collection') { reverseModel._publishAdd(idAdded, reverseAssociation.alias, id, req, {noReverse:true}); } // Otherwise, do a _publishUpdate else { data = {}; data[reverseAssociation.alias] = id; reverseModel._publishUpdate(idAdded, data, req, {noReverse:true}); } } } if (_.isFunction(this._afterPublishAdd)) { this._afterPublishAdd(id, alias, idAdded, req); } }, /** * _publishRemove * * @param {[type]} id [description] * @param {[type]} alias [description] * @param {[type]} idRemoved [description] * @param {[type]} socketToOmit [description] */ _publishRemove: function(id, alias, idRemoved, req, options) { var reverseAssociation; // Make sure there's an options object options = options || {}; // Enforce valid usage var invalidId = !id || _.isObject(id); var invalidAlias = !alias || !_.isString(alias); var invalidRemovedId = !idRemoved || _.isObject(idRemoved); if ( invalidId || invalidAlias || invalidRemovedId ) { return sails.log.error( 'Invalid usage of `' + this.identity + '._publishRemove(id, alias, idRemoved, [socketToOmit])`' ); } if (_.isFunction(this._beforePublishRemove)) { this._beforePublishRemove(id, alias, idRemoved, req); } // Get the reverse model. var reverseModel = sails.models[_.find(this.associations, {alias: alias}).collection]; // Coerce id to match the attribute type of the primary key of the model id = parseId.apply(this,[id]); // Coerce idRemoved to match the attribute type of the primary key of the reverse model idRemoved = parseId.apply(reverseModel,[idRemoved]); // If a request object was sent, get its socket, otherwise assume a socket was sent. var socketToOmit = (req && req.socket ? req.socket : req); this._publishRPS(id, { id: id, verb: 'removedFrom', attribute: alias, removedId: idRemoved }, socketToOmit); if (!options.noReverse) { // Get the reverse association, if any. reverseAssociation = _.find(reverseModel.associations, {alias: _.find(this.associations, {alias: alias}).via}); if (reverseAssociation) { // If this is a many-to-many association, do a _publishAdd for the // other side. if (reverseAssociation.type === 'collection') { reverseModel._publishRemove(idRemoved, reverseAssociation.alias, id, req, {noReverse:true}); } // Otherwise, do a _publishUpdate else { var data = {}; data[reverseAssociation.alias] = null; reverseModel._publishUpdate(idRemoved, data, req, {noReverse:true}); } } } if (_.isFunction(this._afterPublishRemove)) { this._afterPublishRemove(id, alias, idRemoved, req); } }, /** * Publish the creation of model or an array of models * * @param {[Object]|Object} models * - the data to publish * * @param {Request|Socket} req - Optional request for broadcast. * @api private */ _publishCreate: function(models, req, options){ var self = this; // Pluralize so we can use this method regardless of it is an array or not models = pluralize.apply(this, [models]); //Publish all models _.each(models, function(values){ self._publishCreateSingle(values, req, options); }); }, /** * Publish the creation of a model * * @param {Object} values * - the data to publish * * @param {Request|Socket} req - if specified, broadcast using this socket (effectively omitting it) * @api private */ _publishCreateSingle: function(values, req, options) { var reverseAssociation; options = options || {}; if (_.isUndefined(values[this.primaryKey])) { return sails.log.error( 'Invalid usage of _publishCreate() :: ' + 'Values must have an `'+this.primaryKey+'`, instead got ::\n' + util.inspect(values) ); } if (_.isFunction(this._beforePublishCreate)) { this._beforePublishCreate(values, req); } var id = values[this.primaryKey]; // Coerce id to match the attribute type of the primary key of the model id = parseId.apply(this,[id]); // If any of the added values were association attributes, publish add or remove messages. _.each(values, function(val, key) { // If the user hasn't yet given this association a value, bail out if (val === null) { return; } var association = _.find(this.associations, {alias: key}); // If the attribute isn't an assoctiation, return if (!association) { return; } // Get the associated model class var ReferencedModel = sails.models[association.type === 'model' ? association.model : association.collection]; // Bail if the model doesn't exist if (!ReferencedModel) { return; } // Bail if this attribute isn't in the model's schema if (association.type === 'model') { // Get the inverse association definition, if any reverseAssociation = _.find(ReferencedModel.associations, {collection: this.identity, via: key}) || _.find(ReferencedModel.associations, {model: this.identity, via: key}); if (!reverseAssociation) {return;} // If this is a to-many association, do _publishAdd on the other side if (reverseAssociation.type === 'collection') { ReferencedModel._publishAdd( // Depending on the `populate` setting, the val could be an object or a primary key, // so we'll allow for both. val[ReferencedModel.primaryKey] || val, reverseAssociation.alias, id, req, {noReverse:true} ); } } else { // Get the inverse association definition, if any reverseAssociation = _.find(ReferencedModel.associations, {collection: this.identity, via: key}) || _.find(ReferencedModel.associations, {model: this.identity, alias: association.via}); if (!reverseAssociation) {return;} // If this is a to-many association, do publishAdds on the other side if (reverseAssociation.type === 'collection') { // Alert any added models _.each(val, function(pk) { // Depending on the `populate` setting, the val could be an object or a primary key, // so we'll allow for both. if (_.isObject(pk)) { pk = pk[ReferencedModel.primaryKey]; } ReferencedModel._publishAdd(pk, reverseAssociation.alias, id, req, {noReverse:true}); }); } // Otherwise do a _publishUpdate else { // Alert any added models _.each(val, function(pk) { // Depending on the `populate` setting, the val could be an object or a primary key, // so we'll allow for both. if (_.isObject(pk)) { pk = pk[ReferencedModel.primaryKey]; } var pubData = {}; pubData[reverseAssociation.alias] = id; ReferencedModel._publishUpdate(pk, pubData, req, {noReverse:true}); }); } } }, this); // Ensure that we're working with a plain object values = _.clone(values); // If a request object was sent, get its socket, otherwise assume a socket was sent. var socketToOmit = (req && req.socket ? req.socket : req); // Publish to classroom var payload = { verb: 'created', data: values, id: values[this.primaryKey] }; sails.log.silly('Published message to ', this._classRoom(), ': ', payload); var eventName = this.identity; sails.sockets.broadcast(this._classRoom(), eventName, payload, socketToOmit); // Subscribe watchers to the new instance if (!options.noIntroduce) { this._introduce(values[this.primaryKey]); } if (_.isFunction(this._afterPublishCreate)) { this._afterPublishCreate(values, req); } }, /** * * @return {[type]} [description] */ _watch: function ( req ) { var socket = sails.sockets.parseSocket(req); if (!socket) { sails.log.debug('`Model._watch()` called by a non-socket request. Only requests originating from a connected socket may be subscribed. Ignoring...'); return; }//-• sails.sockets.join(socket, this._classRoom()); sails.log.silly('Subscribed socket ', sails.sockets.getId(socket), 'to', this._classRoom()); }, /** * Introduce a new instance * * Take all of the subscribers to the class room and 'introduce' them * to a new instance room * * @param {String|Finite} id * - primary key of the instance we're referring to * * @api private */ _introduce: function(model) { var self = this; // Get the instance ID var id = model[this.primaryKey] || model; // Use addRoomMembersToRooms to subscribe everyone in the class room to the model identity instance room sails.sockets.addRoomMembersToRooms(self._classRoom(), self._room(id) ); }, /** * Bid farewell to a destroyed instance * Take all of the socket subscribers in this instance room * and unsubscribe them from it */ _retire: function(model) { var self = this; // Get the instance ID var id = model[this.primaryKey] || model; // Use removeRoomMembersFromRooms to unsubscribe everyone in the class room from the model identity instance room sails.sockets.removeRoomMembersFromRooms(self._classRoom(), self._room(id) ); } }; } }; ================================================ FILE: lib/hooks/request/README.md ================================================ # request (Core Hook) > In future releases, the various responsibilities of this hook will likely be farmed out to other hooks and/or pulled into core. ## Purpose This hook's responsibilities are: ##### Add properties to `req` + req.params.all() + req.wantsJSON() + req.explicitlyAcceptsHTML() + req.baseUrl + req.port + req._sails (access to the app's `sails` object in case it's not global) ##### Set default view locals (i.e. `app.locals`) + `_` (lodash) + `session` + `req` + `res` + `sails` > Note that this will likely move into the `views` hook in the future. ## FAQ > If you have a question that isn't covered here, please feel free to send a PR adding it to this section (even if you don't have the answer!) ================================================ FILE: lib/hooks/request/index.js ================================================ /** * Module dependencies. */ var _mixinLocals = require('./locals'); var _mixinReqParam = require('./param'); var _mixinReqParamsAll = require('./params.all'); var _mixinServerMetadata = require('./metadata'); var _mixinReqQualifiers = require('./qualifiers'); var _mixinReqValidate = require('./validate'); /* NOTE: Most of the contents of this file could be eventually migrated into the prototypes of the `req` and `res` objects we're extending from our Express router (`_privateRouter`). This would also need to happen separately in the HTTP hook (since its req and res are distinct), which is why adding the methods via middleware has been a perfectly convenient abstraction for the time being. However, this can be rather hard to understand, and as we make an effort to make hooks easier to work with, it may be wise to abstract these built-in Sails functions in a more declarative way, maybe even outside of hooks altogether. This is particularly pertinent in the case of errors ( e.g. res.serverError() ). If you have any ideas, please let me know! (@mikermcneil) */ module.exports = function(sails) { /** * Extend middleware req/res for this route w/ new methods / qualifiers. */ return { defaults: { }, /** * Bind req/res syntactic sugar before applying any app-level routes */ initialize: function(cb) { // Bind an event handler to inject logic before running each individual // middleware function within a route/request. sails.on('router:route', function(requestState) { // ***************************************************************** // Warning: this is a hot code path! // Remember to be sensitive to performance. // ***************************************************************** var req = requestState.req; var res = requestState.res; // req.params.all() must be recalculated before matching each route // since path params (`req.params`) might have changed. _mixinReqParamsAll(req, res); // Since req.param() is deprecated with Express4 we use this facade _mixinReqParam(req, res); }); return cb(); }, routes: { before: { 'all /*': function addMixins (req, res, next) { // Provide access to `sails` object req._sails = req._sails || sails; // Add a few `res.locals` by default _mixinLocals(req, res); // Add information about the server to the request context _mixinServerMetadata(req, res); // Add `req.validate()` method // (Warning: this is actually just an error as of Sails v1.0! See impl for more info.) _mixinReqValidate(req, res); // Only apply HTTP-focused middleware if it makes sense // (i.e. if this is an HTTP request) if (req.protocol === 'http' || req.protocol === 'https') { _mixinReqQualifiers(req, res); } return next(); }//< / 'all /*' > } } }; }; ================================================ FILE: lib/hooks/request/locals.js ================================================ /** * Module dependencies */ var _ = require('@sailshq/lodash'); /** * default locals * * Always share some basic metadata with views. * Roughly analogous to `app.locals` in Express. * * > Application local variables are provided to all templates rendered * > within the application. This is useful for providing helper functions * > to templates, as well as app-level data. * > * > http://expressjs.com/api.html#app.locals * * @param {Request} req * @param {Response} res * @api private */ module.exports = function _mixinLocals(req, res) { // TODO: // Actually take advantage of `app.locals` // for this logic. // TODO: // we might look at pruning the stuff being // passed in here, to improve the optimizations // of Express's production view cache. _.extend(res.locals, { _: _, session: req.session, req: req, res: res, sails: req._sails }); }; ================================================ FILE: lib/hooks/request/metadata.js ================================================ /** * Module dependencies */ var _ = require('@sailshq/lodash'); /** * _mixinServerMetadata() * * Set server metadata on the specified `req`, mutating it in-place. * (Host, port, etc.) * * This is for ALL requests, virtual or not. * * @param {Request} req * * @api private */ module.exports = function _mixinServerMetadata(req) { // Get reference to `sails` (Sails app instance) for convenience. var sails = req._sails; // FUTURE: bring this back, probably. // (but note that it wasn't actually being used anyway as of Sails v0.12-- was being overridden below) // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // // Access to server port, if available // if (sails.hooks.http) { // var nodeHTTPServer = sails.hooks.http.server; // var nodeHTTPServerAddress = (nodeHTTPServer && nodeHTTPServer.address && nodeHTTPServer.address()) || {}; // req.port = req.port || (nodeHTTPServerAddress && nodeHTTPServerAddress.port) || 80; // } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // Set req.port and req.baseUrl using the Host header and req.protocol // // We trust req.protocol to be set by Express when "trust proxy" is enabled. // But Express only delivers the host devoid of a port, so we have to delve into // HTTP headers to pick out the host port ourselves. // // FUTURE: revisit this ^^ var trustProxy; if (req.app && req.app.get('trust proxy')) { trustProxy = req.app.get('trust proxy'); } else if (sails.hooks.http && sails.config.http.trustProxy) { // If this is a virtual request, then "trust proxy" will not have been set // on the current `req.app`, but we still want to consider this the same case // if the sails.config.http.trustProxy was enabled. trustProxy = sails.config.http.trustProxy; } else { trustProxy = false; } if (!_.isFunction(req.get)) { throw new Error('Consistency violation: At this point (in the request hook), req.get() should always exist as a function.'); } // Determine host. var host = ''; var xForwardedHostHeader = req.get('X-Forwarded-Host'); var hostHeader = req.get('Host'); if (trustProxy && xForwardedHostHeader) { // FUTURE: use hostname-- because if trustProxy was configured, it means that we should be able to (as of E4) host = xForwardedHostHeader.split(/,\s*/)[0]; } else if (hostHeader) { host = hostHeader; } else { host = 'could.not.determine.host'; } // Determine host port // (FUTURE: come back to this, esp insofar as it affects virtual requests -- we need to respect trustProxy) var defaultPort; if (req.protocol === 'https' || req.protocol === 'wss') { defaultPort = 443; } else { defaultPort = 80; } var hostPort = parseInt(host.split(/:/)[1], 10) || defaultPort; req.port = hostPort; // Determine appropriate baseUrl // (FUTURE: come back to this, esp insofar as it affects virtual requests -- we need to respect trustProxy) req.baseUrl = req.protocol + '://' + host; }; ================================================ FILE: lib/hooks/request/param.js ================================================ /** * _mixinReqParam * * Facade for req.param('sth') with Express4 * Looking for the param in req.params && req.query && req.body * * Note: this has to be applied per-route, not per request, * in order to refer to the proper route/path parameters * * @param {Request} req * @param {Response} res * @api private */ module.exports = function _mixinReqParam(req /*, res */) { req.param = function(param, defaultValue) { // If the param exists as a route param, use it. if (typeof req.params[param] !== 'undefined') { return req.params[param]; } // If the param exists as a body param, use it. if (req.body && typeof req.body[param] !== 'undefined') { return req.body[param]; } // Return the query param, if it exists. return typeof req.query[param] !== 'undefined' ? req.query[param] : defaultValue; }; }; ================================================ FILE: lib/hooks/request/params.all.js ================================================ /** * Module dependencies */ var _ = require('@sailshq/lodash'); /** * _mixinReqParamsAll * * Mixes in `req.params.all()`, a convenience method to grab all parameters, * whether they're in the path (req.params), query string (req.query), * or request body (req.body). * * Note: this has to be determined per-route, not per request, * in order to refer to the proper route/path parameters. * e.g. if a request comes in and matches `GET /about` AND `GET /:username` * then the set of all params varies depending on which handler is being run. * Now that said, since changing the implementation to avoid precalculating * unless allParams is actually called, this may not necessarily need to be * the case anymore. More exhaustive testing would be required to make that change. * * * @param {Request} req * @param {Response} res * @api private */ module.exports = function _mixinReqParamsAll(req /*, res */) { // Add `req.allParams()` method. req.allParams = function () { // Combines parameters from the query string, and encoded request body // to compose a monolithic object of named parameters, irrespective of source var allParams = _.extend({}, req.query, req.body); // Mixin route params, as long as they have defined values _.each(Object.keys(req.params), function(paramName) { if (allParams[paramName] || !_.isUndefined(req.params[paramName])) { allParams[paramName] = !_.isUndefined(req.params[paramName]) ? req.params[paramName] : allParams[paramName]; } }); return allParams; }; ///////////////////////////////////////////////////////////////////////////////////////////////////////// // Note: // req.params.all() was removed in Sails v1.0 in favor of `req.allParams()`. ///////////////////////////////////////////////////////////////////////////////////////////////////////// // // The following code was removed for performance reasons. // (Object.defineProperty() is slow-- see `parley` benchmarks + commit history c.a. Oct-Dec 2016) ///////////////////////////////////////////////////////////////////////////////////////////////////////// // // Define a new non-enumerable property: req.params.all() // // and make it a synonym to `req.allParams()` // // (but only if `req.params.all` doesn't already exist!) // if (!req.params.all) { // Object.defineProperty(req.params, 'all', { // value: function (){ // throw new Error('req.params.all() is no longer supported as of Sails v1.0. Please use req.allParams() instead.'); // } // }); // } ///////////////////////////////////////////////////////////////////////////////////////////////////////// }; ================================================ FILE: lib/hooks/request/qualifiers.js ================================================ /** * Module dependencies */ var STRINGFILE = require('sails-stringfile'); /** * Mix in convenience flags about this request * * @param {Request} req * @param {Response} res * @api private */ module.exports = function _mixinReqQualifiers(req, res) { var accept = req.get('Accept') || ''; // Flag indicating whether HTML was explicitly mentioned in the Accepts header req.explicitlyAcceptsHTML = (accept.indexOf('html') !== -1); // Flag indicating whether a request would like to receive a JSON response // // This qualification is determined based on a handful of heuristics, including: // • if this looks like an AJAX request // • if this is a virtual request from a socket // • if this request DOESN'T explicitly want HTML // • if this request has a "json" content-type AND ALSO has its "Accept" header set // • if this request has the option "wantsJSON" set req.wantsJSON = req.xhr; req.wantsJSON = req.wantsJSON || req.isSocket; req.wantsJSON = req.wantsJSON || !req.explicitlyAcceptsHTML; req.wantsJSON = req.wantsJSON || (req.is('json') && req.get('Accept')); req.wantsJSON = req.wantsJSON || req.options.wantsJSON; // Deprecated properties bindReqDeprecationNotice(req, 'isJson'); bindReqDeprecationNotice(req, 'isAjax'); bindResDeprecationNotice(res, 'viewExists'); }; /** * Bind deprecation notices for `req.*` properties from 0.8.x, * but only in development env, and only if the property * doesn't already exist (i.e. in case a user-defined * hook bound it on the `req` object.) * * @param {Request} req * @param {String} key */ function bindReqDeprecationNotice(req, key) { if (process.env.NODE_ENV === 'production' || req[key]) { return; } // Attach a getter Object.defineProperty(req, key, { value: function showDeprecationNotice() { var e = STRINGFILE.get('upgrade.req.' + key); throw new Error(e); } }); } /** * Bind deprecation notices for `res.*` properties from 0.8.x, * but only in development env, and only if the property * doesn't already exist (i.e. in case a user-defined * hook bound it on the `res` object.) * * @param {Response} res * @param {String} key */ function bindResDeprecationNotice(res, key) { if (process.env.NODE_ENV === 'production' || res[key]) { return; } // Attach a getter Object.defineProperty(res, key, { value: function showDeprecationNotice() { var e = STRINGFILE.get('upgrade.res.' + key); throw new Error(e); } }); } ================================================ FILE: lib/hooks/request/validate.js ================================================ /** * Module dependencies */ var flaverr = require('flaverr'); /** * Mixes in `req.validate`. * * @param {Request} req * @param {Response} res * @return {Request} * * Note that built-in support for req.validate() has changed in Sails v1.0 * (alongwith other major changes to `anchor`.) */ module.exports = function (req /*, res */) { /** * req.validate() * * @param {Object} usage * (supports either `{type: {}}` or `{}`) * * @param {String} redirectTo * (optional) * * @throws {Error} * @api deprecated */ req.validate = function (/* usage, redirectTo */) { throw flaverr({ code: 'E_REQ_VALIDATE_UNSUPPORTED' }, new Error('As of Sails v1, `req.validate` is no longer supported. Instead use actions2: http://sailsjs.com/docs/concepts/controllers')); };//</function definition :: req.validate> return req; }; ================================================ FILE: lib/hooks/responses/README.md ================================================ # responses (Core Hook) ## Status > ##### Stability: [2](https://github.com/balderdashy/sails-docs/blob/master/contributing/stability-index.md) - Stable ## Dependencies In order for this hook to load, the following other hooks must have already finished loading: - moduleloader ## Dependents If this hook is disabled, in order for Sails to load, the following other core hooks must also be disabled: - blueprints ## Purpose This hook's responsibilities are: ##### Support `response` route target syntax This hook listens for the `route:typeUnknown` event, and if the unknown route target syntax contains a `response` key, it binds the route address to a middleware function that does nothing except send that response. For example: ``` 'post /foo': { response: 'ok' } ``` ...would run `res.ok()` whenever a POST request to `/foo` is received. ##### Load custom responses When Sails loads, this hook loads custom response files from the app's responses folder and merges them with built-in defaults, storing them in-memory as "outlet functions". Conventionally this is `api/responses/*.js`, but it can be configured in `sails.config.paths`. ##### Bind shadow route that exposes response functions as `res.*` This hook binds a shadow route that intercepts all incoming requests and attaches a method to `res` for each of the outlet functions (representing custom responses) that were prepared when the hook was initialized. ## Implicit Defaults This hook sets the following implicit default configuration on `sails.config`: _N/A_ ## Events ##### `hook:responses:loaded` Emitted when this hook has been automatically loaded by Sails core, and triggered the callback in its `initialize` function. ## Methods #### sails.hooks.responses.loadModules() Load custom responses modules from the responses directory in the current app (conventionally this is `api/responses/`). ```javascript sails.hooks.responses.loadModules(cb); ``` ###### Usage | | Argument | Type | Details | --- | --------------------------- | ------------------- | ---------------------------------------------------------------------------------- | 1 | **cb** | ((function)) | Fires when the custom response modules have been loaded or if an error occurs. > ##### API: Private > - Please do not use this method in userland (i.e. in your app or even in a custom hook or other type of Sails plugin). > - Because it is a private API of a core hook, if you use this method in your code it may stop working or change without warning, at any time. > - If you would like to see a version of this method made public and its API stabilized, please open a [proposal](https://github.com/balderdashy/sails/blob/master/CONTRIBUTING.md#v-proposing-features-and-enhancements). > > _(internally in core, note that this is called by the `moduleloader` hook)_ ## FAQ > If you have a question about this hook that isn't covered here, please feel free to send a PR adding it to this section (even if you don't have the answer, a core maintainer will merge your PR and add an answer as soon as possible) ================================================ FILE: lib/hooks/responses/defaults/badRequest.js ================================================ /** * Module dependencies */ var util = require('util'); var _ = require('@sailshq/lodash'); /** * 400 (Bad Request) Handler * * Usage: * return res.badRequest(); * return res.badRequest(data); * * e.g.: * ``` * return res.badRequest( * 'Please choose a valid `password` (6-12 characters)', * 'trial/signup' * ); * ``` */ module.exports = function badRequest(data) { // Get access to `req` and `res` var req = this.req; var res = this.res; // Get access to `sails` var sails = req._sails; // Log error to console if (!_.isUndefined(data)) { sails.log.verbose('Sending 400 ("Bad Request") response: \n', data); } // Set status code res.status(400); // If no data was provided, use res.sendStatus(). if (_.isUndefined(data)) { return res.sendStatus(400); } if (_.isError(data)) { // If the data is an Error instance and it doesn't have a custom .toJSON(), // then util.inspect() it instead (otherwise res.json() will turn it into an empty dictionary). // > Note that we don't do this in production, since (depending on your Node.js version) inspecting // > the Error might reveal the `stack`. And since `res.badRequest()` could certainly be used in // > production, we wouldn't want to inadvertently dump a stack trace. if (!_.isFunction(data.toJSON)) { if (process.env.NODE_ENV === 'production') { return res.sendStatus(400); } // No need to JSON stringify (this is already a string). return res.send(util.inspect(data)); } } return res.json(data); }; ================================================ FILE: lib/hooks/responses/defaults/forbidden.js ================================================ /** * Module dependencies */ // n/a /** * 403 (Forbidden) Handler * * Usage: * return res.forbidden(); * * e.g.: * ``` * return res.forbidden(); * ``` */ module.exports = function forbidden () { // Get access to `res` var res = this.res; // Send status code and "Forbidden" message return res.sendStatus(403); }; ================================================ FILE: lib/hooks/responses/defaults/negotiate.js ================================================ /** * Generic Error Handler / Classifier * * Calls the appropriate custom response for a given error, * out of the bundled response modules: * badRequest, forbidden, notFound, & serverError * * Defaults to `res.serverError` * * Usage: * ```javascript * if (err) return res.negotiate(err); * ``` * * @param {*} error(s) * * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * WARNING: THIS FUNCTION IS DEPRECATED! * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ module.exports = function negotiate (err) { // Get access to request (`req`), response (`res`), and Sails app (`sails`). var req = this.req; var res = this.res; var sails = req._sails; sails.log.debug('`res.negotiate` is deprecated. Use a custom response instead (see http://sailsjs.com/docs/concepts/custom-responses).\n'); var statusCode = 500; var body = err; try { statusCode = err.status || 500; // Set the status // (should be taken care of by res.* methods, but this sets a default just in case) res.status(statusCode); } catch (unusedErr) {} // Respond using the appropriate custom response if (statusCode === 403) { return res.forbidden(body); } if (statusCode === 404) { return res.notFound(body); } if (statusCode >= 400 && statusCode < 500) { return res.badRequest(body); } return res.serverError(body); }; ================================================ FILE: lib/hooks/responses/defaults/notFound.js ================================================ /** * Module dependencies */ // n/a /** * 404 (Not Found) Handler * * Usage: * return res.notFound(); * return res.notFound(err); * return res.notFound(err, 'some/specific/notfound/view'); * * e.g.: * ``` * return res.notFound(); * ``` * * NOTE: * If a request doesn't match any explicit routes (i.e. `config/routes.js`) * or route blueprints (i.e. "shadow routes", Sails will call `res.notFound()` * automatically. */ module.exports = function notFound () { // Get access to `req` and `res` var req = this.req; var res = this.res; // Get access to `sails` var sails = req._sails; // Set status code res.status(404); // If the request wants JSON, send back the appropriate status code. if (req.wantsJSON || !res.view) { return res.sendStatus(404); } return res.view('404', {}, function (err, html) { // If a view error occured, fall back to JSON. if (err) { // // Additionally: // • If the view was missing, ignore the error but provide a verbose log. if (err.code === 'E_VIEW_FAILED') { sails.log.verbose('res.notFound() :: Could not locate view for error page (sending text instead). Details: ', err); } // Otherwise, if this was a more serious error, log to the console with the details. else { sails.log.warn('res.notFound() :: When attempting to render error page view, an error occured (sending text instead). Details: ', err); } return res.sendStatus(404); } return res.send(html); }); }; ================================================ FILE: lib/hooks/responses/defaults/ok.js ================================================ /** * Module dependencies */ var util = require('util'); var _ = require('@sailshq/lodash'); /** * 200 (OK) Response * * Usage: * return res.ok(); * return res.ok(data); * * @param {JSON?} data * @param {Ref?} noLongerSupported */ module.exports = function sendOK (data, noLongerSupported) { // Get access to `req` and `res` var req = this.req; var res = this.res; // Get access to `sails` var sails = req._sails; // If a second argument was given, log a message. if (noLongerSupported) { sails.log.debug('The second argument to `res.ok()` is deprecated.'); sails.log.debug('To serve a view via `res.ok()`, override the response'); sails.log.debug('in \'api/responses/ok.js\'.\n'); } // Set status code res.status(200); // If no data was provided, use res.sendStatus(). if (_.isUndefined(data)) { return res.sendStatus(200); } // Extreme edge case (very rare to pass an Error into res.ok() -- but still, just in case) // If the data is an Error instance and it doesn't have a custom .toJSON(), // then util.inspect() it instead (otherwise res.json() will turn it into an empty dictionary). // > Note that we don't do this in production, since (depending on your Node.js version) inspecting // > the Error might reveal the `stack`. And since `res.ok()` could certainly be used in // > production, and it could inadvertently be passed an Error instance, we censor the stack trace // > as a simple failsafe. if (_.isError(data)) { if (!_.isFunction(data.toJSON)) { if (process.env.NODE_ENV === 'production') { return res.sendStatus(200); } // No need to JSON stringify (it's already a string). return res.send(util.inspect(data)); } } return res.json(data); }; ================================================ FILE: lib/hooks/responses/defaults/serverError.js ================================================ /** * Module dependencies */ var _ = require('@sailshq/lodash'); var flaverr = require('flaverr'); /** * 500 (Server Error) Response * * Usage: * return res.serverError(); * return res.serverError(err); * return res.serverError(err, 'some/specific/error/view'); * * NOTE: * If something throws in a policy or controller, or an internal * error is encountered, Sails will call `res.serverError()` * automatically. */ module.exports = function serverError (data) { // Get access to `req` and `res` var req = this.req; var res = this.res; // Get access to `sails` var sails = req._sails; // Log error to console if (data !== undefined) { sails.log.error('Sending 500 ("Server Error") response: \n', flaverr.parseError(data) || data); } // Don't output error data with response in production. var dontRevealErrorInResponse = process.env.NODE_ENV === 'production'; if (dontRevealErrorInResponse) { data = undefined; } // Set status code res.status(500); // If appropriate, serve data as JSON. if (req.wantsJSON || !res.view) { // If no data was provided, use res.sendStatus(). if (data === undefined) { return res.sendStatus(500); } // If the data is an error instance and it doesn't have a custom .toJSON(), // use its stack instead (otherwise res.json() will turn it into an empty dictionary). if (_.isError(data)) { if (!_.isFunction(data.toJSON)) { data = data.stack; // No need to stringify the stack (it's already a string). return res.send(data); } } return res.json(data); } return res.view('500', { error: data }, function (err, html) { // If a view error occured, fall back to JSON. if (err) { // // Additionally: // • If the view was missing, ignore the error but provide a verbose log. if (err.code === 'E_VIEW_FAILED') { sails.log.verbose( 'res.serverError() :: Could not locate view for error page'+ (dontRevealErrorInResponse? '':' (sending JSON instead)')+'. '+ 'Details: ', err ); } // Otherwise, if this was a more serious error, log to the console with the details. else { sails.log.warn( 'res.serverError() :: When attempting to render error page view, '+ 'an error occured'+(dontRevealErrorInResponse? '':' (sending JSON instead)')+'. '+ 'Details: ', err ); } return res.json(data); } return res.send(html); }); }; ================================================ FILE: lib/hooks/responses/index.js ================================================ /** * Dependencies */ var _ = require('@sailshq/lodash'); var chalk = require('chalk'); var STRINGFILE = require('sails-stringfile'); var Err = require('../../../errors/fatal'); var onRoute = require('./onRoute'); /** * Expose hook definition */ module.exports = function(sails) { return { defaults: {}, configure: function() { // Legacy (< v0.10) support for configured handlers if (typeof sails.config[500] === 'function') { sails.after('lifted', function() { STRINGFILE.logDeprecationNotice('sails.config[500]', STRINGFILE.get('links.docs.migrationGuide.responses'), sails.log.debug); sails.log.debug('sails.config[500] (i.e. `config/500.js`) has been superceded in Sails v0.10.'); sails.log.debug('Please define a "response" instead. (i.e. api/responses/serverError.js)'); sails.log.debug('Your old handler is being ignored. (the format has been upgraded in v0.10)'); sails.log.debug('If you\'d like to use the default handler, just remove this configuration option.'); }); } if (typeof sails.config[404] === 'function') { sails.after('lifted', function() { STRINGFILE.logDeprecationNotice('sails.config[404]', STRINGFILE.get('links.docs.migrationGuide.responses'), sails.log.debug); sails.log.debug('Please define a "response" instead. (i.e. api/responses/notFound.js)'); sails.log.debug('Your old handler is being ignored. (the format has been upgraded in v0.10)'); sails.log.debug('If you\'d like to use the default handler, just remove this configuration option.'); }); } }, /** * When this hook is loaded... */ initialize: function(cb) { // Register route syntax that allows explicit routes // to be bound directly to custom responses by name. // (e.g. {response: 'foo'}) sails.on('route:typeUnknown', onRoute(sails)); cb(); }, /** * Fetch relevant modules, exposing them on `sails` subglobal if necessary, */ loadModules: function(cb) { var hook = this; sails.log.silly('Loading runtime custom response definitions...'); sails.modules.loadResponses(function loadedRuntimeErrorModules(err, responseDefs) { if (err) { return cb(err); } // Check that none of the custom responses provided from userland collie with // reserved response methods/properties. // // Note: this could be made more flexible in the future-- I've found it to be // helpful to sometimes override res.view() in apps. That said, in those circumstances, // I've been able to accomplish this by manually overriding res.view in a custom hook. // That said, if that won't work for your use case, please let me know (tweet @mikermcneil). var reservedResKeys = [ 'view', 'status', 'set', 'get', 'cookie', 'clearCookie', 'redirect', 'location', 'charset', 'send', 'json', 'jsonp', 'type', 'format', 'attachment', 'sendfile', 'download', 'links', 'locals', 'render' ]; _.each(responseDefs, function (responseDef, customResponseKey) { if ( _.contains(reservedResKeys, customResponseKey) ) { Err.invalidCustomResponse(customResponseKey); } }); // Mix in the built-in default definitions for custom responses. _.defaults(responseDefs, { ok: require('./defaults/ok'), negotiate: require('./defaults/negotiate'), notFound: require('./defaults/notFound'), serverError: require('./defaults/serverError'), forbidden: require('./defaults/forbidden'), badRequest: require('./defaults/badRequest') }); // Expose combined custom/default response method definitions on the hook. // (e.g. `serverError`, `notFound`, `ok`, etc.) // TODO: use this instead of exposing as "middleware", since that's confusing naming. // Register blueprint actions as middleware of this hook. hook.middleware = responseDefs; return cb(); }); }, /** * Shadow route bindings * @type {Object} */ routes: { before: { /** * Add custom response methods to `res`. * * @param {Request} req * @param {Response} res * @param {Function} next * @api private */ 'all /*': function addResponseMethods(req, res, next) { // Attach res.jsonx to `res` object _mixinJsonxMethod(req, res); // Attach custom responses to `res` object // Provide access to `req` and `res` in each of their `this` contexts. _.each(sails.middleware.responses, function eachMethod(responseFn, name) { res[name] = responseFn.bind({ req: req, res: res }); }); // Proceed! return next(); } } } }; }; /** * [_mixinJsonxMethod description] * @param {[type]} req [description] * @param {[type]} res [description] * @return {[type]} [description] */ function _mixinJsonxMethod(req, res) { function _stringifyJsonxError(err) { var plainObject = {}; Object.getOwnPropertyNames(err).forEach(function (key) { plainObject[key] = err[key]; }); return JSON.stringify(plainObject); } function _handleJsonxError(err){ var serializedErr; var jsonSerializedErr; try { serializedErr = _stringifyJsonxError(err); jsonSerializedErr = JSON.parse(serializedErr); if (!jsonSerializedErr.stack || !jsonSerializedErr.message) { jsonSerializedErr.message = err.message; jsonSerializedErr.stack = err.stack; } return jsonSerializedErr; } catch (unusedErr){ return {name: err.name, message: err.message, stack: err.stack}; } } /** * res.jsonx(data) * * Serve JSON (and allow JSONP if enabled in `req.options`) * * @param {Object} data * * - - - - - - - - - - - - - - - - - - * WARNING: THIS IS DEPRECATED! * - - - - - - - - - - - - - - - - - - */ res.jsonx = res.jsonx || function jsonx (data){ var caller = jsonx.caller && jsonx.caller.name; // Get easy access to Sails app instance. var sails = req._sails; // Log a deprecation notice. sails.log.debug('***********************************************************************************'); sails.log.debug('`res.jsonx()` is deprecated in Sails v1.0 and will be removed in a future release.'); sails.log.debug(chalk.bold('Any files in `api/responses/` that haven\'t been customized can simply be removed.')); sails.log.debug(chalk.gray('Otherwise see http://sailsjs.com/upgrading for options to replace `res.jsonx()`.')); if (caller) { sails.log.debug(chalk.gray('(jsonx was called from `' + caller + '`)')); } sails.log.debug('***********************************************************************************'); // Send conventional status message if no data was provided // (see http://expressjs.com/en/api.html#res.send) if (_.isUndefined(data)) { return res.status(res.statusCode).send(); } else if (typeof data !== 'object') { // (note that this guard includes arrays, functions, and even `null`) return res.send(data); } // When responding with an Error instance, if it's going to get sringified into // a dictionary with no `.stack` or `.message` properties, add them in. if (data instanceof Error) { data = _handleJsonxError(data); } if ( req.options.jsonp && !req.isSocket ) { return res.jsonp(data); } else { return res.json(data); } };//ƒ }//ƒ // Note for later // We could differentiate between 500 (generic error message) // and 504 (gateway did not receive response from upstream server) which could describe an IO problem // This is worth having a think about, since there are 2 fundamentally different kinds of "server errors": // (a) An infrastructural issue, or 504 (e.g. MySQL database randomly crashed or Twitter is down) // (b) Unexpected bug in app code, or 500 (e.g. `req.session.user.id`, but `req.session.user` doesn't exist) // // See the Sails project roadmap (sailsjs.com/roadmap) for future plans to better account for this. ================================================ FILE: lib/hooks/responses/onRoute.js ================================================ module.exports = function (sails) { /** * Handle `route:typeUnknown` events. * This "teaches" the router to understand `response` in route target syntax. * This allows route addresses to be bound directly to one of this Sails app's response modules. * (i.e. usually defined in your app's `api/responses/` folder, or in the case of res.ok(), * res.serverError(), etc., provided by default from Sails core) * * e.g. * ``` * 'get /admin/sweet-dashboard-or-report-or-something': { response: 'notImplemented' } * ``` * * @param {Dictionary} route * route target definition */ return function onRoute (route) { // If we have a matching response, use it. if (route.target && route.target.response) { if (sails.middleware.responses[route.target.response]) { sails.log.silly('Binding response ('+route.target.response+') to '+route.verb+' '+route.path); sails.router.bind(route.path, function(req, res) { res[route.target.response](); }, route.verb, route.options); } // Invalid respose? Ignore and continue. else { sails.log.error(route.target.response +' :: ' + 'Ignoring invalid attempt to bind route to an undefined response:', 'for path: ', route.path, route.verb ? ('and verb: ' + route.verb) : ''); return; } } }; }; ================================================ FILE: lib/hooks/security/README.md ================================================ # security (Core Hook) ## Status > ##### Stability: [2](https://github.com/balderdashy/sails-docs/blob/master/contributing/stability-index.md) - Stable ## Dependencies In order for this hook to load, the following other hooks must have already finished loading: - moduleloader - userconfig ## Dependents If this hook is disabled, in order for Sails to load, the following other core hooks must also be disabled: _N/A_ ## Purpose This hook's responsibilities are: ##### Bind shadow routes to set appropriate CORS headers When Sails loads, this hook binds a `router:before` listener so that it can bind routes before the router binds explicit routes. Then it binds shadow routes for the appropriate endpoints based on `sails.config.cors` (also mixing in its implicit defaults). ##### Sets up CRSF action It generates `security/grant-csrf-token` action ## Implicit Defaults This hook sets the following implicit default configuration on `sails.config.security`: | Property | Type | Default | |-----------------------------------------------|:-------------:|-----------------| | `sails.config.security.cors.allowOrigins` | ((string)) | `'*'` | `sails.config.security.cors.allRoutes` | ((boolean)) | `false` | `sails.config.security.cors.allowCredentials` | ((boolean)) | `false` | `sails.config.security.cors.allowRequestMethods` | ((string)) | `'GET, HEAD, PUT, PATCH, POST, DELETE'` | `sails.config.security.cors.allowRequestHeaders` | ((string)) | `'content-type'` | `sails.config.security.cors.allowResponseHeaders` | ((string)) | `''` _(empty string)_ | `sails.config.security.cors.allowAnyOriginWithCredentialsUnsafe` | ((boolean)) | `false` | `sails.config.security.csrf` | ((boolean)) | `false` ## Events ##### `hook:security:loaded` Emitted when this hook has been automatically loaded by Sails core, and triggered the callback in its `initialize` function. ## FAQ > If you have a question that isn't covered here, please feel free to send a PR adding it to this section (even if you don't have the answer!) ================================================ FILE: lib/hooks/security/cors/index.js ================================================ module.exports = function(sails) { /** * Module dependencies. */ var _ = require('@sailshq/lodash'); var setHeaders = require('./set-headers'); var setPreflightConfig = require('./set-preflight-config'); var detectVerb = require('../../../util/detect-verb'); /** * Expose hook definition */ return function initializeCors() { // Once it's time to bind shadow routes, get to bindin'. sails.on('router:before', function () { // (FUTURE: consider changing this ^^ to `sails.after()` for consistency) // If we're setting CORS on all routes by default, set up a universal route for it here. // CORS can still be turned off for specific routes by setting `cors:false` if (sails.config.security.cors.allRoutes === true) { sails.router.bind('/*', setHeaders(sails.config.security.cors), 'all', {_middlewareType: 'CORS HOOK: sendHeaders'}); } // Otherwise, default to blocking all cross-origin requests. else { sails.router.bind('/*', setHeaders({allowOrigins: false}), null, {_middlewareType: 'CORS HOOK: clearHeaders'}); } // Declare a var to hold the various CORS settings for preflight OPTIONS routes, which we'll build up // as we look at the route configs below. var optionsRouteConfigs = {}; // Loop through all configured routes, looking for CORS options _.each(sails.router.explicitRoutes, function(routeConfig, route) { // Get some info about the route, like its path and verb. var routeInfo = detectVerb(route); var path = routeInfo.original.toLowerCase(); var verb = routeInfo.verb.toLowerCase(); // Get a handle to the route CORS config. var routeCorsConfig = routeConfig.cors; // If this route doesn't have its own CORS config, move on. if (_.isUndefined(routeCorsConfig)) { return; } // If this route is pointing to the CSRF token route, log a warning. if (routeCorsConfig !== false && routeConfig.action === 'security/grant-csrf-token') { sails.log.verbose('The `grant-csrf-token` action is not supported for cross-origin requests in situations/browsers where 3rd party cookies are blocked.'); sails.log.verbose('(You are seeing this message because the route `' + verb + ' ' + path + '` has CORS settings configured.)'); } optionsRouteConfigs[path] = optionsRouteConfigs[path] || {}; // If cors is set to `true`, and we're not doing all routes by default, set // the CORS headers for this route using the default origin if (routeCorsConfig === true) { if (!sails.config.security.cors.allRoutes) { // Use the default CORS config for this path on an OPTIONS request optionsRouteConfigs[path][verb || 'default'] = sails.config.security.cors; sails.router.bind(route, setHeaders(sails.config.security.cors), null, {_middlewareType: 'CORS HOOK: setHeaders'}); } } // If cors is set to `false`, clear the CORS headers for this route else if (routeCorsConfig === false) { // Clear headers on an OPTIONS request for this path optionsRouteConfigs[path][verb || 'default'] = 'clear'; sails.router.bind(route, setHeaders({allowOrigins: false}), 'all', {_middlewareType: 'CORS HOOK: clearHeaders'}); return; } // Else if cors is set to a string, use that has the origin else if (typeof routeCorsConfig === 'string') { optionsRouteConfigs[path][verb || 'default'] = _.extend({allowOrigins: [routeCorsConfig]}); sails.router.bind(route, setHeaders(_.extend({}, sails.config.security.cors, {allowOrigins: [routeCorsConfig], methods: verb})), null, {_middlewareType: 'CORS HOOK: setHeaders'}); } // Else if cors is an object, use that as the config else if (_.isPlainObject(routeCorsConfig)) { // Set configuration for the preflight OPTIONS request for this route. optionsRouteConfigs[path][verb || 'default'] = routeCorsConfig; // Bind a route that will set CORS headers for this url/path combo. sails.router.bind(route, setHeaders(_.extend({}, routeCorsConfig)), null, {_middlewareType: 'CORS HOOK: setHeaders'}); } // Otherwise we don't recognize the CORS config, so throw a warning else { sails.log.warn('Invalid CORS settings for route '+route); } }); // Now that we have `optionsRouteConfigs`, a list of all of the routes that (possibly) need // to be preflighted, construct a route that will handle OPTIONS requests for all of those routes. // Sending the result of `setPreflightConfig` (a function) into `setHeaders` will cause `setHeaders` // to run the function in order to determine the CORS options to use. sails.router.bind('options /*', setHeaders(setPreflightConfig(optionsRouteConfigs, sails.config.security.cors)), 'options', {_middlewareType: 'CORS HOOK: preflight'}); }); // Continue loading this Sails app. return; }; }; ================================================ FILE: lib/hooks/security/cors/set-headers.js ================================================ /** * This script is a modified version of the Express 4 CORS module: * https://github.com/expressjs/cors * We're making a modified version because that one leaks headers, * but is otherwise still more full-featured and well-thought-out * than what we were previously using to set headers. * * By 'leaks headers', we mean that in certain cases the module * would set headers like `Access-Control-Allow-Origin` or * `Access-Control-Allow-Methods` even if the requesting origin * was not whitelisted. User agents would still reject the response, * but it would allow attackers to sniff some information about * what the server _would_ allow. * * This version of the module _only_ sends headers if the origin * in the request is whitelisted. */ (function () { 'use strict'; var vary = require('vary'); var defaults = { origin: '*', methods: 'GET,HEAD,PUT,PATCH,POST,DELETE', preflightContinue: false, // Note -- the original module default for this setting is 204 optionsSuccessStatus: 200 }; function isString(s) { return typeof s === 'string' || s instanceof String; } function isOriginAllowed(origin, allowedOrigin) { if (Array.isArray(allowedOrigin)) { for (var i = 0; i < allowedOrigin.length; ++i) { if (isOriginAllowed(origin, allowedOrigin[i])) { return true; } } return false; } else if (isString(allowedOrigin)) { return origin === allowedOrigin; } else if (allowedOrigin instanceof RegExp) { return allowedOrigin.test(origin); } else { return !!allowedOrigin; } } function configureOrigin(options, req) { var requestOrigin = req.headers.origin; var headers = []; // If the allowed origin is '*' (or not set, in which case defaulting to '*'), // then we'll send an `Access-Control-Allow-Origin` header. if (!options.origin || options.origin === '*') { // allow any origin headers.push([{ key: 'Access-Control-Allow-Origin', value: '*' }]); } // Otherwise we'll send the header if and ONLY if the requesting origin matches // one of the whitelisted origins. Note that the Express CORS module makes an // exception here if `origin` is set to a string, and always returns the header // even if the requesting origin doesn't match, which seems like a security leak. else { if (isOriginAllowed(requestOrigin, options.origin)) { // reflect origin headers.push([{ key: 'Access-Control-Allow-Origin', value: requestOrigin }]); // Also send a "vary" header to allow proxies to cache this request correctly. headers.push([{ key: 'Vary', value: 'Origin' }]); } } return headers; } function configureMethods(options) { var methods = options.methods || defaults.methods; if (methods.join) { methods = options.methods.join(','); // .methods is an array, so turn it into a string } return { key: 'Access-Control-Allow-Methods', value: methods }; } function configureCredentials(options) { if (options.credentials === true) { return { key: 'Access-Control-Allow-Credentials', value: 'true' }; } return null; } function configureAllowedHeaders(options, req) { var headers = options.allowedHeaders || options.headers; if (!headers) { headers = req.headers['access-control-request-headers']; // .headers wasn't specified, so reflect the request headers } else if (headers.join) { headers = headers.join(','); // .headers is an array, so turn it into a string } if (headers && headers.length) { return { key: 'Access-Control-Allow-Headers', value: headers }; } return null; } function configureExposedHeaders(options) { var headers = options.exposedHeaders; if (!headers) { return null; } else if (headers.join) { headers = headers.join(','); // .headers is an array, so turn it into a string } if (headers && headers.length) { return { key: 'Access-Control-Expose-Headers', value: headers }; } return null; } function configureMaxAge(options) { var maxAge = options.maxAge && options.maxAge.toString(); if (maxAge && maxAge.length) { return { key: 'Access-Control-Max-Age', value: maxAge }; } return null; } function applyHeaders(headers, res) { for (var i = 0, n = headers.length; i < n; i++) { var header = headers[i]; if (header) { if (Array.isArray(header)) { applyHeaders(header, res); } else if (header.key === 'Vary' && header.value) { vary(res, header.value); } else if (header.value) { res.setHeader(header.key, header.value); } } } } function cors(options, req, res, next) { var headers = []; var method = req.method && req.method.toUpperCase && req.method.toUpperCase(); if (method === 'OPTIONS') { // preflight headers = configureOrigin(options, req); // ONLY send additional headers if configureOrigin added the `Access-Control-Allow-Origin` // header, meaning that the requesting origin was whitelisted. if (headers.length) { headers.push(configureCredentials(options, req)); headers.push(configureMethods(options, req)); headers.push(configureAllowedHeaders(options, req)); headers.push(configureMaxAge(options, req)); headers.push(configureExposedHeaders(options, req)); applyHeaders(headers, res); } if (options.preflightContinue ) { return next(); } else { res.statusCode = options.optionsSuccessStatus || defaults.optionsSuccessStatus; res.end(); } } else { // actual response headers = configureOrigin(options, req); // ONLY send additional headers if configureOrigin added the `Access-Control-Allow-Origin` // header, meaning that the requesting origin was whitelisted. if (headers.length) { headers.push(configureCredentials(options, req)); headers.push(configureExposedHeaders(options, req)); applyHeaders(headers, res); } return next(); } } function middlewareWrapper(o) { // if no options were passed in, use the defaults if (!o || o === true) { o = {}; } if (o.origin === undefined) { o.origin = defaults.origin; } if (o.methods === undefined) { o.methods = defaults.methods; } if (o.preflightContinue === undefined) { o.preflightContinue = defaults.preflightContinue; } // if options are static (either via defaults or custom options passed in), wrap in a function var optionsCallback = null; if (typeof o === 'function') { optionsCallback = o; } else { optionsCallback = function (req, cb) { cb(null, o); }; } return function corsMiddleware(req, res, next) { optionsCallback(req, function (err, options) { // Transform the Sails CORS options configs into those expected by this module. options = { origin: options.allowOrigins, credentials: options.allowCredentials, methods: options.allowRequestMethods, headers: options.allowRequestHeaders, exposedHeaders: options.allowResponseHeaders }; // If origin is `*` and `credentials` is true, that means that `allowAnyOriginWithCredentialsUnsafe` // has been set in Sails, so we'll change the origin to `true` (which causes the request origin to // be reflected in the response). if (options.origin === '*' && options.credentials === true) { options.origin = true; } if (err) { return next(err); } else { var originCallback = null; if (options.origin && typeof options.origin === 'function') { originCallback = options.origin; } else if (options.origin) { originCallback = function (origin, cb) { cb(null, options.origin); }; } if (originCallback) { originCallback(req.headers.origin, function (err2, origin) { if (err2 || !origin) { return next(err2); } else { var corsOptions = Object.create(options); corsOptions.origin = origin; cors(corsOptions, req, res, next); } }); } else { return next(); } } }); }; } // can pass either an options hash, an options delegate, or nothing module.exports = middlewareWrapper; }()); ================================================ FILE: lib/hooks/security/cors/set-preflight-config.js ================================================ /** * Module dependencies. */ var _ = require('@sailshq/lodash'); var pathToRegexp = require('path-to-regexp'); /** * * Since the CORS headers module we're using (adapted from https://github.com/expressjs/cors) allows you to provide * options in two ways -- either by passing in a dictionary or by passing in a function that takes the request object and * returns the options at runtime. `setPreflightConfig` prepares a function like that, using a dictionary of information * about Sails routes and their CORS preflight configs, that will be bound to `OPTIONS /*`. That way, when a browser * makes a preflight request to (for example) 'PUT /foo', the constructed function will be run, will look up * `preflightConfigs['/foo']['put']` or `preflightConfigs['/foo']['default']` and use that dictionary of options * (combined with the Sails defaults) to tell the CORS module which headers to send back. * * setPreflightConfig * @param {Dictionary} preflightConfigs A dictionary mapping route path -> dictionary of CORS configs indexed by method (where 'default' is a valid method) * @param {Dictionary} defaultConfig The default CORS config for Sails. * @return {Function} A function that returns the correct set of CORS options for an OPTIONS request to a given path and verb. */ module.exports = function setPreflightConfig(preflightConfigs, defaultConfig) { return function (req, cb) { var path = req.path; var method = (req.headers['access-control-request-method'] || '').toLowerCase() || 'default'; var corsConfig = _.reduce(preflightConfigs, function(memo, configs, preflightConfigPath) { if (memo) {return memo;} var regex = pathToRegexp(preflightConfigPath, []); if (path.match(regex) && (configs[method] || configs.default)) { return (configs[method] || configs.default); } }, null); // If no CORS config is present for this route, set `allowOrigins` to false which // will result in the `acess-control-allow-origin` header being unset. if (!corsConfig) { return cb(null, { allowOrigins: false }); } // Otherwise merge the route CORS config into the default CORS config and use that // for the OPTIONS response headers. return cb(null, _.extend({}, defaultConfig, corsConfig)); }; }; ================================================ FILE: lib/hooks/security/csrf/grant-csrf-token.js ================================================ /** * Action which grants a CSRF token to the requestor. */ module.exports = function grantCsrfToken (req, res /*, next */) { // Don't grant CSRF tokens over sockets. if (req.isSocket) { if (process.env.NODE_ENV === 'production') { return res.notFound(); } return res.badRequest(new Error('Cannot access CSRF token via socket request. Instead, send an HTTP request (i.e. XHR) to this endpoint.')); } // Send the CSRF token wrapped in an object. return res.json({ _csrf: res.locals._csrf }); }; ================================================ FILE: lib/hooks/security/csrf/index.js ================================================ module.exports = function(sails) { /** * Module dependencies. */ var csurf = require('@sailshq/csurf'); var _ = require('@sailshq/lodash'); var pathToRegexp = require('path-to-regexp'); var detectVerb = require('../../../util/detect-verb'); return function initializeCsrf() { // Instantiate CSRF middleware var csrfMiddleware = csurf(); // Loop through the configured routes in order, and create a blacklist and a whitelist // containing regexes to check routes against. var blacklist = []; var whitelist = []; // Regex to check if the route is...a regex. var regExRoute = /^r\|(.*)\|(.*)$/; sails.on('router:after', function() { var sortedRouteAddresses = sails.router.getSortedRouteAddresses(); _.each(sortedRouteAddresses, function(address) { var routeInfo = detectVerb(address); var path = routeInfo.original; var verb = routeInfo.verb.toLowerCase(); var target = sails.router.explicitRoutes[address]; // Ignore targets that don't have CSRF explicitly set. if (target.csrf !== true && target.csrf !== false) { return; } // Ignore targets with GET, HEAD or OPTIONS methods (with warning) if (verb === 'get' || verb === 'head' || verb === 'options') { sails.log.debug('Ignoring `csrf: ' + target.csrf + '` setting for route `' + verb + ' ' + path + '`.'); sails.log.debug('CSRF protection does not apply to GET, HEAD or OPTIONS routes.'); sails.log.debug(); return; } // Perform the check var matches = path.match(regExRoute); var regex; // If it *is* a regex, create a RegExp object that Express can bind, // pull out the params, and wrap the handler in regexRouteWrapper if (matches) { regex = new RegExp(matches[1]); } else { path = path.toLowerCase(); regex = pathToRegexp(path); } if (target.csrf === false && sails.config.security.csrf === true) { blacklist.push({method: verb || '', regex: regex}); } else if (target.csrf === true && sails.config.security.csrf === false) { whitelist.push({method: verb || '', regex: regex}); } }); }); sails.on('router:before', function () { // Start with a clear res.locals._csrf in every request. sails.router.bind('ALL /*', function clearCSRFTokenLocal (req, res, next) { res.locals._csrf = ''; return next(); }); // Check CSRF token on relevant requests, and add the CSRF token as res.locals._csrf // where applicable. sails.router.bind('/*', function(req, res, next) { // If global CSRF is disabled check the whitelist. if (sails.config.security.csrf === false) { // If nothing in the whitelist matches, continue on without checking for a CSRF token. if (!_.any(whitelist, function(whitelistedRoute) { return req.path.match(whitelistedRoute.regex) && (!whitelistedRoute.method || whitelistedRoute.method === req.method.toLowerCase()); })) { return next(); } } // Otherwise check the blacklist else { // If anything in the blacklist matches, continue on without checking for a CSRF token. if (_.any(blacklist, function(blacklistedRoute) { return req.path.match(blacklistedRoute.regex) && (!blacklistedRoute.method || blacklistedRoute.method === req.method.toLowerCase()); })) { return next(); } } // Handle session being disabled. if (!req.session) { // For GET, HEAD and OPTIONS requests, continue on. These aren't covered by CSRF anyway. if (_.contains(['get', 'head', 'options'], req.method.toLowerCase())) { return next(); } // In development mode, give a more explicit account of what's happening. if (process.env.NODE_ENV === 'development') { return next(new Error('Route `' + req.method + ' ' + req.path + '` has CSRF enabled, but the session is disabled!')); } // In production, just return the same CSRF mismatch error you'd get with a bad/missing token in the request. else { return res.forbidden('CSRF mismatch'); } } return csrfMiddleware(req, res, function(err) { if (err) { // Only attempt to handle invalid csrf tokens if (err.code !== 'EBADCSRFTOKEN') { return next(err); } return res.forbidden('CSRF mismatch'); } // If this is not a socket request, provide the CSRF token in res.locals, // so it can be bootstrapped into a view. For purposes of CSRF, we're // treating sockets as inherently insecure (note that we disable // the grant-csrf-token action for sockets as well). You can certainly // _spend_ CSRF tokens over sockets, you just can't retrieve them. if (!req.isSocket) { res.locals._csrf = req.csrfToken(); } next(); }); }, null, {_middlewareType: 'CSRF HOOK: CSRF'}); }); return; }; }; ================================================ FILE: lib/hooks/security/index.js ================================================ module.exports = function(sails) { /** * Module dependencies. */ var _ = require('@sailshq/lodash'); var flaverr = require('flaverr'); var checkOriginUrl = require('../../util/check-origin-url'); var detectVerb = require('../../util/detect-verb'); var initializeCors = require('./cors')(sails); var initializeCsrf = require('./csrf')(sails); var grantCsrfToken = require('./csrf/grant-csrf-token'); /** * Expose hook definition */ return { defaults: { security: { cors: { allowOrigins: '*', allRoutes: false, allowCredentials: false, allowRequestMethods: 'GET,HEAD,PUT,PATCH,POST,DELETE', allowRequestHeaders: 'content-type', allowResponseHeaders: '', allowAnyOriginWithCredentialsUnsafe: false }, csrf: false } }, configure: function() { // ██████╗ ██████╗ ███╗ ██╗███████╗██╗ ██████╗ ██╗ ██╗██████╗ ███████╗ // ██╔════╝██╔═══██╗████╗ ██║██╔════╝██║██╔════╝ ██║ ██║██╔══██╗██╔════╝ // ██║ ██║ ██║██╔██╗ ██║█████╗ ██║██║ ███╗██║ ██║██████╔╝█████╗ // ██║ ██║ ██║██║╚██╗██║██╔══╝ ██║██║ ██║██║ ██║██╔══██╗██╔══╝ // ╚██████╗╚██████╔╝██║ ╚████║██║ ██║╚██████╔╝╚██████╔╝██║ ██║███████╗ // ╚═════╝ ╚═════╝ ╚═╝ ╚═══╝╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝ // // ██████╗███████╗██████╗ ███████╗ // ██╔════╝██╔════╝██╔══██╗██╔════╝ // ██║ ███████╗██████╔╝█████╗ // ██║ ╚════██║██╔══██╗██╔══╝ // ╚██████╗███████║██║ ██║██║ // ╚═════╝╚══════╝╚═╝ ╚═╝╚═╝ if (sails.config.csrf) { sails.log.debug('The `sails.config.csrf` config has been deprecated.'); sails.log.debug('Please use `sails.config.security.csrf` instead.'); sails.log.debug('(we\'ll use your `sails.config.csrf` settings for now).\n'); sails.config.security.csrf = sails.config.csrf; } if (sails.config.security.csrf === true && !sails.hooks.session) { throw flaverr({ name: 'userError', code: 'E_INVALID_SECURITY_CONFIG' }, new Error( '\n-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-\n'+ 'Detected `sails.config.security.csrf set to `true` while session hook is disabled.\n'+ 'Sails CSRF support requires the session hook to be enabled.\n'+ 'See http://sailsjs.com/docs/reference/config/sails-config-session#?disabling-sessions.\n'+ '-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-\n' )); } if (!_.isUndefined(sails.config.security.csrf.routesDisabled)) { throw flaverr({ name: 'userError', code: 'E_INVALID_SECURITY_CONFIG' }, new Error( '\n-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-\n'+ 'Invalid global CSRF settings: `routesDisabled` is no longer supported as of Sails v1.0.\n'+ 'Instead, set `csrf: false` in `config/routes.js` for any route that you want exempted\n'+ 'from CSRF protection.\n'+ 'For more info see: http://sailsjs.com/docs/concepts/security/csrf#?enabling-csrf-protection.\n'+ '-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-\n' )); } if (!_.isUndefined(sails.config.security.csrf.origin)) { throw flaverr({ name: 'userError', code: 'E_INVALID_SECURITY_CONFIG' }, new Error( '\n-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-\n'+ 'Invalid global CSRF settings: `origin` is no longer supported as of Sails v1.0.\n'+ 'Instead, apply CORS settings directly to the CSRF-token-dispensing route in `config/routes.js`.\n'+ 'For more info see: \n'+ 'http://next.sailsjs.com/docs/concepts/security/csrf#?using-ajax-websockets\n'+ 'http://next.sailsjs.com/documentation/concepts/security/cors\n'+ '-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-\n' )); } if (!_.isUndefined(sails.config.security.csrf.grantTokenViaAjax)) { throw flaverr({ name: 'userError', code: 'E_INVALID_SECURITY_CONFIG' }, new Error( '\n-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-\n'+ 'Invalid global CSRF settings: `grantTokenViaAjax` is no longer supported as of Sails v1.0.\n'+ 'Instead, add a route to your `config/routes.js` file using the `security/grant-csrf-token` action.\n'+ 'For more info see: http://next.sailsjs.com/docs/concepts/security/csrf#?using-ajax-websockets\n'+ '-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-\n' )); } // ██████╗ ██████╗ ███╗ ██╗███████╗██╗ ██████╗ ██╗ ██╗██████╗ ███████╗ // ██╔════╝██╔═══██╗████╗ ██║██╔════╝██║██╔════╝ ██║ ██║██╔══██╗██╔════╝ // ██║ ██║ ██║██╔██╗ ██║█████╗ ██║██║ ███╗██║ ██║██████╔╝█████╗ // ██║ ██║ ██║██║╚██╗██║██╔══╝ ██║██║ ██║██║ ██║██╔══██╗██╔══╝ // ╚██████╗╚██████╔╝██║ ╚████║██║ ██║╚██████╔╝╚██████╔╝██║ ██║███████╗ // ╚═════╝ ╚═════╝ ╚═╝ ╚═══╝╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝ // // ██████╗ ██████╗ ██████╗ ███████╗ // ██╔════╝██╔═══██╗██╔══██╗██╔════╝ // ██║ ██║ ██║██████╔╝███████╗ // ██║ ██║ ██║██╔══██╗╚════██║ // ╚██████╗╚██████╔╝██║ ██║███████║ // ╚═════╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝ // ┌─┐┬ ┌─┐┌┐ ┌─┐┬ ┌─┐┌─┐┌┐┌┌─┐┬┌─┐ // │ ┬│ │ │├┴┐├─┤│ │ │ ││││├┤ ││ ┬ // └─┘┴─┘└─┘└─┘┴ ┴┴─┘ └─┘└─┘┘└┘└ ┴└─┘ if (!_.isUndefined(sails.config.cors)) { sails.log.debug('The `sails.config.cors` config has been deprecated.'); sails.log.debug('Please use `sails.config.security.cors` instead.'); sails.log.debug('(we\'ll use your `sails.config.cors` settings for now).\n'); sails.config.security.cors = _.extend(sails.config.security.cors, sails.config.cors); } // Fail to lift if `securityLevel` is used if (!_.isUndefined(sails.config.security.cors.securityLevel)) { throw flaverr({ name: 'userError', code: 'E_INVALID_SECURITY_CONFIG' }, new Error( '\n-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-\n'+ 'Invalid global CORS settings: `securityLevel` is no longer supported as of Sails v1.0.\n'+ 'Instead, to secure your socket requests use `sails.config.sockets.onlyAllowOrigins`.\n'+ 'For more info see: http://sailsjs.com/config/sockets.\n'+ '-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-\n' )); } // Deprecate `origin` in favor of `allowOrigins` if (!_.isUndefined(sails.config.security.cors.origin)) { sails.log.debug('The `sails.config.security.cors.origin` config has been deprecated.'); sails.log.debug('Please use `sails.config.security.cors.allowOrigins` instead.'); sails.log.debug('(See http://sailsjs.com/config/security for more info.)'+'\n'); sails.config.security.cors.allowOrigins = sails.config.security.cors.origin; delete sails.config.security.cors.origin; } // Deprecate declaring `allowOrigins` as a string (except for '*'). if (_.isString(sails.config.security.cors.allowOrigins) && sails.config.security.cors.allowOrigins !== '*') { sails.log.debug('When specifying multiple origins, the `sails.config.security.cors.allowOrigins`'); sails.log.debug('setting should be an array of strings. We\'ll split it up for you this time...\n'); sails.config.security.cors.allowOrigins = _.map(sails.config.security.cors.allowOrigins.split(','), function(origin){ return origin.trim(); }); } // Bail out if `allowOrigins` is not an array or `*`. else if (!_.isUndefined(sails.config.security.cors.allowOrigins) && sails.config.security.cors.allowOrigins !== '*' && !_.isArray(sails.config.security.cors.allowOrigins)) { throw flaverr({ name: 'userError', code: 'E_INVALID_SECURITY_CONFIG' }, new Error('Invalid global CORS settings: if `allowOrigins` is specified, it must be either \'*\' or an array of strings. See http://sailsjs.com/config/security for more info.')); } // Deprecate `credentials` in favor of `allowCredentials` if (!_.isUndefined(sails.config.security.cors.credentials)) { sails.log.debug('The `sails.config.security.cors.credentials` config has been deprecated.'); sails.log.debug('Please use `sails.config.security.cors.allowCredentials` instead.\n'); sails.config.security.cors.allowCredentials = sails.config.security.cors.credentials; delete sails.config.security.cors.credentials; } // Deprecate `headers` in favor of `allowRequestHeaders` if (!_.isUndefined(sails.config.security.cors.headers)) { sails.log.debug('The `sails.config.security.cors.headers` config has been deprecated.'); sails.log.debug('Please use `sails.config.security.cors.allowRequestHeaders` instead.\n'); sails.config.security.cors.allowRequestHeaders = sails.config.security.cors.headers; delete sails.config.security.cors.headers; } // Deprecate `methods` in favor of `allowRequestMethods` if (!_.isUndefined(sails.config.security.cors.methods)) { sails.log.debug('The `sails.config.security.cors.methods` config has been deprecated.'); sails.log.debug('Please use `sails.config.security.cors.allowRequestMethods` instead.\n'); sails.config.security.cors.allowRequestMethods = sails.config.security.cors.methods; delete sails.config.security.cors.methods; } // Deprecate `sails.config.cors.exposeHeaders` in favor of `sails.config.cors.allowResponseHeaders` if (!_.isUndefined(sails.config.security.cors.exposeHeaders)) { sails.log.debug('The `sails.config.security.cors.exposeHeaders` config has been deprecated.'); sails.log.debug('Please use `sails.config.security.cors.allowResponseHeaders` instead.\n'); if (!sails.config.security.cors.allowResponseHeaders) { sails.config.security.cors.allowResponseHeaders = sails.config.security.cors.exposeHeaders; } delete sails.config.security.cors.exposeHeaders; } // Split up non-* strings into an array. // We'll complain about this later when we actually act on the route's CORS config // rather than just validating it. if (_.isString(sails.config.security.cors.allowOrigins) && sails.config.security.cors.allowOrigins !== '*') { sails.log.debug('When specifying multiple allowable CORS origins, the sails.config.security.cors.allowOrigins setting'); sails.log.debug('should be an array of strings. We\'ll split it up for you this time...\n'); sails.config.security.cors.allowOrigins = _.map(sails.config.security.cors.allowOrigins.split(','), function(origin){ return origin.trim(); }); } // If `allowOrigins` is not `*` and not an array at this point, bail. else if (sails.config.security.cors.allowOrigins && sails.config.security.cors.allowOrigins !== '*' && !_.isArray(sails.config.security.cors.allowOrigins)) { throw flaverr({ name: 'userError', code: 'E_BAD_ORIGIN_CONFIG' }, new Error('Invalid global CORS settings: if `sails.config.security.cors.allowOrigins` is specified, it must be \'*\' or an array of strings.')); } // Validate the passed-in origins. // `checkOriginUrl` will throw if any origins are poorly-formed. if (_.isArray(sails.config.security.cors.allowOrigins)) { try { _.each(sails.config.security.cors.allowOrigins, function(origin) { checkOriginUrl(origin); }); } catch (e) { // If we got a poorly-formed origin, throw a more descriptive error. if (e.code === 'E_INVALID') { throw flaverr({ name: 'userError', code: 'E_INVALID_SECURITY_CONFIG' }, new Error('Invalid global CORS `allowOrigins` setting: ' + e.message+' (See http://sailsjs.com/config/security for help.)')); } // Otherwise just throw whatever error we got. throw e; } } // If the app attempts to set `allowOrigins: '*'` and `allowCredentials: true`, bail out if (sails.config.security.cors.allowOrigins === '*' && sails.config.security.cors.allowCredentials === true) { if (sails.config.security.cors.allowAnyOriginWithCredentialsUnsafe !== true) { throw flaverr({ name: 'userError', code: 'E_INVALID_SECURITY_CONFIG' }, new Error('Invalid global CORS settings: if `allowOrigins` is \'*\', `allowCredentials` cannot also be `true` (unless you enable the `allowAnyOriginWithCredentialsUnsafe` flag). For more info, see http://sailsjs.com/config/security.')); } sails.config.security.cors.allowOrigins = true; } // If we're operating in unsafe mode, and origin is '*' and credentials is `true`, // set the default origin to `true` as well which means "reflect origin header". if (sails.config.security.cors.allowAnyOriginWithCredentialsUnsafe && sails.config.security.cors.credentials === true && sails.config.security.cors.allowOrigins === '*') { sails.config.security.cors.allowOrigins = true; } // ┬─┐┌─┐┬ ┬┌┬┐┌─┐ ┌─┐┌─┐┌┐┌┌─┐┬┌─┐ // ├┬┘│ ││ │ │ ├┤ │ │ ││││├┤ ││ ┬ // ┴└─└─┘└─┘ ┴ └─┘ └─┘└─┘┘└┘└ ┴└─┘ // Loop through all of the explicitly-configured routes and look for // deprecated config and/or fatal config issues. _.each(sails.config.routes, function(routeConfig, address) { // Get some info about the route, like its path and verb. // This is used in console messages. var routeInfo = detectVerb(address); var path = routeInfo.original.toLowerCase(); var verb = routeInfo.verb.toLowerCase(); // If this route doesn't have a CORS config, continue. if (!_.isPlainObject(routeConfig.cors)) { return; } // Get a reference to the route CORS config, so that we don't // accidentally mess with routeConfig instead. var routeCorsConfig = routeConfig.cors; // Handle deprecated config. // Deprecate `origin` in favor of `allowOrigins` if (!_.isUndefined(routeCorsConfig.origin)) { sails.log.debug('In route `' + ((verb ? (verb + ' ') : '') + path) + '`: '); sails.log.debug('The `cors.origin` config has been deprecated.'); sails.log.debug('Please use `cors.allowOrigins` instead.'); sails.log.debug('(See http://sailsjs.com/config/security for more info.)'+'\n'); routeCorsConfig.allowOrigins = routeCorsConfig.origin; delete routeCorsConfig.origin; } // Deprecate `credentials` in favor of `allowCredentials` if (!_.isUndefined(routeCorsConfig.credentials)) { sails.log.debug('In route `' + ((verb ? (verb + ' ') : '') + path) + '`: '); sails.log.debug('The `cors.credentials` config has been deprecated.'); sails.log.debug('Please use `cors.allowCredentials` instead.\n'); routeCorsConfig.allowCredentials = routeCorsConfig.credentials; delete routeCorsConfig.credentials; } // Deprecate `headers` in favor of `allowRequestHeaders` if (!_.isUndefined(routeCorsConfig.headers)) { sails.log.debug('In route `' + ((verb ? (verb + ' ') : '') + path) + '`: '); sails.log.debug('The `cors.headers` config has been deprecated.'); sails.log.debug('Please use `cors.allowRequestHeaders` instead.\n'); routeCorsConfig.allowRequestHeaders = routeCorsConfig.headers; delete routeCorsConfig.headers; } // Deprecate `methods` in favor of `allowRequestMethods` if (!_.isUndefined(routeCorsConfig.methods)) { sails.log.debug('In route `' + ((verb ? (verb + ' ') : '') + path) + '`: '); sails.log.debug('The `cors.methods` config has been deprecated.'); sails.log.debug('Please use `cors.allowRequestMethods` instead.\n'); routeCorsConfig.allowRequestMethods = routeCorsConfig.methods; delete routeCorsConfig.methods; } // Deprecate `sails.config.cors.exposeHeaders` in favor of `sails.config.cors.allowResponseHeaders` if (!_.isUndefined(routeCorsConfig.exposeHeaders)) { sails.log.debug('In route `' + ((verb ? (verb + ' ') : '') + path) + '`: '); sails.log.debug('The `cors.exposeHeaders` config has been deprecated.'); sails.log.debug('Please use `cors.allowResponseHeaders` instead.\n'); if (!routeCorsConfig.allowResponseHeaders) { routeCorsConfig.allowResponseHeaders = routeCorsConfig.exposeHeaders; } delete routeCorsConfig.exposeHeaders; } // Apply the global CORS settings as defaults for the route CORS config. routeCorsConfig = _.defaults(routeCorsConfig, sails.config.security.cors); // Bail if `allowOrigins` is `*`, `allowCredentials` is `true` and `allowAnyOriginWithCredentialsUnsafe` is not true. if (routeCorsConfig.allowOrigins === '*' && routeCorsConfig.allowCredentials === true && routeCorsConfig.allowAnyOriginWithCredentialsUnsafe !== true) { throw flaverr({ name: 'userError', code: 'E_UNSAFE'}, new Error('Route `' + address + '` has invalid CORS settings: if `allowOrigins` is \'*\', `credentials` cannot be `true` unless `allowAnyOriginWithCredentialsUnsafe` is also true.')); } // Split up non-* strings into an array. if (_.isString(routeCorsConfig.allowOrigins) && routeCorsConfig.allowOrigins !== '*') { sails.log.debug('In route `' + ((verb ? (verb + ' ') : '') + path) + '`: '); sails.log.debug('When specifying multiple allowable CORS origins, the allowOrigins setting'); sails.log.debug('should be an array of strings. We\'ll split it up for you this time...\n'); routeCorsConfig.allowOrigins = _.map(routeCorsConfig.allowOrigins.split(','), function(origin){ return origin.trim(); }); } // If `allowOrigins` is not `*` and not an array at this point, bail. else if (routeCorsConfig.allowOrigins && routeCorsConfig.allowOrigins !== '*' && !_.isArray(routeCorsConfig.allowOrigins)) { throw flaverr({ name: 'userError', code: 'E_BAD_ORIGIN_CONFIG'}, new Error('Route `' + address + '` has invalid CORS settings: if `allowOrigins` is specified, it must be \'*\' or an array of strings.')); } // If `allowOrigins` is an array, loop through and validate each origin. if (_.isArray(routeCorsConfig.allowOrigins)) { try { _.each(routeCorsConfig.allowOrigins, function(origin) { checkOriginUrl(origin); }); } // If an error occurred validating an origin, forward it up the chain. catch (e) { // If it's an actual origin validation error, gussy it up first. if (e.code === 'E_INVALID') { throw flaverr({ name: 'userError', code: 'E_INVALID_ORIGIN'}, new Error('Route `' + address + '` has invalid CORS `allowOrigins` setting: ' + e.message)); } // Otherwise just throw whatever error we got. throw e; } } }); }, initialize: function(cb) { try { initializeCors(); initializeCsrf(); return sails.hooks.security.registerActions(cb); } catch (err) { return cb(err); } }, registerActions: function(cb) { // Add the csrf-token-granting action (see below for the function definition). sails.registerAction(grantCsrfToken, 'security/grant-csrf-token'); return cb(); } }; }; ================================================ FILE: lib/hooks/services/index.js ================================================ /** * Module dependencies */ var _ = require('@sailshq/lodash'); /** * @param {SailsApp} sails * @return {Function} */ module.exports = function (sails) { /** * `services` * * The definition of `services`, a core hook. * * @param {SailsApp} sails * @return {Dictionary} */ return { /** * Implicit defaults which will be merged into sails.config before this hook is loaded... * @type {Dictionary} */ defaults: { globals: { services: true } }, /** * Before any hooks have begun loading... * (called automatically by Sails core) */ configure: function() { // This initial setup of `sails.services` was included here as an experimental // feature so that these modules would be accessible for other hooks. This will be // deprecated in Sails v1.0 in favor of (likely) the ability for hook authors to register // or unregister services programatically. In addition, services will no longer be exposed // on the `sails` app instance. // // Expose an empty dictionary for `sails.services` so that it is // guaranteed to exist. sails.services = {}; }, /** * Before THIS hook has begun loading... * (called automatically by Sails core) */ loadModules: function(cb) { // In future versions of Sails, the empty registry of services can be initialized here: // sails.services = {}; sails.log.silly('Loading app services...'); // Load service modules using the module loader // (by default, this loads services from files in `api/services/*`) sails.modules.loadServices(function(err, modules) { if (err) { sails.log.error('Error occurred loading modules ::'); sails.log.error(err); return cb(err); } // Expose services on `sails.services` to provide access even when globals are disabled. _.extend(sails.services, modules); // Expose globals (if enabled) if (sails.config.globals.services) { _.each(sails.services, function(service, identity) { var globalId = service.globalId || service.identity || identity; global[globalId] = service; }); } // Relevant modules have finished loading. return cb(); }); }// </loadModules> };//</hook definition> }; ================================================ FILE: lib/hooks/session/README.md ================================================ ## Session Hook At configuration-time, this hook loads verifies valid configuration of the connect session store (configurable in `sails.config.session`), At lift-time, it instantiates the session store and makes it accesible via `sails.session`. It includes methods for: + attaching a connect session to a socket.io connection + generating new sessions + getting and setting the session ##### Contributing to this hook It would be great to see a generic connect session adapter with support for the existing 'connections' in sails (ie. waterline adapters). See [connect-waterline](https://www.npmjs.com/package/connect-waterline). ================================================ FILE: lib/hooks/session/index.js ================================================ /** * Module dependencies. */ var path = require('path'); var util = require('util'); var _ = require('@sailshq/lodash'); var flaverr = require('flaverr'); var Redis = require('machinepack-redis'); // (this dependency is just for creating new cookies) var uid = require('uid-safe'); // (these two dependencies are only here for sails.session.parseSessionIdFromCookie(), // which is only here to enable socket lifecycle callbacks) var parseCookie = require('cookie').parse; var stringifyCookie = require('cookie').serialize; var unsignCookie = require('cookie-signature').unsign; var signCookie = require('cookie-signature').sign; var pathToRegexp = require('path-to-regexp'); module.exports = function(app) { // `session` hook definition var SessionHook = { defaults: { session: { adapter: 'memory', name: 'sails.sid', // By default, disable session for requests to paths that look like static assets. isSessionDisabled: function (req){ return !!req.path.match(req._sails.LOOKS_LIKE_ASSET_RX); }, // Added overrideable function for constructing the session store based on https://github.com/balderdashy/sails/pull/7172 // • `sessionConfig` is what Sails has loaded for sails.config.session // • `configuredSessionAdapter` is the package like what you get from doing require('connect-mongo') // • `expressSessionFromSailsCore` is the version of express-session used by Sails core session hook, provided for your convenience handleConstructingSessionStore: (sessionConfig, configuredSessionAdapter, expressSessionFromSailsCore) => { let CustomStore = configuredSessionAdapter(expressSessionFromSailsCore); return new CustomStore(sessionConfig); } } }, /** * Normalize and validate configuration for this hook. * Then fold any modifications back into `sails.config` */ configure: function() { // Validate config // Ensure that session config is at least an object of some kind. if (app.config.session) { if (!_.isObject(app.config.session)) { throw flaverr({ name: 'userError', code: 'E_INVALID_SESSION_CONFIG' }, new Error('Invalid custom session store configuration!\n' + '\n' + 'Basic usage ::\n' + '{ session: { adapter: "memory", secret: "someVerySecureString", /* ...if applicable: host, port, etc... */ } }' + '\n\nCustom usage ::\n' + '{ session: { store: { /* some custom connect session store instance */ }, secret: "someVerySecureString", /* ...custom settings.... */ } }' )); } } if (!app.config.session.secret && process.env.NODE_ENV !== 'production') { app.config.session.secret = 'extremely-secure-keyboard-cat'; app.log.debug('Warning: no session secret was set! In development mode, we\'ll set this for you,'); app.log.debug('but session secret must be manually specified in production.'); app.log.debug('To set up a session secret, add or update it in `config/session.js`:'); app.log.debug('module.exports.session = { secret: \'extremely-secure-keyboard-cat\' }'); app.log.debug(); app.log.debug('(Or if you don\'t need sessions enabled, disable the hook.)'); app.log.debug(); app.log.debug('For more help:'); app.log.debug(' • https://sailsjs.com/config/session'); app.log.debug(' • https://sailsjs.com/support'); app.log.debug(); } // Throw if the old `routesDisabled` is used instad of `isSessionDisabled`. if (app.config.session.routesDisabled) { throw flaverr({ name: 'userError', code: 'E_INVALID_SESSION_CONFIG' }, new Error( '\n-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-\n'+ 'The `sails.config.session.routesDisabled` property is no longer supported in Sails 1.0.\n'+ 'Instead, specify a `sails.config.session.isSessionDisabled` function which takes the\n'+ 'request object as an argument and returns `true` if the session should be disabled,\n'+ 'and `false` otherwise.\n'+ '-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-\n')); } // Throw if `isSessionDisabled` defined, but is not a function. if (!_.isUndefined(app.config.session.isSessionDisabled) && !_.isFunction(app.config.session.isSessionDisabled)) { throw flaverr({ name: 'userError', code: 'E_INVALID_SESSION_CONFIG' }, new Error( '\n-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-\n'+ 'The `sails.config.session.isSessionDisabled` property, if specified, must be a function.\n'+ 'Instead, got: `' + util.inspect(app.config.session.isSessionDisabled) + '`.\n'+ '-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-\n')); } // Throw if `cookie.secure` is defined, but is not a boolean. if (!_.isUndefined(_.get(app.config.session, 'cookie.secure')) && !_.isBoolean(app.config.session.cookie.secure)) { throw flaverr({ name: 'userError', code: 'E_SESSION_BAD_COOKIE_SECURE' }, new Error( '\n-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-\n'+ 'The `sails.config.session.cookie.secure` property, if specified, must be a boolean.\n'+ 'Instead, got: `' + util.inspect(app.config.session.cookie.secure) + '` (which is type `' + (typeof app.config.session.cookie.secure) + '`).\n'+ '-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-\n')); } // Throw if `cookie.secure` is defined, but is not a boolean. if (process.env.NODE_ENV === 'production') { if (_.get(app.config.session, 'cookie.secure') !== true) { app.log.debug('Warning: since `sails.config.session.cookie.secure` is not set to `true`, the session'); app.log.debug('cookie will be sent over non-TLS connections (i.e. with insecure http:// requests).'); app.log.debug('To enable secure cookies, set `sails.config.session.cookie.secure` to `true`.'); app.log.debug(); app.log.debug('If your app is behind a proxy or load balancer (e.g. on a PaaS like Heroku), you'); app.log.debug('may also need to set `sails.config.http.trustProxy` to `true`.'); app.log.debug(); app.log.debug('For more help:'); app.log.debug(' • https://sailsjs.com/config/session#?the-secure-flag'); app.log.debug(' • https://sailsjs.com/config/session#?do-i-need-an-ssl-certificate'); app.log.debug(' • https://sailsjs.com/config/sails-config-http#?properties'); app.log.debug(' • https://sailsjs.com/support'); app.log.debug(); } else { app.log.debug('Please note: since `sails.config.session.cookie.secure` is set to `true`, the session cookie '); app.log.debug('will _only_ be sent over TLS connections (i.e. secure https:// requests).'); app.log.debug('Requests made via http:// will not include a session cookie!'); app.log.debug(); if (app.config.http.trustProxy === false) { app.log.debug('Also, note that since `sails.config.http.trustProxy` is set to `false`, secure cookies'); app.log.debug('(and potentially all sessions+login over "https://") may not work if your app is hosted'); app.log.debug('behind a proxy or load balancer -- for example, on a PaaS like Heroku or EBS.'); app.log.debug(); } app.log.debug('For more help:'); app.log.debug(' • https://sailsjs.com/config/session#?the-secure-flag'); app.log.debug(' • https://sailsjs.com/config/session#?do-i-need-an-ssl-certificate'); app.log.debug(' • https://sailsjs.com/config/sails-config-http#?properties'); app.log.debug(' • https://sailsjs.com/support'); app.log.debug(); } } // If session secret is undefined, set a secure, one-time use secret if (!app.config.session || !app.config.session.secret) { app.log.verbose('Session secret not defined...'); if (process.env.NODE_ENV === 'production') { throw new Error( 'Session secret should be manually specified in production!\n'+ 'To set up a session secret, add or update it in `config/session.js`:\n'+ 'module.exports.session = { secret: \'extremely-secure-keyboard-cat\' }\n'+ '\n'+ '(Or if you don\'t need sessions enabled, disable the hook.)\n'+ '\n'+ 'For more help:\n'+ ' • https://sailsjs.com/config/session\n'+ ' • https://sailsjs.com/support' ); } } //‡ // If secret _is_ defined, make sure it's a string else if (app.config.session.secret && !_.isString(app.config.session.secret)) { throw flaverr({ name: 'userError', code: 'E_INVALID_SESSION_CONFIG' }, new Error('If provided, sails.config.session.secret should be a string.')); } // Validate `routesDisabled`, if specified. if (app.config.session.routesDisabled && !_.isArray(app.config.session.routesDisabled)) { throw flaverr({ name: 'userError', code: 'E_INVALID_SESSION_CONFIG' }, new Error('Invalid `sails.config.session.routesDisabled` configuration!\n' + '(must be an array of route address strings)' )); } // Backwards-compatibility / shorthand notation // (allow mongo or redis session stores to be specified directly) if (app.config.session.adapter === 'redis') { app.config.session.adapter = 'connect-redis'; } else if (app.config.session.adapter === 'mongo') { app.config.session.adapter = 'connect-mongo'; } // If `key` is provided, transform it to `name` and log a warning. if (_.isString(app.config.session.key)) { app.config.session.name = app.config.session.key; app.log.debug('The `sails.config.session.key` setting is deprecated; please use `sails.config.session.name` instead.\n'); } // If a URL was provided, make sure it has no trailing slash. if (_.isString(app.config.session.url)) { app.config.session.url = app.config.session.url.replace(/\/$/,''); } }, /** * initialize() is run when the session hook is loaded. * * (Its primary responsibility is to create a session store instance * and keep it around.) * * @api private */ initialize: function(cb) { // Build `sails.hooks.session.routesDisabled`. // (only salient if `sails.config.session.routesDisabled` was specified) try { // Regex to check if the route is...a regex. var regExRoute = /^r\|(.*)\|(.*)$/; app.hooks.session.routesDisabled = _.reduce(app.config.session.routesDisabled || [], function (memo, routeAddressStr){ // Validate and parse route address. if (!_.isString(routeAddressStr)){ throw new Error('Cannot parse route address (`'+routeAddressStr+'`). Must be a string.'); } var addrPieces = routeAddressStr.split(/\s/); var method; var urlPattern; if (addrPieces.length === 1) { method = ''; urlPattern = addrPieces[0]; } else if (addrPieces.length === 2) { method = addrPieces[0]; urlPattern = addrPieces[1]; } else { throw new Error('Cannot parse route address (`'+routeAddressStr+'`). When split on whitespace, there are either too many or too few pieces (`'+addrPieces.length+'`).'); } // Generate a regular expression from the URL pattern string. var urlPatternRegExp; // Perform the check var matches = urlPattern.match(regExRoute); // If it *is* a regex, create a RegExp object that Express can bind, // pull out the params, and wrap the handler in regexRouteWrapper if (matches) { urlPatternRegExp = new RegExp(matches[1]); } else { urlPatternRegExp = pathToRegexp(urlPattern, []); } memo.push({ method: method, urlPatternRegExp: urlPatternRegExp }); return memo; }, []);//</_.reduce()> } catch (e) { return cb( new Error('Failed to parse one of the route addresses provided in `sails.config.session.routesDisabled`.\n'+ 'If specified, this config must be an array of normal route address strings.\n'+ 'Error details:'+e.stack) ); } // ┌─┐┌─┐┌┬┐ ┬ ┬┌─┐ ┌─┐┬─┐┌─┐┬ ┬┬┌┬┐┌─┐┌┬┐ ┌─┐┌┬┐┌─┐┌─┐┌┬┐┌─┐┬─┐ // └─┐├┤ │ │ │├─┘ ├─┘├┬┘│ │└┐┌┘│ ││├┤ ││ ├─┤ ││├─┤├─┘ │ ├┤ ├┬┘ // └─┘└─┘ ┴ └─┘┴ ┴ ┴└─└─┘ └┘ ┴─┴┘└─┘─┴┘ ┴ ┴─┴┘┴ ┴┴ ┴ └─┘┴└─ (function setupAdapter(proceed) { // If no adapter config was provided, skip down to creating the session middleware. if (!_.isObject(app.config.session) || !app.config.session.adapter) { return proceed(); } // 'memory' is a special case if (app.config.session.adapter === 'memory') { var MemoryStore = require('express-session').MemoryStore; app.config.session.store = new MemoryStore(); return proceed(); }//‡ // For all other adapters, we'll try to require the module and do some setup. else { // ┌─┐┌─┐┌┬┐ ┬ ┬┌─┐ ┬─┐┌─┐┌┬┐┬┌─┐ ┌─┐┌┬┐┌─┐┌─┐┌┬┐┌─┐┬─┐ // └─┐├┤ │ │ │├─┘ ├┬┘├┤ │││└─┐ ├─┤ ││├─┤├─┘ │ ├┤ ├┬┘ // └─┘└─┘ ┴ └─┘┴ ┴└─└─┘─┴┘┴└─┘ ┴ ┴─┴┘┴ ┴┴ ┴ └─┘┴└─ (function maybeConnectToRedis(proceed) { // If the adapter isn't set to `connect-redis`/`@sailshq/connect-redis`, // or an existing Redis client is being provided, skip this part. if ((app.config.session.adapter !== 'connect-redis' && app.config.session.adapter !== '@sailshq/connect-redis') || app.config.session.client) { return proceed(); }//• // If a connection URL is provided, use that, otherwise construct one from the pieces // provided in the session config. var url = app.config.session.url || Redis.createConnectionUrl(_.pick(app.config.session, ['host', 'port', 'pass', 'db'])).now(); // Create a Redis connection manager. Redis.createManager({ connectionString: url, meta: _.omit(app.config.session, ['host', 'port', 'pass', 'db', 'url', 'adapter', 'prefix']), // Handle failures on the connection. onUnexpectedFailure: function(err) { // If Sails is already on the way out, ignore the Redis issue. if (app._exiting) { return; } // Log the error from Redis in verbose mode. app.log.verbose('Redis connection manager failed unexpectedly. Details:', err); // If the Redis client disconnected, say something and run any custom logic // that was provided for this occasion. if (err.failureType === 'end') { if (_.isFunction(app.config.session.onRedisDisconnect)) { app.config.session.onRedisDisconnect(); } else { app.log.error('Redis session server went offline...'); } // If a disconnected client comes back, say something and run any custom logic // that was provided for this occasion. err.connection.once('ready', function() { if (_.isFunction(app.config.session.onRedisReconnect)) { app.config.session.onRedisReconnect(); } else { app.log.error('Redis session server came back online...'); } }); } } }).exec(function(err, createManagerResult) { if (err) { return proceed(err); } // Use the manager to create a new Redis connection. Redis.getConnection({ manager: createManagerResult.manager }).switch({ error: function(err) { return proceed(err); }, failed: function(report) { return proceed(report.error); }, success: function(result) { // Save the connected client into the session config so that it can be used // by the connect-redis module. app.config.session.client = result.connection; return proceed(); } }); }); })(function afterMaybeConnectToRedis(err) { if (err) { return proceed(err); } // This local variable is used to hold the connect session adapter. // (we determine what it is below) var SessionAdapter; // If `sails.config.session.adapter` is a string, attempt to require the // module identified by the string. if (_.isString(app.config.session.adapter)) { try { SessionAdapter = require(path.resolve(app.config.appPath, 'node_modules', app.config.session.adapter)); } catch(rawRequireErr) { // If an error occurred while attempting to require() the adapter, include // some (hopefully) helpful instructions on installing the adapter. return proceed(new Error( // 'Could not require `' + app.config.session.adapter + '` (a session adapter).\n'+ 'Do you have `' + app.config.session.adapter + '` installed locally?\n'+ 'If not, try running the following command in your app\'s root directory:\n'+ 'npm install ' + app.config.session.adapter + '\n'+ '(Note: Make sure to install a Connect session adapter that is compatible with this version of Sails.)\n'+ '\n'+ 'For debugging purposes, here is the error from attempting to run `require(\''+app.config.session.adapter+'\')`:\n'+ '---\n'+ (function _getAppropriateMessageFromRawRequireErr(){ if (_.isError(rawRequireErr)) { return rawRequireErr.stack; } else if (_.isString(rawRequireErr)) { return rawRequireErr; } else { return util.inspect(rawRequireErr, { depth: null }); } })()+'\n'+ '---\n' )); }//</catch :: require> }//</if .session.adapter is a string> //‡ // Otherwise if it's an object (including a function!), set SessionAdapter to that value. else if (_.isObject(app.config.session.adapter)) { SessionAdapter = app.config.session.adapter; } // Otherwise bail, because sails.config.session.adapter is invalid. else { return proceed(new Error('If configured, `sails.config.session.adapter` should be a reference to an Express session adapter! Instead got `' + util.inspect(app.config.session.adapter))); } // Okay, so now we have an adapter that we can call to create an // Express session store. So we'll attempt to create the store // by passing the `express-session` module and adapter into the // handleConstructingSessionStore function try { app.config.session.store = app.config.session.handleConstructingSessionStore(app.config.session, SessionAdapter, require('express-session')); } catch (rawSessionStoreCreationErr) { // Failed attempting to initialize adapter; output a message w/ error info return proceed(new Error( 'Encountered error attempting to instantiate a session store using the installed version of `' + app.config.session.adapter + '` (a session adapter), or with your custom handleConstructingSessionStore function.\n'+ 'Raw error from the session adapter:\n'+ '---\n'+ (function _getAppropriateMessageFromRawSessionAdapterErr(){ if (_.isError(rawSessionStoreCreationErr)) { // FUTURE: negotiate faw error and give better error msg depending on code // (not sure if things are quite ready in the express-session adapter spec yet to make this possible) return rawSessionStoreCreationErr.stack; } else if (_.isString(rawSessionStoreCreationErr)) { return rawSessionStoreCreationErr; } else { return util.inspect(rawSessionStoreCreationErr, { depth: null }); } })()+'\n'+ '---\n'+ '\n' )); } //</catch :: failed to instantiate session adapter by passing in express-session> return proceed(); });//</ self-calling function> }//</else (if we're using a custom store and NOT the memory store)> })(function afterSettingUpAdapter (err) {//~∞%° if (err) { return cb(err); } // Expose hook as `sails.session` app.session = SessionHook; // Build configuration the raw session middleware, using the // session config built above (including the adapter and store) // and adding a couple of defaults for extra options like `resave`. var opts = _.extend({ resave: true,// FUTURE: set `resave: false` (see https://github.com/expressjs/session/tree/8e56128d8ba014ab586521247977b0d4e67340f9#resave) saveUninitialized: true// FUTURE: set `saveUninitialized: false` (see https://github.com/expressjs/session/tree/8e56128d8ba014ab586521247977b0d4e67340f9#saveuninitialized) }, app.config.session); // Get a raw express-session middleware function using the options // we just built. var rawSessionMiddleware = require('express-session')(opts); // Now wrap up the raw middleware in our own req/res/next function, and expose // it privately so it can be used by the private Sails router and the HTTP session middleware. app._privateSessionMiddleware = function(req, res, next) { // If an `isSessionDisabled` function is configured, run it against the current request // and if it returns `true`, skip the session middleware entirely. if(app.config.session.isSessionDisabled && app.config.session.isSessionDisabled(req)) { return next(); } // Run the express session middleware that actually sets up the session. return rawSessionMiddleware(req, res, next); }; return cb(); }); //</self-calling function that sets up adapter)> }, // </initialize> /** * Generate a cookie to represent a new session. * * @return {String} * @api private */ generateNewSidCookie: function (){ var sid = uid.sync(24); var signedSid = 's:' + signCookie(sid, app.config.session.secret); var cookie = stringifyCookie(app.config.session.name, signedSid, {}); return cookie; }, /** * Parse and unsign (i.e. decrypt) the provided cookie to get the session id. * * (adapted from code in the `express-session`) * * @param {String} cookie * @return {String} [sessionId] * * @throws {Error} If cookie cannot be parsed or unsigned */ parseSessionIdFromCookie: function (cookie){ // e.g. "lolcatparty" var sessionSecret = app.config.session.secret; // Parse cookie var parsedSidCookie = parseCookie(cookie)[app.config.session.name]; if (typeof parsedSidCookie !== 'string') { throw flaverr({ status: 401, code: 'E_SESSION_PARSE_COOKIE' }, new Error('No sid cookie exists')); }//-• if (parsedSidCookie.substr(0, 2) !== 's:') { throw flaverr({ status: 401, code: 'E_SESSION_PARSE_COOKIE' }, new Error('Cookie unsigned')); }//-• // Unsign cookie var sessionId = unsignCookie(parsedSidCookie.slice(2), sessionSecret); if (sessionId === false) { throw flaverr({ status: 401, code: 'E_SESSION_PARSE_COOKIE' }, new Error('Cookie signature invalid')); }//-• return sessionId; }, /** * @param {String} sessionId * @param {Function} cb * * @api private */ get: function(sessionId, cb) { if (!_.isFunction(cb)) { throw new Error('Invalid usage :: `sails.hooks.session.get(sessionId, cb)`'); } app.config.session.store.get(sessionId, function (err, session) { if (err) { return cb(err); } if (!session) { return cb(flaverr('E_SESSION', new Error('Session could not be loaded.'))); } return cb(null, session); });//</store.get> }, /** * @param {String} sessionId * @param {} data * @param {Function} cb * * @api private */ set: function(sessionId, data, cb) { if (!_.isFunction(cb)) { throw new Error('Invalid usage :: `sails.hooks.session.set(sessionId, data, cb)`'); } // Attempt to persist data (upsert) to the session entry with the given `sessionId`. app.config.session.store.set(sessionId, data, function (err) { if (err) { return cb(err); } // Now look up the session so it can be sent back in its entirety. app.config.session.store.get(sessionId, function (err, session) { if (err) { return cb(err); } if (!session) { return cb(flaverr('E_SESSION', new Error('Session (`'+sessionId+'`) could not be located after saving.'))); } return cb(null, session); });//</store.get> });//</store.set> } }; return SessionHook; }; ================================================ FILE: lib/hooks/userconfig/README.md ================================================ ## Userconfig Hook This hook loads app-level user configuration using the moduleloader hook (from the config directory located at `sails.config.paths.config`) It is always loaded first. ##### Contributing to this hook Not a good place to jump in right now. Please tweet @mikermcneil before working on this part! ================================================ FILE: lib/hooks/userconfig/index.js ================================================ module.exports = function(sails) { /** * Module dependencies */ var _ = require('@sailshq/lodash'); var mergeDictionaries = require('merge-dictionaries'); /** * Userconfig * * Load configuration files. */ return { // Default configuration defaults: {}, /** * Fetch relevant modules, exposing them on `sails` subglobal if necessary, */ loadModules: function (cb) { sails.log.silly('Loading app config...'); // Grab reference to mapped overrides // - - - - - - - - - - - - - - - - - - - - - - - - - - - // FUTURE: Optimization: do we need this _.clone()? // - - - - - - - - - - - - - - - - - - - - - - - - - - - var overrides = _.clone(sails.config); // If appPath not specified yet, use process.cwd() // (the directory where this Sails process is being initiated from) if ( ! overrides.appPath ) { sails.config.appPath = process.cwd(); } // Load config dictionary from app modules sails.modules.loadUserConfig(function loadedAppConfigModules (err, userConfig) { if (err) { return cb(err); } // Finally, extend user config with overrides var config = {}; // Merge the overrides into the loaded user config. config = mergeDictionaries(userConfig, overrides); // Ensure final configuration object is valid // (in case moduleloader fails miserably) config = _.isObject(config) ? config : (sails.config || {}); // Save final config into sails.config sails.config = config; cb(); }); } }; }; ================================================ FILE: lib/hooks/userhooks/README.md ================================================ ## Userhooks Hook This hook loads hooks from the user hooks directory, configurable in `sails.config.paths.hooks` (defaults to `api/hooks`) of a sails app. If the directory doesn't exist, this hook is a no-op. ##### Contributing to this hook Not a good place to jump in right now. Please tweet @mikermcneil before working on this part! ================================================ FILE: lib/hooks/userhooks/index.js ================================================ var _ = require('@sailshq/lodash'); module.exports = function(sails) { /** * `userhooks` * * Sails hook for loading user plugins (hooks) */ return { defaults: { }, initialize: function(cb) { if ( !sails.config.hooks.moduleloader ) { return cb('Cannot load user hooks without `moduleloader` hook enabled!'); } sails.log.silly('Loading user hooks...'); // Load user hook definitions sails.modules.loadUserHooks(function hookDefinitionsLoaded(err, hooks) { if (err) { return cb(err); } // Ensure hooks is valid hooks = _.isObject(hooks) ? hooks : {}; // If `sails.config.loadHooks` is set, then only include user hooks if // they are explicitly listed therein. // > Note that `sails.config.hooks` is taken care of as part of the // > implementation of `sails.modules.loadUserHooks()`. if (sails.config.loadHooks) { sails.log.silly('Since `sails.config.userHooks` was specified, checking user hooks against it to make sure they should actually be loaded...'); _.each(hooks, function(def, hookName) { if (!_.contains(sails.config.loadHooks, hookName)) { delete hooks[hookName]; sails.log.verbose('Skipped loading "'+hookName+'" hook, because `sails.config.loadHooks` was specified but did not explicitly include this hook\'s name.'); } }); }//fi // Add the user hooks to the list of hooks to load // (excluding any that were omitted) _.extend(sails.hooks, hooks); return cb(); }); } }; }; ================================================ FILE: lib/hooks/views/configure.js ================================================ /** * Module dependencies */ var util = require('util'); var flaverr = require('flaverr'); var _ = require('@sailshq/lodash'); /** * Marshal relevant parts of sails global configuration, * issue deprecation notices, etc. * * @param {Sails} sails */ module.exports = function configure ( sails ) { if (sails.config.views.engine) { sails.log.debug('The `config.views.engine` config has been deprecated.'); sails.log.debug('In Sails 1.x, use `config.views.extension` to choose your view'); sails.log.debug('extension (defaults to ".ejs"), and use `config.views.getRenderFn`'); sails.log.debug('to configure your template engine or leave it undefined to use'); sails.log.debug('the built-in EJS template support.\n'); sails.config.views.extension = sails.config.views.engine.ext || 'ejs'; delete sails.config.views.engine; } // Make sure the extension is valid. if (sails.config.views.extension === '' || (!_.isString(sails.config.views.extension) && sails.config.views.extension !== false)) { throw flaverr({ name: 'userError', code: 'E_INVALID_VIEW_CONFIG' }, new Error('`sails.config.views.extension` must either be a string or `false`.')); } // Let user know that a leading . is not required in the viewEngine option and then fix it if (sails.config.views.extension[0] === '.') { sails.log.warn('A leading `.` is not required in the config.views.extension option. Removing it for you...'); sails.config.views.extension = sails.config.views.extension.substr(1); } // Make sure the `getRenderFn` is valid, if provided. if (!_.isUndefined(sails.config.views.getRenderFn) && !_.isFunction(sails.config.views.getRenderFn)) { throw flaverr({ name: 'userError', code: 'E_INVALID_VIEW_CONFIG' }, new Error('`sails.config.views.getRenderFn`, if provided, must be a function (got ' + util.inspect(sails.config.views.getRenderFn) + ')')); } else if (sails.config.views.getRenderFn) { var renderFn = sails.config.views.getRenderFn(); if (!_.isFunction(renderFn)) { throw flaverr({ name: 'userError', code: 'E_INVALID_VIEW_CONFIG' }, new Error('`sails.config.views.getRenderFn` returned an invalid value. (expected a function, but got: ' + util.inspect(renderFn) + ')')); } sails.hooks.views._renderFn = renderFn; if (sails.config.views.layout) { sails.log.error('Ignoring `sails.config.views.layout`...'); sails.log.error('Sails\' built-in layout support only works with the default EJS view engine.'); sails.log.error('You\'re using a custom view engine, so you\'ll need to implement layouts on your own!'); } } else { // Custom layout location // (if string specified, it's used as the relative path from the views folder) // (if not string, but truthy, relative path from views folder defaults to ./layout.*) // (if falsy, don't use layout) if ( !_.isString(sails.config.views.layout) && sails.config.views.layout ) { sails.config.views.layout = 'layout.' + sails.config.views.extension; } } }; ================================================ FILE: lib/hooks/views/default-view-rendering-fn.js ================================================ /** * Module dependencies */ var path = require('path'); var fs = require('fs'); var _ = require('@sailshq/lodash'); var ejs = require('ejs'); var exists = fs.existsSync || path.existsSync; var resolve = path.resolve; var extname = path.extname; var dirname = path.dirname; var join = path.join; var basename = path.basename; /** * Implement EJS layouts and partials (a la Express 2). * * This is a slightly modified (for EJS >= 2.3.4) version of the defunct ejs-locals package: * ------------------------------------------------------------------------------------------ * https://github.com/randometc/ejs-locals * (The MIT License) * * Copyright (c) 2012 Robert Sköld <robert@publicclass.se> Copyright (c) 2012 Tom Carden <tom@tom-carden.co.uk> * * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: * ------------------------------------------------------------------------------------------ * For further explanation, see: * => http://sailsjs.com/documentation/concepts/views/layouts#?why-do-layouts-only-work-for-ejs */ module.exports = function renderFile(file, options, fn){ // Express used to set options.locals for us, but now we do it ourselves // (EJS does some __proto__ magic to expose these funcs/values in the template) if (!options.locals) { options.locals = {}; } // Make sure the absolute path to view is always available. // (needed for partials) options.filename = file; if (!options.locals.blocks) { // one set of blocks no matter how often we recurse var blocks = { scripts: new Block(), stylesheets: new Block() }; options.locals.blocks = blocks; options.locals.scripts = blocks.scripts; options.locals.stylesheets = blocks.stylesheets; options.locals.block = block.bind(blocks); options.locals.stylesheet = stylesheet.bind(blocks.stylesheets); options.locals.script = script.bind(blocks.scripts); } // override locals for layout/partial bound to current options options.locals.layout = layout.bind(options); options.locals.partial = partial.bind(options); ejs.renderFile(file, _.extend(options, options.locals), options, function(err, html) { if (err) { return fn(err,html); } var layout = options.locals._layoutFile; // for backward-compatibility, allow options to // set a default layout file for the view or the app // (NB:- not called `layout` any more so it doesn't // conflict with the layout() function) if (layout === undefined) { layout = options._layoutFile; } if (layout) { // use default extension var engine = options.settings['view engine'] || 'ejs'; var desiredExt = '.'+engine; // apply default layout if only "true" was set if (layout === true) { layout = path.sep + 'layout' + desiredExt; } if (extname(layout) !== desiredExt) { layout += desiredExt; } // clear to make sure we don't recurse forever (layouts can be nested) delete options.locals._layoutFile; delete options._layoutFile; // make sure caching works inside ejs.renderFile/render delete options.filename; if (layout.length > 0 && layout[0] === path.sep) { // if layout is an absolute path, find it relative to view options: layout = join(options.settings.views, layout.slice(1)); } else { // otherwise, find layout path relative to current template: layout = resolve(dirname(file), layout); } // now recurse and use the current result as `body` in the layout: options.locals.body = html; renderFile(layout, options, fn); } else { // no layout, just do the default: fn(null, html); } }); }; /** * Memory cache for resolved object names. */ var cache = {}; /** * Resolve partial object name from the view path. * (Or, for performance, use the cached version if available.) * * Examples: * * "user.ejs" becomes "user" * "forum thread.ejs" becomes "forumThread" * "forum/thread/post.ejs" becomes "post" * "blog-post.ejs" becomes "blogPost" * * @return {String} * @api private */ function resolveObjectName(view){ if (cache[view]) { return cache[view]; } cache[view] = view.split('/') .slice(-1)[0] .split('.')[0] .replace(/^_/, '') .replace(/[^a-zA-Z0-9 ]+/g, ' ') .split(/ +/).map(function(word, i){ return i ? word[0].toUpperCase() + word.substr(1) : word; }).join(''); return cache[view]; } /** * Lookup partial path from base path of current template: * * - partial `_<name>` * - any `<name>/index` * - non-layout `../<name>/index` * - any `<root>/<name>` * - partial `<root>/_<name>` * * Options: * * - `cache` store the resolved path for the view, to avoid disk I/O * * @param {String} root, full base path of calling template * @param {String} partial, name of the partial to lookup (can be a relative path) * @param {Object} options, for `options.cache` behavior * @return {String} * @api private */ function lookup(root, partial, options){ var engine = options.settings['view engine'] || 'ejs'; var desiredExt = '.' + engine; var ext = extname(partial) || desiredExt; var key = [ root, partial, ext ].join('-'); if (options.cache && cache[key]) { return cache[key]; } // Make sure we use dirname in case of relative partials // ex: for partial('../user') look for /path/to/root/../user.ejs var dir = dirname(partial); var base = basename(partial, ext); // _ prefix takes precedence over the direct path // ex: for partial('user') look for /root/_user.ejs partial = resolve(root, dir,'_'+base+ext); if( exists(partial) ) { if (options.cache) { cache[key] = partial; } return partial; } // Try the direct path // ex: for partial('user') look for /root/user.ejs partial = resolve(root, dir, base+ext); if( exists(partial) ) { if (options.cache) { cache[key] = partial; } return partial; } // Try index // ex: for partial('user') look for /root/user/index.ejs partial = resolve(root, dir, base, 'index'+ext); if( exists(partial) ) { if (options.cache) { cache[key] = partial; } return partial; } // FIXME: // * there are other path types that Express 2.0 used to support but // the structure of the lookup involved View class methods that we // don't have access to any more // * we probaly need to pass the Express app's views folder path into // this function if we want to support finding partials relative to // it as well as relative to the current view // * we have no tests for finding partials that aren't relative to // the calling view return null; } /** * Render `view` partial with the given `options`. Optionally a * callback `fn(err, str)` may be passed instead of writing to * the socket. * * Options: * * - `object` Single object with name derived from the view (unless `as` is present) * * - `as` Variable name for each `collection` value, defaults to the view name. * * as: 'something' will add the `something` local variable * * as: this will use the collection value as the template context * * as: global will merge the collection value's properties with `locals` * * - `collection` Array of objects, the name is derived from the view name itself. * For example _video.html_ will have a object _video_ available to it. * * @param {String} view * @param {Object|Array} options, collection or object * @return {String} * @api private */ function partial(view, options){ var collection; var object; var locals; var name; // parse options if( options ){ // collection if( options.collection ){ collection = options.collection; delete options.collection; } else if( 'length' in options ){ collection = options; options = {}; } // locals if( options.locals ){ locals = options.locals; delete options.locals; } // object if( 'Object' !== options.constructor.name ){ object = options; options = {}; } else if( options.object !== undefined ){ object = options.object; delete options.object; } } else { options = {}; } // merge locals into options if( locals ) { options.__proto__ = locals; } // merge app locals into options for(var k in this) { options[k] = options[k] || this[k]; } // extract object name from view name = options.as || resolveObjectName(view); // find view, relative to this filename // NOTE -- the original `dirname(options.filename)` stopped working // after ejs-locals was inlined, perhaps due to changed in EJS. // The `options.absPathToView` value is set above in the // main `renderFile` function. -SMG 10/27/2016 var root = dirname(options.filename); var file = lookup(root, view, options); var key = file + ':string'; if( !file ) { throw new Error('Could not find partial ' + view); } // read view var source = options.cache ? cache[key] || (cache[key] = fs.readFileSync(file, 'utf8')) : fs.readFileSync(file, 'utf8'); // Update the options.filename to point to the current partial, // so that relative paths can work in calling nested partials. // -SMG 10/27/2016 options.filename = file; // re-bind partial for relative partial paths options.partial = partial.bind(options); // render partial function render(){ if (object) { if ('string' === typeof name) { options[name] = object; } else if (name === global) { // wtf? // merge(options, object); } } // TODO Support other templates (but it's sync now...) var html = ejs.render(source, options, options); return html; } // Collection support if (collection) { var len = collection.length; var buf = ''; var keys; var prop; var val; var i; if ('number' === typeof len || Array.isArray(collection)) { options.collectionLength = len; for (i = 0; i < len; ++i) { val = collection[i]; options.firstInCollection = i === 0; options.indexInCollection = i; options.lastInCollection = i === len - 1; object = val; buf += render(); } } else { keys = Object.keys(collection); len = keys.length; options.collectionLength = len; options.collectionKeys = keys; for (i = 0; i < len; ++i) { prop = keys[i]; val = collection[prop]; options.keyInCollection = prop; options.firstInCollection = i === 0; options.indexInCollection = i; options.lastInCollection = i === len - 1; object = val; buf += render(); } } return buf; } else { return render(); } } /** * Apply the given `view` as the layout for the current template, * using the current options/locals. The current template will be * supplied to the given `view` as `body`, along with any `blocks` * added by child templates. * * `options` are bound to `this` in renderFile, you just call * `layout('myview')` * * @param {String} view * @api private */ function layout(view){ this.locals._layoutFile = view; } function Block() { this.html = []; } Block.prototype = { toString: function() { return this.html.join('\n'); }, append: function(more) { this.html.push(more); }, prepend: function(more) { this.html.unshift(more); }, replace: function(instead) { this.html = [ instead ]; } }; /** * Return the block with the given name, create it if necessary. * Optionally append the given html to the block. * * The returned Block can append, prepend or replace the block, * as well as render it when included in a parent template. * * @param {String} name * @param {String} html * @return {Block} * @api private */ function block(name, html) { // bound to the blocks object in renderFile var blk = this[name]; if (!blk) { // always create, so if we request a // non-existent block we'll get a new one blk = this[name] = new Block(); } if (html) { blk.append(html); } return blk; } // bound to scripts Block in renderFile function script(path, type) { if (path) { this.append('<script src="'+path+'"'+(type ? 'type="'+type+'"' : '')+'></script>'); } return this; } // bound to stylesheets Block in renderFile function stylesheet(path, media) { if (path) { this.append('<link rel="stylesheet" href="'+path+'"'+(media ? 'media="'+media+'"' : '')+' />'); } return this; } ================================================ FILE: lib/hooks/views/escape-html-entities-deep.js ================================================ /** * Module dependencies */ var _ = require('@sailshq/lodash'); var rttc = require('rttc'); /** * escapeHtmlEntitiesDeep() * * Escape all HTML entities which exist as strings in the provided * data. If the provided data contains any dictionaries or arrays, * traverse them recursively. Note that the returned value will be * JSON-compatible, and the dehydration process will be carried out * using the rules established in rttc.dehydrate(). * * @param {Dictionary} data * The dictionary of data to escape. * * @returns {JSON} a recursively-HTML-escaped copy of the provided data. */ module.exports = function escapeHtmlEntitiesDeep(data){ return rttc.rebuild(data, function escape(val, type){ // _.escape() is for escaping strings for use in HTML. // (this is just the same thing that Lodash uses when you use `<%- %>` in templates) if (type === 'string') { return _.escape(val); } else { return val; } }); }; ================================================ FILE: lib/hooks/views/get-implicit-defaults.js ================================================ /** * getImplicitDefaults() * * Get a dictionary of implicit defaults this hook would like to merge * into `sails.config` when Sails is loaded. * * @param {Dictionary} existingConfig * Existing configuration which has already been loaded * e.g. the Sails app path, and any config overrides (programmtic, from .sailsrc, etc) * * @returns {Dictionary} */ module.exports = function getImplicitDefaults (existingConfig) { return { views: { // Extension for view files extension: 'ejs', // Layout is on by default, in the top level of the view directory // false === don't use a layout // string === path to layout (absolute or relative to views directory), without extension layout: 'layout' }, paths: { views: existingConfig.appPath + '/views', layout: existingConfig.appPath + '/views/layout.ejs' } }; }; ================================================ FILE: lib/hooks/views/html-scriptify.js ================================================ /** * Module dependencies */ var util = require('util'); var _ = require('@sailshq/lodash'); var rttc = require('rttc'); var escapeHtmlEntitiesDeep = require('./escape-html-entities-deep'); var minifiedUnescapeHtmlEntitiesDeepLiteStr = require('./unescape-html-entities-deep-lite.min.string.js'); /** * htmlScriptify() * * Generate a string of HTML code that can be injected onto a page * in order to expose a JSON-serializable version of the provided * data to client-side JavaScript. * * @required {Dictionary} options.data * The dictionary (i.e. of locals) that will be converted into an HTML snippet containing * our script tag. If any of the keys cannot be coerced to be JSON-serializable (i.e. * contain nothing that isn't a string, number, boolean, plain dictionary, array, or null), * then they are simply excluded. Additionally, each key is recursively parsed to snip off * any circular references and otherwise ensure full JSON-serializability of ever nested key * therein. See rttc.dehydrate() for more information. * * @optional {Array} options.keys * An array of strings; the names of keys in data which should be exposed to on the namespace. If left unspecified, all keys in data will be exposed. * * @optional {String} options.namespace * The name of the key on the window object where data should be exposed. Defaults to 'SAILS_LOCALS'. * * @optional {Boolean} options.dontUnescapeOnClient * Defaults to false. When false (by default) client-side JavaScript code will be * injected around the exposed data. When the page loads, the injected client-side * JavaScript runs, unescaping the values so that they are accessible to client-side * JavaScript with no further transformation necessary (i.e. they are immediately * usable just like they would be if they had been fetched using AJAX). If this flag * is enabled, no additional client-side JavaScript code will be injected and so the * exposed values will still be escaped; e.g. `window.SAILS_LOCALS.funnyFace === '<o_o>'` * (this is useful for customizing client-side unescaping logic) * * * @returns {String} a string of HTML code-- specifically a script tag containing the exposed data. * * -- * Example usage: (`sails console`) * sails> sails.hooks.views.htmlScriptify({data: {n: 'stuff<script>'}, dontUnescapeOnClient: true}) */ module.exports = function htmlScriptify(options){ // ██╗ ██╗ █████╗ ██╗ ██╗██████╗ █████╗ ████████╗███████╗ ██╗ ██╗███████╗ █████╗ ██████╗ ███████╗ // ██║ ██║██╔══██╗██║ ██║██╔══██╗██╔══██╗╚══██╔══╝██╔════╝ ██║ ██║██╔════╝██╔══██╗██╔════╝ ██╔════╝ // ██║ ██║███████║██║ ██║██║ ██║███████║ ██║ █████╗ ██║ ██║███████╗███████║██║ ███╗█████╗ // ╚██╗ ██╔╝██╔══██║██║ ██║██║ ██║██╔══██║ ██║ ██╔══╝ ██║ ██║╚════██║██╔══██║██║ ██║██╔══╝ // ╚████╔╝ ██║ ██║███████╗██║██████╔╝██║ ██║ ██║ ███████╗ ╚██████╔╝███████║██║ ██║╚██████╔╝███████╗ // ╚═══╝ ╚═╝ ╚═╝╚══════╝╚═╝╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚══════╝ ╚═════╝ ╚══════╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝ // if (!_.isObject(options)) { throw new Error('Usage: A dictionary of options should be provided as the sole argument `htmlScriptify()`'); } // Verify `data`. if (_.isUndefined(options.data)) { throw new Error('Usage: `data` is a required option'); } if (!_.isObject(options.data) || _.isArray(options.data) || _.isFunction(options.data)) { throw new Error( 'Usage: `data` should be provided as a dictionary. But instead, got: '+ util.inspect(options.data,{depth:null}) ); }//--• // Verify `keys`, if provided. // // > If unspecified, we leave `options.keys` as `undefined`. // > (this indicates that all keys in provided data are permitted; i.e. no whitelist.) if (!_.isUndefined(options.keys)) { try { options.keys = rttc.validate(['string'], options.keys); } catch (e) { if (e.code === 'E_INVALID') { throw new Error('Usage: If provided, `keys` should be an array of strings'); } else { throw e; } } }//--• else { // (if unspecified, we leave `options.keys` as `undefined`) }//>- // Verify `namespace`, if provided (or use default) // // > Note that while we might also consider validating `namespace` as an // > ecmascript-compatible variable name, in the interest of avoiding any // > more dependencies here, we do not. if (!_.isUndefined(options.namespace)) { try { options.namespace = rttc.validate('string', options.namespace); } catch (e) { if (e.code === 'E_INVALID') { throw new Error('Usage: If provided, `namespace` should be a string'); } else { throw e; } } } else { options.namespace = 'SAILS_LOCALS'; }//>- // Verify `dontUnescapeOnClient` flag, if provided (or use default) // // > If special backwards-compatible support for older browsers is needed, or any // > other customizations to the client-side escaping code are necessary, then // > the built-in client-side escaping can be disabled using `dontUnescapeOnClient: true`. if (!_.isUndefined(options.dontUnescapeOnClient)) { try { options.dontUnescapeOnClient = rttc.validate('boolean', options.dontUnescapeOnClient); } catch (e) { if (e.code === 'E_INVALID') { throw new Error('Usage: If provided, `dontUnescapeOnClient` should be either `true` or `false`'); } else { throw e; } } } else { options.dontUnescapeOnClient = false; }//>- // console.log('options.data',options.data); // console.log('_.keys(options.data)',_.keys(options.data)); // ██████╗ ██╗ ██╗██╗██╗ ██████╗ ██╗ ██╗████████╗███╗ ███╗██╗ // ██╔══██╗██║ ██║██║██║ ██╔══██╗ ██║ ██║╚══██╔══╝████╗ ████║██║ // ██████╔╝██║ ██║██║██║ ██║ ██║ ███████║ ██║ ██╔████╔██║██║ // ██╔══██╗██║ ██║██║██║ ██║ ██║ ██╔══██║ ██║ ██║╚██╔╝██║██║ // ██████╔╝╚██████╔╝██║███████╗██████╔╝ ██║ ██║ ██║ ██║ ╚═╝ ██║███████╗ // ╚═════╝ ╚═════╝ ╚═╝╚══════╝╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝╚══════╝ // // ████████╗ ██████╗ ██████╗ ███████╗████████╗██╗ ██╗██████╗ ███╗ ██╗ // ╚══██╔══╝██╔═══██╗ ██╔══██╗██╔════╝╚══██╔══╝██║ ██║██╔══██╗████╗ ██║ // ██║ ██║ ██║ ██████╔╝█████╗ ██║ ██║ ██║██████╔╝██╔██╗ ██║ // ██║ ██║ ██║ ██╔══██╗██╔══╝ ██║ ██║ ██║██╔══██╗██║╚██╗██║ // ██║ ╚██████╔╝ ██║ ██║███████╗ ██║ ╚██████╔╝██║ ██║██║ ╚████║ // ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═══╝ // // Build and return HTML to inject. var html = '<script type="text/javascript">'; html += ' (function (){ '; // By default, we inject client-side code to perform unescaping. // But if the `dontUnescapeOnClient` flag was enabled, then we don't build the // code to do the unescaping. (useful for customizing client-side unescaping logic; // e.g. for legacy browser compatibility <= IE 8) if (!options.dontUnescapeOnClient) { // Inject client-side JavaScript code that will be used to unescape the // bootstrapped data. // // This is kind of like _.unescape()...except it also has to be recursive. // Luckily, we don't have to worry about circular references or any of the // other not-quite-JSON stuff, since we know this was just serialized. html += ' var unescape = ' + minifiedUnescapeHtmlEntitiesDeepLiteStr + ';'; }//</if :: needed to inject client-side unescape function (`dontUnescapeOnClient` flag was NOT enabled)> //>- html += ' window.'+options.namespace+' = { '; // Determine the relevant keys to inject. // (filtering using `options.keys` whitelist, if provided) var keysToInject = _.keys(options.data); if (!_.isUndefined(options.keys)) { keysToInject = _.intersection(keysToInject, options.keys); } // Then inject them in our <script> tag string. _.each(keysToInject, function eachRelevantKey(key){ var unsafeVal = options.data[key]; // If this top-level key in the provided data is undefined, exclude it altogether. if (_.isUndefined(unsafeVal)) { return; } // Now, dive into `unsafeVal` and recursively HTML-escape any nested strings. // Then, compile the whole thing into a JavaScript string which will accurately // represent it as an r-value (watching out for circular refs along the way). var escapedData = rttc.compile(escapeHtmlEntitiesDeep(unsafeVal)); // If the `dontUnescapeOnClient` flag was set, then just stick the compiled, // still-HTML-escaped data in place. (It will have to be recursively unescaped // by hand in the app's custom client-side code!) if (options.dontUnescapeOnClient) { html += ''+key+': '+escapedData+','; } // Otherwise, we're including the client-side code to unescape the data, // so run our unescape function from above. else { html += ''+key+': unescape('+escapedData+'),'; } }); html += ' }; '; html += ' })(); '; html += '</script>'; return html; }; ================================================ FILE: lib/hooks/views/index.js ================================================ /** * Module dependencies */ var configure = require('./configure'); var getImplicitDefaults = require('./get-implicit-defaults'); var onRoute = require('./onRoute'); var defaultViewRenderingFn = require('./default-view-rendering-fn'); var addResViewMethod = require('./res.view'); var render = require('./render'); var statViews = require('./stat-views'); var htmlScriptify = require('./html-scriptify'); module.exports = function (sails) { /** * `views` hook */ return { defaults: getImplicitDefaults, // The view rendering function -- may be overriden if `sails.config.views.getRenderFn` is provided. _renderFn: defaultViewRenderingFn, configure: function (){ configure(sails); }, render: render(sails), htmlScriptify: htmlScriptify, /** * Standard responsibilities of `initialize` are to load middleware methods * and listen for events to know when to bind any special routes. * * @api private */ initialize: function (cb) { if (!sails.hooks.http) { var err = new Error('`views` hook requires the `http` hook, but the `http` hook is disabled. Please enable both or neither.'); err.code = 'E_HOOKINIT_DEP'; err.type = err.code;//<<TODO: remove this err.name = 'failed requires `http` hook'; return cb(err); } // Before handing off incoming requests, bind handler that adds the `res.view()` method to `res`. // (flagging middleware along the way) addResViewMethod._middlewareType = 'VIEWS HOOK: addResViewMethod'; sails.on('router:before', function () { // But wait until after internationalization has happened // (if applicable) if (sails.hooks.i18n) { sails.after('hook:i18n:loaded', function () { sails.router.bind('/*', addResViewMethod, 'all', { }); }); } else { sails.router.bind('/*', addResViewMethod, 'all'); } }); // Register `{view:'/foo'}` route target syntax. sails.on('route:typeUnknown', function (route){ return onRoute(sails, route); }); // Expose `sails.renderView()` function to userland. // (experimental!) sails.renderView = this.render; // Check for the existence of view files. // // This existence tree is used later to detect // and prepare implicit actions for each view file // to support routes with targets like `{view:'...'}`. statViews(sails, this, function (err){ if (err) { return cb(err); } return cb(); }); } }; }; ================================================ FILE: lib/hooks/views/onRoute.js ================================================ /** * Module dependencies */ var path = require('path'); var _ = require('@sailshq/lodash'); /** * onRoute() * * This is used for handling `route:typeUnknown` events; i.e. to * support { view: 'foo/bar' } notation. This "teaches" the router * to understand `view` in route target syntax. This allows route * addresses to be bound directly to serve specific views without * going through a custom action. * * e.g. * ``` * 'get /': { view: 'pages/homepage' } * ``` * * @param {Sails} sails * @param {Dictionary} route * combined route target + route address dictionary */ module.exports = function onRoute (sails, route) { // Ignore unknown route syntax if ( !_.isPlainObject(route.target) || !_.isString(route.target.view) ) { // If it needs to be understood by another hook, the hook would have also received // the typeUnknown event, so we don't need to do anything here. return; } // Ensure there isn't a `.` in the view name. // (This limitation will be improved in a future version of Sails.) else if (route.target.view.match(/\./)) { sails.log.error('Ignoring attempt to bind route (`%s`) to a view with a `.` in the name (`%s`).',route.path, route.target.view); return; } // Otherwise construct an action function which serves a view and then bind it to the route. else { // Merge target into `options` to get hold of relevant route options: route.options = _.merge(route.options, route.target); // Note: this (^) could be moved up into lib/router/bind.js, since its // only pertinent for core options such as `skipAssets`. There would need // to be changes in other hooks as well. // Transform the view path into something Lodash _.get will understand (i.e. dots not slashes) var transformedViewAddress = route.target.view.replace(/\//g, '.'); var referenceInViewsHash = _.get(sails.views, transformedViewAddress); // Look up the view in our views hash and see if it is `true` // (i.e. indicating it is a view template file) if (referenceInViewsHash === true) { return sails.router.bind(route.path, function serveView(req, res) { return res.view(route.target.view); }, route.verb, route.options); } // Look for a relative `/index` view if the specified view // is in the views hash as a dictionary (i.e. indicating it is a directory) else if (_.isObject(referenceInViewsHash) && referenceInViewsHash.index === true) { var indexViewIdentity = path.join(route.target.view,'/index'); return sails.router.bind(route.path, function serveView(req, res) { return res.view(indexViewIdentity); }, route.verb, route.options); } // Otherwise, the specified view in this route target doesn't match a // known view in the project, so ignore the attempt and inform the user. else { sails.log.error('Ignoring attempt to bind route (`%s`) to unknown view: `%s`',route.path, route.target.view); } } }; ================================================ FILE: lib/hooks/views/render.js ================================================ /** * Module dependencies */ var path = require('path'); var _ = require('@sailshq/lodash'); var flaverr = require('flaverr'); var parley = require('parley'); /** * @param {SailsApp} sails * @return {Function} */ module.exports = function (sails) { /** * renderView() * * Return the HTML string obtained by loading and compiling the specified view * template with a dictionary of dynamic runtime data (and/or other view engine * other view engine options like `layout`.) * * Usage: * ``` * var html = await sails.renderView('emails/reminders/email-verify-your-account', { * fullName: 'Maggie Thatcher', * accountCreatedAt: 1510521386197 * }); * ``` * * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * @param {String} relPathToView * Path to the view file to load (minus the file extension, and * relative to the app's `views/` directory.) * * @param {Dictionary} _options * View locals and/or options to pass to template renderer. * * @param {Function} optionalCb(err, compiledHtml) * An optional callback. If provided, it will be called when the * view rendering is complete and this function will return `undefined` * instead of returning a Deferred. * @param {Error} err * @param {String} compiledHtml * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * * @returns {Deferred?} * (See https://npmjs.com/package/parley for more information.) * * @api public */ return function renderView(relPathToView, _options, optionalCb) { // Build an omen, if appropriate. var omen = flaverr.omen(renderView); // Build & return Deferred (or if callback was provided, immediately run // the following logic and then trigger that callback.) return parley( function (done) { // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // TODO: pull out shared logic between this file and res.view.js // into a separate file. // // TODO: reuse code in res.view.js for most of this to make it more maintainable // currently it is not in sync w/ improvements/fixes in that other module. // // (^^^ In particular, note that `exposeLocalsToBrowser` et al is not available // here in sails.renderView() as of yet) // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - if (!relPathToView && !_.isString(relPathToView)) { return done(flaverr({ message: 'First argument must be a string -- the relative path to the desired template (from the `views/` folder).' }), omen); }//• // Shallow clone the provided locals, since we'll be making some modifications. // > (Note that this IS NO LONGER A DEEP CLONE! So references to sails config etc // > provided to views should be respected, and not changed.) var options = _.extend({}, _options || {}); // Trim trailing slash if (relPathToView[(relPathToView.length - 1)] === '/') { relPathToView = relPathToView.slice(0, -1); } // if local `layout` is set to true or unspecified // fall back to global config var layout = options.layout; if (layout === undefined || layout === true) { layout = sails.config.views.layout; } // Disable sails built-in layouts for all view engine's except for ejs if (sails.config.views.getRenderFn) { layout = false; } var pathToViews = sails.config.paths.views; var absPathToView = path.join(pathToViews, relPathToView) + '.' + sails.config.views.extension; // Set layout file if enabled (using ejs-locals) if (layout) { // If a layout was specified, set view local so it will be used options._layoutFile = layout; } options.view = options.view || { path: relPathToView, pathFromViews: relPathToView, pathFromApp: path.join(path.relative(sails.config.appPath, sails.config.paths.views), relPathToView), ext: sails.config.views.extension }; // In development, provide access to complete path to view // via `__dirname` if (process.env.NODE_ENV !== 'production') { options.__dirname = options.__dirname || absPathToView + '.' + sails.config.views.extension; } // Handle compatibility issues with certain view rendering engines. // > Copy all the current options into 'locals', and explicitly set two settings, // > just in case the template engine expects them there. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // TODO: Explain which view engines require this in comments here, or if that // doesn't happen, then remove this code. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - _.extend(options, { locals: _.extend({}, options), settings: { 'view engine': sails.config.views.extension, 'views': sails.config.paths.views } }); // If the i18n hook is enabled, internationalize before proceeding. (function _internationalizingIfRelevant(proceed){ if (!sails.hooks.i18n) { return proceed(); } // Set up a minimal mock request for the i18n hook to use. var req = { headers: {} }; // If a locale was specified as an option, render the view with that locale req.headers['accept-language'] = options.locale || sails.hooks.i18n.defaultLocale; req.locale = options.locale || sails.hooks.i18n.defaultLocale; sails.hooks.i18n.expressMiddleware(req, options, function(err) { if (err) { return proceed(err); } return proceed(); });//_∏_ })(function(err) { if (err) { return done(err); } // Finally, compile the view template. sails.hooks.views._renderFn(absPathToView, options, function(err, compiledHtml){ if (err) { return done(err); } return done(undefined, compiledHtml); });//_∏_ });//_∏_ (†) }, optionalCb, undefined, undefined, omen );//…) };//ƒ }; ================================================ FILE: lib/hooks/views/res.view.js ================================================ /** * Module dependencies */ var path = require('path'); var util = require('util'); var _ = require('@sailshq/lodash'); var htmlScriptify = require('./html-scriptify'); /** * Adds `res.view()` (an enhanced version of res.render) and `res.guessView()` methods to response object. * `res.view()` automatically renders the appropriate view based on the calling middleware's source route * Note: the original function is still accessible via res.render() * * @param {Request} req * @param {Response} res * @param {Function} next */ module.exports = function _addResViewMethod(req, res, next) { var sails = req._sails; // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // TODO: don't pass `next` into this method impl to avoid confusing situations. // i.e. wrap it up: // ``` // function (req,res,next) { _addResViewMethod(req,res); next(); } // ``` // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /** * res.guessView([locals], [couldNotGuessCb]) * * @param {Object} locals * @param {Function} couldNotGuessCb */ res.guessView = function (locals, couldNotGuessCb) { // - - - - - - - - - - - - - - - - - - - - - - - - - - - // FUTURE: Completely remove res.guessView() // - - - - - - - - - - - - - - - - - - - - - - - - - - - sails.log.warn( '`res.guessView()` is deprecated in Sails >= v1.0. If you want to continue to use it\n'+ 'in your Sails app, please just drop its implementation into a hook.\n'+ ' [?] Unsure or need advice? Stop by https://sailsjs.com/support' ); return res.view(locals, function viewReady(err, html) { // If this is an "implied view doesn't exist" error, // just serve JSON instead. if (err && (err.code === 'E_VIEW_INFER' || err.code === 'E_VIEW_FAILED')) { return (couldNotGuessCb||res.serverError)(err); } // But if some other sort of error occurred, call `res.serverError` else if (err) {return res.serverError(err);} // Otherwise we're good, serve the view return res.send(html); }); };//</defun:: res.guessView()> /** * res.view([specifiedPath|locals], [locals]) * * @param {String} specifiedPath * -> path to the view file to load (minus the file extension) * relative to the app's `views` directory * @param {Object} locals * -> view locals (data that is accessible in the view template) * @param {Function} optionalCb(err) * -> called when the view rendering is complete (response is already sent, or in the process) * (probably should be @api private) * @api public */ res.view = function(/* specifiedPath, locals, optionalCb */) { var specifiedPath = arguments[0]; var locals = arguments[1]; var optionalCb = arguments[2]; // sails.log.silly('Running res.view() with arguments:',arguments); // By default, generate a path to the view using what we know about the controller+action var relPathToView; // Ensure req.target is an object, then merge it into req.options // (this helps ensure backwards compatibility for users who were relying // on `req.target` in previous versions of Sails) req.options = _.defaults(req.options, req.target || {}); // Try to guess the view by looking at the controller/action if (!req.options.view && req.options.action) { relPathToView = req.options.action.replace(/\./g, '/'); } // Use the new view config else {relPathToView = req.options.view;} // Now we have a reasonable guess in `relPathToView` // If the path to a view was explicitly specified, use that // Serve the view specified // // If first arg is not an obj or function, treat it like the path if (specifiedPath && !_.isObject(specifiedPath) && !_.isFunction(specifiedPath)) { relPathToView = specifiedPath; } // If the "locals" argument is actually the "specifiedPath" // give em the old switcheroo if (!relPathToView && _.isString(arguments[1])) { relPathToView = arguments[1] || relPathToView; } // If first arg ISSSSS AN object, treat it like locals if (_.isObject(arguments[0])) { locals = arguments[0]; } // If the second argument is a function, treat it like the callback. if (_.isFunction(arguments[1])) { optionalCb = arguments[1]; // In which case if the first argument is a string, it means no locals were specified, // so set `locals` to an empty dictionary and log a warning. if (_.isString(arguments[0])) { sails.log.warn('`res.view` called with (path, cb) signature (using path `' + specifiedPath + '`). You should use `res.view(path, {}, cb)` to render a view without local variables.'); locals = {}; } } // if (_.isFunction(locals)) { // optionalCb = locals; // locals = {}; // } // if (_.isFunction(specifiedPath)) { // optionalCb = specifiedPath; // } // If a view path cannot be inferred, send back an error instead if (!relPathToView) { var err = new Error(); err.name = 'Error in res.view()'; err.code = 'E_VIEW_INFER'; err.type = err.code;// <<TODO remove this err.message = 'No path specified, and no path could be inferred from the request context.'; // Prevent endless recursion: if (req._errorInResView) { return res.sendStatus(500); } req._errorInResView = err; if (optionalCb) { return optionalCb(err); } else {return res.serverError(err);} } // Ensure specifiedPath is a string (important) relPathToView = '' + relPathToView + ''; // Ensure `locals` is an object locals = _.isObject(locals) ? locals : {}; // Mixin locals from req.options. // TODO -- replace this _.merge() with a call to the merge-dictionaries module? if (req.options.locals) { locals = _.merge(locals, req.options.locals); } // Merge with config views locals if (sails.config.views.locals) { // Formerly a deep merge: `_.merge(locals, sails.config.views.locals, _.defaults);` // Now shallow- see https://github.com/balderdashy/sails/issues/3500 _.defaults(locals, sails.config.views.locals); } // If the path was specified, but invalid // else if (specifiedPath) { // return res.serverError(new Error('Specified path for view (' + specifiedPath + ') is invalid!')); // } // Trim trailing slash if (relPathToView[(relPathToView.length - 1)] === '/') { relPathToView = relPathToView.slice(0, -1); } var pathToViews = sails.config.paths.views; var absPathToView = path.join(pathToViews, relPathToView); var absPathToLayout; var relPathToLayout; var layout = false; // Deal with layout options only if there is no custom rendering function in place -- // that is, only if we're using the default EJS layouts. if (!sails.config.views.getRenderFn) { layout = locals.layout; // If local `layout` is set to true or unspecified // fall back to global config if (locals.layout === undefined || locals.layout === true) { layout = sails.config.views.layout; } // Allow `res.locals.layout` to override if it was set: if (typeof res.locals.layout !== 'undefined') { layout = res.locals.layout; } // At this point, layout should be either `false` or a string if (typeof layout !== 'string') { layout = false; } // Set layout file if enabled (using ejs-locals) if (layout) { // Solve relative path to layout from the view itself // (required by ejs-locals module) absPathToLayout = path.join(pathToViews, layout); relPathToLayout = path.relative(path.dirname(absPathToView), absPathToLayout); // If a layout was specified, set view local so it will be used res.locals._layoutFile = relPathToLayout; // sails.log.silly('Using layout at: ', absPathToLayout); } } // Locals passed in to `res.view()` override app and route locals. _.each(locals, function(local, key) { res.locals[key] = local; }); // Provide access to view metadata in locals // (for convenience) if (_.isUndefined(res.locals.view)) { res.locals.view = { path: relPathToView, absPath: absPathToView, pathFromViews: relPathToView, pathFromApp: path.join(path.relative(sails.config.appPath, sails.config.paths.views), relPathToView), ext: sails.config.views.extension }; } // Set up the `exposeLocalsToBrowser` view helper method // (unless there is already a local by the same name) // // Background: // -> https://github.com/balderdashy/sails/pull/3522#issuecomment-174242822 if (_.isUndefined(res.locals.exposeLocalsToBrowser)) { res.locals.exposeLocalsToBrowser = function (options){ if (!_.isObject(options)) { options = {}; } // Note: // We get access to locals using a reference obtained via closure-- // and since this view helper won't be used until AFTER the rest of // the code in THIS file has run, we know any relevant changes to // `locals` below will be available, since we're referring to the // same object. // Note that we include both explicit locals passed to res.view(), // and implicitly-set locals from `res.locals`. But we exclude // non-relevant built-in properties like `sails` and `_`, as well // as experimental properties like `view`. // // Also note that we create a new dictionary to avoid tampering. var relevantLocals = {}; _.each(_.union(_.keys(res.locals), _.keys(locals)), function(localName){ // Explicitly exclude `_locals`, which appears even in explicit locals. // (FUTURE: longer term, could look into doing this _everywhere_ as an optimization- // but need to investigate other view engines for potential differences) if (localName === '_locals') {} // Explicitly exclude `layout`, since it has special meaning, // even when it appears even in explicit locals. else if (localName === 'layout') {} // Otherwise, use explicit local, if available else if (locals[localName] !== undefined) { relevantLocals[localName] = locals[localName]; } // Otherwise, use the one from res.locals... maybe. else { if (localName === '_csrf') { // Special case for CSRF token // > If the security hook is disabled, there won't be a CSRF token in the locals. // > If the hook is enabled but CSRF is disabled for this route, the token will // > be an empty string. In either of those cases we can just skip it. if (res.locals._csrf !== undefined && res.locals._csrf !== '') { relevantLocals._csrf = res.locals._csrf; } } else if (_.contains(['_', 'sails', 'view', 'session', 'req', 'res', '__dirname', '_layoutFile'], localName)) { // Exclude any other auto-injected implicit locals } else if (_.isFunction(res.locals[localName])) { // Exclude any functions } else { // Otherwise include it! relevantLocals[localName] = res.locals[localName]; } } });//∞ // Return an HTML string which includes a special script tag. return htmlScriptify({ data: relevantLocals, keys: options.keys, namespace: options.namespace, dontUnescapeOnClient: options.dontUnescapeOnClient }); };//</defun :: res.locals.exposeLocalsToBrowser()> }//>- // Unless this is production, provide access to complete view path to view via `__dirname` local. if (process.env.NODE_ENV !== 'production') { res.locals.__dirname = res.locals.__dirname || (absPathToView + '.' + sails.config.views.extension); } // If silly logging is enabled, display some diagnostic information about the res.view() call: if (specifiedPath) { sails.log.silly('View override argument passed to res.view(): ', specifiedPath); } sails.log.silly('Serving view at rel path: ', relPathToView); sails.log.silly('View root: ', sails.config.paths.views); // Render the view return res.render(relPathToView, locals, function viewFailedToRender(err, renderedViewStr) { // Prevent endless recursion: if (err && req._errorInResView) { return res.status(500).send(err); } if (err) { req._errorInResView = err; // Ensure that if res.serverError() likes to serve views, // it won't this time because we ran into a view error. req.wantsJSON = true; // Enhance the raw Express view error object // (this error appears when a view is missing) if (_.isObject(err) && err.view) { err = _.extend({ message: util.format( 'Could not render view "%s". Tried locating view file @ "%s".%s', relPathToView, absPathToView, (layout ? util.format(' Layout configured as "%s", so tried using layout @ "%s")', layout, absPathToLayout) : '') ), code: 'E_VIEW_FAILED', status: 500 }, err); err.inspect = function () { return err.message; }; } } // If specified, trigger `res.view()` callback instead of proceeding if (typeof optionalCb === 'function') { // The provided optionalCb callback will receive the error (if there is one) // as the first argument, and the rendered HTML as the second argument. return optionalCb(err, renderedViewStr); } else { // if a template error occurred, don't rely on any of the Sails request/response methods // (since they may not exist yet at this point in the request lifecycle.) if (err) { ////////////////////////////////////////////////////////////////// // TODO: // Consider removing this log and deferring to the logging that is // happening in res.serverError() instead. // sails.log.error('Error rendering view at ::', absPathToView); // sails.log.error('with layout located at ::', absPathToLayout); // sails.log.error(err && err.message); // ////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////// // TODO: // Consider just calling some kind of default error handler fn here // in order to consolidate the "i dont know wtf i should do w/ this err" logic. // (keep in mind the `next` we have here is NOT THE SAME as the `next` from // the point when this error occurred! It is the next from when this // method was initially attached to the request object in the views hook.) // if (res.serverError) { req.wantsJSON = true; return res.serverError(err); } else if (process.env.NODE_ENV !== 'production') { return res.status(500).send(err); } else {return res.sendStatus(500);} // ////////////////////////////////////////////////////////////////// } // If verbose logging is enabled, write a log w/ the layout and view that was rendered. sails.log.verbose('Rendering view: "%s" (located @ "%s")', relPathToView,absPathToView); if (layout) { sails.log.verbose('• using configured layout:: %s (located @ "%s")', layout, absPathToLayout); } // Finally, send the compiled HTML from the view down to the client res.send(renderedViewStr); } }); };//</defun :: res.view() > next(); }; // Express version updates should be closely monitored. // Express is a "hard" dependency. // // While unlikely this will change, it's worth noting that this implementation // relies on express's private implementation of res.render() here: // https://github.com/visionmedia/express/blob/master/lib/response.js#L799 // // To be safe, the version of the Express dependency in package.json will remain locked // until it can be verified that each subsequent version is compatible. Even patch releases!! ================================================ FILE: lib/hooks/views/stat-views.js ================================================ /** * Stat view files and expose the existence tree on `sails.views`. * * @param {Sails} sails * @param {Hook} hook * @param {Function} cb * @param {Error} err * @param {Dictionary} detectedViews * * * @api private */ module.exports = function statViews (sails, hook, cb) { sails.modules.statViews(function (err, detectedViews) { if (err) { return cb(err); } // Save existence tree in `sails.views` for consumption later sails.views = detectedViews || {}; return cb(null, detectedViews); }); }; ================================================ FILE: lib/hooks/views/unescape-html-entities-deep-lite.min.string.js ================================================ // This module exports the `toString()`-ed, minified version of the function defined below (`unescapeHtmlEntitiesDeepLite()`). // // We could do something fancier, but realistically, this shouldn't need to change much. // If it does, the simplest thing to do is drop it into https://skalman.github.io/UglifyJS-online/ // then take the output from that, and run `toString()` on it in the Node REPL (that way you get // single quotes, vs. the double-quotes you'll get in the Chrome JavaScript console.) // // Last generated at: 13:49:09 CDT, Sep 28, 2016 module.exports = 'function unescapeHtmlEntitiesDeepLite(r){if("function"!=typeof Array.isArray||"function"!=typeof Array.prototype.forEach||"function"!=typeof Array.prototype.map||"function"!=typeof Object.keys)throw Error("Unsupported browser: Missing support for `Array.isArray`, `Array.prototype.forEach`, `Array.prototype.map`, or `Object.keys`! (Sails\' built-in HTML-unescaping for exposed locals supports IE9 and up.)");return function t(r){if(null===r)return r;if(r===!0||r===!1)return r;if("number"==typeof r)return r;if("string"==typeof r){var e=/&(?:amp|lt|gt|quot|#39|#96);/g,o=RegExp(e.source);if(""===r)return r;if(o.test(r)){var n={"&":"&","<":"<",">":">",""":\'"\',"'":"\'","`":"`"};return r=r.replace(e,function(r){return n[r]})}return r}return Array.isArray(r)?r=r.map(function(r){return t(r)}):(Object.keys(r).forEach(function(e){r[e]=t(r[e],e)}),r)}(r)}'; // The rest of the code in this file is NEVER ACTUALLY USED DIRECTLY. // It is here as a clear, simple point of reference for how the string above was generated. // ================================================================================================================== /** * Module dependencies */ // N/A // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // This logic DOES NOT rely on any dependencies. // This is so that the function can be used in the browser, or on the server. // // This function is also sensitive to browser compatibility. // (see http://kangax.github.io/compat-table/es5/ and check "Show obsolete platforms") // // The following code is based off of `_.unescape()` and `rttc.rebuild()`, the // latter of which is itself influenced by isaac's `JSON.stringifySafe()`. // (see https://github.com/isaacs/json-stringify-safe/commit/02cfafd45f06d076ac4bf0dd28be6738a07a72f9#diff-c3fcfbed30e93682746088e2ce1a4a24 // but note that the cycle replacer, etc. have been removed for conciseness, // since this function can safely make the strict assumption that incoming // data is already guaranteed to be 100% bidirectionally JSON-compatible.) // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // Note that we leave the function below intact so that it can be statically analyzed. // It is not actually used by backend code!! // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /* eslint-disable no-unused-vars */ /** * unescapeHtmlEntitiesDeepLite() * * Recursively HTML-unescape all strings in the provided data (destructive). * * > • If the provided data contains any dictionaries or arrays, this traverses them recursively * > and unescapes any deeply-nested strings. Remember: any dictionaries/arrays in the original * > value **will be mutated in-place**! * > * > • Also note that the provided value is assumed to already be 100% bidirectionally JSON-compatible. * > That means no undefined values in arrays, or `Infinity`, etc!! In other words, you should be * > able to do JSON.stringify() on this data, then JSON.parse() the resulting string, and get _EXACTLY_ * > what you started with! * * * @param {JSONCompatible} data * The data to escape. Must be 100% bidirectionally JSON-compatible-- which is stricter than normal! (see above) * * @returns {JSONCompatible} * The provided data, which has now been destructively HTML-unescaped. */ function unescapeHtmlEntitiesDeepLite(data){ // Check availability of features. // (Default unescaping supports IE 9 and up. // If you need <=IE 8, etc check out http://kangax.github.io/compat-table/es5/#es5shim. // For complete flexibility, you can also implement your own unescaping code. // To do that, pass in `dontUnescapeOnClient: false` when calling the // `exposeLocalsToBrowser()` view partial.) if ( typeof Array.isArray !== 'function' || typeof Array.prototype.forEach !== 'function' || typeof Array.prototype.map !== 'function' || typeof Object.keys !== 'function' ) { throw new Error('Unsupported browser: Missing support for `Array.isArray`, `Array.prototype.forEach`, `Array.prototype.map`, or `Object.keys`! (Sails\' built-in HTML-unescaping for exposed locals supports IE9 and up.)'); } // Now rebuild the data recursively. // // > This is a self-invoking recursive function. // > The initial call (made below) sets `thisVal` to `data`. return (function _unescapeRecursive (thisVal) { // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // No need to worry about errors, regexps, dates, functions, // readable streams, buffers, constructors, Infinity, -Infinity, // or `-0` (negative zero). // ***(see note above about bidirectional-JSON-compatible-ness)*** // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // If this is `null`, then leave it as-is. if (thisVal === null) { return thisVal; } // ‡ // If this is a boolean, then leave it as-is. else if (thisVal === true || thisVal === false) { return thisVal; } // ‡ // If this is a number, then leave it as-is. else if (typeof thisVal === 'number') { return thisVal; } // ‡ // If this is a string, then convert any unsafe HTML entities it contains // into their decoded, real-world, life-on-the-streets character equivalents. else if (typeof thisVal === 'string') { // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // The code below is a port of the unescaping implementation from // within Lodash since v3.x, thru v4.x, and undoubtedly beyond. // It has been modified only to match the coding conventions and // context of Sails. // // For reference, see: // • Entry point - https://github.com/lodash/lodash/blob/3.10.1/lodash.js#L11008-L11031 // (for future reference, see also https://github.com/lodash/lodash/blob/4.16.1/lodash.js#L14932) // // • RegExps - https://github.com/lodash/lodash/blob/3.10.1/lodash.js#L81 // (for future reference, see also https://github.com/lodash/lodash/blob/4.16.1/lodash.js#L121) // // • `unescapeHtmlChar()` - https://github.com/lodash/lodash/blob/3.10.1/lodash.js#L650-L659 // (for future reference, see also https://github.com/lodash/lodash/blob/4.16.1/lodash.js#L1333) // // • `htmlUnescapes` - https://github.com/lodash/lodash/blob/3.10.1/lodash.js#L215-L223 // (for future reference, see also https://github.com/lodash/lodash/blob/4.16.1/lodash.js#L376) // // > For future reference, see also `basePropertyOf`: // > https://github.com/lodash/lodash/blob/4.16.1/lodash.js#L888 // > (in Lodash 4.x, it's a waypoint between `unescapeHtmlChar()` and the `htmlUnescapes` constant) // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // Define RegExps for matching HTML entities. var X_ESCAPED_HTML = /&(?:amp|lt|gt|quot|#39|#96);/g; var X_HAS_ESCAPED_HTML = RegExp(X_ESCAPED_HTML.source); // If this is empty string, we can go ahead and bail (as an optimization.) // Also, another optimization: if it does not contain any HTML entities representing // unsafe characters, then we bail rather than wasting cycles on a `.replace()`. // Otherwise, that means we have to do a bit more work. In this case, we'll replace // each one of those HTML entities with the corresponding unsafe character. if (thisVal === '') { return thisVal; } else if (!X_HAS_ESCAPED_HTML.test(thisVal)) { return thisVal; } else { var ENTITY_TO_CHAR_MAPPING = { '&': '&', '<': '<', '>': '>', '"': '"', ''': '\'', '`': '`' // << note that this is only necessary because we're using the `_.escape()` from Lodash 3.10.1. }; thisVal = thisVal.replace(X_ESCAPED_HTML, function (htmlEntityStr) { return ENTITY_TO_CHAR_MAPPING[htmlEntityStr]; }); return thisVal; } }//</if this is a string> // ‡ // If this is an array, recursively unescape it. else if (Array.isArray(thisVal)) { thisVal = thisVal.map(function (item) { // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // No need to worry about stripping undefined items. // ***(see note above about bidirectional-JSON-compatible-ness)*** // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - return _unescapeRecursive(item); }); return thisVal; } // ‡ // Otherwise, this must be an "object". And since we know it's // neither `null` nor an array, we can safely assume it is a dictionary. // ***(see note above about bidirectional-JSON-compatible-ness)*** // // So we'll recursively unescape it. else { Object.keys(thisVal).forEach(function (key) { // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // No need to worry about stripping keys with `undefined` values. // ***(see note above about bidirectional-JSON-compatible-ness)*** // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - thisVal[key] = _unescapeRecursive(thisVal[key], key); }); return thisVal; } })(data);//</invoked self-calling function :: _unescapeRecursive> } ================================================ FILE: lib/index.js ================================================ /** * Module dependencies */ var Sails = require('./app'); // Instantiate and expose a Sails singleton // (maintains legacy support) module.exports = new Sails(); // Expose constructor as `.Sails` for convenience/tests: // ========================================================= // To access the Sails app constructor, do: // var Sails = require('sails').constructor; // or: // var Sails = require('sails').Sails; // // Then: // var newApp = new Sails(); // ========================================================= module.exports.Sails = Sails; ================================================ FILE: lib/router/README.md ================================================ # Router ## What does it do? The core Router in Sails is the main (_but not ONLY_) player responsible for routing requests. It is not involved with HTTP, WebSockets, or other internet protocols directly-- instead, it emits events on the `sails` object (a Node EventEmitter) when a route should be bound, allowing flexibility in hooks' implementations. The core Router includes a latent Express instance which is used only for internal routing of requests, and is not actually used by any application code in userland-- that's the job of hooks. It _may_, however, be used by app-level unit tests, in order to run test suites without having to lift a server and occupy a network port. ## Which hooks attach servers / use the Router? At the time of this writing, the `http` hook listens for `bind` events emitted from the core Router and binds them directly to an external instance of Express. On the other hand, the `sockets` hook defers to the core router, emitting a `request` event whenever it receives and interprets a new, appropriately-formatted, socket message. The core Router intercepts this and routes the request using its known middleware bindings. (core middleware, blueprint aka "shadow" routes, and statically configured routes from the `routes.js` config file in userland) ## FAQ + When an HTTP request hits the server, does it hit the Sails router before it hits the Express router? + No- it only hits the Express router. + OK.. what requests DO hit the Sails router? + Requests to other attached servers that don't have their own routers, e.g. the Socket.io interpreter, will hit the Sails router's wildcard handler, which will then talk to the attached server and simulate the appropriate route. + What happens after an HTTP request hits the Express router? + Sails does not touch the Express router once it's been set up. + When and *how* are the routes in your `routes.js` file processed? + `routes.js` is read by the `userconfig` hook, which loads it into `sails.config.routes`. + `sails.config.routes` is used by the Sails router at lifttime (to bind routes to the external Express router) AND at runtime (to detect matches in wildcard routes coming from other attached servers like the Socket.io interpreter) ================================================ FILE: lib/router/bind.js ================================================ /** * Module dependencies. */ var util = require('util'); var _ = require('@sailshq/lodash'); var flaverr = require('flaverr'); var detectVerb = require('../util/detect-verb'); /** * Expose `bind` method. */ module.exports = bind; /** * Bind new route(s) * * @param {String|RegExp} path * @param {String|Object|Array|Function} target * @param {String} verb (optional) * @param {Object} options (optional) * * @this {SJSRouter} * @return {SJSApp} * * @api private */ function bind( /* path, target, verb, options */ ) { var sails = this.sails; var args = sanitize.apply(this, Array.prototype.slice.call(arguments)); var path = args.path; var target = args.target; var verb = args.verb; var options = args.options; // Don't allow paths with "length" as a route param, because Express chokes on it if (path.match(/\/:length($|\/)/)) { throw flaverr({name: 'userError', code: 'E_ROUTE_WITH_LENGTH'}, new Error('Failed to bind route: `'+ path +'`\n'+ 'Routes which contain `/:length` in their address URL are not supported by Sails/Express (consider using `/:len`)')); } // Bind a list of multiple functions in order if (_.isArray(target)) { bindArray.apply(this, [path, target, verb, options]); } // Handle string redirects // (to either public-facing URLs or internal routes) else if (_.isString(target) && target.match(/^(https?:|\/)/)) { bindRedirect.apply(this, [path, target, verb, options]); } // Otherwise if the target is a string, it must be an action. else if (_.isString(target) || (_.isPlainObject(target) && (target.controller || target.action))) { bindAction.apply(this, [path, target, verb, options]); } // Bind a middleware function directly else if (_.isFunction(target)) { bindFunction.apply(this, [path, target, verb, options]); } // If target is an object with a `target`, pull out the rest // of the keys as route options and then bind the target. else if (_.isPlainObject(target) && (target.target || target.fn)) { var _target = target.target || target.fn; // TODO -- replace _.merge() with a call to merge-dictionaries module? options = _.merge(options, _.omit(target, 'target')); bind.apply(this, [path, _target, verb, options]); } else { // If we make it here, the router doesn't know how to parse the target. // // This doesn't mean that it's necessarily invalid though-- // so we'll emit an event informing any listeners that an unrecognized route // target was encountered. Then hooks can listen to this event and act // accordingly. This makes it easier to add functionality to Sails. sails.emit('route:typeUnknown', { path: path, target: target, verb: verb, options: options }); // Note that, in the future, it would be good to track emissions of "typeUnknown" to // avoid logic errors that result in circular routes. // (part of the effort to make a more friendly environment for custom hook developers) } // Makes `.bind()` chainable (sort of) return sails; } /** * Requests will be redirected to the specified string * (which should be a URL or redirectable path.) * * @api private */ function bindRedirect(path, redirectTo, verb, options) { var sails = this.sails; bind.apply(this,[path, function(req, res) { sails.log.verbose('Redirecting request (`' + path + '`) to `' + redirectTo + '`...'); res.redirect(redirectTo); }, verb, options]); } /** * Bind a previously-loaded action to a URL. * (which should be a URL or redirectable path.) * * @api private */ function bindAction(path, target, verb, options) { var self = this; var sails = this.sails; var actionIdentity; try { actionIdentity = self.getActionIdentityForTarget(target); } catch (e) { throw flaverr({name: e.name || 'sailsError', code: e.code || 'E_UNKNOWN_BIND_ERROR'}, new Error('Error attempting to bind `' + (verb || 'ALL') + ' ' + path + '` to ' + JSON.stringify(target) + ': ' + e.message)); } if (_.isObject(target)) { // Fold any other properties in the target into a shallow clone of the "options" dictionary options = _.extend({}, options, _.omit(target, 'action')); } // If there's no loaded action with that identity, log a warning and continue. if (!sails._actions[actionIdentity]) { sails.log.warn('Ignored attempt to bind route (' + path + ') to unknown action ::', target); return; } // Add "action" property to the route options, and set the _middlewareType property if the function doesn't already have one. _.extend(options || {}, {action: actionIdentity, _middlewareType: (sails._actions[actionIdentity] && sails._actions[actionIdentity]._middlewareType || 'ACTION: ' + actionIdentity)}); // Loop through all of the registered action middleware, and find // any that should apply to the action with the given identity. var actionMiddlewareToRun = _.reduce(sails._actionMiddleware, function(memo, middlewareList, key) { // Split the key into an array and sort it so that strings starting with '!' come first. var targets = key.split(',').sort(); _.any(targets, function(target) { // Remove any whitespace surrounding the target. target = target.trim(); // If the target starts with a '!' (meaning that any actions matching it should _not_ // run the middleware), and the target matches, bust out of this loop early. if (target[0] === '!') { target = target.substr(1); if ( // Does the target end in a `/*`, and the action identity matches the wildcard? (target.slice(-2) === '/*' && ((actionIdentity.indexOf(target.slice(0,-1)) === 0) || actionIdentity === target.slice(0, -2)) ) || // Does the target match the action identity exactly? (actionIdentity === target) ) { // We found a matching target, so we can exit this loop. return true; } } // If the target doesn't start with a '!', it means we already got past all of the // negative targets (since the targets are sorted alphabetically), so we can safely // add any middleware that this action matches. else { if ( // If the registered action middleware key is '*'... target === '*' || // Or ends in '/*' so that the current action identity matches the wildcard... (target.slice(-2) === '/*' && ((actionIdentity.indexOf(target.slice(0,-1)) === 0) || actionIdentity === target.slice(0, -2)) ) || // Or matches the current action identity exactly... (actionIdentity === target) ) { // Then add the action middleware from this key to the list of middleware // to run before the action. memo = memo.concat(middlewareList); // We found a matching target, so we can exit this loop. return true; } } // Check the next target. return false; }); // Keep on reducin'. return memo; }, []); // Get a unique list of middleware, in case any were added more than once. actionMiddlewareToRun = _.uniq(actionMiddlewareToRun); // Bind each middleware to the identity. _.each(actionMiddlewareToRun, function(middleware) { // console.log('binding middleware', middleware.toString(), 'to path', verb + ' ' + path); bind.apply(self, [path, middleware, verb, options]); }); // Now, bind a function to the route which calls the specified action. bind.apply(this,[path, function(req, res, next) { // If the specified action doesn't exist in the internal actions dictionary. // bail out early. if (!_.isFunction(sails._actions[actionIdentity])) { return next(new Error('Consistency violation: Request (' + req.method + ' ' + req.path + ') matched a route that is bound to action `' + actionIdentity + '`, but no such action has been registered. This never should have happened, because the route never should have been bound in the first place.')); } // Create a mock "next" function to catch unauthorized use of the third argument to route handlers. var mockNext = function(err) { if (err) { return next(new Error('`next` (as in req,res,next) should never be called in an action function (but in action `' + actionIdentity + '`, it was!) It was called with an error: ' + (_.isError(err) ? err.stack : util.inspect(err, { depth: null }) + '') + ' Please use a method like `res.serverError()` or `res.badRequest()` instead.')); } return next(new Error('`next` (as in req,res,next) should never be called in an action function (but in action `' + actionIdentity + '`, it was!) It was called with no arguments. Please use a method like `res.ok()` or `res.json()` instead.')); };//ƒ try { // Catch errors in async actions. See more notes about async route handlers in the `bindFunction` code below. // > FUTURE: optimize by precomputing this constructor.name check if (sails._actions[actionIdentity].constructor.name === 'AsyncFunction') { // Call the action with the specified identity, passing in req and res, as well as `mockRes` to catch unauthorized // use of `next` inside of end-user action code. var promise = sails._actions[actionIdentity](req, res, mockNext); promise.catch(function(e) { // If we do catch an error, use `next` to let Express handle it correctly. next(e); }); } // For synchronous actions, just call the function. else { // Call the action with the specified identity, passing in req and res, as well as `mockRes` to catch unauthorized // use of `next` inside of end-user action code. return sails._actions[actionIdentity](req, res, mockNext); } } catch(e) { // If we do catch an error, use `next` to let Express handle it correctly. return next(e); } }, verb, options]); } /** * Recursively bind an array of targets in order * * TODO: Use a counter to prevent indefinite loops-- * only possible if a bad route is bound, * but would still potentially be helpful. * * @api private */ function bindArray(path, target, verb, options) { var self = this; var sails = this.sails; if (target.length === 0) { sails.log.verbose('Ignoring empty array in `router.bind(' + path + ')`...'); } else { // Bind each middleware fn _.each(target, function(fn) { bind.apply(self,[path, fn, verb, options]); }); } } /** * Attach middleware function to route. * * @api private */ function bindFunction(path, fn, verb, options) { var sails = this.sails; // Make sure (optional) options is a valid plain object ({}) // TODO -- replace _.isPlainObject with _.isObject && !_.isArray && !_.isFunction ? // TODO -- if we're doing _.cloneDeep here, do we need it in all the places we do it in blueprints? options = _.isPlainObject(options) ? _.cloneDeep(options) : {}; // Warn about no-longer-used blueprint request options. if (_.intersection(_.keys(options), ['populate', 'skip', 'limit', 'sort', 'where']).length > 0) { sails.log.debug('In route `' + verb + ' ' + path + ':'); sails.log.debug('The `populate`, `skip`, `limit`, `sort` and `where` route options are no longer supported in Sails 1.0.'); sails.log.debug('Instead, you can use a `parseBlueprintOptions` function to fully customize blueprint behavior for a route.'); sails.log.debug('See http://sailsjs.com/docs/reference/configuration/sails-config-blueprints#?using-parseblueprintoptions.'); sails.log.debug(); } // Get the _middlewareType of the function. var _middlewareType = // If it was set on the function itself, use that. fn._middlewareType || // Otherwise if options._middlewareType is set (probably because the function was defined inline // in a call to `.bind()`), use that. options._middlewareType || // Otherwise if the function has a name, use that. ('FUNCTION: ' + (fn.name || '<anonymous>')); // Set the middleware type on the function. This can be useful for debugging if the same function // was bound in different contexts (like different actions). fn._middlewareType = _middlewareType; // Remove any _middlewareType property from the options. It's done its job, and we don't need // it to get merged into req.options. // delete options._middlewareType; // Log info about the bound route in SILLY mode. sails.log.silly('Binding route :: ', verb || '', path, _middlewareType); // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // FUTURE: simplify away the unnecessary function declarations below and inline the logic instead. // (will make it much clearer what's going on; and any minimal performance impact will be in the form of gains) // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /** * `router:route` * * Create a closure that emits the `router:route` event each time the route is hit * before actually triggering the target function. * * NOTE: Modifications to route path parameters (i.e. `req.params`) or to `req.options` * must be made here, since their values can change not only on a per-request, but * also a per-route basis. */ var enhancedFn = function routeTargetFnWrapper(req, res, next) { // Set req.options, using `options` to supply default values. req.options = _.merge({}, options || {}, req.options || {}); // This event can be tapped into to take control of // (synchronous) logic that should be run before each bound // route handler function runs. sails.emit('router:route', { req: req, res: res, next: next, options: options, fn: fn }); // Trigger original route handler function. // // > Note that, if it is an async function, then we also attach a handler to `.catch()` // > its return value (which will be a promise) in order to handle rejections in the same // > way we handle exceptions that are thrown synchronously (mainly for the purpose of being able to use async/await) // > (https://trello.com/c/UdK9ooJ3/108-es7-async-await-in-core-sniff-request-handler-function-to-see-if-it-s-an-async-function-if-so-then-grab-the-return-value-from-th) try { if (fn.constructor.name === 'AsyncFunction') { // FUTURE: benchmark this and, if tangible enough, allow configuration to be used to hard-code functions // one way or the other. (Frankly, seems like we could just forcefully swap all request handling functions // over to be async functions -- but that'd be kind of a big change and I'd rather wait for a later release // unless we can prove that that'd definitely be a 100% backwards compatible change) // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - var promise = fn(req, res, next); promise.catch(function(e) { // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // Note that async+await+bluebird+Node 8 errors are not necessarily "true" Error instances, // as per _.isError() anyway (see https://github.com/node-machine/machine/commits/6b9d9590794e33307df1f7ba91e328dd236446a9). // So if we want improve the stack trace here, we'd have to be a bit more relaxed and tolerate // these sorts of "errors" directly as well (by tweezing out the `cause`, which is where the // original Error lives.) // // Note: This is now taken care of automatically by flaverr.parseError() // (The implementation of this "tweezing" is in the default serverError // response handler though.) // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - next(e); // (Note that we don't do `return next(e)` here. That's on purpose-- // to avoid sending the wrong idea to you, dear reader) }); } else { fn(req, res, next); } } catch (e) { return next(e); } }; /** * Wrap a regex route in a helper function that pulls out regex params * * Example: for route: 'r|/\\d+/(.*)/(.*)$|foo,bar', the two parenthesized * groups would be pulled out as req.params[0] and req.params[1] by Express; * the regexRouteWrapper would then map them to req.params['foo'] and req.params['bar'] * * @param {array} params List of params to apply to the req.params object * @return {Function} A middleware function */ var regexRouteWrapper = function(params) { return function(req, res, next) { // Apply the regex route params params.forEach(function(param, index) { req.params[param] = req.params[index]; }); // Call enhancedFn (which is just defined above) enhancedFn(req, res, next); }; }; /** * Wrap a route in a helper function that first checks whether the URL matches * any of a set of regexes, and if so, skips the defined handler. * * @param {array} regexes Array of regexes to match the URL against * @param {Function} fn Middleware function to run if URL does NOT match regexes * @return {Function} A middleware function */ var skipRegexesWrapper = function(regexes, fn) { // Remove anything that's not a regex regexes = _.compact(regexes.map(function(regex) { if (regex instanceof RegExp) { return regex; } sails.log.warn('Invalid regex "' + regex + '" supplied to skipRegexesWrapper; ignoring.'); return undefined; })); return function(req, res, next) { // Check for matches for (var i = 0; i < regexes.length; i++) { if (req.url.match(regexes[i])) { // If we find one, bail out return next(); } } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // TODO: Need to double-check on this, but shouldn't this call `enhancedFn`, instead of just `fn`? // If so, then we can just make that change. Otherwise, we need to do more here. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // Otherwise continue with the handler return fn(req, res, next); }; }; // If verb is not specified, default to CRUD methods. // You can still explicitly route to "all /path" if you want ALLLLlllll the things. var targetVerb = verb || ['get', 'put', 'post', 'delete', 'patch']; // Ensure targetVerb is an array of lowercased verbs. if (!Array.isArray(targetVerb)) {targetVerb = [targetVerb.toLowerCase()];} else { targetVerb = _.map(targetVerb, function(verb) { return verb.toLowerCase(); }); } // Function to actually bind var targetFn; // Regex to check if the route is...a regex. var regExRoute = /^r\|(.*)\|(.*)$/; // Perform the check var matches = path.match(regExRoute); // If it *is* a regex, create a RegExp object that Express can bind, // pull out the params, and wrap the handler in regexRouteWrapper if (matches) { path = new RegExp(matches[1]); var params = matches[2].split(','); targetFn = regexRouteWrapper(params); } // Otherwise just bind enhancedFn else { targetFn = enhancedFn; } // If options.skipRegex is specified, make sure it's an array if (options.skipRegex) { if (!Array.isArray(options.skipRegex)) { options.skipRegex = [options.skipRegex]; } } // Otherwise just make it an empty array else { options.skipRegex = []; } // For GET routes ending in pattern vars, default `skipAssets` to true. if (_.isString(path) && path.match(/\:[^\/]+\/?$/) && _.isUndefined(options.skipAssets) && _.contains(targetVerb, 'get')) { options.skipAssets = true; } // If "skipAssets" option is true, add the skipAssets regex // to the options.skipRegex array if (options.skipAssets) { options.skipRegex.push(sails.LOOKS_LIKE_ASSET_RX); } // If we have anything in the options.skipRegex array, wrap // the target function again. if (options.skipRegex.length) { targetFn = skipRegexesWrapper(options.skipRegex, targetFn); } // Loop through the verbs we want to bind targetVerb.forEach(function(verb) { // Bind the function to the private router sails.router._privateRouter[verb](path, targetFn); // Emit an event to make hooks aware that a route was bound // This allows hooks to handle routes directly if they want to- // e.g. with Express, the handler for this event looks like: // sails.hooks.http.app[verb || 'all'](path, target); sails.emit('router:bind', { path: path, target: targetFn, verb: verb, options: options, originalFn: fn }); }); } /** * Sanitize the arguments to `sails.router.bind()` * * @returns {Object} sanitized arguments * @api private */ function sanitize(path, target, verb, options) { options = options || {}; // If trying to bind '*', that's probably not what was intended, so fix it up path = path === '*' ? '/*' : path; // If route has an HTTP verb (e.g. `get /foo/bar`, `put /bar/foo`, etc.) parse it out, var detectedVerb = detectVerb(path); // then prune it from the path path = detectedVerb.original; // Keep track of parsed verb so we know if it was specified later options.detectedVerb = detectedVerb; // If a verb override was not specified, // use the detected verb from the string route if (!verb) { verb = detectedVerb.verb; } return { path: path, target: target, verb: verb, options: options }; } ================================================ FILE: lib/router/bindDefaultHandlers.js ================================================ /** * Module dependencies */ var _ = require('@sailshq/lodash'); /** * Default 500 and 404 handler. * (defers to res.serverError() and res.notFound() whenever possible) * * With default hook configuration, these handlers apply to both HTTP * and virtual requests */ module.exports = function(sails) { return { /** * Default 500 handler. * (for errors implicitly thrown in middleware/routes) * * @param {*} err * @param {Request} req * @param {Response} res */ 500: function(err, req, res) { // console.log('* * * FIRED DEFAULT HANDLER (500) * * *'); // console.log('args:',arguments); // console.log('* * * </FIRED_DEFAULT_HANDLER_500> * * *'); // console.log(); // First, check for special built-in errors from Express. // We don't necessarily want to treat any error that is thrown with // a `status` property of 400 as if it were intentional. So we also check // the error message. In Express 5, hopefully this can be improved a bit // further. if (_.isError(err)) { var msgMatches = err.message.match(/^Failed to decode param \'([^']+)\'/); if (err.status === 400 && msgMatches) { sails.log.verbose('Bad request: Could not decode the requested URL ('+req.path+')'); // Note for future: The problematic URL section is: `msgMatches[1]` return res.status(400).send('Bad request: Could not decode requested URL.'); } } // Next, try to use `res.serverError()`, if it exists and is valid. try { if (typeof res.serverError === 'function') { return res.serverError(err); }//>- } catch (unusedErr) { /* ignore any unexpected error encountered when attempting to respond w/ res.serverError(). */ } // Catch-all: // Log a message and try to use `res.send` to respond. try { sails.log.error('Server Error:'); sails.log.error(err); if (process.env.NODE_ENV === 'production') { return res.sendStatus(500); } else { return res.status(500).send(err); } } catch (errorSendingResponse) { // Serious error occurred-- unable to send response. // // Note that in the future, we could also emit an `abort` message on the request object // in this case-- then if an attached server is managing this request, it could monitor // for `abort` events and manage its private resources (e.g. TCP sockets) accordingly. // However, such contingencies should really handled by the underlying HTTP hook, so // this might not actually make sense. sails.log.error('But no response could be sent because another error occurred:'); sails.log.error(errorSendingResponse); }//</catch> }, /** * Default 404 handler. * (for unmatched routes) * * @param {Request} req * @param {Response} res */ 404: function(req, res) { // Use `notFound` handler if it exists try { if (typeof res.notFound === 'function') { return res.notFound(); } } catch (unusedErr) { /* If res.notFound() doesn't exists, or fails w/ an error, then silently ignore that and try other ways to send a 404. */ } // Catch-all: // Log a message and try to use `res.send` to respond. try { sails.log.verbose('A request (%s) did not match any routes, and no `res.notFound` handler is configured.', req.url); res.sendStatus(404); return; } catch (err) { // Serious error occurred-- unable to send response. // // Note that in the future, we could also emit an `abort` message on the request object // in this case-- then if an attached server is managing this request, it could monitor // for `abort` events and manage its private resources (e.g. TCP sockets) accordingly. // However, such contingencies should really handled by the underlying HTTP hook, so // this might not actually make sense. sails.log.error('An unmatched route was encountered in a request...'); sails.log.error('But no response could be sent because an error occurred:'); sails.log.error(err); return; } }//ƒ }; }; ================================================ FILE: lib/router/index.js ================================================ /** * Module dependencies. */ var Readable = require('stream').Readable; var QS = require('querystring'); var _ = require('@sailshq/lodash'); var router = require('@sailshq/router'); var flaverr = require('flaverr'); var sortRouteAddresses = require('sort-route-addresses'); var buildReq = require('./req'); var buildRes = require('./res'); var defaultHandlers = require('./bindDefaultHandlers'); var detectVerb = require('../util/detect-verb'); // Private var to hold sorted route addresses var sortedRouteAddresses = []; /** * Expose new instance of `Router` * * @api private */ module.exports = function(sails) { return new Router({sails: sails}); }; /** * Initialize a new `Router` * * @param {Object} options * @api private */ function Router(options) { options = options || {}; this.sails = options.sails; this.defaultHandlers = defaultHandlers(this.sails); // Expose router on `sails` object this.sails.router = this; // Instantiate the private router as an instance of `router. this._privateRouter = router(); // Return the array of sorted route addresses, cloned for our protection. this.getSortedRouteAddresses = function() { return _.clone(sortedRouteAddresses); }; // Bind the context of all instance methods this.load = _.bind(this.load, this); this.bind = _.bind(this.bind, this); this.unbind = _.bind(this.unbind, this); this.reset = _.bind(this.reset, this); this.flush = _.bind(this.flush, this); this.route = _.bind(this.route, this); this.getActionIdentityForTarget = _.bind(this.getActionIdentityForTarget, this); } /** * _privateRouter * * This internal "private" instance of an Express app object * is used only for routing. (i.e. it will not be used for * listening to actual HTTP requests; instead, one or more * delegate servers can be attached- see the `http` or * `sockets` hooks for examples of attaching a server to * Sails) * * NOTE: Requires calling `load()` before use in order to * provide access to the proper NODE_ENV, since Express * uses that to determine its environment (development vs. * production.) */ // Router.prototype._privateRouter; /** * `sails.router.load()` * * Expose the router, create the Express private router, * then call flush(), which will bind configured routes * and emit the appropriate events. * * Note the `results, cb` signature, which is necessary * because this function is called from an async.auto() * where it has dependencies. * * @api public */ Router.prototype.load = function(results, cb) { var sails = this.sails; sails.log.silly('Loading router...'); // Maintain a reference to the static route config this.explicitRoutes = sails.config.routes; // Save reference to sails logger this.log = sails.log; var sessionSecret = sails.config.session && sails.config.session.secret; // If a session store is configured, hook it up as `req.session` by passing // it down to the session middleware if (!sails.hooks.session) { // If available, Sails uses the configured session secret for signing cookies. if (sessionSecret) { // Ensure secret is a string. This check happens in the session hook as well, // but sails.config.session.secret may still be provided even if the session hook // is turned off, so to be extra anal we'll check here as well. if (!_.isString(sessionSecret)) { return cb(new Error('If provided, sails.config.session.secret should be a string.')); } } } if (sessionSecret) { sails._privateCpMware = require('cookie-parser')(sessionSecret); } else { sails._privateCpMware = require('cookie-parser')(); } // Wipe any existing routes and bind them anew try { this.flush(); } // Catch any errors thrown by code handling the router:before and router:after events. catch(e) { return cb(e); } // Listen for requests sails.on('router:request', this.route); // Listen for unhandled errors and unmatched routes sails.on('router:request:500', this.defaultHandlers[500]); sails.on('router:request:404', this.defaultHandlers[404]); cb(); }; /** * `sails.router.route(partialReq, partialRes)` * * Interpret the specified (usually partial) request and response objects into * streams with all of the expected methods, then routes the fully-formed request * using the built-in private router. Useful for creating virtual request/response * streams from non-HTTP sources, like Socket.io or unit tests. * * This method is not always helpful-- it is not called for HTTP requests, for instance, * since the true HTTP req/res streams already exist. In that case, at lift-time, Sails * calls `router:bind`, which loads Sails' routes as normal middleware/routes in the http hook. * stack will run as usual. * * On the other hand, Socket.io needs to use this method (i.e. the `router:request` event) * to simulate a connect-style router since it can't bind dynamic routes ahead of time. * * Keep in mind that, if `route` is not used, the implementing server is responsible * for routing to Sails' default `next(foo)` handler. * * @param {Request} req * @param {Response} res * @api private */ Router.prototype.route = function(req, res) { var sails = this.sails; var _privateRouter = this._privateRouter; // If sails is `_exiting`, ignore the request. if (sails._exiting) { return; } // Provide access to SailsApp instance as `req._sails`. req._sails = req._sails || sails; // Note that, at this point, `req` and `res` are just dictionaries containing // the properties of each object that have been built up _so far_. // // Use base req and res definitions to ensure the specified // objects are at least ducktype-able as standard node HTTP // req and req streams. // // Make sure request and response objects have reasonable defaults // (will use the supplied definitions if possible) req = buildReq(req); res = buildRes(req, res); // Default to 200 status code for OPTIONS requests. // The built-in Express OPTIONS handler just calls `res.end()` (rather // than `res.send()`), so no status code gets set and our mock res.writeHead // method complains. if (req.method === 'OPTIONS' && !res.statusCode) { res.status(200); } // console.log('\n\n\n\n=======================\nReceived request to %s %s\nwith req.body:\n',req.method,req.url, req.body); // Run some basic middleware sails.log.silly('Handling virtual request :: Running virtual querystring parser...'); qsParser(req,res, function (err) { if (err) { return res.status(400).send(err && err.stack); } // Parse cookies parseCookies(req, res, function(err){ if (err) { return res.status(400).send(err && err.stack); } // console.log('Ran cookie parser'); // console.log('res.writeHead= ',res.writeHead); // Load session (if relevant) loadSession(req, res, function (err) { if (err) { return res.status(400).send(err && err.stack); } // console.log('res is now:\n',res); // console.log('\n\n'); // console.log('Ran session middleware'); // console.log('req.sessionID= ',req.sessionID); // console.log('The loaded req.session= ',req.session); sails.log.silly('Handling virtual request :: Running virtual body parser...'); bodyParser(req,res, function (err) { if (err) { return res.status(400).send(err && err.stack); } // Use our private router to route the request _privateRouter(req, res, function handleUnmatchedNext(err) { // // In the event of an unmatched `next()`, `next('foo')`, // or `next('foo', errorCode)`... // // Use the default server error handler if (err) { sails.log.silly('Handling virtual request :: Running final "error" handler...'); sails.emit('router:request:500', err, req, res); return; } // Or the default not found handler sails.log.silly('Handling virtual request :: Running final "not found" handler...'); sails.emit('router:request:404', req, res); return; }); }); }); }); }); }; /** * `sails.router.bind()` * * Bind new route(s) * * @param {String|RegExp} path * @param {String|Object|Array|Function} bindTo * @param {String} verb * @api private */ Router.prototype.bind = require('./bind'); /** * `sails.router.unbind()` * * Unbind existing route * * @param {Object} route * @api private */ Router.prototype.unbind = function(routeToRemove) { var sails = this.sails; // Inform attached servers that route should be unbound sails.emit('router:unbind', routeToRemove); // Remove any route which matches the path and verb of the argument _.remove(this._privateRouter.stack, function(layer) { return (layer.route.path === routeToRemove.path && layer.route.methods[routeToRemove.verb] === true); }); }; /** * `sails.router.reset()` * * Unbind all routes currently attached to the router * * @api private */ Router.prototype.reset = function() { var sails = this.sails; // Make sure that all the routes are deleted this._privateRouter.stack = []; // Emit reset event to allow attached servers to // unbind all of their routes as well sails.emit('router:reset'); }; /** * `sails.router.flush()` * * Unbind all current routes, then re-bind everything, re-emitting the routing * lifecycle events (e.g. `router:before` and `router:after`) * * @param {Object} routes - (optional) * If specified, replaces `this.explicitRoutes` before flushing. * * @api private */ Router.prototype.flush = function(routes) { var self = this; var sails = this.sails; // Wipe routes this.reset(); // Fired before static routes are bound sails.emit('router:before'); // If specified, replace `this.explicitRoutes` if (routes) { this.explicitRoutes = routes; } // Updated the sorted route address cache sortedRouteAddresses = sortRouteAddresses(_.keys(this.explicitRoutes)); // Iterate over each address and bind the route that the address is for. _.each(sortedRouteAddresses, function(address) { var target = self.explicitRoutes[address]; var verb = detectVerb(address).verb; // If the route address ends in a pattern var (e.g. /:id) or a wildcard (i.e. /*) // and it declares a method that could be used to request an asset, and the route // doesn't explicitly declare `skipAssets` true or false, then it should! var shouldDeclareSkipAssets = ( _.isUndefined(target.skipAssets) && (address.match(/\/\*\/?$/) || address.match(/^r\|/)) && (!verb || _.contains(['all', 'get', 'head', 'options'], verb)) ); if (shouldDeclareSkipAssets) { sails.log.warn('Warning: route `' + address + '` should explicitly declare `skipAssets: true` or `skipAssets: false` to ensure correct handling of assets!'); sails.log.warn('See http://sailsjs.com/docs/concepts/routes/url-slugs for more info.'); console.log(); } self.bind(address, target); }); // Fired after static routes are bound sails.emit('router:after'); }; /** * Given a route target configuration, return an action identity for that target. * @param {Dictionary|String} target The route target to get an action identity for * @return {String} An action identity like `user/find` */ Router.prototype.getActionIdentityForTarget = function getActionIdentityForTarget(target) { var actionIdentity; // Unwrap { target: '...' } targets. if (target && target.target) { target = target.target; } // Handle dictionary targets: // {controller: 'UserController', action: 'create'} // - or - // {action: 'user.create'} if (_.isObject(target) && !_.isArray(target) && !_.isFunction(target)) { // Attempt to handle `{controller: 'UserController', action: 'create'}` target. if (target.controller) { if (!target.action) { throw flaverr({name: 'userError', code: 'E_NOT_ACTION_TARGET'}, new Error('If `controller` is specified, `action` must be also!')); } actionIdentity = target.controller.replace('Controller', '') + '/' + target.action; } // Attempt to handle `{action: 'user.create'}` target. else if (target.action) { // Get the action identity by lowercasing the value of the `action` property. actionIdentity = target.action; } else { throw flaverr({name: 'userError', code: 'E_NOT_ACTION_TARGET'}, new Error('If target is a dictionary, it must contain an `action` property!')); } // Bail if the action contains characters other than letters, numbers, dashes and forward slashes. if (!actionIdentity.match(/^[a-zA-Z_\$]+[a-zA-Z0-9_\/\-\$]*$/)) { // If the action didn't contain weird characters, make a suggestion by removing "Controller" and // replacing dots with slashes. var didYouMean = ''; var RX_DOESNT_HAVE_ANY_WEIRD_CHARS = /[^a-zA-Z0-9.\/\-\$]/; if (!actionIdentity.match(RX_DOESNT_HAVE_ANY_WEIRD_CHARS)) { didYouMean = ' Did you mean `' + actionIdentity.replace('Controller', '').replace(/\./g,'/') + '`?'; } throw flaverr({name: 'userError', code: 'E_NOT_ACTION_TARGET'}, new Error( '\nCould not parse invalid action `' + actionIdentity + '`.' + didYouMean + '\n\n' + 'See http://sailsjs.com/docs/concepts/routes/custom-routes#?controller-action-target-syntax\n'+ 'for more info on controller/action and standalone action route syntax.\n' )); } } // Handle string targets: // 'UserController.create' // - or - // 'user.create' // - or - // 'user/create' else if (_.isString(target)) { // Normalize the action identity by removing `Controller` and replacing `.` with `/` actionIdentity = target.replace(/Controller/,'').replace(/\./g,'/'); // If the result contains anything other than letters, numbers, dashes, underscores or forward-slashes, bail. if (!actionIdentity.match(/^[a-zA-Z_\$]+[a-zA-Z0-9_\/\-\$]*$/)) { throw flaverr({name: 'userError', code: 'E_NOT_ACTION_TARGET'}, new Error( '\nCould not parse invalid action `' + target + '`.\n'+ 'See http://sailsjs.com/docs/concepts/routes/custom-routes#?controller-action-target-syntax\n'+ 'for more info on controller/action and standalone action route syntax.\n' )); } } else if (_.isArray(target)) { actionIdentity = (function(){ var actionTarget = _.find(target, function(targetComponent) { return targetComponent.action; }); if (actionTarget) { return getActionIdentityForTarget(actionTarget); } throw flaverr({name: 'userError', code: 'E_NOT_ACTION_TARGET'}, new Error('Target was an array without any items containing an action!')); })(); } else { throw flaverr({name: 'userError', code: 'E_NOT_ACTION_TARGET'}, new Error('Target must be a dictionary or string!')); } // Replace all dots with slashes, and ensure lowercase. actionIdentity = actionIdentity.replace(/\./g, '/').toLowerCase(); return actionIdentity; }; //////////////////////////////////////////////////////////////////////////////////////////////////// // // || Private functions // \/ // //////////////////////////////////////////////////////////////////////////////////////////////////// // Extremely simple query string parser (`req.query`) function qsParser(req,res,next) { var queryStringPos = req.url.indexOf('?'); if (queryStringPos !== -1) { req.query = _.merge(req.query, QS.parse(req.url.substr(queryStringPos + 1))); } else { req.query = req.query || {}; } next(); } // Extremely simple body parser (`req.body`) function bodyParser (req, res, next) { // Set up a mock `req.file()` clarifying that req.file() is not available // outside of the context of Skipper (i.e. in this case, most commonly from // socket.io virtual requests). req.file = function fileUploadsNotAvailable(){ return res.status(500).send('Streaming file uploads via `req.file()` are only available over HTTP with Skipper.'); }; var bodyBuffer=''; if (req.method === 'GET' || req.method === 'HEAD' || req.method === 'DELETE'){ req.body = _.extend({}, req.body); return next(); } // Ensure that `req` is a readable stream at this point if ( !(req instanceof Readable) ) { return next(new Error('Sails Internal Error: `req` should be a Readable stream by the time `route()` is called')); } req.on('readable', function() { var chunk; while (null !== (chunk = req.read())) { bodyBuffer += chunk; } }); req.on('end', function() { var parsedBody; try { parsedBody = JSON.parse(bodyBuffer); } catch (unusedErr) {} // TODO -- replace _.merge() with a call to merge-dictionaries module? req.body = _.merge(req.body, parsedBody); next(); }); } /** * [parseCookies description] * @param {[type]} req [description] * @param {[type]} res [description] * @param {Function} next [description] * @return {[type]} [description] */ function parseCookies (req, res, next){ // req._sails.log.verbose('Parsing cookie:',req.headers.cookie); if (req._sails._privateCpMware) { // Run the middleware return req._sails._privateCpMware(req, res, next); } // Otherwise don't even worry about it. return next(); } /** * [loadSession description] * @param {[type]} req [description] * @param {[type]} res [description] * @param {Function} next [description] * @return {[type]} [description] */ function loadSession (req, res, next){ // If a session store is configured, and we haven't deliberately disabled // session support for this request by setting the "nosession" header, // hook up the store up as `req.session` by passing it down to the // session middleware. if (req._sails._privateSessionMiddleware && !req.headers.nosession) { // Access store preconfigured session middleware as a private property on the app instance. return req._sails._privateSessionMiddleware(req, res, next); } // Otherwise don't even worry about it. return next(); } ================================================ FILE: lib/router/mock-req.js ================================================ /** * mock-req * v0.2.0 * * https://www.npmjs.com/package/mock-req * * The MIT License (MIT) * * Copyright (c) 2014 diachedelic * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in * the Software without restriction, including without limitation the rights to * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of * the Software, and to permit persons to whom the Software is furnished to do so, * subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /* eslint-disable */ module.exports = MockIncomingMessage; var Transform = require('stream').Transform, util = require('util'); function MockIncomingMessage(options) { var self = this; options = options || {}; Transform.call(this); this._writableState.objectMode = true; this._readableState.objectMode = false; // Copy unreserved options var reservedOptions = [ 'method', 'url', 'headers', 'rawHeaders' ]; Object.keys(options).forEach(function(key) { if (reservedOptions.indexOf(key) === -1) self[key] = options[key]; }); this.method = options.method || 'GET'; this.url = options.url || ''; // Set header names this.headers = {}; this.rawHeaders = []; if (options.headers) Object.keys(options.headers).forEach(function(key) { var val = options.headers[key]; if(val !== undefined) { if (typeof val !== 'string') { val += ''; } self.headers[key.toLowerCase()] = val; self.rawHeaders.push(key); self.rawHeaders.push(val); } }); // Auto-end when no body if (this.method === 'GET' || this.method === 'HEAD' || this.method === 'DELETE') this.end(); } util.inherits(MockIncomingMessage, Transform); MockIncomingMessage.prototype._transform = function(chunk, encoding, next) { if (this._failError) return this.emit('error', this._failError); if (typeof chunk !== 'string' && !Buffer.isBuffer(chunk)) chunk = JSON.stringify(chunk); this.push(chunk); next(); }; // Causes the request to emit an error when the body is read. MockIncomingMessage.prototype._fail = function(error) { this._failError = error; }; /* eslint-enable */ ================================================ FILE: lib/router/mock-res.js ================================================ /** * mock-res * v0.3.0 * * https://www.npmjs.com/package/mock-res * * The MIT License (MIT) * * Copyright (c) 2014 diachedelic * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in * the Software without restriction, including without limitation the rights to * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of * the Software, and to permit persons to whom the Software is furnished to do so, * subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /* eslint-disable */ module.exports = MockServerResponse; var Transform = require('stream').Transform, util = require('util'), STATUS_CODES = require('http').STATUS_CODES; function MockServerResponse(finish) { Transform.call(this); this.statusCode = 200; this.statusMessage = STATUS_CODES[this.statusCode]; this._header = this._headers = {}; if (typeof finish === 'function') this.on('finish', finish); } util.inherits(MockServerResponse, Transform); MockServerResponse.prototype._transform = function(chunk, encoding, next) { this.push(chunk); next(); }; MockServerResponse.prototype.setHeader = function(name, value) { this._headers[name.toLowerCase()] = value; }; MockServerResponse.prototype.getHeader = function(name) { return this._headers[name.toLowerCase()]; }; MockServerResponse.prototype.removeHeader = function(name) { delete this._headers[name.toLowerCase()]; }; MockServerResponse.prototype.writeHead = function(statusCode, reason, headers) { if (arguments.length == 2 && typeof arguments[1] !== 'string') { headers = reason; reason = undefined; } this.statusCode = statusCode; this.statusMessage = reason || STATUS_CODES[statusCode] || 'unknown'; if (headers) { for (var name in headers) { this.setHeader(name, headers[name]); } } }; MockServerResponse.prototype._getString = function() { return Buffer.concat(this._readableState.buffer).toString(); }; MockServerResponse.prototype._getJSON = function() { return JSON.parse(this._getString()); }; /* Not implemented: MockServerResponse.prototype.writeContinue() MockServerResponse.prototype.setTimeout(msecs, callback) MockServerResponse.prototype.headersSent MockServerResponse.prototype.sendDate MockServerResponse.prototype.addTrailers(headers) */ /* eslint-enable */ ================================================ FILE: lib/router/req.js ================================================ /** * Module dependencies */ var _ = require('@sailshq/lodash'); var defaultsDeep = require('merge-defaults');// « TODO: Get rid of this var MockReq = require('./mock-req');// «FUTURE: consolidate that into this file var parseurl = require('parseurl'); /** * Factory which builds generic Sails request object (i.e. `req`). * * This generic implementation of `req` forms the basis for * Sails' transport-agnostic support of Connect/Express * middleware. Used by hooks (i.e. sockets) but also for * tests-- both at the app-level and in Sails core. * * @param {Dictionary} _req * the properties of this simulated request object that * have been built up _so far_. * * @return {Request} simulated HTTP request object * @idempotent */ module.exports = function buildRequest (_req) { // Make sure _req is not undefined _req = _req||{}; // Start our request object, which will be built by inheriting/transforming // properties of _req and adding some spice of our own var req; // Attempt to parse the URL in _req, so that we can get the querystring // and path. (But if it fails for any reason, ignore the error and fall back // to an empty dictionary.) var parsedUrl; try {parsedUrl = parseurl(_req) || {};} catch (unusedErr) {parsedUrl = {};} // If `_req` appears to be a stream (duck-typing), then don't try // and turn it into a mock stream again. if (typeof _req === 'object' && _req.read) { req = _req; } else { if (_req.headers && typeof _req.headers === 'object') { for (let headerKey of Object.keys(_req.headers)) { // Strip undefined headers if (undefined === _req.headers[headerKey]) { delete _req.headers[headerKey]; continue; }//• // Make sure all remaining headers are strings if (typeof _req.headers[headerKey] !== 'string') { try { _req.headers[headerKey] = ''+_req.headers[headerKey]; // FUTURE: This behavior is likely being relied upon by apps, so we can't just change it. // But in retrospect, it would probably be better to straight-up reject this here if it's not // a string, since HTTP header values are always supposed to be strings; or at least primitives. // So maybe reject non-primitives, reject `null`, and then accept primitives, but be smart about // this, especially in the context of what the client is doing. } catch (unusedErr) { delete _req.headers[headerKey]; } } }//∞ }//fi // Create a mock IncomingMessage stream. req = new MockReq({ method: _req && (_.isString(_req.method) ? _req.method.toUpperCase() : 'GET'), headers: _req && _req.headers || {}, url: _req && _req.url }); // Add .get() and .header() methods to match express 3 req.get = req.header = function (name) { switch (name = name.toLowerCase()) { case 'referer': case 'referrer': return this.headers.referrer || this.headers.referer; default: return this.headers[name]; } }; // Now pump client request body to the mock IncomingMessage stream (req) // Req stream ends automatically if this is a GET or HEAD or DELETE request // (since there is no request body in that case) so no need to do it again. if (req.method !== 'GET' && req.method !== 'HEAD' && req.method !== 'DELETE') { // Only write the body if there IS a body. if (req.body) { req.write(req.body); } req.end(); } } // Track request start time req._startTime = new Date(); //////////////////////////////////////////////////////////////////////////////// // Note that other core methods _could_ be added here for use w/ the virtual // router. But as per convo w/ dougwilson, the same _cannot_ be done for HTTP // requests coming out of Express. They would either have to (a) rely on modifying // the HTTP request (IncomingMessage) prototype, or (B) rely on context (i.e. `this`), // which would require `_.bind()`-ing them to avoid issues when triggered from // userland code. And re: (B) at that point, the performance impact is effectively // the same as if they were attached on the fly on a per-request basis. // // So we only initially attach `req.*` methods & properties here which are _not_ // already built-in to the mock request, and which are _not_ already taken care of // by hooks, AND which don't rely on `res` (because it hasn't been built yet). //////////////////////////////////////////////////////////////////////////////// // Provide defaults for other request state and methods req = defaultsDeep(req, { params: [], query: (_req && _req.query) || require('querystring').parse(parsedUrl.query) || {}, body: (_req && _req.body) || {}, param: function(paramName, defaultValue) { var key; var params = {}; for (key in (req.params || {}) ) { params[key] = params[key] || req.params[key]; } for (key in (req.query || {}) ) { params[key] = params[key] || req.query[key]; } for (key in (req.body || {}) ) { params[key] = params[key] || req.body[key]; } // Grab the value of the parameter from the appropriate place // and return it if (typeof params[paramName] !== 'undefined') { return params[paramName]; } else { return defaultValue; } }, wantsJSON: (_req && _req.wantsJSON === false) ? false : true, method: 'GET', originalUrl: _req.originalUrl || _req.url, path: _req.path || parsedUrl.pathname }, _req||{}); return req; }; ================================================ FILE: lib/router/res.js ================================================ /** * Module dependencies */ var util = require('util'); var http = require('http'); var Transform = require('stream').Transform; var _ = require('@sailshq/lodash'); var flaverr = require('flaverr'); var MockRes = require('./mock-res');// «FUTURE: consolidate that into this file /** * Ensure that response object has a minimum set of reasonable defaults * Used primarily as a test fixture. * * @api private * @idempotent */ module.exports = function _buildResponse (req, _res) { _res = _res||{}; req = req||{}; var res; // If `_res` appears to be a stream (duck-typing), then don't try // and turn it into a mock stream again. if (typeof _res === 'object' && _res.end) { res = _res; } else { res = new MockRes(); delete res.statusCode; } // Ensure res.headers and res.locals exist. res = _.extend(res, {locals: {}, headers: {}, _headers: {}}); res = _.extend(res, _res); // Now that we're sure `res` is a Transform stream, we'll handle the two different // approaches which a user of the virtual request interpreter might have taken: // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * // (1) Providing a callback function (`_clientCallback`) // // If a `_clientCallback` function was provided, also pipe `res` into a // fake clientRes stream where the response `body` will be buffered. if (res._clientCallback) { // If `res._clientRes` WAS NOT provided, then create one if (!res._clientRes) { res._clientRes = new MockClientResponse(); } // Session is saved automatically since the virtual request interpreter is // using `express-session` directly as of https://github.com/balderdashy/sails/commit/58e93f5a5f2e667e3fbeddf5b4b356f813e3555e. // The stream should trigger the callback when it finishes or errors. res._clientRes.on('finish', function() { return res._clientCallback(res._clientRes); }); res._clientRes.on('error', function(err) { err = err || new Error('Error on response stream'); res._clientRes.statusCode = 500; res._clientRes.body = err; return res._clientCallback(res._clientRes); }); } // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * // (2) Providing a Writable stream (`_clientRes`) // // If a `_clientRes` response Transform stream was provided, pipe `res` directly to it. if (res._clientRes) { res.pipe(res._clientRes); } // // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * // Track whether headers have been written // (TODO: pull all this into mock-res via a PR) // res.writeHead() is wrapped in closure by the `on-header` module, // but it still needs the underlying impl res.writeHead = function ( /* statusCode, [reasonPhrase], headers */) { // console.log('\n\n• res.writeHead(%s)', Array.prototype.slice.call(arguments)); var statusCode = +arguments[0]; // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // FUTURE: Actually use the "reasonPhrase", if one was provided. // ``` // var reasonPhrase = (function(){ // if (arguments[2] && _.isString(arguments[1])) { // return arguments[1]; // } // return undefined; // })(); // ``` // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - var newHeaders = (function (){ if (arguments[2] && _.isObject(arguments[2])) { return arguments[2]; } return arguments[1]; })(); if (!statusCode) { throw new Error('`statusCode` must be passed to res.writeHead().'); } // Set status code res.statusCode = statusCode; // Ensure `._headers` have been merged into `.headers` _.extend(res.headers, res._headers); if (newHeaders) { if (!_.isObject(newHeaders)) { throw new Error('`headers` must be passed to res.writeHead() as an object. Got: '+util.inspect(newHeaders, false, null)); } // Set new headers _.extend(res.headers, newHeaders); } // Set status code and headers on the `_clientRes` stream so they are accessible // to the provider of that stream. // (this has to happen in `send()` because the code/headers might have just changed) if (res._clientRes) { // console.log('Setting headers on clientRes- res.headers = ',res.headers); res._clientRes.headers = res.headers; res._clientRes.statusCode = res.statusCode; } }; // Wrap res.write() and res.end() to get them to call writeHead() var prevWrite = res.write; res.write = function (){ res.writeHead(res.statusCode, _.extend(res._headers,res.headers)); // console.log('res.write():: called writeHead with headers=',_.extend(res._headers,res.headers)); prevWrite.apply(res, Array.prototype.slice.call(arguments)); }; var prevEnd = res.end; res.end = function (){ res.writeHead(res.statusCode, _.extend(res._headers,res.headers)); // console.log('our res.end() was triggered'); // console.log('res.end():: called writeHead with headers=',_.extend(res._headers,res.headers)); prevEnd.apply(res, Array.prototype.slice.call(arguments)); }; // we get `setHeader` from mock-res // see http://nodejs.org/api/http.html#http_response_setheader_name_value // // Usage: // response.setHeader("Set-Cookie", ["type=ninja", "language=javascript"]); // If we ever need to wrap it... // // var prevSetHeader = res.setHeader; // res.setHeader = function (){ // prevSetHeader.apply(res, Array.prototype.slice.call(arguments)); // }; // res.status() res.status = res.status || function _statusShim (statusCode) { res.statusCode = statusCode; return res; }; // res.sendStatus() // (send a text representation of a status code) res.sendStatus = res.sendStatus || function _sendStatusShim (statusCode) { // Get the status codes from the HTTP module var statusCodes = http.STATUS_CODES; // If this is a known code, use its name (e.g. "FORBIDDEN" or "OK"). // Otherwise, just turn the number into a string. var body = statusCodes[statusCode] || String(statusCode); // Set the response status code. res.statusCode = statusCode; // Send the response. return res.send(body); }; // res.send() res.send = res.send || function _sendShim (data, noLongerSupported) { if (!_.isUndefined(noLongerSupported)) { throw new Error('The 2-ary usage of `res.send()` is no longer supported in Express 4/Sails v1. Please use `res.status(statusCode).send(body)` instead.'); } // Don't allow users to respond/redirect more than once per request // FUTURE: prbly move this check to our `res.writeHead()` impl try { onlyAllowOneResponse(res); } catch (e) { if (req._sails && req._sails.log && req._sails.log.error) { req._sails.log.error(e); return; } console.error(e); return; } // Ensure charset is set res.charset = res.charset || 'utf-8'; // Ensure headers are set _.extend(res.headers, res._headers); // Ensure statusCode is set res.statusCode = res.statusCode || 200; // if a `_clientCallback` was specified, we'll skip the streaming stuff for res.send(). if (res._clientCallback) { // Hard-code `res.body` rather than writing to the stream. // (but don't include body if it is empty) if (!_.isUndefined(data)) { res.body = data; // Then expose on res._clientRes.body res._clientRes.body = res.body; } // End the `res` stream (which will in turn end the `res._clientRes` stream) res.end(); return; } // // Otherwise, the hook using the interpreter must have provided us with a `res._clientRes` stream, // so we'll need to serialize everything to work w/ that stream. // // console.log('\n---\nwriting to clientRes stream...'); // console.log('res.headers =>',res.headers); // console.log('res._headers =>',res._headers); // Write body to `res` stream if (!_.isUndefined(data)) { try { var toWrite; // If the data is already a string, don't stringify it. // (This allows for sending plain text, XML, etc.) if (_.isString(data)) { toWrite = data; } else { try { toWrite = JSON.stringify(data); if (!res.get('content-type')) { res.set('content-type', 'application/json'); } } catch(e) { throw new Error( 'Failed to stringify specified JSON response body :: ' + util.inspect(data) + '\nError:\n' + util.inspect(e) ); } // if (process.env.NODE_ENV !== 'production') { // toWrite = e.message; // } }//>- res.write(toWrite); } catch (e) { if (req._sails && req._sails.log && req._sails.log.error) { req._sails.log.error(e); } else { console.error(e); } res.statusCode = 500; } }//</if data was defined> // End the `res` stream. res.end(); }; // res.json() res.json = res.json || function _jsonShim (data, noLongerSupported) { if (!_.isUndefined(noLongerSupported)) { throw new Error('The 2-ary usage of `res.json()` is no longer supported in Express 4/Sails v1. Please use `res.status(statusCode).json(body)` instead.'); } // If data is a string, JSON stringify it. // (Otherwise, we can just rely on `send` to do that for us.) if (_.isString(data)) { data = JSON.stringify(data); res.set('content-type', 'application/json'); } return res.status(res.statusCode || 200).send(data); }; // res.render() res.render = res.render || function _renderShim (relativeViewPath, locals, cb) { if (_.isFunction(locals)) { cb = locals; locals = {}; } try { if (!req._sails) { throw new Error('Cannot call res.render() - `req._sails` was not attached'); } if (!req._sails.renderView) { throw new Error('Cannot call res.render() - `req._sails.renderView` was not attached (perhaps `views` hook is not enabled?)'); } res.set('content-type', 'text/html'); // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // TODO: // Instead of this shim, turn `sails.renderView` into something like // `sails.hooks.views.render()`, and then call it. throw flaverr({statusCode: 501}, new Error('Not implemented in core yet')); // // Instead, do something like the following: // ``` // var html; // // ... // if (cb) { // return cb(undefined, html); // } // else { // return res.status(200).send(html); // } // ``` // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - } catch (e) { if (cb) { return cb(e); } // NOTE: We don't try to use res.serverError() here because we might // _already_ be in the midst of a res.serverError() call. if (req._sails && req._sails.log && req._sails.log.error) { req._sails.log.error('res.render() failed: ', e); } else { console.error('res.render() failed: ', e); } if (process.env.NODE_ENV === 'production') { return res.status(e.statusCode||500).send(e.message); } else { return res.status(e.statusCode||500).send(); } } }; // res.redirect() res.redirect = res.redirect || function _redirectShim (address, noLongerSupported) { if (!_.isUndefined(noLongerSupported)) { throw new Error('The 2-ary usage of `res.redirect()` is no longer supported in Express 4/Sails v1. Please use `res.status(statusCode).redirect(address)` instead.'); } // For familiarity, set content-type header: res.set('content-type', 'text/html'); // Set location header res.set('Location', address); return res.status(res.statusCode||302).send('Redirecting to '+encodeURI(address)); }; /** * res.set( headerName, value ) * * @param {[type]} headerName [description] * @param {[type]} value [description] */ res.set = function (headerName, value) { res.headers = res.headers || {}; res.headers[headerName] = value; return this; }; /** * res.get( headerName ) * * @param {[type]} headerName [description] * @return {[type]} [description] */ res.get = function (headerName) { return res.headers && res.headers[headerName]; }; return res; }; /** * NOTE: ALL RESPONSES (INCLUDING REDIRECTS) ARE PREVENTED ONCE THE RESPONSE HAS BEEN SENT!! * Even though this is not strictly required with sockets, since res.redirect() * is an HTTP-oriented method from Express, it's important to maintain consistency. * * @api private */ function onlyAllowOneResponse (res) { if (res._virtualResponseStarted) { throw new Error('Cannot write to response more than once'); } res._virtualResponseStarted = true; } // The constructor for clientRes stream // (just a normal transform stream) function MockClientResponse() { Transform.call(this); } util.inherits(MockClientResponse, Transform); MockClientResponse.prototype._transform = function(chunk, encoding, next) { this.push(chunk); next(); }; ================================================ FILE: lib/util/check-origin-url.js ================================================ /** * Module dependencies */ var url = require('url'); var _ = require('@sailshq/lodash'); var flaverr = require('flaverr'); var util = require('util'); /** * checkOriginUrl() * * @param {String} originUrl * The origin URL to check. * (This is used when parsing the relevant config from within `sails.config.security` * or `sails.config.sockets`.) * * @throws {Error} if not valid * @property {String} code (==='E_INVALID') */ module.exports = function checkOriginUrl(originUrl) { if (!_.isString(originUrl) || originUrl === '') { throw flaverr('E_INVALID', new Error('Must specify a non-empty string, but instead got: '+util.inspect(originUrl, {depth: null}))); } if (!originUrl.match(/^https?:\/\//)) { throw flaverr('E_INVALID', new Error('Must specify a protocol like http:// or https://, but instead got: '+originUrl)); } // Now do a mostly-correct parse of the URL. var parsedOriginUrl = url.parse(originUrl); var isHttps = parsedOriginUrl.protocol === 'https:'; if (isHttps && parsedOriginUrl.port === '443') { throw flaverr('E_INVALID', new Error('Should not explicitly specify port 443 with https:// (it is implied). But instead got: '+originUrl)); } if (!isHttps && parsedOriginUrl.port === '80') { throw flaverr('E_INVALID', new Error('Should not explicitly specify port 80 with https:// (it is implied). But instead got: '+originUrl)); } // Ensure there is no path or query string or fragment or anything like that. if (parsedOriginUrl.pathname !== '/' || parsedOriginUrl.path !== '/') { throw flaverr('E_INVALID', new Error('Should not specify a path, query string, URL fragment, or anything like that (but instead, got `'+originUrl+'`)')); } // Ensure there is no trailing slice var lastCharacter = originUrl.slice(-1); if (lastCharacter === '/') { throw flaverr('E_INVALID', new Error('Should not specify a trailing slash, but instead got: '+originUrl)); } }; ================================================ FILE: lib/util/deep-extend.js ================================================ // NOTE: This inlined version of the deep-extend npm package is only used by rc (which was also inlined) /*! * @description Recursive object extending * @author Viacheslav Lotsmanov <lotsmanov89@gmail.com> * @license MIT * * The MIT License (MIT) * * Copyright (c) 2013-2018 Viacheslav Lotsmanov * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in * the Software without restriction, including without limitation the rights to * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of * the Software, and to permit persons to whom the Software is furnished to do so, * subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ 'use strict'; var deepExtend; /** * Extending object that entered in first argument. * * Returns extended object or false if have no target object or incorrect type. * * If you wish to clone source object (without modify it), just use empty new * object as first argument, like this: * deepExtend({}, yourObj_1, [yourObj_N]); */ deepExtend = module.exports = function (/*obj_1, [obj_2], [obj_N]*/) { function isSpecificValue(val) { return ( val instanceof Buffer || val instanceof Date || val instanceof RegExp ) ? true : false; } function cloneSpecificValue(val) { if (val instanceof Buffer) { var x = Buffer.alloc ? Buffer.alloc(val.length) : new Buffer(val.length); val.copy(x); return x; } else if (val instanceof Date) { return new Date(val.getTime()); } else if (val instanceof RegExp) { return new RegExp(val); } else { throw new Error('Unexpected situation'); } } /** * Recursive cloning array. */ function deepCloneArray(arr) { var clone = []; arr.forEach(function (item, index) { if (typeof item === 'object' && item !== null) { if (Array.isArray(item)) { clone[index] = deepCloneArray(item); } else if (isSpecificValue(item)) { clone[index] = cloneSpecificValue(item); } else { clone[index] = deepExtend({}, item); } } else { clone[index] = item; } }); return clone; } function safeGetProperty(object, property) { return property === '__proto__' ? undefined : object[property]; } if (arguments.length < 1 || typeof arguments[0] !== 'object') { return false; } if (arguments.length < 2) { return arguments[0]; } var target = arguments[0]; // convert arguments to array and cut off target object var args = Array.prototype.slice.call(arguments, 1); var val; var src; var clone;// eslint-disable-line no-unused-vars args.forEach(function (obj) { // skip argument if isn't an object, is null, or is an array if (typeof obj !== 'object' || obj === null || Array.isArray(obj)) { return; } Object.keys(obj).forEach(function (key) { src = safeGetProperty(target, key); // source value val = safeGetProperty(obj, key); // new value // recursion prevention if (val === target) { return; /** * if new value isn't object then just overwrite by new value * instead of extending. */ } else if (typeof val !== 'object' || val === null) { target[key] = val; return; // just clone arrays (and recursive clone objects inside) } else if (Array.isArray(val)) { target[key] = deepCloneArray(val); return; // custom cloning and overwrite for specific objects } else if (isSpecificValue(val)) { target[key] = cloneSpecificValue(val); return; // overwrite by new value if source isn't object or array } else if (typeof src !== 'object' || src === null || Array.isArray(src)) { target[key] = deepExtend({}, val); return; // source value and new value is objects both, extending... } else { target[key] = deepExtend(src, val); return; } }); }); return target; }; ================================================ FILE: lib/util/detect-verb.js ================================================ /** * Module dependencies */ var _ = require('@sailshq/lodash'); /** * Detect HTTP verb in an expression like: * `get baz` or `get /foo/baz` * * @api private */ module.exports = function (haystack) { var verbExpr = /^\s*(all|get|post|put|delete|trace|options|connect|patch|head)\s+/i; var verbSpecified = _.last(haystack.match(verbExpr) || []) || ''; verbSpecified = verbSpecified.toLowerCase(); // If a verb was specified, eliminate the verb from the original string if (verbSpecified) { haystack = haystack.replace(verbExpr,'').trim(); } else { haystack = haystack.trim(); } return { verb: verbSpecified, original: haystack, path: haystack }; }; ================================================ FILE: lib/util/rc.js ================================================ // Note: This is an inlined version of the rc npm package. /*! * * @description Recursive object extending * @author Viacheslav Lotsmanov <lotsmanov89@gmail.com> * @license MIT * * The MIT License (MIT) * * Copyright (c) 2013-2018 Viacheslav Lotsmanov * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in * the Software without restriction, including without limitation the rights to * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of * the Software, and to permit persons to whom the Software is furnished to do so, * subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /** * Module dependencies */ 'use strict'; var fs = require('fs'); var ini = require('ini'); var path = require('path'); var etc = '/etc'; var win = process.platform === 'win32'; var home = win ? process.env.USERPROFILE : process.env.HOME; var deepExtend = require('./deep-extend.js'); module.exports = function rc(name, defaults, argv, parse) { var parseFn = function (content) { //if it ends in .json or starts with { then it must be json. //must be done this way, because ini accepts everything. //can't just try and parse it and let it throw if it's not ini. //everything is ini. even json with a syntax error. if(/^\s*{/.test(content)) { return JSON.parse(content); } return ini.parse(content); }; var fileFn = function () { var args = [].slice.call(arguments).filter(function (arg) { return arg !== null;}); //path.join breaks if it's a not a string, so just skip this. for(var i in args) { if('string' !== typeof args[i]) { return; } } var file = path.join.apply(null, args); try { return fs.readFileSync(file,'utf-8'); } catch (unusedErr) { return; } }; var jsonFn = function () { var content = fileFn.apply(null, arguments); return content ? parseFn(content) : null; }; var envFn = function (prefix, env) { env = env || process.env; var obj = {}; var l = prefix.length; for(var k in env) { if(k.toLowerCase().indexOf(prefix.toLowerCase()) === 0) { var keypath = k.substring(l).split('__'); // Trim empty strings from keypath array var _emptyStringIndex; while ((_emptyStringIndex=keypath.indexOf('')) > -1) { keypath.splice(_emptyStringIndex, 1); } var cursor = obj; keypath.forEach(function _buildSubObj(_subkey,i){ // (check for _subkey first so we ignore empty strings) // (check for cursor to avoid assignment to primitive objects) if (!_subkey || typeof cursor !== 'object') { return; } // If this is the last key, just stuff the value in there // Assigns actual value from env variable to final key // (unless it's just an empty string- in that case use the last valid key) if (i === keypath.length-1) { cursor[_subkey] = env[k]; } // Build sub-object if nothing already exists at the keypath if (cursor[_subkey] === undefined) { cursor[_subkey] = {}; } // Increment cursor used to track the object at the current depth cursor = cursor[_subkey]; }); } } return obj; }; var findFn = function () { var rel = path.join.apply(null, [].slice.call(arguments)); function find(start, rel) { var file = path.join(start, rel); try { fs.statSync(file); return file; } catch (unusedErr) { if(path.dirname(start) !== start) {// root return find(path.dirname(start), rel); } } } return find(process.cwd(), rel); }; if('string' !== typeof name) { throw new Error('rc(name): name *must* be string'); } if(!argv) { argv = require('minimist')(process.argv.slice(2)); } defaults = ( 'string' === typeof defaults ? jsonFn(defaults) : defaults ) || {}; parse = parse || parseFn; var env = envFn(name + '_'); var configs = [defaults]; var configFiles = []; function addConfigFile (file) { if (configFiles.indexOf(file) >= 0) {return;} var fileConfig = fileFn(file); if (fileConfig) { configs.push(parse(fileConfig)); configFiles.push(file); } } // which files do we look at? if (!win) { [path.join(etc, name, 'config'), path.join(etc, name + 'rc')].forEach(addConfigFile); } if (home) { [ path.join(home, '.config', name, 'config'), path.join(home, '.config', name), path.join(home, '.' + name, 'config'), path.join(home, '.' + name + 'rc') ].forEach(addConfigFile); addConfigFile(findFn('.'+name+'rc')); } if (env.config) { addConfigFile(env.config); } if (argv.config) { addConfigFile(argv.config); } return deepExtend.apply(null, configs.concat([ env, argv, configFiles.length ? {configs: configFiles, config: configFiles[configFiles.length - 1]} : undefined, ])); }; ================================================ FILE: package.json ================================================ { "name": "sails", "author": "Mike McNeil <@mikermcneil>", "version": "1.5.17", "description": "API-driven framework for building realtime apps, using MVC conventions (based on Express and Socket.io)", "license": "MIT", "homepage": "https://sailsjs.com", "keywords": [ "mvc", "web-framework", "express", "sailsjs", "sails.js", "REST", "API", "orm", "socket.io" ], "bin": { "sails": "./bin/sails.js" }, "engines": { "node": ">= 0.10.0", "npm": ">= 1.4.0" }, "dependencies": { "@sailshq/lodash": "^3.10.6", "@sailshq/router": "^1.3.9", "async": "2.6.4", "captains-log": "^2.0.5", "chalk": "2.3.0", "commander": "2.11.0", "common-js-file-extensions": "1.0.2", "compression": "1.8.1", "connect": "3.6.5", "cookie": "0.7.2", "cookie-parser": "1.4.7", "cookie-signature": "1.1.0", "@sailshq/csurf": "1.11.1", "ejs": "3.1.10", "express": "4.22.0", "express-session": "1.18.2", "flaverr": "^1.10.0", "glob": "7.1.2", "i18n-2": "0.7.3", "include-all": "^4.0.0", "machine": "^15.2.2", "machine-as-action": "^10.3.1", "machinepack-process": "^4.0.1", "machinepack-redis": "^2.0.2", "merge-defaults": "0.2.2", "merge-dictionaries": "1.0.0", "minimist": "1.2.6", "parley": "^3.3.4", "parseurl": "1.3.2", "path-to-regexp": "1.9.0", "pluralize": "1.2.1", "prompt": "1.2.1", "rttc": "^10.0.0-0", "sails-generate": "^2.0.11", "sails-stringfile": "^0.3.3", "semver": "7.5.2", "serve-favicon": "2.4.5", "serve-static": "1.16.2", "skipper": "^0.9.5", "sort-route-addresses": "^0.0.4", "uid-safe": "2.1.5", "vary": "1.1.2", "whelk": "^6.0.1" }, "devDependencies": { "benchmark": "^2.1.2", "connect-redis": "3.3.2", "eslint": "5.16.0", "expect.js": "0.3.1", "fs-extra": "4.0.2", "machinepack-fs": "^8.0.2", "mocha": "3.0.2", "nunjucks": "3.0.1", "portfinder": "1.0.13", "@sailshq/request": "2.88.3", "root-require": "0.3.1", "sails-hook-orm": "^4.0.2", "sails-hook-sockets": "^3.0.0", "sails.io.js": "^1.0.0", "session-file-store": "1.1.2", "should": "9.0.0", "socket.io-client": "2.0.3", "supertest": "1.1.0", "tmp": "0.0.29" }, "bugs": { "url": "http://sailsjs.com/bugs" }, "scripts": { "test": "nodever=`node -e \"console.log('\\`node -v\\`'[1]);\"` && if [ $nodever != \"0\" ]; then npm run lint; fi && npm run custom-tests", "custom-tests": "node ./node_modules/mocha/bin/mocha -b", "lint": "node ./node_modules/eslint/bin/eslint . --max-warnings=0 --ignore-pattern 'test/' --ignore-pattern 'testApp/'" }, "main": "./lib/index.js", "repository": { "type": "git", "url": "git://github.com/balderdashy/sails.git" } } ================================================ FILE: test/.eslintrc ================================================ { // ╔═╗╔═╗╦ ╦╔╗╔╔╦╗┬─┐┌─┐ ┌─┐┬ ┬┌─┐┬─┐┬─┐┬┌┬┐┌─┐ // ║╣ ╚═╗║ ║║║║ ║ ├┬┘│ │ │└┐┌┘├┤ ├┬┘├┬┘│ ││├┤ // o╚═╝╚═╝╩═╝╩╝╚╝ ╩ ┴└─└─┘ └─┘ └┘ └─┘┴└─┴└─┴─┴┘└─┘ // ┌─ ┌─┐┌─┐┬─┐ ┌─┐┬ ┬┌┬┐┌─┐┌┬┐┌─┐┌┬┐┌─┐┌┬┐ ┌┬┐┌─┐┌─┐┌┬┐┌─┐ ─┐ // │ ├┤ │ │├┬┘ ├─┤│ │ │ │ ││││├─┤ │ ├┤ ││ │ ├┤ └─┐ │ └─┐ │ // └─ └ └─┘┴└─ ┴ ┴└─┘ ┴ └─┘┴ ┴┴ ┴ ┴ └─┘─┴┘ ┴ └─┘└─┘ ┴ └─┘ ─┘ // > An .eslintrc configuration override for use with the tests in this directory. // // (See .eslintrc in the root directory of this package for more info.) "extends": [ "../.eslintrc" ], "env": { "mocha": true } } ================================================ FILE: test/README.md ================================================ # Sails tests ## Run the tests From the root directory of Sails core, run: ```bash npm test ``` > Or if you're using Windows: > > ```cmd > npm run custom-tests > ``` ## Goals 1. Identify latent inconsistencies or issues that we don't know about yet. 2. Provide low-level coverage of functionality that is difficult or time-consuming to QA / notice. 3. Protect the core from any future breaking changes. 4. Prevent regression. 5. Make merging pull requests easier by removing me (@mikermcneil) as the bottleneck for merging pull requests. (we can just run the tests to see if a change broke anything) 6. Make it easier for folks to contribute more tests, and help unify the style and structure of our existing tests. ## Writing tests > For more information about writing tests (structural conventions, what to test, what _not_ to test) see the [Contribution guide](https://github.com/balderdashy/sails-docs/blob/master/contributing/code-submission-guidelines/writing-tests.md). ================================================ FILE: test/benchmarks/README.md ================================================ # Benchmarks ### Run the benchmarks From the root directory of sails: ```sh $ BENCHMARK=true mocha test/benchmarks ``` To get a more detailed report with millisecond timings for each benchmark, run: ```sh $ BENCHMARK=true mocha test/benchmarks -v ``` ### Goals These tests are related to benchmarking the performance of different parts of Sails. For now, our benchmark tests should be "integration" or "acceptance" tests. By that, I mean they should measure a specific "user action" (e.g. running `sails new`, running `sails lift`, sending an HTTP request to a dummy endpoint, connecting a Socket.io client, etc.). ##### Why test features first, and not each individual method? Feature-wide benchmarks are the "lowest-hanging fruit", if you will. We'll spend much less development time, and still get valuable benchmarks that will give us ongoing data on Sails performance. This way, we'll know where to start writing lower-level benchmarks to identify choke-points. ##### Writing good benchmarks + Pick what you want to test. + Whatever you choose does not have to be atomic (see examples above)-- in an ideal world, we would have benchmarks for every single function in our apps, but that is not how things work today. + Write a benchmark test that isolates that functionality. (the hard part) + Then see how many milliseconds it takes. (easy) > **Advice from Felix Geisendörfer ([@felixge](https://github.com/felixge))** > > + First of all, keep in mind our problems are definitely not the same as Felix's, and we must remember to follow [his own advice](https://github.com/felixge/faster-than-c#taking-performance-advice-from-strangers): `[What]...does not work is taking performance advise (euro-sic) from strangers...` That said, he's got some great ideas. > + [Benchmark-Driven Optimization](https://github.com/felixge/faster-than-c#benchmark-driven-development) > + I also highly recommend this [talk on optimization and benchmarking](http://2012.jsconf.eu/speaker/2012/09/05/faster-than-c-parsing-node-js-streams-.html) ([slides](https://github.com/felixge/faster-than-c)). ### Things to test Here are the most important things we need to benchmark: ##### Features: + Bootstrap + `sails.load` (programmatic) + `sails.lift` (programmatic) and `sails lift` (CLI) + `sails load` + `sails new` and `sails generate *` + (could be pulled into generic generator suite, like adapters) + Router + private Sails requests via `sails.emit('request')` + http requests to the HTTP server + http file uploads to the HTTP server + connections to the socket.io server + socket emissions to the socket.io server + socket broadcasts FROM the socket.io server (pubsub hook) > Thankfully, the ORM is already covered by the benchmarks in Waterline core and its generic adapter tests. ##### Measuring: + Execution time + Memory usage ##### Under varying levels of stress: + Low concurrency (c1k) + High-moderate concurrency (c10k) ##### In varying environments: + Every permutation of the core hook configuration + With different configuration options set ### Considerations Some important things to consider when benchmarking Node.js / Express-based apps in general: + Keep in mind that, unless you use the cluster module, or spin up multiple instances of the server, you're testing performance on one CPU. Most production servers, cloud or not, have more than one CPU available. This may or may not be relevant, depending on the benchmark and whether it is CPU-intensive. + Be sure to configure [`maxSockets`](http://nodejs.org/api/http.html#http_agent_maxsockets), since most of the requests in a benchmark test are likely to originate from the same source. > **Sources:** > + https://groups.google.com/forum/#!topic/nodejs/tgATyqF-HIc ### Benchmarking libraries > Don't know the best route here yet-- but here are some links for reference. Would love to hear your ideas! + https://github.com/spumko/flod + https://github.com/LearnBoost/mongoose/blob/3.8.x/benchmarks/benchjs/casting.js + https://npmjs.org/package/benchmark ================================================ FILE: test/benchmarks/helpers/benchmarx.js ================================================ var Benchmark = require('benchmark'); var _ = require('@sailshq/lodash'); /** * benchmarx() * --------------------------- * @param {String} name * @param {Array} testFns [array of functions] * @param {Function} notifier * @param {Function} done */ module.exports = function benchmarx (name, testFns, done) { Benchmark.options.minSamples = 500; var suite = new Benchmark.Suite({ name: name }); _.each(testFns, function (testFn) { suite = suite.add(testFn.name, { defer: true, fn: function (deferred) { testFn(function _afterRunningTestFn(err){ process.nextTick(function _afterEnsuringAsynchronous(){ if (err) { console.error('An error occured when attempting to benchmark this code:\n',err); // Resolve the deferred either way. } deferred.resolve(); });//</afterwards cb from waiting for nextTick> });//</afterwards cb from running test fn> } });//<suite.add> });//</each testFn> suite.on('cycle', function(event) { console.log(' •',String(event.target), '(avg ' + (event.target.stats.mean * 1000) + ' ms)'); }) .on('complete', function() { console.log('Fastest is ' + this.filter('fastest').map('name')); console.log('Slowest is ' + this.filter('slowest').map('name')); return done(undefined, this); }) .run(); }; ================================================ FILE: test/benchmarks/sails.load.test.js ================================================ var _ = require('@sailshq/lodash'); var chalk = require('chalk'); var portfinder = require('portfinder'); portfinder.basePort = 2001; var SHOW_VERBOSE_BENCHMARK_REPORT = _.any(process.argv, function(arg) { return arg.match(/-v/); }); if (process.env.BENCHMARK) { describe('benchmarks', function() { describe('sails.load()', function() { before(setupBenchmarks); after(reportBenchmarks); // // Instantiate // benchmark('require("sails")', function(cb) { var Sails = require('../../lib/app'); var sails = new Sails(); return cb(); }); // // Load // benchmark('sails.load [first time, no hooks]', function(cb) { var Sails = require('../../lib/app'); var sails = new Sails(); sails.load({ log: { level: 'error' }, globals: false, loadHooks: [] }, _getTestCleanupCallback(sails, cb)); }); benchmark('sails.load [again, no hooks]', function(cb) { this.expected = 25; this.comment = 'faster b/c of require cache'; var Sails = require('../../lib/app'); var sails = new Sails(); sails.load({ log: { level: 'error' }, globals: false, loadHooks: [] }, _getTestCleanupCallback(sails, cb)); }); benchmark('sails.load [with moduleloader hook]', function(cb) { this.expected = 25; this.comment = 'faster b/c of require cache'; var Sails = require('../../lib/app'); var sails = new Sails(); sails.load({ log: { level: 'error' }, globals: false, loadHooks: ['moduleloader'] }, _getTestCleanupCallback(sails, cb)); }); benchmark('sails.load [all core hooks]', function(cb) { this.expected = 3000; var Sails = require('../../lib/app'); var sails = new Sails(); sails.load({ log: { level: 'error' }, globals: false }, _getTestCleanupCallback(sails, cb)); }); benchmark('sails.load [again, all core hooks]', function(cb) { this.expected = 3000; var Sails = require('../../lib/app'); var sails = new Sails(); sails.load({ log: { level: 'error' }, globals: false }, _getTestCleanupCallback(sails, cb)); }); // // Lift // benchmark('sails.lift [w/ a hot require cache]', function(cb) { this.expected = 3000; var Sails = require('../../lib/app'); var sails = new Sails(); portfinder.getPort(function(err, port) { if (err) { throw err; } sails.lift({ log: { level: 'error' }, port: port, globals: false }, _getTestCleanupCallback(sails, cb)); }); }); benchmark('sails.lift [again, w/ a hot require cache]', function(cb) { this.expected = 3000; var Sails = require('../../lib/app'); var sails = new Sails(); portfinder.getPort(function(err, port) { if (err) { throw err; } sails.lift({ log: { level: 'error' }, port: port, globals: false }, _getTestCleanupCallback(sails, cb)); }); }); }); }); /** * Run the specified function, capturing time elapsed. * * @param {[type]} description [description] * @param {Function} fn [description] * @param {Function} afterwards */ function benchmark(description, fn) { it(description, function (cb) { var self = this; var t1 = process.hrtime(); fn.apply(self, [ function _callbackFromFn(err) { var _result = {}; // If a `comment` or `expected` was provided, harvest it _result.expected = self.expected; self.expected = null; _result.comment = self.comment; self.comment = null; var diff = process.hrtime(t1); _.result.duration = (diff[0] * 1e6) + (diff[1] / 1e3); _result.benchmark = description; // console.log('finished ',_result); self.benchmarks.push(_result); if (err) { return cb(err); } return cb.apply(Array.prototype.slice.call(arguments)); } ]); });//</it> } /** * Use in mocha's `before` * * @this {Array} benchmarks */ function setupBenchmarks() { this.benchmarks = []; } /** * Use in mocha's `after` * * @this {Array} benchmarks */ function reportBenchmarks() { var output = '\n\nBenchmark Report ::\n'; output += _.reduce(this.benchmarks, function(memo, result) { // Convert to ms- var ms = (result.duration / 1000.0); // round to 0 decimal places function _roundDecimalTo(num, numPlaces) { return +(Math.round(num + ('e+' + numPlaces)) + ('e-' + numPlaces)); } ms = _roundDecimalTo(ms, 2); var expected = result.expected || 1000; // threshold: the "failure" threshold var threshold = result.expected; var color = (ms < 1 * expected / 10) ? 'green' : (ms < 3 * expected / 10) ? 'green' : (ms < 6 * expected / 10) ? 'cyan' : (ms < threshold) ? 'yellow' : 'red'; ms += 'ms'; ms = ms[color]; // Whether to show expected ms var showExpected = true; // ms >= threshold; return memo + '\n ' + chalk.grey((result.benchmark + '') + ' :: ') + ms + // Expected ms provided, and the test took quite a while chalk.grey(result.expected && showExpected ? '\n (expected ' + expected + 'ms' + (result.comment ? ' --' + result.comment : '') + ')' : // Comment provided - but no expected ms (result.comment ? '\n (' + result.comment + ')\n' : '') ); }, ''); // Log output (optional) if (SHOW_VERBOSE_BENCHMARK_REPORT) { console.log(output); } } /** * * @param {Function} cb [description] * @return {Function} */ function _getTestCleanupCallback(app, cb) { return function afterLoadingSails (err) { if(err) { return cb(new Error('Failed with error: '+err.stack)); } app.lower(function (errLowering){ if (errLowering) { return cb(new Error('Everything was otherwise ok, but failed to `.lower()` app. Details:' + errLowering.stack)); } if (err) { return cb(err); } return cb(); }); }; } } ================================================ FILE: test/benchmarks/sails.request.generic.test.js ================================================ /** * Module dependencies */ var util = require('util'); var assert = require('assert'); var tmp = require('tmp'); var _ = require('@sailshq/lodash'); var request = require('@sailshq/request'); var Filesystem = require('machinepack-fs'); var appHelper = require('../integration/helpers/appHelper'); var Sails = require('../../lib').constructor; var benchmarx = require('./helpers/benchmarx'); if (process.env.BENCHMARK) { tmp.setGracefulCleanup(); describe('benchmarks', function() { describe('sails requests :: ', function() { describe('generic requests ::', function() { this.timeout(0); describe('baseline (load only, no hooks) ::', function() { var curDir, tmpDir, sailsApp; var warn; var warnings = []; before(function(done) { // Cache the current working directory. curDir = process.cwd(); // Create a temp directory. tmpDir = tmp.dirSync({gracefulCleanup: true, unsafeCleanup: true}); // Switch to the temp directory. process.chdir(tmpDir.name); // Load the Sails app. (new Sails()).load({ loadHooks: [], log: {level: 'silent'}, routes: { '/test1': function(req, res) { return res.status(200).send(); }, 'GET /test2': function(req, res) { return res.status(200).send(); }, 'POST /test3': function(req, res) { return res.status(200).send(); }, '/test4/:id': function(req, res) { return res.status(200).send(); }, '/test5/*': function(req, res) { return res.status(200).send(); }, 'r|test6/\\d+|foo': function(req, res) { return res.status(200).send(); }, '/test7': function(req, res) { return res.status(200).send('foo'); }, '/test8': function(req, res) { return res.status(200).json({foo: 'bar'}); }, 'POST /test9': function(req, res) { return res.status(200).send(req.param('foo')); }, 'POST /test10': function(req, res) { return res.status(200).json(req.allParams); }, } }, function(err, _sails) { sailsApp = _sails; return done(err); }); }); after(function(done) { sailsApp.lower(function() { process.chdir(curDir); return done(); }); }); it('', function(done) { benchmarx('', [ function route_with_no_verb(done) { sailsApp.request('http://localhost:1342/test1', done); }, function route_with_GET_verb(done) { sailsApp.request('http://localhost:1342/test2', done); }, function route_with_POST_verb(done) { sailsApp.request({ url: 'http://localhost:1342/test3', method: 'post', data: {foo: 'bar'} }, done); }, function route_with_dynamic_param(done) { sailsApp.request('http://localhost:1342/test4/123', done); }, function route_with_wildcard(done) { sailsApp.request('http://localhost:1342/test5/abc/123', done); }, function route_with_regex(done) { sailsApp.request('http://localhost:1342/test6/666', done); }, function respond_with_string(done) { sailsApp.request('http://localhost:1342/test7', done); }, function respond_with_json(done) { sailsApp.request('http://localhost:1342/test8', done); }, function reflect_one_param(done) { sailsApp.request({ url: 'http://localhost:1342/test9', method: 'post', data: {foo: 'bar'} }, done); }, function reflect_all_params(done) { sailsApp.request({ url: 'http://localhost:1342/test10', method: 'post', data: {foo: 'bar', abc: 123} }, done); } ], done); }); }); describe('lift w/ no hooks besides http and request) ::', function() { var curDir, tmpDir, sailsApp; var warn; var warnings = []; this.timeout(0); before(function(done) { // Cache the current working directory. curDir = process.cwd(); // Create a temp directory. tmpDir = tmp.dirSync({gracefulCleanup: true, unsafeCleanup: true}); // Switch to the temp directory. process.chdir(tmpDir.name); // Load the Sails app. (new Sails()).lift({ port: 1342, loadHooks: ['http', 'request'], log: {level: 'silent'}, routes: { '/test1': function(req, res) { return res.status(200).send(); }, 'GET /test2': function(req, res) { return res.status(200).send(); }, 'POST /test3': function(req, res) { return res.status(200).send(); }, '/test4/:id': function(req, res) { return res.status(200).send(); }, '/test5/*': function(req, res) { return res.status(200).send(); }, 'r|test6/\\d+|foo': function(req, res) { return res.status(200).send(); }, '/test7': function(req, res) { return res.status(200).send('foo'); }, '/test8': function(req, res) { return res.status(200).json({foo: 'bar'}); }, 'POST /test9': function(req, res) { return res.status(200).send(req.param('foo')); }, 'POST /test10': function(req, res) { return res.status(200).json(req.allParams); }, } }, function(err, _sails) { sailsApp = _sails; return done(err); }); }); after(function(done) { sailsApp.lower(function() { process.chdir(curDir); return done(); }); }); it('', function(done) { benchmarx('', [ function route_with_no_verb(done) { request.get('http://localhost:1342/test1', done); }, function route_with_GET_verb(done) { request.get('http://localhost:1342/test2', done); }, function route_with_POST_verb(done) { request({ url: 'http://localhost:1342/test3', method: 'post', json: {foo: 'bar'} }, done); }, function route_with_dynamic_param(done) { request.get('http://localhost:1342/test4/123', done); }, function route_with_wildcard(done) { request.get('http://localhost:1342/test5/abc/123', done); }, function route_with_regex(done) { request.get('http://localhost:1342/test6/666', done); }, function respond_with_string(done) { request.get('http://localhost:1342/test7', done); }, function respond_with_json(done) { request.get('http://localhost:1342/test8', done); }, function reflect_one_param(done) { request.post('http://localhost:1342/test9', {foo: 'bar'}, done); }, function reflect_all_params(done) { request.post('http://localhost:1342/test10', {foo: 'bar', abc: 123}, done); } ], done); }); }); describe('lift with all default hooks ::', function() { var curDir, tmpDir, sailsApp; var warn; var warnings = []; this.timeout(0); before(function(done) { // Cache the current working directory. curDir = process.cwd(); // Create a temp directory. tmpDir = tmp.dirSync({gracefulCleanup: true, unsafeCleanup: true}); // Switch to the temp directory. process.chdir(tmpDir.name); // Link dependencies so that default hooks will work appHelper.linkDeps(tmpDir.name); // Load the Sails app. (new Sails()).lift({ port: 1342, log: {level: 'silent'}, routes: { '/test1': function(req, res) { return res.status(200).send(); }, 'GET /test2': function(req, res) { return res.status(200).send(); }, 'POST /test3': function(req, res) { return res.status(200).send(); }, '/test4/:id': function(req, res) { return res.status(200).send(); }, '/test5/*': function(req, res) { return res.status(200).send(); }, 'r|test6/\\d+|foo': function(req, res) { return res.status(200).send(); }, '/test7': function(req, res) { return res.status(200).send('foo'); }, '/test8': function(req, res) { return res.status(200).json({foo: 'bar'}); }, 'POST /test9': function(req, res) { return res.status(200).send(req.param('foo')); }, 'POST /test10': function(req, res) { return res.status(200).json(req.allParams); }, } }, function(err, _sails) { sailsApp = _sails; return done(err); }); }); after(function(done) { sailsApp.lower(function() { process.chdir(curDir); return done(); }); }); it('', function(done) { benchmarx('', [ function route_with_no_verb(done) { request.get('http://localhost:1342/test1', done); }, function route_with_GET_verb(done) { request.get('http://localhost:1342/test2', done); }, function route_with_POST_verb(done) { request({ url: 'http://localhost:1342/test3', method: 'post', json: {foo: 'bar'} }, done); }, function route_with_dynamic_param(done) { request.get('http://localhost:1342/test4/123', done); }, function route_with_wildcard(done) { request.get('http://localhost:1342/test5/abc/123', done); }, function route_with_regex(done) { request.get('http://localhost:1342/test6/666', done); }, function respond_with_string(done) { request.get('http://localhost:1342/test7', done); }, function respond_with_json(done) { request.get('http://localhost:1342/test8', done); }, function reflect_one_param(done) { request.post('http://localhost:1342/test9', {foo: 'bar'}, done); }, function reflect_all_params(done) { request.post('http://localhost:1342/test10', {foo: 'bar', abc: 123}, done); } ], done); }); }); }); }); }); } ================================================ FILE: test/fixtures/constants.js ================================================ /** * Constants about the Sails framework. * For use in tests. * * @type {Object} */ module.exports = { EXPECTED_DEFAULT_HOOKS: require('../../lib/app/configuration/default-hooks') }; ================================================ FILE: test/fixtures/customHooks.js ================================================ /** * Stub custom hooks for use in tests. * * @type {Object} */ module.exports = { // Extremely simple hook that doesn't do anything. NOOP: function (sails) { return { identity: 'noop' }; }, // Depends on 'noop' hook NOOP2: function (sails) { return { // TODO: indicate dependency on 'noop' hook identity: 'noop2' }; }, // Deliberately rotten hook- it throws. SPOILED_HOOK: function (sails) { throw new Error('smells nasty'); }, // Hook to test `defaults` object DEFAULTS_OBJ: function(sails) { return { identity: 'defaults_obj', defaults: { foo: 'bar', inky: { dinky: 'doo', pinky: 'dont' } } }; }, // Hook to test `defaults` function DEFAULTS_FN: function(sails) { return { identity: 'defaults_fn', defaults: function() { return { foo: 'bar', inky: { dinky: 'doo', pinky: 'dont' } }; } }; }, // Hook to test `initialize` function INIT_FN: function(sails) { return { identity: 'init_fn', initialize: function(cb) { sails.config.hookInitLikeABoss = true; return cb(); } }; }, // Hook to test `configure` function CONFIG_FN: function(sails) { return { identity: 'config_fn', configure: function() { // Test that loaded config is available by copying a value sails.config.hookConfigLikeABoss = sails.config.testConfig; } }; }, // Hook to test `routes` function ROUTES: function(sails) { return { identity: 'routes', routes: { before: { 'GET /foo': function(req, res, next) { sails.config.foo = 'a'; next(); } }, after: { 'GET /foo': function(req, res, next) { sails.config.foo = sails.config.foo + 'c'; res.send(sails.config.foo); } } } }; }, // Hook to test `routes` function ADVANCED_ROUTES: function(sails) { return { identity: 'advanced_routes', initialize: function(cb) { sails.on('router:before', function() { sails.router.bind('GET /foo', function(req, res, next) { sails.config.foo = sails.config.foo + 'b'; next(); }); }); sails.on('router:after', function() { sails.router.bind('GET /foo', function(req, res, next) { sails.config.foo = sails.config.foo + 'e'; res.send(sails.config.foo); }); }); cb(); }, routes: { before: { 'GET /foo': function(req, res, next) { sails.config.foo = 'a'; next(); } }, after: { 'GET /foo': function(req, res, next) { sails.config.foo = sails.config.foo + 'd'; next(); } } } }; }, }; ================================================ FILE: test/fixtures/middleware.js ================================================ /** * Stub middleware/handlers for use in tests. * * @type {Dictionary} */ module.exports = { HELLO: function(req, res) { return res.send('hello world!'); }, GOODBYE: function(req, res) { return res.send('goodbye world!'); }, HELLO_500: function(req, res) { return res.status(500).send('hello world!'); }, JSON_HELLO: function(req, res) { return res.json({ hello: 'world' }); }, SOMETHING_THAT_THROWS: function(req, res) { throw 'oops'; }, }; ================================================ FILE: test/helpers/RouteFactory.helper.js ================================================ /** * Module dependencies */ var _ = require('@sailshq/lodash'); var util = require('util'); /** * @constructor */ function RouteFactory(prefix) { this._prefix = prefix; this._nextTestRoute = 0; } RouteFactory.prototype.next = function() { this._nextTestRoute++; this.current = util.format('/tests/%s%d', this._prefix ? this._prefix + '/' : '', this._nextTestRoute); return this.current; }; module.exports = function (){ return new RouteFactory(); }; ================================================ FILE: test/helpers/router.js ================================================ /** * Module dependencies */ var _ = require('@sailshq/lodash'); var $Router = { /** * Run custom tests if provided * @param {Function} customTests * @return {$Router} */ test: function(customTests) { customTests(); // Chainable return $Router; }, /** * unbind(route) * .expect(expectations) * */ unbind: function(route) { var args = Array.prototype.slice.call(arguments); it('binds the given route', function() { this.sails.router.bind.should.be.ok; this.sails.router.bind.apply(this.sails.router, args); }); it('unbinds the given route', function() { this.sails.router.unbind.should.be.ok; var route = {verb: args[0].split(' ')[0], path: args[0].split(' ')[1]}; this.sails.router.unbind.call(this.sails.router, route); }); var Chainable = { shouldDelete: function(expected) { it('should delete route in _privateRouter', function() { var routeFound = false; _.each(this.sails.router._privateRouter.stack, function(stack){ var samePath = stack.route.path === expected.path; var sameMethod = stack.route.methods[expected.method] || stack.route.methods['_all']; if(samePath && sameMethod) routeFound = true; }); routeFound.should.be.false; }); return Chainable; } }; return Chainable; }, /** * bindRoute(route, handler) * .expectBoundRoute(expectedRoute) * .test(fnContainingCustomMochaTests) * */ bind: function (route, handler) { var args = Array.prototype.slice.call(arguments); it('binds the given route', function() { this.sails.router.bind.should.be.ok; this.sails.router.bind.apply(this.sails.router, args); }); var Chainable = { expectBoundRoute: function(expected) { var readableRoute = expected.method + ' ' + expected.path; it('should create ' + readableRoute + ' in _privateRouter router', function() { var routeFound = false; _.each(this.sails.router._privateRouter.stack, function(stack){ var samePath = stack.route.path === expected.path; var sameMethod = stack.route.methods[expected.method] || stack.route.methods['_all']; if(samePath && sameMethod) routeFound = true; }); routeFound.should.be.true; }); return Chainable; }, // Run custom tests if provided test: function(customTests) { customTests(); return Chainable; } }; return Chainable; } }; module.exports = $Router; // var helper = { // /** // * Send a mock request to the instance of Sails in the test context. // * // * @param {String} url [relative URL] // * @param {Object} options // * // * @return {Function} [bdd] // */ // request: function ( url, options ) { // return function (done) { // var self = this; // var _fakeClient = function (err, response) { // if (err) return done(err); // self.response = response; // done(); // }; // // Emit a request event (will be intercepted by the Router) // this.sails.emit('router:request', _req(), _res(), _fakeClient); // }; // }, // /** // * Bind a route. // * // * @param {String|RegExp} path // * @param {String|Object|Array|Function} target // * @param {String} verb (optional) // * @param {Object} options (optional) // * // * @return {Function} [bdd] // */ // bind: function () { // var args = Array.prototype.slice.call(arguments); // return function () { // this.sails.router.bind.apply(this.sails.router, args); // }; // } // }; // module.exports = helper; // // Private methods // /** // * Test fixture to send requests to Sails. // * // * @api private // */ // function _req ( req ) { // var _enhancedReq = util.defaults(req || {}, { // params: {}, // url: '/', // param: function(paramName) { // return _enhancedReq.params[paramName]; // }, // wantsJSON: true, // method: 'get' // }); // return _enhancedReq; // } // /** // * Test fixture to receive responses from Sails. // * // * @api private // */ // function _res (res) { // var _enhancedRes = util.defaults(res || {}, { // send: function(/* ... */) { // var args = _normalizeResArgs(Array.prototype.slice.call(arguments)); // _enhancedRes._cb(null, { // body: args.other, // headers: {}, // status: args.statusCode || 200 // }); // }, // json: function(body, statusCode) { // // Tolerate bad JSON // var json = util.stringify(body); // if ( !json ) { // var failedStringify = new Error( // 'Failed to stringify specified JSON response body :: ' + body // ); // return _enhancedRes.send(failedStringify.stack, 500); // } // return _enhancedRes.send(json,statusCode); // } // }); // return _enhancedRes; // } // /** // * As long as one of them is a number (i.e. a status code), // * allows a 2-nary method to be called with flip-flopped arguments: // * method( [statusCode|other], [statusCode|other] ) // * // * This avoids confusing errors & provides Express 2.x backwards compat. // * // * E.g. usage in res.send(): // * var args = normalizeResArgs.apply(this, arguments), // * body = args.other, // * statusCode = args.statusCode; // * // * @api private // */ // function _normalizeResArgs( args ) { // // Traditional usage: // // `method( other [,statusCode] )` // var isTraditionalUsage = // 'number' !== typeof args[0] && // ( !args[1] || 'number' === typeof args[1] ); // if ( isTraditionalUsage ) { // return { // statusCode: args[1], // other: args[0] // }; // } // // Explicit usage, i.e. Express 3: // // `method( statusCode [,other] )` // return { // statusCode: args[0], // other: args[1] // }; // } // describe('receives a request', function() { // to('home route (/)', function() { // before(RouterHelper.request('/')); // __it('should trigger the default notFound (404) handler'); // __it('should receive a 404 response from default handler', expect.equal('response.status', 404)); // __it('should not receive a reponse body', expect.notExists('response.body')); // }); // }); // to('a simple fn which calls res.send()', function () { // var route = 'get /simple'; // var fn = function (req, res) { res.send('ok!'); }; // var expectedResponse = { status: 200 }; // __it('binds the route', RouterHelper.bind(route, fn)); // __it('should now exist in the _privateRouter router'); // __it('receives a request to the route',RouterHelper.request(route)); // __it('should have called the proper fn'); // __it('should have sent the expected status code in the response', expect.equal('response.status', expectedResponse.status)); // __it('should have sent the expected response body', expect.equal('response.body', expectedResponse.body)); // __it('should have sent the expected response headers', expect.equal('response.headers', expectedResponse.headers)); // }); // to('a simple fn which throws', function () { // var route = 'get /throws'; // var fn = function (req, res) { throw new Error('heh heh'); }; // var expectedResponse = { status: 500 }; // __it('binds the route', RouterHelper.bind(route, fn)); // __it('should now exist in the _privateRouter router'); // __it('receives a request to the route', RouterHelper.request(route)); // __it('should have called the proper fn'); // __it('should have sent the proper response', expect.equal('response', expectedResponse)); // }); // }); // // private bdd helpers // function __it(name, fn) { // it('\n\t ...it ' + name, fn); // } // function to(name,fn) { // describe('\n\t-- to ' +name+'...', fn); // } ================================================ FILE: test/helpers/sails.js ================================================ /** * Module dependencies */ var _ = require('@sailshq/lodash'); var util = require('util'); var should = require('should'); var domain = require('domain'); var Sails = require('root-require')('lib/app'); /** * Manage an instance of Sails * * @type {Object} */ var helper = { /** * Can call: * -> helper.load() * -> helper.load.withAllHooksDisabled() * -> helper.load.expectingTerminatedProcess() */ load: (function () { /** * _cleanOptions() * * @param {Object} options * @type {Function} * @api private */ function _cleanOptions (options) { var testDefaults = { log: {level: 'error'}, globals: false }; options = _.isObject(options) ? options : {}; return _.defaults(options, testDefaults); } /** * This function is returned by this test helper * to be called by subsequent tests. * * @param {Object} options * @return {SJSApp} */ var _load = function (options) { var testDescription, msSlowThreshold; var sailsOpts = _cleanOptions(options); // Defaults // (except use test defaults) if (!_.isObject(options)) { testDescription = 'default settings'; msSlowThreshold = 750; } else { // Specified options + defaults // (except default log level to 'error') testDescription = util.inspect(options); msSlowThreshold = 2000; } return _with(testDescription, sailsOpts, msSlowThreshold); }; /** * [withAllHooksDisabled description] * @return {[type]} [description] */ _load.withAllHooksDisabled = function () { return _with('all hooks disabled', { log: {level: 'error'}, globals: false, loadHooks: [] }, 500); }; /** * [expectFatalError description] * @param {[type]} options [description] * @return {[type]} [description] */ _load.expectFatalError = function( options ) { options = _.isObject(options) ? options : {}; var sailsOpts = _cleanOptions(options); it(', sails should deliberately terminate process', function (done) { var sails = new Sails(); // TODO: // Pull this error domain into the core and // wrap the hook loading process (or a comparable solution.) // Should not have to do this here! // Use error domain to catch failure var deliberateErrorDomain = domain.create(); deliberateErrorDomain.on('error', function (err) { console.log('domain emitted error', err); deliberateErrorDomain.exit(); return done(); }); deliberateErrorDomain.run(function () { sails.load(sailsOpts || {}, function (err) { var e = 'Should not have made it to load() ' + 'callback, with or without an error!'; if (err) e+='\nError: ' + util.inspect(err); deliberateErrorDomain.exit(); return done(new Error(e)); }); }); }); }; return _load; })(), /** * @return {Sails} `sails` instance from mocha context */ get: function (passbackFn) { // Use mocha context to get a hold of the Sails instance it('should get a Sails instance', function () { passbackFn(this.sails); }); } }; module.exports = helper; /** * Setup and teardown a Sails instance for testing. * * @param {String} description * @param {Object} sailsOpts * @param {Integer} msThreshold [before we consider it "slow"] * * @returns {SJSApp} * @api private */ function _with (description, sailsOpts, msThreshold) { var sails = new Sails(); it('sails loaded (with ' + description + ')', function (done) { if (msThreshold) { this.slow(msThreshold); } // Expose a new app instance as `this.sails` // for other tests to use. this.sails = sails; // Load the app sails.load(sailsOpts || {}, done); }); after(function teardown(done) { // Make sure the app is done sails.lower(function(){setTimeout(done, 100);}); }); return sails; } ================================================ FILE: test/helpers/test-spawning-sails-child-process-in-cwd.js ================================================ /** * Module dependencies */ var path = require('path'); var _ = require('@sailshq/lodash'); var request = require('@sailshq/request'); var MProcess = require('machinepack-process'); /** * testSpawningChildProcessInCwd() * * This concisely-named test helper function injects a describe block, testing how Sails fares * when it comes time to `sails lift` or `sails console` or `node app.js` or otherwise start * a Sails app in the current working directory. * * * @optional {Array} cliArgs * an array of additional string CLI args/opts to pass to the Sails process on start * (e.g. `['--prod', '--port=1331']`) * * @optional {Dictionary} envVars * a dictionary of environment variables to supply to the spawned process. * Note: the default is to use the current process's environment, so if you set this * option, you probably want to merge process.env into the value you use * * @optional {Dictionary} httpRequestInstructions * A dictionary that gets passed in to `request()` when this helper attempts * to contact the Sails server running in the child process. * If provided at all, this can/must contain: * @required {String} method * @required {String} uri * @optional {String} expectedStatusCode [defaults to 200] * * @optional {Function} fnWithAdditionalTests * A function with additional custom tests; that is, it has one or more `it()` blocks inside. * * @optional {Boolean} expectFailedLift * A flag which, if enabled, causes this test helper to _expect_ the app to fail. * Also if it is set AND `httpRequestInstructions` are set, then the HTTP request * will still be sent-- but just to _make sure_ it fails too. */ module.exports = function testSpawningSailsLiftChildProcessInCwd (opts){ describe('and then waiting for a bit', function() { // We can't use HTTP or ws:// requests for IPC for these tests, because we're trying // to determine whether the Sails app has _actually loaded_, not just whether HTTP // requests will work (although that's good to know too). // // To work around that, we use a timeout. // N_SECONDS is used below to determine how long to wait for the lift before // attempting to shut her down with a SIGTERM. We _could_ wait for output, but that's // pretty vague (since it could be anything). At least with the timeout, we know that // if Sails is still lifting, the test will fail. // // In the future, we could use some other sort of IPC strategy to pull this off, // but that would involve changes to the actual Sails core code base, which seems // silly and potentially costly as far as time and technical debt in the code base. // Anyway, it's unnecessary since this works so... daintily. var N_SECONDS = 10; // The max # of seconds to wait for graceful shutdown is used below. // It's the other piece of how we know the app must have successfully lifted vs not. // In addition to the N_SECONDS, this extra time gets tacked on at the very end below. var MAX_SECONDS_TO_WAIT_FOR_GRACEFUL_SHUTDOWN = 1; // Tell mocha not to look red and angry, since we totally planned for it to take this long. this.slow((N_SECONDS*1000)+(MAX_SECONDS_TO_WAIT_FOR_GRACEFUL_SHUTDOWN*1000)+1500); // This variable will hold the reference to the child process. var sailsLiftProc; // Spawn the child process before(function(done) { sailsLiftProc = MProcess.spawnChildProcess({ command: 'node', cliArgs: opts.cliArgs, environmentVars: opts.envVars }).now(); // For debugging, as needed: // sailsLiftProc.stdout.on('data', function (data){ // console.log('stdout:',''+data); // }); // sailsLiftProc.stderr.on('data', function (data){ // console.log('stderr:',''+data); // }); // After N seconds, continue to the test. setTimeout(function (){ return done(); }, N_SECONDS*1000); });//</before> it('should still be lifted', function(done) { // Technically, we don't really know if the server is still lifted in here. // But we will find out in a bit (in the `after`). So we'll do another little // timeout, this time just somewhere between 0 and 500ms. Why not have a little // fun right? Throwing a little entropy into the mix. Bam! setTimeout(function (){ return done(); }, Math.floor(Math.random()*500)); });//</it> // Now if httpRequestInstructions were provided, we ping to the server to see whether this puppy // is ready to handle all those hot hot HTTP requests we have planned for it. // (expectations of response vary based on options passed to this helper) if (!_.isUndefined(opts.httpRequestInstructions)){ // If the `opts.expectFailedLift` flag was provided, we're actually expecting an error here. if (opts.expectFailedLift) { it('should FAIL when a `'+opts.httpRequestInstructions.method+'` request is sent to `'+opts.httpRequestInstructions.uri+', because we\'re expecting Sails to have failed when attempting to lift`', function(done) { request(opts.httpRequestInstructions, function(err, response, body) { if (err) { // Since the `opts.expectFailedLift` flag was provided, we're actually expecting an error here. return done(); } return done(new Error('Expected request to fail, since Sails should not have lifted successfully')); }); });//</it> } // Normal case (expecting sails to have lifted) else { it('should respond with a 200 status code when a `'+opts.httpRequestInstructions.method+'` request is sent to `'+opts.httpRequestInstructions.uri+'`', function(done) { request(opts.httpRequestInstructions, function(err, response, body) { if (err) { // This kind of "omg server is not online" error is generally rather bad. return done(err); } if (response.statusCode !== (opts.httpRequestInstructions.expectedStatusCode||200)) { return done(new Error('Expected to get a '+(opts.httpRequestInstructions.expectedStatusCode||200)+' status code from the server, but instead all we got was this lousy status code: `' + response.statusCode + '`')); } return done(); }); });//</it> } }//</httpRequestInstructions were provided> // Now run any additional tests. // (i.e. this function contains `it` blocks) if (_.isFunction(opts.fnWithAdditionalTests)) { opts.fnWithAdditionalTests(); } // Now send a SIGTERM signal. after(function (done){ MProcess.killChildProcess({ childProcess: sailsLiftProc, maxMsToWait: MAX_SECONDS_TO_WAIT_FOR_GRACEFUL_SHUTDOWN*1000 }).exec(function (err){ // If it worked, that means our child process was // still alive N seconds after it began lifting, and // that it was able to shut down gracefully in a // reasonable amount of time (see `maxMsToWait` above). if (!err) { // The one exception is if the `opts.expectFailedLift` flag was provided, in which case this // is actually bad: In that case, it means the server clearly must have been lifted (since we // just finished SIGTERMing it to death) if (opts.expectFailedLift) { return done(new Error('Hmm... But the Sails app should not have been lifted (the graceful shutdown should have failed).')); } // But otherwise in the general case, this means we're good. return done(); } // Otherwise it didn't work, which means our child process never lifted // properly, or it was still lifting after N seconds. // // It also means we need to force-kill the child process or risk // loosing little Grunt daemons to run amock in our machines. MProcess.killChildProcess({ childProcess: sailsLiftProc, force: true }).exec(function (_forceKillErr){ // Now, if the `opts.expectFailedLift` flag was provided, this is actually what we want. if (opts.expectFailedLift) { return done(); } // But normally it's totally bad news that we're having to SIGKILL this thing, // so we send back an error whether or not the SIGKILL worked. if (_forceKillErr) { return done(new Error('So there was a problem:\n'+err.stack+'\nBut hey, also force-killing the child process didn\'t work. So something weird is going on, I\'d say. Check out the deets on that force kill error:\n' + _forceKillErr.stack)); } else { return done(new Error('Should have been able to gracefully shut down child process, because it should have been lifted. Heres the error that came back when attempting the graceful shutdown (although honestly it prbly doesn\'t matter-- it\'s more likely the app just didn\'t successfully lift). Graceful shutdown error:\n'+err.stack)); } }); }); });//</after> });//</describe> }; ================================================ FILE: test/helpers/test-spawning-sails-lift-child-process-in-cwd.js ================================================ /** * Module dependencies */ var path = require('path'); var _ = require('@sailshq/lodash'); var request = require('@sailshq/request'); var MProcess = require('machinepack-process'); var testSpawningSailsChildProcessInCwd = require('./test-spawning-sails-child-process-in-cwd'); /** * testSpawningSailsLiftChildProcessInCwd() * * This concisely-named test helper function injects a describe block, testing how Sails fares * when it comes time to `sails lift` in the current working directory. * * @required {String} pathToSailsCLI * the absolute path to the Sails CLI * * @required {Array} liftCliArgs * an array of additional string CLI args/opts to pass to `sails lift` * (e.g. `['--prod', '--port=1331']`) * * @optional {Dictionary} envVars * a dictionary of environment variables to supply to the spawned process. * Note: the default is to use the current process's environment, so if you set this * option, you probably want to merge process.env into the value you use * * @optional {Dictionary} httpRequestInstructions * A dictionary that gets passed in to `request()` when this helper attempts * to contact the Sails server running in the child process. * If provided at all, this can/must contain: * @required {String} method * @required {String} uri * @optional {String} expectedStatusCode [defaults to 200] * * @optional {Function} fnWithAdditionalTests * A function with additional custom tests; that is, it has one or more `it()` blocks inside. * * @optional {Boolean} expectFailedLift * A flag which, if enabled, causes this test helper to _expect_ the lift to fail. * Also if it is set AND `httpRequestInstructions` are set, then the HTTP request * will still be sent-- but just to _make sure_ it fails too. */ module.exports = function testSpawningSailsLiftChildProcessInCwd (_opts){ if (!_.isArray(_opts.liftCliArgs)){ throw new Error('Consistency violation: Missing or invalid option (`liftCliArgs` should be an array) in `testSpawningSailsLiftChildProcessInCwd()`. I may just be a test helper, but I\'m serious about assertions!!!'); } if (!_.isString(_opts.pathToSailsCLI)){ throw new Error('Consistency violation: Missing or invalid option (`pathToSailsCLI` should be a string) in `testSpawningSailsLiftChildProcessInCwd()`. I may just be a test helper, but I\'m serious about assertions!!!'); } var opts = _.extend({ cliArgs: [_opts.pathToSailsCLI, 'lift'].concat(_opts.liftCliArgs) }, _opts); return testSpawningSailsChildProcessInCwd(opts); }; ================================================ FILE: test/hooks/blueprints/initialize.test.js ================================================ /** * Module dependencies */ var supertest = require('supertest'); var $Sails = require('../../helpers/sails'); var $Router = require('../../helpers/router'); describe('Blueprints hook', function (){ describe('without ORM hook', function (){ $Sails.load({ globals: false, loadHooks: [ 'moduleloader', 'userconfig', 'blueprints' ] }); // TODO: test that blueprint actions are loaded // TODO: test shadow routes are bound for: // + all controller actions // + controllers' index action }); describe('with controllers hook', function (){ $Sails.load({ globals: false, loadHooks: [ 'moduleloader', 'userconfig', 'controllers', 'blueprints' ] }); // TODO: test that blueprint actions are loaded // TODO: test shadow routes are bound: // + controller.*() // + controller.index() // + CRUD methods (find(),create(),etc.) // + RESTful (GET,POST,PUT,DELETE) // + URL-bar shortcuts (/find, /create, etc.) }); describe('with ORM hook', function (){ $Sails.load({ globals: false, loadHooks: [ 'moduleloader', 'userconfig', 'orm', 'blueprints' ] }); // TODO: test that blueprint actions are loaded // TODO: test shadow routes are bound: // + controller.*() // + controller.index() // + CRUD methods (find(),create(),etc.) // + RESTful (GET,POST,PUT,DELETE) // + URL-bar shortcuts (/find, /create, etc.) }); describe('with ORM and controllers hooks', function (){ $Sails.load({ globals: false, loadHooks: [ 'moduleloader', 'userconfig', 'orm', 'controllers', 'blueprints' ] }); // TODO: test that blueprint actions are loaded // TODO: test shadow routes are bound: // + controller.*() // + controller.index() // + CRUD methods (find(),create(),etc.) // + RESTful (GET,POST,PUT,DELETE) // + URL-bar shortcuts (/find, /create, etc.) }); describe('with ORM and policies hooks', function (){ $Sails.load({ globals: false, loadHooks: [ 'moduleloader', 'userconfig', 'orm', 'policies', 'blueprints' ] }); // TODO: test that blueprint actions are loaded // TODO: test shadow routes are bound: // + controller.*() // + controller.index() // + CRUD methods (find(),create(),etc.) // + RESTful (GET,POST,PUT,DELETE) // + URL-bar shortcuts (/find, /create, etc.) }); describe('with controllers and policies hooks', function (){ $Sails.load({ globals: false, loadHooks: [ 'moduleloader', 'userconfig', 'controllers', 'policies', 'blueprints' ] }); // TODO: test that blueprint actions are loaded // TODO: test shadow routes are bound: // + controller.*() // + controller.index() // + CRUD methods (find(),create(),etc.) // + RESTful (GET,POST,PUT,DELETE) // + URL-bar shortcuts (/find, /create, etc.) }); describe('with controllers, policies, and orm hooks', function (){ $Sails.load({ globals: false, loadHooks: [ 'moduleloader', 'userconfig', 'controllers', 'policies', 'orm', 'blueprints' ] }); // TODO: test that blueprint actions are loaded // TODO: test shadow routes are bound: // + controller.*() // + controller.index() // + CRUD methods (find(),create(),etc.) // + RESTful (GET,POST,PUT,DELETE) // + URL-bar shortcuts (/find, /create, etc.) }); }); ================================================ FILE: test/hooks/http/initialize.test.js ================================================ /** * Module dependencies */ var assert = require('assert'); var util = require('util'); var Sails = require('../../../lib').Sails; var $Router = require('../../helpers/router'); var request = require('@sailshq/request'); describe('HTTP hook', function (){ describe('with custom bodyparser middleware config', function() { var app; before(function(done) { app = Sails(); app.lift({ globals: false, loadHooks: [ 'moduleloader', 'userconfig', 'http' ], log: {level: 'silent'}, http: { middleware: { bodyParser: function(req, res, next) { req.foo = 'bar'; return next(); } } }, routes: { 'get /': function(req, res) {return res.send(req.foo);} }, port: 1342 }, done); }); it('should be able to respond to requests using the custom bodyparser', function(done) { request.get('http://localhost:1342', function(err, res, body) { if (err) { return done(err); } try { assert.equal(body, 'bar'); } catch (e) {return done(e);} return done(); }); }); after(function(done) { app.lower(done); }); }); describe('with invalid `trustProxy` config', function() { it('should throw an error', function(done) { var app = Sails(); app.lift({ globals: false, port: 1535, environment: 'development', log: {level: 'silent'}, http: { trustProxy: '' }, hooks: {grunt: false, pubsub: false}, }, function(err, _app) { if (err && err.code && err.code === 'E_HTTP_BAD_TRUSTPROXY') { return done(); } if (err) { return done(err); } _app.lower(function(err) { if (err) { return done(new Error('App lifted when it should have failed with E_HTTP_BAD_TRUSTPROXY. Additionally, an error occurred while lowering: ' + util.inspect(err))); } return done(new Error('App lifted when it should have failed with E_HTTP_BAD_TRUSTPROXY')); }); }); }); }); }); ================================================ FILE: test/hooks/pubsub/initialize.test.js ================================================ /** * Module dependencies */ var Sails = require('../../../lib').Sails; xdescribe('Pubsub hook', function (){ describe('loading a Sails app', function (){ describe('without ORM hook', function (){ var app = Sails(); it('should fail', function (done){ app.load({ globals: false, log: {level: 'silent'}, loadHooks: ['moduleloader','userconfig','pubsub'] }, function (err){ if (err) { return done(); } return done(new Error('Should have failed to load the pubsub hook w/o the `orm` hook.')); }); }); after(app.lower); }); describe('without sockets hook', function (){ var app = Sails(); it('should fail', function (done){ app.load({ globals: false, log: {level: 'silent'}, loadHooks: ['moduleloader','userconfig','orm', 'pubsub'] }, function (err){ if (err) { return done(); } return done(new Error('Should have failed to load the pubsub hook w/o the `sockets` hook.')); }); }); after(app.lower); }); describe('without http hook', function (){ var app = Sails(); it('should fail', function (done){ app.load({ globals: false, log: {level: 'silent'}, loadHooks: ['moduleloader','userconfig','orm', 'sockets', 'pubsub'] }, function (err){ if (err) { return done(); } return done(new Error('Should have failed to load the pubsub hook w/o the `http` hook.')); }); }); after(app.lower); }); describe('with `orm` and `sockets` hooks', function (){ var app = Sails(); it('should load successfully', function (done){ app.load({ globals: false, log: {level: 'warn'}, hooks: { sockets: require('sails-hook-sockets'), orm: require('sails-hook-orm'), }, loadHooks: ['moduleloader','userconfig','orm', 'http', 'sockets', 'pubsub'] }, function (err){ if (err) { return done(err); } return done(); }); }); after(app.lower); }); }); }); ================================================ FILE: test/hooks/request/initialize.test.js ================================================ /** * Module dependencies */ var assert = require('assert'); var util = require('util'); var $Sails = require('../../helpers/sails'); var $Router = require('../../helpers/router'); describe('Request hook', function (){ var sails = $Sails.load({ globals: false, loadHooks: [ 'moduleloader', 'userconfig', 'request' ] }); it('should expose `req.allParams()`', function (done) { var ROUTEADDRESS = '/req_allParams'; sails.router.bind(ROUTEADDRESS, function (req, res) { try { assert(typeof req.allParams === 'function', 'req.allParams() should be defined when request hook is enabled.'); } catch (e) { res.sendStatus(500); return done(e); } res.sendStatus(200); done(); }) .emit('router:request', {url: ROUTEADDRESS}); }); // NO LONGER SUPPORTED it('should expose `req.validate()`-- but calling it should always fail', function (done) { var ROUTEADDRESS = '/req_validate'; sails.router.bind(ROUTEADDRESS, function (req, res, next) { assert(typeof req.validate === 'function', 'req.validate() should be defined when request hook is enabled.'); try { req.validate('foo'); } catch (e) { return res.sendStatus(420); } return res.sendStatus(200); }); sails.request(ROUTEADDRESS, function (err){ try { assert(err && err.status === 420, new Error('Expecting error: it should no longer be supported')); } catch (e) { return done(e); } return done(); }); });//</it> }); ================================================ FILE: test/hooks/request/req.metadata.test.js ================================================ var assert = require('assert'); var mixinMetadata = require('../../../lib/hooks/request/metadata'); // var Sails = require('../../../lib/app'); describe('Request hook', function () { describe('metadata', function () { beforeEach(function() { this.req = { headers: {}, get: function(key) { return this.headers[key]; }, app: { data: { 'trust proxy': false }, get: function(key) { return this.data[key]; } }, _sails: { hooks: {}, config: {} } }; }); describe('without a reverse proxy', function() { it('should set req.port to 80 for http requests', function() { this.req.protocol = 'http'; this.req.host = 'example.org'; this.req.headers.Host = 'example.org'; mixinMetadata(this.req); assert.equal(this.req.port, 80); }); it('should set req.port to 443 for https requests', function() { this.req.protocol = 'https'; this.req.host = 'example.org'; this.req.headers.Host = 'example.org'; mixinMetadata(this.req); assert.equal(this.req.port, 443); }); it('should not add a port to baseUrl on port 80 or 443', function() { this.req.protocol = 'http'; this.req.host = 'example.org'; this.req.headers.Host = 'example.org'; mixinMetadata(this.req); assert.equal(this.req.baseUrl, 'http://example.org'); this.req.protocol = 'https'; this.req.host = 'example.org'; this.req.headers.Host = 'example.org'; mixinMetadata(this.req); assert.equal(this.req.baseUrl, 'https://example.org'); }); it('should add a port to baseUrl on a custom port', function() { this.req.protocol = 'http'; this.req.host = 'example.org'; this.req.headers.Host = 'example.org:1337'; mixinMetadata(this.req); assert.equal(this.req.baseUrl, 'http://example.org:1337'); }); it('should handle running as HTTP on port 443', function() { this.req.protocol = 'http'; this.req.host = 'example.org'; this.req.headers.Host = 'example.org:443'; mixinMetadata(this.req); assert.equal(this.req.port, 443); assert.equal(this.req.baseUrl, 'http://example.org:443'); }); }); describe('with a reverse proxy and app.enable("trust proxy")', function() { beforeEach(function() { // Fake a situation where trust proxy is enabled // (without having set sails.config.http accordingly) this.req.app.data['trust proxy'] = true; }); /* * In this case, req.protocol as set by Express is aware of the * X-Forwarded-Proto header field. The only complication: req.host * doesn't include a port number; req.header('host') might be wrong, too, * so we can't trust it either. */ it('should handle a simple HTTP case with X-Forwarded-Host', function() { this.req.protocol = 'http'; // we assume Express got this right this.req.host = 'server.local'; this.req.headers.Host = 'server.local'; this.req.headers['X-Forwarded-Host'] = 'example.org'; mixinMetadata(this.req); assert.equal(this.req.port, 80); assert.equal(this.req.baseUrl, 'http://example.org'); }); it('should handle when X-Forwarded-Host is not set', function() { this.req.protocol = 'http'; // we assume Express got this right this.req.host = 'server.local'; this.req.headers.Host = 'example.org:81'; mixinMetadata(this.req); assert.equal(this.req.port, 81); assert.equal(this.req.baseUrl, 'http://example.org:81'); }); it('should handle a simple HTTPS case with X-Forwarded-Host', function() { this.req.protocol = 'https'; // we assume Express got this right this.req.host = 'server.local'; this.req.headers.Host = 'server.local'; this.req.headers['X-Forwarded-Host'] = 'example.org'; mixinMetadata(this.req); assert.equal(this.req.port, 443); assert.equal(this.req.baseUrl, 'https://example.org'); }); it('should handle running on a nonstandard port', function() { this.req.protocol = 'https'; // we assume Express got this right this.req.host = 'server.local'; this.req.headers.Host = 'server.local:10000'; this.req.headers['X-Forwarded-Host'] = 'example.org'; mixinMetadata(this.req); assert.equal(this.req.port, 443); assert.equal(this.req.baseUrl, 'https://example.org'); }); it('should handle a list of x-forwarded-host values', function() { this.req.protocol = 'https'; // we assume Express got this right this.req.host = 'server2.local'; this.req.headers.Host = 'server2.local:10000'; this.req.headers['X-Forwarded-Host'] = 'example.org, server1.local'; mixinMetadata(this.req); assert.equal(this.req.port, 443); assert.equal(this.req.baseUrl, 'https://example.org'); }); it('should handle running on a weird port through a reverse proxy', function() { this.req.protocol = 'http'; this.req.host = 'server.local'; this.req.headers.Host = 'server.local:1000'; this.req.headers['X-Forwarded-Host'] = 'example.org:81'; mixinMetadata(this.req); assert.equal(this.req.port, 81); assert.equal(this.req.baseUrl, 'http://example.org:81'); }); it('should handle running as HTTP on port 443', function() { this.req.protocol = 'http'; this.req.host = 'server.local'; this.req.headers.Host = 'server.local:443'; this.req.headers['X-Forwarded-Host'] = 'example.org:443'; mixinMetadata(this.req); assert.equal(this.req.port, 443); assert.equal(this.req.baseUrl, 'http://example.org:443'); }); }); }); }); ================================================ FILE: test/hooks/request/req.options.sticky.test.js ================================================ /** * Module dependencies */ var assert = require('assert'); var $Sails = require('../../helpers/sails'); var $Router = require('../../helpers/router'); describe('Request hook', function (){ var sails = $Sails.load({ globals: false, log: { level: 'silent' }, loadHooks: [ 'moduleloader', 'userconfig', 'request', 'responses' ] }); describe('using req.options in multiple requests', function () { var opts; before(function(done) { var ROUTEADDRESS = '/route'; sails.router.bind(ROUTEADDRESS, function (req,res,next) { req.options[req.param('opt')] = true; return res.status(200).send(JSON.stringify(req.options)); }); sails.emit('router:request', { url: ROUTEADDRESS, query: { opt: 'foo' } }, {send: function(){}}); sails.emit('router:request', { url: ROUTEADDRESS, query: { opt: 'bar' } }, { send: function (data) { opts = JSON.parse(data); return done(); } }); }); it('req.options should not be sticky', function () { assert(!opts.foo, 'req.options.foo from first request carried over to second request!'); }); }); }); ================================================ FILE: test/hooks/views/ejs/index.i18n.ejs ================================================ <h1><%= __('hello') %></h1> ================================================ FILE: test/hooks/views/intialize.test.js ================================================ /** * Module dependencies */ var assert = require('assert'); var util = require('util'); var _ = require('@sailshq/lodash'); var Sails = require('../../../lib').constructor; describe('Views hook', function (){ it('should FAIL to initialize if the http hook is NOT enabled', function (done) { var sailsApp = new Sails(); sailsApp.load({ globals: false, log: { level: 'silent' }, loadHooks: [ 'moduleloader', 'userconfig', 'views' ] }, function (err) { try { if(!err) { throw new Error('Should have failed to failed to load Sails!'); } if (err.code !== 'E_HOOKINIT_DEP') { throw new Error('Should have failed w/ error code: `E_HOOKINIT_DEP`. But instead, code is: `'+err.code+'`'); } if (!_.isUndefined(err.status)) { throw new Error('Error should not have a `status` property! But instead, status is: `'+err.status+'`'); } } catch (e) { return done(e); } return done(); }); });//</it> it('should initialize as long as the http hook is included (even without the session hook)', function (done) { var sailsApp = new Sails(); sailsApp.load({ globals: false, loadHooks: [ 'moduleloader', 'userconfig', 'http', 'views' ] }, done); });//</it> it('should initialize w/ the session hook', function (done) { var sailsApp = new Sails(); sailsApp.load({ globals: false, loadHooks: [ 'moduleloader', 'userconfig', 'http', 'session', 'views' ] }, done); });//</it> }); ================================================ FILE: test/hooks/views/locales/en.json ================================================ { "hello": "Hello" } ================================================ FILE: test/hooks/views/locales/es.json ================================================ { "hello": "Hola" } ================================================ FILE: test/hooks/views/locales/eu.json ================================================ { "hello": "Kaixo" } ================================================ FILE: test/hooks/views/res.render.i18n.js ================================================ /** * Module dependencies */ var assert = require('assert'); var Sails = require('../../../lib').constructor; var path = require('path'); describe('sails.hooks.views.render() with i18n', function (){ var renderWithLocale = function(req, res){ var data = { locale: req.param('lang'), layout: false }; sails.hooks.views.render('index.i18n', data, function(err, html){ if (err) { return res.status(500).send(err); } return res.status(200).send(html); }); }; var renderWithoutLocale = function(req, res){ var data = { layout: false }; sails.hooks.views.render('index.i18n', data, function(err, html){ if (err) { return res.status(500).send(err); } return res.status(200).send(html); }); }; // Load a Sails app var app; before(function (done) { app = new Sails() .load({ globals: { sails: true, models: false, _: false, async: false, services: false } , loadHooks: [ 'moduleloader', 'userconfig', 'http', 'logger', 'i18n', 'views' ], routes: { 'get /render/:lang': renderWithLocale, 'get /render' : renderWithoutLocale }, i18n: { locales: ['en', 'es', 'eu'], defaultLocale: 'eu', localesDirectory: path.join(__dirname, './locales') }, paths: { views: path.join(__dirname, './ejs') } }, done); }); it('should show the message in basque', function (done) { app .request({url:'/render/eu', method: 'get'}, function(err, res){ assert.equal('<h1>Kaixo</h1>', res.body.trim()); done(); }); }); it('should show the message in spanish', function (done) { app .request({url:'/render/es', method: 'get'}, function(err, res){ assert.equal('<h1>Hola</h1>', res.body.trim()); done(); }); }); it('should show the message in english', function (done) { app .request({url:'/render/en', method: 'get'}, function(err, res){ assert.equal('<h1>Hello</h1>', res.body.trim()); done(); }); }); it('should show the message in basque by default if unknown lang', function (done) { app .request({url:'/render/de', method: 'get'}, function(err, res){ assert.equal('<h1>Kaixo</h1>', res.body.trim()); done(); }); }); it('should show the message in basque by default', function (done) { app .request({url:'/render', method: 'get'}, function(err, res){ assert.equal('<h1>Kaixo</h1>', res.body.trim()); done(); }); }); }); ================================================ FILE: test/hooks/views/res.view.test.js ================================================ /** * Module dependencies */ var assert = require('assert'); var util = require('util'); var Sails = require('../../../lib').constructor; var RouteFactory = require('root-require')('test/helpers/RouteFactory.helper'); describe('res.view()', function (){ // Get some mock routes to use in the tests below var mockRoutes = RouteFactory('res_view()'); // Load a Sails app var app; before(function (done) { app = new Sails() .load({ globals: false, loadHooks: [ 'moduleloader', 'userconfig', 'http', 'views' ] }, done); }); it('should exist when the views hook is enabled', function (done) { app .get(mockRoutes.next(), function (req, res) { assert.equal(typeof res.view, 'function', 'res.view() should be defined when request hook is enabled.'); return res.sendStatus(200); }) .request(mockRoutes.current, done); }); it.skip('should work w/ basic usage', function (done) { // NOTE: // In order to test this using `app.request` (i.e. w/o lifting), // we need to finish the `res.render()` functionality in core. // TODO: adapt this test accordingly app.get(mockRoutes.next(), function (req, res) { return res.view('foo'); }) .request(mockRoutes.current, function (err, res, body) { if (err) return done(err); assert.equal(res.statusCode, 200); assert.equal(body, VIEW_CONTENTS); }); }); }); ================================================ FILE: test/hooks/views/skipAssets.test.js ================================================ /** * Module dependencies */ var path = require('path'); var util = require('util'); var _ = require('@sailshq/lodash'); var tmp = require('tmp'); var request = require('@sailshq/request'); var MProcess = require('machinepack-process'); var MFilesystem = require('machinepack-fs'); var testSpawningSailsLiftChildProcessInCwd = require('../../helpers/test-spawning-sails-lift-child-process-in-cwd'); var appHelper = require('../../integration/helpers/appHelper'); tmp.setGracefulCleanup(); describe('skipAssets', function() { describe('Generate and lift a sails app which has a wildcard route WITHOUT using skipAssets', function() { // Track the location of the Sails CLI, as well as the current working directory // before we stop hopping about all over the place. var originalCwd = process.cwd(); var pathToSailsCLI = path.resolve(__dirname, '../../../bin/sails.js'); var pathToTestApp; before(function(done) { // Create a temp directory. var tmpDir = tmp.dirSync({gracefulCleanup: true, unsafeCleanup: true}); // Switch to the temp directory. process.chdir(tmpDir.name); pathToTestApp = path.resolve(tmpDir.name, 'testApp'); // Create a new Sails app. MProcess.executeCommand({ command: util.format('node %s new %s --fast --traditional --without=lodash,async', pathToSailsCLI, 'testApp'), }).exec(function(err) { if (err) {return done(err);} appHelper.linkDeps(pathToTestApp); return done(); }); }); // Change its routes file to use `skipAssets`. before(function (done){ MFilesystem.write({ destination: path.join(pathToTestApp, 'config/routes.js'), string: 'module.exports.routes = { \'get /*\': function(req, res) {return res.header(\'content-type\',\'text/plain\').send(\'some text\'); } };', force: true, }).exec(done); }); // And CD in. before(function (){ process.chdir(pathToTestApp); }); // Lift the app using `sails lift` in the CWD, // ensuring everything works as expected. describe('running `sails lift', function (){ testSpawningSailsLiftChildProcessInCwd({ pathToSailsCLI: pathToSailsCLI, liftCliArgs: ['--port=1331', '--hooks.grunt=false', '--hooks.pubsub=false', '--traditional'], httpRequestInstructions: { method: 'GET', uri: 'http://localhost:1331', }, fnWithAdditionalTests: function (){ it('should return a view when requesting `http://localhost:1331/js/dependencies/sails.io.js`', function (done){ request({ method: 'GET', uri: 'http://localhost:1331/js/dependencies/sails.io.js', }, function(err, response, body) { if (err) { return done(err); } try { if (!_.isString(response.headers['content-type'])) { return done(new Error('Expected a response content-type header when requesting an asset. `skipAssets` seems to be failing silently!')); } if (!response.headers['content-type'].match(/text\/plain/)) { return done(new Error('Expected text response content-type header when requesting an asset (but got `'+response.headers['content-type']+'`). `skipAssets` seems to be failing silently!')); } if (body !== 'some text') { return done(new Error('Expected text response an asset (but got `'+body+'`). `skipAssets` seems to be failing silently!')); } } catch (e) { return done(e); } return done(); }); }); } }); }); // And CD back to where we were before. after(function () { process.chdir(originalCwd); }); }); describe('Generate and lift a sails app which has a wildcard route using skipAssets', function() { // Track the location of the Sails CLI, as well as the current working directory // before we stop hopping about all over the place. var originalCwd = process.cwd(); var pathToSailsCLI = path.resolve(__dirname, '../../../bin/sails.js'); var pathToTestApp; before(function(done) { // Create a temp directory. var tmpDir = tmp.dirSync({gracefulCleanup: true, unsafeCleanup: true}); // Switch to the temp directory. process.chdir(tmpDir.name); pathToTestApp = path.resolve(tmpDir.name, 'testApp'); // Create a new Sails app. MProcess.executeCommand({ command: util.format('node %s new %s --fast --traditional --without=lodash,async', pathToSailsCLI, 'testApp'), }).exec(function(err) { if (err) {return done(err);} appHelper.linkDeps(pathToTestApp); return done(); }); }); // Change its routes file to use `skipAssets`. before(function (done){ MFilesystem.write({ destination: path.join(pathToTestApp, 'config/routes.js'), string: 'module.exports.routes = { \'get /*\': { view: \'pages/homepage\', skipAssets: true }, \'/js/dependencies/sails.io.js\': function(req,res){ return res.header(\'content-type\', \'application/javascript\').send(\'ok\'); } };', force: true, }).exec(done); }); // And CD in. before(function (){ process.chdir(pathToTestApp); }); // Lift the app using `sails lift` in the CWD, // ensuring everything works as expected. describe('running `sails lift', function (){ testSpawningSailsLiftChildProcessInCwd({ pathToSailsCLI: pathToSailsCLI, liftCliArgs: ['--port=1331', '--hooks.grunt=false', '--hooks.pubsub=false'], httpRequestInstructions: { method: 'GET', uri: 'http://localhost:1331', }, fnWithAdditionalTests: function (){ it('should return a JavaScript file when requesting `http://localhost:1331/js/dependencies/sails.io.js`', function (done){ request({ method: 'GET', uri: 'http://localhost:1331/js/dependencies/sails.io.js', }, function(err, response, body) { if (err) { return done(err); } try { if (!_.isString(response.headers['content-type'])) { return done(new Error('Expected a response content-type header when requesting an asset. `skipAssets` seems to be failing silently!')); } if (!response.headers['content-type'].match(/application\/javascript/)) { return done(new Error('Expected javascript response content-type header when requesting an asset (but got `'+response.headers['content-type']+'`). `skipAssets` seems to be failing silently!')); } if (body !== 'ok') { return done(new Error('Expected body of `sails.io.js` file to be returned, but instead got something else. `skipAssets` seems to be failing silently!')); } } catch (e) { return done(e); } return done(); }); }); } }); }); // And CD back to where we were before. after(function () { process.chdir(originalCwd); }); });//</Generate and lift a sails app which has a wildcard route using skipAssets> }); ================================================ FILE: test/init.js ================================================ // Initialization script that runs once before any tests. // // Set the "SAILS_NEW_LINK" env var so that the "sails new" generator // always uses symlinks (rather than doing npm install) regardless of // which NPM version is installed. // // Traditionally, "sails new" has sped up the process of generating a // new Sails app by symlinking required project dependencies from the // Sails module's node_modules folder. However, starting with NPM 3, // this shortcut will cause subsequent dependencies (installed by the // end-user) to fail on install, due to the new flattened node_modules // file structure. // // Starting with NPM 3, doing "sails new" currently causes // "npm install" to run, in order for the dependencies in the new Sails // app to be properly flattened. However, this takes a long time--too long // for tests. Since none of the tests install separate dependencies // in the fixture app, we can get away with using the old symlink strategy, // which can be activated with a --link option, or with an environment var. process.env.SAILS_NEW_LINK = true; ================================================ FILE: test/integration/README.md ================================================ # Integration tests The goal of these tests is to run Sails just like you or I would, and verify that no anomolies have been introduced in general. This is a great place to jump in if you're interested in contributing code to Sails core! ## How Can I Help? We could use your help writing more integration tests that test Sails under specific conditions. #### Integration tests for each core hook When writing an integration tests that verifies the behavior of a particular hook, the following assertions should be made: 1. Did the hook's default config make it into `sails.config`? 2. Did `sails.load` work as expected with the hook enabled and all the hooks it depends on enabled? 3. Did `sails.lift` work as expected with the hook enabled and all the hooks it depends on enabled? 4. Post-`sails.load`, is the process/application state correct? (i.e. did the hook do what it was supposed to do?) 5. Did the hook do what it was supposed to do after tearing down the server using `sails.lower()`? #### Integration tests for the hook loader We could really use more tests for the following cases: 1. If we `sails.load` using `loadHooks` to allow only specific hooks, or `hooks` to disable particular hooks, only the specified hooks should actually be loaded. 2. `sails.load()` should fail if a test tries to load a hook that depends on other hooks, but those other hooks are disabled. Note that `sails.load` should _fail with a relevant error message_ and _should not_ hang in this case. ================================================ FILE: test/integration/cert/sailstest-cert.pem ================================================ -----BEGIN CERTIFICATE----- MIIC8zCCAdugAwIBAgIUVJ+uHbbJ46i7KrMx4BR8RofnQU4wDQYJKoZIhvcNAQEL BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTI2MDEwOTAyNTAzM1oXDTM2MDEw NzAyNTAzM1owFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEF AAOCAQ8AMIIBCgKCAQEAwZ3B22dLj9gzxzDHhLIb6iVoQ2uwUCmr6eEYbHQlgkfM s0ssmvS2QZZ2246k1W2r/DvjkO2Xobmtzm5uvAPoCyc6gcwJgehpNJCvH5ON+Zid +3ow5VzhVZi023vYTfQ7K1fbdUEY9DF776hZSFk5WvUyj5L+Pu+mxcFLdNZ1PYEo YyPj5wnFswiua474euscKpr4OXykRuOeGikBipLsZtAeTi727WA922/q5vuCZO1g BxCZLoLDyVSgQqC2zepPI69w0Chq6LE4zsRHDUMZ3K+IVPu3Z8E8POeREQqyIUhi RZusplCqllIVTAuWNXwFvICji0r/8bIeX2JvR9XxowIDAQABoz0wOzAaBgNVHREE EzARgglsb2NhbGhvc3SHBH8AAAEwHQYDVR0OBBYEFF4uEUf8VhSWqMbsKgRnhyW4 c9RYMA0GCSqGSIb3DQEBCwUAA4IBAQBXDTR6QZiVaKnuChHKuHdsIbl27+YQKxlF uQksB0SXiTek6tK2euI/1MNkIxLxtK6Hjh9cOWmCp/4rE77Xrw6WaztFVLTj3zAY dUOaAtpi1mC6kQtfbF456rBvwOoQeR96HmrTk9epILSgV5+eqfBJIxyrSsoFExEK l3LMMxkD0Y/S7TT7zmt29m+Pj26ArZIIW7i3NdaqJNlOb08761wMghvGrGjGBCQq seKH4BD5K03qr1D69h61mg0JIbSgZqWhRL4pvDRKYG0nprIu7fS9+xv25UAhCp0N 9f8vvUS+yduuUhp/1AKhstoPX4vPUHLPYn0kLjZShEiyPE9ZRXOB -----END CERTIFICATE----- ================================================ FILE: test/integration/cert/sailstest-key.pem ================================================ -----BEGIN PRIVATE KEY----- MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDBncHbZ0uP2DPH MMeEshvqJWhDa7BQKavp4RhsdCWCR8yzSyya9LZBlnbbjqTVbav8O+OQ7Zehua3O bm68A+gLJzqBzAmB6Gk0kK8fk435mJ37ejDlXOFVmLTbe9hN9DsrV9t1QRj0MXvv qFlIWTla9TKPkv4+76bFwUt01nU9gShjI+PnCcWzCK5rjvh66xwqmvg5fKRG454a KQGKkuxm0B5OLvbtYD3bb+rm+4Jk7WAHEJkugsPJVKBCoLbN6k8jr3DQKGrosTjO xEcNQxncr4hU+7dnwTw855ERCrIhSGJFm6ymUKqWUhVMC5Y1fAW8gKOLSv/xsh5f Ym9H1fGjAgMBAAECggEAEXxmT6LELj+M0MaIN/6MV6WW0jZdlaY0NbEm++4Zaesc gtM206yaJGsRKcJEnL0eggjR0Pr3bZtPv4vPSgFIUAmNvSWb2wGnm4aEA/2j6t2f sJO+hFhsRi2LPlWwmVaFwxclZZePFPuLGxlz+f0k5iDftuiQ0a178FkFFlsJlGSq kC9pedSyCqoWuFpfVvpoe4rn3HN5e3BnrqjX2gk2+fTcVP0QwLoS+8YsmTs/sMgG ueQoWhBlwKqeEEaRbhhACeCjSfplDVkrQUEeGiHq917IYvGgWDryDUxNWGsTccSD 8IuXrnJKvmQWW8GrrqtyUQXtz/Eu18fFcDi/7qGi0QKBgQD108gf7qXAgR598ywv 6t43+6MHsBYf9ZNbSMsOTufVUSF7yYxm0FW82NmkLizY1KWymONPjOz7WdmrqyjF 3w5Ujf+wNq6t/TjCWD7EYpwvDk8qhp2r4vpJG6ie/B1oBMazaz23B2/i57Ftbr7w 5NopKB4bEGcZmo3vw1bgnuwvDQKBgQDJoN4xkasdZFLeV263jKuY/w3v+e+ypO7l Pl+6MFv/v0WN+KVKVLssy93MiOQBmyg6haIXlnIQkYMkSTT7goA6FydfHhC2TgGh zOxFOrKZgYCkmHUe1SKvBftXQqJ8nErEACeo48475Y5n0tk0V+JnI4Vo9qtGXr6S 2ciYtCj3bwKBgQCEH8e4IfREeyGAYGqndnzpaf496454ruz8ayt4DUDdjjWI6tLj j6YFUifn7kl8YQ6N506FOyFEFw6/DcdkUnbJS2jZtQo9yZPwIK3br4RyZiZ2nNOx xtTu5kbC7I6Bkc+aL1GERiMEubLLNnK51sbKyB0mPrKrOD6BV2QiZkhbIQKBgAPN eONOb/+56KYw1/G2QXY9OTIRcKfZ3HeOWZfVWabVIKawzc09E9qgbapx2nr9RiD0 bD4tpDETzXlduBYWO/zceu2cT4xgpD888ifMF5o1iwuPpIXUVzcd0cOvigj3maFg r17MDROsHKdwnpASKD7xuI5mOIy3NLjoSpQ2sZ8ZAoGAYV+248Oxt2WQsy/zeXoA tFBNonGL39TtqmiD7553Q/VaR1BpwdClJE51XN+xEJVhBfhmNh1oREZFHWQC1smF JnmabcjHjP8VrwvRy/yrtUoefsvMGZnDFd+hnBJRdL9JoyHDtYb+IbpjM+ZtZVbz WgWiitQeWoCY86qONNPmWWs= -----END PRIVATE KEY----- ================================================ FILE: test/integration/fixtures/hooks/installable/add-policy/index.js ================================================ module.exports = function(sails) { /** * List of hooks that required for adminpanel to work */ var requiredHooks = [ 'blueprints', 'controllers', 'http', 'policies', ]; function forbiddenPolicy(req, res, next) { return res.forbidden(); } return { defaults: { __configKey__: {} }, initialize: function(cb) { try { var eventsToWaitFor = []; try { /** * Check hooks availability */ _.forEach(requiredHooks, function (hook) { if (!sails.hooks[hook]) { throw new Error('Cannot use `add-policy` hook without the `' + hook + '` hook.'); } //if (hook === 'policies') { // eventsToWaitFor.push('hook:' + hook + ':bound'); //} else { eventsToWaitFor.push('hook:' + hook + ':loaded'); //} }); } catch(err) { if (err) { return cb(err); } } if (!sails.hooks.policies.middleware) { sails.hooks.policies.middleware = {}; } sails.hooks.policies.middleware.forbidden = forbiddenPolicy; cb(); } catch(err) { return cb(err); } } }; }; ================================================ FILE: test/integration/fixtures/hooks/installable/add-policy/package.json ================================================ { "name": "sails-hook-add-policy", "main": "index.js", "sails": { "isHook": true } } ================================================ FILE: test/integration/fixtures/hooks/installable/async/index.js.txt ================================================ module.exports = function(sails) { return { initialize: async function(cb) { var dumb = function() { return new Promise(function(resolve, reject) { setTimeout(function() { if (sails.config.custom && sails.config.custom.reject) { return reject('foo'); } return resolve('foo') }, 100); }); }; this.val = await dumb(); return cb(); } }; }; ================================================ FILE: test/integration/fixtures/hooks/installable/shout/index.js ================================================ module.exports = function(sails) { var phrase; return { defaults: { __configKey__: { phrase: 'make it rain' } }, initialize: function(cb) { phrase = sails.config[this.configKey].phrase; this.isShoutyHook = true; cb(); }, routes: { before: { 'GET /shout': function(req, res, next) { res.send(phrase); } } } }; }; ================================================ FILE: test/integration/fixtures/hooks/installable/shout/package.json ================================================ { "name": "sails-hook-shout", "main": "index.js", "sails": { "isHook": true } } ================================================ FILE: test/integration/fixtures/sampleapp/api/controllers/EmptyController.js ================================================ module.exports = {}; ================================================ FILE: test/integration/fixtures/sampleapp/api/controllers/PetController.js ================================================ module.exports = { watch: function(req, res) { req._sails.models.pet.watch(req); res.sendStatus(200); } }; ================================================ FILE: test/integration/fixtures/sampleapp/api/controllers/QuizController.js ================================================ module.exports = {}; ================================================ FILE: test/integration/fixtures/sampleapp/api/controllers/TestController.js ================================================ module.exports = { verb: function(req, res) { res.send(req.method.toLowerCase()); }, dynamic: function(req, res) { res.json(req.allParams()); }, index: function(req, res) { res.send('index'); }, find: function(req, res) { res.send('find'); }, findOne: function(req, res) { res.send('findOne'); }, create: function(req, res) { res.send('create'); }, update: function(req, res) { res.send('update'); }, destroy: function(req, res) { res.send('destroy'); }, CapitalLetters: function(req, res) { res.send('CapitalLetters'); } }; ================================================ FILE: test/integration/fixtures/sampleapp/api/controllers/UserController.js ================================================ module.exports = { watch: function(req, res) { req._sails.models.user.watch(req); res.sendStatus(200); }, message: function(req, res) { req._sails.models.user.findOne({ user_id: 1 }, function(err, user) { if (err) { return res.status(500).json({ error: err }); } else if (!user) { return res.status(404).json({ error: 'Expected specified user (with user_id=1) to exist...' }); } else { req._sails.models.user.publish([user.user_id], { greeting: 'hello' }, req); return res.sendStatus(200); } }); }, subscribe: function(req, res) { req._sails.models.user.subscribe(req, [req.param('id')]); res.sendStatus(200); } }; ================================================ FILE: test/integration/fixtures/sampleapp/api/controllers/UserProfileController.js ================================================ module.exports = {}; ================================================ FILE: test/integration/fixtures/sampleapp/api/controllers/ViewTestController.js ================================================ module.exports = { index: function(req, res, next) { res.view(); }, create: function(req, res, next) { res.view(); }, viewOptions: function(req, res, next) { res.view(); }, viewOptionsOverride: function(req, res, next) { res.view('viewtest/viewOptions', {foo:'!baz!'}); }, csrf: function(req, res, next) { res.view(); } }; ================================================ FILE: test/integration/fixtures/sampleapp/api/models/Empty.js ================================================ module.exports = { attributes: { foo: 'string' } }; ================================================ FILE: test/integration/fixtures/sampleapp/api/models/Pet.js ================================================ module.exports = { primaryKey: 'pet_id', attributes: { id: false, pet_id: { type: 'integer', autoIncrement: true }, name: 'string', owner: { model: 'user' }, bestFriend: { model: 'user' }, parents: { collection: 'pet', via: 'children' }, children: { collection: 'pet', via: 'parents' }, bestFurryFriend: { model: 'pet' }, vets: { collection: 'user', via: 'patients' }, isPet: { type: 'boolean', defaultsTo: true } } }; ================================================ FILE: test/integration/fixtures/sampleapp/api/models/Quiz.js ================================================ module.exports = { attributes: { bar: 'string' } }; ================================================ FILE: test/integration/fixtures/sampleapp/api/models/Test.js ================================================ module.exports = {}; ================================================ FILE: test/integration/fixtures/sampleapp/api/models/User.js ================================================ module.exports = { primaryKey: 'user_id', attributes: { id: false, user_id: { type: 'integer', autoIncrement: true }, name: 'string', pets: { collection: 'pet', via: 'owner' }, patients: { collection: 'pet', via: 'vets' }, profile: { model: 'userprofile' } } }; ================================================ FILE: test/integration/fixtures/sampleapp/api/models/UserProfile.js ================================================ module.exports = { attributes: { user: { model: 'user' }, zodiac: 'string' } }; ================================================ FILE: test/integration/fixtures/sampleapp/api/policies/error_policy.js ================================================ /** * Error Policy Fixture * * Sends an Error Object to the callback */ module.exports = function(req, res, next) { return res.serverError('Test Error'); }; ================================================ FILE: test/integration/fixtures/sampleapp/api/policies/fake_auth.js ================================================ /** * Fake Auth Policy Fixture * * Fakes an Authentication */ module.exports = function(req, res, next) { req.session.authenticated = true; next(); }; ================================================ FILE: test/integration/fixtures/sampleapp/api/services/TestService.js ================================================ module.exports = {}; ================================================ FILE: test/integration/fixtures/sampleapp/config/local.js ================================================ module.exports = { log: { level: 'silent' }, views: { locals: { foo: '!bar!' } }, models: { migrate: 'alter', schema: true }, globals: false }; ================================================ FILE: test/integration/fixtures/sampleapp/views/app/index.ejs ================================================ App index file ================================================ FILE: test/integration/fixtures/sampleapp/views/app/user/homepage.ejs ================================================ I'm deeply nested! ================================================ FILE: test/integration/fixtures/sampleapp/views/pages/homepage.ejs ================================================ <!-- Default home page --> ================================================ FILE: test/integration/fixtures/sampleapp/views/viewtest/create.ejs ================================================ createView ================================================ FILE: test/integration/fixtures/sampleapp/views/viewtest/csrf.ejs ================================================ csrf=<%= _.isUndefined(_csrf) ? 'no_token' : _csrf %> ================================================ FILE: test/integration/fixtures/sampleapp/views/viewtest/index.ejs ================================================ indexView ================================================ FILE: test/integration/fixtures/sampleapp/views/viewtest/viewOptions.ejs ================================================ <%=foo%> ================================================ FILE: test/integration/fixtures/users.js ================================================ var async = require('async'); module.exports = function(sails, cb) { var users = []; for (var userId = 1; userId <= 11; userId++) { var user = { name: 'user_'+userId, pets: [], profile: {} }; for (var petId = 1; petId <= 11; petId++) { user.pets.push({name: 'user_'+userId+'_pet_'+petId}); } user.profile.zodiac = 'user_'+userId+'_zodiac'; users.push(user); } async.forEach(users, function create(user, cb) {sails.models.user.create(user).exec(cb);}, cb); }; ================================================ FILE: test/integration/generate.test.js ================================================ describe('API and adapter generators', function() { var assert = require('assert'); var fs = require('fs-extra'); var exec = require('child_process').exec; var path = require('path'); // Make existsSync not crash on older versions of Node fs.existsSync = fs.existsSync || require('path').existsSync; function capitalize(string) { return string.charAt(0).toUpperCase() + string.slice(1); } var sailsBin = path.resolve('./bin/sails.js'); var appName = 'testApp'; this.slow(1000); before(function(done) { if (fs.existsSync(appName)) { fs.removeSync(appName); } exec('node ' + sailsBin + ' new ' + appName + ' --fast --traditional --without=lodash,async', function(err) { if (err) { return done(new Error(err)); } // Move into app directory and update sailsBin relative path process.chdir(appName); sailsBin = path.resolve('..', sailsBin); done(); }); }); //</before> after(function(done) { // return to test directory process.chdir('../'); if (fs.existsSync(appName)) { fs.removeSync(appName); } done(); }); describe('sails generate model <modelname>', function() { var modelName = 'user'; it('should throw an error if no model name is specified', function(done) { exec('node ' + sailsBin + ' generate model', function(err) { assert.equal(err.code, 1); done(); }); }); it('should create a model file in models folder', function(done) { exec('node ' + sailsBin + ' generate model ' + modelName, function(err) { if (err) done(new Error(err)); assert.doesNotThrow(function() { fs.readFileSync('./api/models/' + capitalize(modelName) + '.js', 'utf8'); }); done(); }); }); it('should throw an error if a model with the same name exists', function(done) { exec('node ' + sailsBin + ' generate model ' + modelName, function(err) { assert.equal(err.code, 1); done(); }); }); }); describe('sails generate controller <controllerName>', function() { var controllerName = 'user'; it('should throw an error if no controller name is specified', function(done) { exec('node ' + sailsBin + ' generate controller', function(err) { assert.equal(err.code, 1); done(); }); }); it('should create a controller file in controllers folder', function(done) { exec('node ' + sailsBin + ' generate controller ' + controllerName, function(err) { if (err) { return done(new Error(err)); } assert.doesNotThrow(function() { fs.readFileSync('./api/controllers/' + capitalize(controllerName) + 'Controller.js', 'utf8'); }); done(); }); }); it('should throw an error if a controller with the same name exists', function(done) { exec('node ' + sailsBin + ' generate controller ' + controllerName, function(err) { assert.equal(err.code, 1); done(); }); }); }); describe('sails generate adapter <modelname>', function() { var adapterName = 'mongo'; it('should throw an error if no adapter name is specified', function(done) { exec('node ' + sailsBin + ' generate adapter', function(err) { assert.equal(err.code, 1); done(); }); }); it('should create a adapter file in adapters folder', function(done) { exec('node ' + sailsBin + ' generate adapter ' + adapterName, function(err) { if (err) { return done(err); } assert.doesNotThrow(function() { fs.readFileSync('./api/adapters/' + adapterName + '/index.js', 'utf8'); }); done(); }); }); it('should throw an error if an adapter with the same name exists', function(done) { exec('node ' + sailsBin + ' generate adapter ' + adapterName, function(err) { assert.equal(err.code, 1); done(); }); }); }); describe('sails generate', function() { var modelName = 'post'; it('should display usage if no generator type is specified', function(done) { exec('node ' + sailsBin + ' generate', function(err, msg) { if (err) { return done(err); } assert.notEqual(msg.indexOf('Usage'), -1); done(); }); }); }); describe('sails generate api <apiname>', function() { var apiName = 'foo'; it('should display usage if no api name is specified', function(done) { exec('node ' + sailsBin + ' generate api', function(err, dumb, response) { assert.notEqual(response.indexOf('Usage'), -1); done(); }); }); it('should create a controller and a model file', function(done) { exec('node ' + sailsBin + ' generate api ' + apiName, function(err) { if (err) { return done(err); } assert.doesNotThrow(function() { fs.readFileSync('./api/models/' + capitalize(apiName) + '.js', 'utf8'); }); assert.doesNotThrow(function() { fs.readFileSync('./api/controllers/' + capitalize(apiName) + 'Controller.js', 'utf8'); }); done(); }); }); it('should throw an error if a controller file and model file with the same name exists', function(done) { exec('node ' + sailsBin + ' generate api ' + apiName, function(err) { assert.equal(err.code, 1); done(); }); }); }); }); ================================================ FILE: test/integration/globals.test.js ================================================ /** * Test dependencies */ var assert = require('assert'); var tmp = require('tmp'); var path = require('path'); var util = require('util'); var _ = require('@sailshq/lodash'); var appHelper = require('./helpers/appHelper'); var Filesystem = require('machinepack-fs'); var MProcess = require('machinepack-process'); var pathToSailsCLI = path.resolve(__dirname, '../../bin/sails.js'); describe('globals :: ', function() { var curDir; describe('with default settings in an app lifted programmatically with no configuration', function() { var result; before(function(done) { // Cache the current working directory. curDir = process.cwd(); // Create a temp directory. var tmpDir = tmp.dirSync({gracefulCleanup: true, unsafeCleanup: true}); // Switch to the temp directory. process.chdir(tmpDir.name); Filesystem.ensureDir({path: 'node_modules'}).exec(function(err) { if (err) {return done(err);} // Set up some app files (a model, a service and config/models.js with `migrate: 'alter'`) setupAppFiles(); // Symlink Sails to the new app appHelper.linkSails(tmpDir.name); MProcess.executeCommand({ command: util.format('node expose_globals.js'), }).exec(function(err, output) { if (err) {return done(err);} if (output.stderr) {return done(output.stderr);} try { result = JSON.parse(output.stdout); } catch (e) { return done(e); } return done(); }); }); }); it('should not expose `async` as a global', function() { assert.equal(result.async, false); }); it('should not expose `_` as a global', function() { assert.equal(result._, false); }); it('should not expose services as globals', function() { assert.equal(result.services, false); }); it('should not expose models as globals', function() { assert.equal(result.models, false); }); it('should not expose sails as a global', function() { assert.equal(result.sails, false); }); after(function() { process.chdir(curDir); }); }); describe('with default settings in an app generated with `sails new`', function() { var result; before(function(done) { // Cache the current working directory. curDir = process.cwd(); // Create a temp directory. var tmpDir = tmp.dirSync({gracefulCleanup: true, unsafeCleanup: true}); // Switch to the temp directory. process.chdir(tmpDir.name); // Get the full path to the directory containing the app var pathToTestApp = path.resolve(tmpDir.name, 'testApp'); // Create a new Sails app w/out npm install. MProcess.executeCommand({ command: util.format('node %s new %s --fast --traditional', pathToSailsCLI, 'testApp'), }).exec(function(err) { if (err) {return done(err);} // Switch to the app directory. process.chdir(pathToTestApp); // Set up some app files (a model, a service and config/models.js with `migrate: 'alter'`) setupAppFiles(); // Symlink dependencies to the new app appHelper.linkDeps(pathToTestApp); // Symlink Sails to the new app appHelper.linkSails(pathToTestApp); // Symlink `lodash` to the new app appHelper.linkLodash(pathToTestApp); // Symlink `async` to the new app appHelper.linkAsync(pathToTestApp); MProcess.executeCommand({ command: util.format('node expose_globals.js'), }).exec(function(err, output) { if (err) {return done(err);} if (output.stderr) {return done(output.stderr);} try { result = JSON.parse(output.stdout); } catch (e) { return done(new Error('Error parsing child process output as JSON. Error details: ' +e.stack+'\nAnd here\'s the raw output that could not be parsed as JSON:\n'+output.stdout)); } return done(); }); }); }); it('should NO LONGER expose `async` as a global', function() { assert.equal(result.async, false); }); it('should expose `_` as a global', function() { assert(_.isArray(result._)); assert(_.contains(result._, 'contains')); }); it('should expose services as globals', function() { assert(_.isArray(result.services)); assert(_.contains(result.services, 'Foo')); }); it('should expose models as globals', function() { assert(_.isArray(result.models)); assert(_.contains(result.models, 'User')); }); it('should expose sails as a global', function() { assert.equal(result.sails, true); }); after(function() { process.chdir(curDir); }); }); describe('with custom async/lodash in an app generated with `sails new`', function() { var result; before(function(done) { // Cache the current working directory. curDir = process.cwd(); // Create a temp directory. var tmpDir = tmp.dirSync({gracefulCleanup: true, unsafeCleanup: true}); // Switch to the temp directory. process.chdir(tmpDir.name); // Get the full path to the directory containing the app var pathToTestApp = path.resolve(tmpDir.name, 'testApp'); // Create a new Sails app w/out npm install. MProcess.executeCommand({ command: util.format('node %s new %s --fast --traditional', pathToSailsCLI, 'testApp'), }).exec(function(err) { if (err) {return done(err);} // Switch to the app directory. process.chdir(pathToTestApp); // Set up some app files (a model, a service and config/models.js with `migrate: 'alter'`) setupAppFiles(); // Symlink dependencies to the new app appHelper.linkDeps(pathToTestApp); // Symlink Sails to the new app appHelper.linkSails(pathToTestApp); Filesystem.writeSync({ force: true, destination: 'node_modules/@sailshq/lodash/package.json', string: '{"name": "lodash", "version": "0.0.0"}' }).execSync(); Filesystem.writeSync({ force: true, destination: 'node_modules/@sailshq/lodash/index.js', string: 'module.exports = {"foo": "bar"}' }).execSync(); Filesystem.writeSync({ force: true, destination: 'node_modules/async/package.json', string: '{"name": "async", "version": "0.0.0"}' }).execSync(); Filesystem.writeSync({ force: true, destination: 'node_modules/async/index.js', string: 'module.exports = {"owl": "hoot"}' }).execSync(); MProcess.executeCommand({ command: util.format('node expose_globals.js'), }).exec(function(err, output) { if (err) {return done(err);} if (output.stderr) {return done(output.stderr);} try { result = JSON.parse(output.stdout); } catch (e) { return done(e); } return done(); }); }); }); it('should NO LONGER expose `async` as a global', function() { assert.equal(result.async, false); }); it('should expose `_` as a global, using the custom lodash', function() { assert(_.isArray(result._)); assert(_.contains(result._, 'foo')); }); it('should expose services as globals', function() { assert(_.isArray(result.services)); assert(_.contains(result.services, 'Foo')); }); it('should expose models as globals', function() { assert(_.isArray(result.models)); assert(_.contains(result.models, 'User')); }); it('should expose sails as a global', function() { assert.equal(result.sails, true); }); after(function() { process.chdir(curDir); }); }); describe('with `_`, `async`, `models`, `sails` and `services` set to `false`', function() { var result; before(function(done) { // Cache the current working directory. curDir = process.cwd(); // Create a temp directory. var tmpDir = tmp.dirSync({gracefulCleanup: true, unsafeCleanup: true}); // Switch to the temp directory. process.chdir(tmpDir.name); // Get the full path to the directory containing the app var pathToTestApp = path.resolve(tmpDir.name, 'testApp'); // Create a new Sails app w/out npm install. MProcess.executeCommand({ command: util.format('node %s new %s --fast --traditional', pathToSailsCLI, 'testApp'), }).exec(function(err) { if (err) {return done(err);} // Switch to the app directory. process.chdir(pathToTestApp); // Set up some app files (a model, a service and config/models.js with `migrate: 'alter'`) setupAppFiles(); // Symlink dependencies to the new app appHelper.linkDeps(pathToTestApp); // Symlink Sails to the new app appHelper.linkSails(pathToTestApp); Filesystem.writeSync({ force: true, destination: 'config/globals.js', string: 'module.exports.globals = { _: false, async: false, models: false, sails: false, services: false}' }).execSync(); Filesystem.writeSync({ force: true, destination: 'node_modules/lodash/package.json', string: '{"name": "lodash", "version": "0.0.0"}' }).execSync(); Filesystem.writeSync({ force: true, destination: 'node_modules/lodash/index.js', string: 'module.exports = {"foo": "bar"}' }).execSync(); Filesystem.writeSync({ force: true, destination: 'node_modules/async/package.json', string: '{"name": "async", "version": "0.0.0"}' }).execSync(); Filesystem.writeSync({ force: true, destination: 'node_modules/async/index.js', string: 'module.exports = {"owl": "hoot"}' }).execSync(); MProcess.executeCommand({ command: util.format('node expose_globals.js'), }).exec(function(err, output) { if (err) {return done(err);} if (output.stderr) {return done(output.stderr);} try { result = JSON.parse(output.stdout); } catch (e) { return done(e); } return done(); }); }); }); it('should not expose `async` as a global', function() { assert.equal(result.async, false); }); it('should not expose `_` as a global', function() { assert.equal(result._, false); }); it('should not expose services as globals', function() { assert.equal(result.services, false); }); it('should not expose models as globals', function() { assert.equal(result.models, false); }); it('should not expose sails as a global', function() { assert.equal(result.sails, false); }); after(function() { process.chdir(curDir); }); }); describe('with `_` set to `true`', function() { before(function(done) { // Cache the current working directory. curDir = process.cwd(); // Create a temp directory. var tmpDir = tmp.dirSync({gracefulCleanup: true, unsafeCleanup: true}); // Switch to the temp directory. process.chdir(tmpDir.name); Filesystem.ensureDir({path: 'node_modules'}).exec(function(err) { if (err) {return done(err);} // Set up some app files (a model, a service and config/models.js with `migrate: 'alter'`) setupAppFiles(); // Symlink Sails to the new app appHelper.linkSails(tmpDir.name); Filesystem.writeSync({ force: true, destination: 'config/globals.js', string: 'module.exports.globals = { _: true }' }).execSync(); return done(); }); }); it('should fail to lift', function(done) { MProcess.executeCommand({ command: util.format('node expose_globals.js'), }).exec(function(err, output) { if (output.stderr) { if (output.stderr.match('E_BAD_GLOBAL_CONFIG')) { return done(); } return done(err); } return done(new Error('Sails should have failed to lift!')); }); }); after(function() { process.chdir(curDir); }); }); describe('with `async` set to `true`', function() { before(function(done) { // Cache the current working directory. curDir = process.cwd(); // Create a temp directory. var tmpDir = tmp.dirSync({gracefulCleanup: true, unsafeCleanup: true}); // Switch to the temp directory. process.chdir(tmpDir.name); Filesystem.ensureDir({path: 'node_modules'}).exec(function(err) { if (err) {return done(err);} // Set up some app files (a model, a service and config/models.js with `migrate: 'alter'`) setupAppFiles(); // Symlink Sails to the new app appHelper.linkSails(tmpDir.name); Filesystem.writeSync({ force: true, destination: 'config/globals.js', string: 'module.exports.globals = { async: true }' }).execSync(); return done(); }); }); it('should fail to lift', function(done) { MProcess.executeCommand({ command: util.format('node expose_globals.js'), }).exec(function(err, output) { if (output.stderr) { if (output.stderr.match('E_BAD_GLOBAL_CONFIG')) { return done(); } return done(err); } return done(new Error('Sails should have failed to lift!')); }); }); after(function() { process.chdir(curDir); }); }); describe('with `models` set to `undefined`', function() { before(function(done) { // Cache the current working directory. curDir = process.cwd(); // Create a temp directory. var tmpDir = tmp.dirSync({gracefulCleanup: true, unsafeCleanup: true}); // Switch to the temp directory. process.chdir(tmpDir.name); Filesystem.ensureDir({path: 'node_modules'}).exec(function(err) { if (err) {return done(err);} // Set up some app files (a model, a service and config/models.js with `migrate: 'alter'`) setupAppFiles(); // Symlink Sails to the new app appHelper.linkSails(tmpDir.name); Filesystem.writeSync({ force: true, destination: 'config/globals.js', string: 'module.exports.globals = { async: false, _: false, sails: false }' }).execSync(); return done(); }); }); it('should fail to lift', function(done) { MProcess.executeCommand({ command: util.format('node expose_globals.js'), }).exec(function(err, output) { if (output.stderr) { if (output.stderr.match('E_BAD_GLOBAL_CONFIG')) { return done(); } return done(err); } return done(new Error('Sails should have failed to lift!')); }); }); after(function() { process.chdir(curDir); }); }); describe('with `sails` set to `undefined`', function() { before(function(done) { // Cache the current working directory. curDir = process.cwd(); // Create a temp directory. var tmpDir = tmp.dirSync({gracefulCleanup: true, unsafeCleanup: true}); // Switch to the temp directory. process.chdir(tmpDir.name); Filesystem.ensureDir({path: 'node_modules'}).exec(function(err) { if (err) {return done(err);} // Set up some app files (a model, a service and config/models.js with `migrate: 'alter'`) setupAppFiles(); // Symlink Sails to the new app appHelper.linkSails(tmpDir.name); Filesystem.writeSync({ force: true, destination: 'config/globals.js', string: 'module.exports.globals = { async: false, _: false, models: false }' }).execSync(); return done(); }); }); it('should fail to lift', function(done) { MProcess.executeCommand({ command: util.format('node expose_globals.js'), }).exec(function(err, output) { if (output.stderr) { if (output.stderr.match('E_BAD_GLOBAL_CONFIG')) { return done(); } return done(err); } return done(new Error('Sails should have failed to lift!')); }); }); after(function() { process.chdir(curDir); }); }); describe('with `sails.config.globals` set to `true`', function() { before(function(done) { // Cache the current working directory. curDir = process.cwd(); // Create a temp directory. var tmpDir = tmp.dirSync({gracefulCleanup: true, unsafeCleanup: true}); // Switch to the temp directory. process.chdir(tmpDir.name); Filesystem.ensureDir({path: 'node_modules'}).exec(function(err) { if (err) {return done(err);} // Set up some app files (a model, a service and config/models.js with `migrate: 'alter'`) setupAppFiles(); // Symlink Sails to the new app appHelper.linkSails(tmpDir.name); Filesystem.writeSync({ force: true, destination: 'config/globals.js', string: 'module.exports.globals = true' }).execSync(); return done(); }); }); it('should fail to lift', function(done) { MProcess.executeCommand({ command: util.format('node expose_globals.js'), }).exec(function(err, output) { if (err) { return done(err); } if (output.stderr) { if (output.stderr.match('E_BAD_GLOBAL_CONFIG')) { return done(); } return done(new Error(output.stderr)); } return done(new Error('Sails should have failed to lift!')); }); }); after(function() { process.chdir(curDir); }); }); }); function setupAppFiles() { Filesystem.writeSync({ force: true, destination: 'api/models/User.js', string: 'module.exports = { attributes: { name: \'string\' } };' }).execSync(); Filesystem.writeSync({ force: true, destination: 'api/services/Foo.js', string: 'module.exports = function TheFooService() {};' }).execSync(); Filesystem.writeSync({ force: true, destination: 'config/models.js', string: 'module.exports.models = {migrate: \'alter\', attributes: {id: { type: \'number\', autoIncrement: true}}};' }).execSync(); Filesystem.writeSync({ force: true, destination: 'expose_globals.js', string: '(' + (function logGlobalVarsIIFERiddle() { /* eslint-disable no-undef */ var Sails = require('sails'); Sails.load({log: {level: 'silent'}}, function(err, sailsApp) { if (err) {console.error(err); return;} console.log(JSON.stringify({ async: typeof async !== 'undefined' && Object.keys(async), _: typeof _ !== 'undefined' && Object.keys(_), sails: typeof sails !== 'undefined' && sails.constructor.name === 'Sails', models: typeof sailsApp.models !== 'undefined' && Object.keys(sailsApp.models).reduce(function(memo, key) {if (global[sailsApp.models[key].globalId]){memo.push(sailsApp.models[key].globalId);}return memo;}, []), services: typeof sailsApp.services !== 'undefined' && Object.keys(sailsApp.services).reduce(function(memo, key) {if (global[sailsApp.services[key].globalId]){memo.push(sailsApp.services[key].globalId);}return memo;}, []) })); sailsApp.lower(); }); /* eslint-enable no-undef */ }).toString() + ')();' }).execSync(); } ================================================ FILE: test/integration/helpers/appHelper.js ================================================ /** * Module dependencies */ var path = require('path'); var child_process = require('child_process'); var exec = child_process.exec; var fs = require('fs-extra'); var _ = require('@sailshq/lodash'); var SocketIOClient = require('socket.io-client'); delete require.cache[require.resolve('socket.io-client')]; var SailsIOClient = require('sails.io.js'); var Sails = require('../../../lib/app'); // Build a Sails socket client instance. // // (Of course, this runs as soon as this file is first required. // But it's OK because we don't actually connect except in the // functions below.) var io = SailsIOClient(SocketIOClient); io.sails.environment = 'production'; io.sails.autoConnect = false; // Make existsSync not crash on older versions of Node fs.existsSync = fs.existsSync || path.existsSync; // ^ probably not necessary anymore, this is only relevant for pre-Node-0.8 // (or maybe it was Node 0.8, can't remember). Anyways, it was back when // `existsSync()` lived in the `path` lib. module.exports = { /** * Spin up a child process and use the `sails` CLI to create a namespaced * test app. If no appName is given use: 'testApp'. * * It copies all the files in the fixtures folder into their * respective place in the test app so you don't need to worry * about setting up the fixtures. */ build: function(appName, done) { // `appName` is optional. if (_.isFunction(appName)) { done = appName; appName = 'testApp'; } // But `done` callback is required. if (!_.isFunction(done)) { throw new Error('When using the appHelper\'s `build()` method, a callback argument is required'); } var pathToLocalSailsCLI = path.resolve('./bin/sails.js'); // Cleanup old test fixtures if (fs.existsSync(appName)) { fs.removeSync(path.resolve('./', appName)); } // Create an empty directory for the test app. var appDirPath = path.resolve('./', appName); fs.mkdirSync(appDirPath); // process.chdir(appName); child_process.exec('node ' + pathToLocalSailsCLI + ' new --fast --traditional --without=lodash,async', function(err) { if (err) { return done(err); } // Symlink dependencies module.exports.linkDeps('.'); // Copy test fixtures to the test app. fs.copy('../test/integration/fixtures/sampleapp', './', done); }); }, /** * Remove a test app (clean up files on disk.) * * @sync (because it sync filesystem methods) */ teardown: function(appName) { appName = appName ? appName : 'testApp'; var dir = path.resolve('./', appName); if (fs.existsSync(dir)) { fs.removeSync(dir); } }, liftQuiet: function(options, callback) { if (_.isFunction(options)) { callback = options; options = null; } options = options || {}; _.defaults(options, { log: { level: 'silent' } }); return module.exports.lift(options, callback); }, lift: function(options, callback) { // Clear NODE_ENV to avoid unintended consequences. delete process.env.NODE_ENV; if (_.isFunction(options)) { callback = options; options = null; } options = options || {}; _.defaults(options, { port: 1342, environment: process.env.TEST_ENV, globals: false }); options.hooks = options.hooks || {}; options.hooks.grunt = options.hooks.grunt || false; Sails().lift(options, function(err, sails) { if (err) { return callback(err); } return callback(null, sails); }); }, load: function(options, callback) { // Clear NODE_ENV to avoid unintended consequences. delete process.env.NODE_ENV; if (_.isFunction(options)) { callback = options; options = null; } options = options || {}; _.defaults(options, { port: 1342, environment: process.env.TEST_ENV }); options.hooks = options.hooks || {}; options.hooks.grunt = options.hooks.grunt || false; Sails().load(options, function(err, sails) { if (err) { return callback(err); } return callback(null, sails); }); }, buildAndLift: function(appName, options, callback) { if (_.isFunction(options)) { callback = options; options = null; } module.exports.build(appName, function(err) { if (err) { return callback(err); } module.exports.lift(options, callback); }); }, liftWithTwoSockets: function(options, callback) { if (_.isFunction(options)) { callback = options; options = null; } module.exports.lift(options, function(err, sails) { if (err) { return callback(err); } var socket1 = io.sails.connect('http://localhost:1342', { multiplex: false, }); socket1.on('connect', function() { var socket2 = io.sails.connect('http://localhost:1342', { multiplex: false, }); socket2.on('connect', function() { return callback(null, sails, socket1, socket2); }); }); }); }, buildAndLiftWithTwoSockets: function(appName, options, callback) { if (_.isFunction(options)) { callback = options; options = null; } module.exports.build(appName, function(err) { if (err) { return callback(err); } module.exports.liftWithTwoSockets(options, callback); }); }, linkDeps: function(appPath) { // Get the given app's package.json (defaulting to an empty dictionary). var packageJson; try { packageJson = require(path.resolve(appPath, 'package.json')); } catch (e) { packageJson = {}; } var deps = ['sails-hook-orm', 'sails-hook-sockets']; _.each(deps, function(dep) { // Create a symlink fs.ensureSymlinkSync(path.resolve(__dirname, '..', '..', '..', 'node_modules', dep), path.resolve(appPath, 'node_modules', dep)); // Add a entry into the package.json dependencies packageJson.dependencies = packageJson.dependencies || {}; packageJson.dependencies[dep] = '0.0.0'; }); // Output the update package.json fs.writeFileSync(path.resolve(appPath, 'package.json'), JSON.stringify(packageJson)); }, linkLodash: function(appPath) { fs.ensureSymlinkSync(path.resolve(__dirname, '..', '..', '..', 'node_modules', '@sailshq', 'lodash'), path.resolve(appPath, 'node_modules', '@sailshq', 'lodash')); }, linkAsync: function(appPath) { fs.ensureSymlinkSync(path.resolve(__dirname, '..', '..', '..', 'node_modules', 'async'), path.resolve(appPath, 'node_modules', 'async')); }, linkSails: function(appPath) { fs.ensureSymlinkSync(path.resolve(__dirname, '..', '..', '..'), path.resolve(appPath, 'node_modules', 'sails')); }, }; ================================================ FILE: test/integration/helpers/httpHelper.js ================================================ var request = require('@sailshq/request'); var fs = require('fs'); /** * Original test helpers * * TODO: refactor these into the other more modern set of test helpers. * (these are from waaaayyyy back) */ module.exports = { // Write specified routing config to the `config/routes.js` file. writeRoutes: function(routesConfig) { fs.writeFileSync('config/routes.js', 'module.exports.routes = ' + JSON.stringify(routesConfig)); }, // Write specified blueprints config to the `config/blueprints.js` file. writeBlueprint: function(blueprintsConfig) { fs.writeFileSync('config/blueprints.js', 'module.exports.blueprints = ' + JSON.stringify(blueprintsConfig)); }, // Make a request to an already-lifted Sails server running on port 1342. testRoute: function(method, options, callback) { // Prefix url with domain:port if (typeof options === 'string') { options = {url: 'http://localhost:1342/' + options}; } else { options.url = 'http://localhost:1342/' + options.url; } options.method = (method === 'del') ? 'delete' : method; request(options, function(err, response, body) { if (err) { return callback(err, response, body); } return callback(null, response, body); }); } }; ================================================ FILE: test/integration/helpers/socketHelper.js ================================================ var fs = require('fs'); module.exports = { // Write routes object to blueprint config file writeModelConfig: function(config) { fs.writeFileSync('config/models.js', 'module.exports.models = {autosubscribe: [], connection: "localDiskDb"}'); }, // Starts sails server, makes request, returns response, kills sails server testRoute: function(socket, method, options, callback) { var url, data = {}; // Prefix url with domain:port if (typeof options === 'string') { url = options; } else { url = options.url; } if (method === 'get') { socket[method](url, function(response) { callback(null, response); }); } else { socket[method](url, data, function(response) { callback(null, response); }); } } }; ================================================ FILE: test/integration/hook.3rdparty.test.js ================================================ /** * Test dependencies */ var assert = require('assert'); var httpHelper = require('./helpers/httpHelper.js'); var appHelper = require('./helpers/appHelper'); var util = require('util'); var path = require('path'); var fs = require('fs-extra'); var _ = require('@sailshq/lodash'); describe('hooks :: ', function() { var sailsprocess; describe('installing a 3rd-party hook', function() { var appName = 'testApp'; // Before each test, remove the test app's package.json from the require cache, // so that we can change its dependencies on a per-test basis. beforeEach(function() { delete require.cache[path.resolve(__dirname, '../..', appName, 'package.json')]; }); before(function() { appHelper.teardown(); }); describe('into node_modules/sails-hook-shout', function(){ before(function(done) { // Add `sails-hook-shout` as a dependency of the test app. fs.outputFileSync(path.resolve(__dirname, '../..', appName, 'package.json'), '{"dependencies":{"sails-hook-shout":"0.0.0"}}'); // Copy the hook into the test app's node_modules folder. fs.mkdirs(path.resolve(__dirname, '../..', appName, 'node_modules'), function(err) { if (err) {return done(err);} fs.copySync(path.resolve(__dirname, 'fixtures/hooks/installable/shout'), path.resolve(__dirname,'../../testApp/node_modules/sails-hook-shout')); process.chdir(path.resolve(__dirname, '../..', appName)); done(); }); }); after(function() { process.chdir('../'); // Sleep for 500ms--otherwise we get timing errors for this test on Windows setTimeout(function() { appHelper.teardown(); }, 500); }); describe('with default settings', function() { var sails; before(function(done) { appHelper.liftQuiet(function(err, _sails) { if (err) {return done(err);} sails = _sails; return done(); }); }); after(function(done) { sails.lower(function(){setTimeout(done, 100);}); }); it('should install a hook into `sails.hooks.shout`', function() { assert(sails.hooks.shout); }); it('should use merge the default hook config', function() { assert.equal(sails.config.shout.phrase, 'make it rain'); }); it('should bind a /shout route that responds with the default phrase', function(done) { httpHelper.testRoute('GET', 'shout', function(err, resp, body) { assert.equal(body, 'make it rain'); return done(); }); }); }); describe('with hooks.shout set to boolean false', function() { var sails; before(function(done) { appHelper.liftQuiet({hooks: {shout: false}}, function(err, _sails) { if (err) {return done(err);} sails = _sails; return done(); }); }); after(function(done) { sails.lower(function(){setTimeout(done, 100);}); }); it('should not install a hook into `sails.hooks.shout`', function() { assert(_.isUndefined(sails.hooks.shout)); }); }); describe('with hooks.shout set to the string `false`', function() { var sails; before(function(done) { appHelper.liftQuiet({hooks: {shout: 'false'}}, function(err, _sails) { if (err) {return done(err);} sails = _sails; return done(); }); }); after(function(done) { sails.lower(function(){setTimeout(done, 100);}); }); it('should not install a hook into `sails.hooks.shout`', function() { assert(_.isUndefined(sails.hooks.shout)); }); }); describe('with hook-level config options', function() { var sails; before(function(done) { appHelper.liftQuiet({shout: {phrase: 'yolo'}}, function(err, _sails) { if (err) {return done(err);} sails = _sails; return done(); }); }); after(function(done) { sails.lower(function(){setTimeout(done, 100);}); }); it('should bind a /shout route that responds with the configured phrase', function(done) { httpHelper.testRoute('GET', 'shout', function(err, resp, body) { assert(body === 'yolo'); return done(); }); }); }); describe('setting the config key to `shoutHook`', function() { var sails; before(function(done) { appHelper.liftQuiet({installedHooks: {'sails-hook-shout': {configKey: 'shoutHook'}}, shoutHook: {phrase: 'holla back!'}}, function(err, _sails) { if (err) {return done(err);} sails = _sails; return done(); }); }); after(function(done) { sails.lower(function(){setTimeout(done, 100);}); }); it('should bind a /shout route that responds with the configured phrase', function(done) { httpHelper.testRoute('GET', 'shout', function(err, resp, body) { assert(body === 'holla back!'); return done(); }); }); }); describe('setting the hook name to `foobar`', function(){ var sails; before(function(done) { appHelper.liftQuiet({installedHooks: {'sails-hook-shout': {name: 'foobar'}}}, function(err, _sails) { if (err) {return done(err);} sails = _sails; return done(); }); }); after(function(done) { sails.lower(function(){setTimeout(done, 100);}); }); it('should install a hook into `sails.hooks.foobar`', function() { assert(sails.hooks.foobar); }); it('should use merge the default hook config', function() { assert(sails.config.foobar.phrase === 'make it rain', sails.config.foobar.phrase); }); it('should bind a /shout route that responds with the default phrase', function(done) { httpHelper.testRoute('GET', 'shout', function(err, resp, body) { assert(body === 'make it rain'); return done(); }); }); }); describe('setting the hook name to `security` (an existing hook)', function(){ var sails; before(function(done) { appHelper.liftQuiet({installedHooks: {'sails-hook-shout': {name: 'security'}}}, function(err, _sails) { sails = _sails; done(err); }); }); after(function(done) { sails.lower(function(){setTimeout(done, 100);}); }); it('should replace the core `security` hook', function() { assert(sails.hooks.security.isShoutyHook); }); }); }); describe('into node_modules/shouty', function(){ before(function(done) { // Add `shouty` as a dependency of the test app. fs.outputFileSync(path.resolve(__dirname, '../..', appName, 'package.json'), '{"dependencies":{"shouty":"0.0.0"}}'); fs.mkdirs(path.resolve(__dirname, '../..', appName, 'node_modules'), function(err) { if (err) {return done(err);} fs.copySync(path.resolve(__dirname, 'fixtures/hooks/installable/shout'), path.resolve(__dirname,'../../testApp/node_modules/shouty')); var packageJson = JSON.parse(fs.readFileSync(path.resolve(__dirname,'../../testApp/node_modules/shouty','package.json'))); packageJson.name = 'shouty'; fs.writeFileSync(path.resolve(__dirname,'../../testApp/node_modules/shouty','package.json'), JSON.stringify(packageJson)); process.chdir(path.resolve(__dirname, '../..', appName)); done(); }); }); after(function(done) { process.chdir('../'); // Sleep for 500ms--otherwise we get timing errors for this test on Windows setTimeout(function() { appHelper.teardown(); return done(); }, 500); }); describe('with default settings', function() { var sails; before(function(done) { appHelper.liftQuiet(function(err, _sails) { if (err) {return done(err);} sails = _sails; return done(); }); }); after(function(done) { sails.lower(function(){setTimeout(done, 100);}); }); it('should install a hook into `sails.hooks.shouty`', function() { assert(sails.hooks.shouty); }); it('should use merge the default hook config', function() { assert(sails.config.shouty.phrase === 'make it rain', sails.config.shouty.phrase); }); it('should bind a /shout route that responds with the default phrase', function(done) { httpHelper.testRoute('GET', 'shout', function(err, resp, body) { assert(body === 'make it rain'); return done(); }); }); }); describe('with `hookName` set to `security` in the package.json', function() { var sails; before(function(done) { delete require.cache[path.resolve(__dirname,'../../testApp/node_modules/shouty','package.json')]; var packageJson = JSON.parse(fs.readFileSync(path.resolve(__dirname,'../../testApp/node_modules/shouty','package.json'))); packageJson.sails.hookName = 'security'; fs.writeFileSync(path.resolve(__dirname,'../../testApp/node_modules/shouty','package.json'), JSON.stringify(packageJson)); appHelper.liftQuiet(function(err, _sails) { if (err) {return done(err);} sails = _sails; return done(); }); }); after(function(done) { sails.lower(function(){setTimeout(done, 100);}); }); it('should replace the core `security` hook', function() { assert(sails.hooks.security.isShoutyHook); }); }); }); describe('into node_modules/sails-hook-security', function(){ var sails; before(function(done) { // Add `sails-hook-security` as a dependency of the test app. fs.outputFileSync(path.resolve(__dirname, '../..', appName, 'package.json'), '{"dependencies":{"sails-hook-security":"0.0.0"}}'); fs.mkdirs(path.resolve(__dirname, '../..', appName, 'node_modules'), function(err) { if (err) {return done(err);} fs.copySync(path.resolve(__dirname, 'fixtures/hooks/installable/shout'), path.resolve(__dirname,'../../testApp/node_modules/sails-hook-security')); var packageJson = JSON.parse(fs.readFileSync(path.resolve(__dirname,'../../testApp/node_modules/sails-hook-security','package.json'))); packageJson.name = 'sails-hook-security'; fs.writeFileSync(path.resolve(__dirname,'../../testApp/node_modules/sails-hook-security','package.json'), JSON.stringify(packageJson)); process.chdir(path.resolve(__dirname, '../..', appName)); appHelper.liftQuiet(function(err, _sails) { if (err) {return done(err);} sails = _sails; return done(); }); }); }); after(function(done) { sails.lower(function(err) { process.chdir('../'); appHelper.teardown(); return done(err); }); }); it('should replace the core `security` hook', function() { assert(sails.hooks.security.isShoutyHook); }); }); describe('into node_modules/security', function(){ var sails; before(function(done) { // Add `security` as a dependency of the test app. fs.outputFileSync(path.resolve(__dirname, '../..', appName, 'package.json'), '{"dependencies":{"security":"0.0.0"}}'); fs.mkdirs(path.resolve(__dirname, '../..', appName, 'node_modules'), function(err) { if (err) {return done(err);} fs.copySync(path.resolve(__dirname, 'fixtures/hooks/installable/shout'), path.resolve(__dirname,'../../testApp/node_modules/security')); var packageJson = JSON.parse(fs.readFileSync(path.resolve(__dirname,'../../testApp/node_modules/security','package.json'))); packageJson.name = 'security'; fs.writeFileSync(path.resolve(__dirname,'../../testApp/node_modules/security','package.json'), JSON.stringify(packageJson)); process.chdir(path.resolve(__dirname, '../..', appName)); appHelper.liftQuiet(function(err, _sails) { if (err) {return done(err);} sails = _sails; return done(); }); }); }); after(function(done) { sails.lower(function(err) { process.chdir('../'); appHelper.teardown(); return done(err); }); }); it('should replace the core `security` hook', function() { assert(sails.hooks.security.isShoutyHook); }); }); describe('into node_modules/@my-modules/shouty', function(){ describe('with default settings', function() { var sails; before(function(done) { // Add `@my-modules/shouty` as a dependency of the test app. fs.outputFileSync(path.resolve(__dirname, '../..', appName, 'package.json'), '{"dependencies":{"@my-modules/shouty":"0.0.0"}}'); fs.mkdirs(path.resolve(__dirname, '../..', appName, 'node_modules', '@my-modules'), function(err) { if (err) {return done(err);} fs.copySync(path.resolve(__dirname, 'fixtures/hooks/installable/shout'), path.resolve(__dirname,'../../testApp/node_modules/@my-modules/shouty')); var packageJson = JSON.parse(fs.readFileSync(path.resolve(__dirname,'../../testApp/node_modules/@my-modules/shouty','package.json'))); packageJson.name = '@my-modules/shouty'; fs.writeFileSync(path.resolve(__dirname,'../../testApp/node_modules/@my-modules/shouty','package.json'), JSON.stringify(packageJson)); process.chdir(path.resolve(__dirname, '../..', appName)); appHelper.liftQuiet(function(err, _sails) { if (err) {return done(err);} sails = _sails; return done(); }); }); }); after(function(done) { sails.lower(function(err) { process.chdir('../'); appHelper.teardown(); return done(err); }); }); it('should install a hook into `sails.hooks.shouty`', function() { assert(sails.hooks.shouty); }); }); describe('with `hookName` set to `security` in the package.json', function() { var sails; before(function(done) { fs.outputFileSync(path.resolve(__dirname, '../..', appName, 'package.json'), '{"dependencies":{"@my-modules/shouty":"0.0.0"}}'); delete require.cache[path.resolve(__dirname,'../../testApp/node_modules/@my-modules/shouty')]; delete require.cache[path.resolve(__dirname,'../../testApp/node_modules/@my-modules/shouty','package.json')]; fs.mkdirs(path.resolve(__dirname, '../..', appName, 'node_modules', '@my-modules'), function(err) { if (err) {return done(err);} fs.copySync(path.resolve(__dirname, 'fixtures/hooks/installable/shout'), path.resolve(__dirname,'../../testApp/node_modules/@my-modules/shouty')); var packageJson = JSON.parse(fs.readFileSync(path.resolve(__dirname,'../../testApp/node_modules/@my-modules/shouty','package.json'))); packageJson.sails.hookName = 'security'; packageJson.name = '@my-modules/shouty'; fs.writeFileSync(path.resolve(__dirname,'../../testApp/node_modules/@my-modules/shouty','package.json'), JSON.stringify(packageJson)); process.chdir(path.resolve(__dirname, '../..', appName)); appHelper.liftQuiet(function(err, _sails) { if (err) {return done(err);} sails = _sails; return done(); }); }); }); after(function(done) { sails.lower(function(err) { process.chdir('../'); appHelper.teardown(); return done(err); }); }); it('should replace the core `security` hook', function() { assert(sails.hooks.security.isShoutyHook); }); }); }); describe('into node_modules/@my-modules/sails-hook-security', function(){ var sails; before(function(done) { // Add `@my-modules/sails-hook-security` as a dependency of the test app. fs.outputFileSync(path.resolve(__dirname, '../..', appName, 'package.json'), '{"dependencies":{"sails-hook-shout":"0.0.0"}}'); fs.mkdirs(path.resolve(__dirname, '../..', appName, 'node_modules', '@my-modules'), function(err) { if (err) {return done(err);} fs.copySync(path.resolve(__dirname, 'fixtures/hooks/installable/shout'), path.resolve(__dirname,'../../testApp/node_modules/@my-modules/sails-hook-security')); var packageJson = JSON.parse(fs.readFileSync(path.resolve(__dirname,'../../testApp/node_modules/@my-modules/sails-hook-security','package.json'))); fs.writeFileSync(path.resolve(__dirname,'../../testApp/node_modules/@my-modules/sails-hook-security','package.json'), JSON.stringify(packageJson)); process.chdir(path.resolve(__dirname, '../..', appName)); appHelper.liftQuiet(function(err, _sails) { if (err) {return done(err);} sails = _sails; return done(); }); }); }); after(function(done) { sails.lower(function(err) { process.chdir('../'); appHelper.teardown(); return done(err); }); }); it('should replace the core `security` hook', function() { assert(sails.hooks.security.isShoutyHook); }); }); describe('into node_modules/@my-modules/sails-hook-security, with no corresponding package.json entry', function(){ var sails; before(function(done) { fs.mkdirs(path.resolve(__dirname, '../..', appName, 'node_modules', '@my-modules'), function(err) { if (err) {return done(err);} fs.copySync(path.resolve(__dirname, 'fixtures/hooks/installable/shout'), path.resolve(__dirname,'../../testApp/node_modules/@my-modules/sails-hook-security')); var packageJson = JSON.parse(fs.readFileSync(path.resolve(__dirname,'../../testApp/node_modules/@my-modules/sails-hook-security','package.json'))); fs.writeFileSync(path.resolve(__dirname,'../../testApp/node_modules/@my-modules/sails-hook-security','package.json'), JSON.stringify(packageJson)); process.chdir(path.resolve(__dirname, '../..', appName)); appHelper.liftQuiet(function(err, _sails) { if (err) {return done(err);} sails = _sails; return done(); }); }); }); after(function(done) { sails.lower(function(err) { process.chdir('../'); appHelper.teardown(); return done(err); }); }); it('should NOT replace the core `security` hook', function() { assert(!sails.hooks.security.isShoutyHook); }); }); describe('with an invalid package.json file', function(){ var sails; before(function(done) { delete require.cache[path.resolve(__dirname,'../../testApp/node_modules/@my-modules/sails-hook-security/package.json')]; fs.mkdirs(path.resolve(__dirname, '../..', appName, 'node_modules', '@my-modules'), function(err) { if (err) {return done(err);} fs.copySync(path.resolve(__dirname, 'fixtures/hooks/installable/shout'), path.resolve(__dirname,'../../testApp/node_modules/@my-modules/sails-hook-security')); fs.outputFileSync(path.resolve(__dirname,'../../testApp/node_modules/@my-modules/sails-hook-security/package.json'), '{"foo":<%=bar%>}'); process.chdir(path.resolve(__dirname, '../..', appName)); return done(); }); }); after(function(done) { sails ? sails.lower(function(err) { process.chdir('../'); appHelper.teardown(); return done(err); }): done(); }); it('should lift without crashing', function(done) { appHelper.liftQuiet(function(err, _sails) { if (err) {return done(err);} sails = _sails; assert(!sails.hooks.security.isShoutyHook); return done(); }); }); }); }); }); ================================================ FILE: test/integration/hook.blueprints.action.routes.test.js ================================================ /** * Module dependencies */ var util = require('util'); var assert = require('assert'); var tmp = require('tmp'); var _ = require('@sailshq/lodash'); var Filesystem = require('machinepack-fs'); var Sails = require('../../lib').constructor; tmp.setGracefulCleanup(); /** * Errors */ var Err = { badResponse: function(response) { return 'Wrong server response! Response :::\n' + util.inspect(response.body); } }; /** * Tests */ describe('blueprints :: ', function() { describe('actions routes :: ', function() { describe('when turned on :: ', function() { var curDir, tmpDir, sailsApp; before(function(done) { // Cache the current working directory. curDir = process.cwd(); // Create a temp directory. tmpDir = tmp.dirSync({gracefulCleanup: true, unsafeCleanup: true}); // Switch to the temp directory. process.chdir(tmpDir.name); (new Sails()).load({ hooks: { grunt: false, views: false, policies: false, pubsub: false, i18n: false }, models: { migrate: 'drop', schema: true }, blueprints: { actions: true, shortcuts: false, rest: false }, log: {level: 'error'}, controllers: { moduleDefinitions: { 'toplevellegacy/fnaction': function (req, res) { res.send('legacy fn action!'); }, 'toplevellegacy/machineaction': { exits: {success: {outputExample: 'abc123'} }, fn: function (inputs, exits) { exits.success('legacy machine action!'); } }, 'top-level-standalone-fn': function (req, res) { res.send('top level standalone fn!'); }, 'top-level-standalone-machine': { exits: {success: {outputExample: 'abc123'} }, fn: function (inputs, exits) { exits.success('top level standalone machine!'); } }, 'somefolder/someotherfolder/nestedlegacy/fnaction': function (req, res) { res.send('nested legacy fn action!'); }, 'somefolder/someotherfolder/nestedlegacy/machineaction': { exits: {success: {outputExample: 'abc123'} }, fn: function (inputs, exits) { exits.success('nested legacy machine action!'); } }, 'somefolder/someotherfolder/nested-standalone-machine': { exits: {success: {outputExample: 'abc123'} }, fn: function (inputs, exits) { exits.success('nested standalone machine!'); } } } } }, function(err, _sails) { if (err) { return done(err); } sailsApp = _sails; return done(); }); }); after(function(done) { sailsApp.lower(function() { process.chdir(curDir); return done(); }); }); it('should bind a route to \'ALL /toplevellegacy/fnaction\'', function(done) { sailsApp.request('POST /toplevellegacy/fnaction', {}, function (err, resp, data) { assert(!err, err); assert.deepEqual(data, 'legacy fn action!'); sailsApp.request('GET /toplevellegacy/fnaction', {}, function (err, resp, data) { assert(!err, err); assert.deepEqual(data, 'legacy fn action!'); done(); }); }); }); it('should bind a route to \'ALL /toplevellegacy/machineaction\'', function(done) { sailsApp.request('POST /toplevellegacy/machineaction', {}, function (err, resp, data) { assert(!err, err); assert.deepEqual(data, 'legacy machine action!'); sailsApp.request('GET /toplevellegacy/machineaction', {}, function (err, resp, data) { assert(!err, err); assert.deepEqual(data, 'legacy machine action!'); done(); }); }); }); it('should bind a route to \'ALL /top-level-standalone-fn\'', function(done) { sailsApp.request('POST /top-level-standalone-fn', {}, function (err, resp, data) { assert(!err, err); assert.deepEqual(data, 'top level standalone fn!'); sailsApp.request('GET /top-level-standalone-fn', {}, function (err, resp, data) { assert(!err, err); assert.deepEqual(data, 'top level standalone fn!'); done(); }); }); }); it('should bind a route to \'ALL /top-level-standalone-machine\'', function(done) { sailsApp.request('POST /top-level-standalone-machine', {}, function (err, resp, data) { assert(!err, err); assert.deepEqual(data, 'top level standalone machine!'); sailsApp.request('GET /top-level-standalone-machine', {}, function (err, resp, data) { assert(!err, err); assert.deepEqual(data, 'top level standalone machine!'); done(); }); }); }); it('should bind a route to \'ALL /somefolder/someotherfolder/nestedlegacy/fnaction\'', function(done) { sailsApp.request('POST /somefolder/someotherfolder/nestedlegacy/fnaction', {}, function (err, resp, data) { assert(!err, err); assert.deepEqual(data, 'nested legacy fn action!'); sailsApp.request('GET /somefolder/someotherfolder/nestedlegacy/fnaction', {}, function (err, resp, data) { assert(!err, err); assert.deepEqual(data, 'nested legacy fn action!'); done(); }); }); }); it('should bind a route to \'ALL /somefolder/someotherfolder/nestedlegacy/machineaction\'', function(done) { sailsApp.request('POST /somefolder/someotherfolder/nestedlegacy/machineaction', {}, function (err, resp, data) { assert(!err, err); assert.deepEqual(data, 'nested legacy machine action!'); sailsApp.request('GET /somefolder/someotherfolder/nestedlegacy/machineaction', {}, function (err, resp, data) { assert(!err, err); assert.deepEqual(data, 'nested legacy machine action!'); done(); }); }); }); it('should bind a route to \'ALL /somefolder/someotherfolder/nested-standalone-machine\'', function(done) { sailsApp.request('POST /somefolder/someotherfolder/nested-standalone-machine', {}, function (err, resp, data) { assert(!err, err); assert.deepEqual(data, 'nested standalone machine!'); sailsApp.request('GET /somefolder/someotherfolder/nested-standalone-machine', {}, function (err, resp, data) { assert(!err, err); assert.deepEqual(data, 'nested standalone machine!'); done(); }); }); }); }); // </ describe('when turned on :: ', ... > describe('when turned off globally', function() { var curDir, tmpDir, sailsApp; before(function(done) { // Cache the current working directory. curDir = process.cwd(); // Create a temp directory. tmpDir = tmp.dirSync({gracefulCleanup: true, unsafeCleanup: true}); // Switch to the temp directory. process.chdir(tmpDir.name); (new Sails()).load({ hooks: { grunt: false, views: false, policies: false, pubsub: false, i18n: false }, models: { migrate: 'drop', schema: true }, blueprints: { actions: false, shortcuts: false, rest: false }, log: {level: 'error'}, controllers: { moduleDefinitions: { 'toplevellegacy/fnaction': function (req, res) { res.send('legacy fn action!'); }, 'toplevellegacy/machineaction': { exits: {success: {outputExample: 'abc123'} }, fn: function (inputs, exits) { exits.success('legacy machine action!'); } }, 'top-level-standalone-fn': function (req, res) { res.send('top level standalone fn!'); }, 'top-level-standalone-machine': { exits: {success: {outputExample: 'abc123'} }, fn: function (inputs, exits) { exits.success('top level standalone machine!'); } }, 'somefolder/someotherfolder/nestedlegacy/fnaction': function (req, res) { res.send('nested legacy fn action!'); }, 'somefolder/someotherfolder/nestedlegacy/machineaction': { exits: {success: {outputExample: 'abc123'} }, fn: function (inputs, exits) { exits.success('nested legacy machine action!'); } }, 'somefolder/someotherfolder/nested-standalone-machine': { exits: {success: {outputExample: 'abc123'} }, fn: function (inputs, exits) { exits.success('nested standalone machine!'); } } } } }, function(err, _sails) { if (err) { return done(err); } sailsApp = _sails; return done(); }); }); after(function(done) { sailsApp.lower(function() { process.chdir(curDir); return done(); }); }); it('should not bind a route to \'ALL /toplevellegacy/fnaction\'', function(done) { sailsApp.request('POST /toplevellegacy/fnaction', {}, function (err, resp, data) { assert(err); assert.equal(err.status, 404); return done(); }); }); it('should bind a route to \'ALL /top-level-standalone-fn\'', function(done) { sailsApp.request('POST /top-level-standalone-fn', {}, function (err, resp, data) { assert(err); assert.equal(err.status, 404); return done(); }); }); }); // </ describe('when turned off globally :: ', ... > describe('when turned off for a single controller', function() { var curDir, tmpDir, sailsApp; before(function(done) { // Cache the current working directory. curDir = process.cwd(); // Create a temp directory. tmpDir = tmp.dirSync({gracefulCleanup: true, unsafeCleanup: true}); // Switch to the temp directory. process.chdir(tmpDir.name); Filesystem.writeSync({ force: true, destination: 'api/controllers/NoController.js', string: 'module.exports = { _config: { actions: false }, test: function (req, res) { return res.ok(); } }' }).execSync(); Filesystem.writeSync({ force: true, destination: 'api/controllers/YesController.js', string: 'module.exports = { test: function (req, res) { return res.ok(); } }' }).execSync(); (new Sails()).load({ hooks: { grunt: false, views: false, policies: false, pubsub: false, i18n: false }, models: { migrate: 'drop', schema: true }, blueprints: { actions: true, shortcuts: false, rest: false }, log: {level: 'error'}, }, function(err, _sails) { if (err) { return done(err); } sailsApp = _sails; return done(); }); }); after(function(done) { sailsApp.lower(function() { process.chdir(curDir); return done(); }); }); it('should not bind a route to \'ALL /no/test\'', function(done) { sailsApp.request('POST /no/test', {}, function (err, resp, data) { assert(err); assert.equal(err.status, 404); return done(); }); }); it('should bind a route to \'ALL /yes/test\'', function(done) { sailsApp.request('POST /yes/test', {}, function (err, resp, data) { assert(!err, err); done(); }); }); }); // </ describe('for a single controller', ... > }); }); ================================================ FILE: test/integration/hook.blueprints.blacklist.test.js ================================================ /** * Test dependencies */ var assert = require('assert'); var httpHelper = require('./helpers/httpHelper.js'); var appHelper = require('./helpers/appHelper'); var util = require('util'); var async = require('async'); var fixture = require('./fixtures/users.js'); var _ = require('@sailshq/lodash'); var fs = require('fs-extra'); var path = require('path'); var Sails = require('../../lib/app'); xdescribe('blueprints :: ', function() { describe('using the values blacklist ::', function() { describe('updating a model with a non-primary-key "id" attribute', function() { before(function(done) { // Build the app appHelper.build(function(err) { if (err) { return done(err); } var Goal = { attributes: { hash: { type: 'string', unique: true, primaryKey: true }, id: 'integer', active: 'boolean' } }; fs.outputFileSync(path.resolve(__dirname,'../../testApp/api/models/Goal.js'), 'module.exports = ' + JSON.stringify(Goal) + ';'); fs.outputFileSync(path.resolve(__dirname,'../../testApp/api/controllers/GoalController.js'), 'module.exports = {};'); return done(); }); }); var sails = Sails(); before(function(done) { sails.load({ hooks: { grunt: false, i18n: false }, globals: false, log: { level: 'silent' } }, function(err) { if (err) {return done(err);} sails.models.goal.create({id: 1, hash: 'abc', active: false}).exec(done); }); }); it('should update the record successfully', function(done) { sails.request('put /goal/abc', {active: true}, function(err, response, body) { if (err) {return done(err);} assert.equal (response.statusCode, 200); assert.equal(body.id, 1); assert.equal(body.hash, 'abc'); assert.equal(body.active, true); return done(); }); }); after(function(done) { sails.lower(function(err){ if (err) {return done(err);} setTimeout(done, 100); }); });//</after> after(function(done) { process.chdir('../'); appHelper.teardown(); return done(); });//</after> });//</describe(updating a model with a non-primary-key "id" attribute)> });//</describe> });//</describe> ================================================ FILE: test/integration/hook.blueprints.index.routes.test.js ================================================ /** * Test dependencies */ var util = require('util'); var assert = require('assert'); var tmp = require('tmp'); var _ = require('@sailshq/lodash'); var appHelper = require('./helpers/appHelper'); var Sails = require('../../lib').constructor; /** * Errors */ var Err = { badResponse: function(response) { return 'Wrong server response! Response :::\n' + util.inspect(response.body); } }; describe('blueprints :: ', function() { var curDir, tmpDir, sailsApp; var extraSailsConfig = {}; before(function(done) { // Cache the current working directory. curDir = process.cwd(); // Create a temp directory. tmpDir = tmp.dirSync({gracefulCleanup: true, unsafeCleanup: true}); // Switch to the temp directory. process.chdir(tmpDir.name); appHelper.linkDeps(tmpDir.name); (new Sails()).load(_.merge({ hooks: { grunt: false, views: false, policies: false, pubsub: false, i18n: false }, orm: { moduleDefinitions: { models: { 'user': {} } } }, models: { migrate: 'drop', attributes: { createdAt: { type: 'number', autoCreatedAt: true, }, updatedAt: { type: 'number', autoUpdatedAt: true, }, id: { type: 'number', autoIncrement: true} } }, blueprints: { shortcuts: false, actions: true, rest: true, index: true }, log: {level: 'error'}, controllers: { moduleDefinitions: { 'index': function (req, res) { res.send('top-level index!'); }, 'secondlevel/index': function (req, res) { res.send('second-level index!'); }, 'thirdlevel/index': function (req, res) { res.send('third-level index!'); }, 'user/index': function (req, res) { res.send('user index!'); } } } }, extraSailsConfig), function(err, _sails) { if (err) { return done(err); } sailsApp = _sails; return done(); }); }); after(function(done) { sailsApp.lower(function() { process.chdir(curDir); return done(); }); }); it('should bind \'ALL /\' to the `index` action', function(done) { sailsApp.request('POST /', {}, function (err, resp, data) { assert(!err, err); assert.deepEqual(data, 'top-level index!'); sailsApp.request('GET /', {}, function (err, resp, data) { assert(!err, err); assert.deepEqual(data, 'top-level index!'); done(); }); }); }); it('should bind \'ALL /secondlevel\' to the `secondlevel.index` action', function(done) { sailsApp.request('POST /secondlevel', {}, function (err, resp, data) { assert(!err, err); assert.deepEqual(data, 'second-level index!'); sailsApp.request('GET /secondlevel', {}, function (err, resp, data) { assert(!err, err); assert.deepEqual(data, 'second-level index!'); done(); }); }); }); it('should bind \'ALL /thirdlevel\' to the `thirdlevel.index` action', function(done) { sailsApp.request('POST /thirdlevel', {}, function (err, resp, data) { assert(!err, err); assert.deepEqual(data, 'third-level index!'); sailsApp.request('GET /thirdlevel', {}, function (err, resp, data) { assert(!err, err); assert.deepEqual(data, 'third-level index!'); done(); }); }); }); it('should not override RESTful routes', function(done) { sailsApp.request('POST /user', {}, function (err, resp, data) { assert(!err, err); assert.deepEqual(data.id, 1); sailsApp.request('GET /user', function (err, resp, data) { assert(!err, err); assert.deepEqual(data.length, 1); assert.deepEqual(data[0].id, 1); done(); }); }); }); }); ================================================ FILE: test/integration/hook.blueprints.restful.routes.test.js ================================================ /** * Test dependencies */ var util = require('util'); var assert = require('assert'); var tmp = require('tmp'); var _ = require('@sailshq/lodash'); var Filesystem = require('machinepack-fs'); var appHelper = require('./helpers/appHelper'); var Sails = require('../../lib').constructor; /** * Errors */ var Err = { badResponse: function(response) { return 'Wrong server response! Response :::\n' + util.inspect(response.body); } }; describe('blueprints :: ', function() { var curDir, tmpDir, sailsApp; var extraSailsConfig = {}; describe('restful routes :: ', function() { describe('when turned off globaly :: ', function() { before(function(done) { // Cache the current working directory. curDir = process.cwd(); // Create a temp directory. tmpDir = tmp.dirSync({gracefulCleanup: true, unsafeCleanup: true}); // Switch to the temp directory. process.chdir(tmpDir.name); appHelper.linkDeps(tmpDir.name); (new Sails()).load({ hooks: { grunt: false, views: false, policies: false, pubsub: false, i18n: false }, orm: { moduleDefinitions: { models: { user: { attributes: { name: 'string', pets: { collection: 'pet', via: 'owner' } } }, pet: { attributes: { name: 'string', owner: { model: 'user' } } } }, } }, models: { migrate: 'drop', schema: true, attributes: { createdAt: { type: 'number', autoCreatedAt: true, }, updatedAt: { type: 'number', autoUpdatedAt: true, }, // id: { type: 'string', unique: true, columnName: '_id'}, id: { type: 'number', autoIncrement: true} } }, blueprints: { rest: false, shortcuts: false, actions: false }, log: {level: 'error'} }, function(err, _sails) { if (err) { return done(err); } sailsApp = _sails; return done(); }); }); after(function(done) { extraSailsConfig = {}; sailsApp.lower(function() { process.chdir(curDir); return done(); }); }); it('a get request to /:model should return a 404', function(done) { sailsApp.models.user.create({name: 'al'}).exec(function(err) { if (err) {return done (err);} sailsApp.request('get /user', function (err, resp, data) { assert(err); assert.equal(err.status, 404); done(); }); }); }); }); describe('when turned off for a specific controller :: ', function() { before(function(done) { // Cache the current working directory. curDir = process.cwd(); // Create a temp directory. tmpDir = tmp.dirSync({gracefulCleanup: true, unsafeCleanup: true}); // Switch to the temp directory. process.chdir(tmpDir.name); appHelper.linkDeps(tmpDir.name); Filesystem.writeSync({ force: true, destination: 'api/controllers/UserController.js', string: 'module.exports = { _config: { rest: false } }' }).execSync(); (new Sails()).load({ hooks: { grunt: false, views: false, policies: false, pubsub: false, i18n: false }, orm: { moduleDefinitions: { models: { user: { attributes: { name: 'string', pets: { collection: 'pet', via: 'owner' } } }, pet: { attributes: { name: 'string', owner: { model: 'user' } } } }, } }, models: { migrate: 'drop', schema: true, attributes: { createdAt: { type: 'number', autoCreatedAt: true, }, updatedAt: { type: 'number', autoUpdatedAt: true, }, // id: { type: 'string', unique: true, columnName: '_id'}, id: { type: 'number', autoIncrement: true} } }, blueprints: { shortcuts: false, actions: false }, log: {level: 'error'} }, function(err, _sails) { if (err) { return done(err); } sailsApp = _sails; return done(); }); }); after(function(done) { extraSailsConfig = {}; sailsApp.lower(function() { process.chdir(curDir); return done(); }); }); it('a get request to the /:model with REST disabled should return a 404', function(done) { sailsApp.models.user.create({name: 'al'}).exec(function(err) { if (err) {return done (err);} sailsApp.request('get /user', function (err, resp, data) { assert(err); assert.equal(err.status, 404); done(); }); }); }); it('a get request to the /:model with REST enabled should return JSON for all of the instances of the test model', function(done) { sailsApp.models.pet.create({name: 'rex'}).exec(function(err) { if (err) {return done (err);} sailsApp.request('get /pet', function (err, resp, data) { assert(!err, err); assert.equal(data.length, 1); assert.equal(data[0].name, 'rex'); assert.equal(data[0].id, 1); done(); }); }); }); }); describe('when turned on :: ', function() { beforeEach(function(done) { // Cache the current working directory. curDir = process.cwd(); // Create a temp directory. tmpDir = tmp.dirSync({gracefulCleanup: true, unsafeCleanup: true}); // Switch to the temp directory. process.chdir(tmpDir.name); appHelper.linkDeps(tmpDir.name); (new Sails()).load(_.merge({ hooks: { grunt: false, views: false, policies: false, i18n: false }, orm: { moduleDefinitions: { models: { 'user': {} } } }, models: { migrate: 'drop', schema: true, attributes: { createdAt: { type: 'number', autoCreatedAt: true, }, updatedAt: { type: 'number', autoUpdatedAt: true, }, // id: { type: 'string', unique: true, columnName: '_id'}, id: { type: 'number', autoIncrement: true} } }, blueprints: { shortcuts: false, actions: false }, log: {level: 'error'} }, extraSailsConfig), function(err, _sails) { if (err) { return done(err); } sailsApp = _sails; return done(); }); }); afterEach(function(done) { sailsApp.lower(function() { process.chdir(curDir); return done(); }); }); describe('basic usage :: ', function() { before(function() { extraSailsConfig = { orm: { moduleDefinitions: { models: { user: { attributes: { name: 'string', pets: { collection: 'pet', via: 'owner' }, animalFriends: { collection: 'pet', via: 'humanFriends' } } }, pet: { attributes: { name: 'string', owner: { model: 'user' }, humanFriends: { collection: 'user', via: 'animalFriends' } } } }, } } }; }); after(function() { extraSailsConfig = {}; }); describe('a get request to /archive (the default archive model', function() { it('should return a 404', function(done) { sailsApp.request('get /archive', function (err) { assert(err, 'Should have received an error trying to access blueprint for archive model, but didn\'t!'); assert.equal(err.status, 404); done(); }); }); }); describe('a get request to /:model', function() { describe('where a single instance of the model exists', function() { it('should return JSON for all of the instances of the test model', function(done) { sailsApp.models.user.create({name: 'al'}).exec(function(err) { if (err) {return done (err);} sailsApp.request('get /user', function (err, resp, data) { assert(!err, err); assert.equal(data.length, 1); assert.equal(data[0].name, 'al'); assert.equal(data[0].id, 1); done(); }); }); }); it('should populate all associations of the test model', function(done) { sailsApp.models.pet.createEach([{name: 'alice'}, {name: 'tex'}, {name: 'bailey'}]).meta({fetch: true}).exec(function(err, pets) { sailsApp.models.user.create({name: 'al', pets: _.pluck(pets, 'id')}).exec(function(err) { if (err) {return done (err);} sailsApp.request('get /user', function (err, resp, data) { assert(!err, err); assert.equal(data.length, 1); assert.equal(data[0].name, 'al'); assert.equal(data[0].id, 1); assert.equal(data[0].pets.length, 3); done(); }); }); }); }); it('should limit populate records to the default limit (30)', function(done) { var instancesToCreate = _.map(_.range(1,41), function(i) { return { name: 'pet' + i }; }); sailsApp.models.pet.createEach(instancesToCreate).meta({fetch: true}).exec(function(err, pets) { sailsApp.models.user.create({name: 'al', pets: _.pluck(pets, 'id')}).exec(function(err) { if (err) {return done (err);} sailsApp.request('get /user', function (err, resp, data) { assert(!err, err); assert.equal(data.length, 1); assert.equal(data[0].name, 'al'); assert.equal(data[0].id, 1); assert.equal(data[0].pets.length, 30); done(); }); }); }); }); }); describe('where 40 instances of the model exist, with no limit set', function() { it('should return JSON for 30 instances of the test model (becase the default limit is 30)', function(done) { var instancesToCreate = _.map(_.range(1,41), function(i) { return { name: 'user_' + i }; }); sailsApp.models.user.createEach(instancesToCreate).exec(function(err) { if (err) {return done (err);} sailsApp.request('get /user', function (err, resp, data) { assert(!err, err); assert.equal(data.length, 30); done(); }); }); }); }); describe('where 40 instances of the model exist, with limit set to 35', function() { it('should return JSON for 35 instances of the test model', function(done) { var instancesToCreate = _.map(_.range(1,41), function(i) { return { name: 'user_' + i }; }); sailsApp.models.user.createEach(instancesToCreate).exec(function(err) { if (err) {return done (err);} sailsApp.request('get /user?limit=35', function (err, resp, data) { assert(!err, err); assert.equal(data.length, 35); done(); }); }); }); }); }); describe('a get request to /:model?id=1', function() { it('should return an array of 1 item', function(done) { sailsApp.models.user.create({name: 'jeremy'}).exec(function(err) { if (err) {return done (err);} sailsApp.request('get /user?id=1', function (err, resp, data) { assert(!err, err); assert(_.isArray(data), 'Should have receieved an array, but got: ' + util.inspect(data, {depth: null})); assert.equal(data.length, 1); assert.equal(data[0].name, 'jeremy'); assert.equal(data[0].id, 1); done(); }); }); }); }); describe('a get request to /:model/:id', function() { it('should return JSON for the requested instance of the test model', function(done) { sailsApp.models.user.create({name: 'ron'}).exec(function(err) { if (err) {return done (err);} sailsApp.request('get /user/1', function (err, resp, data) { assert(!err, err); assert.equal(data.name, 'ron'); assert.equal(data.id, 1); done(); }); }); }); }); describe('a patch request to /:model/:id', function() { it('should return JSON for an updated instance of the test model', function(done) { sailsApp.models.user.create({name: 'dave'}).exec(function(err) { if (err) {return done (err);} sailsApp.request('patch /user/1', {name: 'larry'}, function (err, resp, data) { if (err) {return done (err);} assert.equal(data.name, 'larry'); assert.equal(data.id, 1); sailsApp.models.user.findOne({id: 1}).exec(function(err, user) { if (err) {return done (err);} assert(user); assert.equal(user.name, 'larry'); return done(); }); }); }); }); }); describe('a put request to /:model/:id', function() { it('should return JSON for an updated instance of the test model', function(done) { sailsApp.models.user.create({name: 'dave'}).exec(function(err) { if (err) {return done (err);} sailsApp.request('put /user/1', {name: 'bob'}, function (err, resp, data) { if (err) {return done (err);} assert.equal(data.name, 'bob'); assert.equal(data.id, 1); sailsApp.models.user.findOne({id: 1}).exec(function(err, user) { if (err) {return done (err);} assert(user); assert.equal(user.name, 'bob'); return done(); }); }); }); }); }); describe('a post request to /:model', function() { it('should return JSON for a newly created instance of the test model', function(done) { sailsApp.request('post /user', {name: 'joe'}, function (err, resp, data) { assert(!err, err); assert.equal(data.name, 'joe'); assert.equal(data.id, 1); sailsApp.models.user.findOne({id: 1}).exec(function(err, user) { if (err) {return done (err);} assert(user); assert.equal(user.name, 'joe'); return done(); }); }); }); }); describe('a delete request to /:model', function() { it('should return JSON for the deleted instance of the test model', function(done) { sailsApp.models.user.create({name: 'bubba'}).exec(function(err) { if (err) {return done (err);} sailsApp.request('delete /user/1', function (err, resp, data) { assert(!err, err); assert.equal(data.name, 'bubba'); assert.equal(data.id, 1); sailsApp.models.user.findOne({id: 1}).exec(function(err, user) { if (err) {return done (err);} assert(!user); return done(); }); }); }); }); }); // █████╗ ███████╗███████╗ ██████╗ ██████╗██╗ █████╗ ████████╗██╗ ██████╗ ███╗ ██╗███████╗ // ██╔══██╗██╔════╝██╔════╝██╔═══██╗██╔════╝██║██╔══██╗╚══██╔══╝██║██╔═══██╗████╗ ██║██╔════╝ // ███████║███████╗███████╗██║ ██║██║ ██║███████║ ██║ ██║██║ ██║██╔██╗ ██║███████╗ // ██╔══██║╚════██║╚════██║██║ ██║██║ ██║██╔══██║ ██║ ██║██║ ██║██║╚██╗██║╚════██║ // ██║ ██║███████║███████║╚██████╔╝╚██████╗██║██║ ██║ ██║ ██║╚██████╔╝██║ ╚████║███████║ // ╚═╝ ╚═╝╚══════╝╚══════╝ ╚═════╝ ╚═════╝╚═╝╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═══╝╚══════╝ // describe('associations :: ', function() { describe('one to many :: ', function() { describe('a post request to /:model with an array specified for a collection attribute', function() { it('should return JSON for the new record including the associated collection', function(done) { sailsApp.models.pet.create({name: 'spot'}).meta({fetch: true}).exec(function(err, spot) { sailsApp.request('post /user', {name: 'will', pets: [spot.id]}, function (err, resp, data) { if (err) {return done (err);} assert.equal(data.name, 'will'); assert.equal(data.id, 1); assert.equal(data.pets.length, 1); assert.equal(data.pets[0].name, 'spot'); return done(); }); }); }); }); describe('a get request to /:model/:parentid/:association for a plural association', function() { describe('where a single child instance exists', function() { it('should return JSON for the specified collection of the test model', function(done) { sailsApp.models.pet.create({name: 'spot'}).meta({fetch: true}).exec(function(err, spot) { sailsApp.models.user.create({name: 'will', pets: [spot.id]}).meta({fetch: true}).exec(function(err, will) { if (err) {return done (err);} sailsApp.request('get /user/1/pets', function (err, resp, data) { if (err) {return done (err);} assert.equal(data.length, 1); assert.equal(data[0].name, 'spot'); assert.equal(data[0].id, 1); assert.equal(data[0].owner, 1); return done(); }); }); }); }); }); describe('where a 40 instances exist, and no limit is given', function() { it('should return JSON for 30 records of the specified collection of the test model (since the default limit is 30)', function(done) { var instancesToCreate = _.map(_.range(1,41), function(i) { return { name: 'pet_' + i }; }); sailsApp.models.pet.createEach(instancesToCreate).meta({fetch: true}).exec(function(err, pets) { sailsApp.models.user.create({name: 'will', pets: _.pluck(pets, 'id')}).meta({fetch: true}).exec(function(err, will) { if (err) {return done (err);} sailsApp.request('get /user/1/pets', function (err, resp, data) { if (err) {return done (err);} assert.equal(data.length, 30); return done(); }); }); }); }); }); describe('where a 40 instances exist, and a limit of 35 is given', function() { it('should return JSON for 35 records of the specified collection of the test model', function(done) { var instancesToCreate = _.map(_.range(1,41), function(i) { return { name: 'pet_' + i }; }); sailsApp.models.pet.createEach(instancesToCreate).meta({fetch: true}).exec(function(err, pets) { sailsApp.models.user.create({name: 'will', pets: _.pluck(pets, 'id')}).meta({fetch: true}).exec(function(err, will) { if (err) {return done (err);} sailsApp.request('get /user/1/pets?limit=35', function (err, resp, data) { if (err) {return done (err);} assert.equal(data.length, 35); return done(); }); }); }); }); }); }); describe('a get request to /:model/:parentid/:association for a plural association with no associated records', function() { it('should return JSON for the specified collection of the test model', function(done) { sailsApp.models.user.create({name: 'will'}).meta({fetch: true}).exec(function(err, will) { if (err) {return done (err);} sailsApp.request('get /user/1/pets', function (err, resp, data) { if (err) {return done (err);} assert.equal(data.length, 0); return done(); }); }); }); }); describe('a get request to /:model/:parentid/:association for a singular association', function() { it('should return JSON for the specified collection of the test model', function(done) { sailsApp.models.pet.create({name: 'spot'}).meta({fetch: true}).exec(function(err, spot) { sailsApp.models.user.create({name: 'will', pets: [spot.id]}).meta({fetch: true}).exec(function(err, will) { if (err) {return done (err);} sailsApp.request('get /pet/1/owner', function (err, resp, data) { if (err) {return done (err);} assert.equal(data.name, 'will'); assert.equal(data.id, 1); return done(); }); }); }); }); }); describe('a get request to /:model/:parentid/:association for a singular association with no associated record', function() { it('should return JSON for the specified collection of the test model', function(done) { sailsApp.models.pet.create({name: 'spot'}).meta({fetch: true}).exec(function(err, spot) { sailsApp.request('get /pet/1/owner', function (err, resp, data) { if (err) { if (err.status && err.status === 404) { return done(); } return done(new Error('Should have responded with a 404 error, but instead got:' + util.inspect(err, {depth: null}))); } return done(new Error('Should have responded with a 404 error, but instead got:' + util.inspect(data, {depth: null}))); }); }); }); }); describe('a get request to /:model/:parentid/:association/:id', function() { it('should return a 404', function(done) { sailsApp.models.pet.createEach([{name: 'bubbles'}, {name: 'dempsey'}]).meta({fetch: true}).exec(function(err, pets) { sailsApp.models.user.create({name: 'roger', pets: _.pluck(pets,'id')}).meta({fetch: true}).exec(function(err) { if (err) {return done (err);} sailsApp.request('get /user/1/pets/2', function (err, resp, data) { if (err) { if (err.status && err.status === 404) { return done(); } return done(new Error('Should have responded with a 404 error, but instead got:' + util.inspect(err, {depth: null}))); } return done(new Error('Should have responded with a 404 error, but instead got:' + util.inspect(data, {depth: null}))); }); }); }); }); }); describe('a put request to /:model/:parentid/:association/:id', function() { it('should return JSON for an instance of the test model, with its collection updated', function(done) { sailsApp.models.user.create({name: 'ira'}).exec(function(err) { if (err) {return done (err);} sailsApp.models.pet.create({name: 'flipper'}).exec(function(err) { if (err) {return done (err);} sailsApp.request('put /user/1/pets/1', function (err, resp, data) { if (err) {return done (err);} assert.equal(data.name, 'ira'); assert.equal(data.id, 1); assert.equal(data.pets.length, 1); assert.equal(data.pets[0].name, 'flipper'); sailsApp.models.user.findOne({id: 1}).populate('pets').exec(function(err, user) { if (err) {return done (err);} assert(user); assert.equal(user.name, 'ira'); assert.equal(user.id, 1); assert.equal(user.pets.length, 1); assert.equal(user.pets[0].name, 'flipper'); return done(); }); }); }); }); }); }); describe('a put request to /:model/:parentid/:association (with empty array)', function() { it('should return JSON for an instance of the test model, with its collection replaced', function(done) { sailsApp.models.user.create({name: 'ira', id: 1}).exec(function(err) { if (err) {return done (err);} sailsApp.models.pet.create({name: 'flipper', id: 1, owner: 1}).exec(function(err) { if (err) {return done (err);} sailsApp.request('put /user/1/pets', [], function (err, resp, data) { if (err) {return done (err);} assert.equal(data.name, 'ira'); assert.equal(data.pets.length, 0); sailsApp.models.pet.findOne({id: 1}).populate('owner').exec(function(err, pet) { if (err) {return done (err);} assert(pet); assert.equal(pet.name, 'flipper'); assert.equal(pet.id, 1); assert.equal(pet.owner, null); return done(); }); }); }); }); }); }); describe('a put request to /:model/:parentid/:association (with new array)', function() { it('should return JSON for an instance of the test model, with its collection replaced', function(done) { sailsApp.models.user.create({name: 'zooey'}).exec(function(err) { if (err) {return done (err);} sailsApp.models.pet.createEach([{name: 'ralph', id: 1, owner: 1}, {name: 'fiona', id: 2}]).exec(function(err) { if (err) {return done (err);} sailsApp.request('put /user/1/pets', [2], function (err, resp, data) { if (err) {return done (err);} assert.equal(data.name, 'zooey'); assert.equal(data.pets.length, 1); assert.equal(data.pets[0].id, 2); assert.equal(data.pets[0].name, 'fiona'); sailsApp.models.pet.findOne({id: 2}).populate('owner').exec(function(err, pet) { if (err) {return done (err);} assert(pet); assert.equal(pet.name, 'fiona'); assert.equal(pet.id, 2); assert.equal(pet.owner.name, 'zooey'); assert.equal(pet.owner.id, 1); return done(); }); }); }); }); }); }); describe('a delete request to /:model/:parentid/:association/:id', function() { it('should return JSON for an instance of the test model, with its collection updated', function(done) { sailsApp.models.pet.create({name: 'alice'}).meta({fetch: true}).exec(function(err, alice) { sailsApp.models.user.create({name: 'larry', pets: [alice.id]}).meta({fetch: true}).exec(function(err) { if (err) {return done (err);} sailsApp.request('delete /user/1/pets/1', function (err, resp, data) { if (err) {return done (err);} assert.equal(data.name, 'larry'); assert.equal(data.id, 1); assert.equal(data.pets.length, 0); sailsApp.models.user.findOne({id: 1}).populate('pets').exec(function(err, user) { if (err) {return done (err);} assert(user); assert.equal(user.name, 'larry'); assert.equal(user.id, 1); assert.equal(user.pets.length, 0); return done(); }); }); }); }); }); }); }); describe('many-to-many :: ', function() { describe('a post request to /:model with an array specified for a collection attribute', function() { it('should return JSON for the new record including the associated collection', function(done) { sailsApp.models.pet.create({name: 'spot'}).meta({fetch: true}).exec(function(err, spot) { sailsApp.request('post /user', {name: 'will', animalFriends: [spot.id]}, function (err, resp, data) { if (err) {return done (err);} assert.equal(data.name, 'will'); assert.equal(data.id, 1); assert.equal(data.animalFriends.length, 1); assert.equal(data.animalFriends[0].name, 'spot'); return done(); }); }); }); }); describe('a get request to /:model/:parentid/:association for a plural association', function() { describe('where a single child instance exists', function() { it('should return JSON for the specified collection of the test model', function(done) { sailsApp.models.pet.create({name: 'spot'}).meta({fetch: true}).exec(function(err, spot) { sailsApp.models.user.create({name: 'will', animalFriends: [spot.id]}).meta({fetch: true}).exec(function(err, will) { if (err) {return done (err);} sailsApp.request('get /user/1/animalFriends', function (err, resp, data) { if (err) {return done (err);} assert.equal(data.length, 1); assert.equal(data[0].name, 'spot'); assert.equal(data[0].id, 1); return done(); }); }); }); }); }); describe('where a 40 instances exist, and no limit is given', function() { it('should return JSON for 30 records of the specified collection of the test model (since the default limit is 30)', function(done) { var instancesToCreate = _.map(_.range(1,41), function(i) { return { name: 'pet_' + i }; }); sailsApp.models.pet.createEach(instancesToCreate).meta({fetch: true}).exec(function(err, pets) { sailsApp.models.user.create({name: 'will', animalFriends: _.pluck(pets, 'id')}).meta({fetch: true}).exec(function(err, will) { if (err) {return done (err);} sailsApp.request('get /user/1/animalFriends', function (err, resp, data) { if (err) {return done (err);} assert.equal(data.length, 30); return done(); }); }); }); }); }); describe('where a 40 instances exist, and a limit of 35 is given', function() { it('should return JSON for 35 records of the specified collection of the test model', function(done) { var instancesToCreate = _.map(_.range(1,41), function(i) { return { name: 'pet_' + i }; }); sailsApp.models.pet.createEach(instancesToCreate).meta({fetch: true}).exec(function(err, pets) { sailsApp.models.user.create({name: 'will', animalFriends: _.pluck(pets, 'id')}).meta({fetch: true}).exec(function(err, will) { if (err) {return done (err);} sailsApp.request('get /user/1/animalFriends?limit=35', function (err, resp, data) { if (err) {return done (err);} assert.equal(data.length, 35); return done(); }); }); }); }); }); }); describe('a get request to /:model/:parentid/:association for a plural association with no associated records', function() { it('should return JSON for the specified collection of the test model', function(done) { sailsApp.models.user.create({name: 'will'}).meta({fetch: true}).exec(function(err, will) { if (err) {return done (err);} sailsApp.request('get /user/1/animalFriends', function (err, resp, data) { if (err) {return done (err);} assert.equal(data.length, 0); return done(); }); }); }); }); describe('a put request to /:model/:parentid/:association/:id', function() { it('should return JSON for an instance of the test model, with its collection updated', function(done) { sailsApp.models.user.create({name: 'ira'}).exec(function(err) { if (err) {return done (err);} sailsApp.models.pet.create({name: 'flipper'}).exec(function(err) { if (err) {return done (err);} sailsApp.request('put /user/1/animalFriends/1', function (err, resp, data) { if (err) {return done (err);} assert.equal(data.name, 'ira'); assert.equal(data.id, 1); assert.equal(data.animalFriends.length, 1); assert.equal(data.animalFriends[0].name, 'flipper'); sailsApp.models.user.findOne({id: 1}).populate('animalFriends').exec(function(err, user) { if (err) {return done (err);} assert(user); assert.equal(user.name, 'ira'); assert.equal(user.id, 1); assert.equal(user.animalFriends.length, 1); assert.equal(user.animalFriends[0].name, 'flipper'); return done(); }); }); }); }); }); }); describe('a put request to /:model/:parentid/:association (with empty array)', function() { it('should return JSON for an instance of the test model, with its collection replaced', function(done) { sailsApp.models.user.create({name: 'ira', id: 1}).exec(function(err) { if (err) {return done (err);} sailsApp.models.pet.create({name: 'flipper', id: 1, humanFriends: [1]}).exec(function(err) { if (err) {return done (err);} sailsApp.request('put /user/1/animalFriends', [], function (err, resp, data) { if (err) {return done (err);} assert.equal(data.name, 'ira'); assert.equal(data.animalFriends.length, 0); sailsApp.models.pet.findOne({id: 1}).populate('humanFriends').exec(function(err, pet) { if (err) {return done (err);} assert(pet); assert.equal(pet.name, 'flipper'); assert.equal(pet.id, 1); assert.equal(pet.humanFriends.length, 0); return done(); }); }); }); }); }); }); describe('a put request to /:model/:parentid/:association (with new array)', function() { it('should return JSON for an instance of the test model, with its collection replaced', function(done) { sailsApp.models.user.create({name: 'zooey'}).exec(function(err) { if (err) {return done (err);} sailsApp.models.pet.createEach([{name: 'ralph', id: 1, humanFriends: [1]}, {name: 'fiona', id: 2}]).exec(function(err) { if (err) {return done (err);} sailsApp.request('put /user/1/animalFriends', [2], function (err, resp, data) { if (err) {return done (err);} assert.equal(data.name, 'zooey'); assert.equal(data.animalFriends.length, 1); assert.equal(data.animalFriends[0].id, 2); assert.equal(data.animalFriends[0].name, 'fiona'); sailsApp.models.pet.findOne({id: 2}).populate('humanFriends').exec(function(err, pet) { if (err) {return done (err);} assert(pet); assert.equal(pet.name, 'fiona'); assert.equal(pet.id, 2); assert.equal(pet.humanFriends.length, 1); assert.equal(pet.humanFriends[0].id, 1); return done(); }); }); }); }); }); }); describe('a delete request to /:model/:parentid/:association/:id', function() { it('should return JSON for an instance of the test model, with its collection updated', function(done) { sailsApp.models.pet.create({name: 'alice'}).meta({fetch: true}).exec(function(err, alice) { sailsApp.models.user.create({name: 'larry', animalFriends: [alice.id]}).meta({fetch: true}).exec(function(err) { if (err) {return done (err);} sailsApp.request('delete /user/1/animalFriends/1', function (err, resp, data) { if (err) {return done (err);} assert.equal(data.name, 'larry'); assert.equal(data.id, 1); assert.equal(data.animalFriends.length, 0); sailsApp.models.user.findOne({id: 1}).populate('animalFriends').exec(function(err, user) { if (err) {return done (err);} assert(user); assert.equal(user.name, 'larry'); assert.equal(user.id, 1); assert.equal(user.animalFriends.length, 0); return done(); }); }); }); }); }); }); }); }); describe('with a custom parseBlueprintOptions for all blueprints', function() { before(function() { extraSailsConfig = { blueprints: { parseBlueprintOptions: function(req) { var queryOptions = req._sails.hooks.blueprints.parseBlueprintOptions(req); if (queryOptions.populates.pets) { queryOptions.populates.pets.limit = 1; } return queryOptions; } }, routes: { 'GET /yolo/:id': 'user/findOne', }, orm: { moduleDefinitions: { models: { user: { attributes: { name: 'string', pets: { collection: 'pet', via: 'owner' } } }, pet: { attributes: { name: 'string', owner: { model: 'user' } } } }, } } }; }); after(function() { extraSailsConfig = {}; }); it('the custom `parseBlueprintOptions` should be applied to the `find` blueprint', function(done) { sailsApp.models.pet.createEach([{name: 'alice'}, {name: 'rex'}]).meta({fetch: true}).exec(function(err, pets) { if (err) {return done(err);} sailsApp.models.user.create({name: 'bill', pets: _.pluck(pets, sailsApp.models.pet.primaryKey)}).exec(function(err, bill) { if (err) {return done(err);} sailsApp.request('get /user/' + bill[sailsApp.models.user.primaryKey], function (err, resp, data) { if (err) {return done (err);} assert.equal(data.name, 'bill'); assert(data.pets, 'Record should have `pets` key, but none was found. Full record: ' + util.inspect(data, {depth: null})); assert.equal(data.pets.length, 1); return done(); }); }); }); }); it('the custom `parseBlueprintOptions` should be applied to the `create` blueprint', function(done) { sailsApp.models.pet.createEach([{name: 'june'}, {name: 'jane'}]).meta({fetch: true}).exec(function(err, pets) { if (err) {return done(err);} sailsApp.request('post /user', {name: 'bob', pets: _.pluck(pets, sailsApp.models.pet.primaryKey)}, function (err, resp, data) { if (err) {return done (err);} assert.equal(data.name, 'bob'); assert(data.pets, 'Record should have `pets` key, but none was found. Full record: ' + util.inspect(data, {depth: null})); assert.equal(data.pets.length, 1); return done(); }); }); }); it('the custom `parseBlueprintOptions` should be applied to a user-defined (i.e. not shadow) route', function(done) { sailsApp.models.pet.createEach([{name: 'lolly'}, {name: 'dolly'}]).meta({fetch: true}).exec(function(err, pets) { if (err) {return done(err);} sailsApp.models.user.create({name: 'bruce', pets: _.pluck(pets, sailsApp.models.pet.primaryKey)}).exec(function(err, bruce) { if (err) {return done(err);} sailsApp.request('get /yolo/' + bruce[sailsApp.models.user.primaryKey], function (err, resp, data) { if (err) {return done (err);} assert.equal(data.name, 'bruce'); assert(data.pets, 'Record should have `pets` key, but none was found. Full record: ' + util.inspect(data, {depth: null})); assert.equal(data.pets.length, 1); return done(); }); }); }); }); }); describe('with a custom parseBlueprintOptions that disables auto-population (tests #4138)', function() { before(function() { extraSailsConfig = { hooks: { pubsub: undefined }, blueprints: { parseBlueprintOptions: function(req) { var queryOptions = req._sails.hooks.blueprints.parseBlueprintOptions(req); if (!req.param('populate', false) && !queryOptions.alias) { queryOptions.populates = {}; } return queryOptions; } }, orm: { moduleDefinitions: { models: { user: { attributes: { name: 'string', pets: { collection: 'pet', via: 'owner' } } }, pet: { attributes: { name: 'string', owner: { model: 'user' } } } }, } } }; }); after(function() { extraSailsConfig = {}; }); it('the delete blueprint should not cause any errors', function(done) { sailsApp.models.pet.createEach([{name: 'alice'}, {name: 'rex'}]).meta({fetch: true}).exec(function(err, pets) { if (err) {return done(err);} sailsApp.models.user.create({name: 'bill', pets: _.pluck(pets, sailsApp.models.pet.primaryKey)}).exec(function(err, bill) { if (err) {return done(err);} sailsApp.request('delete /user/' + bill[sailsApp.models.user.primaryKey], function (err, resp, data) { if (err) {return done (err);} return done(); }); }); }); }); }); describe('with a custom parseBlueprintOptions for a specific route', function() { before(function() { extraSailsConfig = { routes: { 'GET /user/:id': { action: 'user/findOne', parseBlueprintOptions: function(req) { var queryOptions = req._sails.hooks.blueprints.parseBlueprintOptions(req); queryOptions.populates.pets.limit = 1; return queryOptions; } } }, orm: { moduleDefinitions: { models: { user: { attributes: { name: 'string', pets: { collection: 'pet', via: 'owner' } } }, pet: { attributes: { name: 'string', owner: { model: 'user' } } } }, } } }; }); after(function() { extraSailsConfig = {}; }); it('the custom `parseBlueprintOptions` should be applied to the specific route', function(done) { sailsApp.models.pet.createEach([{name: 'alice'}, {name: 'rex'}]).meta({fetch: true}).exec(function(err, pets) { if (err) {return done(err);} sailsApp.models.user.create({name: 'bill', pets: _.pluck(pets, sailsApp.models.pet.primaryKey)}).exec(function(err, bill) { if (err) {return done(err);} sailsApp.request('get /user/' + bill[sailsApp.models.user.primaryKey], function (err, resp, data) { if (err) {return done (err);} assert.equal(data.name, 'bill'); assert(data.pets, 'Record should have `pets` key, but none was found. Full record: ' + util.inspect(data, {depth: null})); assert.equal(data.pets.length, 1); return done(); }); }); }); }); it('the custom `parseBlueprintOptions` should NOT be applied to a different route blueprint', function(done) { sailsApp.models.pet.createEach([{name: 'june'}, {name: 'jane'}]).meta({fetch: true}).exec(function(err, pets) { if (err) {return done(err);} sailsApp.request('post /user', {name: 'bob', pets: _.pluck(pets, sailsApp.models.pet.primaryKey)}, function (err, resp, data) { if (err) {return done (err);} assert.equal(data.name, 'bob'); assert(data.pets, 'Record should have `pets` key, but none was found. Full record: ' + util.inspect(data, {depth: null})); assert.equal(data.pets.length, 2); return done(); }); }); }); }); }); describe('using query string params :: ', function() { describe('with the `find` blueprint :: ', function() { describe('filtering :: ', function() { before(function() { extraSailsConfig = { orm: { moduleDefinitions: { models: { user: { attributes: { name: 'string' } } } } } }; }); after(function() { extraSailsConfig = {}; }); it('a get request to /:model?name=scott should respond with the correctly filtered instances', function(done) { sailsApp.models.user.createEach([{name: 'scott'}, {name: 'mike'}]).exec(function(err) { if (err) {return done(err);} sailsApp.request('get /user?name=scott', function (err, resp, data) { assert(!err, err); assert.equal(data.length, 1); assert.equal(data[0].name, 'scott'); done(); }); }); }); it('a get request to /:model?where={...} should respond with the correctly filtered instances', function(done) { sailsApp.models.user.createEach([{name: 'scott'}, {name: 'mike'}, {name: 'rachael'}, {name: 'cody'}, {name: 'irl'}]).exec(function(err) { if (err) {return done(err);} sailsApp.request('get /user?where={"name": {">": "irl"}}', function (err, resp, data) { assert(!err, err); assert.equal(data.length, 3); var names = _.pluck(data, 'name'); assert(_.contains(names, 'scott')); assert(_.contains(names, 'mike')); assert(_.contains(names, 'rachael')); done(); }); }); }); }); describe('using sort, skip and limit in the query string :: ', function() { before(function() { extraSailsConfig = { orm: { moduleDefinitions: { models: { user: { attributes: { name: 'string' } } } } } }; }); after(function() { extraSailsConfig = {}; }); it('a get request to /:model?sort=name%20asc&limit=2&skip=1 should respond with the correctly filtered instances', function(done) { sailsApp.models.user.createEach([{name: 'scott'}, {name: 'mike'}, {name: 'rachael'}, {name: 'cody'}, {name: 'irl'}]).exec(function(err) { if (err) {return done(err);} sailsApp.request('get /user?sort=name%20asc&limit=2&skip=1', function (err, resp, data) { assert(!err, err); assert.equal(data.length, 2); assert.equal(data[0].name, 'irl'); assert.equal(data[1].name, 'mike'); done(); }); }); }); it('a get request to /:model?sort=name%20desc&limit=2&skip=1 should respond with the correctly filtered instances', function(done) { sailsApp.models.user.createEach([{name: 'scott'}, {name: 'mike'}, {name: 'rachael'}, {name: 'cody'}, {name: 'irl'}]).exec(function(err) { if (err) {return done(err);} sailsApp.request('get /user?sort=name%20desc&limit=2&skip=1', function (err, resp, data) { assert(!err, err); assert.equal(data.length, 2); assert.equal(data[0].name, 'rachael'); assert.equal(data[1].name, 'mike'); done(); }); }); }); it('a get request to /:model?sort={"name":-1}&limit=2&skip=1 should respond with the correctly filtered instances', function(done) { sailsApp.models.user.createEach([{name: 'scott'}, {name: 'mike'}, {name: 'rachael'}, {name: 'cody'}, {name: 'irl'}]).exec(function(err) { if (err) {return done(err);} sailsApp.request('get /user?sort={"name":-1}&limit=2&skip=1', function (err, resp, data) { assert(!err, err); assert.equal(data.length, 2); assert.equal(data[0].name, 'rachael'); assert.equal(data[1].name, 'mike'); done(); }); }); }); }); describe('using `select` and `omit` in the query string', function() { before(function() { extraSailsConfig = { orm: { moduleDefinitions: { models: { user: { attributes: { name: 'string', favoriteColor: 'string', luckyNumber: 'number' } } } } } }; }); after(function() { extraSailsConfig = {}; }); it('a get request to /:model?select=name, luckyNumber should respond with the correctly projected instances', function(done) { sailsApp.models.user.createEach([ {name: 'scott', favoriteColor: 'grey', luckyNumber: 3}, {name: 'mike', favoriteColor: 'blue', luckyNumber: 25}, {name: 'rachael', favoriteColor: 'red', luckyNumber: 12}, {name: 'cody', favoriteColor: 'blue', luckyNumber: 9}, {name: 'irl', favoriteColor: 'black', luckyNumber: 66} ]).exec(function(err) { if (err) {return done(err);} sailsApp.request('get /user?select=name, luckyNumber', function (err, resp, data) { assert(!err, err); assert.equal(data.length, 5); _.each(data, function(row) { assert(row.name); assert(row.luckyNumber); assert(!row.favoriteColor, 'Got favoriteColor for `' + row.name + '`, even though it wasn\'t selected!'); }); done(); }); }); }); it('a get request to /:model?omit=favoriteColor, luckyNumber should respond with the correctly projected instances', function(done) { sailsApp.models.user.createEach([ {name: 'scott', favoriteColor: 'grey', luckyNumber: 3}, {name: 'mike', favoriteColor: 'blue', luckyNumber: 25}, {name: 'rachael', favoriteColor: 'red', luckyNumber: 12}, {name: 'cody', favoriteColor: 'blue', luckyNumber: 9}, {name: 'irl', favoriteColor: 'black', luckyNumber: 66} ]).exec(function(err) { if (err) {return done(err);} sailsApp.request('get /user?omit=favoriteColor, luckyNumber', function (err, resp, data) { assert(!err, err); assert.equal(data.length, 5); _.each(data, function(row) { assert(row.name); assert(!row.luckyNumber, 'Got luckyNumber for `' + row.name + '`, even though it was omitted!'); assert(!row.favoriteColor, 'Got favoriteColor for `' + row.name + '`, even though it was omitted!'); }); done(); }); }); }); }); }); describe('with the `populate` blueprint :: ', function() { describe('filtering :: ', function() { before(function() { extraSailsConfig = { orm: { moduleDefinitions: { models: { user: { attributes: { name: 'string', pets: { collection: 'pet', via: 'owner' } } }, pet: { attributes: { name: 'string', owner: { model: 'user' } } } } } } }; }); after(function() { extraSailsConfig = {}; }); it('a get request to /:model/:id/:association?name=alice should respond with the correctly filtered association records', function(done) { sailsApp.models.user.create({name: 'scott'}).meta({fetch: true}).exec(function(err, scott) { sailsApp.models.pet.createEach([{name: 'alice', owner: scott.id}, {name: 'mojo', owner: scott.id}]).exec(function(err) { if (err) {return done(err);} sailsApp.request('get /user/'+scott.id+'/pets?name=alice', function (err, resp, data) { assert(!err, err); assert.equal(data.length, 1); assert.equal(data[0].name, 'alice'); done(); }); }); }); }); it('a get request to /:model/:id/:association?where={...} should respond with the correctly filtered association records', function(done) { sailsApp.models.user.create({name: 'scott'}).meta({fetch: true}).exec(function(err, scott) { sailsApp.models.pet.createEach([{name: 'alice', owner: scott.id}, {name: 'mojo', owner: scott.id}, {name: 'bert', owner: scott.id}]).exec(function(err) { if (err) {return done(err);} sailsApp.request('get /user/'+scott.id+'/pets?where={"name":{">":"alice"}}', function (err, resp, data) { assert(!err, err); assert.equal(data.length, 2); var names = _.pluck(data, 'name'); assert(_.contains(names, 'bert')); assert(_.contains(names, 'mojo')); done(); }); }); }); }); }); describe('using sort, skip and limit in the query string :: ', function() { before(function() { extraSailsConfig = { orm: { moduleDefinitions: { models: { user: { attributes: { name: 'string', pets: { collection: 'pet', via: 'owner' } } }, pet: { attributes: { name: 'string', owner: { model: 'user' } } } } } } }; }); after(function() { extraSailsConfig = {}; }); it('a get request to /:model/:id/:association?sort=name%20asc&limit=2&skip=1 should respond with the correctly filtered association records', function(done) { sailsApp.models.user.create({name: 'scott'}).meta({fetch: true}).exec(function(err, scott) { sailsApp.models.pet.createEach([{name: 'alice', owner: scott.id}, {name: 'mojo', owner: scott.id}, {name: 'bert', owner: scott.id}, {name: 'bandit', owner: scott.id}]).exec(function(err) { if (err) {return done(err);} sailsApp.request('get /user/'+scott.id+'/pets?sort=name%20asc&limit=2&skip=1', function (err, resp, data) { assert(!err, err); assert.equal(data.length, 2); var names = _.pluck(data, 'name'); assert(_.contains(names, 'bandit')); assert(_.contains(names, 'bert')); done(); }); }); }); }); it('a get request to /:model/:id/:association?sort=name%desc&limit=1&skip=1 should respond with the correctly filtered association records', function(done) { sailsApp.models.user.create({name: 'scott'}).meta({fetch: true}).exec(function(err, scott) { sailsApp.models.pet.createEach([{name: 'alice', owner: scott.id}, {name: 'mojo', owner: scott.id}, {name: 'bert', owner: scott.id}, {name: 'bandit', owner: scott.id}]).exec(function(err) { if (err) {return done(err);} sailsApp.request('get /user/'+scott.id+'/pets?sort=name%20desc&limit=1&skip=1', function (err, resp, data) { assert(!err, err); assert.equal(data.length, 1); var names = _.pluck(data, 'name'); assert(_.contains(names, 'bert')); done(); }); }); }); }); it('a get request to /:model/:id/:association?sort={"name":-1}&limit=2&skip=1 should respond with the correctly filtered association records', function(done) { sailsApp.models.user.create({name: 'scott'}).meta({fetch: true}).exec(function(err, scott) { sailsApp.models.pet.createEach([{name: 'alice', owner: scott.id}, {name: 'mojo', owner: scott.id}, {name: 'bert', owner: scott.id}, {name: 'bandit', owner: scott.id}]).exec(function(err) { if (err) {return done(err);} sailsApp.request('get /user/'+scott.id+'/pets?sort={"name":-1}&limit=1&skip=1', function (err, resp, data) { assert(!err, err); assert.equal(data.length, 1); var names = _.pluck(data, 'name'); assert(_.contains(names, 'bert')); done(); }); }); }); }); }); describe('using `select` and `omit` in the query string', function() { before(function() { extraSailsConfig = { orm: { moduleDefinitions: { models: { user: { attributes: { name: 'string', pets: { collection: 'pet', via: 'owner' } } }, pet: { attributes: { name: 'string', animal: 'string', age: 'number', owner: { model: 'user' } } } } } } }; }); after(function() { extraSailsConfig = {}; }); it('a get request to /:model/:id/:association?select=name, luckyNumber should respond with the correctly projected association records', function(done) { sailsApp.models.user.create({name: 'scott'}).meta({fetch: true}).exec(function(err, scott) { sailsApp.models.pet.createEach([ {name: 'alice', animal: 'cat', age: 3, owner: scott.id}, {name: 'bandit', animal: 'dog', age: 25, owner: scott.id}, {name: 'bert', animal: 'cat', age: 12, owner: scott.id}, {name: 'mojo', animal: 'cat', age: 9, owner: scott.id}, {name: 'rex', animal: 'cheetah', age: 66, owner: scott.id} ]).exec(function(err) { if (err) {return done(err);} sailsApp.request('get /user/' + scott.id + '/pets?select=name, age', function (err, resp, data) { assert(!err, err); assert.equal(data.length, 5); _.each(data, function(row) { assert(row.name); assert(row.age); assert(!row.animal, 'Got animal for `' + row.name + '`, even though it wasn\'t selected!'); }); done(); }); }); }); }); it('a get request to /:model/:id/:association?omit=age, animal should respond with the correctly projected association records', function(done) { sailsApp.models.user.create({name: 'scott'}).meta({fetch: true}).exec(function(err, scott) { sailsApp.models.pet.createEach([ {name: 'alice', animal: 'cat', age: 3, owner: scott.id}, {name: 'bandit', animal: 'dog', age: 25, owner: scott.id}, {name: 'bert', animal: 'cat', age: 12, owner: scott.id}, {name: 'mojo', animal: 'cat', age: 9, owner: scott.id}, {name: 'rex', animal: 'cheetah', age: 66, owner: scott.id} ]).exec(function(err) { if (err) {return done(err);} sailsApp.request('get /user/' + scott.id + '/pets?omit=age, animal', function (err, resp, data) { assert(!err, err); assert.equal(data.length, 5); _.each(data, function(row) { assert(row.name); assert(!row.age, 'Got age for `' + row.name + '`, even though it was omitted!'); assert(!row.animal, 'Got animal for `' + row.name + '`, even though it was omitted!'); }); done(); }); }); }); }); }); }); }); describe('after reloading actions :: ', function() { before(function() { extraSailsConfig = { orm: { moduleDefinitions: { models: { user: { attributes: { name: 'string' } } } } } }; }); after(function() { extraSailsConfig = {}; }); it('should still respond to RESTful blueprint requests correctly :: ', function(done) { sailsApp.models.user.createEach([{name: 'scott'}, {name: 'mike'}]).exec(function(err) { if (err) {return done(err);} sailsApp.reloadActions(function(err) { if (err) {return done(err);} sailsApp.request('get /user', function (err, resp, data) { assert(!err, err); assert.equal(data.length, 2); done(); }); }); }); }); }); describe('with pluralize turned on :: ', function() { before(function() { extraSailsConfig = { blueprints: { pluralize: true }, orm: { moduleDefinitions: { models: { user: {}, quiz: {} } } } }; }); after(function() { extraSailsConfig = {}; }); it('should bind blueprint actions to plural controller names', function(done) { sailsApp.models.user.create({}).exec(function(err) { if (err) {return done (err);} sailsApp.request('get /users', function (err, resp, data) { assert(!err, err); assert.equal(data.length, 1); assert.equal(data[0].id, 1); done(); }); }); }); it('should bind blueprint actions to plural controller names (quiz => quizzes)', function(done) { sailsApp.models.quiz.create({}).exec(function(err) { if (err) {return done (err);} sailsApp.request('get /quizzes', function (err, resp, data) { assert(!err, err); assert.equal(data.length, 1); assert.equal(data[0].id, 1); done(); }); }); }); it('should not bind blueprint actions to singular controller names', function(done) { sailsApp.models.user.create({}).exec(function(err) { if (err) {return done (err);} sailsApp.request('get /user', function (err, resp, data) { assert(err); assert.equal(err.status, 404); done(); }); }); }); }); describe('with `prefix` option set to \'/api\' :: ', function() { before(function() { extraSailsConfig = { blueprints: { prefix: '/api' }, orm: { moduleDefinitions: { models: { user: { attributes: { name: 'string' } } } } } }; }); after(function() { extraSailsConfig = {}; }); describe('a get request to /api/:model', function() { it('should return JSON for all of the instances of the test model', function(done) { sailsApp.models.user.create({name: 'joy'}).exec(function(err) { if (err) {return done (err);} sailsApp.request('get /api/user', function (err, resp, data) { assert(!err, err); assert.equal(data.length, 1); assert.equal(data[0].name, 'joy'); assert.equal(data[0].id, 1); done(); }); }); }); }); describe('a get request to /:model', function() { it('should return a 404', function(done) { sailsApp.request('get /user', function (err, resp, data) { assert(err); assert.equal(err.status, 404); done(); }); }); }); }); describe('with `restPrefix` option set to \'/v1\' :: ', function() { before(function() { extraSailsConfig = { blueprints: { restPrefix: '/v1' }, orm: { moduleDefinitions: { models: { user: { attributes: { name: 'string' } } } } } }; }); after(function() { extraSailsConfig = {}; }); describe('a get request to /v1/:model', function() { it('should return JSON for all of the instances of the test model', function(done) { sailsApp.models.user.create({name: 'wanda'}).exec(function(err) { if (err) {return done (err);} sailsApp.request('get /v1/user', function (err, resp, data) { assert(!err, err); assert.equal(data.length, 1); assert.equal(data[0].name, 'wanda'); assert.equal(data[0].id, 1); done(); }); }); }); }); describe('a get request to /:model', function() { it('should return a 404', function(done) { sailsApp.request('get /user', function (err, resp, data) { assert(err); assert.equal(err.status, 404); done(); }); }); }); }); describe('with `prefix` option set to \'api\' and `restPrefix` option set to \'/v1\' :: ', function() { before(function() { extraSailsConfig = { blueprints: { prefix: '/api', restPrefix: '/v1' }, orm: { moduleDefinitions: { models: { user: { attributes: { name: 'string' } } } } } }; }); after(function() { extraSailsConfig = {}; }); describe('a get request to /api/v1/:model', function() { it('should return JSON for all of the instances of the test model', function(done) { sailsApp.models.user.create({name: 'ron'}).exec(function(err) { if (err) {return done (err);} sailsApp.request('get /api/v1/user', function (err, resp, data) { assert(!err, err); assert.equal(data.length, 1); assert.equal(data[0].name, 'ron'); assert.equal(data[0].id, 1); done(); }); }); }); }); describe('a get request to /:model', function() { it('should return a 404', function(done) { sailsApp.request('get /user', function (err, resp, data) { assert(err); assert.equal(err.status, 404); done(); }); }); }); describe('a get request to /api/:model', function() { it('should return a 404', function(done) { sailsApp.request('get /api/user', function (err, resp, data) { assert(err); assert.equal(err.status, 404); done(); }); }); }); describe('a get request to /v1/:model', function() { it('should return a 404', function(done) { sailsApp.request('get /v1/user', function (err, resp, data) { assert(err); assert.equal(err.status, 404); done(); }); }); }); }); describe('overriding blueprints :: ', function() { before(function() { extraSailsConfig = { orm: { moduleDefinitions: { models: { user: {}, }, } }, controllers: { moduleDefinitions: { 'user/find': function(req, res) { return res.send('find dem users!'); } } } }; }); after(function() { extraSailsConfig = {}; }); it('if a `:model.find` action is explicitly added, it should be used in response to `GET /:model`', function(done) { sailsApp.models.user.create({name: 'al'}).exec(function(err) { if (err) {return done (err);} sailsApp.request('get /user', function (err, resp, data) { assert(!err, err); assert.equal(data, 'find dem users!'); done(); }); }); }); }); }); }); }); ================================================ FILE: test/integration/hook.blueprints.shortcut.routes.test.js ================================================ /** * Test dependencies */ var util = require('util'); var assert = require('assert'); var tmp = require('tmp'); var _ = require('@sailshq/lodash'); var Filesystem = require('machinepack-fs'); var appHelper = require('./helpers/appHelper'); var Sails = require('../../lib').constructor; /** * Errors */ var Err = { badResponse: function(response) { return 'Wrong server response! Response :::\n' + util.inspect(response.body); } }; describe('blueprints :: ', function() { var curDir, tmpDir, sailsApp; var extraSailsConfig = {}; describe('shortcut routes :: ', function() { describe('when turned off globaly :: ', function() { before(function(done) { // Cache the current working directory. curDir = process.cwd(); // Create a temp directory. tmpDir = tmp.dirSync({gracefulCleanup: true, unsafeCleanup: true}); // Switch to the temp directory. process.chdir(tmpDir.name); appHelper.linkDeps(tmpDir.name); (new Sails()).load({ hooks: { grunt: false, views: false, policies: false, pubsub: false, i18n: false }, orm: { moduleDefinitions: { models: { user: { attributes: { name: 'string', pets: { collection: 'pet', via: 'owner' } } }, pet: { attributes: { name: 'string', owner: { model: 'user' } } } }, } }, models: { migrate: 'drop', schema: true, attributes: { createdAt: { type: 'number', autoCreatedAt: true, }, updatedAt: { type: 'number', autoUpdatedAt: true, }, // id: { type: 'string', unique: true, columnName: '_id'}, id: { type: 'number', autoIncrement: true} } }, blueprints: { rest: false, shortcuts: false, actions: false }, log: {level: 'error'} }, function(err, _sails) { if (err) { return done(err); } sailsApp = _sails; return done(); }); }); after(function(done) { extraSailsConfig = {}; sailsApp.lower(function() { process.chdir(curDir); return done(); }); }); it('a get request to /:model/find should return a 404', function(done) { sailsApp.models.user.create({name: 'al'}).exec(function(err) { if (err) {return done (err);} sailsApp.request('get /user/find', function (err, resp, data) { assert(err); assert.equal(err.status, 404); done(); }); }); }); }); describe('when turned off for a specific controller :: ', function() { before(function(done) { // Cache the current working directory. curDir = process.cwd(); // Create a temp directory. tmpDir = tmp.dirSync({gracefulCleanup: true, unsafeCleanup: true}); // Switch to the temp directory. process.chdir(tmpDir.name); appHelper.linkDeps(tmpDir.name); Filesystem.writeSync({ force: true, destination: 'api/controllers/UserController.js', string: 'module.exports = { _config: { shortcuts: false } }' }).execSync(); (new Sails()).load({ hooks: { grunt: false, views: false, policies: false, pubsub: false, i18n: false }, orm: { moduleDefinitions: { models: { user: { attributes: { name: 'string', pets: { collection: 'pet', via: 'owner' } } }, pet: { attributes: { name: 'string', owner: { model: 'user' } } } }, } }, models: { migrate: 'drop', schema: true, attributes: { createdAt: { type: 'number', autoCreatedAt: true, }, updatedAt: { type: 'number', autoUpdatedAt: true, }, // id: { type: 'string', unique: true, columnName: '_id'}, id: { type: 'number', autoIncrement: true} } }, blueprints: { rest: false, actions: false }, log: {level: 'error'} }, function(err, _sails) { if (err) { return done(err); } sailsApp = _sails; return done(); }); }); after(function(done) { extraSailsConfig = {}; sailsApp.lower(function() { process.chdir(curDir); return done(); }); }); it('a get request to the /:model/find with REST disabled should return a 404', function(done) { sailsApp.models.user.create({name: 'al'}).exec(function(err) { if (err) {return done (err);} sailsApp.request('get /user/find', function (err, resp, data) { assert(err); assert.equal(err.status, 404); done(); }); }); }); it('a get request to the /:model/find with REST enabled should return JSON for all of the instances of the test model', function(done) { sailsApp.models.pet.create({name: 'rex'}).exec(function(err) { if (err) {return done (err);} sailsApp.request('get /pet/find', function (err, resp, data) { assert(!err, err); assert.equal(data.length, 1); assert.equal(data[0].name, 'rex'); assert.equal(data[0].id, 1); done(); }); }); }); }); describe('when turned on :: ', function() { beforeEach(function(done) { // Cache the current working directory. curDir = process.cwd(); // Create a temp directory. tmpDir = tmp.dirSync({gracefulCleanup: true, unsafeCleanup: true}); // Switch to the temp directory. process.chdir(tmpDir.name); appHelper.linkDeps(tmpDir.name); (new Sails()).load(_.merge({ hooks: { grunt: false, views: false, policies: false, pubsub: false, i18n: false }, orm: { moduleDefinitions: { models: { 'user': {} } } }, models: { migrate: 'drop', schema: true, attributes: { createdAt: { type: 'number', autoCreatedAt: true, }, updatedAt: { type: 'number', autoUpdatedAt: true, }, // id: { type: 'string', unique: true, columnName: '_id'}, id: { type: 'number', autoIncrement: true} } }, blueprints: { rest: false, actions: false }, log: {level: 'error'} }, extraSailsConfig), function(err, _sails) { if (err) { return done(err); } sailsApp = _sails; return done(); }); }); afterEach(function(done) { sailsApp.lower(function() { process.chdir(curDir); return done(); }); }); describe('basic usage :: ', function() { before(function() { extraSailsConfig = { orm: { moduleDefinitions: { models: { user: { attributes: { name: 'string', pets: { collection: 'pet', via: 'owner' } } }, pet: { attributes: { name: 'string', owner: { model: 'user' } } } }, } } }; }); after(function() { extraSailsConfig = {}; }); describe('a get request to /:model/find', function() { it('should return JSON for all of the instances of the test model', function(done) { sailsApp.models.user.create({name: 'al'}).exec(function(err) { if (err) {return done (err);} sailsApp.request('get /user/find', function (err, resp, data) { assert(!err, err); assert.equal(data.length, 1); assert.equal(data[0].name, 'al'); assert.equal(data[0].id, 1); done(); }); }); }); }); describe('a get request to /:model/find/:id', function() { it('should return JSON for the requested instance of the test model', function(done) { sailsApp.models.user.create({name: 'ron'}).exec(function(err) { if (err) {return done (err);} sailsApp.request('get /user/find/1', function (err, resp, data) { assert(!err, err); assert.equal(data.name, 'ron'); assert.equal(data.id, 1); done(); }); }); }); }); describe('a get request to /:model/update/:id?name=foo', function() { it('should return JSON for an updated instance of the test model', function(done) { sailsApp.models.user.create({name: 'dave'}).exec(function(err) { if (err) {return done (err);} sailsApp.request('get /user/update/1?name=bob', function (err, resp, data) { if (err) {return done (err);} assert.equal(data.name, 'bob'); assert.equal(data.id, 1); sailsApp.models.user.findOne({id: 1}).exec(function(err, user) { if (err) {return done (err);} assert(user); assert.equal(user.name, 'bob'); return done(); }); }); }); }); }); describe('a get request to /:model/create?name=foo', function() { it('should return JSON for a newly created instance of the test model', function(done) { sailsApp.request('get /user/create?name=joe', function (err, resp, data) { assert(!err, err); assert.equal(data.name, 'joe'); assert.equal(data.id, 1); sailsApp.models.user.findOne({id: 1}).exec(function(err, user) { if (err) {return done (err);} assert(user); assert.equal(user.name, 'joe'); return done(); }); }); }); }); describe('a get request to /:model/create?name=foo&pets=[1]', function() { it('should return JSON for the new record including the associated collection', function(done) { sailsApp.models.pet.create({name: 'spot'}).meta({fetch: true}).exec(function(err, spot) { sailsApp.request('get /user/create?name=will&pets=[1]', function (err, resp, data) { if (err) {return done (err);} assert.equal(data.name, 'will'); assert.equal(data.id, 1); assert.equal(data.pets.length, 1); assert.equal(data.pets[0].name, 'spot'); return done(); }); }); }); }); describe('a get request to /:model/destroy/1', function() { it('should return JSON for the deleted instance of the test model', function(done) { sailsApp.models.user.create({name: 'bubba'}).exec(function(err) { if (err) {return done (err);} sailsApp.request('get /user/destroy/1', function (err, resp, data) { assert(!err, err); assert.equal(data.name, 'bubba'); assert.equal(data.id, 1); sailsApp.models.user.findOne({id: 1}).exec(function(err, user) { if (err) {return done (err);} assert(!user); return done(); }); }); }); }); }); describe('associations :: ', function() { describe('a get request to /:model/:parentid/:association', function() { it('should return JSON for the specified collection of the test model', function(done) { sailsApp.models.pet.create({name: 'spot'}).meta({fetch: true}).exec(function(err, spot) { sailsApp.models.user.create({name: 'will', pets: [spot.id]}).meta({fetch: true}).exec(function(err) { if (err) {return done (err);} sailsApp.request('get /user/1/pets', function (err, resp, data) { if (err) {return done (err);} assert.equal(data.length, 1); assert.equal(data[0].name, 'spot'); assert.equal(data[0].id, 1); assert.equal(data[0].owner, 1); return done(); }); }); }); }); }); describe('a get request to /:model/:parentid/:association/:id', function() { it('should return JSON for the specified instance in the collection of the test model', function(done) { sailsApp.models.pet.createEach([{name: 'bubbles'}, {name: 'dempsey'}]).meta({fetch: true}).exec(function(err, pets) { sailsApp.models.user.create({name: 'roger', pets: _.pluck(pets,'id')}).meta({fetch: true}).exec(function(err) { if (err) {return done (err);} sailsApp.request('get /user/1/pets/2', function (err, resp, data) { if (err) { if (err.status && err.status === 404) { return done(); } return done(new Error('Should have responded with a 404 error, but instead got:' + util.inspect(err, {depth: null}))); } return done(new Error('Should have responded with a 404 error, but instead got:' + util.inspect(data, {depth: null}))); }); }); }); }); }); describe('a get request to /:model/:parentid/:association/add/:id', function() { it('should return JSON for an instance of the test model, with its collection updated', function(done) { sailsApp.models.user.create({name: 'ira'}).exec(function(err) { if (err) {return done (err);} sailsApp.models.pet.create({name: 'flipper'}).exec(function(err) { if (err) {return done (err);} sailsApp.request('get /user/1/pets/add/1', function (err, resp, data) { if (err) {return done (err);} assert.equal(data.name, 'ira'); assert.equal(data.id, 1); assert.equal(data.pets.length, 1); assert.equal(data.pets[0].name, 'flipper'); sailsApp.models.user.findOne({id: 1}).populate('pets').exec(function(err, user) { if (err) {return done (err);} assert(user); assert.equal(user.name, 'ira'); assert.equal(user.id, 1); assert.equal(user.pets.length, 1); assert.equal(user.pets[0].name, 'flipper'); return done(); }); }); }); }); }); }); describe('a get request to /:model/:parentid/:association/remove/:id', function() { it('should return JSON for an instance of the test model, with its collection updated', function(done) { sailsApp.models.pet.create({name: 'alice'}).meta({fetch: true}).exec(function(err, alice) { sailsApp.models.user.create({name: 'larry', pets: [alice.id]}).meta({fetch: true}).exec(function(err) { if (err) {return done (err);} sailsApp.request('get /user/1/pets/remove/1', function (err, resp, data) { if (err) {return done (err);} assert.equal(data.name, 'larry'); assert.equal(data.id, 1); assert.equal(data.pets.length, 0); sailsApp.models.user.findOne({id: 1}).populate('pets').exec(function(err, user) { if (err) {return done (err);} assert(user); assert.equal(user.name, 'larry'); assert.equal(user.id, 1); assert.equal(user.pets.length, 0); return done(); }); }); }); }); }); }); describe('a put request to /:model/:parentid/:association/replace (with empty array)', function() { it('should return JSON for an instance of the test model, with its collection replaced', function(done) { sailsApp.models.user.create({name: 'ira', id: 1}).exec(function(err) { if (err) {return done (err);} sailsApp.models.pet.create({name: 'flipper', id: 1, owner: 1}).exec(function(err) { if (err) {return done (err);} sailsApp.request('get /user/1/pets/replace?pets=[]', function (err, resp, data) { if (err) {return done (err);} assert.equal(data.name, 'ira'); assert.equal(data.pets.length, 0); sailsApp.models.pet.findOne({id: 1}).populate('owner').exec(function(err, pet) { if (err) {return done (err);} assert(pet); assert.equal(pet.name, 'flipper'); assert.equal(pet.id, 1); assert.equal(pet.owner, null); return done(); }); }); }); }); }); }); describe('a get request to /:model/:parentid/:association/replace (with new array)', function() { it('should return JSON for an instance of the test model, with its collection replaced', function(done) { sailsApp.models.user.create({name: 'zooey'}).exec(function(err) { if (err) {return done (err);} sailsApp.models.pet.createEach([{name: 'ralph', id: 1, owner: 1}, {name: 'fiona', id: 2}]).exec(function(err) { if (err) {return done (err);} sailsApp.request('get /user/1/pets/replace?pets=[2]', function (err, resp, data) { if (err) {return done (err);} assert.equal(data.name, 'zooey'); assert.equal(data.pets.length, 1); assert.equal(data.pets[0].id, 2); assert.equal(data.pets[0].name, 'fiona'); sailsApp.models.pet.findOne({id: 2}).populate('owner').exec(function(err, pet) { if (err) {return done (err);} assert(pet); assert.equal(pet.name, 'fiona'); assert.equal(pet.id, 2); assert.equal(pet.owner.name, 'zooey'); assert.equal(pet.owner.id, 1); return done(); }); }); }); }); }); }); }); }); describe('filtering in the query string :: ', function() { before(function() { extraSailsConfig = { orm: { moduleDefinitions: { models: { user: { attributes: { name: 'string' } } } } } }; }); after(function() { extraSailsConfig = {}; }); it('a get request to /:model/find?name=scott should respond with the correctly filtered instances', function(done) { sailsApp.models.user.createEach([{name: 'scott'}, {name: 'mike'}]).exec(function(err) { if (err) {return done(err);} sailsApp.request('get /user/find?name=scott', function (err, resp, data) { assert(!err, err); assert.equal(data.length, 1); assert.equal(data[0].name, 'scott'); done(); }); }); }); it('a get request to /:model/find?where={...} should respond with the correctly filtered instances', function(done) { sailsApp.models.user.createEach([{name: 'scott'}, {name: 'mike'}, {name: 'rachael'}, {name: 'cody'}, {name: 'irl'}]).exec(function(err) { if (err) {return done(err);} sailsApp.request('get /user/find?where={"name": {">": "irl"}}', function (err, resp, data) { assert(!err, err); assert.equal(data.length, 3); var names = _.pluck(data, 'name'); assert(_.contains(names, 'scott')); assert(_.contains(names, 'mike')); assert(_.contains(names, 'rachael')); done(); }); }); }); }); describe('using sort, skip and limit in the query string :: ', function() { before(function() { extraSailsConfig = { orm: { moduleDefinitions: { models: { user: { attributes: { name: 'string' } } } } } }; }); after(function() { extraSailsConfig = {}; }); it('a get request to /:model/find?sort=name%20asc&limit=2&skip=1 should respond with the correctly filtered instances', function(done) { sailsApp.models.user.createEach([{name: 'scott'}, {name: 'mike'}, {name: 'rachael'}, {name: 'cody'}, {name: 'irl'}]).exec(function(err) { if (err) {return done(err);} sailsApp.request('get /user/find?sort=name%20asc&limit=2&skip=1', function (err, resp, data) { assert(!err, err); assert.equal(data.length, 2); assert.equal(data[0].name, 'irl'); assert.equal(data[1].name, 'mike'); done(); }); }); }); it('a get request to /:model/find?sort=name%20desc&limit=2&skip=1 should respond with the correctly filtered instances', function(done) { sailsApp.models.user.createEach([{name: 'scott'}, {name: 'mike'}, {name: 'rachael'}, {name: 'cody'}, {name: 'irl'}]).exec(function(err) { if (err) {return done(err);} sailsApp.request('get /user/find?sort=name%20desc&limit=2&skip=1', function (err, resp, data) { assert(!err, err); assert.equal(data.length, 2); assert.equal(data[0].name, 'rachael'); assert.equal(data[1].name, 'mike'); done(); }); }); }); it('a get request to /:model/find?sort={"name":-1}&limit=2&skip=1 should respond with the correctly filtered instances', function(done) { sailsApp.models.user.createEach([{name: 'scott'}, {name: 'mike'}, {name: 'rachael'}, {name: 'cody'}, {name: 'irl'}]).exec(function(err) { if (err) {return done(err);} sailsApp.request('get /user/find?sort={"name":-1}&limit=2&skip=1', function (err, resp, data) { assert(!err, err); assert.equal(data.length, 2); assert.equal(data[0].name, 'rachael'); assert.equal(data[1].name, 'mike'); done(); }); }); }); }); describe('after reloading actions :: ', function() { before(function() { extraSailsConfig = { orm: { moduleDefinitions: { models: { user: { attributes: { name: 'string' } } } } } }; }); after(function() { extraSailsConfig = {}; }); it('should still respond to RESTful blueprint requests correctly :: ', function(done) { sailsApp.models.user.createEach([{name: 'scott'}, {name: 'mike'}]).exec(function(err) { if (err) {return done(err);} sailsApp.reloadActions(function(err) { if (err) {return done(err);} sailsApp.request('get /user/find', function (err, resp, data) { assert(!err, err); assert.equal(data.length, 2); done(); }); }); }); }); }); describe('with pluralize turned on :: ', function() { before(function() { extraSailsConfig = { blueprints: { pluralize: true }, orm: { moduleDefinitions: { models: { user: {}, quiz: {} } } } }; }); after(function() { extraSailsConfig = {}; }); it('should bind blueprint actions to plural controller names', function(done) { sailsApp.models.user.create({}).exec(function(err) { if (err) {return done (err);} sailsApp.request('get /users/find', function (err, resp, data) { assert(!err, err); assert.equal(data.length, 1); assert.equal(data[0].id, 1); done(); }); }); }); it('should bind blueprint actions to plural controller names (quiz => quizzes)', function(done) { sailsApp.models.quiz.create({}).exec(function(err) { if (err) {return done (err);} sailsApp.request('get /quizzes/find', function (err, resp, data) { assert(!err, err); assert.equal(data.length, 1); assert.equal(data[0].id, 1); done(); }); }); }); it('should not bind blueprint actions to singular controller names', function(done) { sailsApp.models.user.create({}).exec(function(err) { if (err) {return done (err);} sailsApp.request('get /user/find', function (err, resp, data) { assert(err); assert.equal(err.status, 404); done(); }); }); }); }); describe('with `prefix` option set to \'/api\' :: ', function() { before(function() { extraSailsConfig = { blueprints: { prefix: '/api' }, orm: { moduleDefinitions: { models: { user: { attributes: { name: 'string' } } } } } }; }); after(function() { extraSailsConfig = {}; }); describe('a get request to /api/:model/find', function() { it('should return JSON for all of the instances of the test model', function(done) { sailsApp.models.user.create({name: 'joy'}).exec(function(err) { if (err) {return done (err);} sailsApp.request('get /api/user/find', function (err, resp, data) { assert(!err, err); assert.equal(data.length, 1); assert.equal(data[0].name, 'joy'); assert.equal(data[0].id, 1); done(); }); }); }); }); describe('a get request to /:model/find', function() { it('should return a 404', function(done) { sailsApp.request('get /user/find', function (err, resp, data) { assert(err); assert.equal(err.status, 404); done(); }); }); }); }); describe('with `restPrefix` option set to \'/v1\' :: ', function() { before(function() { extraSailsConfig = { blueprints: { restPrefix: '/v1' }, orm: { moduleDefinitions: { models: { user: { attributes: { name: 'string' } } } } } }; }); after(function() { extraSailsConfig = {}; }); describe('a get request to /v1/:model/find', function() { it('should return a 404 (rest prefix should have no effect on shortcut routes', function(done) { sailsApp.request('get /v1/user/find', function (err, resp, data) { assert(err); assert.equal(err.status, 404); done(); }); }); }); describe('a get request to /:model/find', function() { it('should return JSON for all of the instances of the test model', function(done) { sailsApp.models.user.create({name: 'al'}).exec(function(err) { if (err) {return done (err);} sailsApp.request('get /user/find', function (err, resp, data) { assert(!err, err); assert.equal(data.length, 1); assert.equal(data[0].name, 'al'); assert.equal(data[0].id, 1); done(); }); }); }); }); }); describe('overriding blueprints :: ', function() { before(function() { extraSailsConfig = { orm: { moduleDefinitions: { models: { user: {}, }, } }, controllers: { moduleDefinitions: { 'user/find': function(req, res) { return res.send('find dem users!'); } } } }; }); after(function() { extraSailsConfig = {}; }); it('if a `:model.find` action is explicitly added, it should be used in response to `GET /:model/find`', function(done) { sailsApp.models.user.create({name: 'al'}).exec(function(err) { if (err) {return done (err);} sailsApp.request('get /user/find', function (err, resp, data) { assert(!err, err); assert.equal(data, 'find dem users!'); done(); }); }); }); }); }); }); }); ================================================ FILE: test/integration/hook.cors.test.js ================================================ /** * Module dependencies */ var assert = require('assert'); var httpHelper = require('./helpers/httpHelper.js'); var appHelper = require('./helpers/appHelper'); var path = require('path'); var fs = require('fs'); var _ = require('@sailshq/lodash'); var tmp = require('tmp'); var Sails = require('../../lib').constructor; describe('CORS config ::', function() { var setups = { 'with default settings': { expectations: [ { route: 'PUT /no-cors-config', request_headers: {origin: 'http://example.com'}, response_status: 200, response_headers: null }, { route: 'OPTIONS /no-cors-config', request_headers: {origin: 'http://example.com', 'access-control-request-method': 'PUT'}, response_status: 200, response_headers: null }, { route: 'PUT /cors-true', request_headers: {origin: 'http://example.com'}, response_status: 200, response_headers: { 'access-control-allow-origin': '*', } }, { route: 'OPTIONS /cors-true', request_headers: {origin: 'http://example.com', 'access-control-request-method': 'PUT'}, response_status: 200, response_headers: { 'access-control-allow-origin': '*', 'access-control-allow-methods': 'GET,HEAD,PUT,PATCH,POST,DELETE', 'access-control-allow-headers': 'content-type', } }, { route: 'OPTIONS /cors-true', request_headers: {origin: 'http://example.com', 'access-control-request-method': 'POST'}, response_status: 200, response_headers: null }, { route: 'PUT /origin-example-com', request_headers: {origin: 'http://example.com'}, response_status: 200, response_headers: { 'access-control-allow-origin': 'http://example.com', 'vary': 'Origin' } }, { route: 'OPTIONS /origin-example-com', request_headers: {origin: 'http://example.com', 'access-control-request-method': 'PUT'}, response_status: 200, response_headers: { 'access-control-allow-origin': 'http://example.com', 'access-control-allow-methods': 'GET,HEAD,PUT,PATCH,POST,DELETE', 'access-control-allow-headers': 'content-type', 'vary': 'Origin' } }, { route: 'OPTIONS /origin-example-com', request_headers: {origin: 'http://example.com', 'access-control-request-method': 'POST'}, response_status: 200, response_headers: null }, { route: 'PUT /origin-example-com', request_headers: {origin: 'http://somewhere.com'}, response_status: 200, response_headers: null }, { route: 'OPTIONS /origin-example-com', request_headers: {origin: 'http://somewhere.com', 'access-control-request-method': 'PUT'}, response_status: 200, response_headers: null }, { route: 'OPTIONS /origin-example-com', request_headers: {origin: 'http://somewhere.com', 'access-control-request-method': 'POST'}, response_status: 200, response_headers: null }, { route: 'PUT /origin-example-com-somewhere-com', request_headers: {origin: 'http://example.com'}, response_status: 200, response_headers: { 'access-control-allow-origin': 'http://example.com', 'vary': 'Origin' } }, { route: 'OPTIONS /origin-example-com-somewhere-com', request_headers: {origin: 'http://example.com', 'access-control-request-method': 'PUT'}, response_status: 200, response_headers: { 'access-control-allow-origin': 'http://example.com', 'access-control-allow-methods': 'GET,HEAD,PUT,PATCH,POST,DELETE', 'access-control-allow-headers': 'content-type', 'vary': 'Origin' } }, { route: 'OPTIONS /origin-example-com-somewhere-com', request_headers: {origin: 'http://example.com', 'access-control-request-method': 'POST'}, response_status: 200, response_headers: null }, { route: 'PUT /origin-example-com-somewhere-com', request_headers: {origin: 'http://somewhere.com'}, response_status: 200, response_headers: { 'access-control-allow-origin': 'http://somewhere.com', 'vary': 'Origin' } }, { route: 'OPTIONS /origin-example-com-somewhere-com', request_headers: {origin: 'http://somewhere.com', 'access-control-request-method': 'PUT'}, response_status: 200, response_headers: { 'access-control-allow-origin': 'http://somewhere.com', 'access-control-allow-methods': 'GET,HEAD,PUT,PATCH,POST,DELETE', 'access-control-allow-headers': 'content-type', 'vary': 'Origin' } }, { route: 'OPTIONS /origin-example-com-somewhere-com', request_headers: {origin: 'http://somewhere.com', 'access-control-request-method': 'POST'}, response_status: 200, response_headers: null }, { route: 'PUT /origin-example-com-somewhere-com-array', request_headers: {origin: 'http://example.com'}, response_status: 200, response_headers: { 'access-control-allow-origin': 'http://example.com', 'vary': 'Origin' } }, { route: 'OPTIONS /origin-example-com-somewhere-com-array', request_headers: {origin: 'http://example.com', 'access-control-request-method': 'PUT'}, response_status: 200, response_headers: { 'access-control-allow-origin': 'http://example.com', 'access-control-allow-methods': 'GET,HEAD,PUT,PATCH,POST,DELETE', 'access-control-allow-headers': 'content-type', 'vary': 'Origin' } }, { route: 'OPTIONS /origin-example-com-somewhere-com-array', request_headers: {origin: 'http://example.com', 'access-control-request-method': 'POST'}, response_status: 200, response_headers: null }, { route: 'PUT /origin-example-com-somewhere-com-array', request_headers: {origin: 'http://somewhere.com'}, response_status: 200, response_headers: { 'access-control-allow-origin': 'http://somewhere.com', 'vary': 'Origin' } }, { route: 'OPTIONS /origin-example-com-somewhere-com-array', request_headers: {origin: 'http://somewhere.com', 'access-control-request-method': 'PUT'}, response_status: 200, response_headers: { 'access-control-allow-origin': 'http://somewhere.com', 'access-control-allow-methods': 'GET,HEAD,PUT,PATCH,POST,DELETE', 'access-control-allow-headers': 'content-type', 'vary': 'Origin' } }, { route: 'OPTIONS /origin-example-com-somewhere-com-array', request_headers: {origin: 'http://somewhere.com', 'access-control-request-method': 'POST'}, response_status: 200, response_headers: null }, { route: 'PUT /all-methods-origin-example-com', request_headers: {origin: 'http://example.com'}, response_status: 200, response_headers: { 'access-control-allow-origin': 'http://example.com', 'vary': 'Origin' } }, { route: 'OPTIONS /all-methods-origin-example-com', request_headers: {origin: 'http://example.com', 'access-control-request-method': 'PUT'}, response_status: 200, response_headers: { 'access-control-allow-origin': 'http://example.com', 'access-control-allow-methods': 'GET,HEAD,PUT,PATCH,POST,DELETE', 'access-control-allow-headers': 'content-type', 'vary': 'Origin' } }, { route: 'POST /all-methods-origin-example-com', request_headers: {origin: 'http://example.com'}, response_status: 200, response_headers: { 'access-control-allow-origin': 'http://example.com', 'vary': 'Origin' } }, { route: 'OPTIONS /all-methods-origin-example-com', request_headers: {origin: 'http://example.com', 'access-control-request-method': 'POST'}, response_status: 200, response_headers: { 'access-control-allow-origin': 'http://example.com', 'access-control-allow-methods': 'GET,HEAD,PUT,PATCH,POST,DELETE', 'access-control-allow-headers': 'content-type', 'vary': 'Origin' } }, { route: 'DELETE /unsafe', request_headers: {origin: 'http://foobar.com'}, response_status: 200, response_headers: { 'access-control-allow-origin': 'http://foobar.com', 'access-control-allow-credentials': 'true', 'vary': 'Origin' } }, { route: 'OPTIONS /unsafe', request_headers: {origin: 'http://foobar.com', 'access-control-request-method': 'DELETE'}, response_status: 200, response_headers: { 'access-control-allow-origin': 'http://foobar.com', 'access-control-allow-methods': 'GET,HEAD,PUT,PATCH,POST,DELETE', 'access-control-allow-credentials': 'true', 'access-control-allow-headers': 'content-type', 'vary': 'Origin' } }, ] }, 'with `allRoutes: true`': { sailsCorsConfig: {allRoutes: true}, expectations: [ { route: 'PUT /no-cors-config', request_headers: {origin: 'http://example.com'}, response_status: 200, response_headers: { 'access-control-allow-origin': '*', } }, { route: 'OPTIONS /no-cors-config', request_headers: {origin: 'http://example.com', 'access-control-request-method': 'PUT'}, response_status: 200, response_headers: { 'access-control-allow-origin': '*', 'access-control-allow-methods': 'GET,HEAD,PUT,PATCH,POST,DELETE', 'access-control-allow-headers': 'content-type', } }, { route: 'PUT /cors-true', request_headers: {origin: 'http://example.com'}, response_status: 200, response_headers: { 'access-control-allow-origin': '*', } }, { route: 'OPTIONS /cors-true', request_headers: {origin: 'http://example.com', 'access-control-request-method': 'PUT'}, response_status: 200, response_headers: { 'access-control-allow-origin': '*', 'access-control-allow-methods': 'GET,HEAD,PUT,PATCH,POST,DELETE', 'access-control-allow-headers': 'content-type', } }, { route: 'OPTIONS /cors-true', request_headers: {origin: 'http://example.com', 'access-control-request-method': 'POST'}, response_status: 200, response_headers: { 'access-control-allow-origin': '*', 'access-control-allow-methods': 'GET,HEAD,PUT,PATCH,POST,DELETE', 'access-control-allow-headers': 'content-type', } }, ], }, 'with `allRoutes: true`, `allowCredentials: true`, `allowAnyOriginWithCredentialsUnsafe: true`': { sailsCorsConfig: {allRoutes: true, allowCredentials: true, allowAnyOriginWithCredentialsUnsafe: true}, expectations: [ { route: 'PUT /no-cors-config', request_headers: {origin: 'http://example.com'}, response_status: 200, response_headers: { 'access-control-allow-origin':'http://example.com', 'access-control-allow-methods': undefined, 'access-control-allow-headers': undefined, 'access-control-allow-credentials': 'true', 'access-control-exposed-headers': undefined, 'vary': 'Origin' } }, { route: 'OPTIONS /no-cors-config', request_headers: {origin: 'http://example.com', 'access-control-request-method': 'PUT'}, response_status: 200, response_headers: { 'access-control-allow-origin':'http://example.com', 'access-control-allow-methods': 'GET,HEAD,PUT,PATCH,POST,DELETE', 'access-control-allow-headers': 'content-type', 'access-control-allow-credentials': 'true', 'access-control-exposed-headers': undefined, 'vary': 'Origin' } }, { route: 'PUT /cors-true', request_headers: {origin: 'http://example.com'}, response_status: 200, response_headers: { 'access-control-allow-origin':'http://example.com', 'access-control-allow-methods': undefined, 'access-control-allow-headers': undefined, 'access-control-allow-credentials': 'true', 'access-control-exposed-headers': undefined, 'vary': 'Origin' } }, { route: 'OPTIONS /cors-true', request_headers: {origin: 'http://example.com', 'access-control-request-method': 'PUT'}, response_status: 200, response_headers: { 'access-control-allow-origin':'http://example.com', 'access-control-allow-methods': 'GET,HEAD,PUT,PATCH,POST,DELETE', 'access-control-allow-headers': 'content-type', 'access-control-allow-credentials': 'true', 'access-control-exposed-headers': undefined, 'vary': 'Origin' } }, { route: 'OPTIONS /cors-true', request_headers: {origin: 'http://example.com', 'access-control-request-method': 'POST'}, response_status: 200, response_headers: { 'access-control-allow-origin':'http://example.com', 'access-control-allow-methods': 'GET,HEAD,PUT,PATCH,POST,DELETE', 'access-control-allow-headers': 'content-type', 'access-control-allow-credentials': 'true', 'access-control-exposed-headers': undefined, 'vary': 'Origin' } }, ] }, 'with `allRoutes: true`, `origin: http://example.com`': { sailsCorsConfig: {allRoutes: true, allowOrigins: 'http://example.com'}, expectations: [ { route: 'PUT /no-cors-config', request_headers: {origin: 'http://example.com'}, response_status: 200, response_headers: { 'access-control-allow-origin': 'http://example.com', 'vary': 'Origin' } }, { route: 'OPTIONS /no-cors-config', request_headers: {origin: 'http://example.com', 'access-control-request-method': 'PUT'}, response_status: 200, response_headers: { 'access-control-allow-origin': 'http://example.com', 'access-control-allow-methods': 'GET,HEAD,PUT,PATCH,POST,DELETE', 'access-control-allow-headers': 'content-type', 'vary': 'Origin' } }, { route: 'PUT /cors-true', request_headers: {origin: 'http://example.com'}, response_status: 200, response_headers: { 'access-control-allow-origin': 'http://example.com', 'vary': 'Origin' } }, { route: 'OPTIONS /cors-true', request_headers: {origin: 'http://example.com', 'access-control-request-method': 'PUT'}, response_status: 200, response_headers: { 'access-control-allow-origin': 'http://example.com', 'access-control-allow-methods': 'GET,HEAD,PUT,PATCH,POST,DELETE', 'access-control-allow-headers': 'content-type', 'vary': 'Origin' } }, { route: 'OPTIONS /cors-true', request_headers: {origin: 'http://example.com', 'access-control-request-method': 'POST'}, response_status: 200, response_headers: { 'access-control-allow-origin': 'http://example.com', 'access-control-allow-methods': 'GET,HEAD,PUT,PATCH,POST,DELETE', 'access-control-allow-headers': 'content-type', 'vary': 'Origin' } }, { route: 'PUT /cors-true', request_headers: {origin: 'http://somewhere.com'}, response_status: 200, response_headers: null }, ] }, 'with `allRoutes: true`, `origin: http://example.com, http://somewhere.com`': { sailsCorsConfig: {allRoutes: true, allowOrigins: 'http://example.com, http://somewhere.com'}, expectations: [ { route: 'PUT /no-cors-config', request_headers: {origin: 'http://example.com'}, response_status: 200, response_headers: { 'access-control-allow-origin': 'http://example.com', 'vary': 'Origin' } }, { route: 'OPTIONS /no-cors-config', request_headers: {origin: 'http://example.com', 'access-control-request-method': 'PUT'}, response_status: 200, response_headers: { 'access-control-allow-origin': 'http://example.com', 'access-control-allow-methods': 'GET,HEAD,PUT,PATCH,POST,DELETE', 'access-control-allow-headers': 'content-type', 'vary': 'Origin' } }, { route: 'PUT /cors-true', request_headers: {origin: 'http://example.com'}, response_status: 200, response_headers: { 'access-control-allow-origin': 'http://example.com', 'vary': 'Origin' } }, { route: 'OPTIONS /cors-true', request_headers: {origin: 'http://example.com', 'access-control-request-method': 'PUT'}, response_status: 200, response_headers: { 'access-control-allow-origin': 'http://example.com', 'access-control-allow-methods': 'GET,HEAD,PUT,PATCH,POST,DELETE', 'access-control-allow-headers': 'content-type', 'vary': 'Origin' } }, { route: 'OPTIONS /cors-true', request_headers: {origin: 'http://example.com', 'access-control-request-method': 'POST'}, response_status: 200, response_headers: { 'access-control-allow-origin': 'http://example.com', 'access-control-allow-methods': 'GET,HEAD,PUT,PATCH,POST,DELETE', 'access-control-allow-headers': 'content-type', 'vary': 'Origin' } }, { route: 'PUT /cors-true', request_headers: {origin: 'http://somewhere.com'}, response_status: 200, response_headers: { 'access-control-allow-origin': 'http://somewhere.com', 'vary': 'Origin' } }, ] }, 'with `allRoutes: true`, `origin: [\'http://example.com\', \'http://somewhere.com\']`': { sailsCorsConfig: {allRoutes: true, allowOrigins: ['http://example.com', 'http://somewhere.com']}, expectations: [ { route: 'PUT /no-cors-config', request_headers: {origin: 'http://example.com'}, response_status: 200, response_headers: { 'access-control-allow-origin': 'http://example.com', 'vary': 'Origin' } }, { route: 'OPTIONS /no-cors-config', request_headers: {origin: 'http://example.com', 'access-control-request-method': 'PUT'}, response_status: 200, response_headers: { 'access-control-allow-origin': 'http://example.com', 'access-control-allow-methods': 'GET,HEAD,PUT,PATCH,POST,DELETE', 'access-control-allow-headers': 'content-type', 'vary': 'Origin' } }, { route: 'PUT /cors-true', request_headers: {origin: 'http://example.com'}, response_status: 200, response_headers: { 'access-control-allow-origin': 'http://example.com', 'vary': 'Origin' } }, { route: 'OPTIONS /cors-true', request_headers: {origin: 'http://example.com', 'access-control-request-method': 'PUT'}, response_status: 200, response_headers: { 'access-control-allow-origin': 'http://example.com', 'access-control-allow-methods': 'GET,HEAD,PUT,PATCH,POST,DELETE', 'access-control-allow-headers': 'content-type', 'vary': 'Origin' } }, { route: 'OPTIONS /cors-true', request_headers: {origin: 'http://example.com', 'access-control-request-method': 'POST'}, response_status: 200, response_headers: { 'access-control-allow-origin': 'http://example.com', 'access-control-allow-methods': 'GET,HEAD,PUT,PATCH,POST,DELETE', 'access-control-allow-headers': 'content-type', 'vary': 'Origin' } }, { route: 'PUT /cors-true', request_headers: {origin: 'http://somewhere.com'}, response_status: 200, response_headers: { 'access-control-allow-origin': 'http://somewhere.com', 'vary': 'Origin' } }, ] }, 'with `allowRequestHeaders: \'x-foo-bar\'`, `allowResponseHeaders: \'x-owl-hoot\'`, `allowRequestMethods: \'PUT,POST\'`': { sailsCorsConfig: {allowRequestHeaders: 'x-foo-bar', allowResponseHeaders: 'x-owl-hoot', allowRequestMethods: 'PUT,POST'}, expectations: [ { route: 'PUT /no-cors-config', request_headers: {origin: 'http://example.com'}, response_status: 200, response_headers: null }, { route: 'PUT /cors-true', request_headers: {origin: 'http://example.com'}, response_status: 200, response_headers: { 'access-control-allow-origin': '*', } }, { route: 'OPTIONS /cors-true', request_headers: {origin: 'http://example.com', 'access-control-request-method': 'PUT'}, response_status: 200, response_headers: { 'access-control-allow-origin': '*', 'access-control-allow-methods': 'PUT,POST', 'access-control-allow-headers': 'x-foo-bar', 'access-control-expose-headers': 'x-owl-hoot', } }, { route: 'OPTIONS /origin-example-com', request_headers: {origin: 'http://example.com', 'access-control-request-method': 'PUT'}, response_status: 200, response_headers: { 'access-control-allow-origin': 'http://example.com', 'access-control-allow-methods': 'PUT,POST', 'access-control-allow-headers': 'x-foo-bar', 'access-control-expose-headers': 'x-owl-hoot', 'vary': 'Origin' } }, ] } }; var only = _.findKey(setups, function(setup, name) { if (name.indexOf('only.') === 0) { return name; } }); if (only) { setups = (function(){ var onlySetup = setups[only]; newSetups = {}; newSetups[only.replace(/^only\./,'')] = onlySetup; return newSetups; })(); } _.each(setups, function(setup, name) { if (name.indexOf('skip.') === 0) { return; } describe(name, function() { var sailsApp; before(function(done) { (new Sails()).load({ hooks: {grunt: false, views: false, blueprints: false, policies: false, i18n: false}, log: {level: 'error'}, security: { cors: _.cloneDeep(setup.sailsCorsConfig) }, routes: { 'PUT /no-cors-config': function(req, res){return res.ok();}, 'PUT /cors-true': {cors: true, target: function(req, res){return res.ok();}}, 'PUT /origin-example-com': {cors: {allowOrigins: 'http://example.com'}, target: function(req, res){return res.ok();}}, 'PUT /origin-example-com-somewhere-com': {cors: {allowOrigins: 'http://example.com, http://somewhere.com'}, target: function(req, res){return res.ok();}}, 'PUT /origin-example-com-somewhere-com-array': {cors: {allowOrigins: ['http://example.com', 'http://somewhere.com']}, target: function(req, res){return res.ok();}}, '/all-methods-origin-example-com': {cors: {allowOrigins: 'http://example.com'}, target: function(req, res){return res.ok();}}, '/unsafe': {cors: {allowOrigins: '*', allowCredentials: true, allowAnyOriginWithCredentialsUnsafe: true}, target: function(req, res){return res.ok();}}, } }, function(err, _sails) { sailsApp = _sails; return done(err); } ); }); after(function(done) { sailsApp.lower(done); }); _.each(setup.expectations, function(expectation) { var routeParts = expectation.route.split(' '); var method = routeParts[0]; var path = routeParts[1]; describe('a ' + method.toUpperCase() + ' request to ' + path + ' using ' + JSON.stringify(expectation.request_headers), function() { var responseHolder = {}; before(function(done) { sailsApp.request({ url: path, method: method, headers: expectation.request_headers }, function (err, response, data) { if (err) {return done(err);} responseHolder.response = response; return done(); }); }); it('should respond with status code ' + expectation.response_status, function() { assert.equal(responseHolder.response.statusCode, expectation.response_status); }); var expectedHeaders = _.extend({}, { 'access-control-allow-origin': undefined, 'access-control-allow-methods': undefined, 'access-control-allow-headers': undefined, 'access-control-allow-credentials': undefined, 'access-control-exposed-headers': undefined, 'vary': undefined }, expectation.response_headers || {}); expectHeaders(responseHolder, expectedHeaders); }); }); }); describe(name + ' (with deprecated config)', function() { var sailsApp; before(function(done) { (new Sails()).load({ hooks: {grunt: false, views: false, blueprints: false, policies: false, i18n: false}, log: {level: 'silent'}, cors: _.cloneDeep(setup.sailsCorsConfig), routes: { 'PUT /no-cors-config': function(req, res){ return res.ok(); }, 'PUT /cors-true': {cors: true, target: function(req, res){return res.ok();}}, 'PUT /origin-example-com': {cors: {origin: 'http://example.com'}, target: function(req, res){return res.ok();}}, 'PUT /origin-example-com-somewhere-com': {cors: {origin: 'http://example.com, http://somewhere.com'}, target: function(req, res){return res.ok();}}, 'PUT /origin-example-com-somewhere-com-array': {cors: {origin: ['http://example.com', 'http://somewhere.com']}, target: function(req, res){return res.ok();}}, '/all-methods-origin-example-com': {cors: {origin: 'http://example.com'}, target: function(req, res){return res.ok();}}, '/unsafe': {cors: {origin: '*', allowCredentials: true, allowAnyOriginWithCredentialsUnsafe: true}, target: function(req, res){return res.ok();}}, } }, function(err, _sails) { sailsApp = _sails; return done(err); } ); }); after(function(done) { sailsApp.lower(done); }); _.each(setup.expectations, function(expectation) { var routeParts = expectation.route.split(' '); var method = routeParts[0]; var path = routeParts[1]; describe('a ' + method.toUpperCase() + ' request to ' + path + ' using ' + JSON.stringify(expectation.request_headers), function() { var responseHolder = {}; before(function(done) { sailsApp.request({ url: path, method: method, headers: expectation.request_headers }, function (err, response, data) { if (err) {return done(err);} responseHolder.response = response; return done(); }); }); it('should respond with status code ' + expectation.response_status, function() { assert.equal(responseHolder.response.statusCode, expectation.response_status); }); var expectedHeaders = _.extend({}, { 'access-control-allow-origin': undefined, 'access-control-allow-methods': undefined, 'access-control-allow-headers': undefined, 'access-control-allow-credentials': undefined, 'access-control-exposed-headers': undefined, 'vary': undefined }, expectation.response_headers || {}); expectHeaders(responseHolder, expectedHeaders); }); }); }); }); describe('with invalid global CORS config ({allowOrigins: \'*\', allowCredentials: true})', function() { it('should fail to lift', function(done) { (new Sails()).load({ hooks: {grunt: false, views: false, blueprints: false, policies: false, i18n: false}, log: {level: 'silent'}, cors: {allowOrigins: '*', allowCredentials: true}, }, function(err, _sails) { if (err) {return done();} return done(new Error('Sails should have failed to lift with invalid global CORS config!')); } ); }); }); describe('with invalid global CORS config ({allowOrigins: 666})', function() { it('should fail to lift', function(done) { (new Sails()).load({ hooks: {grunt: false, views: false, blueprints: false, policies: false, i18n: false}, log: {level: 'silent'}, cors: {allowOrigins: 666}, }, function(err, _sails) { if (err) {return done();} return done(new Error('Sails should have failed to lift with invalid global CORS config!')); } ); }); }); describe('with invalid global CORS config ({allowOrigins: [\'localboast.yarg\']})', function() { it('should fail to lift', function(done) { (new Sails()).load({ hooks: {grunt: false, views: false, blueprints: false, policies: false, i18n: false}, log: {level: 'silent'}, cors: {allowOrigins: ['localboast.yarg']}, }, function(err, _sails) { if (err) {return done();} return done(new Error('Sails should have failed to lift with invalid global CORS config!')); } ); }); }); describe('with invalid global CORS config ({allowOrigins: [\'http://localboast.com:80\']})', function() { it('should fail to lift', function(done) { (new Sails()).load({ hooks: {grunt: false, views: false, blueprints: false, policies: false, i18n: false}, log: {level: 'silent'}, cors: {allowOrigins: ['http://localboast.com:80']}, }, function(err, _sails) { if (err) {return done();} return done(new Error('Sails should have failed to lift with invalid global CORS config!')); } ); }); }); describe('with invalid global CORS config ({allowOrigins: [\'https://localboast.com:443\']})', function() { it('should fail to lift', function(done) { (new Sails()).load({ hooks: {grunt: false, views: false, blueprints: false, policies: false, i18n: false}, log: {level: 'silent'}, cors: {allowOrigins: ['https://localboast.com:443']}, }, function(err, _sails) { if (err) {return done();} return done(new Error('Sails should have failed to lift with invalid global CORS config!')); } ); }); }); describe('with invalid global CORS config ({allowOrigins: [\'\']})', function() { it('should fail to lift', function(done) { (new Sails()).load({ hooks: {grunt: false, views: false, blueprints: false, policies: false, i18n: false}, log: {level: 'silent'}, cors: {allowOrigins: ['']}, }, function(err, _sails) { if (err) {return done();} return done(new Error('Sails should have failed to lift with invalid global CORS config!')); } ); }); }); describe('with invalid global CORS config ({allowOrigins: [666]})', function() { it('should fail to lift', function(done) { (new Sails()).load({ hooks: {grunt: false, views: false, blueprints: false, policies: false, i18n: false}, log: {level: 'silent'}, cors: {allowOrigins: [666]}, }, function(err, _sails) { if (err) {return done();} return done(new Error('Sails should have failed to lift with invalid global CORS config!')); } ); }); }); describe('with invalid route CORS config ({allRoutes: true, origin: \'*\', allowCredentials: true})', function() { it('should fail to lift', function(done) { (new Sails()).load({ hooks: {grunt: false, views: false, blueprints: false, policies: false, i18n: false}, log: {level: 'silent'}, routes: { '/invalid': {cors: {allowOrigins: '*', allowCredentials: true}} } }, function(err, _sails) { if (err) {return done();} return done(new Error('Sails should have failed to lift with invalid route CORS config!')); } ); }); }); describe('with invalid route CORS config ({allowOrigins: [666]})', function() { it('should fail to lift', function(done) { (new Sails()).load({ hooks: {grunt: false, views: false, blueprints: false, policies: false, i18n: false}, log: {level: 'silent'}, routes: { '/invalid': {cors: {allowOrigins: [666]}} } }, function(err, _sails) { if (err) {return done();} return done(new Error('Sails should have failed to lift with invalid route CORS config!')); } ); }); }); describe('with invalid route CORS config ({allowOrigins: 666})', function() { it('should fail to lift', function(done) { (new Sails()).load({ hooks: {grunt: false, views: false, blueprints: false, policies: false, i18n: false}, log: {level: 'silent'}, routes: { '/invalid': {cors: {allowOrigins: 666}} } }, function(err, _sails) { if (err) {return done();} return done(new Error('Sails should have failed to lift with invalid route CORS config!')); } ); }); }); describe('with invalid route CORS config ({allowOrigins: [\'blah\']})', function() { it('should fail to lift', function(done) { (new Sails()).load({ hooks: {grunt: false, views: false, blueprints: false, policies: false, i18n: false}, log: {level: 'silent'}, routes: { '/invalid': {cors: {allowOrigins: ['blah']}} } }, function(err, _sails) { if (err) {return done();} return done(new Error('Sails should have failed to lift with invalid route CORS config!')); } ); }); }); }); //</describe('CORS config ::')> function makeRequest(options, responseHolder, sailsApp) { return function(done) { sailsApp.request(options, function (err, response, data) { if (err) {return done(err);} responseHolder.response = response; return done(); }); }; } function expectHeaders(responseHolder, headers) { _.each(headers, function(val, header) { if (_.isUndefined(val)) { it('`' + header + '` should be undefined', function(){ assert(_.isUndefined(responseHolder.response.headers[header]), 'Got `' + responseHolder.response.headers[header] + '` instead'); }); } else { it('`' + header + '` should be `' + val + '`', function(){ assert.equal(responseHolder.response.headers[header], val); }); } }); } ================================================ FILE: test/integration/hook.csrf.test.js ================================================ /** * Module dependencies */ var assert = require('assert'); var fs = require('fs'); var path = require('path'); var _ = require('@sailshq/lodash'); var tmp = require('tmp'); var Sails = require('../../lib').constructor; var httpHelper = require('./helpers/httpHelper.js'); var appHelper = require('./helpers/appHelper'); describe('CSRF ::', function() { describe('Basic CSRF config ::', function() { var sailsConfig = {}; var sailsApp; beforeEach(function(done) { var _config = _.merge({ hooks: {grunt: false, views: false, blueprints: false, policies: false, i18n: false}, log: {level: 'error'}, routes: { '/csrfToken': {action: 'security/grant-csrf-token'}, 'ALL /viewtest/csrf': function(req, res) { var template = _.template('csrf=\'<%-_csrf%>\''); res.send(template(res.locals)); }, 'GET /user': function(req, res) { return res.sendStatus(200); }, 'POST /user': function(req, res) { return res.status(201).send(); }, 'POST /user/:id': function(req, res) { return res.sendStatus(200); } } }, sailsConfig); (new Sails()).load(_config, function(err, _sails) { sailsApp = _sails; return done(err); } ); }); afterEach(function(done) { sailsApp.lower(done); }); describe('with CSRF set to `false`', function() { before(function() { sailsConfig = {}; }); it('a blank CSRF token should be present in view locals', function(done) { sailsApp.request({url: '/viewtest/csrf', method: 'get'}, function(err, response) { if (err) { return done(err); } assert(response.body.indexOf('csrf=\'\'') !== -1, response.body); done(); }); }); }); describe('with CSRF set to `true`', function() { before(function() { sailsConfig = { security: { csrf: true } }; }); it('a HEAD request to a route with the CSRF used as a view local should not result in an error', function(done) { sailsApp.request({url: '/viewtest/csrf', method: 'head'}, function(err, response) { if (err) { return done(err); } done(); }); }); it('an OPTIONS request to a route with the CSRF used as a view local should not result in an error', function(done) { sailsApp.request({url: '/viewtest/csrf', method: 'options'}, function(err, response) { if (err) { return done(err); } done(); }); }); it('a CSRF token should be present in view locals', function(done) { sailsApp.request({url: '/viewtest/csrf', method: 'get'}, function(err, response) { if (err) { return done(err); } assert(response.body.match(/csrf='.{36}'/), response.body); done(); }); }); it('a request to /csrfToken should respond with a _csrf token', function(done) { sailsApp.request({url: '/csrftoken', method: 'get'}, function(err, response) { if (err) { return done(err); } assert(response.body._csrf, response.body); return done(); }); }); it('a POST request without a CSRF token should result in a 403 response', function(done) { sailsApp.request({url: '/user', method: 'post'}, function(err, response) { if (err && err.status === 403) { return done(); } done(new Error('Expected a 403 error, instead got: ' + (err || response.body))); }); }); it('a POST request with a valid CSRF token should result in a 201 response', function(done) { sailsApp.request({url: '/csrftoken', method: 'get'}, function(err, response) { if (err) { return done(err); } try { var body = response.body; var sid = response.headers['set-cookie'][0].split(';')[0].substr(10); sailsApp.request({ method: 'post', url: '/user', headers: { 'Content-type': 'application/json', 'cookie': 'sails.sid=' + sid }, data: {_csrf: body._csrf} }, function(err, response) { if (err) { return done(err); } assert.equal(response.statusCode, 201); done(); }); } catch (e) { done(e); } }); }); }); describe('with CSRF set to true, blacklisting \'POST /foo/:id, POST /bar/:id?, /user\'}', function() { before(function() { sailsConfig = { security: { csrf: true }, routes: { // Note -- since we don't actually define a target for these, requests that pass CSRF should return a 404. 'POST /foo/:id': {csrf: false}, '/bar/:id?': {csrf: false}, '/user': {csrf: false} } }; }); it('a POST request on /user without a CSRF token should result in a 201 response', function(done) { sailsApp.request({ method: 'post', url: '/user' }, function(err, response) { if (err) { return done(err); } assert.equal(response.statusCode, 201); done(); }); }); it('a POST request on /foo/12 without a CSRF token should result in a 404 response', function(done) { sailsApp.request({url: '/foo/12', method: 'post'}, function(err, response) { if (err && err.status === 404) { return done(); } done(new Error('Expected a 404 error, instead got: ' + (err || response.body))); }); }); it('a POST request on /foo/12?abc=123 without a CSRF token should result in a 404 response', function(done) { sailsApp.request({url: '/foo/12?abc=123', method: 'post'}, function(err, response) { if (err && err.status === 404) { return done(); } done(new Error('Expected a 404 error, instead got: ' + (err || response.body))); }); }); it('a POST request on /bar/12 without a CSRF token should result in a 404 response', function(done) { sailsApp.request({url: '/bar/12', method: 'post'}, function(err, response) { if (err && err.status === 404) { return done(); } done(new Error('Expected a 404 error, instead got: ' + (err || response.body))); }); }); it('a POST request on /bar/12?abc=123 without a CSRF token should result in a 404 response', function(done) { sailsApp.request({url: '/bar/12?abc=123', method: 'post'}, function(err, response) { if (err && err.status === 404) { return done(); } done(new Error('Expected a 404 error, instead got: ' + (err || response.body))); }); }); it('a POST request on /bar without a CSRF token should result in a 404 response', function(done) { sailsApp.request({url: '/bar', method: 'post'}, function(err, response) { if (err && err.status === 404) { return done(); } done(new Error('Expected a 404 error, instead got: ' + (err || response.body))); }); }); it('a POST request on /bar?abc=123 without a CSRF token should result in a 404 response', function(done) { sailsApp.request({url: '/bar?abc=123', method: 'post'}, function(err, response) { if (err && err.status === 404) { return done(); } done(new Error('Expected a 404 error, instead got: ' + (err || response.body))); }); }); it('a PUT request on /foo/12 without a CSRF token should result in a 403 response', function(done) { sailsApp.request({url: '/foo/12', method: 'put'}, function(err, response) { if (err && err.status === 403) { return done(); } done(new Error('Expected a 403 error, instead got: ' + (err || response.body))); }); }); it('a POST request on /test without a CSRF token should result in a 403 response', function(done) { sailsApp.request({url: '/test', method: 'post'}, function(err, response) { if (err && err.status === 403) { return done(); } done(new Error('Expected a 403 error, instead got: ' + (err || response.body))); }); }); it('a POST request on /foo without a CSRF token should result in a 403 response', function(done) { sailsApp.request({url: '/foo', method: 'post'}, function(err, response) { if (err && err.status === 403) { return done(); } done(new Error('Expected a 403 error, instead got: ' + (err || response.body))); }); }); }); describe('with CSRF set to true, blacklisting \'POST /user\\/\\d+/\'', function() { before(function() { sailsConfig = { security: { csrf: true }, routes: { 'POST r|user/\\d+|': {csrf: false} } }; }); it('a POST request on /user/1 without a CSRF token should result in a 200 response', function(done) { sailsApp.request({url: '/user/1', method: 'post'}, function(err, response) { if (err) { return done(err); } assert.equal(response.statusCode, 200); done(); }); }); it('a PUT request on /user/1 without a CSRF token should result in a 403 response', function(done) { sailsApp.request({url: '/user/1', method: 'put'}, function(err, response) { if (err && err.status === 403) { return done(); } done(new Error('Expected a 403 error, instead got: ' + (err || response.body))); }); }); it('a POST request on /user/a without a CSRF token should result in a 403 response', function(done) { sailsApp.request({url: '/user/a', method: 'post'}, function(err, response) { if (err && err.status === 403) { return done(); } done(new Error('Expected a 403 error, instead got: ' + (err || response.body))); }); }); it('a POST request on /user without a CSRF token should result in a 403 response', function(done) { sailsApp.request({url: '/user', method: 'post'}, function(err, response) { if (err && err.status === 403) { return done(); } done(new Error('Expected a 403 error, instead got: ' + (err || response.body))); }); }); }); describe('with CSRF set to false, whitelisting \'/user\\/\\d+/\'', function() { before(function() { sailsConfig = { security: { csrf: false }, routes: { 'r|user/\\d+|': {csrf: true} } }; }); it('a POST request on /user/1 without a CSRF token should result in a 403 response', function(done) { sailsApp.request({url: '/user/1', method: 'post'}, function(err, response) { if (err && err.status === 403) { return done(); } done(new Error('Expected a 403 error, instead got: ' + (err || response.body))); }); }); it('a POST request on /user/a without a CSRF token should result in a 200 response', function(done) { sailsApp.request({url: '/user/a', method: 'post'}, function(err, response) { if (err) { return done(err); } assert.equal(response.statusCode, 200); done(); }); }); it('a POST request on /user without a CSRF token should result in a 201 response', function(done) { sailsApp.request({url: '/user', method: 'post'}, function(err, response) { if (err) { return done(err); } assert.equal(response.statusCode, 201); done(); }); }); }); describe('with CSRF set to true and sessions disabled', function() { before(function() { sailsConfig = { security: { csrf: false }, hooks: {session: false}, routes: { 'GET /user': {csrf: true, target: function(req, res) { return res.sendStatus(200); }}, 'POST /user': {csrf: true, target: function(req, res) { return res.status(201).send(); }} } }; }); it('a GET request on /user should result in a 200 response', function(done) { sailsApp.request({url: '/user', method: 'get'}, function(err, response) { if (err) { return done(err); } assert.equal(response.statusCode, 200); done(); }); }); it('a POST request on /user without a CSRF token should result in a 403 response', function(done) { sailsApp.request({url: '/user', method: 'post'}, function(err, response) { assert(err); assert.equal(err.status, 403); done(); }); }); }); }); //</describe('CSRF config ::')> describe('With CSRF set to `true` globally and the session hook disabled :: ', function() { it('should fail to lift', function(done) { (new Sails()).load({security: {csrf: true}, hooks: {session: false}}, function(err, _sails) { if (err) { return done(); } _sails.lower(function() { return done(new Error('Sails lifted successfully, but it should have failed!')); }); } ); }); }); }); ================================================ FILE: test/integration/hook.helpers.test.js ================================================ /** * Module dependencies */ var util = require('util'); var assert = require('assert'); var tmp = require('tmp'); var _ = require('@sailshq/lodash'); var async = require('async'); var Filesystem = require('machinepack-fs'); var Sails = require('../../lib').constructor; tmp.setGracefulCleanup(); describe('helpers :: ', function() { describe('basic usage :: ', function() { var curDir, tmpDir, sailsApp; before(function(done) { // Cache the current working directory. curDir = process.cwd(); // Create a temp directory. tmpDir = tmp.dirSync({gracefulCleanup: true, unsafeCleanup: true}); // Switch to the temp directory. process.chdir(tmpDir.name); Filesystem.writeSync({ force: true, destination: 'api/helpers/greet.js', string: 'module.exports = { inputs: { name: { example: \'bob\', required: true } }, exits: { success: { outputExample: \'Hi, Bob!\'} }, fn: function (inputs, exits) { return exits.success(\'Hi, \' + inputs.name + \'!\'); } }' }).execSync(); (new Sails()).load({ hooks: { grunt: false, views: false, pubsub: false }, log: {level: 'silent'}, helpers: { moduleDefinitions: { ucase: { sync: true, inputs: { string: { example: 'Hi, Bob!', required: true } }, exits: { success: { outputExample: 'HI, BOB!'} }, fn: function(inputs, exits) { return exits.success(inputs.string.toUpperCase()); } } }, } }, function(err, _sailsApp) { if (err) { return done(err); } sailsApp = _sailsApp; return done(); }); }); after(function(done) { process.chdir(curDir); if (sailsApp) {sailsApp.lower(done);} else { return done(); } }); it('should load helpers from disk and merge them with programmatically added helpers', function() { assert.equal(_.keys(sailsApp.helpers).length, 2); }); it('should load helpers correctly', function() { assert(_.isFunction(sailsApp.helpers.greet)); assert(_.isFunction(sailsApp.helpers.greet.with)); assert(_.isFunction(sailsApp.helpers.ucase)); assert(_.isFunction(sailsApp.helpers.ucase.with)); }); it('should support "serial" and "natural" usage out of the box, and `.with()` too', function(done) { var result1 = sailsApp.helpers.ucase('Hi, Glen!'); var result2 = sailsApp.helpers.ucase.with({ string: 'Hi, Glen!' }); assert.equal(result1, 'HI, GLEN!'); assert.equal(result2, result1); sailsApp.helpers.greet('Glen').switch({ error: done, success: function (result3) { sailsApp.helpers.greet.with({ name: 'Glen' }).exec(function(err, result4) { if (err) { return done(err); } try { assert.equal(result3, 'Hi, Glen!'); assert.equal(result4, result3); } catch (err) { return done(err); } return done(); });//_∏_ }//> });//_∏_ }); it('should support customization', function(done) { sailsApp.helpers.greet.customize({ arginStyle: 'named', execStyle: 'natural' })({ name: 'Glen' }).then(function(result1){ assert.equal(result1, 'Hi, Glen!'); var result2 = sailsApp.helpers.ucase.customize({ arginStyle: 'serial', execStyle: 'deferred' })('Hi, Glen!').now(); var result3 = sailsApp.helpers.ucase.customize({ arginStyle: 'serial', execStyle: 'deferred' }).with({ string: 'Hi, Glen!' }).now(); assert.equal(result2, 'HI, GLEN!'); assert.equal(result3, result2); return done(); }).catch (function(err){ return done(err); });//_∏_ }); }); }); ================================================ FILE: test/integration/hook.i18n.test.js ================================================ /** * Test dependencies */ var assert = require('assert'); var httpHelper = require('./helpers/httpHelper.js'); var appHelper = require('./helpers/appHelper'); var path = require('path'); var fs = require('fs'); // ██╗ ██╗ █████╗ ███╗ ██╗ ██╗ ██╗ ██████╗ ██████╗ ██╗ ██╗ // ██║███║██╔══██╗████╗ ██║ ██║ ██║██╔═══██╗██╔═══██╗██║ ██╔╝ // ██║╚██║╚█████╔╝██╔██╗ ██║ ███████║██║ ██║██║ ██║█████╔╝ // ██║ ██║██╔══██╗██║╚██╗██║ ██╔══██║██║ ██║██║ ██║██╔═██╗ // ██║ ██║╚█████╔╝██║ ╚████║ ██║ ██║╚██████╔╝╚██████╔╝██║ ██╗ // ╚═╝ ╚═╝ ╚════╝ ╚═╝ ╚═══╝ ╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═╝ // // ██╗ ██████╗ ██╗ ██╗███████╗██████╗ █████╗ ██╗ ██╗ ██╗ // ██╔╝██╔═══██╗██║ ██║██╔════╝██╔══██╗██╔══██╗██║ ██║ ╚██╗ // ██║ ██║ ██║██║ ██║█████╗ ██████╔╝███████║██║ ██║ ██║ // ██║ ██║ ██║╚██╗ ██╔╝██╔══╝ ██╔══██╗██╔══██║██║ ██║ ██║ // ╚██╗╚██████╔╝ ╚████╔╝ ███████╗██║ ██║██║ ██║███████╗███████╗██╔╝ // ╚═╝ ╚═════╝ ╚═══╝ ╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝╚══════╝╚══════╝╚═╝ // describe('i18n ::', function() { var appName = 'testApp'; var sailsApp; before(function(done) { appHelper.build(done); }); beforeEach(function(done) { appHelper.lift({ log: { level: 'silent' }, routes: { '/test_req_getlocale': function(req, res) { res.send(req.getLocale()); }, '/test_req_setlocale': function(req, res) { req.setLocale('es'); res.send(req.i18n.__('Welcome')); }, '/test_sails_getlocale': function(req, res) { res.send(req._sails.hooks.i18n.getLocale()); }, '/test_sails_setlocale': function(req, res) { res.send(req._sails.__('Welcome')); }, } }, function(err, sails) { if (err) { return done(err); } sailsApp = sails; return done(); }); }); afterEach(function(done) { sailsApp.lower(done); }); after(function() { process.chdir('../'); appHelper.teardown(); }); describe('with locales generate by sails-generate-backend', function() { it('should say "Welcome" by default', function() { assert.equal(sailsApp.__('Welcome'), 'Welcome'); }); it('should work using `i18n()` as well as `__()`', function() { assert.equal(sailsApp.i18n('Welcome'), 'Welcome'); }); it('should say "Welcome" in English', function() { sailsApp.hooks.i18n.setLocale('en'); assert.equal(sailsApp.__('Welcome'), 'Welcome'); }); it('should say "Bienvenido" in Spanish', function() { sailsApp.hooks.i18n.setLocale('es'); assert.equal(sailsApp.__('Welcome'), 'Bienvenido'); }); it('should support `req.getLocale()` to get the current locale.', function(done) { // sailsApp.hooks.i18n.setLocale('es'); sailsApp.request({url: 'GET /test_req_getlocale', headers: {'Accept-language': 'es'}}, function(err, res, body) { if (err) { return done(err); } assert.equal(body, 'es'); return done(); }); }); it('should support `req.setLocale()` to set the current locale.', function(done) { sailsApp.request('GET /test_req_setlocale', function(err, res, body) { if (err) { return done(err); } assert.equal(body, 'Bienvenido'); return done(); }); }); it('should support `sails.hooks.i18n.getLocale()` to get the current locale.', function(done) { sailsApp.hooks.i18n.setLocale('es'); sailsApp.request('GET /test_sails_getlocale', function(err, res, body) { if (err) { return done(err); } assert.equal(body, 'es'); return done(); }); }); it('should support `sails.hooks.i18n.setLocale()` to set the current locale.', function(done) { sailsApp.hooks.i18n.setLocale('fr'); sailsApp.request('GET /test_sails_setlocale', function(err, res, body) { if (err) { return done(err); } assert.equal(body, 'Bienvenue'); return done(); }); }); it('should say "Bienvenue" in French', function() { sailsApp.hooks.i18n.setLocale('fr'); assert.equal(sailsApp.__('Welcome'), 'Bienvenue'); }); it('should say "Willkommen" in German', function() { sailsApp.hooks.i18n.setLocale('de'); assert.equal(sailsApp.__('Welcome'), 'Willkommen'); }); }); });//</describe i18n tests> // ██╗ ██╗ █████╗ ███╗ ██╗ ██████╗ ██████╗ ███╗ ██╗███████╗██╗ ██████╗ // ██║███║██╔══██╗████╗ ██║ ██╔════╝██╔═══██╗████╗ ██║██╔════╝██║██╔════╝ // ██║╚██║╚█████╔╝██╔██╗ ██║ ██║ ██║ ██║██╔██╗ ██║█████╗ ██║██║ ███╗ // ██║ ██║██╔══██╗██║╚██╗██║ ██║ ██║ ██║██║╚██╗██║██╔══╝ ██║██║ ██║ // ██║ ██║╚█████╔╝██║ ╚████║ ╚██████╗╚██████╔╝██║ ╚████║██║ ██║╚██████╔╝ // ╚═╝ ╚═╝ ╚════╝ ╚═╝ ╚═══╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═══╝╚═╝ ╚═╝ ╚═════╝ // describe('i18n Config ::', function() { var appName = 'testApp'; var sailsApp; describe('with locales generate by config', function() { before(function (done) { appHelper.build(function(err) { if (err) {return done(err);} var config = 'module.exports.i18n = { locales: [\'en\', \'de\'], defaultLocale: \'de\' };'; fs.writeFileSync(path.resolve('../', appName, 'config/i18n.js'), config); appHelper.lift({ log: {level: 'silent'} }, function(err, sails) { if (err) { return done(err); } sailsApp = sails; return done(); }); }); }); after(function(done) { sailsApp.lower(function() { process.chdir('../'); appHelper.teardown(); return done(); }); }); it('should say "Willkommen" by defaultLocale', function() { //see https://github.com/balderdashy/sails-generate-backend/pull/10 assert(sailsApp.__('Welcome') === 'Willkommen'); }); it('should autoupdate the file', function(done) { sailsApp.__('Login'); fs.readFile(path.resolve('../', appName, 'config/locales/de.json'), 'utf8', function read(err, data) { if (err) { return done(err); } var de = JSON.parse(data); assert(de['Login'] === 'Login'); return done(); }); }); }); }); ================================================ FILE: test/integration/hook.policies.test.js ================================================ /** * Module dependencies */ var util = require('util'); var assert = require('assert'); var tmp = require('tmp'); var _ = require('@sailshq/lodash'); var async = require('async'); var Filesystem = require('machinepack-fs'); var Sails = require('../../lib').constructor; tmp.setGracefulCleanup(); describe('policies :: ', function() { describe('basic usage :: ', function() { var curDir, tmpDir; before(function() { // Cache the current working directory. curDir = process.cwd(); // Create a temp directory. tmpDir = tmp.dirSync({gracefulCleanup: true, unsafeCleanup: true}); // Switch to the temp directory. process.chdir(tmpDir.name); Filesystem.writeSync({ force: true, destination: 'api/policies/err.js', string: 'module.exports = function(req, res, next) {return res.serverError(\'Test Error\');}' }).execSync(); }); after(function() { process.chdir(curDir); }); it('should load policies from disk and merge them with programmatically added policies', function(done) { (new Sails()).load({ hooks: { grunt: false, views: false, pubsub: false }, blueprints: { actions: false, rest: false, shortcuts: false }, log: {level: 'silent'}, controllers: {}, routes: {}, policies: { moduleDefinitions: { 'foo': function(req, res, next) {return res.serverError('foo');} }, } }, function(err, sailsApp) { if (err) { return done(err); } assert.equal(_.keys(sailsApp.hooks.policies.middleware).length, 2); assert(sailsApp.hooks.policies.middleware.foo); assert(sailsApp.hooks.policies.middleware.err); return done(); }); }); describe('error policies :: ', function() { var sailsApp; var policyMap = {}; beforeEach(function(done) { (new Sails()).load({ hooks: { grunt: false, views: false, pubsub: false }, blueprints: { actions: false, rest: false, shortcuts: false }, log: {level: 'silent'}, controllers: { moduleDefinitions: { 'user': function(req, res) { return res.send('user'); }, 'user/foo': function(req, res) { return res.send('user.foo'); }, 'user/foo/bar': function(req, res) { return res.send('user.foo.bar'); } } }, routes: { '/user': 'user', '/user-foo': 'user/foo', '/user-foo-bar': 'user/foo/bar' }, policies: policyMap }, function(err, _sails) { if (err) { return done(err); } sailsApp = _sails; return done(); }); }); afterEach(function(done){ if (sailsApp) {sailsApp.lower(done);} else { return done(); } }); describe('with a single, defined "error" policy mapped to user/*', function() { before(function() { policyMap = { 'user/*': ['err'] }; }); it('the policy should apply to all user/* actions', function(done) { async.each(['/user', '/user-foo', '/user-foo-bar'], function(url, cb) { sailsApp.request({ url: url, method: 'GET' }, function (err, response, data) { if (!err) { return cb(new Error('For URL ' + url + ', expected server error, got: ' + data)); } assert.equal(err.body, 'Test Error'); return cb(); }); }, function (err) { if (err) {return done(err);} return done(); }); }); }); describe('with a `false` policy mapped to user/*', function() { before(function() { policyMap = { 'user/*': false }; }); it('the policy should apply to all user/* actions', function(done) { async.each(['/user', '/user-foo', '/user-foo-bar'], function(url, cb) { sailsApp.request({ url: url, method: 'GET' }, function (err, response, data) { if (!err) { return cb(new Error('For URL ' + url + ', expected server error, got: ' + data)); } assert.equal(err.status, 403); return cb(); }); }, function (err) { if (err) {return done(err);} return done(); }); }); }); describe('with a defined "error" policy mapped to user/* and a "blank" policy mapped to user/foo', function() { before(function() { policyMap = { 'user/*': ['err'], 'user/Foo': [] // <-- Note the uppercase F -- this should not matter. }; }); it('the policy should apply to actions `user` and `user/foo/bar`', function(done) { async.each(['/user', '/user-foo-bar'], function(url, cb) { sailsApp.request({ url: url, method: 'GET' }, function (err, response, data) { if (!err) { return cb(new Error('For URL ' + url + ', expected server error, got: ' + data)); } assert.equal(err.body, 'Test Error'); return cb(); }); }, function (err) { if (err) {return done(err);} return done(); }); }); it('the policy should NOT apply to actions `user/foo`', function(done) { sailsApp.request({ url: '/user-foo', method: 'GET' }, function (err, response, data) { if (err) { return done(new Error('For URL /user-foo, expected "user-foo", got: ' + err)); } assert.equal(data, 'user.foo'); return done(); }); }); }); describe('with a defined "error" policy mapped to user/* and a `true` policy mapped to user/foo', function() { before(function() { policyMap = { 'user/*': ['err'], 'user/foo': true }; }); it('the policy should apply to actions `user` and `user/foo/bar`', function(done) { async.each(['/user', '/user-foo-bar'], function(url, cb) { sailsApp.request({ url: url, method: 'GET' }, function (err, response, data) { if (!err) { return cb(new Error('For URL ' + url + ', expected server error, got: ' + data)); } assert.equal(err.body, 'Test Error'); return cb(); }); }, function (err) { if (err) {return done(err);} return done(); }); }); it('the policy should NOT apply to actions `user/foo`', function(done) { sailsApp.request({ url: '/user-foo', method: 'GET' }, function (err, response, data) { if (err) { return cb(new Error('For URL ' + url + ', expected "user-foo", got: ' + err)); } assert.equal(data, 'user.foo'); return done(); }); }); }); describe('with a defined "error" policy mapped to * and a `true` policy mapped to user/foo', function() { before(function() { policyMap = { '*': ['err'], 'user/foo': true }; }); it('the policy should apply to actions `user` and `user/foo/bar`', function(done) { async.each(['/user', '/user-foo-bar'], function(url, cb) { sailsApp.request({ url: url, method: 'GET' }, function (err, response, data) { if (!err) { return cb(new Error('For URL ' + url + ', expected server error, got: ' + data)); } assert.equal(err.body, 'Test Error'); return cb(); }); }, function (err) { if (err) {return done(err);} return done(); }); }); it('the policy should NOT apply to actions `user/foo`', function(done) { sailsApp.request({ url: '/user-foo', method: 'GET' }, function (err, response, data) { if (err) { return done(new Error('For URL /user-foo, expected "user-foo", got: ' + err)); } assert.equal(data, 'user.foo'); return done(); }); }); }); describe('with a defined "error" policy mapped to user/* and a "blank" policy mapped to user/foo/*', function() { before(function() { policyMap = { 'user/*': ['err'], 'user/foo/*': [] }; }); it('the policy should apply to actions `user`', function(done) { async.each(['/user'], function(url, cb) { sailsApp.request({ url: url, method: 'GET' }, function (err, response, data) { if (!err) { return cb(new Error('For URL ' + url + ', expected server error, got: ' + data)); } assert.equal(err.body, 'Test Error'); return cb(); }); }, function (err) { if (err) {return done(err);} return done(); }); }); it('the policy should NOT apply to actions `user/foo`', function(done) { sailsApp.request({ url: '/user-foo', method: 'GET' }, function (err, response, data) { if (err) { return cb(new Error('For URL ' + url + ', expected "user-foo", got: ' + err)); } assert.equal(data, 'user.foo'); return done(); }); }); it('the policy should NOT apply to actions `user/foo/bar`', function(done) { sailsApp.request({ url: '/user-foo-bar', method: 'GET' }, function (err, response, data) { if (err) { return cb(new Error('For URL ' + url + ', expected "user-foo-bar", got: ' + err)); } assert.equal(data, 'user.foo.bar'); return done(); }); }); }); }); describe('pass-thru policies', function() { var sailsApp; var policyMap = {}; beforeEach(function(done) { (new Sails()).load({ hooks: { grunt: false, views: false, pubsub: false }, blueprints: { actions: false, rest: false, shortcuts: false }, log: {level: 'silent'}, controllers: { moduleDefinitions: { 'user': function(req, res) { return res.json({action: 'user', animals: {cat: req.options.cat, owl: req.options.owl}}); }, 'user/foo': function(req, res) { return res.json({action: 'user.foo', animals: {cat: req.options.cat, owl: req.options.owl}}); }, 'user/foo/bar': function(req, res) { return res.json({action: 'user.foo.bar', animals: {cat: req.options.cat, owl: req.options.owl}}); }, 'user/foo/baz': function(req, res) { throw new Error('should never be reached!'); } } }, routes: { '/user': 'user', '/user-foo': 'user/foo', '/user-foo-bar': 'user/foo/bar', '/user-foo-baz': 'user/foo/baz' }, policies: _.extend({ moduleDefinitions: { 'add-owl': function(req, res, next) {req.options.owl = 'hoot'; return next();}, 'add-cat': function(req, res, next) {req.options.cat = 'meow'; return next();} }, },policyMap) }, function(err, _sails) { if (err) { return done(err); } sailsApp = _sails; return done(); }); }); afterEach(function(done){ if (sailsApp) {sailsApp.lower(done);} else { return done(); } }); describe('with a single, defined "pass-thru" policy mapped to user.*', function() { before(function() { policyMap = { 'user/*': ['add-owl'] }; }); it('the policy should apply to all user/* actions', function(done) { async.each(['/user', '/user-foo', '/user-foo-bar'], function(url, cb) { sailsApp.request({ url: url, method: 'GET' }, function (err, response, data) { if (err) { return cb(new Error('For URL ' + url + ', expected successful response, got: ' + err)); } assert.equal(data.action, url.substr(1).replace(/-/g,'.')); assert.equal(data.animals.owl, 'hoot'); assert(_.isUndefined(data.animals.cat)); return cb(); }); }, function (err) { if (err) {return done(err);} return done(); }); }); }); describe('with two defined "pass-thru" policies chained to user/*', function() { before(function() { policyMap = { 'user/*': ['add-owl', 'add-cat'] }; }); it('the policies should apply to all user/* actions', function(done) { async.each(['/user', '/user-foo', '/user-foo-bar'], function(url, cb) { sailsApp.request({ url: url, method: 'GET' }, function (err, response, data) { if (err) { return cb(new Error('For URL ' + url + ', expected successful response, got: ' + err)); } assert.equal(data.action, url.substr(1).replace(/-/g,'.')); assert.equal(data.animals.owl, 'hoot'); assert.equal(data.animals.cat, 'meow'); return cb(); }); }, function (err) { if (err) {return done(err);} return done(); }); }); }); describe('with the "add-owl" policy on user/*, and "add-cat" on user/foo/bar', function() { before(function() { policyMap = { 'user/*': ['add-owl'], 'user/foo/bar': ['add-cat'] }; }); it('the "add-owl" policy (and NOT the "add-cat" policy) should apply to the "user" and "user/foo" actions', function(done) { async.each(['/user', '/user-foo'], function(url, cb) { sailsApp.request({ url: url, method: 'GET' }, function (err, response, data) { if (err) { return cb(new Error('For URL ' + url + ', expected successful response, got: ' + err)); } assert.equal(data.action, url.substr(1).replace(/-/g,'.')); assert.equal(data.animals.owl, 'hoot'); assert(_.isUndefined(data.animals.cat)); return cb(); }); }, function (err) { if (err) {return done(err);} return done(); }); }); it('the "add-cat" policy (and NOT the "add-owl" policy) should apply to the "user/foo/bar" action', function(done) { sailsApp.request({ url: '/user-foo-bar', method: 'GET' }, function (err, response, data) { if (err) { return cb(new Error('For URL ' + url + ', expected successful response, got: ' + err)); } assert.equal(data.action, 'user.foo.bar'); assert.equal(data.animals.cat, 'meow'); assert(_.isUndefined(data.animals.owl)); return done(); }); }); }); describe('(using controller config) with the "add-owl" policy on user/*, and "add-cat" on user/foo/bar', function() { before(function() { policyMap = { 'User': { '*': 'add-OWL', // Capitalization shouldn't matter for policy name... 'foo': ['add-cat'] }, 'user/FooController': { '*': false, 'Bar': 'add-cat' // Capitalization shouldn't matter for action name... } }; }); it('the "add-owl" policy (and NOT the "add-cat" policy) should apply to the "user" action', function(done) { sailsApp.request({ url: '/user', method: 'GET' }, function (err, response, data) { if (err) { return done(new Error('For URL \'/user\', expected successful response, got: ' + err)); } assert.equal(data.action, 'user'); assert.equal(data.animals.owl, 'hoot'); assert(_.isUndefined(data.animals.cat)); return done(); }); }); it('the "add-cat" policy (and NOT the "add-owl" policy) should apply to the "user/foo" and "user/foo/bar" actions', function(done) { async.each(['/user-foo', '/user-foo-bar'], function(url, cb) { sailsApp.request({ url: url, method: 'GET' }, function (err, response, data) { if (err) { return cb(new Error('For URL ' + url + ', expected successful response, got: ' + err)); } assert.equal(data.action, url.substr(1).replace(/-/g,'.')); assert.equal(data.animals.cat, 'meow'); assert(_.isUndefined(data.animals.owl)); return cb(); }); }, function (err) { if (err) {return done(err);} return done(); }); }); it('the "user/foo/baz" route should always return "Forbidden"', function(done) { sailsApp.request({ url: '/user-foo-baz', method: 'GET' }, function (err, response, data) { if (!err) { return done(new Error('For URL \'/user/foo/baz\', expected server error, got: ' + data)); } assert.equal(err.status, 403); return done(); }); }); }); }); describe('Adding policies directly to routes', function() { var sailsApp; var policyMap = {}; before(function(done) { (new Sails()).load({ hooks: { grunt: false, views: false, pubsub: false }, blueprints: { actions: false, rest: false, shortcuts: false }, log: {level: 'silent'}, controllers: { moduleDefinitions: { 'user': function(req, res) { return res.json({action: 'user', foo: req.options.foo, animals: {cat: req.options.cat, owl: req.options.owl}}); }, 'user/foo': function(req, res) { return res.json({action: 'user/foo', foo: req.options.foo, animals: {cat: req.options.cat, owl: req.options.owl}}); }, 'user/foo/bar': function(req, res) { return res.json({action: 'user/foo/bar', foo: req.options.foo, animals: {cat: req.options.cat, owl: req.options.owl}}); } } }, routes: { '/user': [{ policy: 'add-owl', foo: 'bar' }, 'user'], '/user-foo': [{ policy: 'add-cat' }, 'user/foo'], '/user-foo-bar': [ { policy: 'add-owl'}, { policy: 'add-cat' }, 'user/foo/bar'] }, policies: _.extend({ moduleDefinitions: { 'add-owl': function(req, res, next) {req.options.owl = 'hoot'; return next();}, 'add-cat': function(req, res, next) {req.options.cat = 'meow'; return next();} }, },policyMap) }, function(err, _sails) { if (err) { return done(err); } sailsApp = _sails; return done(); }); }); after(function(done){ if (sailsApp) {sailsApp.lower(done);} else { return done(); } }); it('should add the correct policy to `/user` and retain extra options', function(done) { sailsApp.request({ url: '/user', method: 'GET' }, function (err, response, data) { if (err) { return cb(new Error('For URL ' + url + ', expected successful response, got: ' + err)); } assert.equal(data.action, 'user'); assert.equal(data.animals.owl, 'hoot'); assert(_.isUndefined(data.animals.cat)); assert.equal(data.foo, 'bar'); return done(); }); }); it('should add the correct policy to `/user-foo`', function(done) { sailsApp.request({ url: '/user-foo', method: 'GET' }, function (err, response, data) { if (err) { return cb(new Error('For URL ' + url + ', expected successful response, got: ' + err)); } assert.equal(data.action, 'user/foo'); assert.equal(data.animals.cat, 'meow'); assert(_.isUndefined(data.animals.owl)); assert(_.isUndefined(data.foo)); return done(); }); }); it('should add the correct policies to `/user-foo-bar`', function(done) { sailsApp.request({ url: '/user-foo-bar', method: 'GET' }, function (err, response, data) { if (err) { return cb(new Error('For URL ' + url + ', expected successful response, got: ' + err)); } assert.equal(data.action, 'user/foo/bar'); assert.equal(data.animals.cat, 'meow'); assert.equal(data.animals.owl, 'hoot'); assert(_.isUndefined(data.foo)); return done(); }); }); }); }); describe('with invalid configuration :: ', function() { describe('when a non-function policy is specified on disk', function() { var curDir, tmpDir; before(function(done) { // Cache the current working directory. curDir = process.cwd(); // Create a temp directory. tmpDir = tmp.dirSync({gracefulCleanup: true, unsafeCleanup: true}); // Switch to the temp directory. process.chdir(tmpDir.name); Filesystem.writeSync({ force: true, destination: 'api/policies/err.js', string: 'module.exports = {"foo": "bar"}' }).execSync(); return done(); }); after(function() { process.chdir(curDir); }); it('Sails should fail to lift', function(done) { (new Sails()).load({ hooks: { grunt: false, views: false, pubsub: false }, log: {level: 'silent'} }, function(err, sailsApp) { if (!err) { sailsApp.lower(function() { return done(new Error('Expected error lifting, but didn\'t get one!')); }); } return done(); }); }); }); describe('when a non-function policy is specified programmatically', function() { it('Sails should fail to lift', function(done) { (new Sails()).load({ hooks: { grunt: false, views: false, pubsub: false, orm: false }, log: {level: 'silent'}, policies: { moduleDefinitions: { foo: 'bar' } } }, function(err, sailsApp) { if (!err) { sailsApp.lower(function() { return done(new Error('Expected error lifting, but didn\'t get one!')); }); } return done(); }); }); }); }); }); ================================================ FILE: test/integration/hook.pubsub.modelEvents.noSubscribers.test.js ================================================ /** * Test dependencies */ var util = require('util'); var assert = require('assert'); var socketHelper = require('./helpers/socketHelper.js'); var appHelper = require('./helpers/appHelper'); /** * Errors */ var Err = { badResponse: function(response) { return 'Wrong server response! Response :::\n' + util.inspect(response); } }; describe('pubsub :: ', function() { var sailsprocess; var socket1; var socket2; var appName = 'testApp'; describe('Model events', function() { describe('when no one is subscribed to user #1 and User has no watchers ', function() { before(function(done) { appHelper.buildAndLiftWithTwoSockets(appName, function(err, sails, _socket1, _socket2) { if (err) { throw new Error(err); } sailsprocess = sails; socket1 = _socket1; socket2 = _socket2; done(); }); }); after(function(done) { socket1.disconnect(); socket2.disconnect(); process.chdir('../'); appHelper.teardown(); if (sailsprocess) { return sailsprocess.lower(function() { setTimeout(done, 100); }); } return done(); }); this.slow(3000); afterEach(function(done) { socket2.removeAllListeners(); done(); }); it('a post request to /user should result in no `user` events being received', function(done) { socket2.on('user', function(message) { assert(false, 'User event received by socket 2 when it should not have been!'); }); socket1.post('/user', { name: 'scott' }); setTimeout(done, 1000); }); it('updating the user via PUT /user/1 should result in no `user` events being received', function(done) { socket2.on('user', function(message) { assert(false, 'User event received by socket 2 when it should not have been!'); }); socket1.put('/user/1', { name: 'joe' }); setTimeout(done, 1000); }); it('adding a pet to the user via POST /pet should result in no `user` events being received', function(done) { socket2.on('user', function(message) { assert(false, 'User event received by socket 2 when it should not have been!'); }); socket1.post('/pet', { name: 'rex', owner: 1 }); setTimeout(done, 1000); }); it('removing a pet from the user via DELETE /pet/1 should result in no `user` events being received', function(done) { socket2.on('user', function(message) { assert(false, 'User event received by socket 2 when it should not have been!'); }); socket1.delete('/pet/1'); setTimeout(done, 1000); }); it('deleting the user via DELETE /user/1 should result in no `user` events being received', function(done) { socket2.on('user', function(message) { assert(false, 'User event received by socket 2 when it should not have been!'); }); socket1.delete('/user/1'); setTimeout(done, 1000); }); }); }); }); ================================================ FILE: test/integration/hook.pubsub.modelEvents.subscribers.test.js ================================================ /** * Test dependencies */ var util = require('util'); var path = require('path'); var _ = require('@sailshq/lodash'); var assert = require('assert'); var async = require('async'); var socketHelper = require('./helpers/socketHelper.js'); var appHelper = require('./helpers/appHelper'); var fs = require('fs-extra'); /** * Errors */ var Err = { badResponse: function(response) { return 'Wrong server response! Response :::\n' + util.inspect(response); } }; describe('pubsub :: ', function() { describe('Model events', function() { describe('when a socket is watching Users ', function() { var socket1; var socket2; var appName = 'testApp'; var sailsApp; var bootstrapModels = {}; var bootstrappedData = {}; before(appName, function(done) { appHelper.build(done); }); beforeEach(function (done) { appHelper.liftWithTwoSockets({ log: {level: 'warn'}, models: { migrate: 'drop' } }, function(err, sails, _socket1, _socket2) { if (err) { return done(err); } sailsApp = sails; socket1 = _socket1; socket2 = _socket2; async.eachSeries(_.keys(bootstrapModels), function(model, nextModel) { sailsApp.models[model].createEach(bootstrapModels[model]).meta({fetch: true}).exec(function(err, records) { if (err) { return nextModel(err); } bootstrappedData[model] = records; return nextModel(); }); }, function(err) { if (err) {return done(err);} // Subscribe to all users and new user notifications. socket1.get('/user', function(body, jwr) { if (jwr.error) { return done(new Error('Error in tests. Details:' + JSON.stringify(jwr))); } // Subscribe to all pets and new pet notifications. socket1.get('/pet', function(body, jwr) { if (jwr.error) { return done(new Error('Error in tests. Details:' + JSON.stringify(jwr))); } done(); }); }); }); }); }); afterEach(function(done) { bootstrapModels = {}; bootstrappedData = {}; socket1.removeAllListeners(); socket2.removeAllListeners(); var dir = path.resolve('.tmp', 'localDiskDb'); if (fs.existsSync(dir)) { fs.removeSync(dir); } setTimeout(function(){sailsApp.lower(done);},100); }); after(function(done) { // Add a delay before killing the app to account for any queries that // haven't been run by the blueprints yet; otherwise they might casue an error setTimeout(function() { process.chdir('../'); appHelper.teardown(appName); return done(); }, 500); });//</after> // ██████╗██████╗ ███████╗ █████╗ ████████╗███████╗ // ██╔════╝██╔══██╗██╔════╝██╔══██╗╚══██╔══╝██╔════╝ // ██║ ██████╔╝█████╗ ███████║ ██║ █████╗ // ██║ ██╔══██╗██╔══╝ ██╔══██║ ██║ ██╔══╝ // ╚██████╗██║ ██║███████╗██║ ██║ ██║ ███████╗ // ╚═════╝╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝ ╚═╝ ╚══════╝ describe('creating a new user with POST /user', function() { it('should cause a `created` notification to be received by all `user` subscribers', function(done) { expectNotifications({ user: { created: { verb: 'created', id: 1, 'data.name': 'bert' } } }, done); socket2.post('/user', { name: 'bert' }, function (body, jwr) { if (jwr.error) { return done(jwr.error); } // Otherwise, the event handler above should fire (or this test will time out and fail). }); }); }); describe('creating a new pet with POST /pet that includes a value for a singular association in a one-to-many relationship', function () { before(function() { bootstrapModels = { user: [{name: 'bob'}] }; }); it('should cause an `addedTo` notification to be received by all subscribers to the child record', function(done) { expectNotifications({ pet: { created: { verb: 'created', id: 1, 'data.name': 'alice' } }, user: { addedTo: { verb: 'addedTo', id: 1, addedId: 1, attribute: 'pets' } } }, done); socket2.post('/pet', { name: 'alice', owner: 1 }, function (body, jwr) { if (jwr.error) { return done(jwr.error); } // Otherwise, the event handler above should fire (or this test will time out and fail). }); }); }); describe('creating a new user with POST /user that includes a value for a plural association in a many-to-one relationship', function () { describe('and the other side was not already linked to a record', function() { before(function() { bootstrapModels = { pet: [{ name: 'alice'}, {name: 'mr. bailey'}, {name: 'tex'}] }; }); it('should cause an `updated` notification to be received by all subscribers to the child record', function(done) { expectNotifications({ user: { created: { verb: 'created', id: 1, 'data.name': 'bert' } }, pet: { updatedAlice: { verb: 'updated', id: 1, 'data.owner': 1 }, updatedBailey: { verb: 'updated', id: 2, 'data.owner': 1 }, updatedTex: { verb: 'updated', id: 3, 'data.owner': 1 } } }, done); socket2.post('/user', { name: 'bert', pets: [1, 2, 3] }, function (body, jwr) { if (jwr.error) { return done(jwr.error); } // Otherwise, the event handler above should fire (or this test will time out and fail). }); }); }); describe('and the other side was already linked to a record', function() { before(function() { bootstrapModels = { user: [{ name: 'bert' }], pet: [{ name: 'alice', owner: 1}, {name: 'mr. bailey'}, {name: 'tex', owner: 1}] }; }); it('should cause an `updated` notification to be received by all subscribers to the child record, and a `removedFrom` notification to be received by all subscribers to the child\'s former parent record', function(done) { expectNotifications({ user: { created: { verb: 'created', id: 2, 'data.name': 'ernie' }, removedAlice: { verb: 'removedFrom', id: 1, removedId: 1, attribute: 'pets' }, removedText: { verb: 'removedFrom', id: 1, removedId: 3, attribute: 'pets' } }, pet: { updatedAlice: { verb: 'updated', id: 1, 'data.owner': 2 }, updatedBailey: { verb: 'updated', id: 2, 'data.owner': 2 }, updatedTex: { verb: 'updated', id: 3, 'data.owner': 2 } } }, done); socket2.post('/user', { name: 'ernie', pets: [1, 2, 3] }, function (body, jwr) { if (jwr.error) { return done(jwr.error); } // Otherwise, the event handler above should fire (or this test will time out and fail). }); }); }); }); describe('creating a new user with POST /user that includes a value for a plural association in a many-to-many relationship', function () { before(function() { bootstrapModels = { pet: [{ name: 'alice' }, {name: 'mr. bailey'}, {name: 'tex' }] }; }); it('should cause an `addedTo` notification to be received by all subscribers to the child record', function(done) { expectNotifications({ user: { created: { verb: 'created', id: 1, 'data.name': 'bert' } }, pet: { addedToAlice: { verb: 'addedTo', attribute: 'vets', id: 1, addedId: 1 }, addedToBailey: { verb: 'addedTo', attribute: 'vets', id: 2, addedId: 1 }, addedToTex: { verb: 'addedTo', attribute: 'vets', id: 3, addedId: 1 } } }, done); socket2.post('/user', { name: 'bert', patients: [1, 2, 3] }, function (body, jwr) { if (jwr.error) { return done(jwr.error); } // Otherwise, the event handler above should fire (or this test will time out and fail). }); }); }); // ██╗ ██╗██████╗ ██████╗ █████╗ ████████╗███████╗ // ██║ ██║██╔══██╗██╔══██╗██╔══██╗╚══██╔══╝██╔════╝ // ██║ ██║██████╔╝██║ ██║███████║ ██║ █████╗ // ██║ ██║██╔═══╝ ██║ ██║██╔══██║ ██║ ██╔══╝ // ╚██████╔╝██║ ██████╔╝██║ ██║ ██║ ███████╗ // ╚═════╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚══════╝ // describe('updating a record with PATCH /user', function() { before(function() { bootstrapModels = { user: [{ name: 'bert' }] }; }); it('should cause an `updated` notification to be received by all subscribers to the parent record', function(done) { expectNotifications({ user: { updated: { verb: 'updated', id: 1, 'data.name': 'ernie', 'previous.name': 'bert' } } }, done); socket2.patch('/user/1', { name: 'ernie' }, function (body, jwr) { if (jwr.error) { return done(jwr.error); } // Otherwise, the event handler above should fire (or this test will time out and fail). }); }); }); describe('updating a record with PUT /pet with a new value for a singular association', function() { describe('where the previous value was `null`', function() { before(function() { bootstrapModels = { user: [{ name: 'bert' }], pet: [{name: 'alice', owner: null}] }; }); it('should cause an `addedTo` notification to be sent to all subscribers to the new parent record', function(done) { expectNotifications({ pet: { updated: { verb: 'updated', id: 1, 'data.owner': 1, 'previous.owner': null } }, user: { addedTo: { verb: 'addedTo', id: 1, attribute: 'pets', addedId: 1 } } }, done); socket2.patch('/pet/1', { owner: 1 }, function (body, jwr) { if (jwr.error) { return done(jwr.error); } // Otherwise, the event handler above should fire (or this test will time out and fail). }); }); }); describe('where the previous value was not `null`', function() { before(function() { bootstrapModels = { user: [{ name: 'bert' }, {name: 'ernie'}], pet: [{name: 'alice', owner: 1}] }; }); it('should cause a `removedFrom` notification to be sent to all subscribers to the old parent record', function(done) { expectNotifications({ pet: { updated: { verb: 'updated', id: 1, 'data.owner': 2, 'previous.owner.user_id': 1 } }, user: { addedTo: { verb: 'addedTo', id: 2, attribute: 'pets', addedId: 1 }, removedFrom: { verb: 'removedFrom', id: 1, attribute: 'pets', removedId: 1 } } }, done); socket2.patch('/pet/1', { owner: 2 }, function (body, jwr) { if (jwr.error) { return done(jwr.error); } // Otherwise, the event handler above should fire (or this test will time out and fail). }); }); }); }); // █████╗ ██████╗ ██████╗ // ██╔══██╗██╔══██╗██╔══██╗ // ███████║██║ ██║██║ ██║ // ██╔══██║██║ ██║██║ ██║ // ██║ ██║██████╔╝██████╔╝ // ╚═╝ ╚═╝╚═════╝ ╚═════╝ // describe('adding a pet to a user with PUT /user/1/pets/1 where pets->owner is a many-to-one relationship', function () { describe('and the other side was not already linked to a record', function() { before(function() { bootstrapModels = { user: [{name: 'bert'}], pet: [{ name: 'alice'}] }; }); it('should cause an `addedTo` notification to be received by all subscribers to the parent record, and an `updated` notification to be received by all subscribers to the child record', function(done) { expectNotifications({ user: { addedTo: { verb: 'addedTo', id: 1, attribute: 'pets', addedId: 1 } }, pet: { updatedAlice: { verb: 'updated', id: 1, 'data.owner': 1 } } }, done); socket2.put('/user/1/pets/1', {}, function (body, jwr) { if (jwr.error) { return done(jwr.error); } // Otherwise, the event handler above should fire (or this test will time out and fail). }); }); }); describe('and the other side was already linked to a record', function() { before(function() { bootstrapModels = { user: [{ name: 'bert' }, { name: 'ernie' }], pet: [{ name: 'alice', owner: 1}] }; }); it('should cause an `updated` notification to be received by all subscribers to the child record, and a `removedFrom` notification to be received by all subscribers to the child\'s former parent record', function(done) { expectNotifications({ user: { addedTo: { verb: 'addedTo', id: 2, attribute: 'pets', addedId: 1 }, removedFrom: { verb: 'removedFrom', id: 1, removedId: 1, attribute: 'pets' } }, pet: { updatedAlice: { verb: 'updated', id: 1, 'data.owner': 2 } } }, done); socket2.put('/user/2/pets/1', {}, function (body, jwr) { if (jwr.error) { return done(jwr.error); } // Otherwise, the event handler above should fire (or this test will time out and fail). }); }); }); }); describe('adding a patient to a user with PUT /user/1/patients/1 where patients->vets is a many-to-many relationship', function () { before(function() { bootstrapModels = { user: [{name: 'bert'}], pet: [{ name: 'alice' }] }; }); it('should cause an `addedTo` notification to be received by all subscribers to the child record', function(done) { expectNotifications({ pet: { addedTo: { verb: 'addedTo', id: 1, attribute: 'vets', addedId: 1 } }, user: { addedTo: { verb: 'addedTo', id: 1, attribute: 'patients', addedId: 1 } } }, done); socket2.put('/user/1/patients/1', {}, function (body, jwr) { if (jwr.error) { return done(jwr.error); } // Otherwise, the event handler above should fire (or this test will time out and fail). }); }); }); // ██████╗ ███████╗███╗ ███╗ ██████╗ ██╗ ██╗███████╗ // ██╔══██╗██╔════╝████╗ ████║██╔═══██╗██║ ██║██╔════╝ // ██████╔╝█████╗ ██╔████╔██║██║ ██║██║ ██║█████╗ // ██╔══██╗██╔══╝ ██║╚██╔╝██║██║ ██║╚██╗ ██╔╝██╔══╝ // ██║ ██║███████╗██║ ╚═╝ ██║╚██████╔╝ ╚████╔╝ ███████╗ // ╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝ ╚═════╝ ╚═══╝ ╚══════╝ // describe('removing a pet from a user with DELETE /user/1/pets/1 where pets->owner is a many-to-one relationship', function () { before(function() { bootstrapModels = { user: [{ name: 'bert' }], pet: [{ name: 'alice', owner: 1}] }; }); it('should cause a `removedFrom` notification to be received by all subscribers to the parent record, and an `updated` notification to be received by all subscribers to the child record', function(done) { expectNotifications({ user: { removedFrom: { verb: 'removedFrom', id: 1, attribute: 'pets', removedId: 1 }, }, pet: { updatedAlice: { verb: 'updated', id: 1, 'data.owner': null } } }, done); socket2.delete('/user/1/pets/1', {}, function (body, jwr) { if (jwr.error) { return done(jwr.error); } // Otherwise, the event handler above should fire (or this test will time out and fail). }); }); }); describe('removing a patient from a user with DELETE /user/1/patients/1 where patients->vets is a many-to-many relationship', function () { before(function() { bootstrapModels = { user: [{name: 'bert'}], pet: [{ name: 'alice', vets: [1] }] }; }); it('should cause a `removedFrom` notification to be received by all subscribers to the child record', function(done) { expectNotifications({ pet: { removedFrom: { verb: 'removedFrom', id: 1, attribute: 'vets', removedId: 1 } }, user: { removedFrom: { verb: 'removedFrom', id: 1, attribute: 'patients', removedId: 1 } } }, done); socket2.delete('/user/1/patients/1', {}, function (body, jwr) { if (jwr.error) { return done(jwr.error); } // Otherwise, the event handler above should fire (or this test will time out and fail). }); }); }); // ██████╗ ███████╗██████╗ ██╗ █████╗ ██████╗███████╗ // ██╔══██╗██╔════╝██╔══██╗██║ ██╔══██╗██╔════╝██╔════╝ // ██████╔╝█████╗ ██████╔╝██║ ███████║██║ █████╗ // ██╔══██╗██╔══╝ ██╔═══╝ ██║ ██╔══██║██║ ██╔══╝ // ██║ ██║███████╗██║ ███████╗██║ ██║╚██████╗███████╗ // ╚═╝ ╚═╝╚══════╝╚═╝ ╚══════╝╚═╝ ╚═╝ ╚═════╝╚══════╝ // describe('replacing pets of a user with PUT /user/1/pets where pets->owner is a many-to-one relationship', function () { describe('and some of the replacement pets were already linked to owners', function() { before(function() { bootstrapModels = { user: [{ name: 'bert' }, { name: 'ernie' }], pet: [{ name: 'alice', owner: 1}, {name: 'mr. bailey', owner: 2}, {name: 'tex'}] }; }); it('should cause an `updated` notification to be received by all subscribers to the replacement child records, an `addedTo` notification to be received by all subscribers to the new parent record, a `removedFrom` notification to be received by all subscribers to the new parent record (about replaced children) and a `removedFrom` notification to be received by all subscribers to any "stolen" child\'s former parent record', function(done) { expectNotifications({ user: { addedMrBailey: { verb: 'addedTo', id: 1, attribute: 'pets', addedId: 2 }, addedTex: { verb: 'addedTo', id: 1, attribute: 'pets', addedId: 3 }, removedAlice: { verb: 'removedFrom', id: 1, removedId: 1, attribute: 'pets' }, removedBailey: { verb: 'removedFrom', id: 2, removedId: 2, attribute: 'pets' }, }, pet: { updatedAlice: { verb: 'updated', id: 1, 'data.owner': null }, updatedBailey: { verb: 'updated', id: 2, 'data.owner': 1 }, updatedTex: { verb: 'updated', id: 3, 'data.owner': 1 }, } }, done); socket2.put('/user/1/pets', [2,3], function (body, jwr) { if (jwr.error) { return done(jwr.error); } // Otherwise, the event handler above should fire (or this test will time out and fail). }); }); }); }); describe('replacing patients of a user with PUT /user/1/patients where patients->vets is a many-to-many relationship', function () { before(function() { bootstrapModels = { pet: [{ name: 'alice' }, { name: 'mr. bailey'}, {name: 'tex'}], user: [{name: 'bert', patients: [1,2]}], }; }); it('should cause an `updated` notification to be received by all subscribers to the replacement child records, an `addedTo` notification to be received by all subscribers to the new parent record, and a `removedFrom` notification to be received by all subscribers to the new parent record (about replaced children)', function(done) { expectNotifications({ pet: { addedTex: { verb: 'addedTo', id: 3, attribute: 'vets', addedId: 1 }, removedAlice: { verb: 'removedFrom', id: 1, attribute: 'vets', removedId: 1 } }, user: { addedTex: { verb: 'addedTo', id: 1, attribute: 'patients', addedId: 3 }, removedAlice: { verb: 'removedFrom', id: 1, attribute: 'patients', removedId: 1 } } }, done); socket2.put('/user/1/patients', [2,3], function (body, jwr) { if (jwr.error) { return done(jwr.error); } // Otherwise, the event handler above should fire (or this test will time out and fail). }); }); }); // ██████╗ ███████╗███████╗████████╗██████╗ ██████╗ ██╗ ██╗ // ██╔══██╗██╔════╝██╔════╝╚══██╔══╝██╔══██╗██╔═══██╗╚██╗ ██╔╝ // ██║ ██║█████╗ ███████╗ ██║ ██████╔╝██║ ██║ ╚████╔╝ // ██║ ██║██╔══╝ ╚════██║ ██║ ██╔══██╗██║ ██║ ╚██╔╝ // ██████╔╝███████╗███████║ ██║ ██║ ██║╚██████╔╝ ██║ // ╚═════╝ ╚══════╝╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ // describe('destroying a user with DELETE /user/1 where the user has pets and pets->owner is a many-to-one relationship', function () { before(function() { bootstrapModels = { user: [{ name: 'bert' }], pet: [{ name: 'alice', owner: 1}] }; }); it('should cause a `destroyed` notification to be received by all subscribers to the parent record, and an `updated` notification to be received by all subscribers to the child records', function(done) { expectNotifications({ user: { destroyed: { verb: 'destroyed', id: 1, }, }, pet: { updatedAlice: { verb: 'updated', id: 1, 'data.owner': null } } }, done); socket2.delete('/user/1', {}, function (body, jwr) { if (jwr.error) { return done(jwr.error); } // Otherwise, the event handler above should fire (or this test will time out and fail). }); }); }); describe('destroying a user with DELETE /user/1 where the user has patients and patients->vets is a many-to-many relationship', function () { before(function() { bootstrapModels = { user: [{name: 'bert'}], pet: [{ name: 'alice', vets: [1] }] }; }); it('should cause a `removedFrom` notification to be received by all subscribers to the child record', function(done) { expectNotifications({ user: { destroyed: { verb: 'destroyed', id: 1 } }, pet: { removedFrom: { verb: 'removedFrom', id: 1, attribute: 'vets', removedId: 1 } } }, done); socket2.delete('/user/1', {}, function (body, jwr) { if (jwr.error) { return done(jwr.error); } // Otherwise, the event handler above should fire (or this test will time out and fail). }); }); }); function expectNotifications(notifications, done) { var checklist = {}; var errored = false; _.each(notifications, function(modelNotifications, model) { _.each(modelNotifications, function(validator, identifier) { checklist[model + '.' + identifier] = false; }); socket1.on(model, function(notification){ // console.log(notification); if (errored) {return;} try { if (!_.any(modelNotifications, function(validator, identifier) { if (_.all(validator, function(val, path) { return _.get(notification, path) === val; })) { if (checklist[model + '.' + identifier] === true) { errored = true; throw new Error('Got duplicate `' + identifier + '` notification for model `' + model + '`' ); } checklist[model + '.' + identifier] = true; if (_.all(checklist, function(flag) { return flag === true; })) { done(); } return true; } })) { throw new Error('Unexpected `' + model + '` notification: ' + util.inspect(notification, {depth: null})); } } catch (e) { errored = true; return done(e); } }); }); } });//</describe> });//</describe :: Model events> });//</describe :: pubsub> ================================================ FILE: test/integration/hook.sockets.interpreter.test.js ================================================ /** * Test dependencies */ var util = require('util'); var assert = require('assert'); var socketHelper = require('./helpers/socketHelper.js'); var appHelper = require('./helpers/appHelper'); describe('hook:sockets :: ', function() { var sailsApp; var socket1; var socket2; var appName = 'testApp'; describe('interpreter', function() { before(function(done) { appHelper.buildAndLiftWithTwoSockets(appName, { silly: false }, function(err, sails, _socket1, _socket2) { if (err) { return done(err); } if (!_socket1 || !_socket2) { return done(new Error('Failed to connect test sockets')); } sailsApp = sails; socket1 = _socket1; socket2 = _socket2; done(); }); }); after(function(done) { socket1.disconnect(); socket2.disconnect(); process.chdir('../'); appHelper.teardown(); sailsApp.lower(done); }); afterEach(function(done) { socket1.removeAllListeners(); socket2.removeAllListeners(); done(); }); describe('basic usage', function() { it('should probably be tested using a different helper...'); // TODO: use new sails.io.js client to perform these tests // see http://github.com/balderdashy/sails.io.js }); }); }); ================================================ FILE: test/integration/hook.userconfig.test.js ================================================ /** * Test dependencies */ var assert = require('assert'); var httpHelper = require('./helpers/httpHelper.js'); var appHelper = require('./helpers/appHelper'); var util = require('util'); var path = require('path'); var fs = require('fs-extra'); var Sails = require('../../lib/app'); var async = require('async'); describe('hooks :: ', function() { describe('userconfig hook', function() { var appName = 'testApp'; before(function(done) { appHelper.teardown(); async.series([ function(cb) {fs.outputFile(path.resolve(__dirname,'../../testApp/config/abc.js'), 'module.exports = {"foo":"goo"};', cb);}, function(cb) {fs.outputFile(path.resolve(__dirname,'../../testApp/config/foo/bar.js'), 'module.exports = {"foo":"bar", "abc":123, "betty": "boop"};', cb);}, function(cb) {fs.outputFile(path.resolve(__dirname,'../../testApp/config/lara/bar.js'), 'module.exports = {"horse":"neigh", "pig": "oink", "betty": "spaghetti"};', cb);}, function(cb) {fs.outputFile(path.resolve(__dirname,'../../testApp/config/env/development.js'), 'module.exports = {"cat":"meow"};', cb);}, function(cb) {fs.outputFile(path.resolve(__dirname,'../../testApp/config/env/development/config.js'), 'module.exports = {"owl":"hoot"};', cb);}, function(cb) {fs.outputFile(path.resolve(__dirname,'../../testApp/config/env/test-development.js'), 'module.exports = {"duck":"quack"};', cb);}, function(cb) {fs.outputFile(path.resolve(__dirname,'../../testApp/config/env/test-development/config.js'), 'module.exports = {"dog":"woof"};', cb);}, function(cb) {process.chdir('testApp'); cb();} ], done); }); describe('with default options', function() { var sailsApp; it('should merge config options regardless of file structure', function(done) { sailsApp = Sails(); sailsApp.load({hooks:{grunt:false, pubsub: false}}, function(err, sails) { if (err) { return callback(err); } assert.equal(sails.config.foo, 'bar'); assert.equal(sails.config.abc, 123); assert.equal(sails.config.horse, 'neigh'); assert.equal(sails.config.pig, 'oink'); assert.equal(sails.config.betty, 'spaghetti'); assert.equal(typeof(sails.config.bar), 'undefined'); return done(); }); }); after(function (done){ sailsApp.lower(done); }); }); describe('in development environment', function() { var sails; before(function(done) { sails = Sails(); sails.load({hooks:{grunt:false, pubsub: false}}, done); }); it('should load config from config/env/development.js', function() { assert.equal(sails.config.cat, 'meow'); }); it('should load config from config/env/development/** files', function() { assert.equal(sails.config.owl, 'hoot'); }); it('should not load config from config/env/test-development/** files', function() { assert(!sails.config.dog); }); it('should not load config from config/env/test-development.js', function() { assert(!sails.config.duck); }); after(function (done){ sails.lower(done); }); }); describe('in test-development environment', function() { var sails; before(function(done) { sails = Sails(); sails.load({hooks:{grunt:false, pubsub: false}, environment: 'test-development'}, done); }); it('should load config from config/env/test-development.js', function() { assert.equal(sails.config.duck, 'quack'); }); it('should load config from config/env/test-development/** files', function() { assert.equal(sails.config.dog, 'woof'); }); it('should not load config from config/env/development/** files', function() { assert(!sails.config.owl); }); it('should not load config from config/env/development.js', function() { assert(!sails.config.cat); }); after(function (done){ sails.lower(done); }); }); after(function() { process.chdir('../'); appHelper.teardown(); }); }); }); ================================================ FILE: test/integration/hook.views.test.js ================================================ /** * Test dependencies */ var util = require('util'); var assert = require('assert'); var httpHelper = require('./helpers/httpHelper.js'); var appHelper = require('./helpers/appHelper'); var _ = require('@sailshq/lodash'); var Filesystem = require('machinepack-fs'); var tmp = require('tmp'); var Sails = require('../../lib').constructor; tmp.setGracefulCleanup(); describe('hooks :: ', function() { describe('views hook', function() { var curDir, tmpDir, sailsApp; var sailsConfig = {}; var filesToWrite = {}; afterEach(function(done) { sailsApp.lower(function() { process.chdir(curDir); return done(); }); }); beforeEach(function(done) { // Cache the current working directory. curDir = process.cwd(); // Create a temp directory. tmpDir = tmp.dirSync({gracefulCleanup: true, unsafeCleanup: true}); // Switch to the temp directory. process.chdir(tmpDir.name); // Write a layout file for each test. Filesystem.writeSync({ force: true, destination: 'views/layout.ejs', string: '<!DOCTYPE html><html><head><!-- default layout --></head><body><%- body %></body></html>' }).execSync(); // Write out any files specific to this test. _.each(filesToWrite, function(data, filename) { Filesystem.writeSync({ force: true, destination: filename, string: data }).execSync(); }); // Merge the default config with any config specific to this test. var _config = _.merge({ port: 1342, hooks: {grunt: false, blueprints: false, policies: false, pubsub: false}, log: {level: 'error'}, }, sailsConfig); // Lift Sails for this test. (new Sails()).lift(_config, function(err, _sails) { sailsApp = _sails; return done(err); } ); }); afterEach(function(done) { sailsApp.lower(function() { process.chdir(curDir); return done(); }); }); describe('using res.view', function () { before(function() { sailsConfig = { hooks: {i18n: false}, routes: { '/resView': function(req, res) { return res.view('homepage'); } } }; filesToWrite = { 'views/homepage.ejs': '<!-- Default home page -->' }; }); it('should respond to a get request to localhost:1342 with the requested page wrapped in the default layout', function(done) { httpHelper.testRoute('get', 'resView', function(err, response) { if (err) { return done(new Error(err)); } assert.equal(response.body, '<!DOCTYPE html><html><head><!-- default layout --></head><body><!-- Default home page --></body></html>'); done(); }); }); }); describe('using res.view with i18n', function () { before(function() { sailsConfig = { // We must set i18n.locales because otherwise, the hook will be skipped. i18n: { locales: ['en', 'es'] }, routes: { '/resView': function(req, res) { return res.view('homepage'); } } }; filesToWrite = { 'views/homepage.ejs': '<%= __(\'Welcome\') + i18n(\'Welcome\') %>', 'config/locales/es.json': '{"Welcome":"Bienvenido"}' }; }); it('should respond to a get request to localhost:1342 with the requested page wrapped in the default layout', function(done) { httpHelper.testRoute( 'get', {url: 'resView', headers: {'accept-language': 'es'}}, function(err, response) { if (err) { return done(err); } if (response.statusCode !== 200) { return done(new Error('Should have gotten 200 status code, but instead got '+response.statusCode+' with a response body of: '+util.inspect(response.body, {depth:null})+'')); } try { assert.equal(response.body, '<!DOCTYPE html><html><head><!-- default layout --></head><body>BienvenidoBienvenido</body></html>'); } catch (e) { return done(e); } done(); }); }); }); describe('using exposeLocalsToBrowser', function () { describe('with CSRF enabled', function() { before(function() { sailsConfig = { hooks: {i18n: false}, security: { csrf: true }, routes: { '/expose-locals': function(req, res) { return res.view('show-locals', {foo: 'bar', abc: 123}); } } }; filesToWrite = { 'views/show-locals.ejs': '<%- exposeLocalsToBrowser() %>', }; }); it('should respond to a get request to localhost:1342 with a page containing a script exposing locals, including a csrf token', function(done) { httpHelper.testRoute('get', 'expose-locals', function(err, response) { if (err) { return done(new Error(err)); } assert(response.body.indexOf('foo: unescape(\'bar\')') > -1); assert(response.body.indexOf('abc: unescape(123)') > -1); assert(response.body.indexOf('_csrf: unescape') > -1); done(); }); }); }); describe('with CSRF disabled', function() { before(function() { sailsConfig = { hooks: {i18n: false}, routes: { '/expose-locals': function(req, res) { return res.view('show-locals', {foo: 'bar', abc: 123}); } } }; filesToWrite = { 'views/show-locals.ejs': '<%- exposeLocalsToBrowser() %>', }; }); it('should respond to a get request to localhost:1342 with a page containing a script exposing locals, including a csrf token', function(done) { httpHelper.testRoute('get', 'expose-locals', function(err, response) { if (err) { return done(new Error(err)); } assert(response.body.indexOf('foo: unescape(\'bar\')') > -1); assert(response.body.indexOf('abc: unescape(123)') > -1); assert(response.body.indexOf('_csrf: unescape') < 0); done(); }); }); }); }); describe('using res.view with `sails.config.views.layout = false`', function () { before(function() { sailsConfig = { hooks: {i18n: false}, routes: { '/resView': function(req, res) { return res.view('homepage'); } }, views: { layout: false } }; filesToWrite = { 'views/homepage.ejs': '<!-- Default home page -->' }; }); it('should respond to a get request to localhost:1342 with the requested page NOT wrapped in the default layout', function(done) { httpHelper.testRoute('get', 'resView', function(err, response) { if (err) { return done(new Error(err)); } assert.equal(response.body, '<!-- Default home page -->'); done(); }); }); }); describe('using res.view with {layout: false} in locals', function () { before(function() { sailsConfig = { hooks: {i18n: false}, routes: { '/resView': function(req, res) { return res.view('homepage', {layout: false}); } } }; filesToWrite = { 'views/homepage.ejs': '<!-- Default home page -->' }; }); it('should respond to a get request to localhost:1342 with the requested page NOT wrapped in the default layout', function(done) { httpHelper.testRoute('get', 'resView', function(err, response) { if (err) { return done(new Error(err)); } assert.equal(response.body, '<!-- Default home page -->'); done(); }); }); }); describe('using res.view with an alternate layout', function () { before(function() { sailsConfig = { hooks: {i18n: false}, routes: { '/resView': function(req, res) { return res.view('homepage', {layout: 'alt-layout'}); }, } }; filesToWrite = { 'views/homepage.ejs': '<!-- Default home page -->', 'views/alt-layout.ejs': '<FOO><%-body%></FOO>', }; }); it('should respond to a get request to localhost:1342 with the requested page wrapped in the alternate layout', function(done) { httpHelper.testRoute('get', 'resView', function(err, response) { if (err) { return done(new Error(err)); } assert.equal(response.body, '<FOO><!-- Default home page --></FOO>'); done(); }); }); }); describe('using res.view with an alternate extension for EJS', function () { var nunjucks = require('nunjucks'); before(function() { sailsConfig = { hooks: {i18n: false}, routes: { '/resView': function(req, res) { return res.view('homepage', {boss: 'llama'}); }, }, views: { extension: 'foo', } }; filesToWrite = { 'views/layout.foo': '<!DOCTYPE html><html><head><!-- default layout --></head><body><%- body %></body></html>', 'views/homepage.foo': '<!-- vars like a <%= boss %> -->', }; }); it('should respond to a get request to localhost:1342 with the correct content', function(done) { httpHelper.testRoute('get', 'resView', function(err, response) { if (err) { return done(new Error(err)); } assert.equal(response.body, '<!DOCTYPE html><html><head><!-- default layout --></head><body><!-- vars like a llama --></body></html>'); done(); }); }); }); describe('using res.view with an alternate render fn', function () { var nunjucks = require('nunjucks'); before(function() { sailsConfig = { hooks: {i18n: false}, routes: { '/resView': function(req, res) { return res.view('homepage', {boss: 'dinosaur'}); }, }, views: { layout: false, extension: 'html', getRenderFn: function() { var env = nunjucks.configure({ tags: { variableStart: '<$', variableEnd: '$>', } }); return env.render.bind(env); } } }; filesToWrite = { 'views/homepage.html': '<!-- vars like a <$ boss $> -->', }; }); it('should respond to a get request to localhost:1342 with the correct content', function(done) { httpHelper.testRoute('get', 'resView', function(err, response) { if (err) { return done(new Error(err)); } assert.equal(response.body, '<!-- vars like a dinosaur -->'); done(); }); }); }); describe('using partials', function () { describe('with cacheing turned off', function() { before(function() { sailsConfig = { hooks: {i18n: false}, routes: { '/partials': function(req, res) { return res.view('test-partials'); }, } }; filesToWrite = { 'views/test-partials.ejs': '<BLAP><%- partial(\'./partials/outer.ejs\') %></BLAP>', 'views/partials/outer.ejs': '<FOO><%- partial(\'./nested/inner.ejs\') %></FOO>', 'views/partials/nested/inner.ejs': '<BAR>BAZ!</BAR>' }; }); it('should respond to a get request to localhost:1342 with the correct content, and respond differently to a subsequent request after changing the file contents', function(done) { httpHelper.testRoute('get', 'partials', function(err, response) { if (err) { return done(new Error(err)); } assert.equal(response.body, '<!DOCTYPE html><html><head><!-- default layout --></head><body><BLAP><FOO><BAR>BAZ!</BAR></FOO></BLAP></body></html>'); filesToWrite = { 'views/layout.ejs': '<ZAP><%- body %></ZAP>', 'views/test-partials.ejs': '<APPLE><%- partial(\'./partials/outer.ejs\') %></APPLE>', 'views/partials/outer.ejs': '<ORANGE><%- partial(\'./nested/inner.ejs\') %></ORANGE>', 'views/partials/nested/inner.ejs': '<BANANA>TADA!</BANANA>' }; _.each(filesToWrite, function(data, filename) { Filesystem.writeSync({ force: true, destination: filename, string: data }).execSync(); }); httpHelper.testRoute('get', 'partials', function(err, response) { if (err) { return done(new Error(err)); } assert.equal(response.body, '<ZAP><APPLE><ORANGE><BANANA>TADA!</BANANA></ORANGE></APPLE></ZAP>'); done(); }); }); }); }); describe('with cacheing turned on', function() { before(function() { sailsConfig = { hooks: {i18n: false}, routes: { '/partials': function(req, res) { return res.view('test-partials', {cache: true}); }, } }; filesToWrite = { 'views/test-partials.ejs': '<BLAP><%- partial(\'./partials/outer.ejs\') %></BLAP>', 'views/partials/outer.ejs': '<FOO><%- partial(\'./nested/inner.ejs\') %></FOO>', 'views/partials/nested/inner.ejs': '<BAR>BAZ!</BAR>' }; }); it('should respond to a get request to localhost:1342 with the correct content, and respond the same way to a subsequent request after changing the file contents', function(done) { httpHelper.testRoute('get', 'partials', function(err, response) { if (err) { return done(new Error(err)); } assert.equal(response.body, '<!DOCTYPE html><html><head><!-- default layout --></head><body><BLAP><FOO><BAR>BAZ!</BAR></FOO></BLAP></body></html>'); filesToWrite = { 'views/layout.ejs': '<ZAP><%- body %></ZAP>', 'views/test-partials.ejs': '<APPLE><%- partial(\'./partials/outer.ejs\') %></APPLE>', 'views/partials/outer.ejs': '<ORANGE><%- partial(\'./nested/inner.ejs\') %></ORANGE>', 'views/partials/nested/inner.ejs': '<BANANA>TADA!</BANANA>' }; _.each(filesToWrite, function(data, filename) { Filesystem.writeSync({ force: true, destination: filename, string: data }).execSync(); }); httpHelper.testRoute('get', 'partials', function(err, response) { if (err) { return done(new Error(err)); } assert.equal(response.body, '<!DOCTYPE html><html><head><!-- default layout --></head><body><BLAP><FOO><BAR>BAZ!</BAR></FOO></BLAP></body></html>'); done(); }); }); }); }); }); describe('using renderView', function () { before(function() { sailsConfig = { hooks: {i18n: false}, routes: { '/renderView': function(req, res) { req._sails.renderView('homepage', {}, function(err, html) { return res.send(html); }); } } }; filesToWrite = { 'views/homepage.ejs': '<!-- Default home page -->' }; }); it('should respond to a get request to localhost:1342 with welcome page', function(done) { httpHelper.testRoute('get', 'renderView', function(err, response) { if (err) { return done(new Error(err)); } assert.equal(response.body, '<!DOCTYPE html><html><head><!-- default layout --></head><body><!-- Default home page --></body></html>'); done(); }); }); }); describe('using renderView (with i18n disabled)', function () { before(function() { sailsConfig = { hooks: {i18n: false}, routes: { '/renderView': function(req, res) { req._sails.renderView('homepage', {}, function(err, html) { return res.send(html); }); } }, hooks: { i18n: false } }; filesToWrite = { 'views/homepage.ejs': '<!-- Default home page -->' }; }); it('should respond to a get request to localhost:1342 with welcome page', function(done) { httpHelper.testRoute('get', 'renderView', function(err, response) { if (err) { return done(new Error(err)); } assert.equal(response.body, '<!DOCTYPE html><html><head><!-- default layout --></head><body><!-- Default home page --></body></html>'); done(); }); }); }); }); }); ================================================ FILE: test/integration/hooks.user.test.js ================================================ /** * Test dependencies */ var assert = require('assert'); var httpHelper = require('./helpers/httpHelper.js'); var appHelper = require('./helpers/appHelper'); var util = require('util'); var path = require('path'); var fs = require('fs-extra'); var _ = require('@sailshq/lodash'); describe('hooks :: ', function() { var sailsprocess; describe('defining a user hook', function() { var appName = 'testApp'; before(function() { appHelper.teardown(); }); describe('in api/hooks/shout', function(){ before(function(done) { fs.mkdirs(path.resolve(__dirname, '../..', appName, 'api/hooks'), function(err) { if (err) {return done(err);} fs.copySync(path.resolve(__dirname, 'fixtures/hooks/installable/shout/index.js'), path.resolve(__dirname,'../../testApp/api/hooks/shout/index.js')); process.chdir(path.resolve(__dirname, '../..', appName)); done(); }); }); after(function() { process.chdir('../'); // Sleep for 500ms--otherwise we get timing errors for this test on Windows setTimeout(function() { appHelper.teardown(); }, 500); }); describe('with default settings', function() { var sails; before(function(done) { appHelper.liftQuiet({hooks: {pubsub: false}}, function(err, _sails) { if (err) {return done(err);} sails = _sails; return done(); }); }); after(function(done) { sails.lower(function(){setTimeout(done, 100);}); }); it('should install a hook into `sails.hooks.shout`', function() { assert(sails.hooks.shout); }); it('should use merge the default hook config', function() { assert(sails.config.shout.phrase === 'make it rain', sails.config.shout.phrase); }); it('should bind a /shout route that responds with the default phrase', function(done) { httpHelper.testRoute('GET', 'shout', function(err, resp, body) { assert(body === 'make it rain'); return done(); }); }); }); describe('with hooks.shout set to boolean false', function() { var sails; before(function(done) { appHelper.liftQuiet({hooks: {shout: false, pubsub: false}}, function(err, _sails) { if (err) {return done(err);} sails = _sails; return done(); }); }); after(function(done) { sails.lower(function(){setTimeout(done, 100);}); }); it('should not install a hook into `sails.hooks.shout`', function() { assert(_.isUndefined(sails.hooks.shout)); }); }); describe('with hooks.shout set to the string "false"', function() { var sails; before(function(done) { appHelper.liftQuiet({hooks: {shout: 'false', pubsub: false}}, function(err, _sails) { if (err) {return done(err);} sails = _sails; return done(); }); }); after(function(done) { sails.lower(function(){setTimeout(done, 100);}); }); it('should not install a hook into `sails.hooks.shout`', function() { assert(_.isUndefined(sails.hooks.shout)); }); }); describe('with hook-level config options', function() { var sails; before(function(done) { appHelper.liftQuiet({shout: {phrase: 'yolo'}, hooks:{pubsub: false}}, function(err, _sails) { if (err) {return done(err);} sails = _sails; return done(); }); }); after(function(done) { sails.lower(function(){setTimeout(done, 100);}); }); it('should bind a /shout route that responds with the configured phrase', function(done) { httpHelper.testRoute('GET', 'shout', function(err, resp, body) { assert(body === 'yolo'); return done(); }); }); }); }); if (Number(process.version.match(/^v(\d+\.\d+)/)[1]) >= 7.6) { describe('with an asynchronous initialize function', function() { before(function(done) { fs.mkdirs(path.resolve(__dirname, '../..', appName, 'api/hooks'), function(err) { if (err) {return done(err);} fs.copySync(path.resolve(__dirname, 'fixtures/hooks/installable/async/index.js.txt'), path.resolve(__dirname,'../../testApp/api/hooks/async/index.js')); process.chdir(path.resolve(__dirname, '../..', appName)); done(); }); }); after(function(done) { process.chdir('../'); // Sleep for 500ms--otherwise we get timing errors for this test on Windows setTimeout(function() { appHelper.teardown(); return done(); }, 500); }); describe('that runs succesfully', function() { var sails; before(function(done) { appHelper.liftQuiet({hooks: {pubsub: false}}, function(err, _sails) { if (err) {return done(err);} sails = _sails; return done(); }); }); after(function(done) { sails.lower(done, 100); }); it('should run the initialize function successfully', function() { assert.equal(sails.hooks.async.val, 'foo'); }); }); describe('that has an error', function() { var sails; after(function(done) { if (!sails) { return done(); } sails.lower(function(){setTimeout(function() { process.chdir('../'); // Sleep for 500ms--otherwise we get timing errors for this test on Windows setTimeout(function() { appHelper.teardown(); return done(); }, 500); }, 100);}); }); it('should handle the error gracefully', function(done) { appHelper.liftQuiet({hooks: {pubsub: false}, custom: {reject: true}}, function(err, _sails) { if (err) { assert.equal(err, 'foo'); return done(); } sails = _sails; return done(new Error('Should have failed to lift!')); }); }); }); }); } }); }); ================================================ FILE: test/integration/lift.https.test.js ================================================ var assert = require('assert'); var fs = require('fs'); var request = require('@sailshq/request'); var appHelper = require('./helpers/appHelper'); var path = require('path'); describe('Starting HTTPS sails server with lift', function() { var appName = 'testApp'; before(function(done) { appHelper.build(done); }); after(function() { process.chdir('../'); appHelper.teardown(); }); describe('using sails.config.ssl.key and sails.config.ssl.cert', function() { var sailsServer; before(function() { fs.writeFileSync(path.resolve('../', appName, 'config/env/development.js'), "module.exports = {ssl: {key: require('fs').readFileSync('"+require('path').resolve(__dirname, 'cert','sailstest-key.pem').replace(/\\/g,'\\\\')+"'), cert: require('fs').readFileSync('"+require('path').resolve(__dirname, 'cert','sailstest-cert.pem').replace(/\\/g,'\\\\')+"')}};"); }); after(function(done) { if (sailsServer) { return sailsServer.lower(function(){setTimeout(done, 100);}); } return done(); }); it('should start server without error', function(done) { appHelper.lift(function(err, _sailsServer) { assert(!err); sailsServer = _sailsServer; return done(); }); }); it('should respond to a request to port 1342 with a 200 status code', function(done) { if (!sailsServer) {return done('Bailing due to previous test failure!');} request.get({ url:'https://localhost:1342/', ca: require('fs').readFileSync(require('path').resolve(__dirname, 'cert','sailstest-cert.pem')), }, function(err, response) { assert(!err); assert.equal(response.statusCode, 200); return done(); }); }); }); describe('using sails.config.ssl = true and sails.config.http.serverOptions', function() { var sailsServer; before(function() { fs.writeFileSync(path.resolve('../', appName, 'config/env/development.js'), "module.exports = {ssl: true, http: {serverOptions: { key: require('fs').readFileSync('"+require('path').resolve(__dirname, 'cert','sailstest-key.pem').replace(/\\/g,'\\\\')+"'), cert: require('fs').readFileSync('"+require('path').resolve(__dirname, 'cert','sailstest-cert.pem').replace(/\\/g,'\\\\')+"')}}};"); }); after(function(done) { if (sailsServer) { return sailsServer.lower(function(){setTimeout(done, 100);}); } return done(); }); it('should start server without error', function(done) { appHelper.lift(function(err, _sailsServer) { assert(!err); sailsServer = _sailsServer; return done(); }); }); it('should respond to a request to port 1342 with a 200 status code', function(done) { if (!sailsServer) {return done('Bailing due to previous test failure!');} request.get({ url:'https://localhost:1342/', ca: require('fs').readFileSync(require('path').resolve(__dirname, 'cert','sailstest-cert.pem')), }, function(err, response) { assert(!err); assert.equal(response.statusCode, 200); return done(); }); }); }); }); ================================================ FILE: test/integration/lift.lower.test.js ================================================ var assert = require('assert'); var Sails = require('../../lib/app'); var async = require('async'); var _ = require('@sailshq/lodash'); describe('sails being lifted and lowered (e.g in a test framework)', function() { it('should clean up event listeners', function(done) { // Get a list of all the current listeners on the process. // Note that Mocha adds some listeners, so these might not all be empty arrays! var beforeListeners = { sigusr2: process.listeners('SIGUSR2'), sigint: process.listeners('SIGINT'), sigterm: process.listeners('SIGTERM'), exit: process.listeners('exit') }; // Lift and lower 15 Sails apps in a row, to simulate a testing environment async.forEachOfSeries(Array(15), function(undef, i, cb) { var sailsServer = null; Sails().lift({ port: 1342, environment: process.env.TEST_ENV, log: { level: 'error' }, globals: false, hooks: { grunt: false, i18n: false, session: false } }, function(err, sails) { if (err) { return cb(err); } setTimeout(function() { sails.lower(function(){setTimeout(cb, 100);}); }); }); }, function(err) { if (err) { return done(err); } // Check that we have the same # of listeners as before--that is, // that all listeners that were added when the apps were initialized // were subsequently removed when they were lowered. assert.equal(beforeListeners.sigusr2.length, process.listeners('SIGUSR2').length); assert.equal(beforeListeners.sigterm.length, process.listeners('SIGTERM').length); assert.equal(beforeListeners.exit.length, process.listeners('exit').length); assert.equal(beforeListeners.sigint.length, process.listeners('SIGINT').length); return done(); }); }); //</should clean up event listeners> describe('with NODE_ENV set and Sails environment not configured', function() { var sailsApp; var originalNodeEnv; before(function() { originalNodeEnv = process.env.NODE_ENV; process.env.NODE_ENV = 'foobar'; }); after(function(done) { if (_.isUndefined(originalNodeEnv)) { delete process.env.NODE_ENV; } else { process.env.NODE_ENV = originalNodeEnv; } if (sailsApp) { return sailsApp.lower(done); } else { return done(); } }); it('should change the Sails environment to match NODE_ENV it the Sails environment is not explicitly configured', function(done) { // Save reference to original NODE_ENV. var originalNodeEnv = process.env.NODE_ENV; process.env.NODE_ENV = 'foobar'; // Load `app0` deep in the `'cenote'` Sails().load({ log: { level: 'error' }, globals: false, hooks: { grunt: false, i18n: false, session: false } }, function(err, _sailsApp) { if (err) { return done(err); } sailsApp = _sailsApp; // Assert that NODE_ENV is unchanged. assert.equal('foobar', process.env.NODE_ENV); // Assert that Sails environment has been changed to match NODE_ENV assert.equal('foobar', sailsApp.config.environment); return done(); }); }); }); describe('with Sails environment configured but no NODE_ENV set', function() { var sailsApp; var originalNodeEnv; before(function() { originalNodeEnv = process.env.NODE_ENV; delete process.env.NODE_ENV; }); after(function(done) { if (_.isUndefined(originalNodeEnv)) { delete process.env.NODE_ENV; } else { process.env.NODE_ENV = originalNodeEnv; } if (sailsApp) { return sailsApp.lower(done); } else { return done(); } }); it('should not change the NODE_ENV env variable to match the configured Sails environment, or vice versa', function(done) { // Load `app0` deep in the `'cenote'` Sails().load({ environment: 'cenote', log: { level: 'error' }, globals: false, hooks: { grunt: false, i18n: false, session: false } }, function(err, _sailsApp) { if (err) { return done(err); } sailsApp = _sailsApp; // Assert that NODE_ENV is unchanged. assert(typeof process.env.NODE_ENV === 'undefined'); // Assert that sails config is unchanged. assert.equal(sailsApp.config.environment, 'cenote'); return done(); });//</app0.load()> }); }); describe('with both NODE_ENV set and Sails environment configured', function() { var sailsApp; var originalNodeEnv; before(function() { originalNodeEnv = process.env.NODE_ENV; process.env.NODE_ENV = 'foobar'; }); after(function(done) { if (_.isUndefined(originalNodeEnv)) { delete process.env.NODE_ENV; } else { process.env.NODE_ENV = originalNodeEnv; } if (sailsApp) { return sailsApp.lower(done); } else { return done(); } }); it('should not change the NODE_ENV env variable to match the configured Sails environment, or vice versa', function(done) { // Load `app0` deep in the `'cenote'` Sails().load({ environment: 'cenote', log: { level: 'error' }, globals: false, hooks: { grunt: false, i18n: false, session: false } }, function(err, _sailsApp) { if (err) { return done(err); } sailsApp = _sailsApp; // Assert that NODE_ENV is unchanged. assert.equal('foobar', process.env.NODE_ENV); // Assert that sails config is unchanged. assert.equal(sailsApp.config.environment, 'cenote'); return done(); });//</app0.load()> }); }); describe('with Sails environment set to `production`, and the Node environment is `undefined`', function() { var sailsApp; var originalNodeEnv; before(function() { originalNodeEnv = process.env.NODE_ENV; delete process.env.NODE_ENV; }); after(function(done) { if (_.isUndefined(originalNodeEnv)) { delete process.env.NODE_ENV; } else { process.env.NODE_ENV = originalNodeEnv; } if (sailsApp) { return sailsApp.lower(done); } else { return done(); } }); it('should change NODE_ENV to production and log a warning', function(done) { var debugs = []; var customLogger = { level: 'debug', custom: { log: function(){}, warn: function(){}, debug: function(msg) {debugs.push(msg);} }, colors: { warn: '' }, prefixTheme: 'abbreviated' }; // Load `app0` deep in the `'cenote'` Sails().load({ environment: 'production', log: customLogger, globals: false, hooks: { grunt: false, i18n: false, session: false, sockets: false } }, function(err, _sailsApp) { if (err) { return done(err); } sailsApp = _sailsApp; // Assert that NODE_ENV is changed. assert.equal(process.env.NODE_ENV, 'production'); // Assert that sails config is unchanged. assert.equal(sailsApp.config.environment, 'production'); assert (_.any(debugs, function(debug) { return debug.indexOf('Detected Sails environment is "production", but NODE_ENV is `undefined`.') > -1; }), 'Did not log a warning about NODE_ENV being undefined while sails environment is `production`!'); assert (_.any(debugs, function(debug) { return debug.indexOf('Automatically setting the NODE_ENV environment variable to "production".') > -1; }), 'Did not log a warning about NODE_ENV being set to `production`!'); return done(); });//</app0.load()> }); }); describe('with Sails environment set to `production`, and the Node environment is `development`', function() { var sailsApp; var originalNodeEnv; before(function() { originalNodeEnv = process.env.NODE_ENV; process.env.NODE_ENV = 'development'; }); after(function(done) { if (_.isUndefined(originalNodeEnv)) { delete process.env.NODE_ENV; } else { process.env.NODE_ENV = originalNodeEnv; } if (sailsApp) { return sailsApp.lower(done); } else { return done(); } }); it('should fail to lift sails', function(done) { // Load `app0` deep in the `'cenote'` Sails().load({ environment: 'production', log: {level: 'silent'}, globals: false, hooks: { grunt: false, i18n: false, session: false, sockets: false } }, function(err, _sailsApp) { if (!err) { return done(new Error('Sails should have failed to lift!')); } assert.equal(err.code, 'E_INVALID_NODE_ENV'); return done(); });//</app0.load()> }); }); }); ================================================ FILE: test/integration/lift.test.js ================================================ /** * Module dependencies */ var path = require('path'); var util = require('util'); var tmp = require('tmp'); var request = require('@sailshq/request'); var assert = require('assert'); var _ = require('@sailshq/lodash'); var MProcess = require('machinepack-process'); var Filesystem = require('machinepack-fs'); var testSpawningSailsChildProcessInCwd = require('../helpers/test-spawning-sails-child-process-in-cwd'); var testSpawningSailsLiftChildProcessInCwd = require('../helpers/test-spawning-sails-lift-child-process-in-cwd'); var appHelper = require('./helpers/appHelper'); tmp.setGracefulCleanup(); describe('Starting sails server with `sails lift`, `sails console` or `node app.js`', function() { // Track the location of the Sails CLI, as well as the current working directory // before we stop hopping about all over the place. var originalCwd = process.cwd(); var pathToSailsCLI = path.resolve(__dirname, '../../bin/sails.js'); describe('in the directory of a newly-generated sails app', function() { var pathToTestApp; before(function(done) { // Create a temp directory. var tmpDir = tmp.dirSync({gracefulCleanup: true, unsafeCleanup: true}); // Switch to the temp directory. process.chdir(tmpDir.name); pathToTestApp = path.resolve(tmpDir.name, 'testApp'); // Create a new Sails app. MProcess.executeCommand({ command: util.format('node %s new %s --fast --traditional --without=lodash,async', pathToSailsCLI, 'testApp'), }).exec(function(err) { if (err) {return done(err);} appHelper.linkDeps(pathToTestApp); appHelper.linkSails(pathToTestApp); return done(); }); }); // And CD in. before(function (){ process.chdir(pathToTestApp); Filesystem.writeSync({ force: true, destination: 'api/controllers/getconf.js', string: 'module.exports = function (req, res) { return res.json(sails.config); }' }).execSync(); Filesystem.writeSync({ force: true, destination: 'config/routes.js', string: 'module.exports.routes = { \'get /getconf\': \'getconf\' };' }).execSync(); }); // Test `sails lift` in the CWD with env vars for config. describe('running `sails lift`', function (){ testSpawningSailsLiftChildProcessInCwd({ pathToSailsCLI: pathToSailsCLI, liftCliArgs: ['--hooks.pubsub=false'], envVars: _.extend({ 'sails_foo__bar': '{"abc": 123}'}, process.env), httpRequestInstructions: { method: 'GET', uri: 'http://localhost:1337/getconf', }, fnWithAdditionalTests: function (){ it('should humanize the config passed in via env vars', function (done){ request({ method: 'GET', uri: 'http://localhost:1337/getconf', }, function(err, response, body) { if (err) { return done(err); } try { assert.equal(response.statusCode, 200); try { body = JSON.parse(body); } catch(e){ throw new Error('Could not parse as JSON: '+e.stack+'\nHere is what I attempted to parse: '+util.inspect(body, {depth:null})+''); } assert.equal(body.foo && body.foo.bar && body.foo.bar.abc, 123); } catch (e) { return done(e); } return done(); }); }); } }); }); // Test `node app.js` in the CWD with env vars for config. describe('running `node app.js`', function (){ testSpawningSailsChildProcessInCwd({ cliArgs: ['app.js', '--hooks.pubsub=false'], envVars: _.extend({ 'sails_foo__bar': '{"abc": 123}'}, process.env), fnWithAdditionalTests: function (){ it('should humanize the config passed in via env vars', function (done){ request({ method: 'GET', uri: 'http://localhost:1337/getconf', }, function(err, response, body) { if (err) { return done(err); } try { assert.equal(response.statusCode, 200); try { body = JSON.parse(body); } catch(e){ throw new Error('Could not parse as JSON: '+e.stack+'\nHere is what I attempted to parse: '+util.inspect(body, {depth:null})+''); } assert.equal(body.foo && body.foo.bar && body.foo.bar.abc, 123); } catch (e) { return done(e); } return done(); }); }); } }); }); // Test `sails console` in the CWD with env vars for config. describe('running `sails console`', function (){ testSpawningSailsChildProcessInCwd({ cliArgs: [pathToSailsCLI, 'console', '--hooks.pubsub=false'], envVars: _.extend({ 'sails_foo__bar': '{"abc": 123}'}, process.env), fnWithAdditionalTests: function (){ it('should humanize the config passed in via env vars', function (done){ request({ method: 'GET', uri: 'http://localhost:1337/getconf', }, function(err, response, body) { if (err) { return done(err); } try { assert.equal(response.statusCode, 200); try { body = JSON.parse(body); } catch(e){ throw new Error('Could not parse as JSON: '+e.stack+'\nHere is what I attempted to parse: '+util.inspect(body, {depth:null})+''); } assert.equal(body.foo && body.foo.bar && body.foo.bar.abc, 123); } catch (e) { return done(e); } return done(); }); }); } }); }); // Test `sails lift --port=1492` in the CWD. describe('running `sails lift --port=1492`', function (){ testSpawningSailsLiftChildProcessInCwd({ pathToSailsCLI: pathToSailsCLI, liftCliArgs: [ '--port=1492', '--hooks.pubsub=false' ], httpRequestInstructions: { method: 'GET', uri: 'http://localhost:1492/getconf', }, fnWithAdditionalTests: function (){ it('should NOT be able to contact localhost:1337 anymore', function (done){ request({ method: 'GET', uri: 'http://localhost:1337', }, function(err, response, body) { if (err) { return done(); } return done(new Error('Should not be able to communicate with locahost:1337 anymore.... Here is the response we received:'+util.inspect(response,{depth:null})+'\n\n* * Troublehooting * *\n Perhaps the Sails app running in the child process was not properly cleaned up when it received SIGTERM? Or could be a problem with the tests. Find out all this and more after you fix it.')); }); }); } }); }); // And CD back to where we were before. after(function () { process.chdir(originalCwd); }); });//</in the directory of a newly-generated sails app> describe('in an empty directory', function() { var pathToEmptyDirectory; before(function() { // Create a temp directory. var tmpDir = tmp.dirSync({gracefulCleanup: true, unsafeCleanup: true}); // Switch to the temp directory. process.chdir(tmpDir.name); pathToEmptyDirectory = tmpDir.name; }); // And CD in. before(function (){ process.chdir(pathToEmptyDirectory); }); // Now inject a describe block that tests lifing Sails in the CWD using // our wonderful little helper: "testSpawningSailsLiftChildProcessInCwd()". describe('running `sails lift`', function (){ testSpawningSailsLiftChildProcessInCwd({ pathToSailsCLI: pathToSailsCLI, liftCliArgs: ['--hooks.pubsub=false'], httpRequestInstructions: { method: 'GET', uri: 'http://localhost:1337', expectedStatusCode: 404 } }); }); // And CD back to whererever we were before. after(function () { process.chdir(originalCwd); }); });//</in an empty directory> }); ================================================ FILE: test/integration/middleware.404.test.js ================================================ var _ = require('@sailshq/lodash'); var request = require('@sailshq/request'); var Sails = require('../../lib').Sails; var assert = require('assert'); var fs = require('fs-extra'); var request = require('@sailshq/request'); var appHelper = require('./helpers/appHelper'); var path = require('path'); describe('middleware :: ', function() { describe('404 :: ', function() { var appName = 'testApp'; var sailsApp; before(function(done) { appHelper.build(function(err) { if (err) {return done(err);} fs.writeFileSync(path.resolve('..', appName, 'views', '404.ejs'), 'no file here bruh!'); return done(); }); }); after(function() { process.chdir('../'); appHelper.teardown(); }); describe('with no custom 404 handler installed', function() { before(function(done) { appHelper.lift({ hooks: { pubsub: false } }, function(err, _sailsApp) { if (err) { return done(err); } sailsApp = _sailsApp; return done(); }); }); it('the default 404 handler should respond to a request for an unbound URL', function(done) { request( { method: 'GET', uri: 'http://localhost:1342/nothing', headers: { 'Accept': 'text/html' } }, function(err, response, body) { if (err) { return done(err); } assert.equal(response.statusCode, 404); assert(body.match('<html>')); assert(body.match('no file here bruh!')); return done(); } ); }); after(function(done) { sailsApp.lower(done); }); }); describe('with a custom 404 handler installed', function() { before(function(done) { appHelper.lift({ hooks: { pubsub: false }, http: { middleware: { order: [ 'startRequestTimer', 'cookieParser', 'session', 'bodyParser', 'handleBodyParserError', 'compress', 'methodOverride', 'poweredBy', '$custom', 'router', 'www', 'favicon', 'notfound' ], notfound: function (req, res) { return res.send('custom nada bro'); } } } }, function(err, _sailsApp) { if (err) { return done(err); } sailsApp = _sailsApp; return done(); }); }); it('the custom 404 handler should respond to a request for an unbound URL', function(done) { request( { method: 'GET', uri: 'http://localhost:1342/nothing', headers: { 'Accept': 'text/html' } }, function(err, response, body) { if (err) { return done(err); } assert.equal(response.statusCode, 200); assert.equal(body, 'custom nada bro'); return done(); } ); }); after(function(done) { sailsApp.lower(done); }); }); describe('with 404 left out of a custom middleware order', function() { before(function(done) { appHelper.lift({ hooks: { pubsub: false }, http: { middleware: { order: [ 'startRequestTimer', 'cookieParser', 'session', 'bodyParser', 'handleBodyParserError', 'compress', 'methodOverride', 'poweredBy', '$custom', 'router', 'www', 'favicon' ] } } }, function(err, _sailsApp) { if (err) { return done(err); } sailsApp = _sailsApp; return done(); }); }); it('the default 404 handler should still respond to a request for an unbound URL', function(done) { request( { method: 'GET', uri: 'http://localhost:1342/nothing', headers: { 'Accept': 'text/html' } }, function(err, response, body) { if (err) { return done(err); } assert.equal(response.statusCode, 404); assert(body.match('<html>')); assert(body.match('no file here bruh!')); return done(); } ); }); after(function(done) { sailsApp.lower(done); }); }); }); }); ================================================ FILE: test/integration/middleware.500.test.js ================================================ var _ = require('@sailshq/lodash'); var request = require('@sailshq/request'); var Sails = require('../../lib').Sails; var assert = require('assert'); var fs = require('fs-extra'); var request = require('@sailshq/request'); var appHelper = require('./helpers/appHelper'); var path = require('path'); describe('middleware :: ', function() { describe('500 :: ', function() { var appName = 'testApp'; var sailsApp; before(function(done) { appHelper.build(function(err) { if (err) {return done(err);} fs.writeFileSync(path.resolve('..', appName, 'views', '500.ejs'), 'bogus err bruh!'); fs.writeFileSync(path.resolve('..', appName, 'config', 'routes.js'), 'module.exports.routes = { \'/err\': function (req, res) {throw new Error(\'errrr\');} };'); return done(); }); }); after(function() { process.chdir('../'); appHelper.teardown(); }); describe('with no custom 500 handler installed', function() { before(function(done) { appHelper.lift({ hooks: { pubsub: false } }, function(err, _sailsApp) { if (err) { return done(err); } sailsApp = _sailsApp; return done(); }); }); it('the default 500 handler should respond to a request that causes an error', function(done) { request( { method: 'GET', uri: 'http://localhost:1342/err', headers: { 'Accept': 'text/html' } }, function(err, response, body) { if (err) { return done(err); } assert.equal(response.statusCode, 500); assert(body.match('<html>')); assert(body.match('bogus err bruh!')); return done(); } ); }); after(function(done) { sailsApp.lower(done); }); }); describe('with a custom 500 handler installed', function() { before(function(done) { appHelper.lift({ hooks: { pubsub: false }, http: { middleware: { order: [ 'startRequestTimer', 'cookieParser', 'session', 'bodyParser', 'handleBodyParserError', 'compress', 'methodOverride', 'poweredBy', '$custom', 'router', 'www', 'favicon', 'err' ], err: function (err, req, res, next) { return res.send('custom err bro'); } } } }, function(err, _sailsApp) { if (err) { return done(err); } sailsApp = _sailsApp; return done(); }); }); it('the custom 500 handler should respond to a request that causes an error', function(done) { request( { method: 'GET', uri: 'http://localhost:1342/err', headers: { 'Accept': 'text/html' } }, function(err, response, body) { if (err) { return done(err); } assert.equal(response.statusCode, 200); assert.equal(body, 'custom err bro'); return done(); } ); }); after(function(done) { sailsApp.lower(done); }); }); describe('with 500 left out of a custom middleware order', function() { before(function(done) { appHelper.lift({ hooks: { pubsub: false }, http: { middleware: { order: [ 'startRequestTimer', 'cookieParser', 'session', 'bodyParser', 'handleBodyParserError', 'compress', 'methodOverride', 'poweredBy', '$custom', 'router', 'www', 'favicon' ] } } }, function(err, _sailsApp) { if (err) { return done(err); } sailsApp = _sailsApp; return done(); }); }); it('the default 500 handler should respond to a request that causes an error', function(done) { request( { method: 'GET', uri: 'http://localhost:1342/err', headers: { 'Accept': 'text/html' } }, function(err, response, body) { if (err) { return done(err); } assert.equal(response.statusCode, 500); assert(body.match('<html>')); assert(body.match('bogus err bruh!')); return done(); } ); }); after(function(done) { sailsApp.lower(done); }); }); }); }); ================================================ FILE: test/integration/middleware.compression.test.js ================================================ var _ = require('@sailshq/lodash'); var request = require('@sailshq/request'); var Sails = require('../../lib').Sails; var assert = require('assert'); describe('middleware :: ', function() { describe('compression :: ', function() { // Source text (must be > 1024 bytes to trigger compression) var lipsum = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras fringilla mollis sapien sed consequat. Cras vestibulum iaculis rhoncus. Vestibulum et auctor dolor. Interdum et malesuada fames ac ante ipsum primis in faucibus. Nunc vitae risus sit amet massa lacinia luctus. Quisque auctor hendrerit fermentum. Nullam neque diam, condimentum a nisi eu, ornare porttitor nisl. Praesent porttitor augue turpis, eu consectetur ligula suscipit eget. Aliquam placerat turpis ut varius vestibulum. Pellentesque ligula velit, molestie vel purus sollicitudin, elementum rutrum odio. Ut ultricies convallis leo. Integer id nisl vel tellus laoreet iaculis sed et ipsum. In malesuada sem vitae porttitor sollicitudin. Maecenas sodales est eu augue auctor, in accumsan dolor lacinia. Proin at euismod nibh, eu congue velit. Vestibulum risus velit, vulputate in dui in, commodo sodales elit. Curabitur consectetur justo tincidunt odio imperdiet blandit. Etiam gravida eu ante commodo viverra. Ut sed dapibus purus, eu vulputate neque. Maecenas suscipit felis ac sapien iaculis tempor. Etiam quis vulputate turpis. Cras at nulla lectus. Vestibulum non magna sem. Aliquam tristique lacinia ligula, non interdum justo scelerisque vitae. Praesent molestie eu nibh vel volutpat. Pellentesque ut lacus a tortor lacinia condimentum. Quisque blandit facilisis nunc sed tempus. Praesent dapibus leo at enim mollis, tristique facilisis turpis aliquam. Vestibulum tempus felis ac arcu rhoncus, in efficitur elit sodales. Suspendisse eu odio odio. Vestibulum tempus elementum massa, et rutrum risus ultricies ut. Ut ac mattis nulla. Aenean tristique sollicitudin metus. Morbi massa purus, hendrerit non placerat non, imperdiet nec turpis. Nulla et ultrices metus. Nulla eget congue urna, ut rutrum enim. Aenean rutrum dui massa, non luctus urna dignissim vel. Morbi a suscipit ligula. Nunc laoreet nisi eleifend tortor volutpat finibus vel nec risus. Fusce maximus non sem vel mattis. Etiam iaculis, turpis at sollicitudin blandit, massa mi finibus nunc, nec auctor ex nisl sed.'; describe('In the production environment', function() { // Lift a Sails instance in production mode var app = Sails(); var originalNodeEnv; before(function() { originalNodeEnv = process.env.NODE_ENV; process.env.NODE_ENV = 'production'; }); after(function() { process.env.NODE_ENV = originalNodeEnv; }); before(function (done){ app.lift({ globals: false, port: 1535, environment: 'production', log: {level: 'silent'}, hooks: {session: false, grunt: false, pubsub: false, sockets: false}, routes: { '/test': function(req, res) { return res.send(lipsum); } } }, done); }); it('responses should be compressed', function(done) { var rawLen = 0, res = ''; request( { method: 'GET', uri: 'http://localhost:1535/test', gzip: true } ) .on('data', function(data) { // decompressed data as it is received res += data; }) .on('response', function(response) { // unmodified http.IncomingMessage object response.on('data', function(data) { rawLen += data.length; }); }) .on('end', function(err) { if (err) {return done(err);} assert.equal(res, lipsum); assert(rawLen < lipsum.length, 'Expected length of raw response data (' + rawLen.toString() + ') to be < length of source data (' + lipsum.length.toString() + ').'); return done(err); }); }); after(function(done) { app.lower(done); }); }); describe('In the development environment', function() { // Lift a Sails instance in production mode var app = Sails(); before(function (done){ app.lift({ globals: false, port: 1535, environment: 'development', log: {level: 'silent'}, hooks: {session: false, grunt: false}, routes: { '/test': function(req, res) { res.send(lipsum); } } }, done); }); it('responses should not be compressed', function(done) { var rawLen = 0, res = ''; request( { method: 'GET', uri: 'http://localhost:1535/test', gzip: true } ) .on('data', function(data) { // decompressed data as it is received res += data; }) .on('response', function(response) { // unmodified http.IncomingMessage object response.on('data', function(data) { rawLen += data.length; }); }) .on('end', function(err) { if (err) {return done(err);} assert.equal(res, lipsum); assert.equal(rawLen, lipsum.length, 'Expected length of raw response data (' + rawLen.toString() + ') to be equal to length of source data (' + lipsum.length.toString() + ').'); return done(err); }); }); after(function(done) { app.lower(done); }); }); }); }); ================================================ FILE: test/integration/middleware.cookieParser.test.js ================================================ /** * Module dependencies */ var _ = require('@sailshq/lodash'); var request = require('@sailshq/request'); var Sails = require('../../lib').Sails; var assert = require('assert'); describe('middleware :: ', function() { describe('cookie parser :: ', function() { describe('http requests :: ', function() { describe('with a valid session secret', function() { // Lift a Sails instance in production mode var app = Sails(); before(function (done){ app.lift({ globals: false, port: 1535, environment: 'development', log: {level: 'silent'}, session: { secret: 'abc123' }, hooks: {grunt: false, pubsub: false}, routes: { '/test': function(req, res) { res.json({ cookies: req.cookies, signedCookies: req.signedCookies }); } } }, done); }); it('when sending a request with a Cookie: header, req.cookies and req.signedCookies should be populated', function(done) { var rawLen = 0, res = ''; request( { method: 'GET', uri: 'http://localhost:1535/test', headers: { 'cookie': 'foo=bar; owl=s%3Ahoot.v0ELGJM%2B8t4aP0YeUpcC31OKnAQ%2BqUTf%2F4WaLaaosJg; abc=123', } }, function(err, response, body) { if(err){ return done(err); } body = JSON.parse(body); assert(body.cookies); assert(body.signedCookies); assert.equal(body.cookies.foo, 'bar'); assert.equal(body.cookies.abc, '123'); assert.equal(body.signedCookies.owl, 'hoot'); assert(!body.cookies.owl); return done(); } ); }); after(function(done) { app.lower(done); }); }); describe('with no session secret and session hook disabled', function() { // Lift a Sails instance in production mode var app = Sails(); before(function (done){ app.lift({ globals: false, port: 1535, environment: 'development', log: {level: 'silent'}, session: { secret: null }, hooks: { session: false, grunt: false, pubsub: false }, routes: { '/test': function(req, res) { res.json({ cookies: req.cookies, signedCookies: req.signedCookies }); } } }, done); }); it('when sending a request with a Cookie: header, req.cookies and req.signedCookies should be populated', function(done) { var rawLen = 0, res = ''; request( { method: 'GET', uri: 'http://localhost:1535/test', headers: { 'cookie': 'foo=bar; owl=s%3Ahoot.v0ELGJM%2B8t4aP0YeUpcC31OKnAQ%2BqUTf%2F4WaLaaosJg; abc=123', } }, function(err, response, body) { if(err){ return done(err); } body = JSON.parse(body); assert(body.cookies); assert(body.signedCookies); assert.equal(body.cookies.foo, 'bar'); assert.equal(body.cookies.abc, '123'); assert.equal(body.cookies.owl, 's:hoot.v0ELGJM+8t4aP0YeUpcC31OKnAQ+qUTf/4WaLaaosJg'); assert(!body.signedCookies.owl); return done(); } ); }); after(function(done) { app.lower(done); }); }); describe('with an invalid session secret and session hook disabled', function() { var app = Sails(); it('should throw an error when lifting Sails', function(done) { app.lift({ globals: false, port: 1535, environment: 'development', log: {level: 'silent'}, session: { secret: 12345 }, hooks: { session: false, grunt: false, pubsub: false }, routes: { '/test': function(req, res) { res.json({ cookies: req.cookies, signedCookies: req.signedCookies }); } } }, function(err) { if (!err) {return done(new Error('Should have thrown an error!'));} return done(); }); }); after(function(done) { app.lower(done); }); }); }); describe('virtual requests :: ', function() { describe('with a valid session secret', function() { // Lift a Sails instance in production mode var app = Sails(); before(function (done){ app.load({ globals: false, environment: 'development', log: {level: 'silent'}, session: { secret: 'abc123' }, hooks: { http: false, views: false, sockets: false, pubsub: false }, routes: { '/test': function(req, res) { res.json({ cookies: req.cookies, signedCookies: req.signedCookies }); } } }, done); }); it('when sending a request with a Cookie: header, req.cookies and req.signedCookies should be populated', function(done) { var rawLen = 0, res = ''; app.request( { method: 'GET', url: '/test', headers: { 'cookie': 'foo=bar; owl=s%3Ahoot.v0ELGJM%2B8t4aP0YeUpcC31OKnAQ%2BqUTf%2F4WaLaaosJg; abc=123', } }, function(err, response, body) { if(err){ return done(err); } assert(body.cookies); assert(body.signedCookies); assert.equal(body.cookies.foo, 'bar'); assert.equal(body.cookies.abc, '123'); assert.equal(body.signedCookies.owl, 'hoot'); assert(!body.cookies.owl); return done(); } ); }); after(function(done) { app.lower(done); }); }); describe('with no session secret and session hook disabled', function() { // Lift a Sails instance in production mode var app = Sails(); before(function (done){ app.load({ globals: false, environment: 'development', log: {level: 'silent'}, session: { secret: null }, hooks: { session: false, http: false, views: false, sockets: false, pubsub: false }, routes: { '/test': function(req, res) { return res.json({ cookies: req.cookies, signedCookies: req.signedCookies }); } } }, done); }); it('when sending a request with a Cookie: header, req.cookies and req.signedCookies should be populated', function(done) { var rawLen = 0, res = ''; app.request( { method: 'GET', url: '/test', headers: { 'cookie': 'foo=bar; owl=s%3Ahoot.v0ELGJM%2B8t4aP0YeUpcC31OKnAQ%2BqUTf%2F4WaLaaosJg; abc=123', } }, function(err, response, body) { if(err){ return done(err); } assert(body.cookies); assert(body.signedCookies); assert.equal(body.cookies.foo, 'bar'); assert.equal(body.cookies.abc, '123'); assert.equal(body.cookies.owl, 's:hoot.v0ELGJM+8t4aP0YeUpcC31OKnAQ+qUTf/4WaLaaosJg'); assert(!body.signedCookies.owl); return done(); } ); }); after(function(done) { app.lower(done); }); }); describe('with an invalid session secret and session hook disabled', function() { var app = Sails(); it('should throw an error when lifting Sails', function(done) { app.load({ globals: false, environment: 'development', log: {level: 'silent'}, session: { secret: 12345 }, hooks: { session: false, http: false, views: false, sockets: false, pubsub: false }, routes: { '/test': function(req, res) { res.json({ cookies: req.cookies, signedCookies: req.signedCookies }); } } }, function(err) { if (!err) {return done(new Error('Should have thrown an error!'));} return done(); }); }); after(function(done) { app.lower(done); }); }); }); }); }); ================================================ FILE: test/integration/middleware.favicon.test.js ================================================ var _ = require('@sailshq/lodash'); var request = require('@sailshq/request'); var Sails = require('../../lib').Sails; var assert = require('assert'); var fs = require('fs-extra'); var request = require('@sailshq/request'); var appHelper = require('./helpers/appHelper'); var path = require('path'); describe('middleware :: ', function() { describe('favicon :: ', function() { var appName = 'testApp'; var sailsApp; describe('with no favicon file in the assets folder', function() { before(function(done) { appHelper.build(done); }); before(function(done) { appHelper.lift({ hooks: { pubsub: false } }, function(err, _sailsApp) { if (err) { return done(err); } sailsApp = _sailsApp; return done(); }); }); it('the default sailboat favicon should be provided', function(done) { var default_favicon = fs.readFileSync(path.resolve(__dirname, '../../lib/hooks/http/default-favicon.ico')); request( { method: 'GET', uri: 'http://localhost:1342/favicon.ico', }, function(err, response, body) { if (err) { return done(err); } assert.equal(response.statusCode, 200); assert.equal(default_favicon.toString('utf-8'), body); return done(); } ); }); after(function() { process.chdir('../'); appHelper.teardown(); }); after(function(done) { sailsApp.lower(done); }); }); }); }); ================================================ FILE: test/integration/middleware.handleBodyParserError.test.js ================================================ var _ = require('@sailshq/lodash'); var request = require('@sailshq/request'); var Sails = require('../../lib').Sails; var assert = require('assert'); var fs = require('fs-extra'); var request = require('@sailshq/request'); var appHelper = require('./helpers/appHelper'); var path = require('path'); describe('middleware :: ', function() { describe('handleBodyParserError :: ', function() { var appName = 'testApp'; var sailsApp; before(function(done) { appHelper.build(done); }); after(function() { process.chdir('../'); appHelper.teardown(); }); describe('default handleBodyParserError middleware', function() { before(function(done) { appHelper.lift({ hooks: { pubsub: false } }, function(err, _sailsApp) { if (err) { return done(err); } sailsApp = _sailsApp; return done(); }); }); it('should handle body parser errors', function(done) { request( { method: 'POST', uri: 'http://localhost:1342/nothing', headers: { 'Content-type': 'application/json' }, body: '{ foo:' }, function(err, response, body) { if (err) { return done(err); } assert(body.match('Unable to parse HTTP body')); return done(); } ); }); after(function(done) { sailsApp.lower(done); }); }); }); }); ================================================ FILE: test/integration/middleware.sails.test.js ================================================ /** * Module dependencies */ var request = require('@sailshq/request'); var Sails = require('../../lib').Sails; var assert = require('assert'); describe('middleware :: ', function() { describe('sails :: ', function() { describe('http requests :: ', function() { var sid; // Lift a Sails instance. var app = Sails(); before(function (done){ app.lift({ globals: false, port: 1535, environment: 'development', log: {level: 'silent'}, session: { secret: 'abc123' }, hooks: { grunt: false, request: false, pubsub: false }, routes: { '/test': function(req, res) { var defined = (req._sails !== undefined) ? 'defined' : 'undefined'; res.send('req._sails is ' + defined); } } }, done); }); it('req._sails should be set if request hook is disabled', function(done) { request({ method: 'GET', uri: 'http://localhost:1535/test', }, function(err, response, body) { if (err) { return done(err); } assert.equal(body, 'req._sails is defined'); return done(); }); }); after(function(done) { return app.lower(done); }); }); }); }); ================================================ FILE: test/integration/middleware.session.redis.test.js ================================================ var _ = require('@sailshq/lodash'); var request = require('@sailshq/request'); var Sails = require('../../lib').Sails; var assert = require('assert'); var cookie = require('cookie'); var tmp = require('tmp'); var path = require('path'); var fs = require('fs-extra'); if (process.env.TEST_REDIS_SESSION) { describe('middleware :: ', function() { describe('session :: ', function() { describe('with redis adapter ::', function() { var curDir, tmpDir; before(function() { // Cache the current working directory. curDir = process.cwd(); // Create a temp directory. tmpDir = tmp.dirSync({gracefulCleanup: true, unsafeCleanup: true}); // Switch to the temp directory. process.chdir(tmpDir.name); // Ensure a symlink to the connect-redis adapter. fs.ensureSymlinkSync(path.resolve(__dirname, '..', '..', 'node_modules', 'connect-redis'), path.resolve(tmpDir.name, 'node_modules', 'connect-redis')); }); after(function() { process.chdir(curDir); }); it('should fail to lift if the Redis server can\'t be reached', function(done) { var app = Sails(); app.lift({ globals: false, environment: 'development', log: {level: 'silent'}, session: { secret: 'abc123', adapter: 'connect-redis', port: 6300 }, hooks: {grunt: false}, routes: { '/test': function(req, res) { var count = req.session.count || 1; req.session.count = count + 1; return res.send('Count is ' + count); } } }, function(err) { if (err && err.code === 'ECONNREFUSED') { return done(); } else if (err) { return done(err); } else { return done(new Error('Expected an error, but Sails appears to have lifted!')); } }); }); describe('http requests :: ', function() { var sid; // Lift two Sails instances connected to the same Redis server var app1 = Sails(); var app2 = Sails(); before(function (done){ var liftOptions = { globals: false, environment: 'development', log: {level: 'silent'}, session: { secret: 'abc123', pass: 'secret', db: 3, adapter: 'connect-redis', port: 6380 }, hooks: {grunt: false}, routes: { '/test': function(req, res) { var count = req.session.count || 1; req.session.count = count + 1; return res.send('Count is ' + count); } } }; app1.lift(_.extend({port: 1535}, _.cloneDeep(liftOptions)), function(err) { if (err) {return done(err);} app2.lift(_.extend({port: 1536}, _.cloneDeep(liftOptions)), function(err) { if (err) {return done(err);} return done(); }); }); }); it('a server responses should supply a cookie with a session ID', function(done) { request( { method: 'GET', uri: 'http://localhost:1535/test', }, function(err, response, body) { if (err) {return done(err);} assert.equal(body, 'Count is 1'); assert(response.headers['set-cookie']); var cookies = require('cookie').parse(response.headers['set-cookie'][0]); assert(cookies['sails.sid']); sid = cookies['sails.sid']; return done(); } ); }); it('a subsequent request to a different app sharing the same session store, with the same cookie, should retrieve the same session', function(done) { request( { method: 'GET', uri: 'http://localhost:1536/test', headers: { Cookie: 'sails.sid=' + sid } }, function(err, response, body) { if (err) {return done(err);} assert.equal(body, 'Count is 2'); return done(); } ); }); after(function(done) { return app1.lower(function(err) {if(err) {return done(err);} app2.lower(done);}); }); }); describe('virtual requests :: ', function() { var sid; // Lift two Sails instances connected to the same Redis server var app1 = Sails(); var app2 = Sails(); before(function (done){ var liftOptions = { globals: false, environment: 'development', log: {level: 'silent'}, session: { secret: 'abc123', adapter: 'connect-redis', pass: 'secret', db: 3, port: 6380 }, hooks: {grunt: false}, routes: { '/test': function(req, res) { var count = req.session.count || 1; req.session.count = count + 1; return res.send('Count is ' + count); } } }; app1.lift(_.extend({port: 1535}, _.cloneDeep(liftOptions)), function(err) { if (err) {return done(err);} app2.lift(_.extend({port: 1536}, _.cloneDeep(liftOptions)), function(err) { if (err) {return done(err);} return done(); }); }); }); it('a server responses should supply a cookie with a session ID', function(done) { app1.request( { method: 'GET', url: '/test', }, function(err, response, body) { if (err) {return done(err);} assert.equal(body, 'Count is 1'); assert(response.headers['set-cookie']); var cookies = require('cookie').parse(response.headers['set-cookie'][0]); assert(cookies['sails.sid']); sid = cookies['sails.sid']; return done(); } ); }); it('a subsequent request to a different app sharing the same session store, with the same cookie, should retrieve the same session', function(done) { app2.request( { method: 'GET', url: '/test', headers: { Cookie: 'sails.sid=' + sid } }, function(err, response, body) { if (err) {return done(err);} assert.equal(body, 'Count is 2'); return done(); } ); }); after(function(done) { return app1.lower(function(err) {if(err) {return done(err);} app2.lower(done);}); }); }); }); }); }); } ================================================ FILE: test/integration/middleware.session.test.js ================================================ var _ = require('@sailshq/lodash'); var request = require('@sailshq/request'); var Sails = require('../../lib').Sails; var assert = require('assert'); var cookie = require('cookie'); var tmp = require('tmp'); var path = require('path'); var fs = require('fs'); describe('middleware :: ', function() { describe('session :: ', function() { describe('with invalid `cookie.secure` setting', function() { it('should throw an error', function(done) { var app = Sails(); app.lift({ globals: false, port: 1535, environment: 'development', log: {level: 'silent'}, session: { cookie: { secure: 'true' } }, hooks: {grunt: false, pubsub: false}, }, function(err, _app) { if (err && err.code && err.code === 'E_SESSION_BAD_COOKIE_SECURE') { return done(); } if (err) { return done(err); } _app.lower(function(err) { if (err) { return done(new Error('App lifted when it should have failed with E_SESSION_BAD_COOKIE_SECURE. Additionally, an error occurred while lowering: ' + util.inspect(err))); } return done(new Error('App lifted when it should have failed with E_SESSION_BAD_COOKIE_SECURE')); }); }); }); }); describe('http requests :: ', function() { describe('with a valid session secret', function() { describe('using built-in (memory) store', function() { var sid; // Lift a Sails instance in production mode var app = Sails(); before(function (done){ app.lift({ globals: false, port: 1535, environment: 'development', log: {level: 'silent'}, session: { secret: 'abc123' }, hooks: {grunt: false, pubsub: false}, routes: { '/test': function(req, res) { var count = req.session.count || 1; req.session.count = count + 1; return res.send('Count is ' + count); } } }, done); }); it('a server responses should supply a cookie with a session ID', function(done) { request( { method: 'GET', uri: 'http://localhost:1535/test', }, function(err, response, body) { assert.equal(body, 'Count is 1'); assert(response.headers['set-cookie']); var cookies = require('cookie').parse(response.headers['set-cookie'][0]); assert(cookies['sails.sid']); sid = cookies['sails.sid']; return done(); } ); }); it('a subsequent request using that session ID in a "Cookie" header should use the same session', function(done) { request( { method: 'GET', uri: 'http://localhost:1535/test', headers: { Cookie: 'sails.sid=' + sid } }, function(err, response, body) { assert.equal(body, 'Count is 2'); return done(); } ); }); after(function(done) { return app.lower(done); }); }); describe('using 3rd-party (file) store', function() { var curDir, tmpDir, sailsApp; var sid; // Lift a Sails instance in production mode var app = Sails(); before(function (done){ // Cache the current working directory. curDir = process.cwd(); // Create a temp directory. tmpDir = tmp.dirSync({gracefulCleanup: true, unsafeCleanup: true}); // Switch to the temp directory. process.chdir(tmpDir.name); app.lift({ globals: false, port: 1535, environment: 'development', log: {level: 'silent'}, session: { secret: 'abc123', adapter: require('session-file-store'), // adapter: require(path.resolve(__dirname, '..', '..', 'session-file-store')), path: './my-session-files' }, hooks: {grunt: false, pubsub: false}, routes: { '/test': function(req, res) { var count = req.session.count || 1; req.session.count = count + 1; return res.send('Count is ' + count); } } }, done); }); it('should use the 3rd-party adapter', function() { assert(fs.existsSync(path.resolve(tmpDir.name, 'my-session-files'))); }); it('a server responses should supply a cookie with a session ID', function(done) { request( { method: 'GET', uri: 'http://localhost:1535/test', }, function(err, response, body) { assert.equal(body, 'Count is 1'); assert(response.headers['set-cookie']); var cookies = require('cookie').parse(response.headers['set-cookie'][0]); assert(cookies['sails.sid']); sid = cookies['sails.sid']; return done(); } ); }); it('a subsequent request using that session ID in a "Cookie" header should use the same session', function(done) { request( { method: 'GET', uri: 'http://localhost:1535/test', headers: { Cookie: 'sails.sid=' + sid } }, function(err, response, body) { assert.equal(body, 'Count is 2'); return done(); } ); }); after(function(done) { process.chdir(curDir); return app.lower(done); }); }); }); describe('with an invalid session secret', function() { var app = Sails(); it('should throw an error when lifting Sails', function(done) { app.lift({ globals: false, port: 1535, environment: 'development', log: {level: 'silent'}, session: { secret: 12345 }, hooks: {grunt: false}, routes: { '/test': function(req, res) { res.json({ cookies: req.cookies, signedCookies: req.signedCookies }); } } }, function(err) { if (!err) {return done(new Error('Should have thrown an error!'));} return done(); }); }); after(function(done) { return app.lower(done); }); }); describe('requesting a route with default `isSessionDisabled` setting', function() { // Lift a Sails instance in production mode var app = Sails(); before(function (done){ app.lift({ globals: false, port: 1535, environment: 'development', log: {level: 'silent'}, session: { secret: 'abc123' }, hooks: {grunt: false}, routes: { '/sails.io.js': function(req, res) { return res.status(200).send(); } } }, done); }); describe('static asset', function() { it('there should be no `set-cookie` header in the response', function(done) { request( { method: 'GET', uri: 'http://localhost:1535/sails.io.js', }, function(err, response, body) { assert.equal(response.statusCode, 200); assert(_.isUndefined(response.headers['set-cookie'])); return done(); } ); }); }); after(function(done) { return app.lower(done); }); }); describe('requesting a route with custom `isSessionDisabled` setting', function() { var fooRegexp = require('path-to-regexp')('/foo/:id/bar/'); // Lift a Sails instance in production mode var app = Sails(); before(function (done){ app.lift({ globals: false, port: 1535, environment: 'development', log: {level: 'silent'}, session: { secret: 'abc123', isSessionDisabled: function(req) { var path = req.path; var method = req.method; var CRUD = ['GET', 'PUT', 'POST', 'PATCH', 'DELETE']; if ( (path === '/test' && _.contains(CRUD, method)) || (path === '/bar' && method === 'POST') || (path === '/baz') || (path.match(fooRegexp)) ) { return true; } } }, hooks: {grunt: false}, routes: { '/test': function(req, res) { return res.status(200).send(); }, '/bar': function(req, res) { return res.status(200).send(); }, '/baz': function(req, res) { return res.status(200).send(); }, '/foo/123/bar': function(req, res) { return res.status(200).send(); }, '/sails.io.js': function(req, res) { return res.status(200).send(); } } }, done); }); describe('static path (blank verb)', function() { it('there should be no `set-cookie` header in the response when requesting via GET', function(done) { request( { method: 'GET', uri: 'http://localhost:1535/test', }, function(err, response, body) { assert.equal(response.statusCode, 200); assert(_.isUndefined(response.headers['set-cookie'])); return done(); } ); }); it('there should be a `set-cookie` header in the response when requesting via HEAD', function(done) { request( { method: 'HEAD', uri: 'http://localhost:1535/test', }, function(err, response, body) { assert.equal(response.statusCode, 200); assert(response.headers['set-cookie']); return done(); } ); }); }); describe('static path (ALL verb)', function() { it('there should be no `set-cookie` header in the response when requesting via GET', function(done) { request( { method: 'GET', uri: 'http://localhost:1535/baz', }, function(err, response, body) { assert.equal(response.statusCode, 200); assert(_.isUndefined(response.headers['set-cookie'])); return done(); } ); }); it('there should be no `set-cookie` header in the response when requesting via HEAD', function(done) { request( { method: 'HEAD', uri: 'http://localhost:1535/baz', }, function(err, response, body) { assert.equal(response.statusCode, 200); assert(_.isUndefined(response.headers['set-cookie'])); return done(); } ); }); }); describe('static path (POST only)', function() { it('there should be no `set-cookie` header in the response when requesting via POST', function(done) { request( { method: 'POST', uri: 'http://localhost:1535/bar', }, function(err, response, body) { assert.equal(response.statusCode, 200); assert(_.isUndefined(response.headers['set-cookie'])); return done(); } ); }); it('there SHOULD be a `set-cookie` header in the response when requesting via GET', function(done) { request( { method: 'GET', uri: 'http://localhost:1535/bar', }, function(err, response, body) { assert.equal(response.statusCode, 200); assert(response.headers['set-cookie']); return done(); } ); }); }); describe('dynamic path', function() { it('there should be no `set-cookie` header in the response', function(done) { request( { method: 'GET', uri: 'http://localhost:1535/foo/123/bar', }, function(err, response, body) { assert.equal(response.statusCode, 200); assert(_.isUndefined(response.headers['set-cookie'])); return done(); } ); }); }); describe('static asset', function() { it('there SHOULD be a `set-cookie` header in the response', function(done) { request( { method: 'GET', uri: 'http://localhost:1535/sails.io.js', }, function(err, response, body) { assert.equal(response.statusCode, 200); assert(response.headers['set-cookie']); return done(); } ); }); }); after(function(done) { return app.lower(done); }); }); }); describe('virtual requests :: ', function() { describe('with a valid session secret', function() { var sid; // Lift a Sails instance in production mode var app = Sails(); before(function (done){ app.load({ globals: false, environment: 'development', log: {level: 'silent'}, session: { secret: 'abc123' }, routes: { '/test': function(req, res) { var count = req.session.count || 1; req.session.count = count + 1; res.send('Count is ' + count); } } }, done); }); it('a server responses should supply a cookie with a session ID', function(done) { app.request( { method: 'GET', url: '/test', }, function(err, response, body) { assert.equal(body, 'Count is 1'); assert(response.headers['set-cookie']); var cookies = require('cookie').parse(response.headers['set-cookie'][0]); assert(cookies['sails.sid']); sid = cookies['sails.sid']; return done(); } ); }); it('a subsequent request using that session ID in a "Cookie" header should use the same session', function(done) { app.request( { method: 'GET', url: '/test', headers: { Cookie: 'sails.sid=' + sid } }, function(err, response, body) { assert.equal(body, 'Count is 2'); return done(); } ); }); after(function(done) { return app.lower(done); }); }); describe('requesting a route disabled by sails.config.session.isSessionDisabled', function() { // Lift a Sails instance in production mode var app = Sails(); before(function (done){ app.lift({ globals: false, port: 1535, environment: 'development', log: {level: 'silent'}, session: { secret: 'abc123', isSessionDisabled: function(req) { var CRUD = ['GET', 'PUT', 'POST', 'PATCH', 'DELETE']; return (req.path === '/test' && _.contains(CRUD, req.method)) } }, hooks: {grunt: false}, routes: { '/test': function(req, res) { if (_.isUndefined(req.session)) { return res.status(200).send(); } return res.status(500).send(); } } }, done); }); it('there should be no `set-cookie` header in the response', function(done) { request( { method: 'GET', uri: 'http://localhost:1535/test', }, function(err, response, body) { assert.equal(response.statusCode, 200); assert(_.isUndefined(response.headers['set-cookie'])); return done(); } ); }); after(function(done) { return app.lower(done); }); }); }); }); }); ================================================ FILE: test/integration/middleware.startRequestTimer.test.js ================================================ var _ = require('@sailshq/lodash'); var request = require('@sailshq/request'); var Sails = require('../../lib').Sails; var assert = require('assert'); var fs = require('fs-extra'); var request = require('@sailshq/request'); var appHelper = require('./helpers/appHelper'); var path = require('path'); describe('middleware :: ', function() { describe('startRequestTimer :: ', function() { var appName = 'testApp'; var sailsApp; before(function(done) { appHelper.build(done); }); after(function() { process.chdir('../'); appHelper.teardown(); }); describe('default startRequestTimer middleware', function() { before(function(done) { appHelper.lift({ hooks: { pubsub: false }, routes: { '/time': function(req, res) { assert(req._startTime); assert(req._startTime instanceof Date); res.send(); } } }, function(err, _sailsApp) { if (err) { return done(err); } sailsApp = _sailsApp; return done(); }); }); it('should add a _startTime to the request object', function(done) { request( { method: 'GET', uri: 'http://localhost:1342/time', }, function(err, response, body) { return done(err); } ); }); after(function(done) { sailsApp.lower(done); }); }); }); }); ================================================ FILE: test/integration/middleware.static.test.js ================================================ /** * Module dependencies */ var _ = require('@sailshq/lodash'); var request = require('@sailshq/request'); var Sails = require('../../lib').Sails; var assert = require('assert'); var fs = require('fs-extra'); var request = require('@sailshq/request'); var appHelper = require('./helpers/appHelper'); var path = require('path'); describe('middleware :: ', function() { describe('static :: ', function() { var appName = 'testApp'; var customFaviconPath = path.resolve(__dirname, 'fixtures/favicon.ico'); var test_file; before(function(done) { appHelper.build(function(err) { if (err) {return done(err);} fs.copySync(customFaviconPath, path.resolve('../', appName, '.tmp/public/test.txt')); fs.copySync(customFaviconPath, path.resolve('../', appName, '.tmp/public/test.png')); fs.copySync(customFaviconPath, path.resolve('../', appName, '.tmp/public/test.woff')); test_file = fs.readFileSync(customFaviconPath); return done(); }); }); describe('with a test.txt, test.png and test.woff file in the .tmp/public folder', function() { var sailsApp; before(function(done) { appHelper.lift(function(err, _sailsApp) { assert(!err); sailsApp = _sailsApp; return done(); }); }); it('a request to /test.txt should provide the file with the correct content-type header', function(done) { request( { method: 'GET', uri: 'http://localhost:1342/test.txt', }, function(err, response, body) { assert.equal(test_file.toString('utf-8'), body); assert.equal(response.headers['content-type'], 'text/plain; charset=UTF-8'); return done(); } ); }); it('a request to /test.png should provide the file with the correct content-type header', function(done) { request( { method: 'GET', uri: 'http://localhost:1342/test.png', }, function(err, response, body) { assert.equal(test_file.toString('utf-8'), body); assert.equal(response.headers['content-type'], 'image/png'); return done(); } ); }); it('a request to /test.woff should provide the file with the correct content-type header', function(done) { request( { method: 'GET', uri: 'http://localhost:1342/test.woff', }, function(err, response, body) { assert.equal(test_file.toString('utf-8'), body); assert.equal(response.headers['content-type'], 'font/woff'); return done(); } ); }); after(function(done) { sailsApp.lower(done); }); }); describe('with cache time set to 5000 ms', function() { var sailsApp; before(function(done) { appHelper.lift({ http: { cache: 5000 } }, function(err, _sailsApp) { assert(!err); sailsApp = _sailsApp; return done(); }); }); it('a request to /test.txt should provide the file and a correct cache-control header', function(done) { request( { method: 'GET', uri: 'http://localhost:1342/test.txt', }, function(err, response, body) { assert.equal(test_file.toString('utf-8'), body); assert.equal(response.headers['content-type'], 'text/plain; charset=UTF-8'); assert.equal(response.headers['cache-control'], 'public, max-age=5'); return done(); } ); }); after(function(done) { sailsApp.lower(done); }); }); after(function() { process.chdir('../'); appHelper.teardown(); }); }); }); ================================================ FILE: test/integration/new.test.js ================================================ /** * Module dependencies */ var assert = require('assert'); var fs = require('fs-extra'); var exec = require('child_process').exec; var _ = require('@sailshq/lodash'); var appHelper = require('./helpers/appHelper'); var path = require('path'); var util = require('util'); /** * Module errors */ describe('New app generator', function() { var sailsbin = path.resolve('./bin/sails.js'); var appName = 'testApp'; var defaultTemplateLang = 'ejs'; this.slow(1000); beforeEach(function(done) { fs.exists(appName, function(exists) { if (exists) { fs.removeSync(appName); } done(); }); }); afterEach(function(done) { fs.exists(appName, function(exists) { if (exists) { fs.removeSync(appName); } done(); }); }); describe('sails new <appname>', function() { it('should create new, liftable app in new folder', function(done) { exec('node '+ sailsbin + ' new ' + appName + ' --fast --traditional --without=lodash,async', function(err) { if (err) { return done(new Error(err)); } appHelper.lift({log:{level:'silent'}}, function(err, sailsApp) { if (err) {return done(err);} sailsApp.lower(done); }); }); }); it('should not overwrite a folder', function(done) { fs.mkdir(appName, function(err) { if (err) { return done(new Error(err)); } fs.writeFile(path.resolve(appName, 'test'), '', function(err) { if (err) { return done(new Error(err)); } exec('node '+ sailsbin + ' new ' + appName + ' --fast --traditional', function(err, dumb, result) { // In Node v0.10.x on some environments (like in Appveyor), this just // returns an Error in `err` instead of a result, so account for that. if (process.versions.node.split('.')[0] === '0' && process.versions.node.split('.')[1] === '10' && err) { return done(); } assert(result.indexOf('error') > -1, 'Should have received an error, but instead got: ' + result); done(); }); }); }); }); }); describe('sails generate new <appname>', function() { it('should create new app', function(done) { exec('node '+ sailsbin + ' generate new ' + appName + ' --fast --traditional --without=lodash,async', function(err) { if (err) { return done(new Error(err)); } appHelper.lift({log:{level:'silent'}}, function(err, sailsApp) { if (err) {return done(err);} sailsApp.lower(done); }); }); }); it('should not overwrite a folder', function(done) { fs.mkdir(appName, function(err) { if (err) { return done(new Error(err)); } fs.writeFile(path.resolve(appName, 'test'), '', function(err) { if (err) { return done(new Error(err)); } exec('node '+ sailsbin + ' generate new ' + appName + ' --fast --traditional', function(err, dumb, result) { // In Node v0.10.x on some environments (like in Appveyor), this just // returns an Error in `err` instead of a result, so account for that. if (process.versions.node.split('.')[0] === '0' && process.versions.node.split('.')[1] === '10' && err) { return done(); } assert(result.indexOf('error') > -1, 'Should have received an error, but instead got: ' + result); done(); }); }); }); }); }); describe('sails new .', function() { it('should create new app in existing folder', function(done) { // make app folder and move into directory fs.mkdirSync(appName); process.chdir(appName); exec( 'node '+ path.resolve('..', sailsbin) + ' new . --fast --traditional --without=lodash,async', function(err) { if (err) { return done(new Error(err)); } // move from app to its parent directory process.chdir('../'); done(); }); }); it('should not overwrite a folder', function(done) { // make app folder and move into directory fs.mkdirSync(appName); process.chdir(appName); fs.mkdirSync('test'); exec( 'node ' + path.resolve('..', sailsbin) + ' new . --fast --traditional --without=lodash,async', function(err, dumb, result) { // move from app to its parent directory process.chdir('../'); // In Node v0.10.x on some environments (like in Appveyor), this just // returns an Error in `err` instead of a result, so account for that. if (process.versions.node.split('.')[0] === '0' && process.versions.node.split('.')[1] === '10' && err) { return done(); } assert(result.indexOf('error') > -1, 'Should have received an error, but instead got: ' + result); done(); }); }); }); }); ================================================ FILE: test/integration/router.params.test.js ================================================ /** * Module dependencies */ var util = require('util'); var assert = require('assert'); var httpHelper = require('./helpers/httpHelper.js'); var appHelper = require('./helpers/appHelper'); var Err = { badResponse: function(response) { return 'Wrong server response! Response :::\n' + util.inspect(response.body); } }; describe('router :: ', function() { describe('Parameters', function() { var appName = 'testApp'; before(function(done) { appHelper.build(done); }); beforeEach(function(done) { appHelper.lift({verbose: false}, function(err, sails) { if (err) {throw new Error(err);} sailsprocess = sails; setTimeout(done, 100); }); }); afterEach(function(done) { sailsprocess.lower(function(){setTimeout(done, 100);}); }); after(function() { process.chdir('../'); appHelper.teardown(); }); describe('"length" param', function() { before(function(){ require('fs').writeFileSync('config/routes.js', 'module.exports.routes = {"/testLength": function(req,res){res.send(req.param("length"));}};'); }); it('when sent as a query param, should respond with the correct value of `length`', function(done) { httpHelper.testRoute('get', 'testLength?length=long', function(err, response) { if (err) { return done(err); } assert(response.body==='long', Err.badResponse(response)); done(); }); }); it('when sent as a body param, should respond with the correct value of `length`', function(done) { httpHelper.testRoute('post', {url: 'testLength', json: {length: 'short'}}, function(err, response) { if (err) { return done(err); } assert(response.body==='short', Err.badResponse(response)); done(); }); }); }); describe('"touch" param (with no value)', function() { before(function(){ require('fs').writeFileSync('config/routes.js', 'module.exports.routes = {"/testTouch": function(req,res){res.send(typeof req.param("touch") !== "undefined");}};'); }); it('when sent as a query param, should respond with a truthy value', function(done) { httpHelper.testRoute('get', 'testTouch?touch', function(err, response) { if (err) { return done(err); } assert(response.body==='true', Err.badResponse(response)); done(); }); }); }); describe('req.param() precedence', function() { before(function(){ require('fs').writeFileSync('config/routes.js', 'module.exports.routes = {"/test/:foo": function(req,res){res.json(req.param("foo"));}, "/test": function(req,res){res.json(req.param("foo"));}};'); }); it('when sent a value is specified in the query, body and route, route param should take precedence', function(done) { httpHelper.testRoute('post', {url: 'test/abc?foo=123', json: {foo: 666}}, function(err, response) { if (err) { return done(err); } assert(response.body==='abc', Err.badResponse(response)); done(); }); }); it('when sent a value is specified in the query and body, body should take precedence', function(done) { httpHelper.testRoute('post', {url: 'test?foo=123', json: {foo: 666}}, function(err, response) { if (err) { return done(err); } assert(response.body===666, Err.badResponse(response)); done(); }); }); }); describe('req.param() defaults', function() { before(function(){ require('fs').writeFileSync('config/routes.js', 'module.exports.routes = {"/test": function(req,res){res.json(req.param("foo", "bar"));}, "/none": function(req,res){res.json(req.param("foo"));}};'); }); it('when a value for a param is specified, that value should be used instead of the default', function(done) { httpHelper.testRoute('post', {url: 'test/?foo=123'}, function(err, response) { if (err) { return done(err); } assert(response.body==='"123"', Err.badResponse(response)); done(); }); }); it('when no value for a param is specified, the default should be used', function(done) { httpHelper.testRoute('post', {url: 'test'}, function(err, response) { if (err) { return done(err); } assert(response.body==='"bar"', Err.badResponse(response)); done(); }); }); it('when no value for a param is specified, and there is no default, the param should be undefined', function(done) { httpHelper.testRoute('post', {url: 'none'}, function(err, response) { if (err) { return done(err); } assert(response.body==='', Err.badResponse(response)); done(); }); }); }); describe('req.params.allParams', function() { before(function(){ require('fs').writeFileSync('config/routes.js', 'module.exports.routes = {"/testParams/:foo": function(req,res){res.json(req.allParams());}};'); }); it('should return the correct param values, accounting for precedence', function(done) { httpHelper.testRoute('post', {url: 'testParams/abc?foo=123&baz=999&bar=555&touch', json: {bar: 666}}, function(err, response) { if (err) { return done(err); } assert.equal(response.body.foo, 'abc'); assert.equal(response.body.bar, 666); assert.equal(response.body.baz, 999); assert.equal(response.body.touch, ''); done(); }); }); }); }); }); ================================================ FILE: test/integration/router.specifiedRoutes.test.js ================================================ /** * Module dependencies */ var util = require('util'); var assert = require('assert'); var httpHelper = require('./helpers/httpHelper.js'); var appHelper = require('./helpers/appHelper'); var Err = { badResponse: function(response) { return 'Wrong server response! Response :::\n' + util.inspect(response.body); } }; describe('router :: ', function() { describe('Specified routes', function() { var appName = 'testApp'; before(function(done) { appHelper.build(done); }); beforeEach(function(done) { appHelper.lift({verbose: false}, function(err, sails) { if (err) {throw new Error(err);} sailsprocess = sails; setTimeout(done, 100); }); }); afterEach(function(done) { sailsprocess.lower(function(){setTimeout(done, 100);}); }); after(function() { process.chdir('../'); appHelper.teardown(); }); describe('an options request', function() { before(function() { httpHelper.writeRoutes({ '/*': { cors: true, }, '/testRoute': { action: 'test/verb', }, }); }); it('should respond to OPTIONS requests', function(done) { httpHelper.testRoute('options', { url: 'testRoute', headers: { 'Access-Control-Request-Method': 'post', Origin: 'https://foo.shyp.com' }, }, function(err, response, body) { assert.equal(response.statusCode, 200); assert.equal(response.headers['access-control-allow-origin'], '*'); done(); }); }); }); describe('with an unspecified http method', function() { before(function() { httpHelper.writeRoutes({ '/testRoute': { action: 'test/verb' } }); }); it('should respond to get requests', function(done) { httpHelper.testRoute('get', 'testRoute', function(err, response) { if (err) { return done(err); } assert(response.body === 'get', Err.badResponse(response)); done(); }); }); it('should respond to post requests', function(done) { httpHelper.testRoute('post', 'testRoute', function(err, response) { if (err) { return done(err); } assert(response.body === 'post', Err.badResponse(response)); done(); }); }); it('should respond to put requests', function(done) { httpHelper.testRoute('put', 'testRoute', function(err, response) { if (err) { return done(err); } assert(response.body === 'put', Err.badResponse(response)); done(); }); }); it('should respond to delete requests', function(done) { httpHelper.testRoute('del', 'testRoute', function(err, response) { if (err) { return done(err); } assert(response.body === 'delete', Err.badResponse(response)); done(); }); }); }); describe('with get http method specified', function() { before(function() { httpHelper.writeRoutes({ 'get /testRoute': { action: 'test/verb' } }); }); it('should respond to get requests', function(done) { httpHelper.testRoute('get', 'testRoute', function(err, response) { if (err) { return done(err); } assert(response.body === 'get', Err.badResponse(response)); done(); }); }); it('shouldn\'t respond to post requests', function(done) { httpHelper.testRoute('post', 'testRoute', function(err, response) { if (err) { return done(err); } assert(response.body !== 'post', Err.badResponse(response)); done(); }); }); }); describe('with post http method specified', function() { before(function() { httpHelper.writeRoutes({ 'post /testRoute': { action: 'test/verb' } }); }); it('should respond to post requests', function(done) { httpHelper.testRoute('post', 'testRoute', function(err, response) { if (err) { return done(err); } assert(response.body === 'post', Err.badResponse(response)); done(); }); }); }); describe('with put http method specified', function() { before(function() { httpHelper.writeRoutes({ 'put /testRoute': { action: 'test/verb' } }); }); it('should respond to put requests', function(done) { httpHelper.testRoute('put', 'testRoute', function(err, response) { if (err) { return done(err); } assert(response.body === 'put', Err.badResponse(response)); done(); }); }); }); describe('with delete http method specified', function() { before(function(){ httpHelper.writeRoutes({ 'delete /testRoute': { action: 'test/verb' } }); }); it('should respond to delete requests', function(done) { httpHelper.testRoute('del', 'testRoute', function(err, response) { if (err) { return done(err); } assert(response.body === 'delete', Err.badResponse(response)); done(); }); }); }); describe('with dynamic url paths specified', function() { before(function() { httpHelper.writeRoutes({ 'get /test/:category/:size': { action: 'test/dynamic' } }); }); it('should respond to requests that match the url pattern', function(done) { httpHelper.testRoute('get', 'test/shirts/large', function(err, response) { if (err) { return done(err); } var body = JSON.parse(response.body); assert.equal(body.category, 'shirts'); assert.equal(body.size, 'large'); done(); }); }); }); describe('should be case-insensitive', function() { before(function() { httpHelper.writeRoutes({ 'get /testRoute': { action: 'test/verb' } }); }); it('', function(done) { httpHelper.testRoute('get', 'tEStrOutE', function(err, response) { if (err) { return done(err); } assert(response.body === 'get', Err.badResponse(response)); done(); }); }); }); describe('should accept case-insensitive controller key', function() { before(function() { httpHelper.writeRoutes({ 'get /testRoute': { action: 'tEsT/verb' } }); }); it('', function(done) { httpHelper.testRoute('get', 'testRoute', function(err, response) { if (err) { return done(err); } assert(response.body === 'get', Err.badResponse(response)); done(); }); }); }); describe('should accept case-insensitive action key', function() { before(function(){ httpHelper.writeRoutes({ 'get /testRoute': { action: 'test/capiTalleTTers' } }); }); it('', function(done) { httpHelper.testRoute('get', 'testRoute', function(err, response) { if (err) { return done(err); } assert(response.body === 'CapitalLetters', Err.badResponse(response)); done(); }); }); }); describe('regex routes - get r|^/\\\\d+/(\\\\w+)/(\\\\w+)$|foo,bar', function() { before(function(){ require('fs').writeFileSync('config/routes.js', 'module.exports.routes = {"r|^/\\\\d+/(\\\\w+)/(\\\\w+)$|foo,bar": function(req,res){res.json({foo:req.param("foo"),bar:req.param("bar")});}};'); }); it('should match /123/abc/def and put "abc" and "def" in "foo" and "bar" params', function(done) { httpHelper.testRoute('get', '123/abc/def', function(err, response) { if (err) { return done(err); } var body = JSON.parse(response.body); assert(body.foo==='abc', Err.badResponse(response)); assert(body.bar==='def', Err.badResponse(response)); done(); }); }); it('should match /9/fizzle/fazzle and put "fizzle" and "fazzle" in "foo" and "bar" params', function(done) { httpHelper.testRoute('get', '9/fizzle/fazzle', function(err, response) { if (err) {return done(new Error(err));} var body = JSON.parse(response.body); try { assert.equal(body.foo, 'fizzle', Err.badResponse(response)); assert.equal(body.bar, 'fazzle', Err.badResponse(response)); } catch (e) { return done(e); } done(); }); }); }); describe('skipAssets', function() { before(function(){ httpHelper.writeRoutes({ '/*': { skipAssets: true, action: 'test/index' } }); }); it('should match /foo', function(done) { httpHelper.testRoute('get', 'foo', function(err, response) { if (err) { return done(new Error(err)); } try { assert.equal(response.body, 'index', Err.badResponse(response)); } catch (e) { return done(e); } done(); }); }); it('should match /foo?abc=1.2.3', function(done) { httpHelper.testRoute('get', 'foo?abc=1.2.3', function(err, response) { if (err) { return done(err); } try { assert.equal(response.body, 'index', Err.badResponse(response)); } catch (e) { return done(e); } done(); }); }); it('should match /foo.bar/baz?abc=1.2.3', function(done) { httpHelper.testRoute('get', 'foo.bar/baz?abc=1.2.3', function(err, response) { if (err) { return done(err); } try { assert.equal(response.body, 'index', Err.badResponse(response)); } catch (e) { return done(e); } done(); }); }); it('should not match /foo.js', function(done) { httpHelper.testRoute('get', 'foo.js', function(err, response) { if (err) { return done(err); } assert(response.statusCode === 404, Err.badResponse(response)); done(); }); }); it('should not match /js/dependencies/pretend.js', function(done) { httpHelper.testRoute('get', 'js/dependencies/pretend.js', function(err, response) { if (err) { return done(err); } assert(response.statusCode === 404, Err.badResponse(response)); done(); }); }); it('should not match /js/dependencies/pretend.io.js', function(done) { httpHelper.testRoute('get', 'js/dependencies/pretend.io.js', function(err, response) { if (err) { return done(err); } assert(response.statusCode === 404, Err.badResponse(response)); done(); }); }); it('should not match /styles/pretendporter.css', function(done) { httpHelper.testRoute('get', 'styles/pretendporter.css', function(err, response) { if (err) { return done(err); } assert(response.statusCode === 404, Err.badResponse(response)); done(); }); }); it('should not match /foo.bar/foo.js', function(done) { httpHelper.testRoute('get', 'foo.js', function(err, response) { if (err) { return done(err); } assert(response.statusCode === 404, Err.badResponse(response)); done(); }); }); }); describe('skipRegex /abc/', function() { before(function(){ var ROUTES_FILE_CONTENTS = 'module.exports.routes = {\'/*\': {skipRegex: /abc/,action: \'test/index\'}};'; require('fs').writeFileSync('config/routes.js', ROUTES_FILE_CONTENTS); }); it('should match /foo', function(done) { httpHelper.testRoute('get', 'foo', function(err, response) { if (err) { return done(err); } try { assert.equal(response.body, 'index', Err.badResponse(response)); } catch (e) { return done(e); } done(); }); }); it('should not match /fooabcbar', function(done) { httpHelper.testRoute('get', 'fooabcbar', function(err, response) { if (err) { return done(err); } assert(response.statusCode === 404, Err.badResponse(response)); done(); }); }); }); describe('skipRegex [/abc/, /def/]', function() { before(function(){ require('fs').writeFileSync('config/routes.js', 'module.exports.routes = {\'/*\': {skipRegex: [/abc/,/def/],action: \'test/index\'}};'); }); it('should match /foo', function(done) { httpHelper.testRoute('get', 'foo', function(err, response) { if (err) { return done(err); } try { assert.equal(response.body, 'index', Err.badResponse(response)); } catch (e) { return done(e); } done(); }); }); it('should not match /fooabcbar', function(done) { httpHelper.testRoute('get', 'fooabcbar', function(err, response) { if (err) { return done(err); } assert(response.statusCode === 404, Err.badResponse(response)); done(); }); }); it('should not match /foodefbar', function(done) { httpHelper.testRoute('get', 'foodefbar', function(err, response) { if (err) { return done(err); } assert(response.statusCode === 404, Err.badResponse(response)); done(); }); }); }); if (Number(process.version.match(/^v(\d+\.\d+)/)[1]) >= 7.6) { describe('Async route handlers :: ', function() { describe('ad-hoc functions', function() { before(function() { require('fs').writeFileSync('config/routes.js', 'module.exports.routes = {"/testasync": async function(req, res, next) { throw new Error("foo!"); } }'); }); it('should handle uncaught promises correctly', function(done) { httpHelper.testRoute('get', 'testasync', function(err, response) { if (err) { return done(err); } assert(response.statusCode === 500); done(); }); }); }); describe('actions', function() { before(function() { require('fs').writeFileSync('config/routes.js', 'module.exports.routes = {"/testasyncerror": "asynctesterr", "/testasyncok": "asynctestok" }'); require('fs').writeFileSync('api/controllers/asynctesterr.js', 'module.exports = async function(req, res, next) { throw new Error("foo!"); }'); require('fs').writeFileSync('api/controllers/asynctestok.js', 'module.exports = async function(req, res, next) { var dumb = function() { return new Promise (function(resolve, reject) { setTimeout(function(){ return resolve("foo")}, 100);}); }; var val = await dumb(); return res.send(val) }'); }); it('should handle responses correctly', function(done) { httpHelper.testRoute('get', 'testasyncok', function(err, response) { if (err) { return done(err); } assert(response.statusCode === 200); assert(response.body === 'foo'); done(); }); }); it('should handle uncaught promises correctly', function(done) { httpHelper.testRoute('get', 'testasyncerror', function(err, response) { if (err) { return done(err); } assert(response.statusCode === 500); done(); }); }); }); }); } }); }); ================================================ FILE: test/integration/router.viewRendering.test.js ================================================ /** * Test dependencies */ var assert = require('assert'); var httpHelper = require('./helpers/httpHelper.js'); var appHelper = require('./helpers/appHelper'); var _ = require('@sailshq/lodash'); var fs = require('fs'); describe('router :: ', function() { describe('View routes', function() { var appName = 'testApp'; before(function(done) { appHelper.build(function() { fs.writeFileSync('config/extraroutes.js', 'module.exports.routes = ' + JSON.stringify({ '/testView': { view: 'viewtest/index' }, '/app': { view: 'app' }, '/user': { view: 'app/user/homepage' } })); return done(); }); }); beforeEach(function(done) { appHelper.lift({ verbose: false }, function(err, sails) { if (err) { throw new Error(err); } sailsprocess = sails; return done(); }); }); afterEach(function(done) { sailsprocess.lower(done); }); after(function() { process.chdir('../'); appHelper.teardown(); }); describe('with default routing', function() { it('should respond to a get request to localhost:1342 with welcome page', function(done) { httpHelper.testRoute('get', '', function(err, response) { if (err) { return done(new Error(err)); } assert(response.body.indexOf('not found') < 0); assert(response.body.indexOf('<!-- Default home page -->') > -1); done(); }); }); it('should wrap the view in the default layout', function(done) { httpHelper.testRoute('get', '', function(err, response) { if (err) { return done(new Error(err)); } assert(response.body.indexOf('<html>') > -1); done(); }); }); }); describe('with specified routing using the "view:" syntax', function() { it('route with config {view: "app"} should respond to a get request with the "app/index.ejs" view if "app.ejs" does not exist', function(done) { httpHelper.testRoute('get', 'app', function(err, response) { if (err) { return done(new Error(err)); } assert(response.body.indexOf('not found') < 0); assert(response.body.indexOf('App index file') > -1); done(); }); }); it('route with config {view: "viewtest/index"} should respond to a get request with "viewtest/index.ejs"', function(done) { httpHelper.testRoute('get', 'testView', function(err, response) { if (err) { return done(new Error(err)); } assert(response.body.indexOf('not found') < 0); assert(response.body.indexOf('indexView') > -1); done(); }); }); it('route with config {view: "app/user/homepage"} should respond to a get request with "app/user/homepage.ejs"', function(done) { httpHelper.testRoute('get', 'user', function(err, response) { if (err) { return done(new Error(err)); } assert(response.body.indexOf('not found') < 0); assert(response.body.indexOf('I\'m deeply nested!') > -1); done(); }); }); }); xdescribe('with no specified routing', function() { before(function() { httpHelper.writeRoutes({}); }); it('should respond to get request to :controller with the template at views/:controller/index.ejs', function(done) { // Empty router file httpHelper.testRoute('get', 'viewTest', function(err, response) { if (err) { return done(new Error(err)); } assert(response.body.indexOf('indexView') !== -1, response.body); done(); }); }); it('should respond to get request to :controller/:action with the template at views/:controller/:action.ejs', function(done) { httpHelper.testRoute('get', 'viewTest/create', function(err, response) { if (err) { return done(new Error(err)); } assert(response.body.indexOf('createView') !== -1); done(); }); }); it('should merge config.views.locals into the view locals', function(done) { httpHelper.testRoute('get', 'viewTest/viewOptions', function(err, response) { if (err) { return done(new Error(err)); } assert(response.body.indexOf('!bar!') !== -1); done(); }); }); it('should allow config.views.locals to be overridden', function(done) { httpHelper.testRoute('get', 'viewTest/viewOptionsOverride', function(err, response) { if (err) { return done(new Error(err)); } assert(response.body.indexOf('!baz!') !== -1); done(); }); }); }); }); }); ================================================ FILE: test/integration/www.test.js ================================================ /** * Test dependencies */ var assert = require('assert'); var fs = require('fs-extra'); var exec = require('child_process').exec; var path = require('path'); var spawn = require('child_process').spawn; var tmp = require('tmp'); // Make existsSync not crash on older versions of Node fs.existsSync = fs.existsSync || require('path').existsSync; describe('Running sails www', function() { var sailsBin = path.resolve('./bin/sails.js'); var appName = 'testApp'; before(function() { if (fs.existsSync(appName)) { fs.removeSync(appName); } }); describe('in an empty directory', function() { before(function() { // Make empty folder and move into it fs.mkdirSync('empty'); process.chdir('empty'); sailsBin = path.resolve('..', sailsBin); }); // TODO: run tests in here after(function() { // Delete empty folder and move out of it process.chdir('../'); fs.rmdirSync('empty'); sailsBin = path.resolve(sailsBin); }); }); describe('in a sails app directory', function() { var sailsChildProc; var curDir; var tmpDir; before(function(done) { // Cache the current working directory. curDir = process.cwd(); // Create a temp directory. tmpDir = tmp.dirSync({gracefulCleanup: true, unsafeCleanup: true}); // Switch to the temp directory. process.chdir(tmpDir.name); return done(); }); after(function(done) { process.chdir(curDir); return done(); }); it('should start server without error', function(done) { exec('node ' + sailsBin + ' new ' + appName + ' --fast --traditional --without=lodash,async', function(err) { if (err) { return done(new Error(err)); } // Move into app directory process.chdir(appName); sailsBin = path.resolve('..', sailsBin); sailsChildProc = spawn('node', [sailsBin, 'www']); // Any output from stderr is considered an error by this test. sailsChildProc.stderr.on('data', function(data) { return done(data); }); sailsChildProc.stdout.on('data', function(data) { var dataString = data + ''; assert(dataString.indexOf('error') === -1); sailsChildProc.stdout.removeAllListeners('data'); // Move out of app directory process.chdir('../'); sailsChildProc.kill(); return done(); }); }); }); }); describe('with command line arguments', function() { var sailsChildProc; var curDir; var tmpDir; beforeEach(function(done) { // Cache the current working directory. curDir = process.cwd(); // Create a temp directory. tmpDir = tmp.dirSync({gracefulCleanup: true, unsafeCleanup: true}); // Switch to the temp directory. process.chdir(tmpDir.name); // Create a new Sails app in the temp directory. exec('node ' + sailsBin + ' new ' + appName + ' --fast --traditional --without=lodash,async', function(err) { if (err) { return done(new Error(err)); } process.chdir(path.resolve(tmpDir.name, appName)); return done(); }); }); afterEach(function(done) { sailsChildProc.stderr.removeAllListeners('data'); process.chdir(curDir); sailsChildProc.kill(); return done(); }); it('--dev should execute grunt build', function(done) { // Change environment to production in config file fs.writeFileSync('config/application.js', 'module.exports = ' + JSON.stringify({ appName: 'Sails Application', port: 1342, environment: 'production', log: { level: 'info' } })); sailsChildProc = spawn('node', [sailsBin, 'www', '--dev']); sailsChildProc.stdout.on('data', function(data) { var dataString = data + ''; if (dataString.indexOf('`grunt build`') !== -1) { return done(); } }); }); it('--prod should execute grunt buildProd', function(done) { // Overrwrite session config file // to set session adapter:null ( to prevent warning message from appearing on command line ) fs.writeFileSync('config/session.js', 'module.exports.session = { adapter: null }'); sailsChildProc = spawn('node', [sailsBin, 'www', '--prod']); sailsChildProc.stdout.on('data', function(data) { var dataString = data + ''; if (dataString.indexOf('`grunt buildProd`') !== -1) { return done(); } }); }); }); }); ================================================ FILE: test/mocha.opts ================================================ --reporter spec --recursive --slow 2000 --timeout 18000 ================================================ FILE: test/unit/App.prototype.load.test.js ================================================ /** * Module dependencies */ var assert = require('assert'); var util = require('util'); var async = require('async'); var Sails = require('../../lib').constructor; describe('App', function (){ var app; describe('.prototype.load', function () { it('should return app instance (so it can be chained w/ other prototypal methods)', function (done) { app = new Sails(); var returnValue; async.auto({ loadSails: function (next) { returnValue = app.load({ globals: false, loadHooks: [ 'moduleloader', 'userconfig', 'http', 'views' ] }, function onLoaded (err){ if (err) return next(err); // Return value should === `app` whenever sails finishes loading assert.equal(app, returnValue); next(); }); // Return value should === `app` immediately assert.equal(app, returnValue); }, // Return value should === `app` later on laterOn: function (next) { setTimeout(function (){ assert.equal(app, returnValue); next(); }, 150); } }, done); }); }); it('should initialize w/ the session hook', function (done) { app = new Sails(); app.load({ globals: false, loadHooks: [ 'moduleloader', 'userconfig', 'http', 'session', 'views' ] }, done); }); }); ================================================ FILE: test/unit/README.md ================================================ # Unit tests Unit tests shouldn't lift the server (i.e. bind an HTTP server or WebSocket server to a port). Instead, they should bootstrap the minimal set of necessary components to test a particular method (or sometimes a group of methods, if it makes more sense.) The goal is to identify future breaking changes and isolate _exactly what broke_. This makes would-be issues easier to spot in advance, and real bugs easier to track down after the fact. ## What _Not_ To Test Since unit tests are more implementation-specific, we shouldn't unit test parts of Sails which are currently in flux or likely to change. ## How Can I Help? #### Unit tests for the hook loader 1. If we run the hook loader with options for conditionally loading hooks, the server should start with the correct hooks applied. 2. The hook loader should fail if a hook has other hooks as dependencies, but those dependencies are omitted. 3. If a graph of circular dependencies is passed into the hook loader, it should fail. > No one is currently working on this #### Unit tests for each core hook 1. initialize() 2. loadModules() 3. configure() 3. Other important methods (hook-specific) > No one is currently working on this ================================================ FILE: test/unit/app.getRouteFor.test.js ================================================ /** * Module dependencies */ var assert = require('assert'); var util = require('util'); var Sails = require('../../lib').constructor; describe('app.getRouteFor()', function (){ var app; before(function (done){ app = new Sails(); app.load({ globals: false, loadHooks: [], log: { level: 'error' }, routes: { 'get /signup': 'PageController.signup', 'post /signup': 'UserController.signup', 'post /*': 'UserController.signup', 'get /': { controller: 'PageController', action: 'homepage' }, 'get /home': 'index', 'get /about': { target: 'PageController.about' }, 'get /admin': { target: 'PageController.adminDashboard' }, 'get /badmin': { target: 'PageController.admndashboard' }, 'get /wolves': 'WolfController.find', 'get /wolves/:id': { target: 'WolfController.findOne' }, 'post /wolves': { controller: 'WolfController', action: 'create' }, 'options /wolves/test': { target: 'WolfController.CreaTe' }, 'get /my-machineFn': { action: 'machines/machinefn' }, 'get /my-page': { view: 'somepage' }, 'get /cause-trouble': [{ policy: 'be-good'}, { action: 'trouble/cause'}], 'put /cause-more-trouble': [{ policy: 'be-good'}, { controller: 'TroubleController', action: 'cause-more' }], }, controllers: { moduleDefinitions: { 'machines/machinefn': { fn: function () {} } } } }, done); }); it('should return appropriate route info dictionary with simplified usage', function () { var route = app.getRouteFor('PageController.signup'); assert.equal(route.method, 'get'); assert.equal(route.url, '/signup'); }); it('should return appropriate route info dictionary with expanded usage', function () { var route = app.getRouteFor({ target: 'PageController.signup' }); assert.equal(route.method, 'get'); assert.equal(route.url, '/signup'); }); it('should return the _first_ matching route', function () { var route = app.getRouteFor('UserController.signup'); assert.equal(route.method, 'post'); assert.equal(route.url, '/signup'); }); it('should work with new action target syntax', function() { var route = app.getRouteFor('user/signup'); assert.equal(route.method, 'post'); assert.equal(route.url, '/signup'); }); it('should work with strings without dots or slashes', function() { var route = app.getRouteFor('index'); assert.equal(route.method, 'get'); assert.equal(route.url, '/home'); }); it('should throw usage error (i.e. `e.code===\'E_NOT_FOUND\'`) if target to search is not found', function () { try { app.getRouteFor('JuiceController.makeJuice'); assert(false, 'Should have thrown an error'); } catch (e) { if (e.code !== 'E_NOT_FOUND') { assert(false, 'Should have thrown an error w/ code === "E_NOT_FOUND", instead got: ' + util.inspect(e)); } } }); it('should throw usage error (i.e. `e.code===\'E_USAGE\'`) if target to search for not specified or is invalid', function (){ try { app.getRouteFor(); assert(false, 'Should have thrown an error'); } catch (e) { if (e.code !== 'E_USAGE') { assert(false, 'Should have thrown an error w/ code === "E_USAGE"'); } } try { app.getRouteFor(3235); assert(false, 'Should have thrown an error'); } catch (e) { if (e.code !== 'E_USAGE') { assert(false, 'Should have thrown an error w/ code === "E_USAGE"'); } } try { app.getRouteFor({ x: 32, y: 49 }); assert(false, 'Should have thrown an error'); } catch (e) { if (e.code !== 'E_USAGE') { assert(false, 'Should have thrown an error w/ code === "E_USAGE"'); } } try { app.getRouteFor(function(){}); assert(false, 'Should have thrown an error'); } catch (e) { if (e.code !== 'E_USAGE') { assert(false, 'Should have thrown an error w/ code === "E_USAGE"'); } } }); it('should be able to match different syntaxes (routes that specify separate controller+action, or specifically specify a target)', function (){ assert.equal( app.getRouteFor({controller: 'WolfController', action: 'find'}).url, '/wolves' ); assert.equal( app.getRouteFor({controller: 'WolfController', action: 'find'}).method, 'get' ); assert.equal( app.getRouteFor({controller: 'wolf', action: 'find'}).url, '/wolves' ); assert.equal( app.getRouteFor({controller: 'wolf', action: 'find'}).method, 'get' ); assert.equal( app.getRouteFor({target: {controller: 'wolf', action: 'find'}}).url, '/wolves' ); assert.equal( app.getRouteFor({target: {controller: 'wolf', action: 'find'}}).method, 'get' ); assert.equal( app.getRouteFor('WolfController.find').url, '/wolves' ); assert.equal( app.getRouteFor('WolfController.find').method, 'get' ); assert.equal( app.getRouteFor({target: 'WolfController.find'}).url, '/wolves' ); assert.equal( app.getRouteFor({target: 'WolfController.find'}).method, 'get' ); assert.equal( app.getRouteFor('WolfController.findOne').url, '/wolves/:id' ); assert.equal( app.getRouteFor('WolfController.findOne').method, 'get' ); assert.equal( app.getRouteFor('WolfController.create').url, '/wolves' ); assert.equal( app.getRouteFor('WolfController.create').method, 'post' ); assert.equal( app.getRouteFor('machines/machinefn').url, '/my-machineFn' ); assert.equal( app.getRouteFor('machines/machinefn').method, 'get' ); assert.equal( app.getRouteFor('trouble/cause').url, '/cause-trouble' ); assert.equal( app.getRouteFor('trouble/cause').method, 'get' ); assert.equal( app.getRouteFor('trouble/cause-more').url, '/cause-more-trouble' ); assert.equal( app.getRouteFor('trouble/cause-more').method, 'put' ); }); it('should be case-insensitive regarding controller / action names', function (){ assert.equal( app.getRouteFor('WolfController.CreaTe').url, '/wolves' ); assert.equal( app.getRouteFor('WolfController.CreaTe').method, 'post' ); assert.equal( app.getRouteFor({controller: 'WOLF', action: 'finD'}).url, '/wolves' ); assert.equal( app.getRouteFor({controller: 'WOLF', action: 'finD'}).method, 'get' ); }); }); ================================================ FILE: test/unit/app.getUrlFor.test.js ================================================ /** * Module dependencies */ var assert = require('assert'); var util = require('util'); var Sails = require('../../lib').constructor; describe('app.getUrlFor()', function (){ var app; before(function (done){ app = new Sails(); app.load({ globals: false, loadHooks: [], routes: { 'get /signup': 'PageController.signup', 'post /login': 'UserController.login', 'get /login': 'PageController.login', 'post /*': 'UserController.login' }, controllers: { moduleDefinitions: { 'page/signup': function() {}, 'page/login': function() {}, 'user/login': function() {} } } }, done); }); it('should return appropriate route URL with simplified usage', function () { assert.equal( app.getUrlFor('PageController.signup'), '/signup' ); }); it('should return appropriate route URL with expanded usage', function () { assert.equal( app.getUrlFor({ target: 'PageController.login' }), '/login' ); }); it('should return the _first_ matching route URL for the given target', function () { assert.equal( app.getUrlFor('UserController.login'), '/login' ); }); }); ================================================ FILE: test/unit/app.initializeHooks.test.js ================================================ /** * Module dependencies */ var should = require('should'); var assert = require('assert'); var _ = require('@sailshq/lodash'); var constants = require('../fixtures/constants'); var customHooks = require('../fixtures/customHooks'); var $Sails = require('../helpers/sails'); // TIP: // // To get a hold of the `sails` instance as a closure variable // (i.e. if you're tired of using the mocha context): // var sails; // $Sails.get(function (_sails) { sails = _sails; }); describe('app.initializeHooks()', function() { describe('with no hooks', function() { var sails = $Sails.load.withAllHooksDisabled(); it('hooks should be exposed on the `sails` global', function() { sails.hooks.should.be.an.Object; }); }); describe('with all core hooks and default config', function() { var sails = $Sails.load(); it('should expose hooks on the `sails` global', function() { sails.hooks.should.be.an.Object; }); it('should expose at least the expected core hooks', function() { var intersection = _.intersection(_.keys(sails.hooks), _.keys(constants.EXPECTED_DEFAULT_HOOKS)); // If i18n is missing, that might be ok-- but just check to be sure sails.config.i18n.locales is `[]`. // (i.e. it must have turned itself off) if (!_.contains(intersection, 'i18n')) { assert(_.isEqual(sails.config.i18n.locales, []), 'i18n.locales config must be [] in this situation'); } assert.deepEqual(intersection, _.without(_.keys(constants.EXPECTED_DEFAULT_HOOKS), 'i18n'), 'Missing expected default hooks'); }); }); describe('with the grunt hook set to boolean false', function() { var sails = $Sails.load({hooks: {grunt: false}}); it('should expose hooks on the `sails` global', function() { sails.hooks.should.be.an.Object; }); it('should expose all the core hooks except for Grunt', function() { var intersection = _.intersection(_.keys(sails.hooks), _.keys(constants.EXPECTED_DEFAULT_HOOKS)); assert.deepEqual(intersection, _.without(_.keys(constants.EXPECTED_DEFAULT_HOOKS), 'grunt', 'i18n'), 'Missing expected default hooks'); }); }); describe('with the grunt hook set to the string "false"', function() { var sails = $Sails.load({hooks: {grunt: "false"}}); it('should expose hooks on the `sails` global', function() { sails.hooks.should.be.an.Object; }); it('should expose all the core hooks except for Grunt', function() { var intersection = _.intersection(_.keys(sails.hooks), _.keys(constants.EXPECTED_DEFAULT_HOOKS)); assert.deepEqual(intersection, _.without(_.keys(constants.EXPECTED_DEFAULT_HOOKS), 'grunt', 'i18n'), 'Missing expected default hooks'); }); }); describe('configured with a custom hook called `noop`', function() { var sails = $Sails.load({ hooks: { noop: customHooks.NOOP } }); it('should expose `noop`', function() { sails.hooks.should.have .property('noop'); }); it('should also expose the expected core hooks', function() { var intersection = _.intersection(Object.keys(sails.hooks), _.keys(constants.EXPECTED_DEFAULT_HOOKS)); assert.deepEqual(intersection, _.without(_.keys(constants.EXPECTED_DEFAULT_HOOKS), 'i18n'), 'Missing expected default hooks'); }); }); describe('configured with a hook (`noop2`), but not its dependency (`noop`)', function() { var sails = $Sails.load.expectFatalError({ hooks: { // This forced failure is only temporary-- // very hard to test right now as things stand. whadga: function(sails) { throw 'temporary forced failure to simulate dependency issue'; }, noop2: customHooks.NOOP2 } }); }); describe('configured with a hook that always throws', function() { var sails = $Sails.load.expectFatalError({ hooks: { // This forced failure is only temporary-- // very hard to test right now as things stand. badHook: customHooks.SPOILED_HOOK } }); }); describe('configured with a custom hook with a `defaults` object', function() { var sails = $Sails.load({ hooks: { defaults_obj: customHooks.DEFAULTS_OBJ }, inky: { pinky: 'boo' } }); it('should add a `foo` key to sails config', function() { assert(sails.config.foo === 'bar'); }); it('should add an `inky.dinky` key to sails config', function() { assert(sails.config.inky.dinky === 'doo'); }); it('should keep the existing `inky.pinky` key to sails config', function() { assert(sails.config.inky.pinky === 'boo'); }); }); describe('configured with a custom hook with a `defaults` function', function() { var sails = $Sails.load({ hooks: { defaults_fn: customHooks.DEFAULTS_FN }, inky: { pinky: 'boo' } }); it('should add a `foo` key to sails config', function() { assert(sails.config.foo === 'bar'); }); it('should add an `inky.dinky` key to sails config', function() { assert(sails.config.inky.dinky === 'doo'); }); it('should keep the existing `inky.pinky` key to sails config', function() { assert(sails.config.inky.pinky === 'boo'); }); }); describe('configured with a custom hook with a `configure` function', function() { var sails = $Sails.load({ hooks: { config_fn: customHooks.CONFIG_FN }, testConfig: 'oh yeah!' }); it('should add a `hookConfigLikeABoss` key to sails config', function() { assert(sails.config.hookConfigLikeABoss === 'oh yeah!'); }); }); describe('configured with a custom hook with an `initialize` function', function() { var sails = $Sails.load({ hooks: { init_fn: customHooks.INIT_FN } }); it('should add a `hookInitLikeABoss` key to sails config', function() { assert(sails.config.hookInitLikeABoss === true); }); }); describe('configured with a custom hook with a `routes` object', function() { var sails = $Sails.load({ hooks: { routes: customHooks.ROUTES }, routes: { "GET /foo": function(req, res, next) {sails.config.foo += "b"; return next();} } }); it('should add two `/foo` routes to the sails config', function() { var fooRoutes = 0; _.each(sails.router._privateRouter.stack, function(stack){ if(stack.route.path === '/foo' && stack.route.methods.get === true){ fooRoutes += 1; } }); assert(fooRoutes === 3); }); it('should bind the routes in the correct order', function(done) { sails.request({ method: 'get', url: '/foo' }, function (err, res, body) { if (err) return done(err); assert.equal(res.statusCode, 200); assert.equal(body, 'abc'); return done(); }); }); }); describe('configured with a custom hook with advanced routing', function() { var sails = $Sails.load({ hooks: { advanced_routes: customHooks.ADVANCED_ROUTES }, routes: { "GET /foo": function(req, res, next) {sails.config.foo += "c"; return next();} } }); it('should add four `/foo` routes to the sails config', function() { var fooRoutes = 0; _.each(sails.router._privateRouter.stack, function(stack){ if(stack.route.path === '/foo' && stack.route.methods.get === true){ fooRoutes += 1; } }); assert(fooRoutes === 5); }); it('should bind the routes in the correct order', function(done) { sails.request({ method: 'get', url: '/foo' }, function (err, res, body) { if (err) return done(err); assert.equal(res.statusCode, 200); assert.equal(body, 'abcde'); return done(); }); }); }); // describe('configured with a circular hook dependency', function () { // // NOTE #1: not currently implemented // // NOTE #2: not currently possible // // (should be possible after merging @ragulka's PR) // // $Sails.load(); // it('should throw a fatal error'); // }); }); ================================================ FILE: test/unit/app.lower.test.js ================================================ var assert = require('assert'); var async = require('async'); var Sails = require('../../lib').constructor; describe('app.lower', function (){ it('should clean up event listeners', function (done) { // Get a list of all the current listeners on the process. // Note that Mocha adds some listeners, so these might not all be empty arrays! var beforeListeners = { sigusr2: process.listeners('SIGUSR2'), sigint: process.listeners('SIGINT'), sigterm: process.listeners('SIGTERM'), exit: process.listeners('exit') }; // Lift and lower 15 Sails apps in a row, to simulate a testing environment async.eachSeries(Array(15), function(i, cb) { var app = new Sails(); var options = { hooks: {i18n: false}, globals: false, log: { level: 'error' } }; async.series([ function(cb) { app.load(options, cb); }, app.initialize, app.lower ], cb); }, function(err) { if (err) {return done(err);} // Check that we have the same # of listeners as before--that is, // that all listeners that were added when the apps were initialized // were subsequently removed when they were lowered. assert.equal(beforeListeners.sigusr2.length, process.listeners('SIGUSR2').length); assert.equal(beforeListeners.sigint.length, process.listeners('SIGINT').length); assert.equal(beforeListeners.sigterm.length, process.listeners('SIGTERM').length); assert.equal(beforeListeners.exit.length, process.listeners('exit').length); return done(); }); }); }); ================================================ FILE: test/unit/app.registerAction.test.js ================================================ /** * Module dependencies */ var util = require('util'); var assert = require('assert'); var _ = require('@sailshq/lodash'); var Sails = require('../../lib').constructor; describe('sails.registerAction() :: ', function() { var sailsApp; before(function(done) { (new Sails()).load({ hooks: {grunt: false, views: false, blueprints: false, policies: false, i18n: false}, log: {level: 'error'}, routes: { '/foo': {} } }, function(err, _sails) { if (err) { return done(err); } sailsApp = _sails; return done(); }); }); after(function(done) { sailsApp.lower(done); }); it('should allow registering a new action at runtime, if it doesn\'t conflict with an existing action', function() { sailsApp.registerAction(function(req, res) {return res.ok('ok!');}, 'new-action'); assert(_.isFunction(sailsApp._actions['new-action']), 'registerAction() succeeded, but could not find the registered action in the sails._actions dictionary!'); }); it('should allow not registering a new action at runtime, if it conflicts with an existing action', function() { try { sailsApp.registerAction(function(req, res) {return res.ok('ok!');}, 'top-level-standalone-fn'); sailsApp.registerAction(function(req, res) {return res.ok('not ok!');}, 'top-level-standalone-fn'); } catch (err) { assert.equal(err.code, 'E_CONFLICT'); assert.equal(err.identity, 'top-level-standalone-fn'); return; } throw new Error('Expected an E_CONFLICT error, but didn\'t get one!'); }); }); ================================================ FILE: test/unit/app.reloadActions.test.js ================================================ /** * Module dependencies */ var util = require('util'); var assert = require('assert'); var tmp = require('tmp'); var _ = require('@sailshq/lodash'); var Filesystem = require('machinepack-fs'); var Sails = require('../../lib').constructor; tmp.setGracefulCleanup(); describe('sails.reloadActions ::', function() { describe('basic usage ::', function() { var curDir, tmpDir, sailsApp; var userHookStuff = 'foo'; before(function(done) { // Cache the current working directory. curDir = process.cwd(); // Create a temp directory. tmpDir = tmp.dirSync({gracefulCleanup: true, unsafeCleanup: true}); // Switch to the temp directory. process.chdir(tmpDir.name); // Create a top-level legacy controller file. Filesystem.writeSync({ force: true, destination: 'api/controllers/TopLevelController.js', string: 'module.exports = { fnAction: function (req, res) { res.send(\'fn controller action!\'); } };' }).execSync(); // Load the Sails app. (new Sails()).load({ globals: { sails: true, models: false, _: false, async: false, services: false }, hooks: { grunt: false, views: false, blueprints: false, policies: false, pubsub: false, i18n: false, myHook: function() {return {initialize: function(cb) {this.registerActions(cb);}, registerActions: function(cb) {sails.registerAction(function(){}, 'custom-action'); return cb();}};} }, log: {level: 'error'} }, function(err, _sails) { sailsApp = _sails; assert(sailsApp._actions['toplevel/fnaction'], 'Expected to find a `toplevel/fnaction` action, but didn\'t.'); assert(sailsApp._actions['custom-action'], 'Expected to find a `custom-action` action, but didn\'t.'); assert(!sailsApp._actions['toplevel/machineaction'], 'Didn\'t expect `toplevel/machineaction` action to exist!'); assert(!sailsApp._actions['nested/standalone-action'], 'Didn\'t expect `nested/standalone-action` action to exist!'); return done(err); }); }); after(function(done) { sailsApp.lower(function() { process.chdir(curDir); return done(); }); }); it('should reload all modules when no `hooksToSkip` is provided', function(done) { userHookStuff = 'bar'; Filesystem.writeSync({ force: true, destination: 'api/controllers/TopLevelController.js', string: 'module.exports = { fnAction: function (req, res) { res.send(\'fn controller action!\'); }, machineAction: { fn: function (inputs, exits) { exits.success(\'machine!\'); } } };' }).execSync(); Filesystem.writeSync({ force: true, destination: 'api/controllers/nested/standalone-action.js', string: 'module.exports = function (req, res) { res.send(\'standalone action!\'); };' }).execSync(); sailsApp.reloadActions(function(err) { if (err) {return done(err);} assert(sailsApp._actions['toplevel/fnaction'], 'Expected to find a `toplevel/fnaction` action, but didn\'t.'); assert(sailsApp._actions['toplevel/machineaction'], 'Expected to find a `toplevel/machineaction` action, but didn\'t.'); assert(sailsApp._actions['nested/standalone-action'], 'Expected to find a `nested/standalone-action` action, but didn\'t.'); assert(sailsApp._actions['custom-action'], 'Expected to find a `custom-action` action, but didn\'t.'); return done(); }); }); it('should skip modules for hooks listed in `hooksToSkip`', function(done) { sailsApp.reloadActions({hooksToSkip: ['myHook']}, function(err) { if (err) {return done(err);} assert(!sailsApp._actions['custom-action'], 'Expected to not find a `custom-action` action, but did!'); return done(); }); }); }); }); ================================================ FILE: test/unit/bootstrap.test.js ================================================ /** * Module dependencies */ var assert = require('assert'); var Sails = require('root-require')('lib/app'); describe('bootstrap', function (){ it('should pass the proper untampered-with error from the bootstrap to the callback of sails.lift()', function (done) { var ERROR = 'oh no I forgot my keys'; var bootstrapWasFired; Sails().lift({ globals: false, log: { level: 'silent' }, loadHooks: false, bootstrap: function (cb) { bootstrapWasFired = true; cb(ERROR); } }, function (err) { if (!bootstrapWasFired) { return done(new Error('Should have called the bootstrap function')); } if (!err) { return done(new Error('Should have passed an error to the callback of sails.lift()')); } assert.deepEqual(err, ERROR, 'Error should be exactly the same as it was when passed from the bootstrap function'); return done(); }); }); it('if the bootstrap THROWS, Sails should pass the proper untampered-with error to the callback of sails.lift()', function (done) { var ERROR = 'oh no I forgot my keys'; Sails().lift({ globals: false, log: { level: 'silent' }, loadHooks: false, bootstrap: function (cb) { bootstrapWasFired = true; throw ERROR; } }, function (err) { if (!bootstrapWasFired) { return done(new Error('Should have called the bootstrap function')); } if (!err) { return done(new Error('Should have passed an error to the callback of sails.lift()')); } assert.deepEqual(err, ERROR, 'Error should be exactly the same as it was when passed from the bootstrap function'); return done(); }); }); it('if the bootstrap throws AFTER triggering its callback, Sails should log an error'); it('should log an error if the bootstrap\'s callback is called twice'); }); ================================================ FILE: test/unit/controller.test.js ================================================ /** * Module dependencies */ var util = require('util'); var assert = require('assert'); var tmp = require('tmp'); var _ = require('@sailshq/lodash'); var Filesystem = require('machinepack-fs'); var Sails = require('../../lib').constructor; tmp.setGracefulCleanup(); /** * Errors */ var Err = { badResponse: function(response) { return 'Wrong server response! Response :::\n' + util.inspect(response.body); } }; /** * Tests */ describe('controllers :: ', function() { describe('with valid actions', function() { var curDir, tmpDir, sailsApp; var warn; var warnings = []; before(function(done) { // Cache the current working directory. curDir = process.cwd(); // Create a temp directory. tmpDir = tmp.dirSync({gracefulCleanup: true, unsafeCleanup: true}); // Switch to the temp directory. process.chdir(tmpDir.name); // Create a top-level legacy controller file. Filesystem.writeSync({ force: true, destination: 'api/controllers/TopLevelLegacyController.js', string: 'module.exports = { fnAction: function (req, res) { res.send(\'legacy fn action!\'); }, machineAction: { exits: {success: {outputExample: \'abc123\'} }, fn: function (inputs, exits) { exits.success(\'legacy machine action!\'); } }, underscore_action: function(req, res) { return res.send(); }, \'action-with-dashes\': function(req, res) { return res.send(); } };' }).execSync(); // Create a top-level action file with a req/res function. Filesystem.writeSync({ force: true, destination: 'api/controllers/top-level-standalone-fn.js', string: 'module.exports = function (req, res) { res.send(\'top level standalone fn!\'); };' }).execSync(); // Create a top-level action file with a machine. Filesystem.writeSync({ force: true, destination: 'api/controllers/top-level-standalone-machine.js', string: 'module.exports = { exits: {success: {outputExample: \'abc123\'} }, fn: function (inputs, exits) { exits.success(\'top level standalone machine!\'); } };' }).execSync(); // Create a nested legacy controller file. Filesystem.writeSync({ force: true, destination: 'api/controllers/someFolder/someOtherFolder/NestedLegacyController.js', string: 'module.exports = { fnAction: function (req, res) { res.send(\'nested legacy fn action!\'); }, machineAction: { exits: {success: {outputExample: \'abc123\'} }, fn: function (inputs, exits) { exits.success(\'nested legacy machine action!\'); } } };' }).execSync(); // Create a nested legacy controller file, with dots in the subdirectory. Filesystem.writeSync({ force: true, destination: 'api/controllers/some.folder/some.other.folder/NestedLegacyController.js', string: 'module.exports = { fnAction: function (req, res) { res.send(\'nested legacy fn action!\'); }, machineAction: { exits: {success: {outputExample: \'abc123\'} }, fn: function (inputs, exits) { exits.success(\'nested legacy machine action!\'); } } };' }).execSync(); // Create a nested action file with a machine. Filesystem.writeSync({ force: true, destination: 'api/controllers/someFolder/someOtherFolder/nested-standalone-machine.js', string: 'module.exports = { exits: {success: {outputExample: \'abc123\'} }, fn: function (inputs, exits) { exits.success(\'nested standalone machine!\'); } };' }).execSync(); // Create an invalid legacy controller (doesn't contain a dictionary) Filesystem.writeSync({ force: true, destination: 'api/controllers/LegacyControllerWithFn.js', string: 'module.exports = function (req, res) { return res.send(\'garbage\'); };' }).execSync(); // Create an invalid action (doesn't contain a machine) Filesystem.writeSync({ force: true, destination: 'api/controllers/invalid-action.js', string: 'module.exports = {};' }).execSync(); // Create an invalid file (doesn't conform to naming conventions) Filesystem.writeSync({ force: true, destination: 'api/controllers/invalidLyNamed-fileController.js', string: 'module.exports = {};' }).execSync(); // Write a routes.js file Filesystem.writeSync({ force: true, destination: 'config/routes.js', string: 'module.exports.routes = ' + JSON.stringify({ 'POST /route1': 'TopLevelLegacyController.fnAction', 'POST /route1a': 'TopLevelLegacy.fnAction', 'POST /route2': 'TopLevelLegacyController.machineAction', 'POST /route3': { controller: 'TopLevelLegacyController', action: 'fnAction', }, 'POST /route4': { action: 'toplevellegacy/fnAction', }, 'POST /route5': { action: 'top-level-standalone-fn' }, 'POST /route6': { action: 'somefolder/someotherfolder/nestedlegacy/fnaction' }, 'POST /route6a': { action: 'some/folder/some/other/folder/nestedlegacy/fnaction' }, 'POST /route7': { action: 'somefolder/someotherfolder/nested-standalone-machine' }, 'POST /warn1': { controller: 'somefolder/someotherfolder/NestedLegacyController', action: 'machineaction' }, 'POST /warn2': { controller: 'somefolder/someotherfolder/NestedLegacy', action: 'machineaction' }, 'POST /warn3': 'somefolder/someotherfolder/NestedLegacyController.machineAction', 'POST /warn4': 'some/unknown/action', 'POST /warn5': { controller: 'UnknownController', action: 'unknown/action' }, }) }).execSync(); // Load the Sails app. (new Sails()).load({hooks: {security: false, grunt: false, views: false, blueprints: false, policies: false, pubsub: false}, log: {level: 'error'}}, function(err, _sails) { sailsApp = _sails; return done(err); }); }); after(function(done) { sailsApp.lower(function() { process.chdir(curDir); return done(); }); }); it('should load all of the valid controller actions', function() { var expectedActions = [ 'toplevellegacy/fnaction', 'toplevellegacy/machineaction', 'toplevellegacy/underscore_action', 'toplevellegacy/action-with-dashes', 'top-level-standalone-fn', 'top-level-standalone-machine', 'somefolder/someotherfolder/nestedlegacy/fnaction', 'somefolder/someotherfolder/nestedlegacy/machineaction', 'some/folder/some/other/folder/nestedlegacy/fnaction', 'some/folder/some/other/folder/nestedlegacy/machineaction', 'somefolder/someotherfolder/nested-standalone-machine' ]; var unexpectedActions = _.difference(_.keys(sailsApp._actions), expectedActions); assert(!unexpectedActions.length, 'Loaded unexpected actions:\n' + util.inspect(unexpectedActions)); _.each(expectedActions, function(expectedAction) { assert(sailsApp._actions[expectedAction], 'Did not load expected action `' + expectedAction + '`'); assert(_.isFunction(sailsApp._actions[expectedAction]), 'Expected action `' + expectedAction + '` loaded, but instead of a function it\'s a ' + typeof(sailsApp._actions[expectedAction])); }); }); it('should bind a route using \'TopLevelLegacyController/fnAction\'', function(done) { sailsApp.request('POST /route1', {}, function (err, resp, data) { assert(!err, err); assert.deepEqual(data, 'legacy fn action!'); done(); }); }); it('should bind a route using \'TopLevelLegacy/fnAction\'', function(done) { sailsApp.request('POST /route1a', {}, function (err, resp, data) { assert(!err, err); assert.deepEqual(data, 'legacy fn action!'); done(); }); }); it('should bind a route using \'TopLevelLegacyController/machineAction\'', function(done) { sailsApp.request('POST /route2', {}, function (err, resp, data) { assert(!err, err); assert.deepEqual(data, 'legacy machine action!'); done(); }); }); it('should bind a route using {controller: \'TopLevelLegacyController\', action: \'fnAction\'}', function(done) { sailsApp.request('POST /route3', {}, function (err, resp, data) { assert(!err, err); assert.deepEqual(data, 'legacy fn action!'); done(); }); }); it('should bind a route using {action: \'toplevellegacy/fnAction\'}', function(done) { sailsApp.request('POST /route4', {}, function (err, resp, data) { assert(!err, err); assert.deepEqual(data, 'legacy fn action!'); done(); }); }); it('should bind a route using {action: \'top-level-standalone-fn\'}', function(done) { sailsApp.request('POST /route5', {}, function (err, resp, data) { assert(!err, err); assert.deepEqual(data, 'top level standalone fn!'); done(); }); }); it('should bind a route using {action: \'somefolder/someotherfolder/nestedlegacy/fnaction\'}', function(done) { sailsApp.request('POST /route6', {}, function (err, resp, data) { assert(!err, err); assert.deepEqual(data, 'nested legacy fn action!'); done(); }); }); it('should bind a route using {action: \'some/folder/some/other/folder/nestedlegacy/fnaction\'}', function(done) { sailsApp.request('POST /route6', {}, function (err, resp, data) { assert(!err, err); assert.deepEqual(data, 'nested legacy fn action!'); done(); }); }); it('should bind a route using {action: \'somefolder/someotherfolder/nested-standalone-machine\'}', function(done) { sailsApp.request('POST /route7', {}, function (err, resp, data) { assert(!err, err); assert.deepEqual(data, 'nested standalone machine!'); done(); }); }); it('should bind a route (under protest) using {controller: \'somefolder/someotherfolder/NestedLegacyController\', action: \'machineaction\'}', function(done) { sailsApp.request('POST /warn1', {}, function (err, resp, data) { assert(!err, err); assert.deepEqual(data, 'nested legacy machine action!'); done(); }); }); it('should bind a route (under protest) using {controller: \'NestedLegacy\', action: \'machineaction\'}', function(done) { sailsApp.request('POST /warn2', {}, function (err, resp, data) { assert(!err, err); assert.deepEqual(data, 'nested legacy machine action!'); done(); }); }); it('should bind a route (under protest) using \'somefolder/someotherfolder/NestedLegacyController.machineAction\'', function(done) { sailsApp.request('POST /warn3', {}, function (err, resp, data) { assert(!err, err); assert.deepEqual(data, 'nested legacy machine action!'); done(); }); }); it('should return a shallow clone of the actions dictionary when `sails.getActions` is called', function() { var actions = sailsApp.getActions(); assert(actions !== sailsApp._actions, 'sails.getActions is supposed to return a shallow clone, but got an exact reference!'); var expectedActions = [ 'toplevellegacy/fnaction', 'toplevellegacy/machineaction', 'toplevellegacy/underscore_action', 'toplevellegacy/action-with-dashes', 'top-level-standalone-fn', 'top-level-standalone-machine', 'somefolder/someotherfolder/nestedlegacy/fnaction', 'somefolder/someotherfolder/nestedlegacy/machineaction', 'some/folder/some/other/folder/nestedlegacy/fnaction', 'some/folder/some/other/folder/nestedlegacy/machineaction', 'somefolder/someotherfolder/nested-standalone-machine' ]; var unexpectedActions = _.difference(_.keys(actions), expectedActions); assert(!unexpectedActions.length, 'Loaded unexpected actions:\n' + util.inspect(unexpectedActions)); _.each(expectedActions, function(expectedAction) { assert(actions[expectedAction], 'Did not load expected action `' + expectedAction + '`'); assert(_.isFunction(actions[expectedAction]), 'Expected action `' + expectedAction + '` loaded, but instead of a function it\'s a ' + typeof(actions[expectedAction])); }); }); }); describe('with conflicting actions in api/controllers', function() { var curDir, tmpDir, sailsApp; var warn; var warnings = []; before(function(done) { // Cache the current working directory. curDir = process.cwd(); // Create a temp directory. tmpDir = tmp.dirSync({gracefulCleanup: true, unsafeCleanup: true}); // Switch to the temp directory. process.chdir(tmpDir.name); // Create a top-level legacy controller file. Filesystem.writeSync({ force: true, destination: 'api/controllers/TopLevelController.js', string: 'module.exports = { fnAction: function (req, res) { res.send(\'fn controller action!\'); } };' }).execSync(); // Create a top-level action file with a req/res function. Filesystem.writeSync({ force: true, destination: 'api/controllers/toplevel/fnaction.js', string: 'module.exports = function (req, res) { res.send(\'standalone fn!\'); };' }).execSync(); return done(); }); after(function() { process.chdir(curDir); }); it('should fail to load sails', function(done) { // Load the Sails app. (new Sails()).load({hooks: {grunt: false, views: false, blueprints: false, policies: false, pubsub: false}, log: {level: 'error'}}, function(err, _sails) { if (!err) { _sails.lower(function() { return done(new Error('Should have thrown an error!')); }); } assert.equal(err.code, 'E_CONFLICT'); assert.equal(err.identity, 'toplevel/fnaction'); return done(); }); }); }); describe('With a controller with `Controller` in the name', function() { var curDir, tmpDir, sailsApp; var warn; var warnings = []; before(function(done) { // Cache the current working directory. curDir = process.cwd(); // Create a temp directory. tmpDir = tmp.dirSync({gracefulCleanup: true, unsafeCleanup: true}); // Switch to the temp directory. process.chdir(tmpDir.name); // Create a top-level legacy controller file with `Controller` in the name. Filesystem.writeSync({ force: true, destination: 'api/controllers/MicroControllerController.js', string: 'module.exports = { \'check\': function(req, res) { return res.send(\'mate\'); } };' }).execSync(); // Write a routes.js file Filesystem.writeSync({ force: true, destination: 'config/routes.js', string: 'module.exports.routes = ' + JSON.stringify({ 'GET /microcontroller/:id/check': 'MicroControllerController.check', 'GET /microcontroller/:id/check2': { controller: 'MicroControllerController', action: 'check' }, 'GET /microcontroller/:id/check3': 'microcontroller/check' }) }).execSync(); // Load the Sails app. (new Sails()).load({hooks: {security: false, grunt: false, views: false, blueprints: false, policies: false, pubsub: false}, log: {level: 'error'}}, function(err, _sails) { sailsApp = _sails; return done(err); }); }); after(function(done) { sailsApp.lower(function() { process.chdir(curDir); return done(); }); }); it('should bind a route using \'MicroControllerController.check\'', function(done) { sailsApp.request('GET /microcontroller/123/check', {}, function (err, resp, data) { assert(!err, err); assert.deepEqual(data, 'mate'); done(); }); }); it('should bind a route using { controller: \'MicroControllerController\', action: \'check\' }', function(done) { sailsApp.request('GET /microcontroller/123/check2', {}, function (err, resp, data) { assert(!err, err); assert.deepEqual(data, 'mate'); done(); }); }); it('should bind a route using \'microcontroller/check\'', function(done) { sailsApp.request('GET /microcontroller/123/check3', {}, function (err, resp, data) { assert(!err, err); assert.deepEqual(data, 'mate'); done(); }); }); }); }); ================================================ FILE: test/unit/req.errors.test.js ================================================ /** * Module dependencies */ var assert = require('assert'); var _ = require('@sailshq/lodash'); var $Sails = require('../helpers/sails'); describe('request that causes an error', function (){ var sails = $Sails.load({ globals: false, log: { level: 'silent' }, loadHooks: [ 'moduleloader', 'userconfig', 'responses' ] }); var saveServerError; // Restore the default error handler after tests that change it afterEach(function () { if (saveServerError) { sails.registry.responses.serverError = saveServerError; saveServerError = undefined; } }); it('should return the expected error when something throws', function (done) { var ERROR = 'oh no I forgot my keys'; sails.get('/errors/1', function (req, res) { throw ERROR; }); sails.request('GET /errors/1', {}, function (err) { assert.deepEqual(500, err.status); assert.deepEqual(ERROR, err.body); done(); }); }); it('should call the `res.serverError()` handler when something throws and the "responses" hook is enabled, and the error should emerge, untampered-with', function (done) { var ERROR = 'oh no I forgot my keys'; var CHECKPOINT = 'made it'; saveServerError = sails.registry.responses.serverError; sails.registry.responses.serverError = function (err) { assert.deepEqual(ERROR, err); this.res.status(500).send(CHECKPOINT); }; sails.get('/errors/2', function (req, res) { throw ERROR; }); sails.request('GET /errors/2', {}, function (err) { assert.deepEqual(CHECKPOINT, err.body); done(); }); }); it('should return the expected error when something throws an Error object', function (done) { var MESSAGE = 'oh no I forgot my keys again'; var ERROR = new Error(MESSAGE); ERROR.toJSON = function() { return { message: MESSAGE, stack: this.stack }; }; sails.get('/errors/3', function (req, res) { throw ERROR; }); sails.request('GET /errors/3', {}, function (err) { assert.deepEqual(err.status, 500); assert.deepEqual(typeof(err.body), 'object'); assert.deepEqual(err.body.message, MESSAGE); done(); }); }); }); ================================================ FILE: test/unit/req.session.test.js ================================================ /** * Module dependencies */ var assert = require('assert'); var util = require('util'); var _ = require('@sailshq/lodash'); var async = require('async'); var Sails = require('../../lib').Sails; describe('req.session', function (){ var app; before(function (done){ app = Sails(); app.load({ globals: false, loadHooks: [ 'moduleloader', 'userconfig', 'session' ], session: { adapter: 'memory', name: 'sails.sid', secret: 'af9442683372850a85a87150c47b4a31' } }, done); }); describe('when responding to a virtual request',function (){ var doesSessionExist; var isSessionAnObject; var doesTestPropertyStillExist; before(function setupTestRoute(){ app.post('/sessionTest', function (req, res){ doesSessionExist = !!req.session; isSessionAnObject = _.isObject(req.session); req.session.something = 'some string'; res.send(); }); app.get('/sessionTest', function (req, res){ doesSessionExist = !!req.session; isSessionAnObject = _.isObject(req.session); doesTestPropertyStillExist = req.session.something === 'some string'; res.send(); }); app.get('/sails.io.js', function (req, res){ doesSessionExist = !!req.session; res.send(); }); }); describe('with routes disabled by the default `isSessionDisabled` function', function() { it('should not exist', function (done) { app.request({ url: '/sails.io.js', method: 'GET', params: {}, headers: {} }, function (err, res, body){ if (err) return done(err); if (res.statusCode !== 200) return done(new Error('Expected 200 status code')); if (doesSessionExist) return done(new Error('req.session should not exist.')); if (res.headers['set-cookie']) return done(new Error('Should not have a `set-cookie` header in the response.')); return done(); }); }); }); describe('with routes NOT disabled by the default `isSessionDisabled` function', function() { it('should exist', function (done) { app.request({ url: '/sessionTest', method: 'POST', params: {}, headers: {} }, function (err, res, body){ if (err) return done(err); if (res.statusCode !== 200) return done(new Error('Expected 200 status code')); if (!doesSessionExist) return done(new Error('req.session should exist.')); if (!isSessionAnObject) return done(new Error('req.session should be an object.')); return done(); }); }); // // To test: // // DEBUG=express-session mocha test/unit/req.session.test.js -b -g 'should persist' // it('should persist data between requests', function (done){ app.request({ url: '/sessionTest', method: 'POST', params: {}, headers: {} }, function (err, clientRes, body){ if (err) return done(err); if (clientRes.statusCode !== 200) return done(new Error('Expected 200 status code')); if (!doesSessionExist) return done(new Error('req.session should exist.')); if (!isSessionAnObject) return done(new Error('req.session should be an object.')); app.request({ url: '/sessionTest', method: 'GET', params: {}, headers: { cookie: clientRes.headers['set-cookie'] } }, function (err, clientRes, body){ if (err) return done(err); if (clientRes.statusCode !== 200) return done(new Error('Expected 200 status code')); if (!doesSessionExist) return done(new Error('req.session should exist.')); if (!isSessionAnObject) return done(new Error('req.session should be an object.')); if (!doesTestPropertyStillExist) return done(new Error('`req.session.something` should still exist for subsequent requests.')); return done(); }); }); }); }); }); after(function (done) { done(); }); }); ================================================ FILE: test/unit/req.test.js ================================================ /** * Module dependencies */ var assert = require('assert'); var should = require('should'); // https://github.com/visionmedia/should.js/ var buildReq = require('root-require')('lib/router/req'); /** * This mocked implementation of `req` forms the basis for * Sails' transport-agnostic support of Connect/Express * middleware. */ describe('Base Request (`req`)', function (){ describe('with empty request', function() { var req; // Mock the request object. before(function (){ req = buildReq(); req.should.be.an.Object; this.req = req; }); it('.body', function () { req.body.should.be.an.Object; req.body.should.be.empty; }); it('.params', function () { req.params.should.be.an.Array; req.params.should.be.empty; }); it('.query', function (){ req.query.should.be.an.Object; req.query.should.be.empty; }); it('.param()', function () { should(req.param('foo')) .not.be.ok; }); }); describe('with url /hello?abc=123&foo=bar', function() { var req; // Mock the request object. before(function (){ req = buildReq({url: '/hello?abc=123&foo=bar'}); req.should.be.an.Object; this.req = req; }); it('.body', function () { req.body.should.be.an.Object; req.body.should.be.empty; }); it('.params', function () { req.params.should.be.an.Array; req.params.should.be.empty; }); it('.query', function (){ req.query.should.be.an.Object; req.query.should.have.property('abc', '123'); req.query.should.have.property('foo', 'bar'); }); it('.param()', function () { should(req.param('abc')).equal('123'); should(req.param('foo')).equal('bar'); }); it('.path', function() { req.path.should.be.an.String; req.path.should.equal('/hello'); }); it('.url', function() { req.url.should.be.an.String; req.url.should.equal('/hello?abc=123&foo=bar'); }); it('.originalUrl', function() { req.originalUrl.should.be.an.String; req.originalUrl.should.equal('/hello?abc=123&foo=bar'); }); }); }); ================================================ FILE: test/unit/res.test.js ================================================ /** * Module dependencies */ var assert = require('assert'); var should = require('should'); // https://github.com/visionmedia/should.js/ var buildRes = require('root-require')('lib/router/res'); /** * This mocked implementation of `res` forms the basis for * Sails' transport-agnostic support of Connect/Express * middleware. */ describe('Base Response (`res`)', function (){ describe('header handling', function() { var ourCustomMime = 'application/vnd.sails.test.v1+json' it("should set a content-type when we send a JS object but don't set content-type", function () { var res = buildRes() var resBodyData = {foo: 'bar'} res.status(200).send(resBodyData) res.headers['content-type'].should.equal('application/json') }); it('should not overwrite our content-type header when we send a JS object', function () { var res = buildRes() var resBodyData = {foo: 'bar'} res.set('content-type', ourCustomMime) // set our own content-type! res.status(200).send(resBodyData) res.headers['content-type'].should.equal(ourCustomMime) }); it("should NOT automatically set a content-type when we send a string but don't set content-type", function () { var res = buildRes() var resBodyDataString = 'some plain text' res.status(200).send(resBodyDataString) should(res.headers['content-type']).be.empty }); it('should not overwrite our content-type header when we send a string', function () { var res = buildRes() var resBodyDataString = '<some xml=""></some>' res.set('content-type', ourCustomMime) // set our own content-type! res.status(200).send(resBodyDataString) res.headers['content-type'].should.equal(ourCustomMime) }); }); }); ================================================ FILE: test/unit/router.bind.test.js ================================================ /** * Module dependencies */ var supertest = require('supertest'); var assert = require('assert'); var $Sails = require('../helpers/sails'); var $Router = require('../helpers/router'); // Middleware fixtures var RESPOND = require('../fixtures/middleware'); describe('Router.bind', function() { var sails = $Sails.load.withAllHooksDisabled(); it('Should not allow routes with :length as a parameter', function() { assert.throws(function() { this.sails.router.bind('get /foo/:length', RESPOND.HELLO); }); assert.throws(function() { this.sails.router.bind('get /foo/:length/foo', RESPOND.HELLO); }); }); $Router.bind('get /foo', RESPOND.HELLO) .expectBoundRoute({ path: '/foo', method: 'get' }) .test(function() { $Router.bind('get /FOO', RESPOND.GOODBYE) .expectBoundRoute({ path: '/FOO', method: 'get' }).test(function() { it('should send expected response (get /foo)', function(done) { this.sails.request({ url: '/foo', method: 'get' }, function(err, res, body) { if (err) { return done(err); } res.statusCode.should.be.equal(200); body.should.be.equal('hello world!'); return done(); }); }); it('router should not be case-sensitive', function(done) { this.sails.request({ url: '/FOO', method: 'get' }, function(err, res, body) { if (err) { return done(err); } res.statusCode.should.be.equal(200); body.should.be.equal('hello world!'); return done(); }); }); }); }); $Router.bind('get /footarg', { target: RESPOND.HELLO }) .expectBoundRoute({ path: '/footarg', method: 'get' }) .test(function() { it('should send expected response (get /footarg)', function(done) { this.sails.request({ url: '/footarg', method: 'get' }, function(err, res, body) { if (err) { return done(err); } res.statusCode.should.be.equal(200); body.should.be.equal('hello world!'); return done(); }); }); }); $Router.bind('get /foofn', { fn: RESPOND.HELLO }) .expectBoundRoute({ path: '/foofn', method: 'get' }) .test(function() { it('should send expected response (get /foofn)', function(done) { this.sails.request({ url: '/foofn', method: 'get' }, function(err, res, body) { if (err) { return done(err); } res.statusCode.should.be.equal(200); body.should.be.equal('hello world!'); return done(); }); }); }); $Router.bind('/bar', RESPOND.HELLO) .expectBoundRoute({ path: '/bar', method: 'get' }) .expectBoundRoute({ path: '/bar', method: 'put' }) .expectBoundRoute({ path: '/bar', method: 'post' }) .expectBoundRoute({ path: '/bar', method: 'patch' }) .expectBoundRoute({ path: '/bar', method: 'delete' }) .test(function() { it('should send expected response (get /bar)', function(done) { this.sails.request({ url: '/bar', method: 'get' }, function(err, res, body) { if (err) { return done(err); } res.statusCode.should.be.equal(200); body.should.be.equal('hello world!'); return done(); }); }); }) .test(function() { it('should send expected response (post /bar)', function(done) { this.sails.request({ url: '/bar', method: 'post' }, function(err, res, body) { if (err) { return done(err); } res.statusCode.should.be.equal(200); body.should.be.equal('hello world!'); return done(); }); }); }) .test(function() { it('should send expected response (put /bar)', function(done) { this.sails.request({ url: '/bar', method: 'put' }, function(err, res, body) { if (err) { return done(err); } res.statusCode.should.be.equal(200); body.should.be.equal('hello world!'); return done(); }); }); }) .test(function() { it('should send expected response (delete /bar)', function(done) { this.sails.request({ url: '/bar', method: 'delete' }, function(err, res, body) { if (err) { return done(err); } res.statusCode.should.be.equal(200); body.should.be.equal('hello world!'); return done(); }); }); }) .test(function() { it('should send expected response (patch /bar)', function(done) { this.sails.request({ url: '/bar', method: 'patch' }, function(err, res, body) { if (err) { return done(err); } res.statusCode.should.be.equal(200); body.should.be.equal('hello world!'); return done(); }); }); }) .test(function() { it('should send expected response (options /bar)', function(done) { var sails = this.sails; this.sails.request({ url: '/bar', method: 'options' }, function(err, res, body) { // console.dir(sails.router._privateRouter, {depth: null}); if (err) { return done(err); } res.statusCode.should.be.equal(200); return done(); }); }); }) .test(function() { it('should send a 404 response (copy /bar)', function(done) { this.sails.request({ url: '/bar', method: 'copy' }, function(err, res, body) { err.status.should.be.equal(404); return done(); }); }); }); $Router.bind('all /boop', RESPOND.HELLO) .expectBoundRoute({ path: '/boop', method: 'get' }) .expectBoundRoute({ path: '/boop', method: 'put' }) .expectBoundRoute({ path: '/boop', method: 'post' }) .expectBoundRoute({ path: '/boop', method: 'patch' }) .expectBoundRoute({ path: '/boop', method: 'delete' }) .test(function() { it('should send expected response (get /boop)', function(done) { this.sails.request({ url: '/boop', method: 'get' }, function(err, res, body) { if (err) { return done(err); } res.statusCode.should.be.equal(200); body.should.be.equal('hello world!'); return done(); }); }); }) .test(function() { it('should send expected response (post /boop)', function(done) { this.sails.request({ url: '/boop', method: 'post' }, function(err, res, body) { if (err) { return done(err); } res.statusCode.should.be.equal(200); body.should.be.equal('hello world!'); return done(); }); }); }) .test(function() { it('should send expected response (put /boop)', function(done) { this.sails.request({ url: '/boop', method: 'put' }, function(err, res, body) { if (err) { return done(err); } res.statusCode.should.be.equal(200); body.should.be.equal('hello world!'); return done(); }); }); }) .test(function() { it('should send expected response (delete /boop)', function(done) { this.sails.request({ url: '/boop', method: 'delete' }, function(err, res, body) { if (err) { return done(err); } res.statusCode.should.be.equal(200); body.should.be.equal('hello world!'); return done(); }); }); }) .test(function() { it('should send expected response (patch /boop)', function(done) { this.sails.request({ url: '/boop', method: 'patch' }, function(err, res, body) { if (err) { return done(err); } res.statusCode.should.be.equal(200); body.should.be.equal('hello world!'); return done(); }); }); }) .test(function() { it('should send expected response (options /boop)', function(done) { this.sails.request({ url: '/boop', method: 'options' }, function(err, res, body) { if (err) { return done(err); } res.statusCode.should.be.equal(200); body.should.be.equal('hello world!'); return done(); }); }); }) .test(function() { it('should send expected response (options /boop)', function(done) { this.sails.request({ url: '/boop', method: 'copy' }, function(err, res, body) { if (err) { return done(err); } res.statusCode.should.be.equal(200); body.should.be.equal('hello world!'); return done(); }); }); }); $Router.bind('options /blap', RESPOND.GOODBYE) .expectBoundRoute({ path: '/boop', method: 'options' }) .test(function() { it('should send expected response (options /blap)', function(done) { this.sails.request({ url: '/blap', method: 'options' }, function(err, res, body) { if (err) { return done(err); } res.statusCode.should.be.equal(200); body.should.be.equal('goodbye world!'); return done(); }); }); }); $Router.bind('post /bar_baz_beezzz', RESPOND.HELLO_500) .expectBoundRoute({ path: '/bar_baz_beezzz', method: 'post' }) .test(function() { it('should send expected response (post /bar_baz_beezzz)', function(done) { this.sails.request({ url: '/bar_baz_beezzz', method: 'post' }, function(err, res, body) { err.status.should.be.equal(500); err.body.should.be.equal('hello world!'); return done(); }); }); }); $Router.bind('patch /user', RESPOND.JSON_HELLO) .expectBoundRoute({ path: '/user', method: 'patch' }) .test(function() { it('should send expected response (patch /user)', function(done) { this.sails.request({ url: '/user', method: 'patch' }, function(err, res, body) { if (err) { return done(err); } res.statusCode.should.be.equal(200); body.should.be.instanceOf(Object); body.hello.should.be.equal('world'); return done(); }); }); }); $Router.bind(' /whitespace1 ', RESPOND.JSON_HELLO) .expectBoundRoute({ path: '/whitespace1', method: 'get' }) .test(function() { it('should send expected response (get /whitespace1)', function(done) { this.sails.request({ url: '/whitespace1', method: 'get' }, function(err, res, body) { if (err) { return done(err); } res.statusCode.should.be.equal(200); body.should.be.instanceOf(Object); body.hello.should.be.equal('world'); return done(); }); }); }); $Router.bind(' GET /whitespace2 ', RESPOND.JSON_HELLO) .expectBoundRoute({ path: '/whitespace', method: 'get' }) .test(function() { it('should send expected response (get /whitespace2)', function(done) { this.sails.request({ url: '/whitespace2', method: 'get' }, function(err, res, body) { if (err) { return done(err); } res.statusCode.should.be.equal(200); body.should.be.instanceOf(Object); body.hello.should.be.equal('world'); return done(); }); }); it('should send 404 response (put /whitespace2)', function(done) { this.sails.request({ url: '/whitespace2', method: 'put' }, function(err, res, body) { if (err && err.status === 404) { return done(); } else if (err) { return done(err); } else { return done(new Error('Should have returned 404!')); } }); }); }); $Router .test(function() { it('should respond with 404 handler', function(done) { this.sails.request({ url: '/something_undefined', method: 'get' }, function(err, res, body) { err.status.should.be.equal(404); return done(); }); }); }); $Router.bind('post /something_that_throws', RESPOND.SOMETHING_THAT_THROWS) .test(function() { it('should respond with 500 handler if something throws', function(done) { this.sails.request({ url: '/something_that_throws', method: 'post' }, function(err, res, body) { err.status.should.be.equal(500); return done(); }); }); }); }); ================================================ FILE: test/unit/router.ordering.test.js ================================================ /** * Module dependencies */ var util = require('util'); var assert = require('assert'); var tmp = require('tmp'); var _ = require('@sailshq/lodash'); var Sails = require('../../lib').constructor; tmp.setGracefulCleanup(); /** * Errors */ var Err = { badResponse: function(response) { return 'Wrong server response! Response :::\n' + util.inspect(response.body); } }; /** * Tests */ describe('route ordering :: ', function() { var curDir, tmpDir, sailsApp; var testRoutes = { '/*': function(req, res) {res.json({route: '/*', params: req.params});}, 'GET /*': function(req, res) {res.json({route: 'GET /*', params: req.params});}, '/*/baz': function(req, res) {res.json({route: '/*/baz', params: req.params});}, '/*/baz/*': function(req, res) {res.json({route: '/*/baz/*', params: req.params});}, '/*/bar/baz': function(req, res) {res.json({route: '/*/bar/baz', params: req.params});}, '/:foo/*': function(req, res) {res.json({route: '/:foo/*', params: req.params});}, '/:foo/:bar/*': function(req, res) {res.json({route: '/:foo/:bar/*', params: req.params});}, '/:foo/:bar/:baz': function(req, res) {res.json({route: '/:foo/:bar/:baz', params: req.params});}, '/:foo/:bar/baz': function(req, res) {res.json({route: '/:foo/:bar/baz', params: req.params});}, '/:foo/:bar': function(req, res) {res.json({route: '/:foo/:bar', params: req.params});}, '/:foo/bar/:baz': function(req, res) {res.json({route: '/:foo/bar/:baz', params: req.params});}, '/:foo/bar/baz': function(req, res) {res.json({route: '/:foo/bar/baz', params: req.params});}, '/:foo/bar': function(req, res) {res.json({route: '/:foo/bar', params: req.params});}, '/:foo': function(req, res) {res.json({route: '/:foo', params: req.params});}, '/foo/*': function(req, res) {res.json({route: '/foo/*', params: req.params});}, '/foo/*/baz': function(req, res) {res.json({route: '/foo/*/baz', params: req.params});}, '/foo/:bar/:baz': function(req, res) {res.json({route: '/foo/:bar/:baz', params: req.params});}, '/foo/:bar/baz': function(req, res) {res.json({route: '/foo/:bar/baz', params: req.params});}, '/foo/:bar': function(req, res) {res.json({route: '/foo/:bar', params: req.params});}, '/foo/bar/*': function(req, res) {res.json({route: '/foo/bar/*', params: req.params});}, 'GET /foo/bar/*': function(req, res) {res.json({route: 'GET /foo/bar/*', params: req.params});}, '/foo/bar/:baz': function(req, res) {res.json({route: '/foo/bar/:baz', params: req.params});}, 'GET /foo/bar/:baz': function(req, res) {res.json({route: 'GET /foo/bar/:baz', params: req.params});}, '/foo/bar/baz': function(req, res) {res.json({route: '/foo/bar/baz', params: req.params});}, '/foo/bar': function(req, res) {res.json({route: '/foo/bar', params: req.params});}, '/foo': function(req, res) {res.json({route: '/foo', params: req.params});}, 'GET /foo': function(req, res) {res.json({route: 'GET /foo', params: req.params});} }; before(function(done) { // Cache the current working directory. curDir = process.cwd(); // Create a temp directory. tmpDir = tmp.dirSync({gracefulCleanup: true, unsafeCleanup: true}); // Switch to the temp directory. process.chdir(tmpDir.name); var sails = new Sails(); (new Sails()).load({ loadHooks: [], routes: testRoutes }, function(err, _sails) { if (err) { return done(err); } sailsApp = _sails; return done(); }); }); after(function(done) { sailsApp.lower(function() { process.chdir(curDir); return done(); }); }); it('should bind the routes in the correct order', function(){ var sortedRoutes = sailsApp.router.getSortedRouteAddresses(); assert(_.isEqual(sortedRoutes, ['GET /foo', '/foo', '/foo/bar', '/foo/bar/baz', 'GET /foo/bar/:baz', '/foo/bar/:baz', 'GET /foo/bar/*', '/foo/bar/*', '/foo/:bar', '/foo/:bar/baz', '/foo/:bar/:baz', '/foo/*/baz', '/foo/*', '/:foo/bar', '/:foo/bar/baz', '/:foo/bar/:baz', '/:foo/:bar/baz', '/*/bar/baz', '/*/baz/*', '/*/baz', '/:foo', '/:foo/:bar', '/:foo/:bar/:baz', '/:foo/:bar/*', '/:foo/*', 'GET /*', '/*']), sortedRoutes); }); var testRequests = { 'GET /foo': 'GET /foo', '/foo': 'POST /foo', '/foo/bar': 'GET /foo/bar', '/foo/bar/baz': 'GET /foo/bar/baz', 'GET /foo/bar/:baz': 'GET /foo/bar/xxx', '/foo/bar/:baz': 'POST /foo/bar/xxx', 'GET /foo/bar/*': 'GET /foo/bar/xxx/yyy', '/foo/bar/*': 'POST /foo/bar/xxx/yyy', '/foo/:bar': 'GET /foo/xxx', '/foo/:bar/baz': 'GET /foo/xxx/baz', '/foo/:bar/:baz': 'GET /foo/xxx/yyy', '/foo/*/baz': 'GET /foo/xxx/yyy/zzz/baz', '/foo/*': 'GET /foo/xxx/yyy/zzz', '/:foo/bar': 'GET /xxx/bar', '/:foo/bar/baz': 'GET /xxx/bar/baz', '/:foo/bar/:baz': 'GET /xxx/bar/yyy', '/:foo/:bar/baz': 'GET /xxx/yyy/baz', '/*/bar/baz': '/xxx/yyy/bar/baz', '/*/baz/*': '/xxx/baz/yyy', '/*/baz': '/xxx/yyy/zzz/baz', '/:foo': 'GET /xxx', '/:foo/:bar': 'GET /xxx/yyy', '/:foo/:bar/:baz': 'GET /xxx/yyy/zzz', '/:foo/:bar/*': 'GET /xxx/yyy/zzz/owl' }; _.each(testRequests, function(request, expectedRoute) { it('a request to `' + request + '` should be handled by the `' + expectedRoute + '` route', function(done) { sailsApp.request(request, {}, function (err, resp, data) { assert(!err, err); assert.equal(data.route, expectedRoute, 'The `' + data.route + '` route handled it instead!'); done(); }); }); }); }); ================================================ FILE: test/unit/router.test.js ================================================ /** * Module dependencies */ var should = require('should'); var $Sails = require('../helpers/sails'); describe('`sails.router`', function() { var sails = $Sails.load.withAllHooksDisabled(); it('should be exposed on the `sails` global', function () { sails .router ._privateRouter .stack .should.be.ok; }); }); ================================================ FILE: test/unit/router.unbind.test.js ================================================ /** * Module dependencies */ var $Sails = require('root-require')('test/helpers/sails'); var $Router = require('root-require')('test/helpers/router'); describe('sails.router.unbind', function (){ var sails = $Sails.load.withAllHooksDisabled(); $Router.unbind('get /foo') .shouldDelete({ path: '/foo', method: 'get' }); $Router.unbind('post /bar_baz_beezzz') .shouldDelete({ path: '/bar_baz_beezzz', method: 'post' }); $Router.unbind('patch /user') .shouldDelete({ path: '/user', method: 'patch' }); }); ================================================ FILE: test/unit/virtual-request-interpreter.test.js ================================================ /** * Module dependencies */ var assert = require('assert'); var $Sails = require('../helpers/sails'); describe('virtual request interpreter', function (){ var app = $Sails.load({ globals: false, log: { level: 'silent' }, loadHooks: [ 'moduleloader', 'userconfig', 'responses' ] }); // ██████╗ ███████╗███████╗ ██████╗ ███████╗██████╗ ██╗██████╗ ███████╗ ██████╗████████╗ ██╗██╗ // ██╔══██╗██╔════╝██╔════╝ ██╔══██╗██╔════╝██╔══██╗██║██╔══██╗██╔════╝██╔════╝╚══██╔══╝██╔╝╚██╗ // ██████╔╝█████╗ ███████╗ ██████╔╝█████╗ ██║ ██║██║██████╔╝█████╗ ██║ ██║ ██║ ██║ // ██╔══██╗██╔══╝ ╚════██║ ██╔══██╗██╔══╝ ██║ ██║██║██╔══██╗██╔══╝ ██║ ██║ ██║ ██║ // ██║ ██║███████╗███████║██╗██║ ██║███████╗██████╔╝██║██║ ██║███████╗╚██████╗ ██║ ╚██╗██╔╝ // ╚═╝ ╚═╝╚══════╝╚══════╝╚═╝╚═╝ ╚═╝╚══════╝╚═════╝ ╚═╝╚═╝ ╚═╝╚══════╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ // describe('res.redirect()', function (){ it('should support creamy vanilla usage', function (done) { app.get('/res_redirect/1', function (req, res) { return res.redirect('/foo/bar'); }); app.request('GET /res_redirect/1', {}, function (err, resp, data) { assert(!err, err); assert.deepEqual(resp.headers.Location, '/foo/bar'); assert.deepEqual(302, resp.statusCode); done(); }); }); it('should honor .status()', function (done) { app.get('/res_redirect/2', function (req, res) { return res.status(301).redirect('/foo/baz'); }); app.request('GET /res_redirect/2', {}, function (err, resp, data) { assert(!err, err); assert.deepEqual(resp.headers.Location, '/foo/baz'); assert.deepEqual(301, resp.statusCode); done(); }); }); it('should NO LONGER ALLOW a status code to be passed as the first argument', function (done) { app.get('/res_redirect/3', function (req, res) { try { return res.redirect(301, '/foo/baz'); } catch(e) { // Go ahead and throw this again on purpose (so that we test the rest // of the unhandled error flow for the VR interpreter) throw e; } }); app.request('GET /res_redirect/3', {}, function (err, resp, data) { try { assert(err); assert(err.message.match(/Error: The 2\-ary usage of \`res\.redirect\(\)\` is no longer supported/), 'Unexpected error message: '+err.message); } catch (e) { return done(e); } return done(); }); }); it('should NO LONGER ALLOW the status code being passed in as the second argument EITHER', function (done) { app.get('/res_redirect/4', function (req, res) { try { return res.redirect('/foo/baz', 301); } catch(e) { // Go ahead and throw this again on purpose (so that we test the rest // of the unhandled error flow for the VR interpreter) throw e; } }); app.request('GET /res_redirect/4', {}, function (err, resp, data) { try { assert(err); assert(err.message.match(/Error: The 2\-ary usage of \`res\.redirect\(\)\` is no longer supported/), 'Unexpected error message: '+err.message); } catch (e) { return done(e); } return done(); }); }); });//</describe: res.redirect()> // ██████╗ ███████╗███████╗ ███████╗███████╗███╗ ██╗██████╗ ██╗██╗ ██╗ // ██╔══██╗██╔════╝██╔════╝ ██╔════╝██╔════╝████╗ ██║██╔══██╗██╔╝╚██╗ ██║ // ██████╔╝█████╗ ███████╗ ███████╗█████╗ ██╔██╗ ██║██║ ██║██║ ██║ ████████╗ // ██╔══██╗██╔══╝ ╚════██║ ╚════██║██╔══╝ ██║╚██╗██║██║ ██║██║ ██║ ██╔═██╔═╝ // ██║ ██║███████╗███████║██╗███████║███████╗██║ ╚████║██████╔╝╚██╗██╔╝ ██████║ // ╚═╝ ╚═╝╚══════╝╚══════╝╚═╝╚══════╝╚══════╝╚═╝ ╚═══╝╚═════╝ ╚═╝╚═╝ ╚═════╝ // // ██████╗ ███████╗███████╗ ██╗███████╗ ██████╗ ███╗ ██╗ ██╗██╗ // ██╔══██╗██╔════╝██╔════╝ ██║██╔════╝██╔═══██╗████╗ ██║██╔╝╚██╗ // ██████╔╝█████╗ ███████╗ ██║███████╗██║ ██║██╔██╗ ██║██║ ██║ // ██╔══██╗██╔══╝ ╚════██║ ██ ██║╚════██║██║ ██║██║╚██╗██║██║ ██║ // ██║ ██║███████╗███████║██╗╚█████╔╝███████║╚██████╔╝██║ ╚████║╚██╗██╔╝ // ╚═╝ ╚═╝╚══════╝╚══════╝╚═╝ ╚════╝ ╚══════╝ ╚═════╝ ╚═╝ ╚═══╝ ╚═╝╚═╝ // // For reference, here is the actual behavior when testing w/ express over http: // ``` // return res.send(); // 122b // return res.send(''); // 200b (empty body, content length ==> 0) // return res.send(0); // XXXXXXXX WARNING (because deprecated usage-- it sees it as status code only) // return res.send(null); // 163b (empty body, content length ==> 0) // return res.send(false); // 215b (sends down the string `false`, content len ==> 5) // return res.send(true); // same as false basically // return res.send(45); // XXXXXXXX WARNING (because deprecated usage-- it sees it as status code only) // return res.send(""); // exactly like `''` above // // return res.json(); // 154b // return res.json(''); // 212b (body is `""`- content length ==> 2) // return res.json(0); // (content length ==> 1) // return res.json(null); // null - 214b (empty body, content length ==> 4) // return res.json('null'); // "null" -> 216b (empty body, content length ==> 6) // return res.json(false); // 215b (sends down the string `false`, content len ==> 5) // return res.json(true); // same as false basically // return res.json(45); // 212b (content length ==> 2) // return res.json('45'); // "45" content len ==> 4 // return res.json(""); // exactly like `''` above // ``` describe('sending back a string', function (){ describe('using res.send()', function (){ it('should be the body', function (done) { app.get('/res_sending_back_a_string/1', function (req, res) { return res.send('foo'); }); app.request('GET /res_sending_back_a_string/1', {}, function (err, resp, data) { try { assert(!err, err); assert.deepEqual(200, resp.statusCode); assert.strictEqual(resp.body, 'foo'); assert.strictEqual(data, 'foo'); } catch (e) { return done(e); } done(); }); }); it('should be the body, even if it is empty string', function (done) { app.get('/res_sending_back_a_string/1/B', function (req, res) { return res.send(''); }); app.request('GET /res_sending_back_a_string/1/B', {}, function (err, resp, data) { try { assert(!err, err); assert.deepEqual(200, resp.statusCode); assert.strictEqual(resp.body, undefined); assert.strictEqual(data, undefined); } catch (e) { return done(e); } done(); }); }); });//</describe using res.send()> describe('using res.json()', function (){ it('should be the body', function (done) { app.get('/res_sending_back_a_string/2', function (req, res) { return res.json('foo'); }); app.request('GET /res_sending_back_a_string/2', {}, function (err, resp, data) { try { assert(!err, err); assert.deepEqual(200, resp.statusCode); assert.strictEqual(resp.body, 'foo'); assert.strictEqual(data, 'foo'); } catch (e) { return done(e); } done(); }); }); it('should be the body, even if empty string', function (done) { app.get('/res_sending_back_a_string/2/B', function (req, res) { return res.json(''); }); app.request('GET /res_sending_back_a_string/2/B', {}, function (err, resp, data) { try { assert(!err, err); assert.deepEqual(200, resp.statusCode); assert.strictEqual(resp.body, ''); assert.strictEqual(data, ''); } catch (e) { return done(e); } done(); }); }); it('should stay wrapped in quotes if it was wrapped in quotes', function (done) { app.get('/res_sending_back_a_string/3', function (req, res) { return res.json('"foo"'); }); app.request('GET /res_sending_back_a_string/3', {}, function (err, resp, data) { try { assert(!err, err); assert.deepEqual(200, resp.statusCode); assert.strictEqual(resp.body, '"foo"'); assert.strictEqual(data, '"foo"'); } catch (e) { return done(e); } done(); }); }); it('should stay wrapped in quotes if it was wrapped in quotes, even if it empty string wrapped in quotes', function (done) { app.get('/res_sending_back_a_string/3/b', function (req, res) { return res.json('""'); }); app.request('GET /res_sending_back_a_string/3/b', {}, function (err, resp, data) { try { assert(!err, err); assert.deepEqual(200, resp.statusCode); assert.strictEqual(resp.body, '""'); assert.strictEqual(data, '""'); } catch (e) { return done(e); } done(); }); }); });//</describe using res.json()> });//</describe: sending back a string > describe('sending back a number', function (){ describe('using res.send()', function (){ it('should be the body, and NOT interpreted as a status code', function (done) { app.get('/res_sending_back_a_number/1', function (req, res) { return res.send(45); }); app.request('GET /res_sending_back_a_number/1', {}, function (err, resp, data) { try { assert(!err, err); assert.deepEqual(200, resp.statusCode); assert.strictEqual(resp.body, 45); assert.strictEqual(data, 45); } catch (e) { return done(e); } done(); }); }); it('should be the body, and NOT interpreted as a status code, even when zero is used', function (done) { app.get('/res_sending_back_a_number/1/B', function (req, res) { return res.send(0); }); app.request('GET /res_sending_back_a_number/1/B', {}, function (err, resp, data) { try { assert(!err, err); assert.deepEqual(200, resp.statusCode); assert.strictEqual(resp.body, 0); assert.strictEqual(data, 0); } catch (e) { return done(e); } done(); }); }); });//</describe using res.send()> describe('using res.json()', function (){ it('should be the body, and NOT interpreted as a status code', function (done) { app.get('/res_sending_back_a_number/2', function (req, res) { return res.json(45); }); app.request('GET /res_sending_back_a_number/2', {}, function (err, resp, data) { try { assert(!err, err); assert.deepEqual(200, resp.statusCode); assert.strictEqual(resp.body, 45); assert.strictEqual(data, 45); } catch (e) { return done(e); } done(); }); }); it('should be the body, and NOT interpreted as a status code, even when zero is used', function (done) { app.get('/res_sending_back_a_number/2/B', function (req, res) { return res.json(0); }); app.request('GET /res_sending_back_a_number/2/B', {}, function (err, resp, data) { try { assert(!err, err); assert.deepEqual(200, resp.statusCode); assert.strictEqual(resp.body, 0); assert.strictEqual(data, 0); } catch (e) { return done(e); } done(); }); }); it('should stay a string, if it was wrapped in quotes', function (done) { app.get('/res_sending_back_a_number/3', function (req, res) { return res.json('45'); }); app.request('GET /res_sending_back_a_number/3', {}, function (err, resp, data) { try { assert(!err, err); assert.deepEqual(200, resp.statusCode); assert.strictEqual(resp.body, '45'); assert.strictEqual(data, '45'); } catch (e) { return done(e); } done(); }); }); it('should stay a string, if it was wrapped in quotes, even if it is zero', function (done) { app.get('/res_sending_back_a_number/3/B', function (req, res) { return res.json('0'); }); app.request('GET /res_sending_back_a_number/3/B', {}, function (err, resp, data) { try { assert(!err, err); assert.deepEqual(200, resp.statusCode); assert.strictEqual(resp.body, '0'); assert.strictEqual(data, '0'); } catch (e) { return done(e); } done(); }); }); });//</describe using res.json()> });//</describe: sending back a number > describe('sending back `null`', function (){ describe('using res.send()', function (){ it('should be the body', function (done) { app.get('/res_sending_back_the_null_literal/1', function (req, res) { return res.send(null); }); app.request('GET /res_sending_back_the_null_literal/1', {}, function (err, resp, data) { try { assert(!err, err); assert.deepEqual(200, resp.statusCode); assert.strictEqual(resp.body, null); assert.strictEqual(data, null); } catch (e) { return done(e); } done(); }); }); });//</describe using res.send()> describe('using res.json()', function (){ it('should be the body', function (done) { app.get('/res_sending_back_the_null_literal/2', function (req, res) { return res.json(null); }); app.request('GET /res_sending_back_the_null_literal/2', {}, function (err, resp, data) { try { assert(!err, err); assert.deepEqual(200, resp.statusCode); assert.strictEqual(resp.body, null); assert.strictEqual(data, null); } catch (e) { return done(e); } done(); }); }); it('should stay a string, if it was wrapped in quotes', function (done) { app.get('/res_sending_back_the_null_literal/3', function (req, res) { return res.json('null'); }); app.request('GET /res_sending_back_the_null_literal/3', {}, function (err, resp, data) { try { assert(!err, err); assert.deepEqual(200, resp.statusCode); assert.strictEqual(resp.body, 'null'); assert.strictEqual(data, 'null'); } catch (e) { return done(e); } done(); }); }); });//</describe using res.json()> });//</describe: sending back `null` > describe('sending back a boolean', function (){ describe('using res.send()', function (){ describe('`true`', function (){ it('should be the body', function (done) { app.get('/res_sending_back_a_boolean/1', function (req, res) { return res.send(true); }); app.request('GET /res_sending_back_a_boolean/1', {}, function (err, resp, data) { try { assert(!err, err); assert.deepEqual(200, resp.statusCode); assert.strictEqual(resp.body, true); assert.strictEqual(data, true); } catch (e) { return done(e); } done(); }); }); });//</describe: `true`> describe('`false`', function (){ it('should be the body', function (done) { app.get('/res_sending_back_a_boolean/2', function (req, res) { return res.send(false); }); app.request('GET /res_sending_back_a_boolean/2', {}, function (err, resp, data) { try { assert(!err, err); assert.deepEqual(200, resp.statusCode); assert.strictEqual(resp.body, false); assert.strictEqual(data, false); } catch (e) { return done(e); } done(); }); }); });//</describe: `false`> });//</describe using res.send()> describe('using res.json()', function (){ describe('`true`', function (){ it('should be the body', function (done) { app.get('/res_sending_back_a_boolean/3', function (req, res) { return res.json(true); }); app.request('GET /res_sending_back_a_boolean/3', {}, function (err, resp, data) { try { assert(!err, err); assert.deepEqual(200, resp.statusCode); assert.strictEqual(resp.body, true); assert.strictEqual(data, true); } catch (e) { return done(e); } done(); }); }); it('should stay a string, if it was wrapped in quotes', function (done) { app.get('/res_sending_back_a_boolean/4', function (req, res) { return res.json('true'); }); app.request('GET /res_sending_back_a_boolean/4', {}, function (err, resp, data) { try { assert(!err, err); assert.deepEqual(200, resp.statusCode); assert.strictEqual(resp.body, 'true'); assert.strictEqual(data, 'true'); } catch (e) { return done(e); } done(); }); }); });//</describe: `true`> describe('`false`', function (){ it('should be the body', function (done) { app.get('/res_sending_back_a_boolean/5', function (req, res) { return res.json(false); }); app.request('GET /res_sending_back_a_boolean/5', {}, function (err, resp, data) { try { assert(!err, err); assert.deepEqual(200, resp.statusCode); assert.strictEqual(resp.body, false); assert.strictEqual(data, false); } catch (e) { return done(e); } done(); }); }); it('should stay a string, if it was wrapped in quotes', function (done) { app.get('/res_sending_back_a_boolean/6', function (req, res) { return res.json('false'); }); app.request('GET /res_sending_back_a_boolean/6', {}, function (err, resp, data) { try { assert(!err, err); assert.deepEqual(200, resp.statusCode); assert.strictEqual(resp.body, 'false'); assert.strictEqual(data, 'false'); } catch (e) { return done(e); } done(); }); }); });//</describe: `false`> });//</describe using res.json()> });//</describe: sending back a boolean > });//</describe: VR interpreter>