├── .npmrc ├── .eslintignore ├── templates ├── reset-form.ejs └── verify.ejs ├── test ├── mocha.opts ├── fixtures │ ├── simple-app │ │ ├── boot │ │ │ ├── bad.txt │ │ │ └── foo.js │ │ ├── common │ │ │ └── models │ │ │ │ ├── bar.json │ │ │ │ ├── foo.json │ │ │ │ └── bar.js │ │ └── server │ │ │ ├── datasources.json │ │ │ ├── config.json │ │ │ └── model-config.json │ ├── simple-integration-app │ │ ├── common │ │ │ └── models │ │ │ │ ├── access-token.json │ │ │ │ ├── profile.json │ │ │ │ ├── user.json │ │ │ │ ├── customer-forceid.json │ │ │ │ ├── email.json │ │ │ │ ├── widget.json │ │ │ │ ├── physician.json │ │ │ │ ├── store.json │ │ │ │ ├── customer.json │ │ │ │ ├── patient.json │ │ │ │ ├── store-replacing.json │ │ │ │ ├── store-updating.json │ │ │ │ └── appointment.json │ │ └── server │ │ │ ├── datasources.json │ │ │ ├── config.json │ │ │ ├── server.js │ │ │ └── model-config.json │ ├── shared-methods │ │ ├── both-configs-set │ │ │ ├── server │ │ │ │ ├── datasources.json │ │ │ │ ├── server.js │ │ │ │ ├── config.json │ │ │ │ └── model-config.json │ │ │ └── common │ │ │ │ └── models │ │ │ │ ├── todo.js │ │ │ │ └── todo.json │ │ ├── config-default-true │ │ │ ├── server │ │ │ │ ├── datasources.json │ │ │ │ ├── server.js │ │ │ │ ├── config.json │ │ │ │ └── model-config.json │ │ │ └── common │ │ │ │ └── models │ │ │ │ ├── todo.js │ │ │ │ └── todo.json │ │ ├── config-defined-true │ │ │ ├── server │ │ │ │ ├── datasources.json │ │ │ │ ├── server.js │ │ │ │ ├── config.json │ │ │ │ └── model-config.json │ │ │ └── common │ │ │ │ └── models │ │ │ │ ├── todo.js │ │ │ │ └── todo.json │ │ ├── config-default-false │ │ │ ├── server │ │ │ │ ├── datasources.json │ │ │ │ ├── server.js │ │ │ │ ├── config.json │ │ │ │ └── model-config.json │ │ │ └── common │ │ │ │ └── models │ │ │ │ ├── todo.js │ │ │ │ └── todo.json │ │ ├── config-defined-false │ │ │ ├── server │ │ │ │ ├── datasources.json │ │ │ │ ├── server.js │ │ │ │ ├── config.json │ │ │ │ └── model-config.json │ │ │ └── common │ │ │ │ └── models │ │ │ │ ├── todo.js │ │ │ │ └── todo.json │ │ ├── model-config-default-false │ │ │ ├── server │ │ │ │ ├── datasources.json │ │ │ │ ├── server.js │ │ │ │ ├── config.json │ │ │ │ └── model-config.json │ │ │ └── common │ │ │ │ └── models │ │ │ │ ├── todo.js │ │ │ │ └── todo.json │ │ ├── model-config-default-true │ │ │ ├── server │ │ │ │ ├── datasources.json │ │ │ │ ├── server.js │ │ │ │ ├── config.json │ │ │ │ └── model-config.json │ │ │ └── common │ │ │ │ └── models │ │ │ │ ├── todo.js │ │ │ │ └── todo.json │ │ ├── model-config-defined-false │ │ │ ├── server │ │ │ │ ├── datasources.json │ │ │ │ ├── server.js │ │ │ │ ├── config.json │ │ │ │ └── model-config.json │ │ │ └── common │ │ │ │ └── models │ │ │ │ ├── todo.js │ │ │ │ └── todo.json │ │ └── model-config-defined-true │ │ │ ├── server │ │ │ ├── datasources.json │ │ │ ├── server.js │ │ │ ├── config.json │ │ │ └── model-config.json │ │ │ └── common │ │ │ └── models │ │ │ ├── todo.js │ │ │ └── todo.json │ ├── access-control │ │ ├── server │ │ │ ├── datasources.json │ │ │ ├── config.json │ │ │ ├── server.js │ │ │ └── model-config.json │ │ └── common │ │ │ └── models │ │ │ ├── email.json │ │ │ ├── alert.json │ │ │ ├── transaction.json │ │ │ ├── user.json │ │ │ ├── access-token.json │ │ │ ├── bank.json │ │ │ ├── account.json │ │ │ └── accountWithReplaceOnPUTfalse.json │ ├── user-integration-app │ │ ├── server │ │ │ ├── datasources.json │ │ │ ├── config.json │ │ │ ├── server.js │ │ │ └── model-config.json │ │ └── common │ │ │ └── models │ │ │ ├── blog.json │ │ │ ├── my-user.json │ │ │ └── post.json │ └── e2e │ │ └── server │ │ ├── models.js │ │ └── server.js ├── helpers │ ├── expect.js │ ├── use-english.js │ ├── wait-for-event.js │ └── error-loggers.js ├── util │ ├── it.js │ └── describe.js ├── error-handler.test.js ├── e2e │ ├── remote-connector.e2e.js │ └── replication.e2e.js ├── memory.test.js ├── remoting-coercion.test.js ├── hidden-properties.test.js ├── geo-point.test.js ├── registries.test.js ├── email.test.js ├── integration.test.js ├── remote-connector.test.js ├── checkpoint.test.js ├── role-mapping.test.js ├── utils.test.js ├── authorization-scopes.test.js ├── karma.conf.js ├── change-stream.test.js └── data-source.test.js ├── favicon.ico ├── common └── models │ ├── key-value-model.json │ ├── checkpoint.json │ ├── email.json │ ├── scope.json │ ├── acl.json │ ├── change.json │ ├── role.json │ ├── role-mapping.json │ ├── access-token.json │ ├── scope.js │ ├── email.js │ ├── checkpoint.js │ ├── user.json │ ├── application.json │ ├── role-mapping.js │ └── README.md ├── docs ├── assets │ ├── lb-modules.png │ ├── explorer-api.png │ ├── loopback_ov.png │ ├── explorer-endpoint.png │ ├── explorer-listing.png │ ├── explorer-req-res.png │ ├── loopback-concepts.png │ └── loopback-architecture.png └── api-explorer-details.md ├── .nycrc ├── .gitignore ├── .eslintrc ├── .travis.yml ├── CODEOWNERS ├── lib ├── globalize.js ├── runtime.js ├── browser-express.js ├── connectors │ ├── memory.js │ ├── base-connector.js │ └── mail.js ├── current-context.js ├── configure-shared-methods.js ├── builtin-models.js └── utils.js ├── server └── middleware │ ├── error-handler.js │ ├── context.js │ ├── favicon.js │ ├── static.js │ ├── url-not-found.js │ ├── status.js │ └── rest.js ├── .github ├── ISSUE_TEMPLATE │ ├── Question.md │ ├── config.yml │ ├── Feature_request.md │ └── Bug_report.md ├── PULL_REQUEST_TEMPLATE.md └── stale.yml ├── example ├── colors │ └── app.js ├── simple-data-source │ └── app.js ├── client-server │ ├── client.js │ ├── server.js │ └── models.js ├── mobile-models │ └── app.js └── replication │ └── app.js ├── index.js ├── docs.json ├── LICENSE ├── package.json └── intl ├── zh-Hans └── messages.json └── zh-Hant └── messages.json /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist 2 | coverage 3 | -------------------------------------------------------------------------------- /templates/reset-form.ejs: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --require ./test/helpers/use-english 2 | -------------------------------------------------------------------------------- /test/fixtures/simple-app/boot/bad.txt: -------------------------------------------------------------------------------- 1 | this is not a js file! 2 | -------------------------------------------------------------------------------- /favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strongloop/loopback/HEAD/favicon.ico -------------------------------------------------------------------------------- /test/fixtures/simple-app/common/models/bar.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bar", 3 | "properties": {} 4 | } -------------------------------------------------------------------------------- /test/fixtures/simple-app/common/models/foo.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "foo", 3 | "properties": {} 4 | } -------------------------------------------------------------------------------- /common/models/key-value-model.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "KeyValueModel", 3 | "base": "Model" 4 | } 5 | -------------------------------------------------------------------------------- /docs/assets/lb-modules.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strongloop/loopback/HEAD/docs/assets/lb-modules.png -------------------------------------------------------------------------------- /docs/assets/explorer-api.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strongloop/loopback/HEAD/docs/assets/explorer-api.png -------------------------------------------------------------------------------- /docs/assets/loopback_ov.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strongloop/loopback/HEAD/docs/assets/loopback_ov.png -------------------------------------------------------------------------------- /test/fixtures/simple-app/server/datasources.json: -------------------------------------------------------------------------------- 1 | { 2 | "db": { 3 | "connector": "memory" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /.nycrc: -------------------------------------------------------------------------------- 1 | { 2 | "exclude": [ 3 | "Gruntfile.js", 4 | "test/**/*.js" 5 | ], 6 | "cache": true 7 | } 8 | -------------------------------------------------------------------------------- /docs/assets/explorer-endpoint.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strongloop/loopback/HEAD/docs/assets/explorer-endpoint.png -------------------------------------------------------------------------------- /docs/assets/explorer-listing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strongloop/loopback/HEAD/docs/assets/explorer-listing.png -------------------------------------------------------------------------------- /docs/assets/explorer-req-res.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strongloop/loopback/HEAD/docs/assets/explorer-req-res.png -------------------------------------------------------------------------------- /docs/assets/loopback-concepts.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strongloop/loopback/HEAD/docs/assets/loopback-concepts.png -------------------------------------------------------------------------------- /docs/assets/loopback-architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strongloop/loopback/HEAD/docs/assets/loopback-architecture.png -------------------------------------------------------------------------------- /test/fixtures/simple-integration-app/common/models/access-token.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "accessToken", 3 | "base": "AccessToken" 4 | } -------------------------------------------------------------------------------- /test/fixtures/shared-methods/both-configs-set/server/datasources.json: -------------------------------------------------------------------------------- 1 | { 2 | "db": { 3 | "name": "db", 4 | "connector": "memory" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /test/fixtures/shared-methods/config-default-true/server/datasources.json: -------------------------------------------------------------------------------- 1 | { 2 | "db": { 3 | "name": "db", 4 | "connector": "memory" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /test/fixtures/shared-methods/config-defined-true/server/datasources.json: -------------------------------------------------------------------------------- 1 | { 2 | "db": { 3 | "name": "db", 4 | "connector": "memory" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /test/fixtures/shared-methods/config-default-false/server/datasources.json: -------------------------------------------------------------------------------- 1 | { 2 | "db": { 3 | "name": "db", 4 | "connector": "memory" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /test/fixtures/shared-methods/config-defined-false/server/datasources.json: -------------------------------------------------------------------------------- 1 | { 2 | "db": { 3 | "name": "db", 4 | "connector": "memory" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /test/fixtures/access-control/server/datasources.json: -------------------------------------------------------------------------------- 1 | { 2 | "db": { 3 | "connector": "memory" 4 | }, 5 | "mail": { 6 | "connector": "mail" 7 | } 8 | } -------------------------------------------------------------------------------- /test/fixtures/shared-methods/model-config-default-false/server/datasources.json: -------------------------------------------------------------------------------- 1 | { 2 | "db": { 3 | "name": "db", 4 | "connector": "memory" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /test/fixtures/shared-methods/model-config-default-true/server/datasources.json: -------------------------------------------------------------------------------- 1 | { 2 | "db": { 3 | "name": "db", 4 | "connector": "memory" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /test/fixtures/shared-methods/model-config-defined-false/server/datasources.json: -------------------------------------------------------------------------------- 1 | { 2 | "db": { 3 | "name": "db", 4 | "connector": "memory" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /test/fixtures/shared-methods/model-config-defined-true/server/datasources.json: -------------------------------------------------------------------------------- 1 | { 2 | "db": { 3 | "name": "db", 4 | "connector": "memory" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /test/fixtures/simple-integration-app/server/datasources.json: -------------------------------------------------------------------------------- 1 | { 2 | "db": { 3 | "connector": "memory" 4 | }, 5 | "mail": { 6 | "connector": "mail" 7 | } 8 | } -------------------------------------------------------------------------------- /test/fixtures/simple-integration-app/common/models/profile.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "profile", 3 | "base": "PersistedModel", 4 | "properties": { 5 | "points": { 6 | "type": "number" 7 | } 8 | } 9 | } -------------------------------------------------------------------------------- /templates/verify.ejs: -------------------------------------------------------------------------------- 1 |

Thank You

2 | 3 |

4 | Thanks for registering. Please follow the link below to complete your registration. 5 |

6 | 7 |

8 | <%= verifyHref %> 9 |

-------------------------------------------------------------------------------- /test/fixtures/access-control/server/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "port": 3000, 3 | "host": "0.0.0.0", 4 | "remoting": { 5 | "errorHandler": { 6 | "debug": true, 7 | "log": false 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /test/fixtures/simple-app/server/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "port": 3000, 3 | "host": "127.0.0.1", 4 | "remoting": { 5 | "errorHandler": { 6 | "debug": true, 7 | "log": false 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .project 3 | .DS_Store 4 | .vscode/ 5 | *.sublime* 6 | *.seed 7 | *.log 8 | *.csv 9 | *.dat 10 | *.out 11 | *.pid 12 | *.swp 13 | *.swo 14 | node_modules 15 | dist 16 | *xunit.xml 17 | .nyc_output/ 18 | -------------------------------------------------------------------------------- /test/fixtures/user-integration-app/server/datasources.json: -------------------------------------------------------------------------------- 1 | { 2 | "db": { 3 | "connector": "memory" 4 | }, 5 | "mail": { 6 | "connector": "mail", 7 | "transports": [ 8 | {"type": "STUB"} 9 | ] 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "loopback", 3 | "rules": { 4 | "max-len": ["error", 100, 4, { 5 | "ignoreComments": true, 6 | "ignoreUrls": true, 7 | "ignorePattern": "^\\s*var\\s.+=\\s*(require\\s*\\()|(/)" 8 | }] 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /common/models/checkpoint.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Checkpoint", 3 | "properties": { 4 | "seq": { 5 | "type": "number" 6 | }, 7 | "time": { 8 | "type": "date" 9 | }, 10 | "sourceId": { 11 | "type": "string" 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /test/fixtures/simple-integration-app/common/models/user.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "user", 3 | "base": "User", 4 | "relations": { 5 | "accessTokens": { 6 | "model": "accessToken", 7 | "type": "hasMany", 8 | "foreignKey": "userId" 9 | } 10 | } 11 | } -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - "8" 5 | - "10" 6 | - "12" 7 | - "14" 8 | 9 | addons: 10 | chrome: stable 11 | 12 | after_success: npm run coverage 13 | 14 | before_install: 15 | - npm config set registry http://ci.strongloop.com:4873/ 16 | -------------------------------------------------------------------------------- /test/fixtures/access-control/common/models/email.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "email", 3 | "base": "Email", 4 | "acls": [ 5 | { 6 | "accessType": "*", 7 | "permission": "DENY", 8 | "principalType": "ROLE", 9 | "principalId": "$everyone" 10 | } 11 | ] 12 | } -------------------------------------------------------------------------------- /test/fixtures/simple-integration-app/common/models/customer-forceid.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "customerforceidfalse", 3 | "base": "PersistedModel", 4 | "forceId": false, 5 | "properties": { 6 | "name": { 7 | "type": "string", 8 | "required": true 9 | } 10 | } 11 | } -------------------------------------------------------------------------------- /test/fixtures/access-control/common/models/alert.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "alert", 3 | "acls": [ 4 | { 5 | "accessType": "WRITE", 6 | "permission": "DENY", 7 | "principalType": "ROLE", 8 | "principalId": "$everyone" 9 | } 10 | ], 11 | "properties": {} 12 | } -------------------------------------------------------------------------------- /test/fixtures/simple-app/boot/foo.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2013,2018. All Rights Reserved. 2 | // Node module: loopback 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | process.loadedFooJS = true; 8 | -------------------------------------------------------------------------------- /test/fixtures/simple-integration-app/common/models/email.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "email", 3 | "base": "Email", 4 | "acls": [ 5 | { 6 | "accessType": "*", 7 | "permission": "DENY", 8 | "principalType": "ROLE", 9 | "principalId": "$everyone" 10 | } 11 | ] 12 | } -------------------------------------------------------------------------------- /test/fixtures/access-control/common/models/transaction.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "transaction", 3 | "acls": [ 4 | { 5 | "accessType": "*", 6 | "permission": "DENY", 7 | "principalType": "ROLE", 8 | "principalId": "$everyone" 9 | } 10 | ], 11 | "properties": {} 12 | } -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Lines starting with '#' are comments. 2 | # Each line is a file pattern followed by one or more owners, 3 | # the last matching pattern has the most precendence. 4 | 5 | # Current maintainers 6 | 7 | * @bajtos @fabien @clarkorz @ebarault @zbarbuto @nitro404 8 | 9 | # Alumni 10 | 11 | _ @lehni 12 | -------------------------------------------------------------------------------- /common/models/email.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Email", 3 | "base": "Model", 4 | "properties": { 5 | "to": {"type": "String", "required": true}, 6 | "from": {"type": "String", "required": true}, 7 | "subject": {"type": "String", "required": true}, 8 | "text": {"type": "String"}, 9 | "html": {"type": "String"} 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /test/fixtures/simple-integration-app/common/models/widget.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "widget", 3 | "properties": { 4 | "name": { 5 | "type": "string", 6 | "default": "DefaultWidgetName" 7 | } 8 | }, 9 | "relations": { 10 | "store": { 11 | "model": "store", 12 | "type": "belongsTo" 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /test/fixtures/shared-methods/both-configs-set/common/models/todo.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015,2018. All Rights Reserved. 2 | // Node module: loopback 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | module.exports = function(Todo) { 8 | 9 | }; 10 | -------------------------------------------------------------------------------- /test/fixtures/shared-methods/config-default-false/common/models/todo.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015,2018. All Rights Reserved. 2 | // Node module: loopback 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | module.exports = function(Todo) { 8 | 9 | }; 10 | -------------------------------------------------------------------------------- /test/fixtures/shared-methods/config-default-true/common/models/todo.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015,2018. All Rights Reserved. 2 | // Node module: loopback 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | module.exports = function(Todo) { 8 | 9 | }; 10 | -------------------------------------------------------------------------------- /test/fixtures/shared-methods/config-defined-false/common/models/todo.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015,2018. All Rights Reserved. 2 | // Node module: loopback 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | module.exports = function(Todo) { 8 | 9 | }; 10 | -------------------------------------------------------------------------------- /test/fixtures/shared-methods/config-defined-true/common/models/todo.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015,2018. All Rights Reserved. 2 | // Node module: loopback 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | module.exports = function(Todo) { 8 | 9 | }; 10 | -------------------------------------------------------------------------------- /test/fixtures/shared-methods/model-config-default-true/common/models/todo.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015,2018. All Rights Reserved. 2 | // Node module: loopback 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | module.exports = function(Todo) { 8 | 9 | }; 10 | -------------------------------------------------------------------------------- /test/fixtures/shared-methods/model-config-defined-true/common/models/todo.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015,2018. All Rights Reserved. 2 | // Node module: loopback 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | module.exports = function(Todo) { 8 | 9 | }; 10 | -------------------------------------------------------------------------------- /test/fixtures/simple-app/common/models/bar.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015,2018. All Rights Reserved. 2 | // Node module: loopback 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | module.exports = function(Bar) { 8 | process.loadedBarJS = true; 9 | }; 10 | -------------------------------------------------------------------------------- /test/fixtures/simple-integration-app/common/models/physician.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "physician", 3 | "properties": { 4 | "name": "string" 5 | }, 6 | "relations": { 7 | "patients": { 8 | "model": "patient", 9 | "type": "hasMany", 10 | "through": "appointment", 11 | "foreignKey": "patientId" 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /test/fixtures/shared-methods/model-config-default-false/common/models/todo.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015,2018. All Rights Reserved. 2 | // Node module: loopback 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | module.exports = function(Todo) { 8 | 9 | }; 10 | -------------------------------------------------------------------------------- /test/fixtures/shared-methods/model-config-defined-false/common/models/todo.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015,2018. All Rights Reserved. 2 | // Node module: loopback 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | module.exports = function(Todo) { 8 | 9 | }; 10 | -------------------------------------------------------------------------------- /test/fixtures/simple-integration-app/common/models/store.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "store", 3 | "properties": {}, 4 | "scopes": { 5 | "superStores": { 6 | "where": { 7 | "size": "super" 8 | } 9 | } 10 | }, 11 | "relations": { 12 | "widgets": { 13 | "model": "widget", 14 | "type": "hasMany" 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /test/fixtures/simple-integration-app/common/models/customer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "customer", 3 | "base": "PersistedModel", 4 | "properties": { 5 | "name": { 6 | "type": "string", 7 | "required": true 8 | } 9 | }, 10 | "relations": { 11 | "profile": { 12 | "type": "hasOne", 13 | "model": "profile" 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /common/models/scope.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Scope", 3 | "description": [ 4 | "Schema for Scope which represents the permissions that are granted", 5 | "to client applications by the resource owner" 6 | ], 7 | "properties": { 8 | "name": { 9 | "type": "string", 10 | "required": true 11 | }, 12 | "description": "string" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /test/fixtures/user-integration-app/common/models/blog.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "blog", 3 | "base": "PersistedModel", 4 | "properties": { 5 | "title": { 6 | "type": "string" 7 | }, 8 | "content": { 9 | "type": "string" 10 | } 11 | }, 12 | "relations": { 13 | "user": { 14 | "type": "belongsTo", 15 | "model": "myUser" 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /test/fixtures/simple-integration-app/common/models/patient.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "patient", 3 | "properties": { 4 | "name": "string" 5 | }, 6 | "options": { 7 | "relations": { 8 | "physicians": { 9 | "model": "physician", 10 | "type": "hasMany", 11 | "through": "appointment", 12 | "foreignKey": "physicianId" 13 | } 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /test/helpers/expect.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2016,2019. All Rights Reserved. 2 | // Node module: loopback 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | 8 | const chai = require('chai'); 9 | chai.use(require('dirty-chai')); 10 | chai.use(require('sinon-chai')); 11 | 12 | module.exports = chai.expect; 13 | -------------------------------------------------------------------------------- /test/fixtures/shared-methods/both-configs-set/common/models/todo.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Todo", 3 | "base": "PersistedModel", 4 | "idInjection": true, 5 | "options": { 6 | "validateUpsert": true 7 | }, 8 | "properties": { 9 | "content": { 10 | "type": "string", 11 | "required": true 12 | } 13 | }, 14 | "validations": [], 15 | "relations": {}, 16 | "acls": [], 17 | "methods": {} 18 | } 19 | -------------------------------------------------------------------------------- /lib/globalize.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2016,2019. All Rights Reserved. 2 | // Node module: loopback 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | const path = require('path'); 8 | const SG = require('strong-globalize'); 9 | 10 | SG.SetRootDir(path.join(__dirname, '..'), {autonomousMsgLoading: 'all'}); 11 | module.exports = SG(); 12 | -------------------------------------------------------------------------------- /test/fixtures/shared-methods/config-default-false/common/models/todo.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Todo", 3 | "base": "PersistedModel", 4 | "idInjection": true, 5 | "options": { 6 | "validateUpsert": true 7 | }, 8 | "properties": { 9 | "content": { 10 | "type": "string", 11 | "required": true 12 | } 13 | }, 14 | "validations": [], 15 | "relations": {}, 16 | "acls": [], 17 | "methods": {} 18 | } 19 | -------------------------------------------------------------------------------- /test/fixtures/shared-methods/config-default-true/common/models/todo.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Todo", 3 | "base": "PersistedModel", 4 | "idInjection": true, 5 | "options": { 6 | "validateUpsert": true 7 | }, 8 | "properties": { 9 | "content": { 10 | "type": "string", 11 | "required": true 12 | } 13 | }, 14 | "validations": [], 15 | "relations": {}, 16 | "acls": [], 17 | "methods": {} 18 | } 19 | -------------------------------------------------------------------------------- /test/fixtures/shared-methods/config-defined-false/common/models/todo.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Todo", 3 | "base": "PersistedModel", 4 | "idInjection": true, 5 | "options": { 6 | "validateUpsert": true 7 | }, 8 | "properties": { 9 | "content": { 10 | "type": "string", 11 | "required": true 12 | } 13 | }, 14 | "validations": [], 15 | "relations": {}, 16 | "acls": [], 17 | "methods": {} 18 | } 19 | -------------------------------------------------------------------------------- /test/fixtures/shared-methods/config-defined-true/common/models/todo.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Todo", 3 | "base": "PersistedModel", 4 | "idInjection": true, 5 | "options": { 6 | "validateUpsert": true 7 | }, 8 | "properties": { 9 | "content": { 10 | "type": "string", 11 | "required": true 12 | } 13 | }, 14 | "validations": [], 15 | "relations": {}, 16 | "acls": [], 17 | "methods": {} 18 | } 19 | -------------------------------------------------------------------------------- /test/fixtures/simple-integration-app/server/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "port": 3000, 3 | "host": "0.0.0.0", 4 | "cookieSecret": "2d13a01d-44fb-455c-80cb-db9cb3cd3cd0", 5 | "remoting": { 6 | "json": { 7 | "limit": "1kb", 8 | "strict": false 9 | }, 10 | "urlencoded": { 11 | "limit": "8kb" 12 | }, 13 | "errorHandler": { 14 | "debug": true, 15 | "log": false 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /test/fixtures/user-integration-app/server/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "port": 3000, 3 | "host": "0.0.0.0", 4 | "cookieSecret": "2d13a01d-44fb-455c-80cb-db9cb3cd3cd0", 5 | "remoting": { 6 | "json": { 7 | "limit": "1kb", 8 | "strict": false 9 | }, 10 | "urlencoded": { 11 | "limit": "8kb" 12 | }, 13 | "errorHandler": { 14 | "debug": true, 15 | "log": false 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /test/fixtures/shared-methods/model-config-default-false/common/models/todo.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Todo", 3 | "base": "PersistedModel", 4 | "idInjection": true, 5 | "options": { 6 | "validateUpsert": true 7 | }, 8 | "properties": { 9 | "content": { 10 | "type": "string", 11 | "required": true 12 | } 13 | }, 14 | "validations": [], 15 | "relations": {}, 16 | "acls": [], 17 | "methods": {} 18 | } 19 | -------------------------------------------------------------------------------- /test/fixtures/shared-methods/model-config-default-true/common/models/todo.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Todo", 3 | "base": "PersistedModel", 4 | "idInjection": true, 5 | "options": { 6 | "validateUpsert": true 7 | }, 8 | "properties": { 9 | "content": { 10 | "type": "string", 11 | "required": true 12 | } 13 | }, 14 | "validations": [], 15 | "relations": {}, 16 | "acls": [], 17 | "methods": {} 18 | } 19 | -------------------------------------------------------------------------------- /test/fixtures/shared-methods/model-config-defined-false/common/models/todo.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Todo", 3 | "base": "PersistedModel", 4 | "idInjection": true, 5 | "options": { 6 | "validateUpsert": true 7 | }, 8 | "properties": { 9 | "content": { 10 | "type": "string", 11 | "required": true 12 | } 13 | }, 14 | "validations": [], 15 | "relations": {}, 16 | "acls": [], 17 | "methods": {} 18 | } 19 | -------------------------------------------------------------------------------- /test/fixtures/shared-methods/model-config-defined-true/common/models/todo.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Todo", 3 | "base": "PersistedModel", 4 | "idInjection": true, 5 | "options": { 6 | "validateUpsert": true 7 | }, 8 | "properties": { 9 | "content": { 10 | "type": "string", 11 | "required": true 12 | } 13 | }, 14 | "validations": [], 15 | "relations": {}, 16 | "acls": [], 17 | "methods": {} 18 | } 19 | -------------------------------------------------------------------------------- /server/middleware/error-handler.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015,2018. All Rights Reserved. 2 | // Node module: loopback 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | module.exports = function(options) { 8 | throw new Error('loopback.errorHandler is no longer available.' + 9 | ' Please use the module "strong-error-handler" instead.'); 10 | }; 11 | -------------------------------------------------------------------------------- /test/fixtures/user-integration-app/common/models/my-user.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "myUser", 3 | "base": "User", 4 | "relations": { 5 | "blogs": { 6 | "model": "blog", 7 | "type": "hasMany", 8 | "foreignKey": "userId" 9 | } 10 | }, 11 | "acls": [ 12 | { 13 | "permission": "ALLOW", 14 | "principalType": "ROLE", 15 | "principalId": "$owner" 16 | } 17 | ], 18 | "saltWorkFactor": 4 19 | } 20 | -------------------------------------------------------------------------------- /test/fixtures/user-integration-app/common/models/post.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Post", 3 | "base": "PersistedModel", 4 | "properties": { 5 | "title": { 6 | "type": "string" 7 | }, 8 | "content": { 9 | "type": "string" 10 | }, 11 | "notes": { 12 | "type": "string" 13 | } 14 | }, 15 | "relations": { 16 | "user": { 17 | "type": "belongsTo", 18 | "model": "User" 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /test/fixtures/simple-integration-app/common/models/store-replacing.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "storeWithReplaceOnPUTtrue", 3 | "plural": "stores-replacing", 4 | "properties": {}, 5 | "scopes": { 6 | "superStores": { 7 | "where": { 8 | "size": "super" 9 | } 10 | } 11 | }, 12 | "relations": { 13 | "widgets": { 14 | "model": "widget", 15 | "type": "hasMany" 16 | } 17 | }, 18 | "replaceOnPUT": true 19 | } -------------------------------------------------------------------------------- /test/fixtures/simple-integration-app/common/models/store-updating.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "storeWithReplaceOnPUTfalse", 3 | "plural": "stores-updating", 4 | "properties": {}, 5 | "scopes": { 6 | "superStores": { 7 | "where": { 8 | "size": "super" 9 | } 10 | } 11 | }, 12 | "relations": { 13 | "widgets": { 14 | "model": "widget", 15 | "type": "hasMany" 16 | } 17 | }, 18 | "replaceOnPUT": false 19 | } -------------------------------------------------------------------------------- /common/models/acl.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ACL", 3 | "properties": { 4 | "model": { 5 | "type": "string", 6 | "description": "The name of the model" 7 | }, 8 | "property": { 9 | "type": "string", 10 | "description": "The name of the property, method, scope, or relation" 11 | }, 12 | "accessType": "string", 13 | "permission": "string", 14 | "principalType": "string", 15 | "principalId": "string" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /test/fixtures/e2e/server/models.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015,2019. All Rights Reserved. 2 | // Node module: loopback 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | const loopback = require('../../../../index'); 8 | const PersistedModel = loopback.PersistedModel; 9 | 10 | exports.TestModel = PersistedModel.extend('TestModel', {}, { 11 | trackChanges: true, 12 | }); 13 | -------------------------------------------------------------------------------- /test/fixtures/shared-methods/both-configs-set/server/server.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015,2019. All Rights Reserved. 2 | // Node module: loopback 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | const boot = require('loopback-boot'); 8 | const loopback = require('../../../../../index'); 9 | 10 | const app = module.exports = loopback(); 11 | boot(app, __dirname); 12 | app.use(loopback.rest()); 13 | -------------------------------------------------------------------------------- /test/fixtures/shared-methods/config-default-true/server/server.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015,2019. All Rights Reserved. 2 | // Node module: loopback 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | const boot = require('loopback-boot'); 8 | const loopback = require('../../../../../index'); 9 | 10 | const app = module.exports = loopback(); 11 | boot(app, __dirname); 12 | app.use(loopback.rest()); 13 | -------------------------------------------------------------------------------- /test/fixtures/shared-methods/config-defined-true/server/server.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015,2019. All Rights Reserved. 2 | // Node module: loopback 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | const boot = require('loopback-boot'); 8 | const loopback = require('../../../../../index'); 9 | 10 | const app = module.exports = loopback(); 11 | boot(app, __dirname); 12 | app.use(loopback.rest()); 13 | -------------------------------------------------------------------------------- /test/helpers/use-english.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2017,2019. All Rights Reserved. 2 | // Node module: loopback 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | 8 | const env = process.env; 9 | 10 | // delete any user-provided language settings 11 | delete env.LC_ALL; 12 | delete env.LC_MESSAGES; 13 | delete env.LANG; 14 | delete env.LANGUAGE; 15 | delete env.STRONGLOOP_GLOBALIZE_APP_LANGUAGE; 16 | -------------------------------------------------------------------------------- /common/models/change.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Change", 3 | "trackChanges": false, 4 | "properties": { 5 | "id": { 6 | "type": "string", 7 | "id": true 8 | }, 9 | "rev": { 10 | "type": "string" 11 | }, 12 | "prev": { 13 | "type": "string" 14 | }, 15 | "checkpoint": { 16 | "type": "number" 17 | }, 18 | "modelName": { 19 | "type": "string" 20 | }, 21 | "modelId": { 22 | "type": "string" 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /test/fixtures/shared-methods/config-default-false/server/server.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015,2019. All Rights Reserved. 2 | // Node module: loopback 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | const boot = require('loopback-boot'); 8 | const loopback = require('../../../../../index'); 9 | 10 | const app = module.exports = loopback(); 11 | boot(app, __dirname); 12 | app.use(loopback.rest()); 13 | -------------------------------------------------------------------------------- /test/fixtures/shared-methods/config-defined-false/server/server.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015,2019. All Rights Reserved. 2 | // Node module: loopback 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | const boot = require('loopback-boot'); 8 | const loopback = require('../../../../../index'); 9 | 10 | const app = module.exports = loopback(); 11 | boot(app, __dirname); 12 | app.use(loopback.rest()); 13 | -------------------------------------------------------------------------------- /test/fixtures/shared-methods/model-config-default-false/server/server.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015,2019. All Rights Reserved. 2 | // Node module: loopback 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | const boot = require('loopback-boot'); 8 | const loopback = require('../../../../../index'); 9 | 10 | const app = module.exports = loopback(); 11 | boot(app, __dirname); 12 | app.use(loopback.rest()); 13 | -------------------------------------------------------------------------------- /test/fixtures/shared-methods/model-config-default-true/server/server.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015,2019. All Rights Reserved. 2 | // Node module: loopback 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | const boot = require('loopback-boot'); 8 | const loopback = require('../../../../../index'); 9 | 10 | const app = module.exports = loopback(); 11 | boot(app, __dirname); 12 | app.use(loopback.rest()); 13 | -------------------------------------------------------------------------------- /test/fixtures/shared-methods/model-config-defined-false/server/server.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015,2019. All Rights Reserved. 2 | // Node module: loopback 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | const boot = require('loopback-boot'); 8 | const loopback = require('../../../../../index'); 9 | 10 | const app = module.exports = loopback(); 11 | boot(app, __dirname); 12 | app.use(loopback.rest()); 13 | -------------------------------------------------------------------------------- /test/fixtures/shared-methods/model-config-defined-true/server/server.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015,2019. All Rights Reserved. 2 | // Node module: loopback 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | const boot = require('loopback-boot'); 8 | const loopback = require('../../../../../index'); 9 | 10 | const app = module.exports = loopback(); 11 | boot(app, __dirname); 12 | app.use(loopback.rest()); 13 | -------------------------------------------------------------------------------- /test/fixtures/simple-integration-app/common/models/appointment.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "appointment", 3 | "properties": { 4 | "date": "date" 5 | }, 6 | "options": { 7 | "relations": { 8 | "physician": { 9 | "model": "physician", 10 | "type": "belongsTo", 11 | "foreignKey": "physicianId" 12 | }, 13 | "patient": { 14 | "model": "patient", 15 | "type": "belongsTo", 16 | "foreignKey": "patientId" 17 | } 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /test/helpers/wait-for-event.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2017,2018. All Rights Reserved. 2 | // Node module: loopback 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | 8 | const Promise = require('bluebird'); 9 | 10 | function waitForEvent(emitter, event) { 11 | return new Promise((resolve, reject) => { 12 | emitter.on(event, resolve); 13 | }); 14 | } 15 | 16 | module.exports = waitForEvent; 17 | 18 | -------------------------------------------------------------------------------- /server/middleware/context.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2014,2019. All Rights Reserved. 2 | // Node module: loopback 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | const g = require('../../lib/globalize'); 8 | 9 | module.exports = function() { 10 | throw new Error(g.f( 11 | '%s middleware was removed in version 3.0. See %s for more details.', 12 | 'loopback#context', 13 | 'http://loopback.io/doc/en/lb2/Using-current-context.html', 14 | )); 15 | }; 16 | -------------------------------------------------------------------------------- /test/fixtures/shared-methods/model-config-default-false/server/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "restApiRoot": "/api", 3 | "host": "0.0.0.0", 4 | "port": 3000, 5 | "remoting": { 6 | "rest": { 7 | "normalizeHttpPath": false, 8 | "xml": false 9 | }, 10 | "json": { 11 | "strict": false, 12 | "limit": "100kb" 13 | }, 14 | "urlencoded": { 15 | "extended": true, 16 | "limit": "100kb" 17 | }, 18 | "cors": false, 19 | "errorHandler": { 20 | "debug": true, 21 | "log": false 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /test/fixtures/shared-methods/model-config-default-true/server/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "restApiRoot": "/api", 3 | "host": "0.0.0.0", 4 | "port": 3000, 5 | "remoting": { 6 | "rest": { 7 | "normalizeHttpPath": false, 8 | "xml": false 9 | }, 10 | "json": { 11 | "strict": false, 12 | "limit": "100kb" 13 | }, 14 | "urlencoded": { 15 | "extended": true, 16 | "limit": "100kb" 17 | }, 18 | "cors": false, 19 | "errorHandler": { 20 | "debug": true, 21 | "log": false 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /test/fixtures/shared-methods/model-config-defined-false/server/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "restApiRoot": "/api", 3 | "host": "0.0.0.0", 4 | "port": 3000, 5 | "remoting": { 6 | "rest": { 7 | "normalizeHttpPath": false, 8 | "xml": false 9 | }, 10 | "json": { 11 | "strict": false, 12 | "limit": "100kb" 13 | }, 14 | "urlencoded": { 15 | "extended": true, 16 | "limit": "100kb" 17 | }, 18 | "cors": false, 19 | "errorHandler": { 20 | "debug": true, 21 | "log": false 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /test/fixtures/shared-methods/model-config-defined-true/server/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "restApiRoot": "/api", 3 | "host": "0.0.0.0", 4 | "port": 3000, 5 | "remoting": { 6 | "rest": { 7 | "normalizeHttpPath": false, 8 | "xml": false 9 | }, 10 | "json": { 11 | "strict": false, 12 | "limit": "100kb" 13 | }, 14 | "urlencoded": { 15 | "extended": true, 16 | "limit": "100kb" 17 | }, 18 | "cors": false, 19 | "errorHandler": { 20 | "debug": true, 21 | "log": false 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /server/middleware/favicon.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2014,2019. All Rights Reserved. 2 | // Node module: loopback 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | const favicon = require('serve-favicon'); 8 | const path = require('path'); 9 | 10 | /** 11 | * Serve the LoopBack favicon. 12 | * @header loopback.favicon() 13 | */ 14 | module.exports = function(icon, options) { 15 | icon = icon || path.join(__dirname, '../../favicon.ico'); 16 | return favicon(icon, options); 17 | }; 18 | -------------------------------------------------------------------------------- /test/fixtures/access-control/common/models/user.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "user", 3 | "base": "User", 4 | "relations": { 5 | "accessTokens": { 6 | "model": "accessToken", 7 | "type": "hasMany", 8 | "foreignKey": "userId" 9 | }, 10 | "transactions": { 11 | "model": "transaction", 12 | "type": "hasMany" 13 | } 14 | }, 15 | "acls": [ 16 | { 17 | "accessType": "*", 18 | "permission": "DENY", 19 | "principalType": "ROLE", 20 | "principalId": "$everyone" 21 | } 22 | ], 23 | "replaceOnPUT": false 24 | } 25 | -------------------------------------------------------------------------------- /test/fixtures/shared-methods/config-default-true/server/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "restApiRoot": "/api", 3 | "host": "0.0.0.0", 4 | "port": 3000, 5 | "remoting": { 6 | "rest": { 7 | "normalizeHttpPath": false, 8 | "xml": false 9 | }, 10 | "json": { 11 | "strict": false, 12 | "limit": "100kb" 13 | }, 14 | "urlencoded": { 15 | "extended": true, 16 | "limit": "100kb" 17 | }, 18 | "cors": false, 19 | "errorHandler": { 20 | "debug": true, 21 | "log": false 22 | }, 23 | "sharedMethods": { 24 | "*": true 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /test/fixtures/shared-methods/config-default-false/server/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "restApiRoot": "/api", 3 | "host": "0.0.0.0", 4 | "port": 3000, 5 | "remoting": { 6 | "rest": { 7 | "normalizeHttpPath": false, 8 | "xml": false 9 | }, 10 | "json": { 11 | "strict": false, 12 | "limit": "100kb" 13 | }, 14 | "urlencoded": { 15 | "extended": true, 16 | "limit": "100kb" 17 | }, 18 | "cors": false, 19 | "errorHandler": { 20 | "debug": true, 21 | "log": false 22 | }, 23 | "sharedMethods": { 24 | "*": false 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /test/fixtures/shared-methods/config-defined-false/server/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "restApiRoot": "/api", 3 | "host": "0.0.0.0", 4 | "port": 3000, 5 | "remoting": { 6 | "rest": { 7 | "normalizeHttpPath": false, 8 | "xml": false 9 | }, 10 | "json": { 11 | "strict": false, 12 | "limit": "100kb" 13 | }, 14 | "urlencoded": { 15 | "extended": true, 16 | "limit": "100kb" 17 | }, 18 | "cors": false, 19 | "errorHandler": { 20 | "debug": true, 21 | "log": false 22 | }, 23 | "sharedMethods": { 24 | "find": false 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /test/fixtures/shared-methods/config-defined-true/server/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "restApiRoot": "/api", 3 | "host": "0.0.0.0", 4 | "port": 3000, 5 | "remoting": { 6 | "rest": { 7 | "normalizeHttpPath": false, 8 | "xml": false 9 | }, 10 | "json": { 11 | "strict": false, 12 | "limit": "100kb" 13 | }, 14 | "urlencoded": { 15 | "extended": true, 16 | "limit": "100kb" 17 | }, 18 | "cors": false, 19 | "errorHandler": { 20 | "debug": true, 21 | "log": false 22 | }, 23 | "sharedMethods": { 24 | "find": true 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /test/fixtures/shared-methods/both-configs-set/server/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "restApiRoot": "/api", 3 | "host": "0.0.0.0", 4 | "port": 3000, 5 | "remoting": { 6 | "rest": { 7 | "normalizeHttpPath": false, 8 | "xml": false 9 | }, 10 | "json": { 11 | "strict": false, 12 | "limit": "100kb" 13 | }, 14 | "urlencoded": { 15 | "extended": true, 16 | "limit": "100kb" 17 | }, 18 | "cors": false, 19 | "errorHandler": { 20 | "debug": true, 21 | "log": false 22 | }, 23 | "sharedMethods": { 24 | "*": false, 25 | "destroyAll": true 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /common/models/role.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Role", 3 | "properties": { 4 | 5 | "id": { 6 | "type": "string", 7 | "id": true, 8 | "generated": true 9 | }, 10 | "name": { 11 | "type": "string", 12 | "required": true 13 | }, 14 | "description": "string", 15 | "created": { 16 | "type":"date", 17 | "defaultFn": "now" 18 | }, 19 | "modified": { 20 | "type":"date", 21 | "defaultFn": "now" 22 | } 23 | }, 24 | "relations": { 25 | "principals": { 26 | "type": "hasMany", 27 | "model": "RoleMapping", 28 | "foreignKey": "roleId" 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /test/fixtures/access-control/common/models/access-token.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "accessToken", 3 | "base": "AccessToken", 4 | "baseUrl": "access-tokens", 5 | "acls": [ 6 | { 7 | "accessType": "*", 8 | "permission": "DENY", 9 | "principalType": "ROLE", 10 | "principalId": "$everyone" 11 | }, 12 | { 13 | "permission": "ALLOW", 14 | "principalType": "ROLE", 15 | "principalId": "$everyone", 16 | "property": "create" 17 | } 18 | ], 19 | "relations": { 20 | "user": { 21 | "type": "belongsTo", 22 | "model": "user", 23 | "foreignKey": "userId" 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /test/util/it.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2014,2019. All Rights Reserved. 2 | // Node module: loopback 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | const loopback = require('../../'); 8 | 9 | module.exports = it; 10 | 11 | it.onServer = function itOnServer(name, fn) { 12 | if (loopback.isServer) { 13 | it(name, fn); 14 | } else { 15 | it.skip(name, fn); 16 | } 17 | }; 18 | 19 | it.inBrowser = function itInBrowser(name, fn) { 20 | if (loopback.isBrowser) { 21 | it(name, fn); 22 | } else { 23 | it.skip(name, fn); 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /test/fixtures/simple-integration-app/server/server.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015,2019. All Rights Reserved. 2 | // Node module: loopback 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | const loopback = require('../../../../index'); 8 | const boot = require('loopback-boot'); 9 | const app = module.exports = loopback({localRegistry: true}); 10 | const errorHandler = require('strong-error-handler'); 11 | 12 | boot(app, __dirname); 13 | const apiPath = '/api'; 14 | app.use(apiPath, loopback.rest()); 15 | app.use(loopback.urlNotFound()); 16 | app.use(errorHandler()); 17 | -------------------------------------------------------------------------------- /test/fixtures/simple-app/server/model-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "sources": [ 4 | "../common/models", 5 | "./models", 6 | "../../../../common/models" 7 | ] 8 | }, 9 | "User": { 10 | "dataSource": "db", 11 | "public": false 12 | }, 13 | "AccessToken": { 14 | "dataSource": "db", 15 | "public": false 16 | }, 17 | "ACL": { 18 | "dataSource": "db", 19 | "public": false 20 | }, 21 | "RoleMapping": { 22 | "dataSource": "db", 23 | "public": false 24 | }, 25 | "Role": { 26 | "dataSource": "db", 27 | "public": false 28 | }, 29 | "foo": { 30 | "dataSource": "db" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /common/models/role-mapping.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "RoleMapping", 3 | "description": "Map principals to roles", 4 | "properties": { 5 | "id": { 6 | "type": "string", 7 | "id": true, 8 | "generated": true 9 | }, 10 | "principalType": { 11 | "type": "string", 12 | "description": "The principal type, such as USER, APPLICATION, ROLE, or user model name in case of multiple user models" 13 | }, 14 | "principalId": { 15 | "type": "string", 16 | "index": true 17 | } 18 | }, 19 | "relations": { 20 | "role": { 21 | "type": "belongsTo", 22 | "model": "Role", 23 | "foreignKey": "roleId" 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /test/error-handler.test.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015,2019. All Rights Reserved. 2 | // Node module: loopback 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | const loopback = require('../'); 8 | let app; 9 | const assert = require('assert'); 10 | const request = require('supertest'); 11 | const expect = require('./helpers/expect'); 12 | 13 | describe('loopback.errorHandler(options)', function() { 14 | it('should throw a descriptive error', function() { 15 | expect(function() { loopback.errorHandler(); }) 16 | .to.throw(/no longer available.*strong-error-handler/); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /server/middleware/static.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2014,2018. All Rights Reserved. 2 | // Node module: loopback 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | /** 7 | * Serve static assets of a LoopBack application. 8 | * 9 | * @param {string} root The root directory from which the static assets are to 10 | * be served. 11 | * @param {object} options Refer to 12 | * [express documentation](http://expressjs.com/4x/api.html#express.static) 13 | * for the full list of available options. 14 | * @header loopback.static(root, [options]) 15 | */ 16 | 'use strict'; 17 | module.exports = require('express').static; 18 | -------------------------------------------------------------------------------- /test/util/describe.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2014,2019. All Rights Reserved. 2 | // Node module: loopback 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | const loopback = require('../../'); 8 | 9 | module.exports = describe; 10 | 11 | describe.onServer = function describeOnServer(name, fn) { 12 | if (loopback.isServer) { 13 | describe(name, fn); 14 | } else { 15 | describe.skip(name, fn); 16 | } 17 | }; 18 | 19 | describe.inBrowser = function describeInBrowser(name, fn) { 20 | if (loopback.isBrowser) { 21 | describe(name, fn); 22 | } else { 23 | describe.skip(name, fn); 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/Question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Question 3 | about: The issue tracker is not for questions. Please use Stack Overflow or other resources for help. 4 | labels: question 5 | 6 | --- 7 | 8 | 28 | -------------------------------------------------------------------------------- /test/helpers/error-loggers.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2017,2018. All Rights Reserved. 2 | // Node module: loopback 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | 8 | exports.logAllServerErrors = function(app) { 9 | exports.logServerErrorsOtherThan(-1, app); 10 | }; 11 | 12 | exports.logServerErrorsOtherThan = function(statusCode, app) { 13 | app.use((err, req, res, next) => { 14 | if ((err.statusCode || 500) !== statusCode) { 15 | console.log('Unhandled error for request %s %s: %s', 16 | req.method, req.url, err.stack || err); 17 | } 18 | res.statusCode = err.statusCode || 500; 19 | res.json(err); 20 | }); 21 | }; 22 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 10 | 11 | ## Checklist 12 | 13 | 👉 [Read and sign the CLA (Contributor License Agreement)](https://cla.strongloop.com/agreements/strongloop/loopback) 👈 14 | 15 | - [ ] `npm test` passes on your machine 16 | - [ ] New tests added or existing tests modified to cover all changes 17 | - [ ] Code conforms with the [style guide](https://loopback.io/doc/en/contrib/style-guide-es6.html) 18 | - [ ] Commit messages are following our [guidelines](https://loopback.io/doc/en/contrib/git-commit-messages.html) 19 | -------------------------------------------------------------------------------- /test/fixtures/access-control/server/server.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015,2019. All Rights Reserved. 2 | // Node module: loopback 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | const loopback = require('../../../..'); 8 | const boot = require('loopback-boot'); 9 | const app = module.exports = loopback({ 10 | localRegistry: true, 11 | loadBuiltinModels: true, 12 | }); 13 | const errorHandler = require('strong-error-handler'); 14 | 15 | boot(app, __dirname); 16 | 17 | const apiPath = '/api'; 18 | app.use(loopback.token({model: app.models.accessToken})); 19 | app.use(apiPath, loopback.rest()); 20 | 21 | app.use(loopback.urlNotFound()); 22 | app.use(errorHandler()); 23 | app.enableAuth(); 24 | -------------------------------------------------------------------------------- /test/fixtures/user-integration-app/server/server.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015,2019. All Rights Reserved. 2 | // Node module: loopback 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | const loopback = require('../../../../index'); 8 | const boot = require('loopback-boot'); 9 | const app = module.exports = loopback({ 10 | localRegistry: true, 11 | loadBuiltinModels: true, 12 | }); 13 | const errorHandler = require('strong-error-handler'); 14 | 15 | app.enableAuth(); 16 | boot(app, __dirname); 17 | app.use(loopback.token({model: app.models.AccessToken})); 18 | const apiPath = '/api'; 19 | app.use(apiPath, loopback.rest()); 20 | app.use(loopback.urlNotFound()); 21 | app.use(errorHandler()); 22 | -------------------------------------------------------------------------------- /test/fixtures/e2e/server/server.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015,2019. All Rights Reserved. 2 | // Node module: loopback 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | const loopback = require('../../../../index'); 8 | const app = module.exports = loopback({localRegistry: true}); 9 | const models = require('./models'); 10 | const TestModel = models.TestModel; 11 | 12 | const apiPath = '/api'; 13 | app.use(apiPath, loopback.rest()); 14 | 15 | TestModel.attachTo(loopback.memory()); 16 | app.model(TestModel); 17 | app.model(TestModel.getChangeModel()); 18 | 19 | // app.use(loopback.static(path.join(__dirname, 'public'))); 20 | app.use(loopback.urlNotFound()); 21 | app.use(loopback.errorHandler()); 22 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Report a security vulnerability 4 | url: https://loopback.io/doc/en/contrib/Reporting-issues.html#security-issues 5 | about: > 6 | LoopBack 3 has reached End-of-Life. No new security fixes will be provided 7 | or accepted. 8 | 9 | Do not report security vulnerabilities using GitHub issues. Please send an 10 | email to `reachsl@us.ibm.com` instead. 11 | - name: Get help on StackOverflow 12 | url: https://stackoverflow.com/tags/loopbackjs 13 | about: Please ask and answer questions on StackOverflow. 14 | - name: Join our mailing list 15 | url: https://groups.google.com/forum/#!forum/loopbackjs 16 | about: You can also post your question to our mailing list. 17 | -------------------------------------------------------------------------------- /lib/runtime.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2014,2019. All Rights Reserved. 2 | // Node module: loopback 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | /* 7 | * This is an internal file that should not be used outside of loopback. 8 | * All exported entities can be accessed via the `loopback` object. 9 | * @private 10 | */ 11 | 12 | 'use strict'; 13 | const runtime = exports; 14 | 15 | /** 16 | * True if running in a browser environment; false otherwise. 17 | * @header loopback.isBrowser 18 | */ 19 | 20 | runtime.isBrowser = typeof window !== 'undefined'; 21 | 22 | /** 23 | * True if running in a server environment; false otherwise. 24 | * @header loopback.isServer 25 | */ 26 | 27 | runtime.isServer = !runtime.isBrowser; 28 | -------------------------------------------------------------------------------- /test/fixtures/access-control/common/models/bank.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bank", 3 | "relations": { 4 | "users": { 5 | "model": "user", 6 | "type": "hasMany" 7 | }, 8 | "accounts": { 9 | "model": "account", 10 | "type": "hasMany" 11 | } 12 | }, 13 | "acls": [ 14 | { 15 | "accessType": "*", 16 | "permission": "DENY", 17 | "principalType": "ROLE", 18 | "principalId": "$everyone" 19 | }, 20 | { 21 | "accessType": "READ", 22 | "permission": "ALLOW", 23 | "principalType": "ROLE", 24 | "principalId": "$everyone" 25 | }, 26 | { 27 | "accessType": "WRITE", 28 | "permission": "ALLOW", 29 | "principalType": "ROLE", 30 | "principalId": "$dynamic-role" 31 | } 32 | ], 33 | "properties": {} 34 | } -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/Feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | labels: feature 5 | 6 | --- 7 | 8 | 16 | 17 | ## Suggestion 18 | 19 | 20 | 21 | ## Use Cases 22 | 23 | 27 | 28 | ## Examples 29 | 30 | 31 | 32 | ## Acceptance criteria 33 | 34 | TBD - will be filled by the team. 35 | -------------------------------------------------------------------------------- /example/colors/app.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2013,2019. All Rights Reserved. 2 | // Node module: loopback 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | const g = require('../../lib/globalize'); 8 | const loopback = require('../../'); 9 | const app = loopback(); 10 | 11 | app.use(loopback.rest()); 12 | 13 | const schema = { 14 | name: String, 15 | }; 16 | 17 | app.dataSource('db', {connector: 'memory'}); 18 | const Color = app.registry.createModel('color', schema); 19 | app.model(Color, {dataSource: 'db'}); 20 | 21 | Color.create({name: 'red'}); 22 | Color.create({name: 'green'}); 23 | Color.create({name: 'blue'}); 24 | 25 | app.listen(3000); 26 | 27 | g.log('a list of colors is available at {{http://localhost:3000/colors}}'); 28 | -------------------------------------------------------------------------------- /example/simple-data-source/app.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2013,2019. All Rights Reserved. 2 | // Node module: loopback 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | const g = require('../../lib/globalize'); 8 | const loopback = require('../../'); 9 | const app = loopback(); 10 | 11 | app.use(loopback.rest()); 12 | 13 | const dataSource = app.dataSource('db', {adapter: 'memory'}); 14 | 15 | const Color = dataSource.define('color', { 16 | 'name': String, 17 | }); 18 | 19 | Color.create({name: 'red'}); 20 | Color.create({name: 'green'}); 21 | Color.create({name: 'blue'}); 22 | 23 | Color.all(function() { 24 | console.log(arguments); 25 | }); 26 | 27 | app.listen(3000); 28 | 29 | g.log('a list of colors is available at {{http://localhost:3000/colors}}'); 30 | -------------------------------------------------------------------------------- /example/client-server/client.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2014,2019. All Rights Reserved. 2 | // Node module: loopback 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | const g = require('../../lib/globalize'); 8 | const loopback = require('../../'); 9 | const client = loopback(); 10 | const CartItem = require('./models').CartItem; 11 | const remote = loopback.createDataSource({ 12 | connector: loopback.Remote, 13 | url: 'http://localhost:3000', 14 | }); 15 | 16 | client.model(CartItem); 17 | CartItem.attachTo(remote); 18 | 19 | // call the remote method 20 | CartItem.sum(1, function(err, total) { 21 | g.log('result:%s', err || total); 22 | }); 23 | 24 | // call a built in remote method 25 | CartItem.find(function(err, items) { 26 | console.log(items); 27 | }); 28 | -------------------------------------------------------------------------------- /server/middleware/url-not-found.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2014,2019. All Rights Reserved. 2 | // Node module: loopback 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | /*! 7 | * Export the middleware. 8 | * See discussion in Connect pull request #954 for more details 9 | * https://github.com/senchalabs/connect/pull/954. 10 | */ 11 | 'use strict'; 12 | module.exports = urlNotFound; 13 | 14 | /** 15 | * Convert any request not handled so far to a 404 error 16 | * to be handled by error-handling middleware. 17 | * @header loopback.urlNotFound() 18 | */ 19 | function urlNotFound() { 20 | return function raiseUrlNotFoundError(req, res, next) { 21 | const error = new Error('Cannot ' + req.method + ' ' + req.url); 22 | error.status = 404; 23 | next(error); 24 | }; 25 | } 26 | -------------------------------------------------------------------------------- /test/fixtures/shared-methods/config-default-false/server/model-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "sources": [ 4 | "loopback/common/models", 5 | "loopback/server/models", 6 | "../common/models", 7 | "./models" 8 | ], 9 | "mixins": [ 10 | "loopback/common/mixins", 11 | "loopback/server/mixins", 12 | "../common/mixins", 13 | "./mixins" 14 | ] 15 | }, 16 | "User": { 17 | "dataSource": "db" 18 | }, 19 | "AccessToken": { 20 | "dataSource": "db", 21 | "public": false 22 | }, 23 | "ACL": { 24 | "dataSource": "db", 25 | "public": false 26 | }, 27 | "RoleMapping": { 28 | "dataSource": "db", 29 | "public": false 30 | }, 31 | "Role": { 32 | "dataSource": "db", 33 | "public": false 34 | }, 35 | "Todo": { 36 | "dataSource": "db", 37 | "public": true 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /test/fixtures/shared-methods/config-default-true/server/model-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "sources": [ 4 | "loopback/common/models", 5 | "loopback/server/models", 6 | "../common/models", 7 | "./models" 8 | ], 9 | "mixins": [ 10 | "loopback/common/mixins", 11 | "loopback/server/mixins", 12 | "../common/mixins", 13 | "./mixins" 14 | ] 15 | }, 16 | "User": { 17 | "dataSource": "db" 18 | }, 19 | "AccessToken": { 20 | "dataSource": "db", 21 | "public": false 22 | }, 23 | "ACL": { 24 | "dataSource": "db", 25 | "public": false 26 | }, 27 | "RoleMapping": { 28 | "dataSource": "db", 29 | "public": false 30 | }, 31 | "Role": { 32 | "dataSource": "db", 33 | "public": false 34 | }, 35 | "Todo": { 36 | "dataSource": "db", 37 | "public": true 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /test/fixtures/shared-methods/config-defined-false/server/model-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "sources": [ 4 | "loopback/common/models", 5 | "loopback/server/models", 6 | "../common/models", 7 | "./models" 8 | ], 9 | "mixins": [ 10 | "loopback/common/mixins", 11 | "loopback/server/mixins", 12 | "../common/mixins", 13 | "./mixins" 14 | ] 15 | }, 16 | "User": { 17 | "dataSource": "db" 18 | }, 19 | "AccessToken": { 20 | "dataSource": "db", 21 | "public": false 22 | }, 23 | "ACL": { 24 | "dataSource": "db", 25 | "public": false 26 | }, 27 | "RoleMapping": { 28 | "dataSource": "db", 29 | "public": false 30 | }, 31 | "Role": { 32 | "dataSource": "db", 33 | "public": false 34 | }, 35 | "Todo": { 36 | "dataSource": "db", 37 | "public": true 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /test/fixtures/shared-methods/config-defined-true/server/model-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "sources": [ 4 | "loopback/common/models", 5 | "loopback/server/models", 6 | "../common/models", 7 | "./models" 8 | ], 9 | "mixins": [ 10 | "loopback/common/mixins", 11 | "loopback/server/mixins", 12 | "../common/mixins", 13 | "./mixins" 14 | ] 15 | }, 16 | "User": { 17 | "dataSource": "db" 18 | }, 19 | "AccessToken": { 20 | "dataSource": "db", 21 | "public": false 22 | }, 23 | "ACL": { 24 | "dataSource": "db", 25 | "public": false 26 | }, 27 | "RoleMapping": { 28 | "dataSource": "db", 29 | "public": false 30 | }, 31 | "Role": { 32 | "dataSource": "db", 33 | "public": false 34 | }, 35 | "Todo": { 36 | "dataSource": "db", 37 | "public": true 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /common/models/access-token.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "AccessToken", 3 | "properties": { 4 | "id": { 5 | "type": "string", 6 | "id": true 7 | }, 8 | "ttl": { 9 | "type": "number", 10 | "ttl": true, 11 | "default": 1209600, 12 | "description": "time to live in seconds (2 weeks by default)" 13 | }, 14 | "scopes": { 15 | "type": ["string"], 16 | "description": "Array of scopes granted to this access token." 17 | }, 18 | "created": { 19 | "type": "Date", 20 | "defaultFn": "now" 21 | } 22 | }, 23 | "relations": { 24 | "user": { 25 | "type": "belongsTo", 26 | "model": "User", 27 | "foreignKey": "userId" 28 | } 29 | }, 30 | "acls": [ 31 | { 32 | "principalType": "ROLE", 33 | "principalId": "$everyone", 34 | "permission": "DENY" 35 | } 36 | ] 37 | } 38 | -------------------------------------------------------------------------------- /example/client-server/server.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2014,2019. All Rights Reserved. 2 | // Node module: loopback 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | const loopback = require('../../'); 8 | const server = module.exports = loopback(); 9 | const CartItem = require('./models').CartItem; 10 | const memory = loopback.createDataSource({ 11 | connector: loopback.Memory, 12 | }); 13 | 14 | server.use(loopback.rest()); 15 | server.model(CartItem); 16 | 17 | CartItem.attachTo(memory); 18 | 19 | // test data 20 | CartItem.create([ 21 | {item: 'red hat', qty: 6, price: 19.99, cartId: 1}, 22 | {item: 'green shirt', qty: 1, price: 14.99, cartId: 1}, 23 | {item: 'orange pants', qty: 58, price: 9.99, cartId: 1}, 24 | ]); 25 | 26 | CartItem.sum(1, function(err, total) { 27 | console.log(total); 28 | }); 29 | 30 | server.listen(3000); 31 | -------------------------------------------------------------------------------- /server/middleware/status.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2014,2019. All Rights Reserved. 2 | // Node module: loopback 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | /*! 7 | * Export the middleware. 8 | */ 9 | 10 | 'use strict'; 11 | module.exports = status; 12 | 13 | /** 14 | * Return [HTTP response](http://expressjs.com/4x/api.html#res.send) with basic application status information: 15 | * date the application was started and uptime, in JSON format. 16 | * For example: 17 | * ```js 18 | * { 19 | * "started": "2014-06-05T00:26:49.750Z", 20 | * "uptime": 9.394 21 | * } 22 | * ``` 23 | * 24 | * @header loopback.status() 25 | */ 26 | function status() { 27 | const started = new Date(); 28 | 29 | return function(req, res) { 30 | res.send({ 31 | started: started, 32 | uptime: (Date.now() - Number(started)) / 1000, 33 | }); 34 | }; 35 | } 36 | -------------------------------------------------------------------------------- /lib/browser-express.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2014,2019. All Rights Reserved. 2 | // Node module: loopback 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | const EventEmitter = require('events').EventEmitter; 8 | const util = require('util'); 9 | 10 | module.exports = browserExpress; 11 | 12 | function browserExpress() { 13 | return new BrowserExpress(); 14 | } 15 | 16 | browserExpress.errorHandler = {}; 17 | 18 | function BrowserExpress() { 19 | this.settings = {}; 20 | } 21 | 22 | util.inherits(BrowserExpress, EventEmitter); 23 | 24 | BrowserExpress.prototype.set = function(key, value) { 25 | if (arguments.length == 1) { 26 | return this.get(key); 27 | } 28 | 29 | this.settings[key] = value; 30 | 31 | return this; // fluent API 32 | }; 33 | 34 | BrowserExpress.prototype.get = function(key) { 35 | return this.settings[key]; 36 | }; 37 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2013,2018. All Rights Reserved. 2 | // Node module: loopback 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | /** 8 | * loopback ~ public api 9 | */ 10 | 11 | const loopback = module.exports = require('./lib/loopback'); 12 | const datasourceJuggler = require('loopback-datasource-juggler'); 13 | 14 | /** 15 | * Connectors 16 | */ 17 | 18 | loopback.Connector = require('./lib/connectors/base-connector'); 19 | loopback.Memory = require('./lib/connectors/memory'); 20 | loopback.Mail = require('./lib/connectors/mail'); 21 | loopback.Remote = require('loopback-connector-remote'); 22 | 23 | /** 24 | * Types 25 | */ 26 | 27 | loopback.GeoPoint = require('loopback-datasource-juggler/lib/geo').GeoPoint; 28 | loopback.DateString = require('loopback-datasource-juggler/lib/date-string'); 29 | loopback.ValidationError = loopback.Model.ValidationError; 30 | -------------------------------------------------------------------------------- /test/fixtures/shared-methods/both-configs-set/server/model-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "sources": [ 4 | "loopback/common/models", 5 | "loopback/server/models", 6 | "../common/models", 7 | "./models" 8 | ], 9 | "mixins": [ 10 | "loopback/common/mixins", 11 | "loopback/server/mixins", 12 | "../common/mixins", 13 | "./mixins" 14 | ] 15 | }, 16 | "User": { 17 | "dataSource": "db" 18 | }, 19 | "AccessToken": { 20 | "dataSource": "db", 21 | "public": false 22 | }, 23 | "ACL": { 24 | "dataSource": "db", 25 | "public": false 26 | }, 27 | "RoleMapping": { 28 | "dataSource": "db", 29 | "public": false 30 | }, 31 | "Role": { 32 | "dataSource": "db", 33 | "public": false 34 | }, 35 | "Todo": { 36 | "dataSource": "db", 37 | "public": true, 38 | "options": { 39 | "remoting": { 40 | "sharedMethods": { 41 | "destroyAll": false 42 | } 43 | } 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /test/fixtures/shared-methods/model-config-default-true/server/model-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "sources": [ 4 | "loopback/common/models", 5 | "loopback/server/models", 6 | "../common/models", 7 | "./models" 8 | ], 9 | "mixins": [ 10 | "loopback/common/mixins", 11 | "loopback/server/mixins", 12 | "../common/mixins", 13 | "./mixins" 14 | ] 15 | }, 16 | "User": { 17 | "dataSource": "db" 18 | }, 19 | "AccessToken": { 20 | "dataSource": "db", 21 | "public": false 22 | }, 23 | "ACL": { 24 | "dataSource": "db", 25 | "public": false 26 | }, 27 | "RoleMapping": { 28 | "dataSource": "db", 29 | "public": false 30 | }, 31 | "Role": { 32 | "dataSource": "db", 33 | "public": false 34 | }, 35 | "Todo": { 36 | "dataSource": "db", 37 | "public": true, 38 | "options": { 39 | "remoting": { 40 | "sharedMethods": { 41 | "*": true 42 | } 43 | } 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /test/fixtures/shared-methods/model-config-default-false/server/model-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "sources": [ 4 | "loopback/common/models", 5 | "loopback/server/models", 6 | "../common/models", 7 | "./models" 8 | ], 9 | "mixins": [ 10 | "loopback/common/mixins", 11 | "loopback/server/mixins", 12 | "../common/mixins", 13 | "./mixins" 14 | ] 15 | }, 16 | "User": { 17 | "dataSource": "db" 18 | }, 19 | "AccessToken": { 20 | "dataSource": "db", 21 | "public": false 22 | }, 23 | "ACL": { 24 | "dataSource": "db", 25 | "public": false 26 | }, 27 | "RoleMapping": { 28 | "dataSource": "db", 29 | "public": false 30 | }, 31 | "Role": { 32 | "dataSource": "db", 33 | "public": false 34 | }, 35 | "Todo": { 36 | "dataSource": "db", 37 | "public": true, 38 | "options": { 39 | "remoting": { 40 | "sharedMethods": { 41 | "*": false 42 | } 43 | } 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /test/fixtures/shared-methods/model-config-defined-false/server/model-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "sources": [ 4 | "loopback/common/models", 5 | "loopback/server/models", 6 | "../common/models", 7 | "./models" 8 | ], 9 | "mixins": [ 10 | "loopback/common/mixins", 11 | "loopback/server/mixins", 12 | "../common/mixins", 13 | "./mixins" 14 | ] 15 | }, 16 | "User": { 17 | "dataSource": "db" 18 | }, 19 | "AccessToken": { 20 | "dataSource": "db", 21 | "public": false 22 | }, 23 | "ACL": { 24 | "dataSource": "db", 25 | "public": false 26 | }, 27 | "RoleMapping": { 28 | "dataSource": "db", 29 | "public": false 30 | }, 31 | "Role": { 32 | "dataSource": "db", 33 | "public": false 34 | }, 35 | "Todo": { 36 | "dataSource": "db", 37 | "public": true, 38 | "options": { 39 | "remoting": { 40 | "sharedMethods": { 41 | "find": false 42 | } 43 | } 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /test/fixtures/shared-methods/model-config-defined-true/server/model-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "sources": [ 4 | "loopback/common/models", 5 | "loopback/server/models", 6 | "../common/models", 7 | "./models" 8 | ], 9 | "mixins": [ 10 | "loopback/common/mixins", 11 | "loopback/server/mixins", 12 | "../common/mixins", 13 | "./mixins" 14 | ] 15 | }, 16 | "User": { 17 | "dataSource": "db" 18 | }, 19 | "AccessToken": { 20 | "dataSource": "db", 21 | "public": false 22 | }, 23 | "ACL": { 24 | "dataSource": "db", 25 | "public": false 26 | }, 27 | "RoleMapping": { 28 | "dataSource": "db", 29 | "public": false 30 | }, 31 | "Role": { 32 | "dataSource": "db", 33 | "public": false 34 | }, 35 | "Todo": { 36 | "dataSource": "db", 37 | "public": true, 38 | "options": { 39 | "remoting": { 40 | "sharedMethods": { 41 | "find": true 42 | } 43 | } 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /docs.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "LoopBack Documentation", 3 | "content": [ 4 | "lib/application.js", 5 | "lib/server-app.js", 6 | "lib/loopback.js", 7 | "lib/registry.js", 8 | "lib/access-context.js", 9 | { "title": "Base models", "depth": 2 }, 10 | "lib/model.js", 11 | "lib/persisted-model.js", 12 | { "title": "Middleware", "depth": 2 }, 13 | "server/middleware/favicon.js", 14 | "server/middleware/rest.js", 15 | "server/middleware/static.js", 16 | "server/middleware/status.js", 17 | "server/middleware/token.js", 18 | "server/middleware/url-not-found.js", 19 | { "title": "Built-in models", "depth": 2 }, 20 | "common/models/access-token.js", 21 | "common/models/acl.js", 22 | "common/models/application.js", 23 | "common/models/change.js", 24 | "common/models/email.js", 25 | "common/models/key-value-model.js", 26 | "common/models/role.js", 27 | "common/models/role-mapping.js", 28 | "common/models/scope.js", 29 | "common/models/user.js" 30 | ], 31 | "assets": "/docs/assets" 32 | } 33 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an issue becomes stale 2 | daysUntilStale: 60 3 | # Number of days of inactivity before a stale issue is closed 4 | daysUntilClose: 14 5 | # Issues with these labels will never be considered stale 6 | exemptLabels: 7 | - pinned 8 | - security 9 | - critical 10 | # Label to use when marking an issue as stale 11 | staleLabel: stale 12 | # Comment to post when marking an issue as stale. Set to `false` to disable 13 | markComment: > 14 | This issue has been automatically marked as stale because it has not had 15 | recent activity. It will be closed if no further activity occurs. Thank you 16 | for your contributions. 17 | # Comment to post when closing a stale issue. Set to `false` to disable 18 | closeComment: > 19 | This issue has been closed due to continued inactivity. Thank you for your understanding. 20 | If you believe this to be in error, please contact one of the code owners, 21 | listed in the [`CODEOWNERS`](https://github.com/strongloop/loopback/blob/master/CODEOWNERS) file at the top-level of this repository. 22 | -------------------------------------------------------------------------------- /lib/connectors/memory.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2013,2019. All Rights Reserved. 2 | // Node module: loopback 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | /** 7 | * Expose `Memory`. 8 | */ 9 | 10 | 'use strict'; 11 | module.exports = Memory; 12 | 13 | /** 14 | * Module dependencies. 15 | */ 16 | 17 | const Connector = require('./base-connector'); 18 | const debug = require('debug')('memory'); 19 | const util = require('util'); 20 | const inherits = util.inherits; 21 | const assert = require('assert'); 22 | const JdbMemory = require('loopback-datasource-juggler/lib/connectors/memory'); 23 | 24 | /** 25 | * Create a new `Memory` connector with the given `options`. 26 | * 27 | * @param {Object} options 28 | * @return {Memory} 29 | */ 30 | 31 | function Memory() { 32 | // TODO implement entire memory connector 33 | } 34 | 35 | /** 36 | * Inherit from `DBConnector`. 37 | */ 38 | 39 | inherits(Memory, Connector); 40 | 41 | /** 42 | * JugglingDB Compatibility 43 | */ 44 | 45 | Memory.initialize = JdbMemory.initialize; 46 | -------------------------------------------------------------------------------- /test/fixtures/access-control/common/models/account.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "accountWithReplaceOnPUTtrue", 3 | "plural": "accounts-replacing", 4 | "relations": { 5 | "transactions": { 6 | "model": "transaction", 7 | "type": "hasMany" 8 | }, 9 | "user": { 10 | "model": "user", 11 | "type": "belongsTo", 12 | "foreignKey": "userId" 13 | } 14 | }, 15 | "acls": [ 16 | { 17 | "accessType": "*", 18 | "permission": "DENY", 19 | "principalType": "ROLE", 20 | "principalId": "$everyone" 21 | }, 22 | { 23 | "accessType": "*", 24 | "permission": "ALLOW", 25 | "principalType": "ROLE", 26 | "principalId": "$owner" 27 | }, 28 | { 29 | "permission": "DENY", 30 | "principalType": "ROLE", 31 | "principalId": "$owner", 32 | "property": "deleteById" 33 | }, 34 | { 35 | "accessType": "*", 36 | "permission": "DENY", 37 | "property": "find", 38 | "principalType": "ROLE", 39 | "principalId": "$dummy" 40 | } 41 | ], 42 | "properties": {}, 43 | "replaceOnPUT": true 44 | } -------------------------------------------------------------------------------- /test/fixtures/access-control/server/model-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "sources": [ 4 | "../common/models", 5 | "./models", 6 | "../../../../common/models" 7 | ] 8 | }, 9 | "ACL": { 10 | "dataSource": "db", 11 | "public": false 12 | }, 13 | "RoleMapping": { 14 | "dataSource": "db", 15 | "public": false 16 | }, 17 | "Role": { 18 | "dataSource": "db", 19 | "public": false 20 | }, 21 | "email": { 22 | "dataSource": "mail", 23 | "public": false 24 | }, 25 | "user": { 26 | "dataSource": "db", 27 | "public": true 28 | }, 29 | "accessToken": { 30 | "dataSource": "db", 31 | "public": true 32 | }, 33 | "bank": { 34 | "public": true, 35 | "dataSource": "db" 36 | }, 37 | "accountWithReplaceOnPUTtrue": { 38 | "public": true, 39 | "dataSource": "db" 40 | }, 41 | "accountWithReplaceOnPUTfalse": { 42 | "public": true, 43 | "dataSource": "db" 44 | }, 45 | "transaction": { 46 | "public": true, 47 | "dataSource": "db" 48 | }, 49 | "alert": { 50 | "public": true, 51 | "dataSource": "db" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /test/fixtures/access-control/common/models/accountWithReplaceOnPUTfalse.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "accountWithReplaceOnPUTfalse", 3 | "plural": "accounts-updating", 4 | "relations": { 5 | "transactions": { 6 | "model": "transaction", 7 | "type": "hasMany" 8 | }, 9 | "user": { 10 | "model": "user", 11 | "type": "belongsTo", 12 | "foreignKey": "userId" 13 | } 14 | }, 15 | "acls": [ 16 | { 17 | "accessType": "*", 18 | "permission": "DENY", 19 | "principalType": "ROLE", 20 | "principalId": "$everyone" 21 | }, 22 | { 23 | "accessType": "*", 24 | "permission": "ALLOW", 25 | "principalType": "ROLE", 26 | "principalId": "$owner" 27 | }, 28 | { 29 | "permission": "DENY", 30 | "principalType": "ROLE", 31 | "principalId": "$owner", 32 | "property": "deleteById" 33 | }, 34 | { 35 | "accessType": "*", 36 | "permission": "DENY", 37 | "property": "find", 38 | "principalType": "ROLE", 39 | "principalId": "$dummy" 40 | } 41 | ], 42 | "properties": {}, 43 | "replaceOnPUT": false 44 | } -------------------------------------------------------------------------------- /example/client-server/models.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2014,2019. All Rights Reserved. 2 | // Node module: loopback 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | const loopback = require('../../'); 8 | 9 | const CartItem = exports.CartItem = loopback.PersistedModel.extend('CartItem', { 10 | tax: {type: Number, default: 0.1}, 11 | price: Number, 12 | item: String, 13 | qty: {type: Number, default: 0}, 14 | cartId: Number, 15 | }); 16 | 17 | CartItem.sum = function(cartId, callback) { 18 | this.find({where: {cartId: cartId}}, function(err, items) { 19 | if (err) return callback(err); 20 | const total = items 21 | .map(function(item) { 22 | return item.total(); 23 | }) 24 | .reduce(function(cur, prev) { 25 | return prev + cur; 26 | }, 0); 27 | 28 | callback(null, total); 29 | }); 30 | }; 31 | 32 | CartItem.remoteMethod('sum', 33 | { 34 | accepts: {arg: 'cartId', type: 'number'}, 35 | returns: {arg: 'total', type: 'number'}, 36 | }); 37 | 38 | CartItem.prototype.total = function() { 39 | return this.price * this.qty * (1 + this.tax); 40 | }; 41 | -------------------------------------------------------------------------------- /test/fixtures/user-integration-app/server/model-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "sources": [ 4 | "../common/models", 5 | "./models", 6 | "../../../../common/models" 7 | ] 8 | }, 9 | "Email": { 10 | "dataSource": "mail", 11 | "public": false 12 | }, 13 | "User": { 14 | "dataSource": "db", 15 | "public": true, 16 | "relations": { 17 | "posts": { 18 | "model": "Post", 19 | "type": "hasMany", 20 | "foreignKey": "userId" 21 | } 22 | }, 23 | "acls": [ 24 | { 25 | "permission": "ALLOW", 26 | "principalType": "ROLE", 27 | "principalId": "$owner" 28 | } 29 | ] 30 | }, 31 | "AccessToken": { 32 | "dataSource": "db", 33 | "public": false 34 | }, 35 | "ACL": { 36 | "dataSource": "db", 37 | "public": false 38 | }, 39 | "RoleMapping": { 40 | "dataSource": "db", 41 | "public": false 42 | }, 43 | "Role": { 44 | "dataSource": "db", 45 | "public": false 46 | }, 47 | "myUser": { 48 | "dataSource": "db", 49 | "public": true 50 | }, 51 | "blog": { 52 | "dataSource": "db", 53 | "public": true 54 | }, 55 | "Post": { 56 | "dataSource": "db", 57 | "public": true 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) IBM Corp. 2013,2018. All Rights Reserved. 2 | Node module: loopback 3 | This project is licensed under the MIT License, full text below. 4 | 5 | -------- 6 | 7 | MIT license 8 | 9 | Permission is hereby granted, free of charge, to any person obtaining a copy 10 | of this software and associated documentation files (the "Software"), to deal 11 | in the Software without restriction, including without limitation the rights 12 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | copies of the Software, and to permit persons to whom the Software is 14 | furnished to do so, subject to the following conditions: 15 | 16 | The above copyright notice and this permission notice shall be included in 17 | all copies or substantial portions of the Software. 18 | 19 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 25 | THE SOFTWARE. 26 | -------------------------------------------------------------------------------- /test/e2e/remote-connector.e2e.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2014,2019. All Rights Reserved. 2 | // Node module: loopback 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | const path = require('path'); 8 | const loopback = require('../../'); 9 | const models = require('../fixtures/e2e/models'); 10 | const TestModel = models.TestModel; 11 | const assert = require('assert'); 12 | 13 | describe('RemoteConnector', function() { 14 | before(function() { 15 | // setup the remote connector 16 | const ds = loopback.createDataSource({ 17 | url: 'http://127.0.0.1:3000/api', 18 | connector: loopback.Remote, 19 | }); 20 | TestModel.attachTo(ds); 21 | }); 22 | 23 | it('should be able to call create', function(done) { 24 | TestModel.create({ 25 | foo: 'bar', 26 | }, function(err, inst) { 27 | if (err) return done(err); 28 | 29 | assert(inst.id); 30 | 31 | done(); 32 | }); 33 | }); 34 | 35 | it('should be able to call save', function(done) { 36 | const m = new TestModel({ 37 | foo: 'bar', 38 | }); 39 | m.save(function(err, data) { 40 | if (err) return done(err); 41 | 42 | assert(data.foo === 'bar'); 43 | 44 | done(); 45 | }); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /test/memory.test.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2013,2019. All Rights Reserved. 2 | // Node module: loopback 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | const assert = require('assert'); 8 | const loopback = require('../'); 9 | 10 | describe('Memory Connector', function() { 11 | it('Create a model using the memory connector', function(done) { 12 | // use the built in memory function 13 | // to create a memory data source 14 | let memory = loopback.memory(); 15 | 16 | // or create it using the standard 17 | // data source creation api 18 | memory = loopback.createDataSource({ 19 | connector: loopback.Memory, 20 | }); 21 | 22 | // create a model using the 23 | // memory data source 24 | const properties = { 25 | name: String, 26 | price: Number, 27 | }; 28 | 29 | const Product = memory.createModel('product', properties); 30 | 31 | Product.create([ 32 | {name: 'apple', price: 0.79}, 33 | {name: 'pear', price: 1.29}, 34 | {name: 'orange', price: 0.59}, 35 | ], count); 36 | 37 | function count() { 38 | Product.count(function(err, count) { 39 | assert.equal(count, 3); 40 | 41 | done(); 42 | }); 43 | } 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /lib/current-context.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2016,2019. All Rights Reserved. 2 | // Node module: loopback 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | const g = require('./globalize'); 8 | const juggler = require('loopback-datasource-juggler'); 9 | const remoting = require('strong-remoting'); 10 | 11 | module.exports = function(loopback) { 12 | juggler.getCurrentContext = 13 | remoting.getCurrentContext = 14 | loopback.getCurrentContext = function() { 15 | throw new Error(g.f( 16 | '%s was removed in version 3.0. See %s for more details.', 17 | 'loopback.getCurrentContext()', 18 | 'http://loopback.io/doc/en/lb2/Using-current-context.html', 19 | )); 20 | }; 21 | 22 | loopback.runInContext = function(fn) { 23 | throw new Error(g.f( 24 | '%s was removed in version 3.0. See %s for more details.', 25 | 'loopback.runInContext()', 26 | 'http://loopback.io/doc/en/lb2/Using-current-context.html', 27 | )); 28 | }; 29 | 30 | loopback.createContext = function(scopeName) { 31 | throw new Error(g.f( 32 | '%s was removed in version 3.0. See %s for more details.', 33 | 'loopback.createContext()', 34 | 'http://loopback.io/doc/en/lb2/Using-current-context.html', 35 | )); 36 | }; 37 | }; 38 | -------------------------------------------------------------------------------- /test/remoting-coercion.test.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2014,2019. All Rights Reserved. 2 | // Node module: loopback 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | const assert = require('assert'); 8 | const loopback = require('../'); 9 | const request = require('supertest'); 10 | 11 | describe('remoting coercion', function() { 12 | it('should coerce arguments based on the type', function(done) { 13 | let called = false; 14 | const app = loopback(); 15 | app.use(loopback.rest()); 16 | 17 | const TestModel = app.registry.createModel('TestModel', 18 | {}, 19 | {base: 'Model'}); 20 | app.model(TestModel, {public: true}); 21 | 22 | TestModel.test = function(inst, cb) { 23 | called = true; 24 | assert(inst instanceof TestModel); 25 | assert(inst.foo === 'bar'); 26 | cb(); 27 | }; 28 | TestModel.remoteMethod('test', { 29 | accepts: {arg: 'inst', type: 'TestModel', http: {source: 'body'}}, 30 | http: {path: '/test', verb: 'post'}, 31 | }); 32 | 33 | request(app) 34 | .post('/TestModels/test') 35 | .set('Content-Type', 'application/json') 36 | .send({ 37 | foo: 'bar', 38 | }) 39 | .end(function(err) { 40 | if (err) return done(err); 41 | 42 | assert(called); 43 | 44 | done(); 45 | }); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /test/e2e/replication.e2e.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2014,2019. All Rights Reserved. 2 | // Node module: loopback 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | const path = require('path'); 8 | const loopback = require('../../'); 9 | const models = require('../fixtures/e2e/models'); 10 | const TestModel = models.TestModel; 11 | const LocalTestModel = TestModel.extend('LocalTestModel', {}, { 12 | trackChanges: true, 13 | }); 14 | const assert = require('assert'); 15 | 16 | describe('Replication', function() { 17 | before(function() { 18 | // setup the remote connector 19 | const ds = loopback.createDataSource({ 20 | url: 'http://127.0.0.1:3000/api', 21 | connector: loopback.Remote, 22 | }); 23 | TestModel.attachTo(ds); 24 | const memory = loopback.memory(); 25 | LocalTestModel.attachTo(memory); 26 | }); 27 | 28 | it('should replicate local data to the remote', function(done) { 29 | const RANDOM = Math.random(); 30 | 31 | LocalTestModel.create({ 32 | n: RANDOM, 33 | }, function(err, created) { 34 | LocalTestModel.replicate(0, TestModel, function() { 35 | if (err) return done(err); 36 | 37 | TestModel.findOne({n: RANDOM}, function(err, found) { 38 | assert.equal(created.id, found.id); 39 | 40 | done(); 41 | }); 42 | }); 43 | }); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /lib/connectors/base-connector.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2013,2019. All Rights Reserved. 2 | // Node module: loopback 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | /** 7 | * Expose `Connector`. 8 | */ 9 | 10 | 'use strict'; 11 | module.exports = Connector; 12 | 13 | /** 14 | * Module dependencies. 15 | */ 16 | 17 | const EventEmitter = require('events').EventEmitter; 18 | const debug = require('debug')('connector'); 19 | const util = require('util'); 20 | const inherits = util.inherits; 21 | const assert = require('assert'); 22 | 23 | /** 24 | * Create a new `Connector` with the given `options`. 25 | * 26 | * @param {Object} options 27 | * @return {Connector} 28 | */ 29 | 30 | function Connector(options) { 31 | EventEmitter.apply(this, arguments); 32 | this.options = options; 33 | 34 | debug('created with options', options); 35 | } 36 | 37 | /** 38 | * Inherit from `EventEmitter`. 39 | */ 40 | 41 | inherits(Connector, EventEmitter); 42 | 43 | /*! 44 | * Create an connector instance from a JugglingDB adapter. 45 | */ 46 | 47 | Connector._createJDBAdapter = function(jdbModule) { 48 | const fauxSchema = {}; 49 | jdbModule.initialize(fauxSchema, function() { 50 | // connected 51 | }); 52 | }; 53 | 54 | /*! 55 | * Add default crud operations from a JugglingDB adapter. 56 | */ 57 | 58 | Connector.prototype._addCrudOperationsFromJDBAdapter = function(connector) { 59 | 60 | }; 61 | -------------------------------------------------------------------------------- /test/fixtures/simple-integration-app/server/model-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "sources": [ 4 | "../common/models", 5 | "./models", 6 | "../../../../common/models" 7 | ] 8 | }, 9 | "ACL": { 10 | "dataSource": "db", 11 | "public": false 12 | }, 13 | "RoleMapping": { 14 | "dataSource": "db", 15 | "public": false 16 | }, 17 | "Role": { 18 | "dataSource": "db", 19 | "public": false 20 | }, 21 | "email": { 22 | "dataSource": "mail", 23 | "public": false 24 | }, 25 | "user": { 26 | "dataSource": "db", 27 | "public": true 28 | }, 29 | "accessToken": { 30 | "dataSource": "db", 31 | "public": true 32 | }, 33 | "widget": { 34 | "public": true, 35 | "dataSource": "db" 36 | }, 37 | "store": { 38 | "public": true, 39 | "dataSource": "db" 40 | }, 41 | "storeWithReplaceOnPUTfalse": { 42 | "public": true, 43 | "dataSource": "db" 44 | }, 45 | "storeWithReplaceOnPUTtrue": { 46 | "public": true, 47 | "dataSource": "db" 48 | }, 49 | "physician": { 50 | "dataSource": "db", 51 | "public": true 52 | }, 53 | "patient": { 54 | "dataSource": "db", 55 | "public": true 56 | }, 57 | "appointment": { 58 | "dataSource": "db", 59 | "public": true 60 | }, 61 | "customer": { 62 | "dataSource": "db", 63 | "public": true 64 | }, 65 | "profile": { 66 | "dataSource": "db", 67 | "public": true 68 | }, 69 | "customerforceidfalse": { 70 | "dataSource": "db", 71 | "public": true 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /example/mobile-models/app.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2013,2019. All Rights Reserved. 2 | // Node module: loopback 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | const g = require('../../lib/globalize'); 8 | const models = require('../../lib/models'); 9 | const loopback = require('../../'); 10 | const app = loopback(); 11 | 12 | app.use(loopback.rest()); 13 | 14 | const dataSource = loopback.createDataSource('db', {connector: loopback.Memory}); 15 | 16 | const Application = models.Application(dataSource); 17 | 18 | app.model(Application); 19 | 20 | const data = { 21 | pushSettings: [{ 22 | 'platform': 'apns', 23 | 'apns': { 24 | 'pushOptions': { 25 | 'gateway': 'gateway.sandbox.push.apple.com', 26 | 'cert': 'credentials/apns_cert_dev.pem', 27 | 'key': 'credentials/apns_key_dev.pem', 28 | }, 29 | 30 | 'feedbackOptions': { 31 | 'gateway': 'feedback.sandbox.push.apple.com', 32 | 'cert': 'credentials/apns_cert_dev.pem', 33 | 'key': 'credentials/apns_key_dev.pem', 34 | 'batchFeedback': true, 35 | 'interval': 300, 36 | }, 37 | }, 38 | }], 39 | }; 40 | 41 | Application.create(data, function(err, data) { 42 | g.log('Created: %s', data.toObject()); 43 | }); 44 | 45 | Application.register('rfeng', 'MyApp', {description: g.f('My first mobile application')}, 46 | function(err, result) { 47 | console.log(result.toObject()); 48 | 49 | result.resetKeys(function(err, result) { 50 | console.log(result.toObject()); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/Bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | labels: bug 5 | 6 | --- 7 | 8 | 21 | 22 | ## Steps to reproduce 23 | 24 | 25 | 26 | ## Current Behavior 27 | 28 | 29 | 30 | ## Expected Behavior 31 | 32 | 33 | 34 | ## Link to reproduction sandbox 35 | 36 | 40 | 41 | ## Additional information 42 | 43 | 48 | 49 | ## Related Issues 50 | 51 | 52 | 53 | _See [Reporting Issues](http://loopback.io/doc/en/contrib/Reporting-issues.html) for more tips on writing good issues_ 54 | -------------------------------------------------------------------------------- /common/models/scope.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2014,2019. All Rights Reserved. 2 | // Node module: loopback 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | const assert = require('assert'); 8 | const loopback = require('../../lib/loopback'); 9 | 10 | /** 11 | * Resource owner grants/delegates permissions to client applications 12 | * 13 | * For a protected resource, does the client application have the authorization 14 | * from the resource owner (user or system)? 15 | * 16 | * Scope has many resource access entries 17 | * 18 | * @class Scope 19 | */ 20 | 21 | module.exports = function(Scope) { 22 | Scope.resolveRelatedModels = function() { 23 | if (!this.aclModel) { 24 | const reg = this.registry; 25 | this.aclModel = reg.getModelByType(loopback.ACL); 26 | } 27 | }; 28 | 29 | /** 30 | * Check if the given scope is allowed to access the model/property 31 | * @param {String} scope The scope name 32 | * @param {String} model The model name 33 | * @param {String} property The property/method/relation name 34 | * @param {String} accessType The access type 35 | * @callback {Function} callback 36 | * @param {String|Error} err The error object 37 | * @param {AccessRequest} result The access permission 38 | */ 39 | Scope.checkPermission = function(scope, model, property, accessType, callback) { 40 | this.resolveRelatedModels(); 41 | const aclModel = this.aclModel; 42 | assert(aclModel, 43 | 'ACL model must be defined before Scope.checkPermission is called'); 44 | 45 | this.findOne({where: {name: scope}}, function(err, scope) { 46 | if (err) { 47 | if (callback) callback(err); 48 | } else { 49 | aclModel.checkPermission( 50 | aclModel.SCOPE, scope.id, model, property, accessType, callback, 51 | ); 52 | } 53 | }); 54 | }; 55 | }; 56 | -------------------------------------------------------------------------------- /common/models/email.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2014,2019. All Rights Reserved. 2 | // Node module: loopback 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | const g = require('../../lib/globalize'); 8 | 9 | /** 10 | * Email model. Extends LoopBack base [Model](#model-new-model). 11 | * @property {String} to Email addressee. Required. 12 | * @property {String} from Email sender address. Required. 13 | * @property {String} subject Email subject string. Required. 14 | * @property {String} text Text body of email. 15 | * @property {String} html HTML body of email. 16 | * 17 | * @class Email 18 | * @inherits {Model} 19 | */ 20 | 21 | module.exports = function(Email) { 22 | /** 23 | * Send an email with the given `options`. 24 | * 25 | * Example Options: 26 | * 27 | * ```js 28 | * { 29 | * from: "Fred Foo ", // sender address 30 | * to: "bar@blurdybloop.com, baz@blurdybloop.com", // list of receivers 31 | * subject: "Hello", // Subject line 32 | * text: "Hello world", // plaintext body 33 | * html: "Hello world" // html body 34 | * } 35 | * ``` 36 | * 37 | * See https://github.com/andris9/Nodemailer for other supported options. 38 | * 39 | * @options {Object} options See below 40 | * @prop {String} from Senders's email address 41 | * @prop {String} to List of one or more recipient email addresses (comma-delimited) 42 | * @prop {String} subject Subject line 43 | * @prop {String} text Body text 44 | * @prop {String} html Body HTML (optional) 45 | * @param {Function} callback Called after the e-mail is sent or the sending failed 46 | */ 47 | 48 | Email.send = function() { 49 | throw new Error(g.f('You must connect the {{Email}} Model to a {{Mail}} connector')); 50 | }; 51 | 52 | /** 53 | * A shortcut for Email.send(this). 54 | */ 55 | Email.prototype.send = function() { 56 | throw new Error(g.f('You must connect the {{Email}} Model to a {{Mail}} connector')); 57 | }; 58 | }; 59 | -------------------------------------------------------------------------------- /server/middleware/rest.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2014,2019. All Rights Reserved. 2 | // Node module: loopback 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | /*! 7 | * Module dependencies. 8 | */ 9 | 10 | 'use strict'; 11 | const g = require('../../lib/globalize'); 12 | const loopback = require('../../lib/loopback'); 13 | const async = require('async'); 14 | 15 | /*! 16 | * Export the middleware. 17 | */ 18 | 19 | module.exports = rest; 20 | 21 | /** 22 | * Expose models over REST. 23 | * 24 | * For example: 25 | * ```js 26 | * app.use(loopback.rest()); 27 | * ``` 28 | * For more information, see [Exposing models over a REST API](http://loopback.io/doc/en/lb2/Exposing-models-over-REST.html). 29 | * @header loopback.rest() 30 | */ 31 | 32 | function rest() { 33 | let handlers; // Cached handlers 34 | 35 | return function restApiHandler(req, res, next) { 36 | const app = req.app; 37 | const registry = app.registry; 38 | 39 | if (!handlers) { 40 | handlers = []; 41 | const remotingOptions = app.get('remoting') || {}; 42 | 43 | const contextOptions = remotingOptions.context; 44 | if (contextOptions !== undefined && contextOptions !== false) { 45 | throw new Error(g.f( 46 | '%s was removed in version 3.0. See %s for more details.', 47 | 'remoting.context option', 48 | 'http://loopback.io/doc/en/lb2/Using-current-context.html', 49 | )); 50 | } 51 | 52 | if (app.isAuthEnabled) { 53 | const AccessToken = registry.getModelByType('AccessToken'); 54 | handlers.push(loopback.token({model: AccessToken, app: app})); 55 | } 56 | 57 | handlers.push(function(req, res, next) { 58 | // Need to get an instance of the REST handler per request 59 | return app.handler('rest')(req, res, next); 60 | }); 61 | } 62 | if (handlers.length === 1) { 63 | return handlers[0](req, res, next); 64 | } 65 | async.eachSeries(handlers, function(handler, done) { 66 | handler(req, res, done); 67 | }, next); 68 | }; 69 | } 70 | -------------------------------------------------------------------------------- /test/hidden-properties.test.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2014,2019. All Rights Reserved. 2 | // Node module: loopback 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | const assert = require('assert'); 8 | const loopback = require('../'); 9 | const request = require('supertest'); 10 | 11 | describe('hidden properties', function() { 12 | beforeEach(function(done) { 13 | const app = this.app = loopback(); 14 | const Product = this.Product = loopback.PersistedModel.extend( 15 | 'product', 16 | {}, 17 | {hidden: ['secret']}, 18 | ); 19 | Product.attachTo(loopback.memory()); 20 | 21 | const Category = this.Category = loopback.PersistedModel.extend('category'); 22 | Category.attachTo(loopback.memory()); 23 | Category.hasMany(Product); 24 | 25 | app.model(Product); 26 | app.model(Category); 27 | app.use(loopback.rest()); 28 | 29 | Category.create({ 30 | name: 'my category', 31 | }, function(err, category) { 32 | category.products.create({ 33 | name: 'pencil', 34 | secret: 'a secret', 35 | }, done); 36 | }); 37 | }); 38 | 39 | afterEach(function(done) { 40 | const Product = this.Product; 41 | this.Category.destroyAll(function() { 42 | Product.destroyAll(done); 43 | }); 44 | }); 45 | 46 | it('should hide a property remotely', function(done) { 47 | request(this.app) 48 | .get('/products') 49 | .expect('Content-Type', /json/) 50 | .expect(200) 51 | .end(function(err, res) { 52 | if (err) return done(err); 53 | 54 | const product = res.body[0]; 55 | assert.equal(product.secret, undefined); 56 | 57 | done(); 58 | }); 59 | }); 60 | 61 | it('should hide a property of nested models', function(done) { 62 | const app = this.app; 63 | request(app) 64 | .get('/categories?filter[include]=products') 65 | .expect('Content-Type', /json/) 66 | .expect(200) 67 | .end(function(err, res) { 68 | if (err) return done(err); 69 | 70 | const category = res.body[0]; 71 | const product = category.products[0]; 72 | assert.equal(product.secret, undefined); 73 | 74 | done(); 75 | }); 76 | }); 77 | }); 78 | -------------------------------------------------------------------------------- /test/geo-point.test.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2013,2019. All Rights Reserved. 2 | // Node module: loopback 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | const assert = require('assert'); 8 | const loopback = require('../'); 9 | const GeoPoint = loopback.GeoPoint; 10 | 11 | describe('GeoPoint', function() { 12 | describe('geoPoint.distanceTo(geoPoint, options)', function() { 13 | it('Get the distance to another `GeoPoint`', function() { 14 | const here = new GeoPoint({lat: 10, lng: 10}); 15 | const there = new GeoPoint({lat: 5, lng: 5}); 16 | const distance = here.distanceTo(there, {type: 'meters'}); 17 | 18 | assert.equal(Math.floor(distance), 782777); 19 | }); 20 | }); 21 | 22 | describe('GeoPoint.distanceBetween(a, b, options)', function() { 23 | it('Get the distance between two points', function() { 24 | const here = new GeoPoint({lat: 10, lng: 10}); 25 | const there = new GeoPoint({lat: 5, lng: 5}); 26 | const distance = GeoPoint.distanceBetween(here, there, {type: 'feet'}); 27 | 28 | assert.equal(Math.floor(distance), 2568169); 29 | }); 30 | }); 31 | 32 | describe('GeoPoint()', function() { 33 | it('Create from string', function() { 34 | const point = new GeoPoint('1.234,5.678'); 35 | assert.equal(point.lat, 1.234); 36 | assert.equal(point.lng, 5.678); 37 | const point2 = new GeoPoint('1.222, 5.333'); 38 | assert.equal(point2.lat, 1.222); 39 | assert.equal(point2.lng, 5.333); 40 | const point3 = new GeoPoint('1.333, 5.111'); 41 | assert.equal(point3.lat, 1.333); 42 | assert.equal(point3.lng, 5.111); 43 | }); 44 | it('Serialize as string', function() { 45 | const str = '1.234,5.678'; 46 | const point = new GeoPoint(str); 47 | assert.equal(point.toString(), str); 48 | }); 49 | it('Create from array', function() { 50 | const point = new GeoPoint([5.555, 6.777]); 51 | assert.equal(point.lat, 5.555); 52 | assert.equal(point.lng, 6.777); 53 | }); 54 | it('Create as Model property', function() { 55 | const Model = loopback.createModel('geo-model', { 56 | geo: {type: 'GeoPoint'}, 57 | }); 58 | 59 | const m = new Model({ 60 | geo: '1.222,3.444', 61 | }); 62 | 63 | assert(m.geo instanceof GeoPoint); 64 | assert.equal(m.geo.lat, 1.222); 65 | assert.equal(m.geo.lng, 3.444); 66 | }); 67 | }); 68 | }); 69 | -------------------------------------------------------------------------------- /test/registries.test.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015,2019. All Rights Reserved. 2 | // Node module: loopback 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | const assert = require('assert'); 8 | const expect = require('./helpers/expect'); 9 | const loopback = require('../'); 10 | 11 | describe('Registry', function() { 12 | describe('createModel', function() { 13 | it('should throw error upon extending non-exist base model', function() { 14 | const app = loopback(); 15 | const props = {}; 16 | const opts = {base: 'nonexistent'}; 17 | expect(function() { app.registry.createModel('aModel', props, opts); }) 18 | .to.throw(/model\s`aModel`(.*)unknown\smodel\s`nonexistent`/); 19 | }); 20 | }); 21 | 22 | describe('one per app', function() { 23 | it('should allow two apps to reuse the same model name', function(done) { 24 | const appFoo = loopback(); 25 | const appBar = loopback(); 26 | const modelName = 'MyModel'; 27 | const subModelName = 'Sub' + modelName; 28 | const settings = {base: 'PersistedModel'}; 29 | appFoo.set('perAppRegistries', true); 30 | appBar.set('perAppRegistries', true); 31 | const dsFoo = appFoo.dataSource('dsFoo', {connector: 'memory'}); 32 | const dsBar = appFoo.dataSource('dsBar', {connector: 'memory'}); 33 | 34 | const FooModel = appFoo.registry.createModel(modelName, {}, settings); 35 | appFoo.model(FooModel, {dataSource: dsFoo}); 36 | 37 | const FooSubModel = appFoo.registry.createModel(subModelName, {}, settings); 38 | appFoo.model(FooSubModel, {dataSource: dsFoo}); 39 | 40 | const BarModel = appBar.registry.createModel(modelName, {}, settings); 41 | appBar.model(BarModel, {dataSource: dsBar}); 42 | 43 | const BarSubModel = appBar.registry.createModel(subModelName, {}, settings); 44 | appBar.model(BarSubModel, {dataSource: dsBar}); 45 | 46 | FooModel.hasMany(FooSubModel); 47 | BarModel.hasMany(BarSubModel); 48 | 49 | expect(appFoo.models[modelName]).to.not.equal(appBar.models[modelName]); 50 | 51 | BarModel.create({name: 'bar'}, function(err, bar) { 52 | assert(!err); 53 | bar.subMyModels.create({parent: 'bar'}, function(err) { 54 | assert(!err); 55 | FooSubModel.find(function(err, foos) { 56 | assert(!err); 57 | expect(foos).to.eql([]); 58 | BarSubModel.find(function(err, bars) { 59 | assert(!err); 60 | expect(bars.map(function(f) { 61 | return f.parent; 62 | })).to.eql(['bar']); 63 | 64 | done(); 65 | }); 66 | }); 67 | }); 68 | }); 69 | }); 70 | }); 71 | }); 72 | -------------------------------------------------------------------------------- /common/models/checkpoint.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2014,2019. All Rights Reserved. 2 | // Node module: loopback 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | /** 7 | * Module Dependencies. 8 | */ 9 | 10 | 'use strict'; 11 | const assert = require('assert'); 12 | 13 | /** 14 | * Checkpoint list entry. 15 | * 16 | * @property id {Number} the sequencial identifier of a checkpoint 17 | * @property time {Number} the time when the checkpoint was created 18 | * @property sourceId {String} the source identifier 19 | * 20 | * @class Checkpoint 21 | * @inherits {PersistedModel} 22 | */ 23 | 24 | module.exports = function(Checkpoint) { 25 | // Workaround for https://github.com/strongloop/loopback/issues/292 26 | Checkpoint.definition.rawProperties.time.default = 27 | Checkpoint.definition.properties.time.default = function() { 28 | return new Date(); 29 | }; 30 | 31 | /** 32 | * Get the current checkpoint id 33 | * @callback {Function} callback 34 | * @param {Error} err 35 | * @param {Number} checkpoint The current checkpoint seq 36 | */ 37 | Checkpoint.current = function(cb) { 38 | const Checkpoint = this; 39 | Checkpoint._getSingleton(function(err, cp) { 40 | cb(err, cp.seq); 41 | }); 42 | }; 43 | 44 | Checkpoint._getSingleton = function(cb) { 45 | const query = {limit: 1}; // match all instances, return only one 46 | const initialData = {seq: 1}; 47 | this.findOrCreate(query, initialData, cb); 48 | }; 49 | 50 | /** 51 | * Increase the current checkpoint if it already exists otherwise initialize it 52 | * @callback {Function} callback 53 | * @param {Error} err 54 | * @param {Object} checkpoint The current checkpoint 55 | */ 56 | Checkpoint.bumpLastSeq = function(cb) { 57 | const Checkpoint = this; 58 | Checkpoint._getSingleton(function(err, cp) { 59 | if (err) return cb(err); 60 | const originalSeq = cp.seq; 61 | cp.seq++; 62 | // Update the checkpoint but only if it was not changed under our hands 63 | Checkpoint.updateAll({id: cp.id, seq: originalSeq}, {seq: cp.seq}, function(err, info) { 64 | if (err) return cb(err); 65 | // possible outcomes 66 | // 1) seq was updated to seq+1 - exactly what we wanted! 67 | // 2) somebody else already updated seq to seq+1 and our call was a no-op. 68 | // That should be ok, checkpoints are time based, so we reuse the one created just now 69 | // 3) seq was bumped more than once, so we will be using a value that is behind the latest seq. 70 | // @bajtos is not entirely sure if this is ok, but since it wasn't handled by the current implementation either, 71 | // he thinks we can keep it this way. 72 | cb(null, cp); 73 | }); 74 | }); 75 | }; 76 | }; 77 | -------------------------------------------------------------------------------- /common/models/user.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "User", 3 | "properties": { 4 | "realm": { 5 | "type": "string" 6 | }, 7 | "username": { 8 | "type": "string" 9 | }, 10 | "password": { 11 | "type": "string", 12 | "required": true 13 | }, 14 | "email": { 15 | "type": "string", 16 | "required": true 17 | }, 18 | "emailVerified": "boolean", 19 | "verificationToken": "string" 20 | }, 21 | "options": { 22 | "caseSensitiveEmail": true 23 | }, 24 | "hidden": ["password", "verificationToken"], 25 | "acls": [ 26 | { 27 | "principalType": "ROLE", 28 | "principalId": "$everyone", 29 | "permission": "DENY" 30 | }, 31 | { 32 | "principalType": "ROLE", 33 | "principalId": "$everyone", 34 | "permission": "ALLOW", 35 | "property": "create" 36 | }, 37 | { 38 | "principalType": "ROLE", 39 | "principalId": "$owner", 40 | "permission": "ALLOW", 41 | "property": "deleteById" 42 | }, 43 | { 44 | "principalType": "ROLE", 45 | "principalId": "$everyone", 46 | "permission": "ALLOW", 47 | "property": "login" 48 | }, 49 | { 50 | "principalType": "ROLE", 51 | "principalId": "$everyone", 52 | "permission": "ALLOW", 53 | "property": "logout" 54 | }, 55 | { 56 | "principalType": "ROLE", 57 | "principalId": "$owner", 58 | "permission": "ALLOW", 59 | "property": "findById" 60 | }, 61 | { 62 | "principalType": "ROLE", 63 | "principalId": "$owner", 64 | "permission": "ALLOW", 65 | "property": "patchAttributes" 66 | }, 67 | { 68 | "principalType": "ROLE", 69 | "principalId": "$owner", 70 | "permission": "ALLOW", 71 | "property": "replaceById" 72 | }, 73 | { 74 | "principalType": "ROLE", 75 | "principalId": "$everyone", 76 | "permission": "ALLOW", 77 | "property": "verify", 78 | "accessType": "EXECUTE" 79 | }, 80 | { 81 | "principalType": "ROLE", 82 | "principalId": "$everyone", 83 | "permission": "ALLOW", 84 | "property": "confirm" 85 | }, 86 | { 87 | "principalType": "ROLE", 88 | "principalId": "$everyone", 89 | "permission": "ALLOW", 90 | "property": "resetPassword", 91 | "accessType": "EXECUTE" 92 | }, 93 | { 94 | "principalType": "ROLE", 95 | "principalId": "$authenticated", 96 | "permission": "ALLOW", 97 | "property": "changePassword", 98 | "accessType": "EXECUTE" 99 | }, 100 | { 101 | "principalType": "ROLE", 102 | "principalId": "$authenticated", 103 | "permission": "ALLOW", 104 | "property": "setPassword", 105 | "accessType": "EXECUTE" 106 | } 107 | ], 108 | "relations": { 109 | "accessTokens": { 110 | "type": "hasMany", 111 | "model": "AccessToken", 112 | "foreignKey": "userId", 113 | "options": { 114 | "disableInclude": true 115 | } 116 | } 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /lib/configure-shared-methods.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2017,2019. All Rights Reserved. 2 | // Node module: loopback 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | 8 | const util = require('util'); 9 | const extend = require('util')._extend; 10 | const g = require('./globalize'); 11 | 12 | module.exports = function(modelCtor, remotingConfig, modelConfig) { 13 | const settings = {}; 14 | 15 | // apply config.json settings 16 | const configHasSharedMethodsSettings = remotingConfig && 17 | remotingConfig.sharedMethods && 18 | typeof remotingConfig.sharedMethods === 'object'; 19 | if (configHasSharedMethodsSettings) 20 | util._extend(settings, remotingConfig.sharedMethods); 21 | 22 | // apply model-config.json settings 23 | const options = modelConfig.options; 24 | const modelConfigHasSharedMethodsSettings = options && 25 | options.remoting && 26 | options.remoting.sharedMethods && 27 | typeof options.remoting.sharedMethods === 'object'; 28 | if (modelConfigHasSharedMethodsSettings) 29 | util._extend(settings, options.remoting.sharedMethods); 30 | 31 | // validate setting values 32 | Object.keys(settings).forEach(function(setting) { 33 | const settingValue = settings[setting]; 34 | const settingValueType = typeof settingValue; 35 | if (settingValueType !== 'boolean') 36 | throw new TypeError(g.f('Expected boolean, got %s', settingValueType)); 37 | }); 38 | 39 | // set sharedMethod.shared using the merged settings 40 | const sharedMethods = modelCtor.sharedClass.methods({includeDisabled: true}); 41 | 42 | // re-map glob style values to regular expressions 43 | const tests = Object 44 | .keys(settings) 45 | .filter(function(setting) { 46 | return settings.hasOwnProperty(setting) && setting.indexOf('*') >= 0; 47 | }) 48 | .map(function(setting) { 49 | // Turn * into an testable regexp string 50 | const glob = escapeRegExp(setting).replace(/\*/g, '(.)*'); 51 | return {regex: new RegExp(glob), setting: settings[setting]}; 52 | }) || []; 53 | sharedMethods.forEach(function(sharedMethod) { 54 | // use the specific setting if it exists 55 | const methodName = sharedMethod.isStatic ? sharedMethod.name : 'prototype.' + sharedMethod.name; 56 | const hasSpecificSetting = settings.hasOwnProperty(methodName); 57 | if (hasSpecificSetting) { 58 | if (settings[methodName] === false) { 59 | sharedMethod.sharedClass.disableMethodByName(methodName); 60 | } else { 61 | sharedMethod.shared = true; 62 | } 63 | } else { 64 | tests.forEach(function(glob) { 65 | if (glob.regex.test(methodName)) { 66 | if (glob.setting === false) { 67 | sharedMethod.sharedClass.disableMethodByName(methodName); 68 | } else { 69 | sharedMethod.shared = true; 70 | } 71 | } 72 | }); 73 | } 74 | }); 75 | }; 76 | 77 | // Sanitize all RegExp reserved characters except * for pattern gobbing 78 | function escapeRegExp(str) { 79 | return str.replace(/[\-\[\]\/\{\}\(\)\+\?\.\\\^\$\|]/g, '\\$&'); 80 | } 81 | -------------------------------------------------------------------------------- /lib/builtin-models.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2014,2019. All Rights Reserved. 2 | // Node module: loopback 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | 8 | const assert = require('assert'); 9 | 10 | module.exports = function(registry) { 11 | // NOTE(bajtos) we must use static require() due to browserify limitations 12 | 13 | registry.KeyValueModel = createModel( 14 | require('../common/models/key-value-model.json'), 15 | require('../common/models/key-value-model.js'), 16 | ); 17 | 18 | registry.Email = createModel( 19 | require('../common/models/email.json'), 20 | require('../common/models/email.js'), 21 | ); 22 | 23 | registry.Application = createModel( 24 | require('../common/models/application.json'), 25 | require('../common/models/application.js'), 26 | ); 27 | 28 | registry.AccessToken = createModel( 29 | require('../common/models/access-token.json'), 30 | require('../common/models/access-token.js'), 31 | ); 32 | 33 | registry.User = createModel( 34 | require('../common/models/user.json'), 35 | require('../common/models/user.js'), 36 | ); 37 | 38 | registry.RoleMapping = createModel( 39 | require('../common/models/role-mapping.json'), 40 | require('../common/models/role-mapping.js'), 41 | ); 42 | 43 | registry.Role = createModel( 44 | require('../common/models/role.json'), 45 | require('../common/models/role.js'), 46 | ); 47 | 48 | registry.ACL = createModel( 49 | require('../common/models/acl.json'), 50 | require('../common/models/acl.js'), 51 | ); 52 | 53 | registry.Scope = createModel( 54 | require('../common/models/scope.json'), 55 | require('../common/models/scope.js'), 56 | ); 57 | 58 | registry.Change = createModel( 59 | require('../common/models/change.json'), 60 | require('../common/models/change.js'), 61 | ); 62 | 63 | registry.Checkpoint = createModel( 64 | require('../common/models/checkpoint.json'), 65 | require('../common/models/checkpoint.js'), 66 | ); 67 | 68 | function createModel(definitionJson, customizeFn) { 69 | // Clone the JSON definition to allow applications 70 | // to modify model settings while not affecting 71 | // settings of new models created in the local registry 72 | // of another app. 73 | // This is needed because require() always returns the same 74 | // object instance it loaded during the first call. 75 | definitionJson = cloneDeepJson(definitionJson); 76 | 77 | const Model = registry.createModel(definitionJson); 78 | customizeFn(Model); 79 | return Model; 80 | } 81 | }; 82 | 83 | // Because we are cloning objects created by JSON.parse, 84 | // the cloning algorithm can stay much simpler than a general-purpose 85 | // "cloneDeep" e.g. from lodash. 86 | function cloneDeepJson(obj) { 87 | const result = Array.isArray(obj) ? [] : {}; 88 | assert.equal(Object.getPrototypeOf(result), Object.getPrototypeOf(obj)); 89 | for (const key in obj) { 90 | const value = obj[key]; 91 | if (typeof value === 'object') { 92 | result[key] = cloneDeepJson(value); 93 | } else { 94 | result[key] = value; 95 | } 96 | } 97 | return result; 98 | } 99 | -------------------------------------------------------------------------------- /test/email.test.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2013,2019. All Rights Reserved. 2 | // Node module: loopback 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | const loopback = require('../'); 8 | let MyEmail; 9 | const assert = require('assert'); 10 | const MailConnector = require('../lib/connectors/mail'); 11 | 12 | describe('Email connector', function() { 13 | it('should set up SMTP', function() { 14 | const connector = new MailConnector({transports: [ 15 | {type: 'smtp', service: 'gmail'}, 16 | ]}); 17 | assert(connector.transportForName('smtp')); 18 | }); 19 | 20 | it('should set up DIRECT', function() { 21 | const connector = new MailConnector({transports: [ 22 | {type: 'direct', name: 'localhost'}, 23 | ]}); 24 | assert(connector.transportForName('direct')); 25 | }); 26 | 27 | it('should set up STUB', function() { 28 | const connector = new MailConnector({transports: [ 29 | {type: 'stub', service: 'gmail'}, 30 | ]}); 31 | assert(connector.transportForName('stub')); 32 | }); 33 | 34 | it('should set up a single transport for SMTP', function() { 35 | const connector = new MailConnector({transport: 36 | {type: 'smtp', service: 'gmail'}, 37 | }); 38 | 39 | assert(connector.transportForName('smtp')); 40 | }); 41 | 42 | it('should set up a aliased transport for SMTP', function() { 43 | const connector = new MailConnector({transport: 44 | {type: 'smtp', service: 'ses-us-east-1', alias: 'ses-smtp'}, 45 | }); 46 | 47 | assert(connector.transportForName('ses-smtp')); 48 | }); 49 | }); 50 | 51 | describe('Email and SMTP', function() { 52 | beforeEach(function() { 53 | MyEmail = loopback.Email.extend('my-email'); 54 | const ds = loopback.createDataSource('email', { 55 | connector: loopback.Mail, 56 | transports: [{type: 'STUB'}], 57 | }); 58 | MyEmail.attachTo(ds); 59 | }); 60 | 61 | it('should have a send method', function() { 62 | assert(typeof MyEmail.send === 'function'); 63 | assert(typeof MyEmail.prototype.send === 'function'); 64 | }); 65 | 66 | describe('MyEmail', function() { 67 | it('MyEmail.send(options, callback)', function(done) { 68 | const options = { 69 | to: 'to@to.com', 70 | from: 'from@from.com', 71 | subject: 'subject', 72 | text: 'text', 73 | html: '

html

', 74 | }; 75 | 76 | MyEmail.send(options, function(err, mail) { 77 | assert(!err); 78 | assert(mail.response); 79 | assert(mail.envelope); 80 | assert(mail.messageId); 81 | 82 | done(err); 83 | }); 84 | }); 85 | 86 | it('myEmail.send(callback)', function(done) { 87 | const message = new MyEmail({ 88 | to: 'to@to.com', 89 | from: 'from@from.com', 90 | subject: 'subject', 91 | text: 'text', 92 | html: '

html

', 93 | }); 94 | 95 | message.send(function(err, mail) { 96 | assert(mail.response); 97 | assert(mail.envelope); 98 | assert(mail.messageId); 99 | 100 | done(err); 101 | }); 102 | }); 103 | }); 104 | }); 105 | -------------------------------------------------------------------------------- /test/integration.test.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2014,2019. All Rights Reserved. 2 | // Node module: loopback 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | const expect = require('./helpers/expect'); 8 | const loopback = require('../'); 9 | const net = require('net'); 10 | 11 | describe('loopback application', function() { 12 | it('pauses request stream during authentication', function(done) { 13 | // This test reproduces the issue reported in 14 | // https://github.com/strongloop/loopback-storage-service/issues/7 15 | const app = loopback(); 16 | setupAppWithStreamingMethod(); 17 | 18 | app.listen(0, function() { 19 | sendHttpRequestInOnePacket( 20 | this.address().port, 21 | 'POST /streamers/read HTTP/1.0\n' + 22 | 'Content-Length: 1\n' + 23 | 'Content-Type: application/x-custom-octet-stream\n' + 24 | '\n' + 25 | 'X', 26 | function(err, res) { 27 | if (err) return done(err); 28 | 29 | expect(res).to.match(/\nX$/); 30 | 31 | done(); 32 | }, 33 | ); 34 | }); 35 | 36 | function setupAppWithStreamingMethod() { 37 | app.dataSource('db', { 38 | connector: loopback.Memory, 39 | }); 40 | const db = app.datasources.db; 41 | 42 | loopback.User.attachTo(db); 43 | loopback.AccessToken.attachTo(db); 44 | loopback.Role.attachTo(db); 45 | loopback.ACL.attachTo(db); 46 | loopback.User.hasMany(loopback.AccessToken, {as: 'accessTokens'}); 47 | 48 | const Streamer = app.registry.createModel('Streamer'); 49 | app.model(Streamer, {dataSource: 'db'}); 50 | Streamer.read = function(req, res, cb) { 51 | let body = new Buffer(0); 52 | req.on('data', function(chunk) { 53 | body += chunk; 54 | }); 55 | req.on('end', function() { 56 | res.end(body.toString()); 57 | // we must not call the callback here 58 | // because it will attempt to add response headers 59 | }); 60 | req.once('error', function(err) { 61 | cb(err); 62 | }); 63 | }; 64 | loopback.remoteMethod(Streamer.read, { 65 | http: {method: 'post'}, 66 | accepts: [ 67 | {arg: 'req', type: 'Object', http: {source: 'req'}}, 68 | {arg: 'res', type: 'Object', http: {source: 'res'}}, 69 | ], 70 | }); 71 | 72 | app.enableAuth(); 73 | app.use(loopback.token({model: app.models.accessToken})); 74 | app.use(loopback.rest()); 75 | } 76 | 77 | function sendHttpRequestInOnePacket(port, reqString, cb) { 78 | const socket = net.createConnection(port); 79 | let response = new Buffer(0); 80 | 81 | socket.on('data', function(chunk) { 82 | response += chunk; 83 | }); 84 | socket.on('end', function() { 85 | callCb(null, response.toString()); 86 | }); 87 | socket.once('error', function(err) { 88 | callCb(err); 89 | }); 90 | 91 | socket.write(reqString.replace(/\n/g, '\r\n')); 92 | 93 | function callCb(err, res) { 94 | if (!cb) return; 95 | cb(err, res); 96 | cb = null; 97 | } 98 | } 99 | }); 100 | }); 101 | -------------------------------------------------------------------------------- /test/remote-connector.test.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2014,2019. All Rights Reserved. 2 | // Node module: loopback 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | const assert = require('assert'); 8 | const loopback = require('../'); 9 | const defineModelTestsWithDataSource = require('./util/model-tests'); 10 | 11 | describe('RemoteConnector', function() { 12 | this.timeout(10000); 13 | 14 | let remoteApp, remote; 15 | 16 | defineModelTestsWithDataSource({ 17 | beforeEach: function(done) { 18 | const test = this; 19 | remoteApp = loopback(); 20 | remoteApp.set('remoting', { 21 | errorHandler: {debug: true, log: false}, 22 | types: {warnWhenOverridingType: false}, 23 | }); 24 | remoteApp.use(loopback.rest()); 25 | remoteApp.listen(0, function() { 26 | test.dataSource = loopback.createDataSource({ 27 | host: 'localhost', 28 | port: remoteApp.get('port'), 29 | connector: loopback.Remote, 30 | }); 31 | 32 | done(); 33 | }); 34 | }, 35 | 36 | // We are defining the model attached to the remote connector datasource, 37 | // therefore change tracking must be disabled, only the remote API for 38 | // replication should be present 39 | trackChanges: false, 40 | enableRemoteReplication: true, 41 | 42 | onDefine: function(Model) { 43 | const ServerModel = Model.extend('Server' + Model.modelName, {}, { 44 | plural: Model.pluralModelName, 45 | // This is the model running on the server & attached to a real 46 | // datasource, that's the place where to keep track of changes 47 | trackChanges: true, 48 | }); 49 | ServerModel.attachTo(loopback.createDataSource({ 50 | connector: loopback.Memory, 51 | })); 52 | remoteApp.model(ServerModel); 53 | }, 54 | }); 55 | 56 | beforeEach(function(done) { 57 | const test = this; 58 | remoteApp = this.remoteApp = loopback(); 59 | remoteApp.set('remoting', { 60 | types: {warnWhenOverridingType: false}, 61 | }); 62 | remoteApp.use(loopback.rest()); 63 | const ServerModel = this.ServerModel = loopback.PersistedModel.extend('TestModel'); 64 | 65 | remoteApp.model(ServerModel); 66 | 67 | remoteApp.listen(0, function() { 68 | test.remote = loopback.createDataSource({ 69 | host: 'localhost', 70 | port: remoteApp.get('port'), 71 | connector: loopback.Remote, 72 | }); 73 | 74 | done(); 75 | }); 76 | }); 77 | 78 | it('should support the save method', function(done) { 79 | let calledServerCreate = false; 80 | const RemoteModel = loopback.PersistedModel.extend('TestModel'); 81 | RemoteModel.attachTo(this.remote); 82 | 83 | const ServerModel = this.ServerModel; 84 | 85 | ServerModel.create = function(data, options, cb) { 86 | calledServerCreate = true; 87 | data.id = 1; 88 | cb(null, data); 89 | }; 90 | 91 | ServerModel.setupRemoting(); 92 | 93 | const m = new RemoteModel({foo: 'bar'}); 94 | m.save(function(err, inst) { 95 | if (err) return done(err); 96 | 97 | assert(inst instanceof RemoteModel); 98 | assert(calledServerCreate); 99 | 100 | done(); 101 | }); 102 | }); 103 | }); 104 | -------------------------------------------------------------------------------- /docs/api-explorer-details.md: -------------------------------------------------------------------------------- 1 | 4 | 5 | # REST API specs 6 | 7 | LoopBack API Explorer is built on top of the popular 8 | [Swagger Framework](https://github.com/wordnik/swagger-core/wiki). There are two 9 | components involved. 10 | 11 | 1. LoopBack builds up formal specifications of the REST APIs using the knowledge of 12 | model definitions, JavaScript method declarations, and remote mappings. The 13 | specifications are served over the following endpoints. 14 | 15 | 2. The wonderful Web UI is brought you by [Swagger UI](https://github.com/strongloop/swagger-ui). 16 | Swagger UI is a collection of HTML, Javascript, and CSS assets that dynamically 17 | generate beautiful documentation and sandbox from the REST API specifications. 18 | 19 | ## Resource Listing 20 | The first part is a listing of the REST APIs. 21 | 22 | - http://localhost:3000/swagger/resources 23 | 24 | ```javascript 25 | { 26 | "swaggerVersion": "1.1", 27 | "basePath": "http://localhost:3000", 28 | "apis": [ 29 | { 30 | "path": "/swagger/ammo" 31 | }, 32 | { 33 | "path": "/swagger/customers" 34 | }, 35 | { 36 | "path": "/swagger/inventory" 37 | }, 38 | { 39 | "path": "/swagger/locations" 40 | }, 41 | { 42 | "path": "/swagger/weapons" 43 | } 44 | ] 45 | } 46 | ``` 47 | 48 | ## Resource Operations 49 | The second part describes all operations of a given model. 50 | 51 | - http://localhost:3000/swagger/locations 52 | 53 | ```javascript 54 | { 55 | "swaggerVersion": "1.1", 56 | "basePath": "http://localhost:3000", 57 | "apis": [ 58 | { 59 | "path": "/locations", 60 | "operations": [ 61 | { 62 | "httpMethod": "POST", 63 | "nickname": "locations_create", 64 | "responseClass": "object", 65 | "parameters": [ 66 | { 67 | "paramType": "body", 68 | "name": "data", 69 | "description": "Model instance data", 70 | "dataType": "object", 71 | "required": false, 72 | "allowMultiple": false 73 | } 74 | ], 75 | "errorResponses": [], 76 | "summary": "Create a new instance of the model and persist it into the data source", 77 | "notes": "" 78 | } 79 | ] 80 | }, 81 | ... 82 | { 83 | "path": "/locations/{id}", 84 | "operations": [ 85 | { 86 | "httpMethod": "GET", 87 | "nickname": "locations_findById", 88 | "responseClass": "any", 89 | "parameters": [ 90 | { 91 | "paramType": "path", 92 | "name": "id", 93 | "description": "Model id", 94 | "dataType": "any", 95 | "required": true, 96 | "allowMultiple": false 97 | } 98 | ], 99 | "errorResponses": [], 100 | "summary": "Find a model instance by id from the data source", 101 | "notes": "" 102 | } 103 | ] 104 | }, 105 | ... 106 | ] 107 | } 108 | ``` 109 | -------------------------------------------------------------------------------- /common/models/application.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Application", 3 | "properties": { 4 | "id": { 5 | "type": "string", 6 | "id": true 7 | }, 8 | "realm": { 9 | "type": "string" 10 | }, 11 | "name": { 12 | "type": "string", 13 | "required": true 14 | }, 15 | "description": "string", 16 | "icon": { 17 | "type": "string", 18 | "description": "The icon image url" 19 | }, 20 | 21 | "owner": { 22 | "type": "string", 23 | "description": "The user id of the developer who registers the application" 24 | }, 25 | "collaborators": { 26 | "type": ["string"], 27 | "description": "A list of users ids who have permissions to work on this app" 28 | }, 29 | 30 | "email": "string", 31 | "emailVerified": "boolean", 32 | 33 | "url": { 34 | "type": "string", 35 | "description": "The application URL for OAuth 2.0" 36 | }, 37 | "callbackUrls": { 38 | "type": ["string"], 39 | "description": "OAuth 2.0 code/token callback URLs" 40 | }, 41 | "permissions": { 42 | "type": ["string"], 43 | "description": "A list of permissions required by the application" 44 | }, 45 | 46 | "clientKey": "string", 47 | "javaScriptKey": "string", 48 | "restApiKey": "string", 49 | "windowsKey": "string", 50 | "masterKey": "string", 51 | 52 | "pushSettings": { 53 | "apns": { 54 | "production": { 55 | "type": "boolean", 56 | "description": [ 57 | "Production or development mode. It denotes what default APNS", 58 | "servers to be used to send notifications.", 59 | "See API documentation for more details." 60 | ] 61 | }, 62 | 63 | "certData": { 64 | "type": "string", 65 | "description": "The certificate data loaded from the cert.pem file" 66 | }, 67 | "keyData": { 68 | "type": "string", 69 | "description": "The key data loaded from the key.pem file" 70 | }, 71 | 72 | "pushOptions": { 73 | "type": { 74 | "gateway": "string", 75 | "port": "number" 76 | } 77 | }, 78 | 79 | "feedbackOptions": { 80 | "type": { 81 | "gateway": "string", 82 | "port": "number", 83 | "batchFeedback": "boolean", 84 | "interval": "number" 85 | } 86 | } 87 | }, 88 | 89 | "gcm": { 90 | "serverApiKey": "string" 91 | } 92 | }, 93 | 94 | "authenticationEnabled": { 95 | "type": "boolean", 96 | "default": true 97 | }, 98 | "anonymousAllowed": { 99 | "type": "boolean", 100 | "default": true 101 | }, 102 | "authenticationSchemes": [ 103 | { 104 | "scheme": { 105 | "type": "string", 106 | "description": "See the API docs for the list of supported values." 107 | }, 108 | "credential": { 109 | "type": "object", 110 | "description": "Scheme-specific credentials" 111 | } 112 | } 113 | ], 114 | 115 | "status": { 116 | "type": "string", 117 | "default": "sandbox", 118 | "description": "Status of the application, production/sandbox/disabled" 119 | }, 120 | 121 | "created": { 122 | "type": "date", 123 | "defaultFn": "now" 124 | }, 125 | "modified": { 126 | "type": "date", 127 | "defaultFn": "now" 128 | } 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /common/models/role-mapping.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2014,2019. All Rights Reserved. 2 | // Node module: loopback 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | const loopback = require('../../lib/loopback'); 8 | const utils = require('../../lib/utils'); 9 | 10 | /** 11 | * The `RoleMapping` model extends from the built in `loopback.Model` type. 12 | * 13 | * @property {String} id Generated ID. 14 | * @property {String} name Name of the role. 15 | * @property {String} Description Text description. 16 | * 17 | * @class RoleMapping 18 | * @inherits {PersistedModel} 19 | */ 20 | 21 | module.exports = function(RoleMapping) { 22 | // Principal types 23 | RoleMapping.USER = 'USER'; 24 | RoleMapping.APP = RoleMapping.APPLICATION = 'APP'; 25 | RoleMapping.ROLE = 'ROLE'; 26 | 27 | RoleMapping.resolveRelatedModels = function() { 28 | if (!this.userModel) { 29 | const reg = this.registry; 30 | this.roleModel = reg.getModelByType('Role'); 31 | this.userModel = reg.getModelByType('User'); 32 | this.applicationModel = reg.getModelByType('Application'); 33 | } 34 | }; 35 | 36 | /** 37 | * Get the application principal 38 | * @callback {Function} callback 39 | * @param {Error} err 40 | * @param {Application} application 41 | */ 42 | RoleMapping.prototype.application = function(callback) { 43 | callback = callback || utils.createPromiseCallback(); 44 | this.constructor.resolveRelatedModels(); 45 | 46 | if (this.principalType === RoleMapping.APPLICATION) { 47 | const applicationModel = this.constructor.applicationModel; 48 | applicationModel.findById(this.principalId, callback); 49 | } else { 50 | process.nextTick(function() { 51 | callback(null, null); 52 | }); 53 | } 54 | return callback.promise; 55 | }; 56 | 57 | /** 58 | * Get the user principal 59 | * @callback {Function} callback 60 | * @param {Error} err 61 | * @param {User} user 62 | */ 63 | RoleMapping.prototype.user = function(callback) { 64 | callback = callback || utils.createPromiseCallback(); 65 | this.constructor.resolveRelatedModels(); 66 | let userModel; 67 | 68 | if (this.principalType === RoleMapping.USER) { 69 | userModel = this.constructor.userModel; 70 | userModel.findById(this.principalId, callback); 71 | return callback.promise; 72 | } 73 | 74 | // try resolving a user model that matches principalType 75 | userModel = this.constructor.registry.findModel(this.principalType); 76 | if (userModel) { 77 | userModel.findById(this.principalId, callback); 78 | } else { 79 | process.nextTick(function() { 80 | callback(null, null); 81 | }); 82 | } 83 | return callback.promise; 84 | }; 85 | 86 | /** 87 | * Get the child role principal 88 | * @callback {Function} callback 89 | * @param {Error} err 90 | * @param {User} childUser 91 | */ 92 | RoleMapping.prototype.childRole = function(callback) { 93 | callback = callback || utils.createPromiseCallback(); 94 | this.constructor.resolveRelatedModels(); 95 | 96 | if (this.principalType === RoleMapping.ROLE) { 97 | const roleModel = this.constructor.roleModel; 98 | roleModel.findById(this.principalId, callback); 99 | } else { 100 | process.nextTick(function() { 101 | callback(null, null); 102 | }); 103 | } 104 | return callback.promise; 105 | }; 106 | }; 107 | -------------------------------------------------------------------------------- /test/checkpoint.test.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2014,2019. All Rights Reserved. 2 | // Node module: loopback 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | const async = require('async'); 8 | const loopback = require('../'); 9 | const expect = require('./helpers/expect'); 10 | 11 | const Checkpoint = loopback.Checkpoint.extend('TestCheckpoint'); 12 | 13 | describe('Checkpoint', function() { 14 | describe('bumpLastSeq() and current()', function() { 15 | beforeEach(function() { 16 | const memory = loopback.createDataSource({ 17 | connector: loopback.Memory, 18 | }); 19 | Checkpoint.attachTo(memory); 20 | }); 21 | 22 | it('returns the highest `seq` value', function(done) { 23 | async.series([ 24 | Checkpoint.bumpLastSeq.bind(Checkpoint), 25 | Checkpoint.bumpLastSeq.bind(Checkpoint), 26 | function(next) { 27 | Checkpoint.current(function(err, seq) { 28 | if (err) next(err); 29 | 30 | expect(seq).to.equal(3); 31 | 32 | next(); 33 | }); 34 | }, 35 | ], done); 36 | }); 37 | 38 | it('Should be no race condition for current() when calling in parallel', function(done) { 39 | async.parallel([ 40 | function(next) { Checkpoint.current(next); }, 41 | function(next) { Checkpoint.current(next); }, 42 | ], function(err, list) { 43 | if (err) return done(err); 44 | 45 | Checkpoint.find(function(err, data) { 46 | if (err) return done(err); 47 | 48 | expect(data).to.have.length(1); 49 | 50 | done(); 51 | }); 52 | }); 53 | }); 54 | 55 | it('Should be no race condition for bumpLastSeq() when calling in parallel', function(done) { 56 | async.parallel([ 57 | function(next) { Checkpoint.bumpLastSeq(next); }, 58 | function(next) { Checkpoint.bumpLastSeq(next); }, 59 | ], function(err, list) { 60 | if (err) return done(err); 61 | 62 | Checkpoint.find(function(err, data) { 63 | if (err) return done(err); 64 | // The invariant "we have at most 1 checkpoint instance" is preserved 65 | // even when multiple calls are made in parallel 66 | expect(data).to.have.length(1); 67 | // There is a race condition here, we could end up with both 2 or 3 as the "seq". 68 | // The current implementation of the memory connector always yields 2 though. 69 | expect(data[0].seq).to.equal(2); 70 | // In this particular case, since the new last seq is always 2, both results 71 | // should be 2. 72 | expect(list.map(function(it) { return it.seq; })) 73 | .to.eql([2, 2]); 74 | 75 | done(); 76 | }); 77 | }); 78 | }); 79 | 80 | it('Checkpoint.current() for non existing checkpoint should initialize checkpoint', 81 | function(done) { 82 | Checkpoint.current(function(err, seq) { 83 | expect(seq).to.equal(1); 84 | 85 | done(err); 86 | }); 87 | }); 88 | 89 | it('bumpLastSeq() works when singleton instance does not exists yet', function(done) { 90 | Checkpoint.bumpLastSeq(function(err, cp) { 91 | // We expect `seq` to be 2 since `checkpoint` does not exist and 92 | // `bumpLastSeq` for the first time not only initializes it to one, 93 | // but also increments the initialized value by one. 94 | expect(cp.seq).to.equal(2); 95 | 96 | done(err); 97 | }); 98 | }); 99 | }); 100 | }); 101 | -------------------------------------------------------------------------------- /common/models/README.md: -------------------------------------------------------------------------------- 1 | # Application 2 | 3 | Application model represents the metadata for a client application that has its 4 | own identity and associated configuration with the LoopBack server. 5 | 6 | ## Each application has the following basic properties: 7 | 8 | * id: Automatically generated id 9 | * name: Name of the application (required) 10 | * description: Description of the application (optional) 11 | * icon: URL of the icon 12 | * status: Status of the application, such as production/sandbox/disabled 13 | * created: Timestamp of the record being created 14 | * modified: Timestamp of the record being modified 15 | 16 | ## An application has the following properties linking to users: 17 | 18 | * owner: The user id of the developer who registers the application 19 | * collaborators: A array of users ids who have permissions to work on this app 20 | 21 | ## oAuth 2.0 settings 22 | 23 | * url: The application url 24 | * callbackUrls: An array of preregistered callback urls for oAuth 2.0 25 | * permissions: An array of oAuth 2.0 scopes that can be requested by the application 26 | 27 | ## Security keys 28 | 29 | The following keys are automatically generated by the application creation 30 | process. They can be reset upon request. 31 | 32 | * clientKey: Secret for mobile clients 33 | * javaScriptKey: Secret for JavaScript clients 34 | * restApiKey: Secret for REST APIs 35 | * windowsKey: Secret for Windows applications 36 | * masterKey: Secret for REST APIS. It bypasses model level permissions 37 | 38 | ## Push notification settings 39 | 40 | The application can be configured to support multiple methods of push notifications. 41 | 42 | * pushSettings 43 | 44 | 45 | pushSettings: { 46 | apns: { 47 | certData: config.apnsCertData, 48 | keyData: config.apnsKeyData, 49 | production: false, // Development mode 50 | pushOptions: { 51 | // Extra options can go here for APN 52 | }, 53 | feedbackOptions: { 54 | batchFeedback: true, 55 | interval: 300 56 | } 57 | }, 58 | gcm: { 59 | serverApiKey: config.gcmServerApiKey 60 | } 61 | } 62 | 63 | 64 | ## Authentication schemes 65 | 66 | * authenticationEnabled 67 | * anonymousAllowed 68 | * authenticationSchemes 69 | 70 | ### Authentication scheme settings 71 | 72 | * scheme: Name of the authentication scheme, such as local, facebook, google, 73 | twitter, linkedin, github 74 | * credential: Scheme-specific credentials 75 | 76 | ## APIs for Application model 77 | 78 | In addition to the CRUD methods, the Application model also has the following 79 | apis: 80 | 81 | ### Register a new application 82 | 83 | You can register a new application by providing the owner user id, application 84 | name, and other properties in the options object. 85 | 86 | Application.register('rfeng', 'MyApp1', 87 | {description: 'My first loopback application'}, 88 | function (err, result) { 89 | var app = result; 90 | ... 91 | }); 92 | 93 | ### Reset keys 94 | 95 | You can reset keys for a given application by id. 96 | 97 | Application.resetKeys(appId, function (err, result) { 98 | var app = result; 99 | ... 100 | }); 101 | 102 | ### Authenticate by appId and key 103 | 104 | You can authenticate an application by id and one of the keys. If successful, 105 | it calls back with the key name in the result argument. Otherwise, the 106 | keyName is null. 107 | 108 | Application.authenticate(appId, clientKey, function (err, keyName) { 109 | assert.equal(keyName, 'clientKey'); 110 | ... 111 | }); 112 | 113 | 114 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "loopback", 3 | "version": "3.28.0", 4 | "description": "LoopBack: Open Source Framework for Node.js", 5 | "homepage": "http://loopback.io", 6 | "keywords": [ 7 | "web", 8 | "restful", 9 | "rest", 10 | "api", 11 | "express", 12 | "restify", 13 | "koa", 14 | "auth", 15 | "security", 16 | "oracle", 17 | "mysql", 18 | "nosql", 19 | "mongo", 20 | "mongodb", 21 | "sqlserver", 22 | "mssql", 23 | "postgres", 24 | "postgresql", 25 | "soap", 26 | "StrongLoop", 27 | "framework", 28 | "mobile", 29 | "mBaaS" 30 | ], 31 | "scripts": { 32 | "lint": "grunt eslint", 33 | "coverage": "nyc report --reporter=text-lcov | coveralls", 34 | "test": "nyc grunt mocha-and-karma" 35 | }, 36 | "engines": { 37 | "node": ">=8" 38 | }, 39 | "dependencies": { 40 | "async": "^2.0.1", 41 | "bcryptjs": "^2.1.0", 42 | "bluebird": "^3.1.1", 43 | "body-parser": "^1.12.0", 44 | "canonical-json": "0.0.4", 45 | "debug": "^2.1.2", 46 | "depd": "^1.0.0", 47 | "ejs": "^2.3.1", 48 | "express": "^4.14.0", 49 | "inflection": "^1.6.0", 50 | "isemail": "^3.2.0", 51 | "loopback-connector-remote": "^3.0.0", 52 | "loopback-datasource-juggler": "^3.28.0", 53 | "loopback-filters": "^1.0.0", 54 | "loopback-phase": "^3.0.0", 55 | "nodemailer": "^6.4.16", 56 | "nodemailer-direct-transport": "^3.3.2", 57 | "nodemailer-stub-transport": "^1.1.0", 58 | "serve-favicon": "^2.2.0", 59 | "stable": "^0.1.5", 60 | "strong-globalize": "^4.1.1", 61 | "strong-remoting": "^3.11.0", 62 | "uid2": "0.0.3", 63 | "underscore.string": "^3.3.5" 64 | }, 65 | "devDependencies": { 66 | "browserify": "^16.5.0", 67 | "chai": "^4.2.0", 68 | "cookie-parser": "^1.3.4", 69 | "coveralls": "^3.0.2", 70 | "dirty-chai": "^2.0.1", 71 | "eslint": "^6.5.1", 72 | "eslint-config-loopback": "^13.1.0", 73 | "express-session": "^1.14.0", 74 | "grunt": "^1.0.1", 75 | "grunt-browserify": "^5.0.0", 76 | "grunt-cli": "^1.2.0", 77 | "grunt-contrib-uglify": "^4.0.1", 78 | "grunt-contrib-watch": "^1.0.0", 79 | "grunt-eslint": "^22.0.0", 80 | "grunt-karma": "^3.0.2", 81 | "grunt-mocha-test": "^0.13.3", 82 | "is-docker": "^2.0.0", 83 | "karma": "^4.1.0", 84 | "karma-browserify": "^6.0.0", 85 | "karma-chrome-launcher": "^3.1.0", 86 | "karma-es6-shim": "^1.0.0", 87 | "karma-firefox-launcher": "^1.0.0", 88 | "karma-html2js-preprocessor": "^1.0.0", 89 | "karma-junit-reporter": "^1.2.0", 90 | "karma-mocha": "^1.1.1", 91 | "karma-script-launcher": "^1.0.0", 92 | "loopback-boot": "^2.7.0", 93 | "loopback-context": "^1.0.0", 94 | "mocha": "^6.2.1", 95 | "nyc": "^14.1.1", 96 | "sinon": "^7.5.0", 97 | "sinon-chai": "^3.2.0", 98 | "strong-error-handler": "^3.0.0", 99 | "strong-task-emitter": "^0.0.8", 100 | "supertest": "^4.0.2", 101 | "which": "^2.0.1" 102 | }, 103 | "repository": { 104 | "type": "git", 105 | "url": "https://github.com/strongloop/loopback" 106 | }, 107 | "browser": { 108 | "express": "./lib/browser-express.js", 109 | "./lib/server-app.js": "./lib/browser-express.js", 110 | "connect": false, 111 | "nodemailer": false, 112 | "supertest": false, 113 | "depd": "loopback-datasource-juggler/lib/browser.depd.js", 114 | "bcrypt": false 115 | }, 116 | "config": { 117 | "ci": { 118 | "debug": "*,-mocha:*,-eslint:*" 119 | } 120 | }, 121 | "copyright.owner": "IBM Corp.", 122 | "license": "MIT", 123 | "author": "IBM Corp.", 124 | "ci": { 125 | "downstreamIgnoreList": [ 126 | "bluemix-service-broker", 127 | "gateway-director-bluemix", 128 | "plan-manager" 129 | ] 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /test/role-mapping.test.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2017,2019. All Rights Reserved. 2 | // Node module: loopback 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | const expect = require('./helpers/expect'); 8 | const loopback = require('../'); 9 | const Promise = require('bluebird'); 10 | 11 | describe('role-mapping model', function() { 12 | this.timeout(10000); 13 | 14 | let app, oneUser, anApp, aRole; 15 | const models = {}; 16 | 17 | beforeEach(function() { 18 | app = loopback({localRegistry: true, loadBuiltinModels: true}); 19 | app.dataSource('db', {connector: 'memory'}); 20 | 21 | // setup models 22 | ['User', 'Role', 'RoleMapping', 'Application'].map(setupModel); 23 | 24 | // create generic instances 25 | return Promise.all([ 26 | models.User.create({ 27 | username: 'oneUser', 28 | email: 'user@email.com', 29 | password: 'password', 30 | }), 31 | models.Application.create({name: 'anApp'}), 32 | models.Role.create({name: 'aRole'}), 33 | ]) 34 | .spread(function(u, a, r) { 35 | oneUser = u; 36 | anApp = a; 37 | aRole = r; 38 | }); 39 | 40 | // helper 41 | function setupModel(modelName) { 42 | const model = app.registry.getModel(modelName); 43 | app.model(model, {dataSource: 'db'}); 44 | models[modelName] = model; 45 | } 46 | }); 47 | 48 | it('supports .user() with a callback', function(done) { 49 | models.RoleMapping.create( 50 | {principalType: 'USER', principalId: oneUser.id}, 51 | function(err, mapping) { 52 | if (err) done(err); 53 | mapping.user(function(err, user) { 54 | if (err) done(err); 55 | expect(user.id).to.equal(oneUser.id); 56 | done(); 57 | }); 58 | }, 59 | ); 60 | }); 61 | 62 | it('supports .user() returning a promise', function() { 63 | return models.RoleMapping.create({principalType: 'USER', principalId: oneUser.id}) 64 | .then(function(mapping) { 65 | return mapping.user(); 66 | }) 67 | .then(function(user) { 68 | expect(user.id).to.equal(oneUser.id); 69 | }); 70 | }); 71 | 72 | it('supports .application() with a callback', function(done) { 73 | models.RoleMapping.create( 74 | {principalType: 'APP', principalId: anApp.id}, 75 | function(err, mapping) { 76 | if (err) done(err); 77 | mapping.application(function(err, app) { 78 | if (err) done(err); 79 | expect(app.id).to.equal(anApp.id); 80 | done(); 81 | }); 82 | }, 83 | ); 84 | }); 85 | 86 | it('supports .application() returning a promise', function() { 87 | return models.RoleMapping.create({principalType: 'APP', principalId: anApp.id}) 88 | .then(function(mapping) { 89 | return mapping.application(); 90 | }) 91 | .then(function(app) { 92 | expect(app.id).to.equal(anApp.id); 93 | }); 94 | }); 95 | 96 | it('supports .childRole() with a callback', function(done) { 97 | models.RoleMapping.create( 98 | {principalType: 'ROLE', principalId: aRole.id}, 99 | function(err, mapping) { 100 | if (err) done(err); 101 | mapping.childRole(function(err, role) { 102 | if (err) done(err); 103 | expect(role.id).to.equal(aRole.id); 104 | done(); 105 | }); 106 | }, 107 | ); 108 | }); 109 | 110 | it('supports .childRole() returning a promise', function() { 111 | return models.RoleMapping.create({principalType: 'ROLE', principalId: aRole.id}) 112 | .then(function(mapping) { 113 | return mapping.childRole(); 114 | }) 115 | .then(function(role) { 116 | expect(role.id).to.equal(aRole.id); 117 | }); 118 | }); 119 | }); 120 | -------------------------------------------------------------------------------- /example/replication/app.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2014,2019. All Rights Reserved. 2 | // Node module: loopback 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | const loopback = require('../../'); 8 | const app = loopback(); 9 | const db = app.dataSource('db', {connector: 'memory'}); 10 | const Color = app.registry.createModel('color', {}, {trackChanges: true}); 11 | app.model(Color, {dataSource: 'db'}); 12 | const Color2 = app.registry.createModel('color2', {}, {trackChanges: true}); 13 | app.model(Color2, {dataSource: 'db'}); 14 | const target = Color2; 15 | const source = Color; 16 | const SPEED = process.env.SPEED || 100; 17 | let conflicts; 18 | 19 | const steps = [ 20 | 21 | createSomeInitialSourceData, 22 | 23 | replicateSourceToTarget, 24 | list.bind(this, source, 'current SOURCE data'), 25 | list.bind(this, target, 'current TARGET data'), 26 | 27 | updateSomeTargetData, 28 | 29 | replicateSourceToTarget, 30 | list.bind(this, source, 'current SOURCE data '), 31 | list.bind(this, target, 'current TARGET data (includes conflicting update)'), 32 | 33 | updateSomeSourceDataCausingAConflict, 34 | 35 | replicateSourceToTarget, 36 | list.bind(this, source, 'current SOURCE data (now has a conflict)'), 37 | list.bind(this, target, 'current TARGET data (includes conflicting update)'), 38 | 39 | resolveAllConflicts, 40 | 41 | replicateSourceToTarget, 42 | list.bind(this, source, 'current SOURCE data (conflict resolved)'), 43 | list.bind(this, target, 'current TARGET data (conflict resolved)'), 44 | 45 | createMoreSourceData, 46 | 47 | replicateSourceToTarget, 48 | list.bind(this, source, 'current SOURCE data'), 49 | list.bind(this, target, 'current TARGET data'), 50 | 51 | createEvenMoreSourceData, 52 | 53 | replicateSourceToTarget, 54 | list.bind(this, source, 'current SOURCE data'), 55 | list.bind(this, target, 'current TARGET data'), 56 | 57 | deleteAllSourceData, 58 | 59 | replicateSourceToTarget, 60 | list.bind(this, source, 'current SOURCE data (empty)'), 61 | list.bind(this, target, 'current TARGET data (empty)'), 62 | 63 | createSomeNewSourceData, 64 | 65 | replicateSourceToTarget, 66 | list.bind(this, source, 'current SOURCE data'), 67 | list.bind(this, target, 'current TARGET data'), 68 | ]; 69 | 70 | run(steps); 71 | 72 | function createSomeInitialSourceData() { 73 | Color.create([ 74 | {name: 'red'}, 75 | {name: 'blue'}, 76 | {name: 'green'}, 77 | ]); 78 | } 79 | 80 | function replicateSourceToTarget() { 81 | Color.replicate(0, Color2, {}, function(err, replicationConflicts) { 82 | conflicts = replicationConflicts; 83 | }); 84 | } 85 | 86 | function resolveAllConflicts() { 87 | if (conflicts.length) { 88 | conflicts.forEach(function(conflict) { 89 | conflict.resolve(); 90 | }); 91 | } 92 | } 93 | 94 | function updateSomeTargetData() { 95 | Color2.findById(1, function(err, color) { 96 | color.name = 'conflict'; 97 | color.save(); 98 | }); 99 | } 100 | 101 | function createMoreSourceData() { 102 | Color.create({name: 'orange'}); 103 | } 104 | 105 | function createEvenMoreSourceData() { 106 | Color.create({name: 'black'}); 107 | } 108 | 109 | function updateSomeSourceDataCausingAConflict() { 110 | Color.findById(1, function(err, color) { 111 | color.name = 'red!!!!'; 112 | color.save(); 113 | }); 114 | } 115 | 116 | function deleteAllSourceData() { 117 | Color.destroyAll(); 118 | } 119 | 120 | function createSomeNewSourceData() { 121 | Color.create([ 122 | {name: 'violet'}, 123 | {name: 'amber'}, 124 | {name: 'olive'}, 125 | ]); 126 | } 127 | 128 | function list(model, msg) { 129 | console.log(msg); 130 | model.find(function(err, items) { 131 | items.forEach(function(item) { 132 | console.log(' -', item.name); 133 | }); 134 | console.log(); 135 | }); 136 | } 137 | 138 | function run(steps) { 139 | setInterval(function() { 140 | const step = steps.shift(); 141 | if (step) { 142 | console.log(step.name); 143 | step(); 144 | } 145 | }, SPEED); 146 | } 147 | -------------------------------------------------------------------------------- /test/utils.test.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2016,2019. All Rights Reserved. 2 | // Node module: loopback 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | 8 | const utils = require('../lib/utils'); 9 | const assert = require('assert'); 10 | 11 | describe('Utils', function() { 12 | describe('uploadInChunks', function() { 13 | it('calls process function for each chunk', function(done) { 14 | const largeArray = ['item1', 'item2', 'item3']; 15 | const calls = []; 16 | 17 | utils.uploadInChunks(largeArray, 1, function processFunction(array, cb) { 18 | calls.push(array); 19 | cb(); 20 | }, function finished(err) { 21 | if (err) return done(err); 22 | assert.deepEqual(calls, [['item1'], ['item2'], ['item3']]); 23 | done(); 24 | }); 25 | }); 26 | 27 | it('calls process function only once when array is smaller than chunk size', function(done) { 28 | const largeArray = ['item1', 'item2']; 29 | const calls = []; 30 | 31 | utils.uploadInChunks(largeArray, 3, function processFunction(array, cb) { 32 | calls.push(array); 33 | cb(); 34 | }, function finished(err) { 35 | if (err) return done(err); 36 | assert.deepEqual(calls, [['item1', 'item2']]); 37 | done(); 38 | }); 39 | }); 40 | 41 | it('concats results from each call to the process function', function(done) { 42 | const largeArray = ['item1', 'item2', 'item3', 'item4']; 43 | 44 | utils.uploadInChunks(largeArray, 2, function processFunction(array, cb) { 45 | cb(null, array); 46 | }, function finished(err, results) { 47 | if (err) return done(err); 48 | assert.deepEqual(results, ['item1', 'item2', 'item3', 'item4']); 49 | done(); 50 | }); 51 | }); 52 | }); 53 | 54 | describe('downloadInChunks', function() { 55 | let largeArray, calls, chunkSize, skip; 56 | 57 | beforeEach(function() { 58 | largeArray = ['item1', 'item2', 'item3']; 59 | calls = []; 60 | chunkSize = 2; 61 | skip = 0; 62 | }); 63 | 64 | function processFunction(filter, cb) { 65 | calls.push(Object.assign({}, filter)); 66 | const results = []; 67 | 68 | for (let i = 0; i < chunkSize; i++) { 69 | if (largeArray[skip + i]) { 70 | results.push(largeArray[skip + i]); 71 | } 72 | } 73 | 74 | skip += chunkSize; 75 | cb(null, results); 76 | } 77 | 78 | it('calls process function with the correct filter', function(done) { 79 | const expectedFilters = [{skip: 0, limit: chunkSize}, {skip: chunkSize, limit: chunkSize}]; 80 | utils.downloadInChunks({}, chunkSize, processFunction, function finished(err) { 81 | if (err) return done(err); 82 | assert.deepEqual(calls, expectedFilters); 83 | done(); 84 | }); 85 | }); 86 | 87 | it('concats the results of all calls of the process function', function(done) { 88 | utils.downloadInChunks({}, chunkSize, processFunction, function finished(err, results) { 89 | if (err) return done(err); 90 | assert.deepEqual(results, largeArray); 91 | done(); 92 | }); 93 | }); 94 | }); 95 | 96 | describe('concatResults', function() { 97 | it('concats regular arrays', function() { 98 | const array1 = ['item1', 'item2']; 99 | const array2 = ['item3', 'item4']; 100 | 101 | const concatResults = utils.concatResults(array1, array2); 102 | assert.deepEqual(concatResults, ['item1', 'item2', 'item3', 'item4']); 103 | }); 104 | 105 | it('concats objects containing arrays', function() { 106 | const object1 = {deltas: [{change: 'change 1'}], conflict: []}; 107 | const object2 = {deltas: [{change: 'change 2'}], conflict: [{conflict: 'conflict 1'}]}; 108 | const expectedResults = { 109 | deltas: [{change: 'change 1'}, {change: 'change 2'}], 110 | conflict: [{conflict: 'conflict 1'}], 111 | }; 112 | 113 | const concatResults = utils.concatResults(object1, object2); 114 | assert.deepEqual(concatResults, expectedResults); 115 | }); 116 | }); 117 | }); 118 | -------------------------------------------------------------------------------- /test/authorization-scopes.test.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2017,2018. All Rights Reserved. 2 | // Node module: loopback 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | 8 | const loopback = require('../'); 9 | const supertest = require('supertest'); 10 | const strongErrorHandler = require('strong-error-handler'); 11 | const loggers = require('./helpers/error-loggers'); 12 | 13 | const logAllServerErrors = loggers.logAllServerErrors; 14 | const logServerErrorsOtherThan = loggers.logServerErrorsOtherThan; 15 | 16 | describe('Authorization scopes', () => { 17 | const CUSTOM_SCOPE = 'read:custom'; 18 | 19 | let app, request, User, testUser, regularToken, scopedToken; 20 | beforeEach(givenAppAndRequest); 21 | beforeEach(givenRemoteMethodWithCustomScope); 22 | beforeEach(givenUser); 23 | beforeEach(givenDefaultToken); 24 | beforeEach(givenScopedToken); 25 | 26 | it('denies regular token to invoke custom-scoped method', () => { 27 | logServerErrorsOtherThan(401, app); 28 | return request.get('/users/scoped') 29 | .set('Authorization', regularToken.id) 30 | .expect(401); 31 | }); 32 | 33 | it('allows regular tokens to invoke default-scoped method', () => { 34 | logAllServerErrors(app); 35 | return request.get('/users/' + testUser.id) 36 | .set('Authorization', regularToken.id) 37 | .expect(200); 38 | }); 39 | 40 | it('allows scoped token to invoke custom-scoped method', () => { 41 | logAllServerErrors(app); 42 | return request.get('/users/scoped') 43 | .set('Authorization', scopedToken.id) 44 | .expect(204); 45 | }); 46 | 47 | it('denies scoped token to invoke default-scoped method', () => { 48 | logServerErrorsOtherThan(401, app); 49 | return request.get('/users/' + testUser.id) 50 | .set('Authorization', scopedToken.id) 51 | .expect(401); 52 | }); 53 | 54 | describe('token granted both default and custom scope', () => { 55 | beforeEach('given token with default and custom scope', 56 | () => givenScopedToken(['DEFAULT', CUSTOM_SCOPE])); 57 | beforeEach(() => logAllServerErrors(app)); 58 | 59 | it('allows invocation of default-scoped method', () => { 60 | return request.get('/users/' + testUser.id) 61 | .set('Authorization', scopedToken.id) 62 | .expect(200); 63 | }); 64 | 65 | it('allows invocation of custom-scoped method', () => { 66 | return request.get('/users/scoped') 67 | .set('Authorization', scopedToken.id) 68 | .expect(204); 69 | }); 70 | }); 71 | 72 | it('allows invocation when at least one method scope is matched', () => { 73 | givenRemoteMethodWithCustomScope(['read', 'write']); 74 | return givenScopedToken(['read', 'execute']).then(() => { 75 | return request.get('/users/scoped') 76 | .set('Authorization', scopedToken.id) 77 | .expect(204); 78 | }); 79 | }); 80 | 81 | function givenAppAndRequest() { 82 | app = loopback({localRegistry: true, loadBuiltinModels: true}); 83 | app.set('remoting', {rest: {handleErrors: false}}); 84 | app.dataSource('db', {connector: 'memory'}); 85 | app.enableAuth({dataSource: 'db'}); 86 | request = supertest(app); 87 | 88 | app.use(loopback.rest()); 89 | 90 | User = app.models.User; 91 | } 92 | 93 | function givenRemoteMethodWithCustomScope() { 94 | // Delete any previously registered instance of the method "scoped" 95 | User.sharedClass._methods = User.sharedClass._methods 96 | .filter(m => m.name !== 'scoped'); 97 | 98 | const accessScopes = arguments[0] || [CUSTOM_SCOPE]; 99 | User.scoped = function(cb) { cb(); }; 100 | User.remoteMethod('scoped', { 101 | accessScopes, 102 | http: {verb: 'GET', path: '/scoped'}, 103 | }); 104 | User.settings.acls.push({ 105 | principalType: 'ROLE', 106 | principalId: '$authenticated', 107 | permission: 'ALLOW', 108 | property: 'scoped', 109 | accessType: 'EXECUTE', 110 | }); 111 | } 112 | 113 | function givenUser() { 114 | return User.create({email: 'test@example.com', password: 'pass'}) 115 | .then(u => testUser = u); 116 | } 117 | 118 | function givenDefaultToken() { 119 | return testUser.createAccessToken(60) 120 | .then(t => regularToken = t); 121 | } 122 | 123 | function givenScopedToken() { 124 | const scopes = arguments[0] || [CUSTOM_SCOPE]; 125 | return testUser.accessTokens.create({ttl: 60, scopes}) 126 | .then(t => scopedToken = t); 127 | } 128 | }); 129 | -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015,2019. All Rights Reserved. 2 | // Node module: loopback 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | 8 | exports.createPromiseCallback = createPromiseCallback; 9 | exports.uploadInChunks = uploadInChunks; 10 | exports.downloadInChunks = downloadInChunks; 11 | exports.concatResults = concatResults; 12 | 13 | const Promise = require('bluebird'); 14 | const async = require('async'); 15 | 16 | function createPromiseCallback() { 17 | let cb; 18 | const promise = new Promise(function(resolve, reject) { 19 | cb = function(err, data) { 20 | if (err) return reject(err); 21 | return resolve(data); 22 | }; 23 | }); 24 | cb.promise = promise; 25 | return cb; 26 | } 27 | 28 | function throwPromiseNotDefined() { 29 | throw new Error( 30 | 'Your Node runtime does support ES6 Promises. ' + 31 | 'Set "global.Promise" to your preferred implementation of promises.', 32 | ); 33 | } 34 | 35 | /** 36 | * Divide an async call with large array into multiple calls using smaller chunks 37 | * @param {Array} largeArray - the large array to be chunked 38 | * @param {Number} chunkSize - size of each chunks 39 | * @param {Function} processFunction - the function to be called multiple times 40 | * @param {Function} cb - the callback 41 | */ 42 | function uploadInChunks(largeArray, chunkSize, processFunction, cb) { 43 | const chunkArrays = []; 44 | 45 | if (!chunkSize || chunkSize < 1 || largeArray.length <= chunkSize) { 46 | // if chunking not required 47 | processFunction(largeArray, cb); 48 | } else { 49 | // copying so that the largeArray object does not get affected during splice 50 | const copyOfLargeArray = [].concat(largeArray); 51 | 52 | // chunking to smaller arrays 53 | while (copyOfLargeArray.length > 0) { 54 | chunkArrays.push(copyOfLargeArray.splice(0, chunkSize)); 55 | } 56 | 57 | const tasks = chunkArrays.map(function(chunkArray) { 58 | return function(previousResults, chunkCallback) { 59 | const lastArg = arguments[arguments.length - 1]; 60 | 61 | if (typeof lastArg === 'function') { 62 | chunkCallback = lastArg; 63 | } 64 | 65 | processFunction(chunkArray, function(err, results) { 66 | if (err) { 67 | return chunkCallback(err); 68 | } 69 | 70 | // if this is the first async waterfall call or if previous results was not defined 71 | if (typeof previousResults === 'function' || typeof previousResults === 'undefined' || 72 | previousResults === null) { 73 | previousResults = results; 74 | } else if (results) { 75 | previousResults = concatResults(previousResults, results); 76 | } 77 | 78 | chunkCallback(err, previousResults); 79 | }); 80 | }; 81 | }); 82 | 83 | async.waterfall(tasks, cb); 84 | } 85 | } 86 | 87 | /** 88 | * Page async download calls 89 | * @param {Object} filter - filter object used for the async call 90 | * @param {Number} chunkSize - size of each chunks 91 | * @param {Function} processFunction - the function to be called multiple times 92 | * @param {Function} cb - the callback 93 | */ 94 | function downloadInChunks(filter, chunkSize, processFunction, cb) { 95 | let results = []; 96 | filter = filter ? JSON.parse(JSON.stringify(filter)) : {}; 97 | 98 | if (!chunkSize || chunkSize < 1) { 99 | // if chunking not required 100 | processFunction(filter, cb); 101 | } else { 102 | filter.skip = 0; 103 | filter.limit = chunkSize; 104 | 105 | processFunction(JSON.parse(JSON.stringify(filter)), pageAndConcatResults); 106 | } 107 | 108 | function pageAndConcatResults(err, pagedResults) { 109 | if (err) { 110 | return cb(err); 111 | } else { 112 | results = concatResults(results, pagedResults); 113 | if (pagedResults.length >= chunkSize) { 114 | filter.skip += pagedResults.length; 115 | processFunction(JSON.parse(JSON.stringify(filter)), pageAndConcatResults); 116 | } else { 117 | cb(null, results); 118 | } 119 | } 120 | } 121 | } 122 | 123 | /** 124 | * Concat current results into previous results 125 | * Assumption made here that the previous results and current results are homogeneous 126 | * @param {Object|Array} previousResults 127 | * @param {Object|Array} currentResults 128 | */ 129 | function concatResults(previousResults, currentResults) { 130 | if (Array.isArray(currentResults)) { 131 | previousResults = previousResults.concat(currentResults); 132 | } else if (typeof currentResults === 'object') { 133 | Object.keys(currentResults).forEach(function(key) { 134 | previousResults[key] = concatResults(previousResults[key], currentResults[key]); 135 | }); 136 | } else { 137 | previousResults = currentResults; 138 | } 139 | 140 | return previousResults; 141 | } 142 | -------------------------------------------------------------------------------- /test/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2014,2019. All Rights Reserved. 2 | // Node module: loopback 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | 8 | const isDocker = require('is-docker'); 9 | const which = require('which'); 10 | 11 | // Karma configuration 12 | // http://karma-runner.github.io/0.12/config/configuration-file.html 13 | 14 | module.exports = function(config) { 15 | // see https://github.com/docker/for-linux/issues/496 16 | const disableChromeSandbox = isDocker() && !process.env.TRAVIS; 17 | if (disableChromeSandbox) { 18 | console.log('!! Disabling Chrome sandbox to support un-privileged Docker !!'); 19 | } 20 | 21 | const hasChromium = 22 | which.sync('chromium-browser', {nothrow: true}) || 23 | which.sync('chromium', {nothrow: true}); 24 | 25 | config.set({ 26 | customLaunchers: { 27 | ChromeDocker: { 28 | // cis-jenkins build server does not provide Chrome, only Chromium 29 | base: hasChromium ? 'ChromiumHeadless' : 'ChromeHeadless', 30 | // We must disable the Chrome sandbox when running Chrome inside Docker 31 | // (Chrome's sandbox needs more permissions than Docker allows by default) 32 | // See https://github.com/docker/for-linux/issues/496 33 | flags: disableChromeSandbox ? ['--no-sandbox'] : [], 34 | }, 35 | }, 36 | 37 | // enable / disable watching file and executing tests whenever any file changes 38 | autoWatch: true, 39 | 40 | // base path, that will be used to resolve files and exclude 41 | basePath: '../', 42 | 43 | // testing framework to use (jasmine/mocha/qunit/...) 44 | frameworks: ['es6-shim', 'browserify', 'mocha'], 45 | 46 | // list of files / patterns to load in the browser 47 | files: [ 48 | 'test/loopback.test.js', 49 | 'test/model.test.js', 50 | // [rfeng] Browserified common/models/application.js 51 | // (crypto.randomBytes()) is not compatible with phantomjs. Skip 52 | // the karma test for now. 53 | // 'test/model.application.test.js', 54 | 'test/geo-point.test.js', 55 | 'test/replication.test.js', 56 | 'test/change.test.js', 57 | 'test/checkpoint.test.js', 58 | 'test/app.test.js', 59 | ], 60 | 61 | // list of files / patterns to exclude 62 | exclude: [ 63 | ], 64 | 65 | // test results reporter to use 66 | // possible values: 'dots', 'progress', 'junit', 'growl', 'coverage' 67 | reporters: ['dots'], 68 | 69 | // web server port 70 | port: 9876, 71 | 72 | // cli runner port 73 | runnerPort: 9100, 74 | 75 | // Start these browsers, currently available: 76 | // - Chrome 77 | // - ChromeCanary 78 | // - Firefox 79 | // - Opera 80 | // - Safari (only Mac) 81 | // - PhantomJS 82 | // - IE (only Windows) 83 | browsers: [ 84 | 'Chrome', 85 | ], 86 | 87 | // Which plugins to enable 88 | plugins: [ 89 | 'karma-browserify', 90 | 'karma-es6-shim', 91 | 'karma-mocha', 92 | 'karma-chrome-launcher', 93 | 'karma-junit-reporter', 94 | ], 95 | 96 | // If browser does not capture in given timeout [ms], kill it 97 | captureTimeout: 60000, 98 | 99 | // to avoid DISCONNECTED messages 100 | browserDisconnectTimeout: 10000, // default 2000 101 | browserDisconnectTolerance: 1, // default 0 102 | browserNoActivityTimeout: 60000, // default 10000 103 | 104 | // Continuous Integration mode 105 | // if true, it capture browsers, run tests and exit 106 | singleRun: false, 107 | 108 | colors: true, 109 | 110 | // level of logging 111 | // possible values: LOG_DISABLE || LOG_ERROR || LOG_WARN || LOG_INFO || LOG_DEBUG 112 | logLevel: config.LOG_INFO, 113 | 114 | // Uncomment the following lines if you are using grunt's server to run the tests 115 | // proxies: { 116 | // '/': 'http://localhost:9000/' 117 | // }, 118 | // URL root prevent conflicts with the site root 119 | // urlRoot: '_karma_' 120 | 121 | // Browserify config (all optional) 122 | browserify: { 123 | // extensions: ['.coffee'], 124 | ignore: [ 125 | 'nodemailer', 126 | 'passport', 127 | 'passport-local', 128 | 'superagent', 129 | 'supertest', 130 | ], 131 | packageFilter: function(pkg, dir) { 132 | // async@3 (used e.g. by loopback-connector) is specifying custom 133 | // browserify config, in particular it wants to apply transformation 134 | // `babelify`. We don't have `babelify` installed because we are 135 | // testing using latest Chrome and thus don't need any transpilation. 136 | // Let's remove the browserify config from the package and force 137 | // browserify to use our config instead. 138 | if (pkg.name === 'async') { 139 | delete pkg.browserify; 140 | } 141 | return pkg; 142 | }, 143 | debug: true, 144 | // noParse: ['jquery'], 145 | watch: true, 146 | }, 147 | 148 | // Add browserify to preprocessors 149 | preprocessors: {'test/**/*.js': ['browserify']}, 150 | }); 151 | }; 152 | -------------------------------------------------------------------------------- /test/change-stream.test.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015,2019. All Rights Reserved. 2 | // Node module: loopback 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | const expect = require('./helpers/expect'); 8 | const sinon = require('sinon'); 9 | const loopback = require('../'); 10 | 11 | describe('PersistedModel.createChangeStream()', function() { 12 | describe('configured to source changes locally', function() { 13 | before(function() { 14 | const test = this; 15 | const app = loopback({localRegistry: true}); 16 | const ds = app.dataSource('ds', {connector: 'memory'}); 17 | const Score = app.registry.createModel('Score'); 18 | this.Score = app.model(Score, { 19 | dataSource: 'ds', 20 | changeDataSource: false, // use only local observers 21 | }); 22 | }); 23 | 24 | afterEach(verifyObserversRemoval); 25 | 26 | it('should detect create', function(done) { 27 | const Score = this.Score; 28 | 29 | Score.createChangeStream(function(err, changes) { 30 | changes.on('data', function(change) { 31 | expect(change.type).to.equal('create'); 32 | changes.destroy(); 33 | done(); 34 | }); 35 | 36 | Score.create({team: 'foo'}); 37 | }); 38 | }); 39 | 40 | it('should detect update', function(done) { 41 | const Score = this.Score; 42 | Score.create({team: 'foo'}, function(err, newScore) { 43 | Score.createChangeStream(function(err, changes) { 44 | changes.on('data', function(change) { 45 | expect(change.type).to.equal('update'); 46 | changes.destroy(); 47 | 48 | done(); 49 | }); 50 | newScore.updateAttributes({ 51 | bat: 'baz', 52 | }); 53 | }); 54 | }); 55 | }); 56 | 57 | it('should detect delete', function(done) { 58 | const Score = this.Score; 59 | Score.create({team: 'foo'}, function(err, newScore) { 60 | Score.createChangeStream(function(err, changes) { 61 | changes.on('data', function(change) { 62 | expect(change.type).to.equal('remove'); 63 | changes.destroy(); 64 | 65 | done(); 66 | }); 67 | 68 | newScore.remove(); 69 | }); 70 | }); 71 | }); 72 | 73 | it('should apply "where" and "fields" to create events', function() { 74 | const Score = this.Score; 75 | const data = [ 76 | {team: 'baz', player: 'baz', value: 1}, 77 | {team: 'bar', player: 'baz', value: 2}, 78 | {team: 'foo', player: 'bar', value: 3}, 79 | ]; 80 | const options = {where: {player: 'bar'}, fields: ['team', 'value']}; 81 | const changes = []; 82 | let changeStream; 83 | 84 | return Score.createChangeStream(options) 85 | .then(stream => { 86 | changeStream = stream; 87 | changeStream.on('data', function(change) { 88 | changes.push(change); 89 | }); 90 | 91 | return Score.create(data); 92 | }) 93 | .then(scores => { 94 | changeStream.destroy(); 95 | 96 | expect(changes).to.have.length(1); 97 | expect(changes[0]).to.have.property('type', 'create'); 98 | expect(changes[0].data).to.eql({ 99 | 'team': 'foo', 100 | value: 3, 101 | }); 102 | }); 103 | }); 104 | 105 | it('should not emit changes after destroy', function(done) { 106 | const Score = this.Score; 107 | 108 | const spy = sinon.spy(); 109 | 110 | Score.createChangeStream(function(err, changes) { 111 | changes.on('data', function() { 112 | spy(); 113 | changes.destroy(); 114 | }); 115 | 116 | Score.create({team: 'foo'}) 117 | .then(() => Score.deleteAll()) 118 | .then(() => { 119 | expect(spy.calledOnce); 120 | done(); 121 | }); 122 | }); 123 | }); 124 | 125 | function verifyObserversRemoval() { 126 | const Score = this.Score; 127 | expect(Score._observers['after save']).to.be.empty(); 128 | expect(Score._observers['after delete']).to.be.empty(); 129 | } 130 | }); 131 | 132 | // TODO(ritch) implement multi-server support 133 | describe.skip('configured to source changes using pubsub', function() { 134 | before(function() { 135 | const test = this; 136 | const app = loopback({localRegistry: true}); 137 | const db = app.dataSource('ds', {connector: 'memory'}); 138 | const ps = app.dataSource('ps', { 139 | host: 'localhost', 140 | port: '12345', 141 | connector: 'pubsub', 142 | pubsubAdapter: 'mqtt', 143 | }); 144 | this.Score = app.model('Score', { 145 | dataSource: 'db', 146 | changeDataSource: 'ps', 147 | }); 148 | }); 149 | 150 | it('should detect a change', function(done) { 151 | const Score = this.Score; 152 | 153 | Score.createChangeStream(function(err, changes) { 154 | changes.on('data', function(change) { 155 | done(); 156 | }); 157 | }); 158 | }); 159 | }); 160 | }); 161 | -------------------------------------------------------------------------------- /test/data-source.test.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2013,2019. All Rights Reserved. 2 | // Node module: loopback 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | const assert = require('assert'); 8 | const loopback = require('../'); 9 | 10 | describe('DataSource', function() { 11 | let memory; 12 | 13 | beforeEach(function() { 14 | memory = loopback.createDataSource({ 15 | connector: loopback.Memory, 16 | }); 17 | 18 | assertValidDataSource(memory); 19 | }); 20 | 21 | describe('dataSource.createModel(name, properties, settings)', function() { 22 | it('Define a model and attach it to a `DataSource`', function() { 23 | const Color = memory.createModel('color', {name: String}); 24 | assert.isFunc(Color, 'find'); 25 | assert.isFunc(Color, 'findById'); 26 | assert.isFunc(Color, 'findOne'); 27 | assert.isFunc(Color, 'create'); 28 | assert.isFunc(Color, 'updateOrCreate'); 29 | assert.isFunc(Color, 'upsertWithWhere'); 30 | assert.isFunc(Color, 'upsert'); 31 | assert.isFunc(Color, 'findOrCreate'); 32 | assert.isFunc(Color, 'exists'); 33 | assert.isFunc(Color, 'destroyAll'); 34 | assert.isFunc(Color, 'count'); 35 | assert.isFunc(Color, 'include'); 36 | assert.isFunc(Color, 'hasMany'); 37 | assert.isFunc(Color, 'belongsTo'); 38 | assert.isFunc(Color, 'hasAndBelongsToMany'); 39 | assert.isFunc(Color.prototype, 'save'); 40 | assert.isFunc(Color.prototype, 'isNewRecord'); 41 | assert.isFunc(Color.prototype, 'destroy'); 42 | assert.isFunc(Color.prototype, 'updateAttribute'); 43 | assert.isFunc(Color.prototype, 'updateAttributes'); 44 | assert.isFunc(Color.prototype, 'reload'); 45 | }); 46 | 47 | it('should honor settings.base', function() { 48 | const Base = memory.createModel('base'); 49 | const Color = memory.createModel('color', {name: String}, {base: Base}); 50 | assert(Color.prototype instanceof Base); 51 | assert.equal(Color.base, Base); 52 | }); 53 | 54 | it('should use loopback.PersistedModel as the base for DBs', function() { 55 | const Color = memory.createModel('color', {name: String}); 56 | assert(Color.prototype instanceof loopback.PersistedModel); 57 | assert.equal(Color.base, loopback.PersistedModel); 58 | }); 59 | 60 | it('should use loopback.Model as the base for non DBs', function() { 61 | // Mock up a non-DB connector 62 | const Connector = function() { 63 | }; 64 | Connector.prototype.getTypes = function() { 65 | return ['rest']; 66 | }; 67 | 68 | const ds = loopback.createDataSource({ 69 | connector: new Connector(), 70 | }); 71 | 72 | const Color = ds.createModel('color', {name: String}); 73 | assert(Color.prototype instanceof Color.registry.getModel('Model')); 74 | assert.equal(Color.base.modelName, 'PersistedModel'); 75 | }); 76 | }); 77 | 78 | describe.skip('PersistedModel Methods', function() { 79 | it('List the enabled and disabled methods', function() { 80 | const TestModel = loopback.PersistedModel.extend('TestPersistedModel'); 81 | TestModel.attachTo(loopback.memory()); 82 | 83 | // assert the defaults 84 | // - true: the method should be remote enabled 85 | // - false: the method should not be remote enabled 86 | // - 87 | existsAndShared('_forDB', false); 88 | existsAndShared('create', true); 89 | existsAndShared('updateOrCreate', true); 90 | existsAndShared('upsertWithWhere', true); 91 | existsAndShared('upsert', true); 92 | existsAndShared('findOrCreate', false); 93 | existsAndShared('exists', true); 94 | existsAndShared('find', true); 95 | existsAndShared('findOne', true); 96 | existsAndShared('destroyAll', false); 97 | existsAndShared('count', true); 98 | existsAndShared('include', false); 99 | existsAndShared('hasMany', false); 100 | existsAndShared('belongsTo', false); 101 | existsAndShared('hasAndBelongsToMany', false); 102 | existsAndShared('save', false); 103 | existsAndShared('isNewRecord', false); 104 | existsAndShared('_adapter', false); 105 | existsAndShared('destroyById', true); 106 | existsAndShared('destroy', false); 107 | existsAndShared('updateAttributes', true); 108 | existsAndShared('updateAll', true); 109 | existsAndShared('reload', false); 110 | 111 | function existsAndShared(Model, name, isRemoteEnabled, isProto) { 112 | const scope = isProto ? Model.prototype : Model; 113 | const fn = scope[name]; 114 | const actuallyEnabled = Model.getRemoteMethod(name); 115 | assert(fn, name + ' should be defined!'); 116 | assert(actuallyEnabled === isRemoteEnabled, 117 | name + ' ' + (isRemoteEnabled ? 'should' : 'should not') + 118 | ' be remote enabled'); 119 | } 120 | }); 121 | }); 122 | }); 123 | 124 | function assertValidDataSource(dataSource) { 125 | // has methods 126 | assert.isFunc(dataSource, 'createModel'); 127 | assert.isFunc(dataSource, 'discoverModelDefinitions'); 128 | assert.isFunc(dataSource, 'discoverSchema'); 129 | assert.isFunc(dataSource, 'enableRemote'); 130 | assert.isFunc(dataSource, 'disableRemote'); 131 | assert.isFunc(dataSource, 'defineOperation'); 132 | assert.isFunc(dataSource, 'operations'); 133 | } 134 | 135 | assert.isFunc = function(obj, name) { 136 | assert(obj, 'cannot assert function ' + name + ' on object that doesnt exist'); 137 | assert(typeof obj[name] === 'function', name + ' is not a function'); 138 | }; 139 | -------------------------------------------------------------------------------- /lib/connectors/mail.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2013,2019. All Rights Reserved. 2 | // Node module: loopback 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | /** 7 | * Dependencies. 8 | */ 9 | 10 | 'use strict'; 11 | const g = require('../globalize'); 12 | const mailer = require('nodemailer'); 13 | const assert = require('assert'); 14 | const debug = require('debug')('loopback:connector:mail'); 15 | const loopback = require('../loopback'); 16 | 17 | /** 18 | * Export the MailConnector class. 19 | */ 20 | 21 | module.exports = MailConnector; 22 | 23 | /** 24 | * Create an instance of the connector with the given `settings`. 25 | */ 26 | 27 | function MailConnector(settings) { 28 | assert(typeof settings === 'object', 'cannot initialize MailConnector without a settings object'); 29 | 30 | let transports = settings.transports; 31 | 32 | // if transports is not in settings object AND settings.transport exists 33 | if (!transports && settings.transport) { 34 | // then wrap single transport in an array and assign to transports 35 | transports = [settings.transport]; 36 | } 37 | 38 | if (!transports) { 39 | transports = []; 40 | } 41 | 42 | this.transportsIndex = {}; 43 | this.transports = []; 44 | 45 | if (loopback.isServer) { 46 | transports.forEach(this.setupTransport.bind(this)); 47 | } 48 | } 49 | 50 | MailConnector.initialize = function(dataSource, callback) { 51 | dataSource.connector = new MailConnector(dataSource.settings); 52 | callback(); 53 | }; 54 | 55 | MailConnector.prototype.DataAccessObject = Mailer; 56 | 57 | /** 58 | * Add a transport to the available transports. See https://github.com/andris9/Nodemailer#setting-up-a-transport-method. 59 | * 60 | * Example: 61 | * 62 | * Email.setupTransport({ 63 | * type: "SMTP", 64 | * host: "smtp.gmail.com", // hostname 65 | * secureConnection: true, // use SSL 66 | * port: 465, // port for secure SMTP 67 | * alias: "gmail", // optional alias for use with 'transport' option when sending 68 | * auth: { 69 | * user: "gmail.user@gmail.com", 70 | * pass: "userpass" 71 | * } 72 | * }); 73 | * 74 | */ 75 | 76 | MailConnector.prototype.setupTransport = function(setting) { 77 | const connector = this; 78 | connector.transports = connector.transports || []; 79 | connector.transportsIndex = connector.transportsIndex || {}; 80 | 81 | let transport; 82 | const transportType = (setting.type || 'STUB').toLowerCase(); 83 | if (transportType === 'smtp') { 84 | transport = mailer.createTransport(setting); 85 | } else { 86 | const transportModuleName = 'nodemailer-' + transportType + '-transport'; 87 | const transportModule = require(transportModuleName); 88 | transport = mailer.createTransport(transportModule(setting)); 89 | } 90 | 91 | connector.transportsIndex[setting.alias || setting.type] = transport; 92 | connector.transports.push(transport); 93 | }; 94 | 95 | function Mailer() { 96 | 97 | } 98 | 99 | /** 100 | * Get a transport by name. 101 | * 102 | * @param {String} name 103 | * @return {Transport} transport 104 | */ 105 | 106 | MailConnector.prototype.transportForName = function(name) { 107 | return this.transportsIndex[name]; 108 | }; 109 | 110 | /** 111 | * Get the default transport. 112 | * 113 | * @return {Transport} transport 114 | */ 115 | 116 | MailConnector.prototype.defaultTransport = function() { 117 | return this.transports[0] || this.stubTransport; 118 | }; 119 | 120 | /** 121 | * Send an email with the given `options`. 122 | * 123 | * Example Options: 124 | * 125 | * { 126 | * from: "Fred Foo ✔ ", // sender address 127 | * to: "bar@blurdybloop.com, baz@blurdybloop.com", // list of receivers 128 | * subject: "Hello ✔", // Subject line 129 | * text: "Hello world ✔", // plaintext body 130 | * html: "Hello world ✔", // html body 131 | * transport: "gmail", // See 'alias' option above in setupTransport 132 | * } 133 | * 134 | * See https://github.com/andris9/Nodemailer for other supported options. 135 | * 136 | * @param {Object} options 137 | * @param {Function} callback Called after the e-mail is sent or the sending failed 138 | */ 139 | 140 | Mailer.send = function(options, fn) { 141 | const dataSource = this.dataSource; 142 | const settings = dataSource && dataSource.settings; 143 | const connector = dataSource.connector; 144 | assert(connector, 'Cannot send mail without a connector!'); 145 | 146 | let transport = connector.transportForName(options.transport); 147 | 148 | if (!transport) { 149 | transport = connector.defaultTransport(); 150 | } 151 | 152 | if (debug.enabled || settings && settings.debug) { 153 | g.log('Sending Mail:'); 154 | if (options.transport) { 155 | console.log(g.f('\t TRANSPORT:%s', options.transport)); 156 | } 157 | g.log('\t TO:%s', options.to); 158 | g.log('\t FROM:%s', options.from); 159 | g.log('\t SUBJECT:%s', options.subject); 160 | g.log('\t TEXT:%s', options.text); 161 | g.log('\t HTML:%s', options.html); 162 | } 163 | 164 | if (transport) { 165 | assert(transport.sendMail, 166 | 'You must supply an Email.settings.transports containing a valid transport'); 167 | transport.sendMail(options, fn); 168 | } else { 169 | g.warn('Warning: No email transport specified for sending email.' + 170 | ' Setup a transport to send mail messages.'); 171 | process.nextTick(function() { 172 | fn(null, options); 173 | }); 174 | } 175 | }; 176 | 177 | /** 178 | * Send an email instance using `modelInstance.send()`. 179 | */ 180 | 181 | Mailer.prototype.send = function(fn) { 182 | this.constructor.send(this, fn); 183 | }; 184 | 185 | /** 186 | * Access the node mailer object. 187 | */ 188 | 189 | MailConnector.mailer = 190 | MailConnector.prototype.mailer = 191 | Mailer.mailer = 192 | Mailer.prototype.mailer = mailer; 193 | -------------------------------------------------------------------------------- /intl/zh-Hans/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "03f79fa268fe199de2ce4345515431c1": "对于标识为 {1} 的 {0},找不到任何更改记录", 3 | "04bd8af876f001ceaf443aad6a9002f9": "认证需要定义模型 {0}。", 4 | "095afbf2f1f0e5be678f5dac5c54e717": "拒绝访问", 5 | "0caffe1d763c8cca6a61814abe33b776": "电子邮件是必需的", 6 | "0da38687fed24275c1547e815914a8e3": "按标识删除 {0} 的相关项。", 7 | "0e21aad369dd09e1965c11949303cefd": "{0}.{1} {{\"isStatic\"}} 的远程处理元数据不匹配新的基于方法名称的样式。", 8 | "10e01c895dc0b2fecc385f9f462f1ca6": "颜色列表位于:{{http://localhost:3000/colors}}", 9 | "1b2a6076dccbe91a56f1672eb3b8598c": "响应主体包含在登录时创建的 {{AccessToken}} 的属性。\n根据“include”参数的值,主体可包含其他属性:\n\n - `user` - `U+007BUserU+007D` - 当前已登录用户的数据。 {{(`include=user`)}}\n\n", 10 | "1d7833c3ca2f05fdad8fad7537531c40": "\t主题:{0}", 11 | "1e85f822b547a75d7d385048030e4ecb": "创建时间:{0}", 12 | "22fe62fa8d595b72c62208beddaa2a56": "按标识更新 {0} 的相关项。", 13 | "275f22ab95671f095640ca99194b7635": "\t发件人:{0}", 14 | "2860bccdf9ef1e350c1a38932ed12173": "V3.0 中移除了 {0}。请参阅 {1} 以获取更多详细信息。", 15 | "2d3071e3b18681c80a090dc0efbdb349": "无法找到标识为 {1} 的 {0}", 16 | "316e5b82c203cf3de31a449ee07d0650": "期望布尔值,获取 {0}", 17 | "320c482401afa1207c04343ab162e803": "无效的主体类型:{0}", 18 | "3438fab56cc7ab92dfd88f0497e523e0": "“{0}”配置的关系属性必须是对象。", 19 | "3591f1d3e115b46f9f195df5ca548a6a": "找不到模型:模型“{0}”正在扩展未知的模型“{1}”。", 20 | "35e5252c62d80f8c54a5290d30f4c7d0": "请通过在 Web 浏览器中打开此链接来验证您的电子邮件:\n\t{0}", 21 | "37bcd4b50dfae98734772e39ffb1ea3d": "输入的密码过长。最大长度为 {0}(输入的长度为 {1})", 22 | "3aae63bb7e8e046641767571c1591441": "因为尚未验证电子邮件,登录失败", 23 | "3aecb24fa8bdd3f79d168761ca8a6729": "未知的 {{middleware}} 阶段 {0}", 24 | "3ca45aa6f705c46a4c598a900716f086": "{0} 正在使用目前不再可用的模型设置 {1}。", 25 | "3caaa84fc103d6d5612173ae6d43b245": "无效的令牌:{0}", 26 | "3d617953470be16d0c2b32f0bcfbb5ee": "感谢您注册", 27 | "3d63008ccfb2af1db2142e8cc2716ace": "警告:未指定用于发送电子邮件的电子邮件传输。设置传输以发送电子邮件消息。", 28 | "4203ab415ec66a78d3164345439ba76e": "无法调用 {0}.{1}()。尚未设置 {2} 方法。{{PersistedModel}} 未正确附加到 {{DataSource}}!", 29 | "42a36bac5cf03c4418d664500c81047a": "不再支持 {{DataSource}} 选项 {{\"defaultForType\"}}", 30 | "44a6c8b1ded4ed653d19ddeaaf89a606": "找不到电子邮件", 31 | "4a4f04a4e480fc5d4ee73b84d9a4b904": "正在发送电子邮件:", 32 | "4b494de07f524703ac0879addbd64b13": "尚未验证电子邮件", 33 | "528325f3cbf1b0ab9a08447515daac9a": "更新此模型的 {0}。", 34 | "543d19bad5e47ee1e9eb8af688e857b4": "{0} 的外键。", 35 | "57b87ae0e65f6ab7a2e3e6cbdfca49a4": "无法创建数据源 {0}:{1}", 36 | "5858e63efaa0e4ad86b61c0459ea32fa": "您必须将 {{Email}} 模型连接到 {{Mail}} 连接器", 37 | "598ff0255ffd1d1b71e8de55dbe2c034": "按标识检查项的 {0} 关系是否存在。", 38 | "5a36cc6ba0cc27c754f6c5ed6015ea3c": "按标识除去项的 {0} 关系。", 39 | "5e81ad3847a290dc650b47618b9cbc7e": "登录失败", 40 | "5fa3afb425819ebde958043e598cb664": "找不到具有 {{id}} {0} 的模型", 41 | "61e5deebaf44d68f4e6a508f30cc31a3": "对于模型“{1}”,关系“{0}”不存在", 42 | "62e8b0a733417978bab22c8dacf5d7e6": "无法应用批量更新,连接器未正确报告更新的记录数。", 43 | "63a091ced88001ab6acb58f61ec041c5": "\t 文本:{0}", 44 | "651f0b3cbba001635152ec3d3d954d0a": "按标识查找 {0} 的相关项。", 45 | "6bc376432cd9972cf991aad3de371e78": "缺少更改的数据:{0}", 46 | "705c2d456a3e204c4af56e671ec3225c": "无法找到 {{accessToken}}", 47 | "734a7bebb65e10899935126ba63dd51f": "“{0}”配置的选项属性必须是对象。", 48 | "779467f467862836e19f494a37d6ab77": "“{0}”配置的 acls 属性必须是对象数组。", 49 | "7bc7b301ad9c4fc873029d57fb9740fe": "查询 {1} 的 {0}。", 50 | "7c837b88fd0e509bd3fc722d7ddf0711": "{0} 的外键", 51 | "7d5e7ed0efaedf3f55f380caae0df8b8": "我的第一个移动应用程序", 52 | "7e0fca41d098607e1c9aa353c67e0fa1": "无效的访问令牌", 53 | "7e287fc885d9fdcf42da3a12f38572c1": "需要授权", 54 | "7ea04ea91aac3cb7ce0ddd96b7ff1fa4": "{{accessToken}} 需要注销", 55 | "80a32e80cbed65eba2103201a7c94710": "找不到模型:{0}", 56 | "830cb6c862f8f364e9064cea0026f701": "访存 hasOne 关系 {0}。", 57 | "83cbdc2560ba9f09155ccfc63e08f1a1": "无法针对“{1}”重新配置属性“{0}”。", 58 | "855eb8db89b4921c42072832d33d2dc2": "密码无效。", 59 | "855ecd4a64885ba272d782435f72a4d4": "未知的“{0}”标识“{1}”。", 60 | "860d1a0b8bd340411fb32baa72867989": "传输不支持 HTTP 重定向。", 61 | "86254879d01a60826a851066987703f2": "按标识添加 {0} 的相关项。", 62 | "895b1f941d026870b3cc8e6af087c197": "{{username}} 或 {{email}} 是必需的", 63 | "8ae418c605b6a45f2651be9b1677c180": "无效的远程方法:“{0}”", 64 | "8bab6720ecc58ec6412358c858a53484": "批量更新失败,连接器已修改意外数量的记录:{0}", 65 | "8ecab7f534de38360bd1b1c88e880123": "“{0}”的子模型不会继承最新定义的远程方法 {1}。", 66 | "93ba9a1d03da3b7696332d3f155c5bb7": "\t HTML:{0}", 67 | "97795efe0c3eb7f35ce8cf8cfe70682b": "“{0}”的配置缺少 {{`dataSource`}} 属性。\n使用“null”或“false”来标记未附加到任何数据源的模型。", 68 | "9e3cbc1d5a9347cdcf6d1b4a6dbb55b7": "访存 belongsTo 关系 {0}。", 69 | "a50d10fc6e0959b220e085454c40381e": "找不到用户:{0}", 70 | "b6f740aeb6f2eb9bee9cb049dbfe6a28": "未知的“{0}”{{key}}“{1}”。", 71 | "ba96498b10c179f9cd75f75c8def4f70": "{{realm}} 是必需的", 72 | "c0057a569ff9d3b509bac61a4b2f605d": "删除此模型的所有 {0}。", 73 | "c2b5d51f007178170ca3952d59640ca4": "无法纠正 {0} 更改:\n{1}", 74 | "c4ee6d177c974532c3552d2f98eb72ea": "V3.0 中移除了 {0} 中间件。请参阅 {1} 以获取更多详细信息。", 75 | "c61a5a02ba3801a892308f70f5d55a14": "不推荐使用远程处理元数据 {{\"isStatic\"}}。而是针对 {{isStatic=false}} 在方法名称中指定 {{\"prototype.name\"}}。", 76 | "c68a93f0a9524fed4ff64372fc90c55f": "必须提供有效电子邮件", 77 | "cd0412f2f33a4a2a316acc834f3f21a6": "必须指定 {{id}} 或 {{data}}", 78 | "d5552322de5605c58b62f47ad26d2716": "已移除 {{`app.boot`}},请改用新模块 {{loopback-boot}}", 79 | "d6f43b266533b04d442bdb3955622592": "在此模型的 {0} 中创建新实例。", 80 | "da13d3cdf21330557254670dddd8c5c7": "计算 {0} 的数量({1})。", 81 | "dc568bee32deb0f6eaf63e73b20e8ceb": "忽略“{0}”的非对象“方法”设置。", 82 | "e4434de4bb8f5a3cd1d416e4d80d7e0b": "未知的“{0}”{{id}}“{1}”。", 83 | "e92aa25b6b864e3454b65a7c422bd114": "批量更新失败,连接器已删除意外数量的记录:{0}", 84 | "ea63d226b6968e328bdf6876010786b5": "无法应用批量更新,连接器未正确报告删除的记录数。", 85 | "ead044e2b4bce74b4357f8a03fb78ec4": "无法调用 {0}.{1}()。尚未设置 {2} 方法。{{KeyValueModel}} 未正确附加到 {{DataSource}}!", 86 | "ecb06666ef95e5db27a5ac1d6a17923b": "\t 收件人:{0}", 87 | "f0aed00a3d3d0b97d6594e4b70e0c201": "\t 传输:{0}", 88 | "f0bd73df8714cefb925e3b8da2f4c5f6": "结果:{0}", 89 | "f1d4ac54357cc0932f385d56814ba7e4": "冲突", 90 | "f66ae3cf379b2fce28575a3282defe1a": "删除此模型的 {0}。", 91 | "f8e26bcca62a47f579562f1cd2c785ff": "请重新设计您的应用程序以使用正式解决方案插入来自请求上下文的“options”自变量,\n请参阅 {0}" 92 | } 93 | 94 | -------------------------------------------------------------------------------- /intl/zh-Hant/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "03f79fa268fe199de2ce4345515431c1": "對於 id 為 {1} 的 {0},找不到變更記錄", 3 | "04bd8af876f001ceaf443aad6a9002f9": "需要定義模型 {0} 才能鑑別。", 4 | "095afbf2f1f0e5be678f5dac5c54e717": "拒絕存取", 5 | "0caffe1d763c8cca6a61814abe33b776": "需要電子郵件", 6 | "0da38687fed24275c1547e815914a8e3": "依 id 刪除 {0} 的相關項目。", 7 | "0e21aad369dd09e1965c11949303cefd": "{0} 的遠端 meta 資料。{1} {{\"isStatic\"}} 不符合新的方法名稱型樣式。", 8 | "10e01c895dc0b2fecc385f9f462f1ca6": "{{http://localhost:3000/colors}} 提供顏色清單", 9 | "1b2a6076dccbe91a56f1672eb3b8598c": "回應內文包含登入時建立的 {{AccessToken}} 的內容。\n根據 `include` 參數的值而定,內文可能包含其他內容:\n\n - `user` - `U+007BUserU+007D` - 目前登入的使用者的資料。 {{(`include=user`)}}\n\n", 10 | "1d7833c3ca2f05fdad8fad7537531c40": "\t 主旨:{0}", 11 | "1e85f822b547a75d7d385048030e4ecb": "已建立:{0}", 12 | "22fe62fa8d595b72c62208beddaa2a56": "依 id 更新 {0} 的相關項目。", 13 | "275f22ab95671f095640ca99194b7635": "\t 寄件者:{0}", 14 | "2860bccdf9ef1e350c1a38932ed12173": "3.0 版中已移除 {0}。如需詳細資料,請參閱 {1}。", 15 | "2d3071e3b18681c80a090dc0efbdb349": "找不到 id 為 {1} 的 {0}", 16 | "316e5b82c203cf3de31a449ee07d0650": "預期為布林,但卻取得 {0}", 17 | "320c482401afa1207c04343ab162e803": "無效的主體類型:{0}", 18 | "3438fab56cc7ab92dfd88f0497e523e0": "`{0}` 配置的 relations 內容必須是物件", 19 | "3591f1d3e115b46f9f195df5ca548a6a": "找不到模型:模型 `{0}` 正在延伸不明模型 `{1}`。", 20 | "35e5252c62d80f8c54a5290d30f4c7d0": "請在 Web 瀏覽器中開啟此鏈結來驗證電子郵件:\n\t{0}", 21 | "37bcd4b50dfae98734772e39ffb1ea3d": "輸入的密碼太長。長度上限為 {0}(輸入了 {1})", 22 | "3aae63bb7e8e046641767571c1591441": "因為尚未驗證電子郵件,所以登入失敗", 23 | "3aecb24fa8bdd3f79d168761ca8a6729": "{{middleware}} 階段 {0} 不明", 24 | "3ca45aa6f705c46a4c598a900716f086": "{0} 正在使用已不再可用的模型設定 {1}。", 25 | "3caaa84fc103d6d5612173ae6d43b245": "無效記號:{0}", 26 | "3d617953470be16d0c2b32f0bcfbb5ee": "感謝您登錄", 27 | "3d63008ccfb2af1db2142e8cc2716ace": "警告:未指定用於傳送電子郵件的電子郵件傳輸。請設定傳輸來傳送郵件訊息。", 28 | "4203ab415ec66a78d3164345439ba76e": "無法呼叫 {0}。{1}()。尚未設定 {2} 方法。{{PersistedModel}} 未正確連接至 {{DataSource}}!", 29 | "42a36bac5cf03c4418d664500c81047a": "不再支援 {{DataSource}} 選項 {{\"defaultForType\"}}", 30 | "44a6c8b1ded4ed653d19ddeaaf89a606": "找不到電子郵件", 31 | "4a4f04a4e480fc5d4ee73b84d9a4b904": "正在傳送郵件:", 32 | "4b494de07f524703ac0879addbd64b13": "尚未驗證電子郵件", 33 | "528325f3cbf1b0ab9a08447515daac9a": "更新這個模型的 {0}。", 34 | "543d19bad5e47ee1e9eb8af688e857b4": "{0} 的外部索引鍵。", 35 | "57b87ae0e65f6ab7a2e3e6cbdfca49a4": "無法建立資料來源 {0}:{1}", 36 | "5858e63efaa0e4ad86b61c0459ea32fa": "您必須將 {{Email}} 模型連接至 {{Mail}} 連接器", 37 | "598ff0255ffd1d1b71e8de55dbe2c034": "依 id 檢查項目的 {0} 關係是否存在。", 38 | "5a36cc6ba0cc27c754f6c5ed6015ea3c": "依 id 移除項目的 {0} 關係。", 39 | "5e81ad3847a290dc650b47618b9cbc7e": "登入失敗", 40 | "5fa3afb425819ebde958043e598cb664": "找不到 {{id}} 為 {0} 的模型", 41 | "61e5deebaf44d68f4e6a508f30cc31a3": "模型 `{1}` 的關係 `{0}` 不存在", 42 | "62e8b0a733417978bab22c8dacf5d7e6": "無法套用大量更新,連接器未正確報告已更新的記錄數。", 43 | "63a091ced88001ab6acb58f61ec041c5": "\t 文字:{0}", 44 | "651f0b3cbba001635152ec3d3d954d0a": "依 id 尋找 {0} 的相關項目。", 45 | "6bc376432cd9972cf991aad3de371e78": "遺漏變更的資料:{0}", 46 | "705c2d456a3e204c4af56e671ec3225c": "找不到 {{accessToken}}", 47 | "734a7bebb65e10899935126ba63dd51f": "`{0}` 配置的 options 內容必須是物件", 48 | "779467f467862836e19f494a37d6ab77": "`{0}` 配置的 acls 內容必須是物件陣列", 49 | "7bc7b301ad9c4fc873029d57fb9740fe": "查詢 {0} 個(共 {1} 個)。", 50 | "7c837b88fd0e509bd3fc722d7ddf0711": "{0} 的外部索引鍵", 51 | "7d5e7ed0efaedf3f55f380caae0df8b8": "我的第一個行動式應用程式", 52 | "7e0fca41d098607e1c9aa353c67e0fa1": "存取記號無效", 53 | "7e287fc885d9fdcf42da3a12f38572c1": "需要授權", 54 | "7ea04ea91aac3cb7ce0ddd96b7ff1fa4": "需要 {{accessToken}} 才能登出", 55 | "80a32e80cbed65eba2103201a7c94710": "找不到模型:{0}", 56 | "830cb6c862f8f364e9064cea0026f701": "提取 hasOne 關係 {0}。", 57 | "83cbdc2560ba9f09155ccfc63e08f1a1": "無法為 `{1}` 重新配置內容 `{0}`", 58 | "855eb8db89b4921c42072832d33d2dc2": "密碼無效。", 59 | "855ecd4a64885ba272d782435f72a4d4": "\"{0}\" ID \"{1}\" 不明。", 60 | "860d1a0b8bd340411fb32baa72867989": "傳輸不支援 HTTP 重新導向。", 61 | "86254879d01a60826a851066987703f2": "依 id 新增 {0} 的相關項目。", 62 | "895b1f941d026870b3cc8e6af087c197": "需要 {{username}} 或 {{email}}", 63 | "8ae418c605b6a45f2651be9b1677c180": "無效的遠端方法:`{0}`", 64 | "8bab6720ecc58ec6412358c858a53484": "大量更新失敗,連接器已修改超乎預期的記錄數:{0}", 65 | "8ecab7f534de38360bd1b1c88e880123": "`{0}` 的子項模型將不會繼承新定義的遠端方法 {1}。", 66 | "93ba9a1d03da3b7696332d3f155c5bb7": "\t HTML:{0}", 67 | "97795efe0c3eb7f35ce8cf8cfe70682b": "`{0}` 的配置遺漏 {{`dataSource`}} 內容。\n請使用 `null` 或 `false` 來標示未連接至任何資料來源的模型。", 68 | "9e3cbc1d5a9347cdcf6d1b4a6dbb55b7": "提取 belongsTo 關係 {0}。", 69 | "a50d10fc6e0959b220e085454c40381e": "找不到使用者:{0}", 70 | "b6f740aeb6f2eb9bee9cb049dbfe6a28": "\"{0}\" {{key}} \"{1}\" 不明。", 71 | "ba96498b10c179f9cd75f75c8def4f70": "需要 {{realm}}", 72 | "c0057a569ff9d3b509bac61a4b2f605d": "刪除這個模型的所有 {0}。", 73 | "c2b5d51f007178170ca3952d59640ca4": "無法更正 {0} 個變更:\n{1}", 74 | "c4ee6d177c974532c3552d2f98eb72ea": "3.0 版中已移除 {0} 中介軟體。如需詳細資料,請參閱 {1}。", 75 | "c61a5a02ba3801a892308f70f5d55a14": "遠端 meta 資料 {{\"isStatic\"}} 已淘汰。請在方法名稱中指定 {{\"prototype.name\"}} 來代替 {{isStatic=false}}。", 76 | "c68a93f0a9524fed4ff64372fc90c55f": "必須提供有效的電子郵件", 77 | "cd0412f2f33a4a2a316acc834f3f21a6": "必須指定 {{id}} 或 {{data}}", 78 | "d5552322de5605c58b62f47ad26d2716": "已移除 {{`app.boot`}},請改用新的模組 {{loopback-boot}}", 79 | "d6f43b266533b04d442bdb3955622592": "在這個模型的 {0} 中建立新實例。", 80 | "da13d3cdf21330557254670dddd8c5c7": "計算 {0} 個(共 {1} 個)。", 81 | "dc568bee32deb0f6eaf63e73b20e8ceb": "忽略 \"{0}\" 的非物件 \"methods\" 設定。", 82 | "e4434de4bb8f5a3cd1d416e4d80d7e0b": "\"{0}\" {{id}} \"{1}\" 不明。", 83 | "e92aa25b6b864e3454b65a7c422bd114": "大量更新失敗,連接器已刪除非預期的記錄數:{0}", 84 | "ea63d226b6968e328bdf6876010786b5": "無法套用大量更新,連接器未正確報告已刪除的記錄數。", 85 | "ead044e2b4bce74b4357f8a03fb78ec4": "無法呼叫 {0}。{1}()。尚未設定 {2} 方法。{{KeyValueModel}} 未正確連接至 {{DataSource}}!", 86 | "ecb06666ef95e5db27a5ac1d6a17923b": "\t 收件者:{0}", 87 | "f0aed00a3d3d0b97d6594e4b70e0c201": "\t 傳輸:{0}", 88 | "f0bd73df8714cefb925e3b8da2f4c5f6": "結果:{0}", 89 | "f1d4ac54357cc0932f385d56814ba7e4": "衝突", 90 | "f66ae3cf379b2fce28575a3282defe1a": "刪除這個模型的 {0}。", 91 | "f8e26bcca62a47f579562f1cd2c785ff": "請重做您的應用程式以使用正式解決方案,從要求環境定義注入 \"options\" 引數,\n請參閱 {0}" 92 | } 93 | 94 | --------------------------------------------------------------------------------