Compare commits
753 Commits
Author | SHA1 | Date |
---|---|---|
|
13371fd2a1 | |
|
1316be55c5 | |
|
a22d4f9197 | |
|
223dd5d58e | |
|
fbd9783e47 | |
|
6f04b61f88 | |
|
ab3e159fed | |
|
5b61efb855 | |
|
e457e2651d | |
|
ab2e462ca6 | |
|
f2f7332916 | |
|
06fcaa144e | |
|
d929b57da4 | |
|
58f65454e9 | |
|
cdf48b648e | |
|
f788aae464 | |
|
6a7fc529fc | |
|
b93187b0f9 | |
|
2db09a3d00 | |
|
4edcc4f14c | |
|
b848bd9d9f | |
|
aa76a19e33 | |
|
f2f0b3aa91 | |
|
82cd6681f6 | |
|
397d141036 | |
|
aec86ccc3f | |
|
5dd9561cb6 | |
|
dd6d2a5e6c | |
|
da51c992ba | |
|
4846048e48 | |
|
edb925529e | |
|
bd276d551a | |
|
83612aac8f | |
|
af9f776ed0 | |
|
387835faef | |
|
8311138f3e | |
|
cceb8a3c26 | |
|
9b1d4885ab | |
|
58a0e6c8e9 | |
|
e682914ecd | |
|
716347e25c | |
|
cd8db0b5cf | |
|
686c6fe88e | |
|
c9a277884e | |
|
a685553b49 | |
|
4b3a3a347c | |
|
e8d115bdc2 | |
|
ae3fff9e7b | |
|
0e0ca455ac | |
|
e82a74cce3 | |
|
182ab7fb22 | |
|
3a2a645e0c | |
|
f19a8ab6fe | |
|
66ec2aa57f | |
|
f6ba1afe91 | |
|
b05d7320d7 | |
|
a175b36939 | |
|
34acd02a7e | |
|
a9a86fe380 | |
|
b83f5635cf | |
|
6caf506769 | |
|
b91938ecdf | |
|
c8a31e2725 | |
|
4226da5fc4 | |
|
1e33ec596f | |
|
c38900b785 | |
|
edb8dbc517 | |
|
b77907ffa5 | |
|
242c20fecf | |
|
2532b0b67e | |
|
0bb8c23e2d | |
|
6eb8c0ed3a | |
|
6b748748bd | |
|
da2b8d8676 | |
|
71c651123f | |
|
aad97c2036 | |
|
81d1f7406f | |
|
f1c613ac07 | |
|
a239a11656 | |
|
a4d7caba1a | |
|
818a7506d8 | |
|
69df6386fc | |
|
29c5f20d90 | |
|
be91ba3dad | |
|
b944b9d8a5 | |
|
9ea6b6ab03 | |
|
4c49b5fa1f | |
|
d66c912d06 | |
|
0488cb7ba1 | |
|
e33d10fe44 | |
|
97a55bf67a | |
|
2b7b0e1cc1 | |
|
d7a32aea70 | |
|
0bb7cd67ec | |
|
186ae2ae57 | |
|
fa644d6a31 | |
|
acc1cd0ee5 | |
|
cab1ed5463 | |
|
c9164aed17 | |
|
9f1eb419d1 | |
|
516fbbd91a | |
|
d028876b94 | |
|
78342ebf80 | |
|
8ad0f93552 | |
|
55e48cf572 | |
|
74d7043edc | |
|
3caf65d4a6 | |
|
700085c8d6 | |
|
1c30628a8a | |
|
6b0a880efa | |
|
89ff760b6d | |
|
416b86a5ec | |
|
aaa1381964 | |
|
0dac936bba | |
|
5ee731eafd | |
|
6d23c2edd1 | |
|
962efec9cd | |
|
951b61a2ae | |
|
f90e1c5e01 | |
|
cdab1519af | |
|
7e7f8facd8 | |
|
72d85fa9ea | |
|
7ad5a2e1bc | |
|
f9e9aaa8ce | |
|
cd1b31920e | |
|
9d77ba28d3 | |
|
95bcf035a8 | |
|
37e57f6943 | |
|
9c554fd4f0 | |
|
4dd195e654 | |
|
44951a1032 | |
|
0590bd1bb4 | |
|
7d93b27a80 | |
|
5a87f8df16 | |
|
f17346ba50 | |
|
995c14eccf | |
|
83fc62c0af | |
|
58090b4b80 | |
|
ec46c457d3 | |
|
85aa3fd679 | |
|
8c4c91a5d1 | |
|
cf5b78fc6c | |
|
c6d8565216 | |
|
cc4fc2197f | |
|
eb3301abf2 | |
|
36ef35dd60 | |
|
59b14093c5 | |
|
c2077dfcb0 | |
|
b2bc449e24 | |
|
3e3092c017 | |
|
07e759259e | |
|
2aead13f11 | |
|
df70f83d67 | |
|
7a148caa26 | |
|
0cd380c590 | |
|
77a35231dc | |
|
743b2d1495 | |
|
77d3d57252 | |
|
f4527c9c91 | |
|
6ddf268cb6 | |
|
c611bbbe04 | |
|
8e9fd36878 | |
|
49bdf2fe3c | |
|
66497ead70 | |
|
38f3d728b1 | |
|
0feda03d5b | |
|
386615a1df | |
|
3723f107db | |
|
d23ff84587 | |
|
50e2b49efe | |
|
0eeb99060f | |
|
6b4234b18d | |
|
2c909b8223 | |
|
c5ff5faf0d | |
|
60c9dd166b | |
|
ab791fc258 | |
|
72d48c3bfa | |
|
60750b4508 | |
|
0a4940e31e | |
|
00169d2312 | |
|
7c030c6900 | |
|
243af4bfc2 | |
|
b045e4a6be | |
|
317e00d92c | |
|
010bbc6369 | |
|
73cc950b1b | |
|
fdb453943a | |
|
3af6a1bbaa | |
|
3bf84bacde | |
|
2bfd67ccaa | |
|
b362776e73 | |
|
0bac0a933f | |
|
1babfcde9f | |
|
5dd5a674ce | |
|
1a2d8a4571 | |
|
0737f5476d | |
|
b67a096f9e | |
|
cb600d1470 | |
|
825d5a6373 | |
|
91729ee550 | |
|
010c7bcd5f | |
|
6570b94843 | |
|
3996f56ab9 | |
|
4d4070e542 | |
|
2761e62533 | |
|
0a2a45512c | |
|
4ebc517a78 | |
|
083fb5d668 | |
|
c3450df4db | |
|
f30159cd23 | |
|
64d60fb6f7 | |
|
2f02fbac89 | |
|
fb8f3d9df3 | |
|
9176ee2e11 | |
|
d0a4941668 | |
|
826ee2aca8 | |
|
8488da2e26 | |
|
33989d776c | |
|
1dd0ab31e0 | |
|
db8130ac6d | |
|
c9913927e5 | |
|
c453ad52c2 | |
|
c0a0f09f3a | |
|
fcfaf7ef53 | |
|
658d228789 | |
|
e17132d061 | |
|
ef7175a4d5 | |
|
2128ecde46 | |
|
d2d8fabb16 | |
|
4c4430ea95 | |
|
883667ce8e | |
|
0f40ca8f8e | |
|
d405432b2d | |
|
30f3161c65 | |
|
63721fd253 | |
|
6ba35c297b | |
|
8bff145c65 | |
|
16ede97033 | |
|
29e89f50e5 | |
|
3651c09782 | |
|
661f7741cd | |
|
4a8e3f1327 | |
|
bf4b5de648 | |
|
b3602bdd18 | |
|
b1adba1c4a | |
|
2ebe38b4d5 | |
|
bc8778908e | |
|
44dd048036 | |
|
a736f782af | |
|
5db00c6ab2 | |
|
06a2b6d86b | |
|
724a7d1928 | |
|
df87c17c08 | |
|
9f8321ad61 | |
|
132ce4c4ae | |
|
dcb190ffc8 | |
|
74cbe12918 | |
|
2fd5701ede | |
|
2809d62990 | |
|
d3d0ef4ec1 | |
|
5cd95e42f2 | |
|
5562dca118 | |
|
3915c49a89 | |
|
619a0e468d | |
|
5dd0d196ee | |
|
7879ec346a | |
|
deec84f3b5 | |
|
679ecbc12d | |
|
8589629e7a | |
|
478e3f3b16 | |
|
1fcc40f1d1 | |
|
131b78dc84 | |
|
a3bf813088 | |
|
c761dc5279 | |
|
d1f6129eaf | |
|
5ed74ab24b | |
|
839c58639d | |
|
f7b88e3435 | |
|
8ed92a12e0 | |
|
ca3a21ddd5 | |
|
065eedab7b | |
|
f85551b715 | |
|
75b4a45968 | |
|
1be0a9f3d5 | |
|
04983831ca | |
|
359a6a5762 | |
|
3a10209502 | |
|
375d476d28 | |
|
f8db64c9c3 | |
|
4735efa41f | |
|
ee68b8067c | |
|
9fb67315f9 | |
|
a9c13a8f6c | |
|
69df11bb8e | |
|
6a4bd6d09f | |
|
d1719fd9a8 | |
|
b9fbf51b27 | |
|
b96605c63a | |
|
c5ca2e1c2e | |
|
e27419086c | |
|
d95ec66a23 | |
|
9c63abef52 | |
|
faa4975b78 | |
|
912aad8b35 | |
|
31e22d3b3f | |
|
52d3faed80 | |
|
95240598b3 | |
|
c5145bdf34 | |
|
9fef5284c7 | |
|
fbf818b2dc | |
|
0499d09d6b | |
|
75fdd3cf32 | |
|
981bd309fc | |
|
53182affef | |
|
94f267884f | |
|
f736da64f4 | |
|
b550cdcf43 | |
|
048110ee01 | |
|
27ed712528 | |
|
cb7e2114ec | |
|
8aa98a80ef | |
|
960e118c4e | |
|
89c9f5bb04 | |
|
5da8075fc9 | |
|
3a93167685 | |
|
6467658fdc | |
|
0448184a6c | |
|
55adb6c50e | |
|
3f4b5ece71 | |
|
011dc1f6b8 | |
|
0969493ab7 | |
|
48bf16bd5a | |
|
b5a8564956 | |
|
a22b1e13f1 | |
|
cf98d379c4 | |
|
01ce9b5f5a | |
|
5ebc9b6a2e | |
|
8510982bea | |
|
ed4f56c7f8 | |
|
79f441b9c4 | |
|
f0c9700e1d | |
|
c45954cdaa | |
|
abf8246382 | |
|
4570626e9d | |
|
2570dda984 | |
|
ec9274c5aa | |
|
2cac589860 | |
|
4796b7daf9 | |
|
37b49194f2 | |
|
f871358688 | |
|
7078c5d0e5 | |
|
92317e811a | |
|
440b9a52a6 | |
|
c69e010670 | |
|
c8271491af | |
|
f0e70dd8a9 | |
|
0dbfe8970f | |
|
88c38e7ff8 | |
|
a80f844e88 | |
|
d80957819a | |
|
ea05a2c4dc | |
|
304ecc4784 | |
|
9fe084fffd | |
|
798ebfba81 | |
|
5b404cad6c | |
|
db3c7b6dbd | |
|
3c209ee1de | |
|
76dd35e1d8 | |
|
e19c0dd974 | |
|
ceceb44e78 | |
|
c099d8c731 | |
|
dcb2f159ec | |
|
a63fad402e | |
|
7eeadc4289 | |
|
c7e0a15a44 | |
|
3b17a0cf5c | |
|
bb939bec07 | |
|
c072d0695d | |
|
5fd79b14f4 | |
|
8f80aecc1f | |
|
5607460854 | |
|
1d02e049b2 | |
|
d45c1ae7bb | |
|
0fd8f8af72 | |
|
82d6ede086 | |
|
19c3b47c4b | |
|
be8dd0ded8 | |
|
450aa84ae8 | |
|
78f8b9124a | |
|
93ab8644c4 | |
|
dfa1f6035b | |
|
035f4d095b | |
|
c6427065be | |
|
4c9c6236e3 | |
|
9a1f86baef | |
|
746265b393 | |
|
efd8237dc6 | |
|
a7d93f32b5 | |
|
e63fea83f7 | |
|
a6d511d8b4 | |
|
ff53933085 | |
|
4ffe5fe17f | |
|
e17cc3d23a | |
|
24bb15233d | |
|
3946462828 | |
|
9fec6e1615 | |
|
d2e9e7ee77 | |
|
b86eac5cf6 | |
|
3e2ac9217f | |
|
3ed525f753 | |
|
4e6f9b978a | |
|
70eecfab70 | |
|
12be94e0db | |
|
cddfb9c77d | |
|
298635dad1 | |
|
8a2c01b3d8 | |
|
f4b167698a | |
|
0a6740cc30 | |
|
0d1f74e2cd | |
|
355e40d750 | |
|
e15a656714 | |
|
baa50518cf | |
|
697614dd45 | |
|
d9ae32429b | |
|
a21fca6089 | |
|
60ea3e96bc | |
|
4de3aa77e3 | |
|
668a9d0ed6 | |
|
f71030f8c2 | |
|
92ab09338e | |
|
f27cd2ccfb | |
|
29a17f39d5 | |
|
fa8bca8d6e | |
|
dac1295ad7 | |
|
f476613ab1 | |
|
57a053bbf2 | |
|
4edce47b24 | |
|
63beaa21fe | |
|
7f2cdc106c | |
|
5016703f21 | |
|
acdfb432d0 | |
|
fab5bd4fc5 | |
|
e4cb2afdb7 | |
|
91970f975a | |
|
3e1ae2d413 | |
|
a34f321d2b | |
|
94c786f2f7 | |
|
ca3ec134d6 | |
|
b0d6c4a7d2 | |
|
3ecb5e1cfe | |
|
80b0bb8392 | |
|
bee157a792 | |
|
1d96a678a5 | |
|
d4e0efcab3 | |
|
a673a3884c | |
|
ea4f3ecb05 | |
|
f3ee41a0bc | |
|
5815ca8211 | |
|
fa7cb923cd | |
|
d4bee2b764 | |
|
f72a29671f | |
|
2c1ebdcc2e | |
|
9f4e9d8532 | |
|
17512f6e77 | |
|
1ec7cf0a37 | |
|
c3ab932012 | |
|
06cb481c3f | |
|
ef0478cc97 | |
|
308c18b1e0 | |
|
61c0c85ab5 | |
|
b3c43b60cd | |
|
a0bc204159 | |
|
b4f1b2f02c | |
|
54ee8d8bb5 | |
|
3f5e49c3d6 | |
|
7d64a92ce7 | |
|
ff347dd170 | |
|
d146476757 | |
|
e07feb838f | |
|
85da50cbc8 | |
|
d85ff32abb | |
|
2c3b0b4d0b | |
|
6b6ca95e2d | |
|
eb640d8da0 | |
|
fcbe028e11 | |
|
8f08398c30 | |
|
d1ae8aad90 | |
|
4922f425fc | |
|
6d5d7f0d4a | |
|
ae4d53d08d | |
|
5252fba376 | |
|
8cc71a4dc0 | |
|
dcc58a9d50 | |
|
9cc473ce52 | |
|
cd447144e2 | |
|
ed76a3475a | |
|
2e1ca9e3d0 | |
|
df061b09d7 | |
|
9a28ed103e | |
|
046491f1b6 | |
|
46ab65cf50 | |
|
1439446b36 | |
|
3229fdbc08 | |
|
6808159427 | |
|
953cfa9e2d | |
|
ee4d79fdf0 | |
|
927387502c | |
|
31913c048d | |
|
602eebf2fb | |
|
4e66009963 | |
|
c58bff6c3d | |
|
f9a58083ae | |
|
5f5e874564 | |
|
9fb393196e | |
|
1abbbc18e1 | |
|
18d9c75abd | |
|
72b57ca68d | |
|
a31d5eea5b | |
|
ed8b651b98 | |
|
246da8fc5e | |
|
8a6ae998e4 | |
|
8d0f319dd6 | |
|
a835d09bc8 | |
|
9d259ce5a3 | |
|
54bf395249 | |
|
640f3a8ca7 | |
|
0cd157ca3d | |
|
2f0dd6d6ec | |
|
0ab33a82d4 | |
|
92ed2138fb | |
|
d5679666d9 | |
|
489ed919a5 | |
|
d544ae1bf8 | |
|
c3ba632aa3 | |
|
8061d1216d | |
|
bcc2d99a95 | |
|
845b73d4eb | |
|
6752dd3af3 | |
|
d4b8cf670f | |
|
3eb9009741 | |
|
21ff383eb3 | |
|
eec85367e7 | |
|
252b6f41c6 | |
|
6e1defcb18 | |
|
92a5a08671 | |
|
32bdeccebf | |
|
f5acf6aebd | |
|
f76edd5d61 | |
|
554ccbd035 | |
|
94f9a1bd05 | |
|
da09876585 | |
|
832e2c391c | |
|
a6f8ec672d | |
|
d78d4c9c29 | |
|
d13d2a7ab0 | |
|
b48f7173ee | |
|
9a75ee6f30 | |
|
6220d1f986 | |
|
aef6dca30c | |
|
387be29185 | |
|
3dc0d5a64e | |
|
69f7f0941f | |
|
9db0682b07 | |
|
32b879cf73 | |
|
6c0b159a84 | |
|
987f29e609 | |
|
40f0690573 | |
|
ca21243067 | |
|
3437d782d9 | |
|
0f5136d072 | |
|
358fdbf184 | |
|
4ec0ac2218 | |
|
5567917c12 | |
|
b80666a507 | |
|
98eed7238d | |
|
4ff9a4c2ef | |
|
01c1656fc2 | |
|
cc95860c68 | |
|
88e4de5341 | |
|
3dfd86f6ff | |
|
edd5275b8b | |
|
ba5f36fb91 | |
|
01a9fa2ab2 | |
|
22345cfcda | |
|
21ce174939 | |
|
da0a543983 | |
|
d4769c7adf | |
|
1a62ed7f27 | |
|
eec326dc80 | |
|
80a0b7d7ad | |
|
21bdb28d37 | |
|
7aebf0d132 | |
|
899ab457e9 | |
|
32ecd2fc5c | |
|
a259e59afc | |
|
f13e584686 | |
|
59a82a9d5e | |
|
b087c930ed | |
|
5fd1766a81 | |
|
19618209c8 | |
|
57ca624979 | |
|
360ec41180 | |
|
a751230cd9 | |
|
bb9a1b5b24 | |
|
79a21b824d | |
|
3239942ff1 | |
|
1c1f2c2e8c | |
|
b52a7217a9 | |
|
de0f51b664 | |
|
39da31bb5a | |
|
5d18d41b28 | |
|
caaa296a82 | |
|
48205fb2bd | |
|
7546ee531d | |
|
46435bde0f | |
|
17a046d7a1 | |
|
389fd85218 | |
|
fd0b6fcb96 | |
|
ea21169da0 | |
|
98816217c9 | |
|
1b55d35542 | |
|
68ed6166d8 | |
|
0fa3327112 | |
|
8f7e032a01 | |
|
c22803c3a3 | |
|
b53a22bfb3 | |
|
7ed003e973 | |
|
7117dbcf12 | |
|
1b053d44fe | |
|
2e06ded55f | |
|
3bf9065203 | |
|
45e523d66c | |
|
ec51e833b6 | |
|
03e85b838d | |
|
4a02f97572 | |
|
42c83f69a0 | |
|
74aba5f80c | |
|
1fc51d1296 | |
|
a2e199f025 | |
|
103935cef7 | |
|
83b5d72073 | |
|
6bbe7be266 | |
|
6502309e34 | |
|
ed45358be8 | |
|
ddb5327e64 | |
|
276fb5bf69 | |
|
8d295b70f6 | |
|
70cec0755a | |
|
eb8f398c6a | |
|
a9628b9f63 | |
|
5c3d021fe7 | |
|
4fd1d2bcd4 | |
|
9ec52ec415 | |
|
31158f0427 | |
|
aa47f79ca6 | |
|
09da46a34d | |
|
e723d8b641 | |
|
a6f8c07301 | |
|
817e76e424 | |
|
363bc4d6c1 | |
|
8a6deb868c | |
|
8ab6fccdea | |
|
341390a74e | |
|
ca1baee0f9 | |
|
04e26fae5c | |
|
1559db2ca3 | |
|
dd78b36a17 | |
|
d4a869bddf | |
|
6964914bab | |
|
8acac40ec3 | |
|
097984154b | |
|
ed766f55b4 | |
|
4d7154a31a | |
|
462cec4c1c | |
|
7bc303b4c5 | |
|
9b39a59813 | |
|
87bbf4502a | |
|
0e6db30640 | |
|
35d9fa4b54 | |
|
095dce0373 | |
|
6b40c69bb3 | |
|
dcf88baf68 | |
|
972a657759 | |
|
455f0fc0ac | |
|
69b2b41692 | |
|
d16c789638 | |
|
04f5434894 | |
|
0ca24389b2 | |
|
4cd84dcd8a | |
|
159aaf5afa | |
|
58f32dd70e | |
|
88d41f84fd | |
|
a9c7801380 | |
|
9e0405de9f | |
|
1e7adb21ae | |
|
f9702b0ace | |
|
2a86e9535b | |
|
eb09681f21 | |
|
d69ab1e411 | |
|
982b8ac228 | |
|
b5b900e0ff | |
|
b4e230389d | |
|
3302391ac7 | |
|
913dd3a188 | |
|
93d487ffc0 | |
|
05f35731b9 | |
|
b9acace932 | |
|
804265b801 | |
|
632bbeec8f | |
|
0e637962d5 | |
|
782e89758e | |
|
21ec3c8899 | |
|
91c1df96f0 | |
|
e7f49af7f4 | |
|
7ed5cf88ca | |
|
f86ee167c1 | |
|
ab5254fcba | |
|
d26d6ff3ed | |
|
524058d8fc | |
|
cd9e5d4173 | |
|
b0959b7ad8 | |
|
ef9ad587c8 | |
|
c317204c74 | |
|
0ad150cb6e | |
|
18da6993d9 | |
|
804c71d7c6 | |
|
76bd587198 | |
|
7252c7686c | |
|
77eee6a817 | |
|
c9be67e4d3 | |
|
e06bd1a8b0 | |
|
492e7da3e9 | |
|
27c9e263e0 | |
|
2741d50342 | |
|
f1f0100311 | |
|
865789017d | |
|
08a2786b04 | |
|
70984bd5c0 | |
|
889c561ed3 | |
|
4b30c27fa2 | |
|
dd7fb60b41 | |
|
4ff035aac7 | |
|
05556ff661 | |
|
186e3e8f92 | |
|
cdb2605633 | |
|
e8179e119d | |
|
d4040dc39d | |
|
1af03613cc | |
|
b0a62422c1 | |
|
326f11c8d0 | |
|
d630b764a3 | |
|
e0ce1fc446 | |
|
4717fb3dbd |
|
@ -0,0 +1,2 @@
|
|||
dist
|
||||
coverage
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"extends": "loopback",
|
||||
"rules": {
|
||||
"max-len": ["error", 100, 4, {
|
||||
"ignoreComments": true,
|
||||
"ignoreUrls": true,
|
||||
"ignorePattern": "^\\s*var\\s.+=\\s*(require\\s*\\()|(/)"
|
||||
}]
|
||||
}
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
labels: bug
|
||||
|
||||
---
|
||||
|
||||
<!-- 🚨 STOP 🚨 STOP 🚨 STOP 🚨
|
||||
|
||||
Are you using LoopBack version 4? Please report the bug here:
|
||||
https://github.com/strongloop/loopback-next/issues/new
|
||||
|
||||
HELP US HELP YOU, PLEASE
|
||||
- Do a quick search to avoid duplicate issues
|
||||
- Provide as much information as possible (reproduction sandbox, use case for features, etc.)
|
||||
- Consider using a more suitable venue for questions such as Stack Overflow, Gitter, etc.
|
||||
|
||||
Please fill in the *entire* template below.
|
||||
|
||||
-->
|
||||
|
||||
## Steps to reproduce
|
||||
|
||||
<!-- Describe how to reproduce the issue -->
|
||||
|
||||
## Current Behavior
|
||||
|
||||
<!-- Describe the observed result -->
|
||||
|
||||
## Expected Behavior
|
||||
|
||||
<!-- Describe what did you expect instead, what is the desired outcome? -->
|
||||
|
||||
## Link to reproduction sandbox
|
||||
|
||||
<!--
|
||||
See https://loopback.io/doc/en/contrib/Reporting-issues.html#loopback-3x-bugs
|
||||
Note: Failure to provide a sandbox application for reproduction purposes will result in the issue being closed.
|
||||
-->
|
||||
|
||||
## Additional information
|
||||
|
||||
<!--
|
||||
Copy+paste the output of these two commands:
|
||||
node -e 'console.log(process.platform, process.arch, process.versions.node)'
|
||||
npm ls --prod --depth 0 | grep loopback
|
||||
-->
|
||||
|
||||
## Related Issues
|
||||
|
||||
<!-- Did you find other bugs that looked similar? -->
|
||||
|
||||
_See [Reporting Issues](http://loopback.io/doc/en/contrib/Reporting-issues.html) for more tips on writing good issues_
|
|
@ -0,0 +1,34 @@
|
|||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
labels: feature
|
||||
|
||||
---
|
||||
|
||||
<!-- 🚨 STOP 🚨 STOP 🚨 STOP 🚨
|
||||
|
||||
LoopBack version 3 is in LTS mode, we are not accepting new features.
|
||||
|
||||
We are actively developing version 4, you can find the new GitHub
|
||||
repository here: https://github.com/strongloop/loopback-next
|
||||
|
||||
-->
|
||||
|
||||
## Suggestion
|
||||
|
||||
<!-- A summary of what you'd like to see added or changed -->
|
||||
|
||||
## Use Cases
|
||||
|
||||
<!--
|
||||
What do you want to use this for?
|
||||
What shortcomings exist with current approaches?
|
||||
-->
|
||||
|
||||
## Examples
|
||||
|
||||
<!-- Show how this would be used and what the behavior would be -->
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
TBD - will be filled by the team.
|
|
@ -0,0 +1,27 @@
|
|||
---
|
||||
name: Question
|
||||
about: The issue tracker is not for questions. Please use Stack Overflow or other resources for help.
|
||||
labels: question
|
||||
|
||||
---
|
||||
|
||||
<!-- 🚨 STOP 🚨 STOP 🚨 STOP 🚨
|
||||
|
||||
THE ISSUE TRACKER IS NOT FOR QUESTIONS.
|
||||
|
||||
DO NOT CREATE A NEW ISSUE TO ASK A QUESTION.
|
||||
|
||||
Please use one of the following resources for help:
|
||||
|
||||
**Questions**
|
||||
|
||||
- https://stackoverflow.com/tags/loopbackjs
|
||||
- https://groups.google.com/forum/#!forum/loopbackjs
|
||||
- https://gitter.im/strongloop/loopback
|
||||
|
||||
**Immediate support**
|
||||
|
||||
- https://strongloop.com/api-connect-faqs/
|
||||
- https://strongloop.com/node-js/subscription-plans/
|
||||
|
||||
-->
|
|
@ -0,0 +1,16 @@
|
|||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Report a security vulnerability
|
||||
url: https://loopback.io/doc/en/contrib/Reporting-issues.html#security-issues
|
||||
about: >
|
||||
LoopBack 3 has reached End-of-Life. No new security fixes will be provided
|
||||
or accepted.
|
||||
|
||||
Do not report security vulnerabilities using GitHub issues. Please send an
|
||||
email to `reachsl@us.ibm.com` instead.
|
||||
- name: Get help on StackOverflow
|
||||
url: https://stackoverflow.com/tags/loopbackjs
|
||||
about: Please ask and answer questions on StackOverflow.
|
||||
- name: Join our mailing list
|
||||
url: https://groups.google.com/forum/#!forum/loopbackjs
|
||||
about: You can also post your question to our mailing list.
|
|
@ -0,0 +1,18 @@
|
|||
<!--
|
||||
Please provide a high-level description of the changes made by your pull request.
|
||||
|
||||
Include references to all related GitHub issues and other pull requests, for example:
|
||||
|
||||
Fixes #123
|
||||
Implements #254
|
||||
See also #23
|
||||
-->
|
||||
|
||||
## Checklist
|
||||
|
||||
👉 [Read and sign the CLA (Contributor License Agreement)](https://cla.strongloop.com/agreements/strongloop/loopback) 👈
|
||||
|
||||
- [ ] `npm test` passes on your machine
|
||||
- [ ] New tests added or existing tests modified to cover all changes
|
||||
- [ ] Code conforms with the [style guide](https://loopback.io/doc/en/contrib/style-guide-es6.html)
|
||||
- [ ] Commit messages are following our [guidelines](https://loopback.io/doc/en/contrib/git-commit-messages.html)
|
|
@ -0,0 +1,21 @@
|
|||
# Number of days of inactivity before an issue becomes stale
|
||||
daysUntilStale: 60
|
||||
# Number of days of inactivity before a stale issue is closed
|
||||
daysUntilClose: 14
|
||||
# Issues with these labels will never be considered stale
|
||||
exemptLabels:
|
||||
- pinned
|
||||
- security
|
||||
- critical
|
||||
# Label to use when marking an issue as stale
|
||||
staleLabel: stale
|
||||
# Comment to post when marking an issue as stale. Set to `false` to disable
|
||||
markComment: >
|
||||
This issue has been automatically marked as stale because it has not had
|
||||
recent activity. It will be closed if no further activity occurs. Thank you
|
||||
for your contributions.
|
||||
# Comment to post when closing a stale issue. Set to `false` to disable
|
||||
closeComment: >
|
||||
This issue has been closed due to continued inactivity. Thank you for your understanding.
|
||||
If you believe this to be in error, please contact one of the code owners,
|
||||
listed in the [`CODEOWNERS`](https://github.com/strongloop/loopback/blob/master/CODEOWNERS) file at the top-level of this repository.
|
|
@ -1,6 +1,7 @@
|
|||
.idea
|
||||
.project
|
||||
.DS_Store
|
||||
.vscode/
|
||||
*.sublime*
|
||||
*.seed
|
||||
*.log
|
||||
|
@ -13,3 +14,4 @@
|
|||
node_modules
|
||||
dist
|
||||
*xunit.xml
|
||||
.nyc_output/
|
||||
|
|
23
.jscsrc
23
.jscsrc
|
@ -1,23 +0,0 @@
|
|||
{
|
||||
"preset": "google",
|
||||
"requireCurlyBraces": [
|
||||
"else",
|
||||
"for",
|
||||
"while",
|
||||
"do",
|
||||
"try",
|
||||
"catch"
|
||||
],
|
||||
"disallowMultipleVarDecl": "exceptUndefined",
|
||||
"disallowSpacesInsideObjectBrackets": null,
|
||||
"maximumLineLength": {
|
||||
"value": 150,
|
||||
"allowComments": true,
|
||||
"allowRegex": true
|
||||
},
|
||||
"validateJSDoc": {
|
||||
"checkParamNames": false,
|
||||
"checkRedundantParams": false,
|
||||
"requireParamTypes": true
|
||||
}
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
node_modules
|
34
.jshintrc
34
.jshintrc
|
@ -1,34 +0,0 @@
|
|||
{
|
||||
"node": true,
|
||||
"camelcase": true,
|
||||
"eqnull": true,
|
||||
"indent": 2,
|
||||
"undef": true,
|
||||
"quotmark": "single",
|
||||
"newcap": true,
|
||||
"nonew": true,
|
||||
"sub": true,
|
||||
"laxcomma": true,
|
||||
"laxbreak": true,
|
||||
"globals": {
|
||||
/* mocha */
|
||||
"after": true,
|
||||
"afterEach": true,
|
||||
"assert": true,
|
||||
"before": true,
|
||||
"beforeEach": true,
|
||||
"context": true,
|
||||
"describe": true,
|
||||
"expect": true,
|
||||
"it": true,
|
||||
|
||||
/* loopback */
|
||||
"app": true,
|
||||
"assertValidDataSource": true,
|
||||
"GeoPoint": true,
|
||||
"loopback": true,
|
||||
"memoryConnector": true,
|
||||
"request": true,
|
||||
"TaskEmitter": true
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"exclude": [
|
||||
"Gruntfile.js",
|
||||
"test/**/*.js"
|
||||
],
|
||||
"cache": true
|
||||
}
|
14
.travis.yml
14
.travis.yml
|
@ -1,7 +1,15 @@
|
|||
sudo: false
|
||||
language: node_js
|
||||
node_js:
|
||||
- "0.10"
|
||||
- "0.12"
|
||||
- "iojs"
|
||||
- "8"
|
||||
- "10"
|
||||
- "12"
|
||||
- "14"
|
||||
|
||||
addons:
|
||||
chrome: stable
|
||||
|
||||
after_success: npm run coverage
|
||||
|
||||
before_install:
|
||||
- npm config set registry http://ci.strongloop.com:4873/
|
||||
|
|
1010
CHANGES.md
1010
CHANGES.md
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,11 @@
|
|||
# Lines starting with '#' are comments.
|
||||
# Each line is a file pattern followed by one or more owners,
|
||||
# the last matching pattern has the most precendence.
|
||||
|
||||
# Current maintainers
|
||||
|
||||
* @bajtos @fabien @clarkorz @ebarault @zbarbuto @nitro404
|
||||
|
||||
# Alumni
|
||||
|
||||
_ @lehni
|
|
@ -147,5 +147,5 @@ Contributing to `loopback` is easy. In a few simple steps:
|
|||
inaccurate in any respect. Email us at callback@strongloop.com.
|
||||
```
|
||||
|
||||
[Google C++ Style Guide]: https://google-styleguide.googlecode.com/svn/trunk/cppguide.xml
|
||||
[Google Javascript Style Guide]: https://google-styleguide.googlecode.com/svn/trunk/javascriptguide.xml
|
||||
[Google C++ Style Guide]: https://google.github.io/styleguide/cppguide.html
|
||||
[Google Javascript Style Guide]: https://google.github.io/styleguide/javascriptguide.xml
|
||||
|
|
133
Gruntfile.js
133
Gruntfile.js
|
@ -1,6 +1,11 @@
|
|||
/*global module:false*/
|
||||
module.exports = function(grunt) {
|
||||
// Copyright IBM Corp. 2014,2019. All Rights Reserved.
|
||||
// Node module: loopback
|
||||
// This file is licensed under the MIT License.
|
||||
// License text available at https://opensource.org/licenses/MIT
|
||||
|
||||
'use strict';
|
||||
|
||||
module.exports = function(grunt) {
|
||||
// Do not report warnings from unit-tests exercising deprecated paths
|
||||
process.env.NO_DEPRECATION = 'loopback';
|
||||
|
||||
|
@ -18,58 +23,56 @@ module.exports = function(grunt) {
|
|||
// Task configuration.
|
||||
uglify: {
|
||||
options: {
|
||||
banner: '<%= banner %>'
|
||||
banner: '<%= banner %>',
|
||||
},
|
||||
dist: {
|
||||
files: {
|
||||
'dist/loopback.min.js': ['dist/loopback.js']
|
||||
}
|
||||
}
|
||||
},
|
||||
jshint: {
|
||||
options: {
|
||||
jshintrc: true
|
||||
'dist/loopback.min.js': ['dist/loopback.js'],
|
||||
},
|
||||
},
|
||||
},
|
||||
eslint: {
|
||||
gruntfile: {
|
||||
src: 'Gruntfile.js'
|
||||
src: 'Gruntfile.js',
|
||||
},
|
||||
lib: {
|
||||
src: ['lib/**/*.js']
|
||||
src: ['lib/**/*.js'],
|
||||
},
|
||||
common: {
|
||||
src: ['common/**/*.js']
|
||||
},
|
||||
browser: {
|
||||
src: ['browser/**/*.js']
|
||||
src: ['common/**/*.js'],
|
||||
},
|
||||
server: {
|
||||
src: ['server/**/*.js']
|
||||
src: ['server/**/*.js'],
|
||||
},
|
||||
test: {
|
||||
src: ['test/**/*.js']
|
||||
}
|
||||
},
|
||||
jscs: {
|
||||
gruntfile: 'Gruntfile.js',
|
||||
lib: ['lib/**/*.js'],
|
||||
common: ['common/**/*.js'],
|
||||
server: ['server/**/*.js'],
|
||||
browser: ['browser/**/*.js'],
|
||||
test: ['test/**/*.js']
|
||||
src: ['test/**/*.js'],
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
gruntfile: {
|
||||
files: '<%= jshint.gruntfile.src %>',
|
||||
tasks: ['jshint:gruntfile']
|
||||
files: '<%= eslint.gruntfile.src %>',
|
||||
tasks: ['eslint:gruntfile'],
|
||||
},
|
||||
browser: {
|
||||
files: ['<%= eslint.browser.src %>'],
|
||||
tasks: ['eslint:browser'],
|
||||
},
|
||||
common: {
|
||||
files: ['<%= eslint.common.src %>'],
|
||||
tasks: ['eslint:common'],
|
||||
},
|
||||
lib: {
|
||||
files: ['<%= jshint.lib.src %>'],
|
||||
tasks: ['jshint:lib']
|
||||
files: ['<%= eslint.lib.src %>'],
|
||||
tasks: ['eslint:lib'],
|
||||
},
|
||||
server: {
|
||||
files: ['<%= eslint.server.src %>'],
|
||||
tasks: ['eslint:server'],
|
||||
},
|
||||
test: {
|
||||
files: ['<%= jshint.test.src %>'],
|
||||
tasks: ['jshint:test']
|
||||
}
|
||||
files: ['<%= eslint.test.src %>'],
|
||||
tasks: ['eslint:test'],
|
||||
},
|
||||
},
|
||||
browserify: {
|
||||
dist: {
|
||||
|
@ -78,29 +81,30 @@ module.exports = function(grunt) {
|
|||
},
|
||||
options: {
|
||||
ignore: ['nodemailer', 'passport', 'bcrypt'],
|
||||
standalone: 'loopback'
|
||||
}
|
||||
}
|
||||
standalone: 'loopback',
|
||||
},
|
||||
},
|
||||
},
|
||||
mochaTest: {
|
||||
'unit': {
|
||||
src: 'test/*.js',
|
||||
options: {
|
||||
reporter: 'dot',
|
||||
}
|
||||
require: require.resolve('./test/helpers/use-english.js'),
|
||||
},
|
||||
},
|
||||
'unit-xml': {
|
||||
src: 'test/*.js',
|
||||
options: {
|
||||
reporter: 'xunit',
|
||||
captureFile: 'xunit.xml'
|
||||
}
|
||||
}
|
||||
captureFile: 'xunit.xml',
|
||||
},
|
||||
},
|
||||
},
|
||||
karma: {
|
||||
'unit-once': {
|
||||
configFile: 'test/karma.conf.js',
|
||||
browsers: ['PhantomJS'],
|
||||
browsers: ['ChromeDocker'],
|
||||
singleRun: true,
|
||||
reporters: ['dots', 'junit'],
|
||||
|
||||
|
@ -109,7 +113,7 @@ module.exports = function(grunt) {
|
|||
|
||||
// CI friendly test output
|
||||
junitReporter: {
|
||||
outputFile: 'karma-xunit.xml'
|
||||
outputFile: 'karma-xunit.xml',
|
||||
},
|
||||
|
||||
browserify: {
|
||||
|
@ -117,8 +121,8 @@ module.exports = function(grunt) {
|
|||
// Fatal error: Maximum call stack size exceeded
|
||||
debug: false,
|
||||
// Disable watcher, grunt will exit after the first run
|
||||
watch: false
|
||||
}
|
||||
watch: false,
|
||||
},
|
||||
},
|
||||
unit: {
|
||||
configFile: 'test/karma.conf.js',
|
||||
|
@ -134,7 +138,7 @@ module.exports = function(grunt) {
|
|||
// list of files / patterns to load in the browser
|
||||
files: [
|
||||
'test/e2e/remote-connector.e2e.js',
|
||||
'test/e2e/replication.e2e.js'
|
||||
'test/e2e/replication.e2e.js',
|
||||
],
|
||||
|
||||
// list of files to exclude
|
||||
|
@ -171,7 +175,7 @@ module.exports = function(grunt) {
|
|||
// - PhantomJS
|
||||
// - IE (only Windows)
|
||||
browsers: [
|
||||
'Chrome'
|
||||
'Chrome',
|
||||
],
|
||||
|
||||
// If browser does not capture in given timeout [ms], kill it
|
||||
|
@ -190,7 +194,7 @@ module.exports = function(grunt) {
|
|||
'passport-local',
|
||||
'superagent',
|
||||
'supertest',
|
||||
'bcrypt'
|
||||
'bcrypt',
|
||||
],
|
||||
// transform: ['coffeeify'],
|
||||
// debug: true,
|
||||
|
@ -199,25 +203,32 @@ module.exports = function(grunt) {
|
|||
},
|
||||
|
||||
// Add browserify to preprocessors
|
||||
preprocessors: {'test/e2e/*': ['browserify']}
|
||||
}
|
||||
}
|
||||
}
|
||||
preprocessors: {'test/e2e/*': ['browserify']},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
// These plugins provide necessary tasks.
|
||||
grunt.loadNpmTasks('grunt-browserify');
|
||||
grunt.loadNpmTasks('grunt-contrib-uglify');
|
||||
grunt.loadNpmTasks('grunt-contrib-jshint');
|
||||
grunt.loadNpmTasks('grunt-eslint');
|
||||
grunt.loadNpmTasks('grunt-contrib-watch');
|
||||
grunt.loadNpmTasks('grunt-jscs');
|
||||
grunt.loadNpmTasks('grunt-karma');
|
||||
|
||||
grunt.registerTask('e2e-server', function() {
|
||||
var done = this.async();
|
||||
var app = require('./test/fixtures/e2e/app');
|
||||
app.listen(3000, done);
|
||||
const done = this.async();
|
||||
const app = require('./test/fixtures/e2e/app');
|
||||
app.listen(0, function() {
|
||||
process.env.PORT = this.address().port;
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
grunt.registerTask('skip-karma', function() {
|
||||
console.log(`*** SKIPPING PHANTOM-JS BASED TESTS ON ${process.platform}` +
|
||||
` ${process.arch} ***`);
|
||||
});
|
||||
|
||||
grunt.registerTask('e2e', ['e2e-server', 'karma:e2e']);
|
||||
|
@ -226,10 +237,12 @@ module.exports = function(grunt) {
|
|||
grunt.registerTask('default', ['browserify']);
|
||||
|
||||
grunt.registerTask('test', [
|
||||
'jscs',
|
||||
'jshint',
|
||||
'eslint',
|
||||
process.env.JENKINS_HOME ? 'mochaTest:unit-xml' : 'mochaTest:unit',
|
||||
'karma:unit-once']);
|
||||
process.env.JENKINS_HOME && (/^win/.test(process.platform) ||
|
||||
/^s390x/.test(process.arch) || /^ppc64/.test(process.arch)) ?
|
||||
'skip-karma' : 'karma:unit-once',
|
||||
]);
|
||||
|
||||
// alias for sl-ci-run and `npm test`
|
||||
grunt.registerTask('mocha-and-karma', ['test']);
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
Copyright (c) IBM Corp. 2013,2018. All Rights Reserved.
|
||||
Node module: loopback
|
||||
This project is licensed under the MIT License, full text below.
|
||||
|
||||
--------
|
||||
|
||||
MIT license
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
|
@ -1,9 +0,0 @@
|
|||
Copyright (c) 2013-2015 StrongLoop, Inc and other contributors.
|
||||
|
||||
loopback uses a dual license model.
|
||||
|
||||
You may use this library under the terms of the [MIT License][],
|
||||
or under the terms of the [StrongLoop Subscription Agreement][].
|
||||
|
||||
[MIT License]: http://opensource.org/licenses/MIT
|
||||
[StrongLoop Subscription Agreement]: http://strongloop.com/license
|
53
README.md
53
README.md
|
@ -1,6 +1,24 @@
|
|||
# LoopBack
|
||||
|
||||
[](https://gitter.im/strongloop/loopback?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
||||
[](http://github.com/CloudNativeJS/ModuleLTS)
|
||||
[](http://ibm.biz/node-support)
|
||||
|
||||
**⚠️ LoopBack 3 has reached end of life. We are no longer accepting pull requests or providing
|
||||
support for community users. The only exception is fixes for critical bugs and security
|
||||
vulnerabilities provided as part of support for IBM API Connect customers.
|
||||
We urge all LoopBack 3 users to migrate their applications to LoopBack 4 as soon as possible.
|
||||
Learn more about
|
||||
<a href="https://loopback.io/doc/en/contrib/Long-term-support.html">LoopBack's long term support policy.</a>
|
||||
will be provided or accepted. (See
|
||||
[Module Long Term Support Policy](#module-long-term-support-policy) below.)**
|
||||
|
||||
We urge all LoopBack 3 users to migrate their applications to LoopBack 4 as
|
||||
soon as possible. Refer to our
|
||||
[Migration Guide](https://loopback.io/doc/en/lb4/migration-overview.html)
|
||||
for more information on how to upgrade.
|
||||
|
||||
## Overview
|
||||
|
||||
LoopBack is a highly-extensible, open-source Node.js framework that enables you to:
|
||||
|
||||
|
@ -18,10 +36,24 @@ LoopBack consists of:
|
|||
* Client SDKs for iOS, Android, and web clients.
|
||||
|
||||
LoopBack tools include:
|
||||
* Command-line tool `slc loopback` to create applications, models, data sources, and so on.
|
||||
* StrongLoop Arc, a graphical tool for editing LoopBack applications; and for deploying and monitoring applications.
|
||||
* Command-line tool `loopback-cli` to create applications, models, data sources, and so on.
|
||||
|
||||
For more details, see [http://loopback.io/](http://loopback.io/).
|
||||
For more details, see [https://loopback.io/](https://loopback.io/).
|
||||
|
||||
|
||||
## Module Long Term Support Policy
|
||||
|
||||
LoopBack 3.x has reached End-of-Life.
|
||||
|
||||
This module adopts the [Module Long Term Support (LTS)](http://github.com/CloudNativeJS/ModuleLTS) policy, with the following End Of Life (EOL) dates:
|
||||
|
||||
| Version | Status | Published | EOL |
|
||||
| ---------- | --------------- | --------- | -------------------- |
|
||||
| LoopBack 4 | Current | Oct 2018 | Apr 2023 _(minimum)_ |
|
||||
| LoopBack 3 | End-of-Life | Dec 2016 | Dec 2020 |
|
||||
| LoopBack 2 | End-of-Life | Jul 2014 | Apr 2019 |
|
||||
|
||||
Learn more about our LTS plan in [docs](https://loopback.io/doc/en/contrib/Long-term-support.html).
|
||||
|
||||
## LoopBack modules
|
||||
|
||||
|
@ -48,7 +80,7 @@ The LoopBack framework is a set of Node.js modules that you can use independentl
|
|||
|
||||
### Community Connectors
|
||||
|
||||
The LoopBack community has created and supports a number of additional connectors. See [Community connectors](http://docs.strongloop.com/display/LB/Community+connectors) for details.
|
||||
The LoopBack community has created and supports a number of additional connectors. See [Community connectors](https://loopback.io/doc/en/lb2/Community-connectors.html) for details.
|
||||
|
||||
### Components
|
||||
* [loopback-component-push](https://github.com/strongloop/loopback-component-push)
|
||||
|
@ -71,22 +103,23 @@ The LoopBack community has created and supports a number of additional connector
|
|||
|
||||
StrongLoop provides a number of example applications that illustrate various key LoopBack features. In some cases, they have accompanying step-by-step instructions (tutorials).
|
||||
|
||||
See [loopback-example](https://github.com/strongloop/loopback-example) for details.
|
||||
See [examples at loopback.io](https://loopback.io/examples/) for details.
|
||||
|
||||
## Resources
|
||||
|
||||
* [Documentation](http://docs.strongloop.com/display/LB/LoopBack).
|
||||
* [API documentation](http://apidocs.strongloop.com/loopback).
|
||||
* [Documentation](https://loopback.io/doc/).
|
||||
* [API documentation](https://apidocs.strongloop.com/loopback).
|
||||
* [LoopBack Announcements](https://groups.google.com/forum/#!forum/loopbackjs-announcements)
|
||||
* [LoopBack Google Group](https://groups.google.com/forum/#!forum/loopbackjs).
|
||||
* [GitHub issues](https://github.com/strongloop/loopback/issues).
|
||||
* [Gitter chat](https://gitter.im/strongloop/loopback).
|
||||
|
||||
## Contributing
|
||||
|
||||
See https://github.com/strongloop/loopback/wiki/Contributing-code
|
||||
Contributions to the LoopBack project are welcome! See [Contributing to LoopBack](https://loopback.io/doc/en/contrib/index.html) for more information.
|
||||
|
||||
## Issues
|
||||
## Reporting issues
|
||||
|
||||
See https://github.com/strongloop/loopback/wiki/Reporting-issues
|
||||
One of the easiest ways to contribute to LoopBack is to report an issue. See [Reporting issues](https://loopback.io/doc/en/contrib/Reporting-issues.html) for more information.
|
||||
|
||||
[](https://github.com/strongloop/loopback)
|
||||
|
|
|
@ -1,10 +0,0 @@
|
|||
module.exports = function(loopback) {
|
||||
loopback.getCurrentContext = function() {
|
||||
return null;
|
||||
};
|
||||
|
||||
loopback.runInContext =
|
||||
loopback.createContext = function() {
|
||||
throw new Error('Current context is not supported in the browser.');
|
||||
};
|
||||
};
|
|
@ -1,11 +1,18 @@
|
|||
// Copyright IBM Corp. 2014,2019. All Rights Reserved.
|
||||
// Node module: loopback
|
||||
// This file is licensed under the MIT License.
|
||||
// License text available at https://opensource.org/licenses/MIT
|
||||
|
||||
/*!
|
||||
* Module Dependencies.
|
||||
*/
|
||||
|
||||
var loopback = require('../../lib/loopback');
|
||||
var assert = require('assert');
|
||||
var uid = require('uid2');
|
||||
var DEFAULT_TOKEN_LEN = 64;
|
||||
'use strict';
|
||||
const g = require('../../lib/globalize');
|
||||
const loopback = require('../../lib/loopback');
|
||||
const assert = require('assert');
|
||||
const uid = require('uid2');
|
||||
const DEFAULT_TOKEN_LEN = 64;
|
||||
|
||||
/**
|
||||
* Token based authentication and access control.
|
||||
|
@ -27,13 +34,6 @@ var DEFAULT_TOKEN_LEN = 64;
|
|||
*/
|
||||
|
||||
module.exports = function(AccessToken) {
|
||||
|
||||
// Workaround for https://github.com/strongloop/loopback/issues/292
|
||||
AccessToken.definition.rawProperties.created.default =
|
||||
AccessToken.definition.properties.created.default = function() {
|
||||
return new Date();
|
||||
};
|
||||
|
||||
/**
|
||||
* Anonymous Token
|
||||
*
|
||||
|
@ -79,94 +79,25 @@ module.exports = function(AccessToken) {
|
|||
});
|
||||
|
||||
/**
|
||||
* Find a token for the given `ServerRequest`.
|
||||
*
|
||||
* @param {ServerRequest} req
|
||||
* @param {Object} [options] Options for finding the token
|
||||
* @callback {Function} callback
|
||||
* @param {Error} err
|
||||
* @param {AccessToken} token
|
||||
* Extract the access token id from the HTTP request
|
||||
* @param {Request} req HTTP request object
|
||||
* @options {Object} [options] Each option array is used to add additional keys to find an `accessToken` for a `request`.
|
||||
* @property {Array} [cookies] Array of cookie names.
|
||||
* @property {Array} [headers] Array of header names.
|
||||
* @property {Array} [params] Array of param names.
|
||||
* @property {Boolean} [searchDefaultTokenKeys] Use the default search locations for Token in request
|
||||
* @property {Boolean} [bearerTokenBase64Encoded] Defaults to `true`. For `Bearer` token based `Authorization` headers,
|
||||
* decode the value from `Base64`. If set to `false`, the decoding will be skipped and the token id will be the raw value
|
||||
* parsed from the header.
|
||||
* @return {String} The access token
|
||||
*/
|
||||
|
||||
AccessToken.findForRequest = function(req, options, cb) {
|
||||
if (cb === undefined && typeof options === 'function') {
|
||||
cb = options;
|
||||
options = {};
|
||||
}
|
||||
|
||||
var id = tokenIdForRequest(req, options);
|
||||
|
||||
if (id) {
|
||||
this.findById(id, function(err, token) {
|
||||
if (err) {
|
||||
cb(err);
|
||||
} else if (token) {
|
||||
token.validate(function(err, isValid) {
|
||||
if (err) {
|
||||
cb(err);
|
||||
} else if (isValid) {
|
||||
cb(null, token);
|
||||
} else {
|
||||
var e = new Error('Invalid Access Token');
|
||||
e.status = e.statusCode = 401;
|
||||
e.code = 'INVALID_TOKEN';
|
||||
cb(e);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
cb();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
process.nextTick(function() {
|
||||
cb();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Validate the token.
|
||||
*
|
||||
* @callback {Function} callback
|
||||
* @param {Error} err
|
||||
* @param {Boolean} isValid
|
||||
*/
|
||||
|
||||
AccessToken.prototype.validate = function(cb) {
|
||||
try {
|
||||
assert(
|
||||
this.created && typeof this.created.getTime === 'function',
|
||||
'token.created must be a valid Date'
|
||||
);
|
||||
assert(this.ttl !== 0, 'token.ttl must be not be 0');
|
||||
assert(this.ttl, 'token.ttl must exist');
|
||||
assert(this.ttl >= -1, 'token.ttl must be >= -1');
|
||||
|
||||
var now = Date.now();
|
||||
var created = this.created.getTime();
|
||||
var elapsedSeconds = (now - created) / 1000;
|
||||
var secondsToLive = this.ttl;
|
||||
var isValid = elapsedSeconds < secondsToLive;
|
||||
|
||||
if (isValid) {
|
||||
cb(null, isValid);
|
||||
} else {
|
||||
this.destroy(function(err) {
|
||||
cb(err, isValid);
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
cb(e);
|
||||
}
|
||||
};
|
||||
|
||||
function tokenIdForRequest(req, options) {
|
||||
var params = options.params || [];
|
||||
var headers = options.headers || [];
|
||||
var cookies = options.cookies || [];
|
||||
var i = 0;
|
||||
var length;
|
||||
var id;
|
||||
AccessToken.getIdForRequest = function(req, options) {
|
||||
options = options || {};
|
||||
let params = options.params || [];
|
||||
let headers = options.headers || [];
|
||||
let cookies = options.cookies || [];
|
||||
let i = 0;
|
||||
let length, id;
|
||||
|
||||
// https://github.com/strongloop/loopback/issues/1326
|
||||
if (options.searchDefaultTokenKeys !== false) {
|
||||
|
@ -176,12 +107,12 @@ module.exports = function(AccessToken) {
|
|||
}
|
||||
|
||||
for (length = params.length; i < length; i++) {
|
||||
var param = params[i];
|
||||
const param = params[i];
|
||||
// replacement for deprecated req.param()
|
||||
id = req.params && req.params[param] !== undefined ? req.params[param] :
|
||||
req.body && req.body[param] !== undefined ? req.body[param] :
|
||||
req.query && req.query[param] !== undefined ? req.query[param] :
|
||||
undefined;
|
||||
req.query && req.query[param] !== undefined ? req.query[param] :
|
||||
undefined;
|
||||
|
||||
if (typeof id === 'string') {
|
||||
return id;
|
||||
|
@ -194,11 +125,18 @@ module.exports = function(AccessToken) {
|
|||
if (typeof id === 'string') {
|
||||
// Add support for oAuth 2.0 bearer token
|
||||
// http://tools.ietf.org/html/rfc6750
|
||||
|
||||
// To prevent Error: Model::findById requires the id argument
|
||||
// with loopback-datasource-juggler 2.56.0+
|
||||
if (id === '') continue;
|
||||
|
||||
if (id.indexOf('Bearer ') === 0) {
|
||||
id = id.substring(7);
|
||||
// Decode from base64
|
||||
var buf = new Buffer(id, 'base64');
|
||||
id = buf.toString('utf8');
|
||||
if (options.bearerTokenBase64Encoded) {
|
||||
// Decode from base64
|
||||
const buf = new Buffer(id, 'base64');
|
||||
id = buf.toString('utf8');
|
||||
}
|
||||
} else if (/^Basic /i.test(id)) {
|
||||
id = id.substring(6);
|
||||
id = (new Buffer(id, 'base64')).toString('utf8');
|
||||
|
@ -209,7 +147,7 @@ module.exports = function(AccessToken) {
|
|||
// "a2b2c3:" (curl http://a2b2c3@localhost:3000/)
|
||||
// "token:a2b2c3" (curl http://token:a2b2c3@localhost:3000/)
|
||||
// ":a2b2c3"
|
||||
var parts = /^([^:]*):(.*)$/.exec(id);
|
||||
const parts = /^([^:]*):(.*)$/.exec(id);
|
||||
if (parts) {
|
||||
id = parts[2].length > parts[1].length ? parts[2] : parts[1];
|
||||
}
|
||||
|
@ -228,5 +166,116 @@ module.exports = function(AccessToken) {
|
|||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Resolve and validate the access token by id
|
||||
* @param {String} id Access token
|
||||
* @callback {Function} cb Callback function
|
||||
* @param {Error} err Error information
|
||||
* @param {Object} Resolved access token object
|
||||
*/
|
||||
AccessToken.resolve = function(id, cb) {
|
||||
this.findById(id, function(err, token) {
|
||||
if (err) {
|
||||
cb(err);
|
||||
} else if (token) {
|
||||
token.validate(function(err, isValid) {
|
||||
if (err) {
|
||||
cb(err);
|
||||
} else if (isValid) {
|
||||
cb(null, token);
|
||||
} else {
|
||||
const e = new Error(g.f('Invalid Access Token'));
|
||||
e.status = e.statusCode = 401;
|
||||
e.code = 'INVALID_TOKEN';
|
||||
cb(e);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
cb();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Find a token for the given `ServerRequest`.
|
||||
*
|
||||
* @param {ServerRequest} req
|
||||
* @param {Object} [options] Options for finding the token
|
||||
* @callback {Function} callback
|
||||
* @param {Error} err
|
||||
* @param {AccessToken} token
|
||||
*/
|
||||
AccessToken.findForRequest = function(req, options, cb) {
|
||||
if (cb === undefined && typeof options === 'function') {
|
||||
cb = options;
|
||||
options = {};
|
||||
}
|
||||
|
||||
const id = this.getIdForRequest(req, options);
|
||||
|
||||
if (id) {
|
||||
this.resolve(id, cb);
|
||||
} else {
|
||||
process.nextTick(cb);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Validate the token.
|
||||
*
|
||||
* @callback {Function} callback
|
||||
* @param {Error} err
|
||||
* @param {Boolean} isValid
|
||||
*/
|
||||
AccessToken.prototype.validate = function(cb) {
|
||||
try {
|
||||
assert(
|
||||
this.created && typeof this.created.getTime === 'function',
|
||||
'token.created must be a valid Date',
|
||||
);
|
||||
assert(this.ttl !== 0, 'token.ttl must be not be 0');
|
||||
assert(this.ttl, 'token.ttl must exist');
|
||||
assert(this.ttl >= -1, 'token.ttl must be >= -1');
|
||||
|
||||
const AccessToken = this.constructor;
|
||||
const userRelation = AccessToken.relations.user; // may not be set up
|
||||
let User = userRelation && userRelation.modelTo;
|
||||
|
||||
// redefine user model if accessToken's principalType is available
|
||||
if (this.principalType) {
|
||||
User = AccessToken.registry.findModel(this.principalType);
|
||||
if (!User) {
|
||||
process.nextTick(function() {
|
||||
return cb(null, false);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
const created = this.created.getTime();
|
||||
const elapsedSeconds = (now - created) / 1000;
|
||||
const secondsToLive = this.ttl;
|
||||
const eternalTokensAllowed = !!(User && User.settings.allowEternalTokens);
|
||||
const isEternalToken = secondsToLive === -1;
|
||||
const isValid = isEternalToken ?
|
||||
eternalTokensAllowed :
|
||||
elapsedSeconds < secondsToLive;
|
||||
|
||||
if (isValid) {
|
||||
process.nextTick(function() {
|
||||
cb(null, isValid);
|
||||
});
|
||||
} else {
|
||||
this.destroy(function(err) {
|
||||
cb(err, isValid);
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
process.nextTick(function() {
|
||||
cb(e);
|
||||
});
|
||||
}
|
||||
};
|
||||
};
|
||||
|
|
|
@ -11,8 +11,13 @@
|
|||
"default": 1209600,
|
||||
"description": "time to live in seconds (2 weeks by default)"
|
||||
},
|
||||
"scopes": {
|
||||
"type": ["string"],
|
||||
"description": "Array of scopes granted to this access token."
|
||||
},
|
||||
"created": {
|
||||
"type": "Date"
|
||||
"type": "Date",
|
||||
"defaultFn": "now"
|
||||
}
|
||||
},
|
||||
"relations": {
|
||||
|
@ -27,12 +32,6 @@
|
|||
"principalType": "ROLE",
|
||||
"principalId": "$everyone",
|
||||
"permission": "DENY"
|
||||
},
|
||||
{
|
||||
"principalType": "ROLE",
|
||||
"principalId": "$everyone",
|
||||
"property": "create",
|
||||
"permission": "ALLOW"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
// Copyright IBM Corp. 2014,2019. All Rights Reserved.
|
||||
// Node module: loopback
|
||||
// This file is licensed under the MIT License.
|
||||
// License text available at https://opensource.org/licenses/MIT
|
||||
|
||||
'use strict';
|
||||
/*!
|
||||
Schema ACL options
|
||||
|
||||
Object level permissions, for example, an album owned by a user
|
||||
|
||||
Factors to be authorized against:
|
||||
|
||||
* model name: Album
|
||||
* model instance properties: userId of the album, friends, shared
|
||||
* methods
|
||||
|
@ -16,32 +19,31 @@
|
|||
** none
|
||||
** everyone
|
||||
** relations: owner/friend/granted
|
||||
|
||||
Class level permissions, for example, Album
|
||||
* model name: Album
|
||||
* methods
|
||||
|
||||
URL/Route level permissions
|
||||
* url pattern
|
||||
* application id
|
||||
* ip addresses
|
||||
* http headers
|
||||
|
||||
Map to oAuth 2.0 scopes
|
||||
|
||||
*/
|
||||
|
||||
var loopback = require('../../lib/loopback');
|
||||
var async = require('async');
|
||||
var assert = require('assert');
|
||||
var debug = require('debug')('loopback:security:acl');
|
||||
const g = require('../../lib/globalize');
|
||||
const loopback = require('../../lib/loopback');
|
||||
const utils = require('../../lib/utils');
|
||||
const async = require('async');
|
||||
const extend = require('util')._extend;
|
||||
const assert = require('assert');
|
||||
const debug = require('debug')('loopback:security:acl');
|
||||
|
||||
var ctx = require('../../lib/access-context');
|
||||
var AccessContext = ctx.AccessContext;
|
||||
var Principal = ctx.Principal;
|
||||
var AccessRequest = ctx.AccessRequest;
|
||||
const ctx = require('../../lib/access-context');
|
||||
const AccessContext = ctx.AccessContext;
|
||||
const Principal = ctx.Principal;
|
||||
const AccessRequest = ctx.AccessRequest;
|
||||
|
||||
var Role = loopback.Role;
|
||||
const Role = loopback.Role;
|
||||
assert(Role, 'Role model must be defined before ACL model');
|
||||
|
||||
/**
|
||||
|
@ -67,7 +69,7 @@ assert(Role, 'Role model must be defined before ACL model');
|
|||
* - ALLOW: Explicitly grants access to the resource.
|
||||
* - AUDIT: Log, in a system-dependent way, the access specified in the permissions component of the ACL entry.
|
||||
* - DENY: Explicitly denies access to the resource.
|
||||
* @property {String} principalType Type of the principal; one of: Application, Use, Role.
|
||||
* @property {String} principalType Type of the principal; one of: APPLICATION, USER, ROLE.
|
||||
* @property {String} principalId ID of the principal - such as appId, userId or roleId.
|
||||
* @property {Object} settings Extends the `Model.settings` object.
|
||||
* @property {String} settings.defaultPermission Default permission setting: ALLOW, DENY, ALARM, or AUDIT. Default is ALLOW.
|
||||
|
@ -78,7 +80,6 @@ assert(Role, 'Role model must be defined before ACL model');
|
|||
*/
|
||||
|
||||
module.exports = function(ACL) {
|
||||
|
||||
ACL.ALL = AccessContext.ALL;
|
||||
|
||||
ACL.DEFAULT = AccessContext.DEFAULT; // Not specified
|
||||
|
@ -97,6 +98,8 @@ module.exports = function(ACL) {
|
|||
ACL.ROLE = Principal.ROLE;
|
||||
ACL.SCOPE = Principal.SCOPE;
|
||||
|
||||
ACL.DEFAULT_SCOPE = ctx.DEFAULT_SCOPES[0];
|
||||
|
||||
/**
|
||||
* Calculate the matching score for the given rule and request
|
||||
* @param {ACL} rule The ACL entry
|
||||
|
@ -104,17 +107,18 @@ module.exports = function(ACL) {
|
|||
* @returns {Number}
|
||||
*/
|
||||
ACL.getMatchingScore = function getMatchingScore(rule, req) {
|
||||
var props = ['model', 'property', 'accessType'];
|
||||
var score = 0;
|
||||
const props = ['model', 'property', 'accessType'];
|
||||
let score = 0;
|
||||
|
||||
for (var i = 0; i < props.length; i++) {
|
||||
for (let i = 0; i < props.length; i++) {
|
||||
// Shift the score by 4 for each of the properties as the weight
|
||||
score = score * 4;
|
||||
var ruleValue = rule[props[i]] || ACL.ALL;
|
||||
var requestedValue = req[props[i]] || ACL.ALL;
|
||||
var isMatchingMethodName = props[i] === 'property' && req.methodNames.indexOf(ruleValue) !== -1;
|
||||
const ruleValue = rule[props[i]] || ACL.ALL;
|
||||
const requestedValue = req[props[i]] || ACL.ALL;
|
||||
const isMatchingMethodName = props[i] === 'property' &&
|
||||
req.methodNames.indexOf(ruleValue) !== -1;
|
||||
|
||||
var isMatchingAccessType = ruleValue === requestedValue;
|
||||
let isMatchingAccessType = ruleValue === requestedValue;
|
||||
if (props[i] === 'accessType' && !isMatchingAccessType) {
|
||||
switch (ruleValue) {
|
||||
case ACL.EXECUTE:
|
||||
|
@ -203,22 +207,23 @@ module.exports = function(ACL) {
|
|||
/*!
|
||||
* Resolve permission from the ACLs
|
||||
* @param {Object[]) acls The list of ACLs
|
||||
* @param {Object} req The request
|
||||
* @returns {AccessRequest} result The effective ACL
|
||||
* @param {AccessRequest} req The access request
|
||||
* @returns {AccessRequest} result The resolved access request
|
||||
*/
|
||||
ACL.resolvePermission = function resolvePermission(acls, req) {
|
||||
if (!(req instanceof AccessRequest)) {
|
||||
req.registry = this.registry;
|
||||
req = new AccessRequest(req);
|
||||
}
|
||||
// Sort by the matching score in descending order
|
||||
acls = acls.sort(function(rule1, rule2) {
|
||||
return ACL.getMatchingScore(rule2, req) - ACL.getMatchingScore(rule1, req);
|
||||
});
|
||||
var permission = ACL.DEFAULT;
|
||||
var score = 0;
|
||||
let permission = ACL.DEFAULT;
|
||||
let score = 0;
|
||||
|
||||
for (var i = 0; i < acls.length; i++) {
|
||||
var candidate = acls[i];
|
||||
for (let i = 0; i < acls.length; i++) {
|
||||
const candidate = acls[i];
|
||||
score = ACL.getMatchingScore(candidate, req);
|
||||
if (score < 0) {
|
||||
// the highest scored ACL did not match
|
||||
|
@ -234,10 +239,11 @@ module.exports = function(ACL) {
|
|||
break;
|
||||
}
|
||||
// For wildcard match, find the strongest permission
|
||||
var candidateOrder = AccessContext.permissionOrder[candidate.permission];
|
||||
var permissionOrder = AccessContext.permissionOrder[permission];
|
||||
const candidateOrder = AccessContext.permissionOrder[candidate.permission];
|
||||
const permissionOrder = AccessContext.permissionOrder[permission];
|
||||
if (candidateOrder > permissionOrder) {
|
||||
permission = candidate.permission;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -249,9 +255,16 @@ module.exports = function(ACL) {
|
|||
debug('with score:', acl.score(req));
|
||||
});
|
||||
}
|
||||
const res = new AccessRequest({
|
||||
model: req.model,
|
||||
property: req.property,
|
||||
accessType: req.accessType,
|
||||
permission: permission || ACL.DEFAULT,
|
||||
registry: this.registry});
|
||||
|
||||
// Elucidate permission status if DEFAULT
|
||||
res.settleDefaultPermission();
|
||||
|
||||
var res = new AccessRequest(req.model, req.property, req.accessType,
|
||||
permission || ACL.DEFAULT);
|
||||
return res;
|
||||
};
|
||||
|
||||
|
@ -263,11 +276,11 @@ module.exports = function(ACL) {
|
|||
* @return {Object[]} An array of ACLs
|
||||
*/
|
||||
ACL.getStaticACLs = function getStaticACLs(model, property) {
|
||||
var modelClass = loopback.findModel(model);
|
||||
var staticACLs = [];
|
||||
const modelClass = this.registry.findModel(model);
|
||||
const staticACLs = [];
|
||||
if (modelClass && modelClass.settings.acls) {
|
||||
modelClass.settings.acls.forEach(function(acl) {
|
||||
var prop = acl.property;
|
||||
let prop = acl.property;
|
||||
// We support static ACL property with array of string values.
|
||||
if (Array.isArray(prop) && prop.indexOf(property) >= 0)
|
||||
prop = property;
|
||||
|
@ -278,12 +291,12 @@ module.exports = function(ACL) {
|
|||
principalType: acl.principalType,
|
||||
principalId: acl.principalId, // TODO: Should it be a name?
|
||||
accessType: acl.accessType || ACL.ALL,
|
||||
permission: acl.permission
|
||||
permission: acl.permission,
|
||||
}));
|
||||
}
|
||||
});
|
||||
}
|
||||
var prop = modelClass && (
|
||||
const prop = modelClass && (
|
||||
// regular property
|
||||
modelClass.definition.properties[property] ||
|
||||
// relation/scope
|
||||
|
@ -300,7 +313,7 @@ module.exports = function(ACL) {
|
|||
principalType: acl.principalType,
|
||||
principalId: acl.principalId,
|
||||
accessType: acl.accessType,
|
||||
permission: acl.permission
|
||||
permission: acl.permission,
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
@ -315,51 +328,51 @@ module.exports = function(ACL) {
|
|||
* @param {String} property The property/method/relation name.
|
||||
* @param {String} accessType The access type.
|
||||
* @callback {Function} callback Callback function.
|
||||
* @param {String|Error} err The error object
|
||||
* @param {AccessRequest} result The access permission
|
||||
* @param {String|Error} err The error object.
|
||||
* @param {AccessRequest} result The resolved access request.
|
||||
*/
|
||||
ACL.checkPermission = function checkPermission(principalType, principalId,
|
||||
model, property, accessType,
|
||||
callback) {
|
||||
model, property, accessType,
|
||||
callback) {
|
||||
if (!callback) callback = utils.createPromiseCallback();
|
||||
if (principalId !== null && principalId !== undefined && (typeof principalId !== 'string')) {
|
||||
principalId = principalId.toString();
|
||||
}
|
||||
property = property || ACL.ALL;
|
||||
var propertyQuery = (property === ACL.ALL) ? undefined : {inq: [property, ACL.ALL]};
|
||||
const propertyQuery = (property === ACL.ALL) ? undefined : {inq: [property, ACL.ALL]};
|
||||
accessType = accessType || ACL.ALL;
|
||||
var accessTypeQuery = (accessType === ACL.ALL) ? undefined : {inq: [accessType, ACL.ALL, ACL.EXECUTE]};
|
||||
const accessTypeQuery = (accessType === ACL.ALL) ? undefined :
|
||||
{inq: [accessType, ACL.ALL, ACL.EXECUTE]};
|
||||
|
||||
var req = new AccessRequest(model, property, accessType);
|
||||
const req = new AccessRequest({model, property, accessType, registry: this.registry});
|
||||
|
||||
var acls = this.getStaticACLs(model, property);
|
||||
let acls = this.getStaticACLs(model, property);
|
||||
|
||||
var resolved = this.resolvePermission(acls, req);
|
||||
// resolved is an instance of AccessRequest
|
||||
let resolved = this.resolvePermission(acls, req);
|
||||
|
||||
if (resolved && resolved.permission === ACL.DENY) {
|
||||
debug('Permission denied by statically resolved permission');
|
||||
debug(' Resolved Permission: %j', resolved);
|
||||
process.nextTick(function() {
|
||||
if (callback) callback(null, resolved);
|
||||
callback(null, resolved);
|
||||
});
|
||||
return;
|
||||
return callback.promise;
|
||||
}
|
||||
|
||||
var self = this;
|
||||
const self = this;
|
||||
this.find({where: {principalType: principalType, principalId: principalId,
|
||||
model: model, property: propertyQuery, accessType: accessTypeQuery}},
|
||||
function(err, dynACLs) {
|
||||
if (err) {
|
||||
if (callback) callback(err);
|
||||
return;
|
||||
}
|
||||
acls = acls.concat(dynACLs);
|
||||
resolved = self.resolvePermission(acls, req);
|
||||
if (resolved && resolved.permission === ACL.DEFAULT) {
|
||||
var modelClass = loopback.findModel(model);
|
||||
resolved.permission = (modelClass && modelClass.settings.defaultPermission) || ACL.ALLOW;
|
||||
}
|
||||
if (callback) callback(null, resolved);
|
||||
});
|
||||
model: model, property: propertyQuery, accessType: accessTypeQuery}},
|
||||
function(err, dynACLs) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
acls = acls.concat(dynACLs);
|
||||
// resolved is an instance of AccessRequest
|
||||
resolved = self.resolvePermission(acls, req);
|
||||
return callback(null, resolved);
|
||||
});
|
||||
return callback.promise;
|
||||
};
|
||||
|
||||
ACL.prototype.debug = function() {
|
||||
|
@ -374,62 +387,117 @@ module.exports = function(ACL) {
|
|||
}
|
||||
};
|
||||
|
||||
// NOTE Regarding ACL.isAllowed() and ACL.prototype.isAllowed()
|
||||
// Extending existing logic, including from ACL.checkAccessForContext() method,
|
||||
// ACL instance with missing property `permission` are not promoted to
|
||||
// permission = ACL.DEFAULT config. Such ACL instances will hence always be
|
||||
// inefective
|
||||
|
||||
/**
|
||||
* Test if ACL's permission is ALLOW
|
||||
* @param {String} permission The permission to test, expects one of 'ALLOW', 'DENY', 'DEFAULT'
|
||||
* @param {String} defaultPermission The default permission to apply if not providing a finite one in the permission parameter
|
||||
* @returns {Boolean} true if ACL permission is ALLOW
|
||||
*/
|
||||
ACL.isAllowed = function(permission, defaultPermission) {
|
||||
if (permission === ACL.DEFAULT) {
|
||||
permission = defaultPermission || ACL.ALLOW;
|
||||
}
|
||||
return permission !== loopback.ACL.DENY;
|
||||
};
|
||||
|
||||
/**
|
||||
* Test if ACL's permission is ALLOW
|
||||
* @param {String} defaultPermission The default permission to apply if missing in ACL instance
|
||||
* @returns {Boolean} true if ACL permission is ALLOW
|
||||
*/
|
||||
ACL.prototype.isAllowed = function(defaultPermission) {
|
||||
return this.constructor.isAllowed(this.permission, defaultPermission);
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if the request has the permission to access.
|
||||
* @options {Object} context See below.
|
||||
* @options {AccessContext|Object} context
|
||||
* An AccessContext instance or a plain object with the following properties.
|
||||
* @property {Object[]} principals An array of principals.
|
||||
* @property {String|Model} model The model name or model class.
|
||||
* @property {*} id The model instance ID.
|
||||
* @property {*} modelId The model instance ID.
|
||||
* @property {String} property The property/method/relation name.
|
||||
* @property {String} accessType The access type:
|
||||
* READ, REPLICATE, WRITE, or EXECUTE.
|
||||
* @param {Function} callback Callback function
|
||||
* READ, REPLICATE, WRITE, or EXECUTE.
|
||||
* @callback {Function} callback Callback function
|
||||
* @param {String|Error} err The error object.
|
||||
* @param {AccessRequest} result The resolved access request.
|
||||
*/
|
||||
|
||||
ACL.checkAccessForContext = function(context, callback) {
|
||||
var registry = this.registry;
|
||||
if (!callback) callback = utils.createPromiseCallback();
|
||||
const self = this;
|
||||
self.resolveRelatedModels();
|
||||
const roleModel = self.roleModel;
|
||||
|
||||
if (!(context instanceof AccessContext)) {
|
||||
context.registry = this.registry;
|
||||
context = new AccessContext(context);
|
||||
}
|
||||
|
||||
var model = context.model;
|
||||
var property = context.property;
|
||||
var accessType = context.accessType;
|
||||
var modelName = context.modelName;
|
||||
let authorizedRoles = {};
|
||||
const remotingContext = context.remotingContext;
|
||||
const model = context.model;
|
||||
const modelDefaultPermission = model && model.settings.defaultPermission;
|
||||
const property = context.property;
|
||||
const accessType = context.accessType;
|
||||
const modelName = context.modelName;
|
||||
|
||||
var methodNames = context.methodNames;
|
||||
var propertyQuery = (property === ACL.ALL) ? undefined : {inq: methodNames.concat([ACL.ALL])};
|
||||
const methodNames = context.methodNames;
|
||||
const propertyQuery = (property === ACL.ALL) ? undefined : {inq: methodNames.concat([ACL.ALL])};
|
||||
|
||||
var accessTypeQuery = (accessType === ACL.ALL) ?
|
||||
const accessTypeQuery = (accessType === ACL.ALL) ?
|
||||
undefined :
|
||||
(accessType === ACL.REPLICATE) ?
|
||||
{inq: [ACL.REPLICATE, ACL.WRITE, ACL.ALL]} :
|
||||
{inq: [accessType, ACL.ALL]};
|
||||
|
||||
var req = new AccessRequest(modelName, property, accessType, ACL.DEFAULT, methodNames);
|
||||
const req = new AccessRequest({
|
||||
model: modelName,
|
||||
property,
|
||||
accessType,
|
||||
permission: ACL.DEFAULT,
|
||||
methodNames,
|
||||
registry: this.registry});
|
||||
|
||||
var effectiveACLs = [];
|
||||
var staticACLs = this.getStaticACLs(model.modelName, property);
|
||||
if (!context.isScopeAllowed()) {
|
||||
req.permission = ACL.DENY;
|
||||
debug('--Denied by scope config--');
|
||||
debug('Scopes allowed:', context.accessToken.scopes || ctx.DEFAULT_SCOPES);
|
||||
debug('Scope required:', context.getScopes());
|
||||
context.debug();
|
||||
callback(null, req);
|
||||
return callback.promise;
|
||||
}
|
||||
|
||||
var self = this;
|
||||
var roleModel = registry.getModelByType(Role);
|
||||
this.find({where: {model: model.modelName, property: propertyQuery,
|
||||
accessType: accessTypeQuery}}, function(err, acls) {
|
||||
if (err) {
|
||||
if (callback) callback(err);
|
||||
return;
|
||||
}
|
||||
var inRoleTasks = [];
|
||||
const effectiveACLs = [];
|
||||
const staticACLs = self.getStaticACLs(model.modelName, property);
|
||||
|
||||
const query = {
|
||||
where: {
|
||||
model: {inq: [model.modelName, ACL.ALL]},
|
||||
property: propertyQuery,
|
||||
accessType: accessTypeQuery,
|
||||
},
|
||||
};
|
||||
|
||||
this.find(query, function(err, acls) {
|
||||
if (err) return callback(err);
|
||||
const inRoleTasks = [];
|
||||
|
||||
acls = acls.concat(staticACLs);
|
||||
|
||||
acls.forEach(function(acl) {
|
||||
// Check exact matches
|
||||
for (var i = 0; i < context.principals.length; i++) {
|
||||
var p = context.principals[i];
|
||||
var typeMatch = p.type === acl.principalType;
|
||||
var idMatch = String(p.id) === String(acl.principalId);
|
||||
for (let i = 0; i < context.principals.length; i++) {
|
||||
const p = context.principals[i];
|
||||
const typeMatch = p.type === acl.principalType;
|
||||
const idMatch = String(p.id) === String(acl.principalId);
|
||||
if (typeMatch && idMatch) {
|
||||
effectiveACLs.push(acl);
|
||||
return;
|
||||
|
@ -443,6 +511,9 @@ module.exports = function(ACL) {
|
|||
function(err, inRole) {
|
||||
if (!err && inRole) {
|
||||
effectiveACLs.push(acl);
|
||||
// add the role to authorizedRoles if allowed
|
||||
if (acl.isAllowed(modelDefaultPermission))
|
||||
authorizedRoles[acl.principalId] = true;
|
||||
}
|
||||
done(err, acl);
|
||||
});
|
||||
|
@ -451,22 +522,33 @@ module.exports = function(ACL) {
|
|||
});
|
||||
|
||||
async.parallel(inRoleTasks, function(err, results) {
|
||||
if (err) {
|
||||
if (callback) callback(err, null);
|
||||
return;
|
||||
}
|
||||
if (err) return callback(err, null);
|
||||
|
||||
var resolved = self.resolvePermission(effectiveACLs, req);
|
||||
if (resolved && resolved.permission === ACL.DEFAULT) {
|
||||
resolved.permission = (model && model.settings.defaultPermission) || ACL.ALLOW;
|
||||
}
|
||||
// resolved is an instance of AccessRequest
|
||||
const resolved = self.resolvePermission(effectiveACLs, req);
|
||||
debug('---Resolved---');
|
||||
resolved.debug();
|
||||
if (callback) callback(null, resolved);
|
||||
|
||||
// set authorizedRoles in remotingContext options argument if
|
||||
// resolved AccessRequest permission is ALLOW, else set it to empty object
|
||||
authorizedRoles = resolved.isAllowed() ? authorizedRoles : {};
|
||||
saveAuthorizedRolesToRemotingContext(remotingContext, authorizedRoles);
|
||||
return callback(null, resolved);
|
||||
});
|
||||
});
|
||||
return callback.promise;
|
||||
};
|
||||
|
||||
function saveAuthorizedRolesToRemotingContext(remotingContext, authorizedRoles) {
|
||||
const options = remotingContext && remotingContext.args && remotingContext.args.options;
|
||||
// authorizedRoles key/value map is added to the options argument only if
|
||||
// the latter exists and is an object. This means that the feature's availability
|
||||
// will depend on the app configuration
|
||||
if (options && typeof options === 'object') { // null is object too
|
||||
options.authorizedRoles = authorizedRoles;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the given access token can invoke the method
|
||||
* @param {AccessToken} token The access token
|
||||
|
@ -479,31 +561,30 @@ module.exports = function(ACL) {
|
|||
*/
|
||||
ACL.checkAccessForToken = function(token, model, modelId, method, callback) {
|
||||
assert(token, 'Access token is required');
|
||||
|
||||
var context = new AccessContext({
|
||||
if (!callback) callback = utils.createPromiseCallback();
|
||||
const context = new AccessContext({
|
||||
registry: this.registry,
|
||||
accessToken: token,
|
||||
model: model,
|
||||
property: method,
|
||||
method: method,
|
||||
modelId: modelId
|
||||
modelId: modelId,
|
||||
});
|
||||
|
||||
this.checkAccessForContext(context, function(err, access) {
|
||||
if (err) {
|
||||
if (callback) callback(err);
|
||||
return;
|
||||
}
|
||||
if (callback) callback(null, access.permission !== ACL.DENY);
|
||||
this.checkAccessForContext(context, function(err, accessRequest) {
|
||||
if (err) callback(err);
|
||||
else callback(null, accessRequest.isAllowed());
|
||||
});
|
||||
return callback.promise;
|
||||
};
|
||||
|
||||
ACL.resolveRelatedModels = function() {
|
||||
if (!this.roleModel) {
|
||||
var reg = this.registry;
|
||||
this.roleModel = reg.getModelByType(loopback.Role);
|
||||
this.roleMappingModel = reg.getModelByType(loopback.RoleMapping);
|
||||
this.userModel = reg.getModelByType(loopback.User);
|
||||
this.applicationModel = reg.getModelByType(loopback.Application);
|
||||
const reg = this.registry;
|
||||
this.roleModel = reg.getModelByType('Role');
|
||||
this.roleMappingModel = reg.getModelByType('RoleMapping');
|
||||
this.userModel = reg.getModelByType('User');
|
||||
this.applicationModel = reg.getModelByType('Application');
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -511,30 +592,47 @@ module.exports = function(ACL) {
|
|||
* Resolve a principal by type/id
|
||||
* @param {String} type Principal type - ROLE/APP/USER
|
||||
* @param {String|Number} id Principal id or name
|
||||
* @param {Function} cb Callback function
|
||||
* @callback {Function} callback Callback function
|
||||
* @param {String|Error} err The error object
|
||||
* @param {Object} result An instance of principal (Role, Application or User)
|
||||
*/
|
||||
ACL.resolvePrincipal = function(type, id, cb) {
|
||||
cb = cb || utils.createPromiseCallback();
|
||||
type = type || ACL.ROLE;
|
||||
this.resolveRelatedModels();
|
||||
|
||||
switch (type) {
|
||||
case ACL.ROLE:
|
||||
this.roleModel.findOne({where: {or: [{name: id}, {id: id}]}}, cb);
|
||||
break;
|
||||
case ACL.USER:
|
||||
this.userModel.findOne(
|
||||
{where: {or: [{username: id}, {email: id}, {id: id}]}}, cb);
|
||||
{where: {or: [{username: id}, {email: id}, {id: id}]}}, cb,
|
||||
);
|
||||
break;
|
||||
case ACL.APP:
|
||||
this.applicationModel.findOne(
|
||||
{where: {or: [{name: id}, {email: id}, {id: id}]}}, cb);
|
||||
{where: {or: [{name: id}, {email: id}, {id: id}]}}, cb,
|
||||
);
|
||||
break;
|
||||
default:
|
||||
process.nextTick(function() {
|
||||
var err = new Error('Invalid principal type: ' + type);
|
||||
err.statusCode = 400;
|
||||
cb(err);
|
||||
});
|
||||
// try resolving a user model with a name matching the principalType
|
||||
const userModel = this.registry.findModel(type);
|
||||
if (userModel) {
|
||||
userModel.findOne(
|
||||
{where: {or: [{username: id}, {email: id}, {id: id}]}},
|
||||
cb,
|
||||
);
|
||||
} else {
|
||||
process.nextTick(function() {
|
||||
const err = new Error(g.f('Invalid principal type: %s', type));
|
||||
err.statusCode = 400;
|
||||
err.code = 'INVALID_PRINCIPAL_TYPE';
|
||||
cb(err);
|
||||
});
|
||||
}
|
||||
}
|
||||
return cb.promise;
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -542,10 +640,13 @@ module.exports = function(ACL) {
|
|||
* @param {String} principalType Principal type
|
||||
* @param {String|*} principalId Principal id/name
|
||||
* @param {String|*} role Role id/name
|
||||
* @param {Function} cb Callback function
|
||||
* @callback {Function} callback Callback function
|
||||
* @param {String|Error} err The error object
|
||||
* @param {Boolean} isMapped is the ACL mapped to the role
|
||||
*/
|
||||
ACL.isMappedToRole = function(principalType, principalId, role, cb) {
|
||||
var self = this;
|
||||
cb = cb || utils.createPromiseCallback();
|
||||
const self = this;
|
||||
this.resolvePrincipal(principalType, principalId,
|
||||
function(err, principal) {
|
||||
if (err) return cb(err);
|
||||
|
@ -559,13 +660,14 @@ module.exports = function(ACL) {
|
|||
where: {
|
||||
roleId: role.id,
|
||||
principalType: principalType,
|
||||
principalId: String(principalId)
|
||||
}
|
||||
principalId: String(principalId),
|
||||
},
|
||||
}, function(err, result) {
|
||||
if (err) return cb(err);
|
||||
return cb(null, !!result);
|
||||
});
|
||||
});
|
||||
});
|
||||
return cb.promise;
|
||||
};
|
||||
};
|
||||
|
|
|
@ -1,20 +1,26 @@
|
|||
var assert = require('assert');
|
||||
var utils = require('../../lib/utils');
|
||||
// Copyright IBM Corp. 2014,2019. All Rights Reserved.
|
||||
// Node module: loopback
|
||||
// This file is licensed under the MIT License.
|
||||
// License text available at https://opensource.org/licenses/MIT
|
||||
|
||||
'use strict';
|
||||
const assert = require('assert');
|
||||
const utils = require('../../lib/utils');
|
||||
|
||||
/*!
|
||||
* Application management functions
|
||||
*/
|
||||
|
||||
var crypto = require('crypto');
|
||||
const crypto = require('crypto');
|
||||
|
||||
function generateKey(hmacKey, algorithm, encoding) {
|
||||
hmacKey = hmacKey || 'loopback';
|
||||
algorithm = algorithm || 'sha1';
|
||||
encoding = encoding || 'hex';
|
||||
var hmac = crypto.createHmac(algorithm, hmacKey);
|
||||
var buf = crypto.randomBytes(32);
|
||||
const hmac = crypto.createHmac(algorithm, hmacKey);
|
||||
const buf = crypto.randomBytes(32);
|
||||
hmac.update(buf);
|
||||
var key = hmac.digest(encoding);
|
||||
const key = hmac.digest(encoding);
|
||||
return key;
|
||||
}
|
||||
|
||||
|
@ -65,19 +71,6 @@ function generateKey(hmacKey, algorithm, encoding) {
|
|||
*/
|
||||
|
||||
module.exports = function(Application) {
|
||||
|
||||
// Workaround for https://github.com/strongloop/loopback/issues/292
|
||||
Application.definition.rawProperties.created.default =
|
||||
Application.definition.properties.created.default = function() {
|
||||
return new Date();
|
||||
};
|
||||
|
||||
// Workaround for https://github.com/strongloop/loopback/issues/292
|
||||
Application.definition.rawProperties.modified.default =
|
||||
Application.definition.properties.modified.default = function() {
|
||||
return new Date();
|
||||
};
|
||||
|
||||
/*!
|
||||
* A hook to generate keys before creation
|
||||
* @param next
|
||||
|
@ -90,7 +83,7 @@ module.exports = function(Application) {
|
|||
return next();
|
||||
}
|
||||
|
||||
var app = ctx.instance;
|
||||
const app = ctx.instance;
|
||||
app.created = app.modified = new Date();
|
||||
if (!app.id) {
|
||||
app.id = generateKey('id', 'md5');
|
||||
|
@ -108,7 +101,9 @@ module.exports = function(Application) {
|
|||
* @param {String} owner Owner's user ID.
|
||||
* @param {String} name Name of the application
|
||||
* @param {Object} options Other options
|
||||
* @param {Function} callback Callback function
|
||||
* @callback {Function} callback Callback function
|
||||
* @param {Error} err
|
||||
* @promise
|
||||
*/
|
||||
Application.register = function(owner, name, options, cb) {
|
||||
assert(owner, 'owner is required');
|
||||
|
@ -120,8 +115,8 @@ module.exports = function(Application) {
|
|||
}
|
||||
cb = cb || utils.createPromiseCallback();
|
||||
|
||||
var props = {owner: owner, name: name};
|
||||
for (var p in options) {
|
||||
const props = {owner: owner, name: name};
|
||||
for (const p in options) {
|
||||
if (!(p in props)) {
|
||||
props[p] = options[p];
|
||||
}
|
||||
|
@ -150,6 +145,7 @@ module.exports = function(Application) {
|
|||
* @param {Any} appId
|
||||
* @callback {Function} callback
|
||||
* @param {Error} err
|
||||
* @promise
|
||||
*/
|
||||
Application.resetKeys = function(appId, cb) {
|
||||
cb = cb || utils.createPromiseCallback();
|
||||
|
@ -176,7 +172,7 @@ module.exports = function(Application) {
|
|||
* - restApiKey
|
||||
* - windowsKey
|
||||
* - masterKey
|
||||
*
|
||||
* @promise
|
||||
*/
|
||||
Application.authenticate = function(appId, key, cb) {
|
||||
cb = cb || utils.createPromiseCallback();
|
||||
|
@ -186,13 +182,13 @@ module.exports = function(Application) {
|
|||
cb(err, null);
|
||||
return cb.promise;
|
||||
}
|
||||
var result = null;
|
||||
var keyNames = ['clientKey', 'javaScriptKey', 'restApiKey', 'windowsKey', 'masterKey'];
|
||||
for (var i = 0; i < keyNames.length; i++) {
|
||||
let result = null;
|
||||
const keyNames = ['clientKey', 'javaScriptKey', 'restApiKey', 'windowsKey', 'masterKey'];
|
||||
for (let i = 0; i < keyNames.length; i++) {
|
||||
if (app[keyNames[i]] === key) {
|
||||
result = {
|
||||
application: app,
|
||||
keyType: keyNames[i]
|
||||
keyType: keyNames[i],
|
||||
};
|
||||
break;
|
||||
}
|
||||
|
|
|
@ -118,7 +118,13 @@
|
|||
"description": "Status of the application, production/sandbox/disabled"
|
||||
},
|
||||
|
||||
"created": "date",
|
||||
"modified": "date"
|
||||
"created": {
|
||||
"type": "date",
|
||||
"defaultFn": "now"
|
||||
},
|
||||
"modified": {
|
||||
"type": "date",
|
||||
"defaultFn": "now"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,15 +1,22 @@
|
|||
// Copyright IBM Corp. 2014,2019. All Rights Reserved.
|
||||
// Node module: loopback
|
||||
// This file is licensed under the MIT License.
|
||||
// License text available at https://opensource.org/licenses/MIT
|
||||
|
||||
/*!
|
||||
* Module Dependencies.
|
||||
*/
|
||||
|
||||
var PersistedModel = require('../../lib/loopback').PersistedModel;
|
||||
var loopback = require('../../lib/loopback');
|
||||
var crypto = require('crypto');
|
||||
var CJSON = {stringify: require('canonical-json')};
|
||||
var async = require('async');
|
||||
var assert = require('assert');
|
||||
var debug = require('debug')('loopback:change');
|
||||
var deprecate = require('depd')('loopback');
|
||||
'use strict';
|
||||
const g = require('../../lib/globalize');
|
||||
const PersistedModel = require('../../lib/loopback').PersistedModel;
|
||||
const loopback = require('../../lib/loopback');
|
||||
const utils = require('../../lib/utils');
|
||||
const crypto = require('crypto');
|
||||
const CJSON = {stringify: require('canonical-json')};
|
||||
const async = require('async');
|
||||
const assert = require('assert');
|
||||
const debug = require('debug')('loopback:change');
|
||||
|
||||
/**
|
||||
* Change list entry.
|
||||
|
@ -30,7 +37,6 @@ var deprecate = require('depd')('loopback');
|
|||
*/
|
||||
|
||||
module.exports = function(Change) {
|
||||
|
||||
/*!
|
||||
* Constants
|
||||
*/
|
||||
|
@ -52,10 +58,10 @@ module.exports = function(Change) {
|
|||
|
||||
Change.setup = function() {
|
||||
PersistedModel.setup.call(this);
|
||||
var Change = this;
|
||||
const Change = this;
|
||||
|
||||
Change.getter.id = function() {
|
||||
var hasModel = this.modelName && this.modelId;
|
||||
const hasModel = this.modelName && this.modelId;
|
||||
if (!hasModel) return null;
|
||||
|
||||
return Change.idForModel(this.modelName, this.modelId);
|
||||
|
@ -74,10 +80,12 @@ module.exports = function(Change) {
|
|||
*/
|
||||
|
||||
Change.rectifyModelChanges = function(modelName, modelIds, callback) {
|
||||
var Change = this;
|
||||
var errors = [];
|
||||
const Change = this;
|
||||
const errors = [];
|
||||
|
||||
var tasks = modelIds.map(function(id) {
|
||||
callback = callback || utils.createPromiseCallback();
|
||||
|
||||
const tasks = modelIds.map(function(id) {
|
||||
return function(cb) {
|
||||
Change.findOrCreateChange(modelName, id, function(err, change) {
|
||||
if (err) return next(err);
|
||||
|
@ -98,19 +106,20 @@ module.exports = function(Change) {
|
|||
async.parallel(tasks, function(err) {
|
||||
if (err) return callback(err);
|
||||
if (errors.length) {
|
||||
var desc = errors
|
||||
const desc = errors
|
||||
.map(function(e) {
|
||||
return '#' + e.modelId + ' - ' + e.toString();
|
||||
})
|
||||
.join('\n');
|
||||
|
||||
var msg = 'Cannot rectify ' + modelName + ' changes:\n' + desc;
|
||||
const msg = g.f('Cannot rectify %s changes:\n%s', modelName, desc);
|
||||
err = new Error(msg);
|
||||
err.details = { errors: errors };
|
||||
err.details = {errors: errors};
|
||||
return callback(err);
|
||||
}
|
||||
callback();
|
||||
});
|
||||
return callback.promise;
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -137,24 +146,26 @@ module.exports = function(Change) {
|
|||
*/
|
||||
|
||||
Change.findOrCreateChange = function(modelName, modelId, callback) {
|
||||
assert(loopback.findModel(modelName), modelName + ' does not exist');
|
||||
var id = this.idForModel(modelName, modelId);
|
||||
var Change = this;
|
||||
assert(this.registry.findModel(modelName), modelName + ' does not exist');
|
||||
callback = callback || utils.createPromiseCallback();
|
||||
const id = this.idForModel(modelName, modelId);
|
||||
const Change = this;
|
||||
|
||||
this.findById(id, function(err, change) {
|
||||
if (err) return callback(err);
|
||||
if (change) {
|
||||
callback(null, change);
|
||||
} else {
|
||||
var ch = new Change({
|
||||
const ch = new Change({
|
||||
id: id,
|
||||
modelName: modelName,
|
||||
modelId: modelId
|
||||
modelId: modelId,
|
||||
});
|
||||
ch.debug('creating change');
|
||||
Change.updateOrCreate(ch, callback);
|
||||
}
|
||||
});
|
||||
return callback.promise;
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -166,34 +177,48 @@ module.exports = function(Change) {
|
|||
*/
|
||||
|
||||
Change.prototype.rectify = function(cb) {
|
||||
var change = this;
|
||||
var currentRev = this.rev;
|
||||
const change = this;
|
||||
const currentRev = this.rev;
|
||||
|
||||
change.debug('rectify change');
|
||||
|
||||
cb = cb || function(err) {
|
||||
if (err) throw new Error(err);
|
||||
};
|
||||
cb = cb || utils.createPromiseCallback();
|
||||
|
||||
change.currentRevision(function(err, rev) {
|
||||
const model = this.getModelCtor();
|
||||
const id = this.getModelId();
|
||||
|
||||
model.findById(id, function(err, inst) {
|
||||
if (err) return cb(err);
|
||||
|
||||
if (inst) {
|
||||
inst.fillCustomChangeProperties(change, function() {
|
||||
const rev = Change.revisionForInst(inst);
|
||||
prepareAndDoRectify(rev);
|
||||
});
|
||||
} else {
|
||||
prepareAndDoRectify(null);
|
||||
}
|
||||
});
|
||||
|
||||
return cb.promise;
|
||||
|
||||
function prepareAndDoRectify(rev) {
|
||||
// avoid setting rev and prev to the same value
|
||||
if (currentRev === rev) {
|
||||
change.debug('rev and prev are equal (not updating anything)');
|
||||
return cb(null, change);
|
||||
}
|
||||
|
||||
// FIXME(@bajtos) Allo callers to pass in the checkpoint value
|
||||
// FIXME(@bajtos) Allow callers to pass in the checkpoint value
|
||||
// (or even better - a memoized async function to get the cp value)
|
||||
// That will enable `rectifyAll` to cache the checkpoint value
|
||||
change.constructor.getCheckpointModel().current(
|
||||
function(err, checkpoint) {
|
||||
if (err) return cb(err);
|
||||
doRectify(checkpoint, rev);
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function doRectify(checkpoint, rev) {
|
||||
if (rev) {
|
||||
|
@ -218,7 +243,7 @@ module.exports = function(Change) {
|
|||
if (currentRev) {
|
||||
change.prev = currentRev;
|
||||
} else if (!change.prev) {
|
||||
change.debug('ERROR - could not determing prev');
|
||||
change.debug('ERROR - could not determine prev');
|
||||
change.prev = Change.UNKNOWN;
|
||||
}
|
||||
change.debug('updated prev');
|
||||
|
@ -248,8 +273,9 @@ module.exports = function(Change) {
|
|||
*/
|
||||
|
||||
Change.prototype.currentRevision = function(cb) {
|
||||
var model = this.getModelCtor();
|
||||
var id = this.getModelId();
|
||||
cb = cb || utils.createPromiseCallback();
|
||||
const model = this.getModelCtor();
|
||||
const id = this.getModelId();
|
||||
model.findById(id, function(err, inst) {
|
||||
if (err) return cb(err);
|
||||
if (inst) {
|
||||
|
@ -258,6 +284,7 @@ module.exports = function(Change) {
|
|||
cb(null, null);
|
||||
}
|
||||
});
|
||||
return cb.promise;
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -318,8 +345,8 @@ module.exports = function(Change) {
|
|||
|
||||
Change.prototype.equals = function(change) {
|
||||
if (!change) return false;
|
||||
var thisRev = this.rev || null;
|
||||
var thatRev = change.rev || null;
|
||||
const thisRev = this.rev || null;
|
||||
const thatRev = change.rev || null;
|
||||
return thisRev === thatRev;
|
||||
};
|
||||
|
||||
|
@ -390,11 +417,14 @@ module.exports = function(Change) {
|
|||
*/
|
||||
|
||||
Change.diff = function(modelName, since, remoteChanges, callback) {
|
||||
callback = callback || utils.createPromiseCallback();
|
||||
|
||||
if (!Array.isArray(remoteChanges) || remoteChanges.length === 0) {
|
||||
return callback(null, {deltas: [], conflicts: []});
|
||||
callback(null, {deltas: [], conflicts: []});
|
||||
return callback.promise;
|
||||
}
|
||||
var remoteChangeIndex = {};
|
||||
var modelIds = [];
|
||||
const remoteChangeIndex = {};
|
||||
const modelIds = [];
|
||||
remoteChanges.forEach(function(ch) {
|
||||
modelIds.push(ch.modelId);
|
||||
remoteChangeIndex[ch.modelId] = new Change(ch);
|
||||
|
@ -405,22 +435,22 @@ module.exports = function(Change) {
|
|||
this.find({
|
||||
where: {
|
||||
modelName: modelName,
|
||||
modelId: {inq: modelIds}
|
||||
}
|
||||
modelId: {inq: modelIds},
|
||||
},
|
||||
}, function(err, allLocalChanges) {
|
||||
if (err) return callback(err);
|
||||
var deltas = [];
|
||||
var conflicts = [];
|
||||
var localModelIds = [];
|
||||
const deltas = [];
|
||||
const conflicts = [];
|
||||
const localModelIds = [];
|
||||
|
||||
var localChanges = allLocalChanges.filter(function(c) {
|
||||
const localChanges = allLocalChanges.filter(function(c) {
|
||||
return c.checkpoint >= since;
|
||||
});
|
||||
|
||||
localChanges.forEach(function(localChange) {
|
||||
localChange = new Change(localChange);
|
||||
localModelIds.push(localChange.modelId);
|
||||
var remoteChange = remoteChangeIndex[localChange.modelId];
|
||||
const remoteChange = remoteChangeIndex[localChange.modelId];
|
||||
if (remoteChange && !localChange.equals(remoteChange)) {
|
||||
if (remoteChange.conflictsWith(localChange)) {
|
||||
remoteChange.debug('remote conflict');
|
||||
|
@ -436,8 +466,8 @@ module.exports = function(Change) {
|
|||
modelIds.forEach(function(id) {
|
||||
if (localModelIds.indexOf(id) !== -1) return;
|
||||
|
||||
var d = remoteChangeIndex[id];
|
||||
var oldChange = allLocalChanges.filter(function(c) {
|
||||
const d = remoteChangeIndex[id];
|
||||
const oldChange = allLocalChanges.filter(function(c) {
|
||||
return c.modelId === id;
|
||||
})[0];
|
||||
|
||||
|
@ -452,9 +482,10 @@ module.exports = function(Change) {
|
|||
|
||||
callback(null, {
|
||||
deltas: deltas,
|
||||
conflicts: conflicts
|
||||
conflicts: conflicts,
|
||||
});
|
||||
});
|
||||
return callback.promise;
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -464,14 +495,15 @@ module.exports = function(Change) {
|
|||
|
||||
Change.rectifyAll = function(cb) {
|
||||
debug('rectify all');
|
||||
var Change = this;
|
||||
const Change = this;
|
||||
// this should be optimized
|
||||
this.find(function(err, changes) {
|
||||
if (err) return cb(err);
|
||||
async.each(
|
||||
changes,
|
||||
function(c, next) { c.rectify(next); },
|
||||
cb);
|
||||
cb,
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -481,7 +513,7 @@ module.exports = function(Change) {
|
|||
*/
|
||||
|
||||
Change.getCheckpointModel = function() {
|
||||
var checkpointModel = this.Checkpoint;
|
||||
let checkpointModel = this.Checkpoint;
|
||||
if (checkpointModel) return checkpointModel;
|
||||
// FIXME(bajtos) This code creates multiple different models with the same
|
||||
// model name, which is not a valid supported usage of juggler's API.
|
||||
|
@ -492,18 +524,9 @@ module.exports = function(Change) {
|
|||
return checkpointModel;
|
||||
};
|
||||
|
||||
Change.handleError = function(err) {
|
||||
deprecate('Change.handleError is deprecated, ' +
|
||||
'you should pass errors to your callback instead.');
|
||||
|
||||
if (!this.settings.ignoreErrors) {
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
Change.prototype.debug = function() {
|
||||
if (debug.enabled) {
|
||||
var args = Array.prototype.slice.call(arguments);
|
||||
const args = Array.prototype.slice.call(arguments);
|
||||
args[0] = args[0] + ' %s';
|
||||
args.push(this.modelName);
|
||||
debug.apply(this, args);
|
||||
|
@ -528,16 +551,16 @@ module.exports = function(Change) {
|
|||
|
||||
Change.prototype.getModelId = function() {
|
||||
// TODO(ritch) get rid of the need to create an instance
|
||||
var Model = this.getModelCtor();
|
||||
var id = this.modelId;
|
||||
var m = new Model();
|
||||
const Model = this.getModelCtor();
|
||||
const id = this.modelId;
|
||||
const m = new Model();
|
||||
m.setId(id);
|
||||
return m.getId();
|
||||
};
|
||||
|
||||
Change.prototype.getModel = function(callback) {
|
||||
var Model = this.constructor.settings.trackModel;
|
||||
var id = this.getModelId();
|
||||
const Model = this.constructor.settings.trackModel;
|
||||
const id = this.getModelId();
|
||||
Model.findById(id, callback);
|
||||
};
|
||||
|
||||
|
@ -572,15 +595,14 @@ module.exports = function(Change) {
|
|||
*/
|
||||
|
||||
Conflict.prototype.models = function(cb) {
|
||||
var conflict = this;
|
||||
var SourceModel = this.SourceModel;
|
||||
var TargetModel = this.TargetModel;
|
||||
var source;
|
||||
var target;
|
||||
const conflict = this;
|
||||
const SourceModel = this.SourceModel;
|
||||
const TargetModel = this.TargetModel;
|
||||
let source, target;
|
||||
|
||||
async.parallel([
|
||||
getSourceModel,
|
||||
getTargetModel
|
||||
getTargetModel,
|
||||
], done);
|
||||
|
||||
function getSourceModel(cb) {
|
||||
|
@ -615,17 +637,16 @@ module.exports = function(Change) {
|
|||
*/
|
||||
|
||||
Conflict.prototype.changes = function(cb) {
|
||||
var conflict = this;
|
||||
var sourceChange;
|
||||
var targetChange;
|
||||
const conflict = this;
|
||||
let sourceChange, targetChange;
|
||||
|
||||
async.parallel([
|
||||
getSourceChange,
|
||||
getTargetChange
|
||||
getTargetChange,
|
||||
], done);
|
||||
|
||||
function getSourceChange(cb) {
|
||||
var SourceModel = conflict.SourceModel;
|
||||
const SourceModel = conflict.SourceModel;
|
||||
SourceModel.findLastChange(conflict.modelId, function(err, change) {
|
||||
if (err) return cb(err);
|
||||
sourceChange = change;
|
||||
|
@ -634,7 +655,7 @@ module.exports = function(Change) {
|
|||
}
|
||||
|
||||
function getTargetChange(cb) {
|
||||
var TargetModel = conflict.TargetModel;
|
||||
const TargetModel = conflict.TargetModel;
|
||||
TargetModel.findLastChange(conflict.modelId, function(err, change) {
|
||||
if (err) return cb(err);
|
||||
targetChange = change;
|
||||
|
@ -663,16 +684,18 @@ module.exports = function(Change) {
|
|||
*/
|
||||
|
||||
Conflict.prototype.resolve = function(cb) {
|
||||
var conflict = this;
|
||||
const conflict = this;
|
||||
conflict.TargetModel.findLastChange(
|
||||
this.modelId,
|
||||
function(err, targetChange) {
|
||||
if (err) return cb(err);
|
||||
conflict.SourceModel.updateLastChange(
|
||||
conflict.modelId,
|
||||
{ prev: targetChange.rev },
|
||||
cb);
|
||||
});
|
||||
{prev: targetChange.rev},
|
||||
cb,
|
||||
);
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -695,16 +718,17 @@ module.exports = function(Change) {
|
|||
* @param {Error} err
|
||||
*/
|
||||
Conflict.prototype.resolveUsingTarget = function(cb) {
|
||||
var conflict = this;
|
||||
const conflict = this;
|
||||
|
||||
conflict.models(function(err, source, target) {
|
||||
if (err) return done(err);
|
||||
if (target === null) {
|
||||
return conflict.SourceModel.deleteById(conflict.modelId, done);
|
||||
}
|
||||
var inst = new conflict.SourceModel(
|
||||
const inst = new conflict.SourceModel(
|
||||
target.toObject(),
|
||||
{ persisted: true });
|
||||
{persisted: true},
|
||||
);
|
||||
inst.save(done);
|
||||
});
|
||||
|
||||
|
@ -727,7 +751,7 @@ module.exports = function(Change) {
|
|||
* @returns {Conflict} A new Conflict instance.
|
||||
*/
|
||||
Conflict.prototype.swapParties = function() {
|
||||
var Ctor = this.constructor;
|
||||
const Ctor = this.constructor;
|
||||
return new Ctor(this.modelId, this.TargetModel, this.SourceModel);
|
||||
};
|
||||
|
||||
|
@ -741,14 +765,14 @@ module.exports = function(Change) {
|
|||
*/
|
||||
|
||||
Conflict.prototype.resolveManually = function(data, cb) {
|
||||
var conflict = this;
|
||||
const conflict = this;
|
||||
if (!data) {
|
||||
return conflict.SourceModel.deleteById(conflict.modelId, done);
|
||||
}
|
||||
|
||||
conflict.models(function(err, source, target) {
|
||||
if (err) return done(err);
|
||||
var inst = source || new conflict.SourceModel(target);
|
||||
const inst = source || new conflict.SourceModel(target);
|
||||
inst.setAttributes(data);
|
||||
inst.save(function(err) {
|
||||
if (err) return done(err);
|
||||
|
@ -777,11 +801,11 @@ module.exports = function(Change) {
|
|||
*/
|
||||
|
||||
Conflict.prototype.type = function(cb) {
|
||||
var conflict = this;
|
||||
const conflict = this;
|
||||
this.changes(function(err, sourceChange, targetChange) {
|
||||
if (err) return cb(err);
|
||||
var sourceChangeType = sourceChange.type();
|
||||
var targetChangeType = targetChange.type();
|
||||
const sourceChangeType = sourceChange.type();
|
||||
const targetChangeType = targetChange.type();
|
||||
if (sourceChangeType === Change.UPDATE && targetChangeType === Change.UPDATE) {
|
||||
return cb(null, Change.UPDATE);
|
||||
}
|
||||
|
|
|
@ -1,8 +1,14 @@
|
|||
// Copyright IBM Corp. 2014,2019. All Rights Reserved.
|
||||
// Node module: loopback
|
||||
// This file is licensed under the MIT License.
|
||||
// License text available at https://opensource.org/licenses/MIT
|
||||
|
||||
/**
|
||||
* Module Dependencies.
|
||||
*/
|
||||
|
||||
var assert = require('assert');
|
||||
'use strict';
|
||||
const assert = require('assert');
|
||||
|
||||
/**
|
||||
* Checkpoint list entry.
|
||||
|
@ -16,7 +22,6 @@ var assert = require('assert');
|
|||
*/
|
||||
|
||||
module.exports = function(Checkpoint) {
|
||||
|
||||
// Workaround for https://github.com/strongloop/loopback/issues/292
|
||||
Checkpoint.definition.rawProperties.time.default =
|
||||
Checkpoint.definition.properties.time.default = function() {
|
||||
|
@ -27,43 +32,45 @@ module.exports = function(Checkpoint) {
|
|||
* Get the current checkpoint id
|
||||
* @callback {Function} callback
|
||||
* @param {Error} err
|
||||
* @param {Number} checkpointId The current checkpoint id
|
||||
* @param {Number} checkpoint The current checkpoint seq
|
||||
*/
|
||||
|
||||
Checkpoint.current = function(cb) {
|
||||
var Checkpoint = this;
|
||||
this.find({
|
||||
limit: 1,
|
||||
order: 'seq DESC'
|
||||
}, function(err, checkpoints) {
|
||||
if (err) return cb(err);
|
||||
var checkpoint = checkpoints[0];
|
||||
if (checkpoint) {
|
||||
cb(null, checkpoint.seq);
|
||||
} else {
|
||||
Checkpoint.create({ seq: 1 }, function(err, checkpoint) {
|
||||
if (err) return cb(err);
|
||||
cb(null, checkpoint.seq);
|
||||
});
|
||||
}
|
||||
const Checkpoint = this;
|
||||
Checkpoint._getSingleton(function(err, cp) {
|
||||
cb(err, cp.seq);
|
||||
});
|
||||
};
|
||||
|
||||
Checkpoint.observe('before save', function(ctx, next) {
|
||||
if (!ctx.instance) {
|
||||
// Example: Checkpoint.updateAll() and Checkpoint.updateOrCreate()
|
||||
return next(new Error('Checkpoint does not support partial updates.'));
|
||||
}
|
||||
Checkpoint._getSingleton = function(cb) {
|
||||
const query = {limit: 1}; // match all instances, return only one
|
||||
const initialData = {seq: 1};
|
||||
this.findOrCreate(query, initialData, cb);
|
||||
};
|
||||
|
||||
var model = ctx.instance;
|
||||
if (!model.getId() && model.seq === undefined) {
|
||||
model.constructor.current(function(err, seq) {
|
||||
if (err) return next(err);
|
||||
model.seq = seq + 1;
|
||||
next();
|
||||
/**
|
||||
* Increase the current checkpoint if it already exists otherwise initialize it
|
||||
* @callback {Function} callback
|
||||
* @param {Error} err
|
||||
* @param {Object} checkpoint The current checkpoint
|
||||
*/
|
||||
Checkpoint.bumpLastSeq = function(cb) {
|
||||
const Checkpoint = this;
|
||||
Checkpoint._getSingleton(function(err, cp) {
|
||||
if (err) return cb(err);
|
||||
const originalSeq = cp.seq;
|
||||
cp.seq++;
|
||||
// Update the checkpoint but only if it was not changed under our hands
|
||||
Checkpoint.updateAll({id: cp.id, seq: originalSeq}, {seq: cp.seq}, function(err, info) {
|
||||
if (err) return cb(err);
|
||||
// possible outcomes
|
||||
// 1) seq was updated to seq+1 - exactly what we wanted!
|
||||
// 2) somebody else already updated seq to seq+1 and our call was a no-op.
|
||||
// That should be ok, checkpoints are time based, so we reuse the one created just now
|
||||
// 3) seq was bumped more than once, so we will be using a value that is behind the latest seq.
|
||||
// @bajtos is not entirely sure if this is ok, but since it wasn't handled by the current implementation either,
|
||||
// he thinks we can keep it this way.
|
||||
cb(null, cp);
|
||||
});
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
};
|
||||
|
|
|
@ -1,17 +1,24 @@
|
|||
// Copyright IBM Corp. 2014,2019. All Rights Reserved.
|
||||
// Node module: loopback
|
||||
// This file is licensed under the MIT License.
|
||||
// License text available at https://opensource.org/licenses/MIT
|
||||
|
||||
'use strict';
|
||||
const g = require('../../lib/globalize');
|
||||
|
||||
/**
|
||||
* Email model. Extends LoopBack base [Model](#model-new-model).
|
||||
* @property {String} to Email addressee. Required.
|
||||
* @property {String} from Email sender address. Required.
|
||||
* @property {String} subject Email subject string. Required.
|
||||
* @property {String} text Text body of email.
|
||||
* @property {String} html HTML body of email.
|
||||
*
|
||||
* @class Email
|
||||
* @inherits {Model}
|
||||
*/
|
||||
* Email model. Extends LoopBack base [Model](#model-new-model).
|
||||
* @property {String} to Email addressee. Required.
|
||||
* @property {String} from Email sender address. Required.
|
||||
* @property {String} subject Email subject string. Required.
|
||||
* @property {String} text Text body of email.
|
||||
* @property {String} html HTML body of email.
|
||||
*
|
||||
* @class Email
|
||||
* @inherits {Model}
|
||||
*/
|
||||
|
||||
module.exports = function(Email) {
|
||||
|
||||
/**
|
||||
* Send an email with the given `options`.
|
||||
*
|
||||
|
@ -39,13 +46,13 @@ module.exports = function(Email) {
|
|||
*/
|
||||
|
||||
Email.send = function() {
|
||||
throw new Error('You must connect the Email Model to a Mail connector');
|
||||
throw new Error(g.f('You must connect the {{Email}} Model to a {{Mail}} connector'));
|
||||
};
|
||||
|
||||
/**
|
||||
* A shortcut for Email.send(this).
|
||||
*/
|
||||
Email.prototype.send = function() {
|
||||
throw new Error('You must connect the Email Model to a Mail connector');
|
||||
throw new Error(g.f('You must connect the {{Email}} Model to a {{Mail}} connector'));
|
||||
};
|
||||
};
|
||||
|
|
|
@ -0,0 +1,243 @@
|
|||
// Copyright IBM Corp. 2016,2019. All Rights Reserved.
|
||||
// Node module: loopback
|
||||
// This file is licensed under the MIT License.
|
||||
// License text available at https://opensource.org/licenses/MIT
|
||||
|
||||
'use strict';
|
||||
const g = require('../../lib/globalize');
|
||||
|
||||
/**
|
||||
* Data model for key-value databases.
|
||||
*
|
||||
* @class KeyValueModel
|
||||
* @inherits {Model}
|
||||
*/
|
||||
|
||||
module.exports = function(KeyValueModel) {
|
||||
/**
|
||||
* Return the value associated with a given key.
|
||||
*
|
||||
* @param {String} key Key to use when searching the database.
|
||||
* @options {Object} options
|
||||
* @callback {Function} callback
|
||||
* @param {Error} err Error object.
|
||||
* @param {Any} result Value associated with the given key.
|
||||
* @promise
|
||||
*
|
||||
* @header KeyValueModel.get(key, cb)
|
||||
*/
|
||||
KeyValueModel.get = function(key, options, callback) {
|
||||
throwNotAttached(this.modelName, 'get');
|
||||
};
|
||||
|
||||
/**
|
||||
* Persist a value and associate it with the given key.
|
||||
*
|
||||
* @param {String} key Key to associate with the given value.
|
||||
* @param {Any} value Value to persist.
|
||||
* @options {Number|Object} options Optional settings for the key-value
|
||||
* pair. If a Number is provided, it is set as the TTL (time to live) in ms
|
||||
* (milliseconds) for the key-value pair.
|
||||
* @property {Number} ttl TTL for the key-value pair in ms.
|
||||
* @callback {Function} callback
|
||||
* @param {Error} err Error object.
|
||||
* @promise
|
||||
*
|
||||
* @header KeyValueModel.set(key, value, cb)
|
||||
*/
|
||||
KeyValueModel.set = function(key, value, options, callback) {
|
||||
throwNotAttached(this.modelName, 'set');
|
||||
};
|
||||
|
||||
/**
|
||||
* Set the TTL (time to live) in ms (milliseconds) for a given key. TTL is the
|
||||
* remaining time before a key-value pair is discarded from the database.
|
||||
*
|
||||
* @param {String} key Key to use when searching the database.
|
||||
* @param {Number} ttl TTL in ms to set for the key.
|
||||
* @options {Object} options
|
||||
* @callback {Function} callback
|
||||
* @param {Error} err Error object.
|
||||
* @promise
|
||||
*
|
||||
* @header KeyValueModel.expire(key, ttl, cb)
|
||||
*/
|
||||
KeyValueModel.expire = function(key, ttl, options, callback) {
|
||||
throwNotAttached(this.modelName, 'expire');
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the TTL (time to live) for a given key. TTL is the remaining time
|
||||
* before a key-value pair is discarded from the database.
|
||||
*
|
||||
* @param {String} key Key to use when searching the database.
|
||||
* @options {Object} options
|
||||
* @callback {Function} callback
|
||||
* @param {Error} error
|
||||
* @param {Number} ttl Expiration time for the key-value pair. `undefined` if
|
||||
* TTL was not initially set.
|
||||
* @promise
|
||||
*
|
||||
* @header KeyValueModel.ttl(key, cb)
|
||||
*/
|
||||
KeyValueModel.ttl = function(key, options, callback) {
|
||||
throwNotAttached(this.modelName, 'ttl');
|
||||
};
|
||||
|
||||
/**
|
||||
* Return all keys in the database.
|
||||
*
|
||||
* **WARNING**: This method is not suitable for large data sets as all
|
||||
* key-values pairs are loaded into memory at once. For large data sets,
|
||||
* use `iterateKeys()` instead.
|
||||
*
|
||||
* @param {Object} filter An optional filter object with the following
|
||||
* @param {String} filter.match Glob string used to filter returned
|
||||
* keys (i.e. `userid.*`). All connectors are required to support `*` and
|
||||
* `?`, but may also support additional special characters specific to the
|
||||
* database.
|
||||
* @param {Object} options
|
||||
* @callback {Function} callback
|
||||
* @promise
|
||||
*
|
||||
* @header KeyValueModel.keys(filter, cb)
|
||||
*/
|
||||
KeyValueModel.keys = function(filter, options, callback) {
|
||||
throwNotAttached(this.modelName, 'keys');
|
||||
};
|
||||
|
||||
/**
|
||||
* Asynchronously iterate all keys in the database. Similar to `.keys()` but
|
||||
* instead allows for iteration over large data sets without having to load
|
||||
* everything into memory at once.
|
||||
*
|
||||
* Callback example:
|
||||
* ```js
|
||||
* // Given a model named `Color` with two keys `red` and `blue`
|
||||
* var iterator = Color.iterateKeys();
|
||||
* it.next(function(err, key) {
|
||||
* // key contains `red`
|
||||
* it.next(function(err, key) {
|
||||
* // key contains `blue`
|
||||
* });
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* Promise example:
|
||||
* ```js
|
||||
* // Given a model named `Color` with two keys `red` and `blue`
|
||||
* var iterator = Color.iterateKeys();
|
||||
* Promise.resolve().then(function() {
|
||||
* return it.next();
|
||||
* })
|
||||
* .then(function(key) {
|
||||
* // key contains `red`
|
||||
* return it.next();
|
||||
* });
|
||||
* .then(function(key) {
|
||||
* // key contains `blue`
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* @param {Object} filter An optional filter object with the following
|
||||
* @param {String} filter.match Glob string to use to filter returned
|
||||
* keys (i.e. `userid.*`). All connectors are required to support `*` and
|
||||
* `?`. They may also support additional special characters that are
|
||||
* specific to the backing database.
|
||||
* @param {Object} options
|
||||
* @returns {AsyncIterator} An Object implementing `next(cb) -> Promise`
|
||||
* function that can be used to iterate all keys.
|
||||
*
|
||||
* @header KeyValueModel.iterateKeys(filter)
|
||||
*/
|
||||
KeyValueModel.iterateKeys = function(filter, options) {
|
||||
throwNotAttached(this.modelName, 'iterateKeys');
|
||||
};
|
||||
|
||||
/*!
|
||||
* Set up remoting metadata for this model.
|
||||
*
|
||||
* **Notes**:
|
||||
* - The method is called automatically by `Model.extend` and/or
|
||||
* `app.registry.createModel`
|
||||
* - In general, base models use call this to ensure remote methods are
|
||||
* inherited correctly, see bug at
|
||||
* https://github.com/strongloop/loopback/issues/2350
|
||||
*/
|
||||
KeyValueModel.setup = function() {
|
||||
KeyValueModel.base.setup.apply(this, arguments);
|
||||
|
||||
this.remoteMethod('get', {
|
||||
accepts: {
|
||||
arg: 'key', type: 'string', required: true,
|
||||
http: {source: 'path'},
|
||||
},
|
||||
returns: {arg: 'value', type: 'any', root: true},
|
||||
http: {path: '/:key', verb: 'get'},
|
||||
rest: {after: convertNullToNotFoundError},
|
||||
});
|
||||
|
||||
this.remoteMethod('set', {
|
||||
accepts: [
|
||||
{arg: 'key', type: 'string', required: true,
|
||||
http: {source: 'path'}},
|
||||
{arg: 'value', type: 'any', required: true,
|
||||
http: {source: 'body'}},
|
||||
{arg: 'ttl', type: 'number',
|
||||
http: {source: 'query'},
|
||||
description: 'time to live in milliseconds'},
|
||||
],
|
||||
http: {path: '/:key', verb: 'put'},
|
||||
});
|
||||
|
||||
this.remoteMethod('expire', {
|
||||
accepts: [
|
||||
{arg: 'key', type: 'string', required: true,
|
||||
http: {source: 'path'}},
|
||||
{arg: 'ttl', type: 'number', required: true,
|
||||
http: {source: 'form'}},
|
||||
],
|
||||
http: {path: '/:key/expire', verb: 'put'},
|
||||
});
|
||||
|
||||
this.remoteMethod('ttl', {
|
||||
accepts: {
|
||||
arg: 'key', type: 'string', required: true,
|
||||
http: {source: 'path'},
|
||||
},
|
||||
returns: {arg: 'value', type: 'any', root: true},
|
||||
http: {path: '/:key/ttl', verb: 'get'},
|
||||
});
|
||||
|
||||
this.remoteMethod('keys', {
|
||||
accepts: {
|
||||
arg: 'filter', type: 'object', required: false,
|
||||
http: {source: 'query'},
|
||||
},
|
||||
returns: {arg: 'keys', type: ['string'], root: true},
|
||||
http: {path: '/keys', verb: 'get'},
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
function throwNotAttached(modelName, methodName) {
|
||||
throw new Error(g.f(
|
||||
'Cannot call %s.%s(). ' +
|
||||
'The %s method has not been setup. ' +
|
||||
'The {{KeyValueModel}} has not been correctly attached ' +
|
||||
'to a {{DataSource}}!',
|
||||
modelName, methodName, methodName,
|
||||
));
|
||||
}
|
||||
|
||||
function convertNullToNotFoundError(ctx, cb) {
|
||||
if (ctx.result !== null) return cb();
|
||||
|
||||
const modelName = ctx.method.sharedClass.name;
|
||||
const id = ctx.getArgByName('id');
|
||||
const msg = g.f('Unknown "%s" {{key}} "%s".', modelName, id);
|
||||
const error = new Error(msg);
|
||||
error.statusCode = error.status = 404;
|
||||
error.code = 'KEY_NOT_FOUND';
|
||||
cb(error);
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"name": "KeyValueModel",
|
||||
"base": "Model"
|
||||
}
|
|
@ -1,4 +1,11 @@
|
|||
var loopback = require('../../lib/loopback');
|
||||
// Copyright IBM Corp. 2014,2019. All Rights Reserved.
|
||||
// Node module: loopback
|
||||
// This file is licensed under the MIT License.
|
||||
// License text available at https://opensource.org/licenses/MIT
|
||||
|
||||
'use strict';
|
||||
const loopback = require('../../lib/loopback');
|
||||
const utils = require('../../lib/utils');
|
||||
|
||||
/**
|
||||
* The `RoleMapping` model extends from the built in `loopback.Model` type.
|
||||
|
@ -19,10 +26,10 @@ module.exports = function(RoleMapping) {
|
|||
|
||||
RoleMapping.resolveRelatedModels = function() {
|
||||
if (!this.userModel) {
|
||||
var reg = this.registry;
|
||||
this.roleModel = reg.getModelByType(loopback.Role);
|
||||
this.userModel = reg.getModelByType(loopback.User);
|
||||
this.applicationModel = reg.getModelByType(loopback.Application);
|
||||
const reg = this.registry;
|
||||
this.roleModel = reg.getModelByType('Role');
|
||||
this.userModel = reg.getModelByType('User');
|
||||
this.applicationModel = reg.getModelByType('Application');
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -33,16 +40,18 @@ module.exports = function(RoleMapping) {
|
|||
* @param {Application} application
|
||||
*/
|
||||
RoleMapping.prototype.application = function(callback) {
|
||||
callback = callback || utils.createPromiseCallback();
|
||||
this.constructor.resolveRelatedModels();
|
||||
|
||||
if (this.principalType === RoleMapping.APPLICATION) {
|
||||
var applicationModel = this.constructor.applicationModel;
|
||||
const applicationModel = this.constructor.applicationModel;
|
||||
applicationModel.findById(this.principalId, callback);
|
||||
} else {
|
||||
process.nextTick(function() {
|
||||
if (callback) callback(null, null);
|
||||
callback(null, null);
|
||||
});
|
||||
}
|
||||
return callback.promise;
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -52,15 +61,26 @@ module.exports = function(RoleMapping) {
|
|||
* @param {User} user
|
||||
*/
|
||||
RoleMapping.prototype.user = function(callback) {
|
||||
callback = callback || utils.createPromiseCallback();
|
||||
this.constructor.resolveRelatedModels();
|
||||
let userModel;
|
||||
|
||||
if (this.principalType === RoleMapping.USER) {
|
||||
var userModel = this.constructor.userModel;
|
||||
userModel = this.constructor.userModel;
|
||||
userModel.findById(this.principalId, callback);
|
||||
return callback.promise;
|
||||
}
|
||||
|
||||
// try resolving a user model that matches principalType
|
||||
userModel = this.constructor.registry.findModel(this.principalType);
|
||||
if (userModel) {
|
||||
userModel.findById(this.principalId, callback);
|
||||
} else {
|
||||
process.nextTick(function() {
|
||||
if (callback) callback(null, null);
|
||||
callback(null, null);
|
||||
});
|
||||
}
|
||||
return callback.promise;
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -70,15 +90,17 @@ module.exports = function(RoleMapping) {
|
|||
* @param {User} childUser
|
||||
*/
|
||||
RoleMapping.prototype.childRole = function(callback) {
|
||||
callback = callback || utils.createPromiseCallback();
|
||||
this.constructor.resolveRelatedModels();
|
||||
|
||||
if (this.principalType === RoleMapping.ROLE) {
|
||||
var roleModel = this.constructor.roleModel;
|
||||
const roleModel = this.constructor.roleModel;
|
||||
roleModel.findById(this.principalId, callback);
|
||||
} else {
|
||||
process.nextTick(function() {
|
||||
if (callback) callback(null, null);
|
||||
callback(null, null);
|
||||
});
|
||||
}
|
||||
return callback.promise;
|
||||
};
|
||||
};
|
||||
|
|
|
@ -9,9 +9,12 @@
|
|||
},
|
||||
"principalType": {
|
||||
"type": "string",
|
||||
"description": "The principal type, such as user, application, or role"
|
||||
"description": "The principal type, such as USER, APPLICATION, ROLE, or user model name in case of multiple user models"
|
||||
},
|
||||
"principalId": "string"
|
||||
"principalId": {
|
||||
"type": "string",
|
||||
"index": true
|
||||
}
|
||||
},
|
||||
"relations": {
|
||||
"role": {
|
||||
|
|
|
@ -1,11 +1,18 @@
|
|||
var loopback = require('../../lib/loopback');
|
||||
var debug = require('debug')('loopback:security:role');
|
||||
var assert = require('assert');
|
||||
var async = require('async');
|
||||
// Copyright IBM Corp. 2014,2019. All Rights Reserved.
|
||||
// Node module: loopback
|
||||
// This file is licensed under the MIT License.
|
||||
// License text available at https://opensource.org/licenses/MIT
|
||||
|
||||
var AccessContext = require('../../lib/access-context').AccessContext;
|
||||
|
||||
var RoleMapping = loopback.RoleMapping;
|
||||
'use strict';
|
||||
const loopback = require('../../lib/loopback');
|
||||
const debug = require('debug')('loopback:security:role');
|
||||
const assert = require('assert');
|
||||
const async = require('async');
|
||||
const utils = require('../../lib/utils');
|
||||
const ctx = require('../../lib/access-context');
|
||||
const AccessContext = ctx.AccessContext;
|
||||
const Principal = ctx.Principal;
|
||||
const RoleMapping = loopback.RoleMapping;
|
||||
|
||||
assert(RoleMapping, 'RoleMapping model must be defined before Role model');
|
||||
|
||||
|
@ -15,92 +22,121 @@ assert(RoleMapping, 'RoleMapping model must be defined before Role model');
|
|||
* @header Role object
|
||||
*/
|
||||
module.exports = function(Role) {
|
||||
|
||||
// Workaround for https://github.com/strongloop/loopback/issues/292
|
||||
Role.definition.rawProperties.created.default =
|
||||
Role.definition.properties.created.default = function() {
|
||||
return new Date();
|
||||
};
|
||||
|
||||
// Workaround for https://github.com/strongloop/loopback/issues/292
|
||||
Role.definition.rawProperties.modified.default =
|
||||
Role.definition.properties.modified.default = function() {
|
||||
return new Date();
|
||||
};
|
||||
|
||||
Role.resolveRelatedModels = function() {
|
||||
if (!this.userModel) {
|
||||
var reg = this.registry;
|
||||
this.roleMappingModel = reg.getModelByType(loopback.RoleMapping);
|
||||
this.userModel = reg.getModelByType(loopback.User);
|
||||
this.applicationModel = reg.getModelByType(loopback.Application);
|
||||
const reg = this.registry;
|
||||
this.roleMappingModel = reg.getModelByType('RoleMapping');
|
||||
this.userModel = reg.getModelByType('User');
|
||||
this.applicationModel = reg.getModelByType('Application');
|
||||
}
|
||||
};
|
||||
|
||||
// Set up the connection to users/applications/roles once the model
|
||||
Role.once('dataSourceAttached', function(roleModel) {
|
||||
|
||||
['users', 'applications', 'roles'].forEach(function(rel) {
|
||||
/**
|
||||
* Fetch all users assigned to this role
|
||||
* @function Role.prototype#users
|
||||
* @param {object} [query] query object passed to model find call
|
||||
* @param {Function} [callback]
|
||||
* @callback {Function} [callback] The callback function
|
||||
* @param {String|Error} err The error string or object
|
||||
* @param {Array} list The list of users.
|
||||
* @promise
|
||||
*/
|
||||
/**
|
||||
* Fetch all applications assigned to this role
|
||||
* @function Role.prototype#applications
|
||||
* @param {object} [query] query object passed to model find call
|
||||
* @param {Function} [callback]
|
||||
* @callback {Function} [callback] The callback function
|
||||
* @param {String|Error} err The error string or object
|
||||
* @param {Array} list The list of applications.
|
||||
* @promise
|
||||
*/
|
||||
/**
|
||||
* Fetch all roles assigned to this role
|
||||
* @function Role.prototype#roles
|
||||
* @param {object} [query] query object passed to model find call
|
||||
* @param {Function} [callback]
|
||||
* @callback {Function} [callback] The callback function
|
||||
* @param {String|Error} err The error string or object
|
||||
* @param {Array} list The list of roles.
|
||||
* @promise
|
||||
*/
|
||||
Role.prototype[rel] = function(query, callback) {
|
||||
if (!callback) {
|
||||
if (typeof query === 'function') {
|
||||
callback = query;
|
||||
query = {};
|
||||
} else {
|
||||
callback = utils.createPromiseCallback();
|
||||
}
|
||||
}
|
||||
query = query || {};
|
||||
query.where = query.where || {};
|
||||
|
||||
roleModel.resolveRelatedModels();
|
||||
var relsToModels = {
|
||||
const relsToModels = {
|
||||
users: roleModel.userModel,
|
||||
applications: roleModel.applicationModel,
|
||||
roles: roleModel
|
||||
roles: roleModel,
|
||||
};
|
||||
|
||||
var ACL = loopback.ACL;
|
||||
var relsToTypes = {
|
||||
const ACL = loopback.ACL;
|
||||
const relsToTypes = {
|
||||
users: ACL.USER,
|
||||
applications: ACL.APP,
|
||||
roles: ACL.ROLE
|
||||
roles: ACL.ROLE,
|
||||
};
|
||||
|
||||
var model = relsToModels[rel];
|
||||
listByPrincipalType(model, relsToTypes[rel], query, callback);
|
||||
let principalModel = relsToModels[rel];
|
||||
let principalType = relsToTypes[rel];
|
||||
|
||||
// redefine user model and user type if user principalType is custom (available and not "USER")
|
||||
const isCustomUserPrincipalType = rel === 'users' &&
|
||||
query.where.principalType &&
|
||||
query.where.principalType !== RoleMapping.USER;
|
||||
|
||||
if (isCustomUserPrincipalType) {
|
||||
const registry = this.constructor.registry;
|
||||
principalModel = registry.findModel(query.where.principalType);
|
||||
principalType = query.where.principalType;
|
||||
}
|
||||
// make sure we don't keep principalType in userModel query
|
||||
delete query.where.principalType;
|
||||
|
||||
if (principalModel) {
|
||||
listByPrincipalType(this, principalModel, principalType, query, callback);
|
||||
} else {
|
||||
process.nextTick(function() {
|
||||
callback(null, []);
|
||||
});
|
||||
}
|
||||
return callback.promise;
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* Fetch all models assigned to this role
|
||||
* @private
|
||||
* @param {object} Context role context
|
||||
* @param {*} model model type to fetch
|
||||
* @param {String} [principalType] principalType used in the rolemapping for model
|
||||
* @param {object} [query] query object passed to model find call
|
||||
* @param {Function} [callback] callback function called with `(err, models)` arguments.
|
||||
*/
|
||||
function listByPrincipalType(model, principalType, query, callback) {
|
||||
if (callback === undefined) {
|
||||
function listByPrincipalType(context, model, principalType, query, callback) {
|
||||
if (callback === undefined && typeof query === 'function') {
|
||||
callback = query;
|
||||
query = {};
|
||||
}
|
||||
query = query || {};
|
||||
|
||||
roleModel.roleMappingModel.find({
|
||||
where: {roleId: this.id, principalType: principalType}
|
||||
where: {roleId: context.id, principalType: principalType},
|
||||
}, function(err, mappings) {
|
||||
var ids;
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
ids = mappings.map(function(m) {
|
||||
const ids = mappings.map(function(m) {
|
||||
return m.principalId;
|
||||
});
|
||||
query.where = query.where || {};
|
||||
|
@ -110,7 +146,6 @@ module.exports = function(Role) {
|
|||
});
|
||||
});
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
// Special roles
|
||||
|
@ -123,8 +158,9 @@ module.exports = function(Role) {
|
|||
/**
|
||||
* Add custom handler for roles.
|
||||
* @param {String} role Name of role.
|
||||
* @param {Function} resolver Function that determines if a principal is in the specified role.
|
||||
* Signature must be `function(role, context, callback)`
|
||||
* @param {Function} resolver Function that determines
|
||||
* if a principal is in the specified role.
|
||||
* Should provide a callback or return a promise.
|
||||
*/
|
||||
Role.registerResolver = function(role, resolver) {
|
||||
if (!Role.resolvers) {
|
||||
|
@ -140,19 +176,20 @@ module.exports = function(Role) {
|
|||
});
|
||||
return;
|
||||
}
|
||||
var modelClass = context.model;
|
||||
var modelId = context.modelId;
|
||||
var userId = context.getUserId();
|
||||
Role.isOwner(modelClass, modelId, userId, callback);
|
||||
const modelClass = context.model;
|
||||
const modelId = context.modelId;
|
||||
const user = context.getUser();
|
||||
const userId = user && user.id;
|
||||
const principalType = user && user.principalType;
|
||||
const opts = {accessToken: context.accessToken};
|
||||
Role.isOwner(modelClass, modelId, userId, principalType, opts, callback);
|
||||
});
|
||||
|
||||
function isUserClass(modelClass) {
|
||||
if (modelClass) {
|
||||
return modelClass === loopback.User ||
|
||||
modelClass.prototype instanceof loopback.User;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
if (!modelClass) return false;
|
||||
const User = modelClass.modelBuilder.models.User;
|
||||
if (!User) return false;
|
||||
return modelClass == User || modelClass.prototype instanceof User;
|
||||
}
|
||||
|
||||
/*!
|
||||
|
@ -175,62 +212,200 @@ module.exports = function(Role) {
|
|||
* @param {Function} modelClass The model class
|
||||
* @param {*} modelId The model ID
|
||||
* @param {*} userId The user ID
|
||||
* @param {Function} callback Callback function
|
||||
* @param {String} principalType The user principalType (optional)
|
||||
* @options {Object} options
|
||||
* @property {accessToken} The access token used to authorize the current user.
|
||||
* @callback {Function} [callback] The callback function
|
||||
* @param {String|Error} err The error string or object
|
||||
* @param {Boolean} isOwner True if the user is an owner.
|
||||
* @promise
|
||||
*/
|
||||
Role.isOwner = function isOwner(modelClass, modelId, userId, callback) {
|
||||
Role.isOwner = function isOwner(modelClass, modelId, userId, principalType, options, callback) {
|
||||
const _this = this;
|
||||
|
||||
if (!callback && typeof options === 'function') {
|
||||
callback = options;
|
||||
options = {};
|
||||
} else if (!callback && typeof principalType === 'function') {
|
||||
callback = principalType;
|
||||
principalType = undefined;
|
||||
options = {};
|
||||
}
|
||||
principalType = principalType || Principal.USER;
|
||||
|
||||
assert(modelClass, 'Model class is required');
|
||||
debug('isOwner(): %s %s userId: %s', modelClass && modelClass.modelName, modelId, userId);
|
||||
// No userId is present
|
||||
if (!callback) callback = utils.createPromiseCallback();
|
||||
|
||||
debug('isOwner(): %s %s userId: %s principalType: %s',
|
||||
modelClass && modelClass.modelName, modelId, userId, principalType);
|
||||
|
||||
// Resolve isOwner false if userId is missing
|
||||
if (!userId) {
|
||||
debug('isOwner(): no user id was set, returning false');
|
||||
process.nextTick(function() {
|
||||
callback(null, false);
|
||||
});
|
||||
return;
|
||||
return callback.promise;
|
||||
}
|
||||
|
||||
// At this stage, principalType is valid in one of 2 following condition:
|
||||
// 1. the app has a single user model and principalType is 'USER'
|
||||
// 2. the app has multiple user models and principalType is not 'USER'
|
||||
// multiple user models
|
||||
const isMultipleUsers = _isMultipleUsers();
|
||||
const isPrincipalTypeValid =
|
||||
(!isMultipleUsers && principalType === Principal.USER) ||
|
||||
(isMultipleUsers && principalType !== Principal.USER);
|
||||
|
||||
debug('isOwner(): isMultipleUsers?', isMultipleUsers,
|
||||
'isPrincipalTypeValid?', isPrincipalTypeValid);
|
||||
|
||||
// Resolve isOwner false if principalType is invalid
|
||||
if (!isPrincipalTypeValid) {
|
||||
process.nextTick(function() {
|
||||
callback(null, false);
|
||||
});
|
||||
return callback.promise;
|
||||
}
|
||||
|
||||
// Is the modelClass User or a subclass of User?
|
||||
if (isUserClass(modelClass)) {
|
||||
process.nextTick(function() {
|
||||
callback(null, matches(modelId, userId));
|
||||
});
|
||||
return;
|
||||
const userModelName = modelClass.modelName;
|
||||
// matching ids is enough if principalType is USER or matches given user model name
|
||||
if (principalType === Principal.USER || principalType === userModelName) {
|
||||
process.nextTick(function() {
|
||||
callback(null, matches(modelId, userId));
|
||||
});
|
||||
return callback.promise;
|
||||
}
|
||||
// otherwise continue with the regular owner resolution
|
||||
}
|
||||
|
||||
modelClass.findById(modelId, function(err, inst) {
|
||||
modelClass.findById(modelId, options, function(err, inst) {
|
||||
if (err || !inst) {
|
||||
debug('Model not found for id %j', modelId);
|
||||
if (callback) callback(err, false);
|
||||
return;
|
||||
return callback(err, false);
|
||||
}
|
||||
debug('Model found: %j', inst);
|
||||
var ownerId = inst.userId || inst.owner;
|
||||
// Ensure ownerId exists and is not a function/relation
|
||||
if (ownerId && 'function' !== typeof ownerId) {
|
||||
if (callback) callback(null, matches(ownerId, userId));
|
||||
return;
|
||||
} else {
|
||||
// Try to follow belongsTo
|
||||
for (var r in modelClass.relations) {
|
||||
var rel = modelClass.relations[r];
|
||||
if (rel.type === 'belongsTo' && isUserClass(rel.modelTo)) {
|
||||
debug('Checking relation %s to %s: %j', r, rel.modelTo.modelName, rel);
|
||||
inst[r](processRelatedUser);
|
||||
return;
|
||||
}
|
||||
}
|
||||
debug('No matching belongsTo relation found for model %j and user: %j', modelId, userId);
|
||||
if (callback) callback(null, false);
|
||||
}
|
||||
|
||||
function processRelatedUser(err, user) {
|
||||
if (!err && user) {
|
||||
debug('User found: %j', user.id);
|
||||
if (callback) callback(null, matches(user.id, userId));
|
||||
} else {
|
||||
if (callback) callback(err, false);
|
||||
}
|
||||
const ownerRelations = modelClass.settings.ownerRelations;
|
||||
if (!ownerRelations) {
|
||||
return legacyOwnershipCheck(inst);
|
||||
} else {
|
||||
return checkOwnership(inst);
|
||||
}
|
||||
});
|
||||
return callback.promise;
|
||||
|
||||
// NOTE Historically, for principalType USER, we were resolving isOwner()
|
||||
// as true if the model has "userId" or "owner" property matching
|
||||
// id of the current user (principalId), even though there was no
|
||||
// belongsTo relation set up.
|
||||
// Additionaly, the original implementation did not support the
|
||||
// possibility for a model to have multiple related users: when
|
||||
// testing belongsTo relations, the first related user failing the
|
||||
// ownership check induced the whole isOwner() to resolve as false.
|
||||
// This behaviour will be pruned at next LoopBack major release.
|
||||
function legacyOwnershipCheck(inst) {
|
||||
const ownerId = inst.userId || inst.owner;
|
||||
if (principalType === Principal.USER && ownerId && 'function' !== typeof ownerId) {
|
||||
return callback(null, matches(ownerId, userId));
|
||||
}
|
||||
|
||||
// Try to follow belongsTo
|
||||
for (const r in modelClass.relations) {
|
||||
const rel = modelClass.relations[r];
|
||||
// relation should be belongsTo and target a User based class
|
||||
const belongsToUser = rel.type === 'belongsTo' && isUserClass(rel.modelTo);
|
||||
if (!belongsToUser) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// checking related user
|
||||
const relatedUser = rel.modelTo;
|
||||
const userModelName = relatedUser.modelName;
|
||||
const isMultipleUsers = _isMultipleUsers(relatedUser);
|
||||
// a relation can be considered for isOwner resolution if:
|
||||
// 1. the app has a single user model and principalType is 'USER'
|
||||
// 2. the app has multiple user models and principalType is the related user model name
|
||||
if ((!isMultipleUsers && principalType === Principal.USER) ||
|
||||
(isMultipleUsers && principalType === userModelName)) {
|
||||
debug('Checking relation %s to %s: %j', r, userModelName, rel);
|
||||
inst[r](processRelatedUser);
|
||||
return;
|
||||
}
|
||||
}
|
||||
debug('No matching belongsTo relation found for model %j - user %j principalType %j',
|
||||
modelId, userId, principalType);
|
||||
callback(null, false);
|
||||
|
||||
function processRelatedUser(err, user) {
|
||||
if (err || !user) return callback(err, false);
|
||||
|
||||
debug('User found: %j', user.id);
|
||||
callback(null, matches(user.id, userId));
|
||||
}
|
||||
}
|
||||
|
||||
function checkOwnership(inst) {
|
||||
const ownerRelations = inst.constructor.settings.ownerRelations;
|
||||
// collecting related users
|
||||
const relWithUsers = [];
|
||||
for (const r in modelClass.relations) {
|
||||
const rel = modelClass.relations[r];
|
||||
// relation should be belongsTo and target a User based class
|
||||
if (rel.type !== 'belongsTo' || !isUserClass(rel.modelTo)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// checking related user
|
||||
const relatedUser = rel.modelTo;
|
||||
const userModelName = relatedUser.modelName;
|
||||
const isMultipleUsers = _isMultipleUsers(relatedUser);
|
||||
// a relation can be considered for isOwner resolution if:
|
||||
// 1. the app has a single user model and principalType is 'USER'
|
||||
// 2. the app has multiple user models and principalType is the related user model name
|
||||
// In addition, if an array of relations if provided with the ownerRelations option,
|
||||
// then the given relation name is further checked against this array
|
||||
if ((!isMultipleUsers && principalType === Principal.USER) ||
|
||||
(isMultipleUsers && principalType === userModelName)) {
|
||||
debug('Checking relation %s to %s: %j', r, userModelName, rel);
|
||||
if (ownerRelations === true) {
|
||||
relWithUsers.push(r);
|
||||
} else if (Array.isArray(ownerRelations) && ownerRelations.indexOf(r) !== -1) {
|
||||
relWithUsers.push(r);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (relWithUsers.length === 0) {
|
||||
debug('No matching belongsTo relation found for model %j and user: %j principalType: %j',
|
||||
modelId, userId, principalType);
|
||||
return callback(null, false);
|
||||
}
|
||||
|
||||
// check related users: someSeries is used to avoid spamming the db
|
||||
async.someSeries(relWithUsers, processRelation, callback);
|
||||
|
||||
function processRelation(r, cb) {
|
||||
inst[r](function processRelatedUser(err, user) {
|
||||
if (err || !user) return cb(err, false);
|
||||
|
||||
debug('User found: %j (through %j)', user.id, r);
|
||||
cb(null, matches(user.id, userId));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// A helper function to check if the app user config is multiple users or
|
||||
// single user. It can be used with or without a reference user model.
|
||||
// In case no user model is provided, we use the registry to get any of the
|
||||
// user model by type. The relation with AccessToken is used to check
|
||||
// if polymorphism is used, and thus if multiple users.
|
||||
function _isMultipleUsers(userModel) {
|
||||
const oneOfUserModels = userModel || _this.registry.getModelByType('User');
|
||||
const accessTokensRel = oneOfUserModels.relations.accessTokens;
|
||||
return !!(accessTokensRel && accessTokensRel.polymorphic);
|
||||
}
|
||||
};
|
||||
|
||||
Role.registerResolver(Role.AUTHENTICATED, function(role, context, callback) {
|
||||
|
@ -250,11 +425,14 @@ module.exports = function(Role) {
|
|||
* @callback {Function} callback Callback function.
|
||||
* @param {Error} err Error object.
|
||||
* @param {Boolean} isAuthenticated True if the user is authenticated.
|
||||
* @promise
|
||||
*/
|
||||
Role.isAuthenticated = function isAuthenticated(context, callback) {
|
||||
if (!callback) callback = utils.createPromiseCallback();
|
||||
process.nextTick(function() {
|
||||
if (callback) callback(null, context.isAuthenticated());
|
||||
});
|
||||
return callback.promise;
|
||||
};
|
||||
|
||||
Role.registerResolver(Role.UNAUTHENTICATED, function(role, context, callback) {
|
||||
|
@ -278,22 +456,41 @@ module.exports = function(Role) {
|
|||
* @callback {Function} callback Callback function.
|
||||
* @param {Error} err Error object.
|
||||
* @param {Boolean} isInRole True if the principal is in the specified role.
|
||||
* @promise
|
||||
*/
|
||||
Role.isInRole = function(role, context, callback) {
|
||||
context.registry = this.registry;
|
||||
if (!(context instanceof AccessContext)) {
|
||||
context = new AccessContext(context);
|
||||
}
|
||||
|
||||
if (!callback) {
|
||||
callback = utils.createPromiseCallback();
|
||||
// historically, isInRole is returning the Role instance instead of true
|
||||
// we are preserving that behaviour for callback-based invocation,
|
||||
// but fixing it when invoked in Promise mode
|
||||
callback.promise = callback.promise.then(function(isInRole) {
|
||||
return !!isInRole;
|
||||
});
|
||||
}
|
||||
|
||||
this.resolveRelatedModels();
|
||||
|
||||
debug('isInRole(): %s', role);
|
||||
context.debug();
|
||||
|
||||
var resolver = Role.resolvers[role];
|
||||
const resolver = Role.resolvers[role];
|
||||
if (resolver) {
|
||||
debug('Custom resolver found for role %s', role);
|
||||
resolver(role, context, callback);
|
||||
return;
|
||||
|
||||
const promise = resolver(role, context, callback);
|
||||
if (promise && typeof promise.then === 'function') {
|
||||
promise.then(
|
||||
function(result) { callback(null, result); },
|
||||
callback,
|
||||
);
|
||||
}
|
||||
return callback.promise;
|
||||
}
|
||||
|
||||
if (context.principals.length === 0) {
|
||||
|
@ -301,13 +498,12 @@ module.exports = function(Role) {
|
|||
process.nextTick(function() {
|
||||
if (callback) callback(null, false);
|
||||
});
|
||||
return;
|
||||
return callback.promise;
|
||||
}
|
||||
|
||||
var inRole = context.principals.some(function(p) {
|
||||
|
||||
var principalType = p.type || undefined;
|
||||
var principalId = p.id || undefined;
|
||||
const inRole = context.principals.some(function(p) {
|
||||
const principalType = p.type || undefined;
|
||||
const principalId = p.id || undefined;
|
||||
|
||||
// Check if it's the same role
|
||||
return principalType === RoleMapping.ROLE && principalId === role;
|
||||
|
@ -318,10 +514,10 @@ module.exports = function(Role) {
|
|||
process.nextTick(function() {
|
||||
if (callback) callback(null, true);
|
||||
});
|
||||
return;
|
||||
return callback.promise;
|
||||
}
|
||||
|
||||
var roleMappingModel = this.roleMappingModel;
|
||||
const roleMappingModel = this.roleMappingModel;
|
||||
this.findOne({where: {name: role}}, function(err, result) {
|
||||
if (err) {
|
||||
if (callback) callback(err);
|
||||
|
@ -335,21 +531,22 @@ module.exports = function(Role) {
|
|||
|
||||
// Iterate through the list of principals
|
||||
async.some(context.principals, function(p, done) {
|
||||
var principalType = p.type || undefined;
|
||||
var principalId = p.id || undefined;
|
||||
var roleId = result.id.toString();
|
||||
const principalType = p.type || undefined;
|
||||
let principalId = p.id || undefined;
|
||||
const roleId = result.id.toString();
|
||||
const principalIdIsString = typeof principalId === 'string';
|
||||
|
||||
if (principalId !== null && principalId !== undefined && (typeof principalId !== 'string')) {
|
||||
if (principalId !== null && principalId !== undefined && !principalIdIsString) {
|
||||
principalId = principalId.toString();
|
||||
}
|
||||
|
||||
if (principalType && principalId) {
|
||||
roleMappingModel.findOne({where: {roleId: roleId,
|
||||
principalType: principalType, principalId: principalId}},
|
||||
function(err, result) {
|
||||
debug('Role mapping found: %j', result);
|
||||
done(!err && result); // The only arg is the result
|
||||
});
|
||||
principalType: principalType, principalId: principalId}},
|
||||
function(err, result) {
|
||||
debug('Role mapping found: %j', result);
|
||||
done(!err && result); // The only arg is the result
|
||||
});
|
||||
} else {
|
||||
process.nextTick(function() {
|
||||
done(false);
|
||||
|
@ -360,7 +557,7 @@ module.exports = function(Role) {
|
|||
if (callback) callback(null, inRole);
|
||||
});
|
||||
});
|
||||
|
||||
return callback.promise;
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -370,23 +567,35 @@ module.exports = function(Role) {
|
|||
* @callback {Function} callback Callback function.
|
||||
* @param {Error} err Error object.
|
||||
* @param {String[]} roles An array of role IDs
|
||||
* @promise
|
||||
*/
|
||||
Role.getRoles = function(context, callback) {
|
||||
Role.getRoles = function(context, options, callback) {
|
||||
if (!callback) {
|
||||
if (typeof options === 'function') {
|
||||
callback = options;
|
||||
options = {};
|
||||
} else {
|
||||
callback = utils.createPromiseCallback();
|
||||
}
|
||||
}
|
||||
if (!options) options = {};
|
||||
|
||||
context.registry = this.registry;
|
||||
if (!(context instanceof AccessContext)) {
|
||||
context = new AccessContext(context);
|
||||
}
|
||||
var roles = [];
|
||||
const roles = [];
|
||||
this.resolveRelatedModels();
|
||||
|
||||
var addRole = function(role) {
|
||||
const addRole = function(role) {
|
||||
if (role && roles.indexOf(role) === -1) {
|
||||
roles.push(role);
|
||||
}
|
||||
};
|
||||
|
||||
var self = this;
|
||||
const self = this;
|
||||
// Check against the smart roles
|
||||
var inRoleTasks = [];
|
||||
const inRoleTasks = [];
|
||||
Object.keys(Role.resolvers).forEach(function(role) {
|
||||
inRoleTasks.push(function(done) {
|
||||
self.isInRole(role, context, function(err, inRole) {
|
||||
|
@ -403,11 +612,11 @@ module.exports = function(Role) {
|
|||
});
|
||||
});
|
||||
|
||||
var roleMappingModel = this.roleMappingModel;
|
||||
const roleMappingModel = this.roleMappingModel;
|
||||
context.principals.forEach(function(p) {
|
||||
// Check against the role mappings
|
||||
var principalType = p.type || undefined;
|
||||
var principalId = p.id == null ? undefined : p.id;
|
||||
const principalType = p.type || undefined;
|
||||
let principalId = p.id == null ? undefined : p.id;
|
||||
|
||||
if (typeof principalId !== 'string' && principalId != null) {
|
||||
principalId = principalId.toString();
|
||||
|
@ -421,15 +630,24 @@ module.exports = function(Role) {
|
|||
if (principalType && principalId) {
|
||||
// Please find() treat undefined matches all values
|
||||
inRoleTasks.push(function(done) {
|
||||
roleMappingModel.find({where: {principalType: principalType,
|
||||
principalId: principalId}}, function(err, mappings) {
|
||||
const filter = {where: {principalType: principalType, principalId: principalId}};
|
||||
if (options.returnOnlyRoleNames === true) {
|
||||
filter.include = ['role'];
|
||||
}
|
||||
roleMappingModel.find(filter, function(err, mappings) {
|
||||
debug('Role mappings found: %s %j', err, mappings);
|
||||
if (err) {
|
||||
if (done) done(err);
|
||||
return;
|
||||
}
|
||||
mappings.forEach(function(m) {
|
||||
addRole(m.roleId);
|
||||
let role;
|
||||
if (options.returnOnlyRoleNames === true) {
|
||||
role = m.toJSON().role.name;
|
||||
} else {
|
||||
role = m.roleId;
|
||||
}
|
||||
addRole(role);
|
||||
});
|
||||
if (done) done();
|
||||
});
|
||||
|
@ -441,5 +659,8 @@ module.exports = function(Role) {
|
|||
debug('getRoles() returns: %j %j', err, roles);
|
||||
if (callback) callback(err, roles);
|
||||
});
|
||||
return callback.promise;
|
||||
};
|
||||
|
||||
Role.validatesUniquenessOf('name', {message: 'already exists'});
|
||||
};
|
||||
|
|
|
@ -12,9 +12,14 @@
|
|||
"required": true
|
||||
},
|
||||
"description": "string",
|
||||
|
||||
"created": "date",
|
||||
"modified": "date"
|
||||
"created": {
|
||||
"type":"date",
|
||||
"defaultFn": "now"
|
||||
},
|
||||
"modified": {
|
||||
"type":"date",
|
||||
"defaultFn": "now"
|
||||
}
|
||||
},
|
||||
"relations": {
|
||||
"principals": {
|
||||
|
|
|
@ -1,5 +1,11 @@
|
|||
var assert = require('assert');
|
||||
var loopback = require('../../lib/loopback');
|
||||
// Copyright IBM Corp. 2014,2019. All Rights Reserved.
|
||||
// Node module: loopback
|
||||
// This file is licensed under the MIT License.
|
||||
// License text available at https://opensource.org/licenses/MIT
|
||||
|
||||
'use strict';
|
||||
const assert = require('assert');
|
||||
const loopback = require('../../lib/loopback');
|
||||
|
||||
/**
|
||||
* Resource owner grants/delegates permissions to client applications
|
||||
|
@ -15,7 +21,7 @@ var loopback = require('../../lib/loopback');
|
|||
module.exports = function(Scope) {
|
||||
Scope.resolveRelatedModels = function() {
|
||||
if (!this.aclModel) {
|
||||
var reg = this.registry;
|
||||
const reg = this.registry;
|
||||
this.aclModel = reg.getModelByType(loopback.ACL);
|
||||
}
|
||||
};
|
||||
|
@ -32,7 +38,7 @@ module.exports = function(Scope) {
|
|||
*/
|
||||
Scope.checkPermission = function(scope, model, property, accessType, callback) {
|
||||
this.resolveRelatedModels();
|
||||
var aclModel = this.aclModel;
|
||||
const aclModel = this.aclModel;
|
||||
assert(aclModel,
|
||||
'ACL model must be defined before Scope.checkPermission is called');
|
||||
|
||||
|
@ -41,7 +47,8 @@ module.exports = function(Scope) {
|
|||
if (callback) callback(err);
|
||||
} else {
|
||||
aclModel.checkPermission(
|
||||
aclModel.SCOPE, scope.id, model, property, accessType, callback);
|
||||
aclModel.SCOPE, scope.id, model, property, accessType, callback,
|
||||
);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -11,28 +11,17 @@
|
|||
"type": "string",
|
||||
"required": true
|
||||
},
|
||||
"credentials": {
|
||||
"type": "object",
|
||||
"deprecated": true
|
||||
},
|
||||
"challenges": {
|
||||
"type": "object",
|
||||
"deprecated": true
|
||||
},
|
||||
"email": {
|
||||
"type": "string",
|
||||
"required": true
|
||||
},
|
||||
"emailVerified": "boolean",
|
||||
"verificationToken": "string",
|
||||
"status": "string",
|
||||
"created": "date",
|
||||
"lastUpdated": "date"
|
||||
"verificationToken": "string"
|
||||
},
|
||||
"options": {
|
||||
"caseSensitiveEmail": true
|
||||
},
|
||||
"hidden": ["password"],
|
||||
"hidden": ["password", "verificationToken"],
|
||||
"acls": [
|
||||
{
|
||||
"principalType": "ROLE",
|
||||
|
@ -73,7 +62,20 @@
|
|||
"principalType": "ROLE",
|
||||
"principalId": "$owner",
|
||||
"permission": "ALLOW",
|
||||
"property": "updateAttributes"
|
||||
"property": "patchAttributes"
|
||||
},
|
||||
{
|
||||
"principalType": "ROLE",
|
||||
"principalId": "$owner",
|
||||
"permission": "ALLOW",
|
||||
"property": "replaceById"
|
||||
},
|
||||
{
|
||||
"principalType": "ROLE",
|
||||
"principalId": "$everyone",
|
||||
"permission": "ALLOW",
|
||||
"property": "verify",
|
||||
"accessType": "EXECUTE"
|
||||
},
|
||||
{
|
||||
"principalType": "ROLE",
|
||||
|
@ -87,6 +89,20 @@
|
|||
"permission": "ALLOW",
|
||||
"property": "resetPassword",
|
||||
"accessType": "EXECUTE"
|
||||
},
|
||||
{
|
||||
"principalType": "ROLE",
|
||||
"principalId": "$authenticated",
|
||||
"permission": "ALLOW",
|
||||
"property": "changePassword",
|
||||
"accessType": "EXECUTE"
|
||||
},
|
||||
{
|
||||
"principalType": "ROLE",
|
||||
"principalId": "$authenticated",
|
||||
"permission": "ALLOW",
|
||||
"property": "setPassword",
|
||||
"accessType": "EXECUTE"
|
||||
}
|
||||
],
|
||||
"relations": {
|
||||
|
|
|
@ -5,13 +5,11 @@
|
|||
"lib/server-app.js",
|
||||
"lib/loopback.js",
|
||||
"lib/registry.js",
|
||||
"server/current-context.js",
|
||||
"lib/access-context.js",
|
||||
{ "title": "Base models", "depth": 2 },
|
||||
"lib/model.js",
|
||||
"lib/persisted-model.js",
|
||||
{ "title": "Middleware", "depth": 2 },
|
||||
"server/middleware/context.js",
|
||||
"server/middleware/favicon.js",
|
||||
"server/middleware/rest.js",
|
||||
"server/middleware/static.js",
|
||||
|
@ -24,6 +22,7 @@
|
|||
"common/models/application.js",
|
||||
"common/models/change.js",
|
||||
"common/models/email.js",
|
||||
"common/models/key-value-model.js",
|
||||
"common/models/role.js",
|
||||
"common/models/role-mapping.js",
|
||||
"common/models/scope.js",
|
||||
|
|
|
@ -1,9 +1,16 @@
|
|||
var loopback = require('../../');
|
||||
var client = loopback();
|
||||
var CartItem = require('./models').CartItem;
|
||||
var remote = loopback.createDataSource({
|
||||
// Copyright IBM Corp. 2014,2019. All Rights Reserved.
|
||||
// Node module: loopback
|
||||
// This file is licensed under the MIT License.
|
||||
// License text available at https://opensource.org/licenses/MIT
|
||||
|
||||
'use strict';
|
||||
const g = require('../../lib/globalize');
|
||||
const loopback = require('../../');
|
||||
const client = loopback();
|
||||
const CartItem = require('./models').CartItem;
|
||||
const remote = loopback.createDataSource({
|
||||
connector: loopback.Remote,
|
||||
url: 'http://localhost:3000'
|
||||
url: 'http://localhost:3000',
|
||||
});
|
||||
|
||||
client.model(CartItem);
|
||||
|
@ -11,7 +18,7 @@ CartItem.attachTo(remote);
|
|||
|
||||
// call the remote method
|
||||
CartItem.sum(1, function(err, total) {
|
||||
console.log('result:', err || total);
|
||||
g.log('result:%s', err || total);
|
||||
});
|
||||
|
||||
// call a built in remote method
|
||||
|
|
|
@ -1,16 +1,23 @@
|
|||
var loopback = require('../../');
|
||||
// Copyright IBM Corp. 2014,2019. All Rights Reserved.
|
||||
// Node module: loopback
|
||||
// This file is licensed under the MIT License.
|
||||
// License text available at https://opensource.org/licenses/MIT
|
||||
|
||||
var CartItem = exports.CartItem = loopback.PersistedModel.extend('CartItem', {
|
||||
'use strict';
|
||||
const loopback = require('../../');
|
||||
|
||||
const CartItem = exports.CartItem = loopback.PersistedModel.extend('CartItem', {
|
||||
tax: {type: Number, default: 0.1},
|
||||
price: Number,
|
||||
item: String,
|
||||
qty: {type: Number, default: 0},
|
||||
cartId: Number
|
||||
cartId: Number,
|
||||
});
|
||||
|
||||
CartItem.sum = function(cartId, callback) {
|
||||
this.find({where: {cartId: 1}}, function(err, items) {
|
||||
var total = items
|
||||
this.find({where: {cartId: cartId}}, function(err, items) {
|
||||
if (err) return callback(err);
|
||||
const total = items
|
||||
.map(function(item) {
|
||||
return item.total();
|
||||
})
|
||||
|
@ -20,15 +27,14 @@ CartItem.sum = function(cartId, callback) {
|
|||
|
||||
callback(null, total);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
CartItem.remoteMethod('sum',
|
||||
{
|
||||
accepts: {arg: 'cartId', type: 'number'},
|
||||
returns: {arg: 'total', type: 'number'}
|
||||
}
|
||||
);
|
||||
returns: {arg: 'total', type: 'number'},
|
||||
});
|
||||
|
||||
CartItem.prototype.total = function() {
|
||||
return this.price * this.qty * 1 + this.tax;
|
||||
}
|
||||
return this.price * this.qty * (1 + this.tax);
|
||||
};
|
||||
|
|
|
@ -1,8 +1,14 @@
|
|||
var loopback = require('../../');
|
||||
var server = module.exports = loopback();
|
||||
var CartItem = require('./models').CartItem;
|
||||
var memory = loopback.createDataSource({
|
||||
connector: loopback.Memory
|
||||
// Copyright IBM Corp. 2014,2019. All Rights Reserved.
|
||||
// Node module: loopback
|
||||
// This file is licensed under the MIT License.
|
||||
// License text available at https://opensource.org/licenses/MIT
|
||||
|
||||
'use strict';
|
||||
const loopback = require('../../');
|
||||
const server = module.exports = loopback();
|
||||
const CartItem = require('./models').CartItem;
|
||||
const memory = loopback.createDataSource({
|
||||
connector: loopback.Memory,
|
||||
});
|
||||
|
||||
server.use(loopback.rest());
|
||||
|
@ -14,7 +20,7 @@ CartItem.attachTo(memory);
|
|||
CartItem.create([
|
||||
{item: 'red hat', qty: 6, price: 19.99, cartId: 1},
|
||||
{item: 'green shirt', qty: 1, price: 14.99, cartId: 1},
|
||||
{item: 'orange pants', qty: 58, price: 9.99, cartId: 1}
|
||||
{item: 'orange pants', qty: 58, price: 9.99, cartId: 1},
|
||||
]);
|
||||
|
||||
CartItem.sum(1, function(err, total) {
|
||||
|
|
|
@ -1,15 +1,22 @@
|
|||
var loopback = require('../../');
|
||||
var app = loopback();
|
||||
// Copyright IBM Corp. 2013,2019. All Rights Reserved.
|
||||
// Node module: loopback
|
||||
// This file is licensed under the MIT License.
|
||||
// License text available at https://opensource.org/licenses/MIT
|
||||
|
||||
'use strict';
|
||||
const g = require('../../lib/globalize');
|
||||
const loopback = require('../../');
|
||||
const app = loopback();
|
||||
|
||||
app.use(loopback.rest());
|
||||
|
||||
var schema = {
|
||||
name: String
|
||||
const schema = {
|
||||
name: String,
|
||||
};
|
||||
|
||||
var Color = app.model('color', schema);
|
||||
|
||||
app.dataSource('db', {adapter: 'memory'}).attach(Color);
|
||||
app.dataSource('db', {connector: 'memory'});
|
||||
const Color = app.registry.createModel('color', schema);
|
||||
app.model(Color, {dataSource: 'db'});
|
||||
|
||||
Color.create({name: 'red'});
|
||||
Color.create({name: 'green'});
|
||||
|
@ -17,4 +24,4 @@ Color.create({name: 'blue'});
|
|||
|
||||
app.listen(3000);
|
||||
|
||||
console.log('a list of colors is available at http://localhost:3000/colors');
|
||||
g.log('a list of colors is available at {{http://localhost:3000/colors}}');
|
||||
|
|
|
@ -1,29 +0,0 @@
|
|||
var loopback = require('../../');
|
||||
var app = loopback();
|
||||
|
||||
// Create a LoopBack context for all requests
|
||||
app.use(loopback.context());
|
||||
|
||||
// Store a request property in the context
|
||||
app.use(function saveHostToContext(req, res, next) {
|
||||
var ns = loopback.getCurrentContext();
|
||||
ns.set('host', req.host);
|
||||
next();
|
||||
});
|
||||
|
||||
app.use(loopback.rest());
|
||||
|
||||
var Color = loopback.createModel('color', { 'name': String });
|
||||
Color.beforeRemote('**', function (ctx, unused, next) {
|
||||
// Inside LoopBack code, you can read the property from the context
|
||||
var ns = loopback.getCurrentContext();
|
||||
console.log('Request to host', ns && ns.get('host'));
|
||||
next();
|
||||
});
|
||||
|
||||
app.dataSource('db', { connector: 'memory' });
|
||||
app.model(Color, { dataSource: 'db' });
|
||||
|
||||
app.listen(3000, function() {
|
||||
console.log('A list of colors is available at http://localhost:3000/colors');
|
||||
});
|
|
@ -1,45 +1,52 @@
|
|||
var models = require('../../lib/models');
|
||||
// Copyright IBM Corp. 2013,2019. All Rights Reserved.
|
||||
// Node module: loopback
|
||||
// This file is licensed under the MIT License.
|
||||
// License text available at https://opensource.org/licenses/MIT
|
||||
|
||||
var loopback = require('../../');
|
||||
var app = loopback();
|
||||
'use strict';
|
||||
const g = require('../../lib/globalize');
|
||||
const models = require('../../lib/models');
|
||||
const loopback = require('../../');
|
||||
const app = loopback();
|
||||
|
||||
app.use(loopback.rest());
|
||||
|
||||
var dataSource = loopback.createDataSource('db', {connector: loopback.Memory});
|
||||
const dataSource = loopback.createDataSource('db', {connector: loopback.Memory});
|
||||
|
||||
var Application = models.Application(dataSource);
|
||||
const Application = models.Application(dataSource);
|
||||
|
||||
app.model(Application);
|
||||
|
||||
const data = {
|
||||
pushSettings: [{
|
||||
'platform': 'apns',
|
||||
'apns': {
|
||||
'pushOptions': {
|
||||
'gateway': 'gateway.sandbox.push.apple.com',
|
||||
'cert': 'credentials/apns_cert_dev.pem',
|
||||
'key': 'credentials/apns_key_dev.pem',
|
||||
},
|
||||
|
||||
var data = {pushSettings: [
|
||||
{ "platform": "apns",
|
||||
"apns": {
|
||||
"pushOptions": {
|
||||
"gateway": "gateway.sandbox.push.apple.com",
|
||||
"cert": "credentials/apns_cert_dev.pem",
|
||||
"key": "credentials/apns_key_dev.pem"
|
||||
},
|
||||
|
||||
"feedbackOptions": {
|
||||
"gateway": "feedback.sandbox.push.apple.com",
|
||||
"cert": "credentials/apns_cert_dev.pem",
|
||||
"key": "credentials/apns_key_dev.pem",
|
||||
"batchFeedback": true,
|
||||
"interval": 300
|
||||
}
|
||||
}}
|
||||
]}
|
||||
'feedbackOptions': {
|
||||
'gateway': 'feedback.sandbox.push.apple.com',
|
||||
'cert': 'credentials/apns_cert_dev.pem',
|
||||
'key': 'credentials/apns_key_dev.pem',
|
||||
'batchFeedback': true,
|
||||
'interval': 300,
|
||||
},
|
||||
},
|
||||
}],
|
||||
};
|
||||
|
||||
Application.create(data, function(err, data) {
|
||||
console.log('Created: ', data.toObject());
|
||||
g.log('Created: %s', data.toObject());
|
||||
});
|
||||
|
||||
|
||||
Application.register('rfeng', 'MyApp', {description: 'My first mobile application'}, function (err, result) {
|
||||
Application.register('rfeng', 'MyApp', {description: g.f('My first mobile application')},
|
||||
function(err, result) {
|
||||
console.log(result.toObject());
|
||||
|
||||
result.resetKeys(function (err, result) {
|
||||
console.log(result.toObject());
|
||||
result.resetKeys(function(err, result) {
|
||||
console.log(result.toObject());
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,62 +1,70 @@
|
|||
var loopback = require('../../');
|
||||
var app = loopback();
|
||||
var db = app.dataSource('db', {connector: loopback.Memory});
|
||||
var Color = app.model('color', {dataSource: 'db', options: {trackChanges: true}});
|
||||
var Color2 = app.model('color2', {dataSource: 'db', options: {trackChanges: true}});
|
||||
var target = Color2;
|
||||
var source = Color;
|
||||
var SPEED = process.env.SPEED || 100;
|
||||
var conflicts;
|
||||
// Copyright IBM Corp. 2014,2019. All Rights Reserved.
|
||||
// Node module: loopback
|
||||
// This file is licensed under the MIT License.
|
||||
// License text available at https://opensource.org/licenses/MIT
|
||||
|
||||
var steps = [
|
||||
'use strict';
|
||||
const loopback = require('../../');
|
||||
const app = loopback();
|
||||
const db = app.dataSource('db', {connector: 'memory'});
|
||||
const Color = app.registry.createModel('color', {}, {trackChanges: true});
|
||||
app.model(Color, {dataSource: 'db'});
|
||||
const Color2 = app.registry.createModel('color2', {}, {trackChanges: true});
|
||||
app.model(Color2, {dataSource: 'db'});
|
||||
const target = Color2;
|
||||
const source = Color;
|
||||
const SPEED = process.env.SPEED || 100;
|
||||
let conflicts;
|
||||
|
||||
const steps = [
|
||||
|
||||
createSomeInitialSourceData,
|
||||
|
||||
replicateSourceToTarget,
|
||||
list.bind(this, source, 'current SOURCE data'),
|
||||
list.bind(this, target, 'current TARGET data'),
|
||||
replicateSourceToTarget,
|
||||
list.bind(this, source, 'current SOURCE data'),
|
||||
list.bind(this, target, 'current TARGET data'),
|
||||
|
||||
updateSomeTargetData,
|
||||
|
||||
replicateSourceToTarget,
|
||||
list.bind(this, source, 'current SOURCE data '),
|
||||
list.bind(this, target, 'current TARGET data (includes conflicting update)'),
|
||||
replicateSourceToTarget,
|
||||
list.bind(this, source, 'current SOURCE data '),
|
||||
list.bind(this, target, 'current TARGET data (includes conflicting update)'),
|
||||
|
||||
updateSomeSourceDataCausingAConflict,
|
||||
|
||||
replicateSourceToTarget,
|
||||
list.bind(this, source, 'current SOURCE data (now has a conflict)'),
|
||||
list.bind(this, target, 'current TARGET data (includes conflicting update)'),
|
||||
replicateSourceToTarget,
|
||||
list.bind(this, source, 'current SOURCE data (now has a conflict)'),
|
||||
list.bind(this, target, 'current TARGET data (includes conflicting update)'),
|
||||
|
||||
resolveAllConflicts,
|
||||
|
||||
replicateSourceToTarget,
|
||||
list.bind(this, source, 'current SOURCE data (conflict resolved)'),
|
||||
list.bind(this, target, 'current TARGET data (conflict resolved)'),
|
||||
replicateSourceToTarget,
|
||||
list.bind(this, source, 'current SOURCE data (conflict resolved)'),
|
||||
list.bind(this, target, 'current TARGET data (conflict resolved)'),
|
||||
|
||||
createMoreSourceData,
|
||||
|
||||
replicateSourceToTarget,
|
||||
list.bind(this, source, 'current SOURCE data'),
|
||||
list.bind(this, target, 'current TARGET data'),
|
||||
replicateSourceToTarget,
|
||||
list.bind(this, source, 'current SOURCE data'),
|
||||
list.bind(this, target, 'current TARGET data'),
|
||||
|
||||
createEvenMoreSourceData,
|
||||
|
||||
replicateSourceToTarget,
|
||||
list.bind(this, source, 'current SOURCE data'),
|
||||
list.bind(this, target, 'current TARGET data'),
|
||||
replicateSourceToTarget,
|
||||
list.bind(this, source, 'current SOURCE data'),
|
||||
list.bind(this, target, 'current TARGET data'),
|
||||
|
||||
deleteAllSourceData,
|
||||
|
||||
replicateSourceToTarget,
|
||||
list.bind(this, source, 'current SOURCE data (empty)'),
|
||||
list.bind(this, target, 'current TARGET data (empty)'),
|
||||
replicateSourceToTarget,
|
||||
list.bind(this, source, 'current SOURCE data (empty)'),
|
||||
list.bind(this, target, 'current TARGET data (empty)'),
|
||||
|
||||
createSomeNewSourceData,
|
||||
|
||||
replicateSourceToTarget,
|
||||
list.bind(this, source, 'current SOURCE data'),
|
||||
list.bind(this, target, 'current TARGET data')
|
||||
replicateSourceToTarget,
|
||||
list.bind(this, source, 'current SOURCE data'),
|
||||
list.bind(this, target, 'current TARGET data'),
|
||||
];
|
||||
|
||||
run(steps);
|
||||
|
@ -65,7 +73,7 @@ function createSomeInitialSourceData() {
|
|||
Color.create([
|
||||
{name: 'red'},
|
||||
{name: 'blue'},
|
||||
{name: 'green'}
|
||||
{name: 'green'},
|
||||
]);
|
||||
}
|
||||
|
||||
|
@ -76,7 +84,7 @@ function replicateSourceToTarget() {
|
|||
}
|
||||
|
||||
function resolveAllConflicts() {
|
||||
if(conflicts.length) {
|
||||
if (conflicts.length) {
|
||||
conflicts.forEach(function(conflict) {
|
||||
conflict.resolve();
|
||||
});
|
||||
|
@ -113,7 +121,7 @@ function createSomeNewSourceData() {
|
|||
Color.create([
|
||||
{name: 'violet'},
|
||||
{name: 'amber'},
|
||||
{name: 'olive'}
|
||||
{name: 'olive'},
|
||||
]);
|
||||
}
|
||||
|
||||
|
@ -129,8 +137,8 @@ function list(model, msg) {
|
|||
|
||||
function run(steps) {
|
||||
setInterval(function() {
|
||||
var step = steps.shift();
|
||||
if(step) {
|
||||
const step = steps.shift();
|
||||
if (step) {
|
||||
console.log(step.name);
|
||||
step();
|
||||
}
|
||||
|
|
|
@ -1,23 +1,29 @@
|
|||
var loopback = require('../../');
|
||||
var app = loopback();
|
||||
// Copyright IBM Corp. 2013,2019. All Rights Reserved.
|
||||
// Node module: loopback
|
||||
// This file is licensed under the MIT License.
|
||||
// License text available at https://opensource.org/licenses/MIT
|
||||
|
||||
'use strict';
|
||||
const g = require('../../lib/globalize');
|
||||
const loopback = require('../../');
|
||||
const app = loopback();
|
||||
|
||||
app.use(loopback.rest());
|
||||
|
||||
const dataSource = app.dataSource('db', {adapter: 'memory'});
|
||||
|
||||
var dataSource = app.dataSource('db', {adapter: 'memory'});
|
||||
|
||||
var Color = dataSource.define('color', {
|
||||
'name': String
|
||||
const Color = dataSource.define('color', {
|
||||
'name': String,
|
||||
});
|
||||
|
||||
Color.create({name: 'red'});
|
||||
Color.create({name: 'green'});
|
||||
Color.create({name: 'blue'});
|
||||
|
||||
Color.all(function () {
|
||||
Color.all(function() {
|
||||
console.log(arguments);
|
||||
});
|
||||
|
||||
app.listen(3000);
|
||||
|
||||
console.log('a list of colors is available at http://localhost:3000/colors');
|
||||
g.log('a list of colors is available at {{http://localhost:3000/colors}}');
|
||||
|
|
13
index.js
13
index.js
|
@ -1,9 +1,15 @@
|
|||
// Copyright IBM Corp. 2013,2018. All Rights Reserved.
|
||||
// Node module: loopback
|
||||
// This file is licensed under the MIT License.
|
||||
// License text available at https://opensource.org/licenses/MIT
|
||||
|
||||
'use strict';
|
||||
/**
|
||||
* loopback ~ public api
|
||||
*/
|
||||
|
||||
var loopback = module.exports = require('./lib/loopback');
|
||||
var datasourceJuggler = require('loopback-datasource-juggler');
|
||||
|
||||
const loopback = module.exports = require('./lib/loopback');
|
||||
const datasourceJuggler = require('loopback-datasource-juggler');
|
||||
|
||||
/**
|
||||
* Connectors
|
||||
|
@ -19,4 +25,5 @@ loopback.Remote = require('loopback-connector-remote');
|
|||
*/
|
||||
|
||||
loopback.GeoPoint = require('loopback-datasource-juggler/lib/geo').GeoPoint;
|
||||
loopback.DateString = require('loopback-datasource-juggler/lib/date-string');
|
||||
loopback.ValidationError = loopback.Model.ValidationError;
|
||||
|
|
|
@ -0,0 +1,93 @@
|
|||
{
|
||||
"03f79fa268fe199de2ce4345515431c1": "Nebyl nalezen žádný záznam změny pro {0} s ID {1}",
|
||||
"04bd8af876f001ceaf443aad6a9002f9": "Ověření vyžaduje, aby byl definován model {0}.",
|
||||
"095afbf2f1f0e5be678f5dac5c54e717": "Přístup odepřen",
|
||||
"0caffe1d763c8cca6a61814abe33b776": "Je vyžadován e-mail.",
|
||||
"0da38687fed24275c1547e815914a8e3": "Odstraňte související položku podle ID pro {0}.",
|
||||
"0e21aad369dd09e1965c11949303cefd": "Metadata vzdálené komunikace pro {0}.{1} {{\"isStatic\"}} se neshodují s novým stylem založeném na názvu metody.",
|
||||
"10e01c895dc0b2fecc385f9f462f1ca6": "Seznam barev je dostupný na adrese {{http://localhost:3000/colors}}",
|
||||
"1b2a6076dccbe91a56f1672eb3b8598c": "Tělo odezvy obsahuje vlastnosti {{AccessToken}} vytvořené při přihlášení.\nV závislosti na hodnotě parametru `include` může tělo obsahovat další vlastnosti:\n\n - `user` - `U+007BUserU+007D` - Data aktuálně přihlášeného uživatele. {{(`include=user`)}}\n\n",
|
||||
"1d7833c3ca2f05fdad8fad7537531c40": "\t PŘEDMĚT:{0}",
|
||||
"1e85f822b547a75d7d385048030e4ecb": "VytvořenO: {0}",
|
||||
"22fe62fa8d595b72c62208beddaa2a56": "Aktualizujte související položku podle ID pro {0}.",
|
||||
"275f22ab95671f095640ca99194b7635": "\t OD: {0}",
|
||||
"2860bccdf9ef1e350c1a38932ed12173": "{0} byl odebrán ve verzi 3.0. Další podrobnosti naleznete v části {1}.",
|
||||
"2d3071e3b18681c80a090dc0efbdb349": "Nelze najít {0} s ID {1}",
|
||||
"316e5b82c203cf3de31a449ee07d0650": "Očekávaná logická hodnota, obdrženo: {0}",
|
||||
"320c482401afa1207c04343ab162e803": "Neplatný typ činitele: {0}",
|
||||
"3438fab56cc7ab92dfd88f0497e523e0": "Vlastnost vztahů konfigurace `{0}` musí být objekt",
|
||||
"3591f1d3e115b46f9f195df5ca548a6a": "Model nebyl nalezen: model `{0}` rozšiřuje neznámý model `{1}`.",
|
||||
"35e5252c62d80f8c54a5290d30f4c7d0": "Ověřte svůj e-mail otevřením tohoto odkazu ve webovém prohlížeči:\n\t {0}",
|
||||
"37bcd4b50dfae98734772e39ffb1ea3d": "Zadané heslo je příliš dlouhé. Maximální délka je {0} (zadáno {1})",
|
||||
"3aae63bb7e8e046641767571c1591441": "Přihlášení se nezdařilo, protože e-mail nebyl ověřen",
|
||||
"3aecb24fa8bdd3f79d168761ca8a6729": "Neznámá fáze {{middleware}} {0}",
|
||||
"3ca45aa6f705c46a4c598a900716f086": "{0} používá nastavení modelu {1}, které již není dostupné.",
|
||||
"3caaa84fc103d6d5612173ae6d43b245": "Neplatný token: {0}",
|
||||
"3d617953470be16d0c2b32f0bcfbb5ee": "Děkujeme za registraci",
|
||||
"3d63008ccfb2af1db2142e8cc2716ace": "Varování: Pro odeslání e-mailu nebyl zadán žádný přenos e-mailu. Nastavení přenosu pro odesílání poštovních zpráv.",
|
||||
"4203ab415ec66a78d3164345439ba76e": "Nelze volat {0}.{1}(). Metoda {2} nebyla nastavena. {{PersistedModel}} nebyl správně připojen ke {{DataSource}}!",
|
||||
"42a36bac5cf03c4418d664500c81047a": "Volba {{DataSource}} {{\"defaultForType\"}} se již nepodporuje",
|
||||
"44a6c8b1ded4ed653d19ddeaaf89a606": "E-mail nebyl nalezen",
|
||||
"4a4f04a4e480fc5d4ee73b84d9a4b904": "Odesílání pošty:",
|
||||
"4b494de07f524703ac0879addbd64b13": "E-mail nebyl ověřen",
|
||||
"528325f3cbf1b0ab9a08447515daac9a": "Aktualizujte {0} tohoto modelu.",
|
||||
"543d19bad5e47ee1e9eb8af688e857b4": "Cizí klíč pro {0}.",
|
||||
"57b87ae0e65f6ab7a2e3e6cbdfca49a4": "Nelze vytvořit zdroj dat {0}: {1}",
|
||||
"5858e63efaa0e4ad86b61c0459ea32fa": "Musíte připojit model {{Email}} ke konektoru {{Mail}}",
|
||||
"598ff0255ffd1d1b71e8de55dbe2c034": "Zkontrolujte existenci vztahu {0} k položce podle ID.",
|
||||
"5a36cc6ba0cc27c754f6c5ed6015ea3c": "Odeberte vztah {0} k položce podle ID.",
|
||||
"5e81ad3847a290dc650b47618b9cbc7e": "Přihlášení se nezdařilo",
|
||||
"5fa3afb425819ebde958043e598cb664": "Nelze najít model s {{id}} {0}",
|
||||
"61e5deebaf44d68f4e6a508f30cc31a3": "Vztah `{0}` neexistuje pro model `{1}`",
|
||||
"62e8b0a733417978bab22c8dacf5d7e6": "Nelze použít hromadné aktualizace, konektor správně nehlásí počet aktualizovaných záznamů.",
|
||||
"63a091ced88001ab6acb58f61ec041c5": "\t TEXT:{0}",
|
||||
"651f0b3cbba001635152ec3d3d954d0a": "Najdete související položku podle ID pro {0}.",
|
||||
"6bc376432cd9972cf991aad3de371e78": "Chybí data pro změnu: {0}",
|
||||
"705c2d456a3e204c4af56e671ec3225c": "Nebyl nalezen {{accessToken}}.",
|
||||
"734a7bebb65e10899935126ba63dd51f": "Vlastnost voleb konfigurace `{0}` musí být objekt",
|
||||
"779467f467862836e19f494a37d6ab77": "Vlastnost acls konfigurace `{0}` musí být pole objektů",
|
||||
"7bc7b301ad9c4fc873029d57fb9740fe": "Dotazy {0} z {1}.",
|
||||
"7c837b88fd0e509bd3fc722d7ddf0711": "Cizí klíč pro {0}",
|
||||
"7d5e7ed0efaedf3f55f380caae0df8b8": "Moje první mobilní aplikace",
|
||||
"7e0fca41d098607e1c9aa353c67e0fa1": "Neplatný přístupový token",
|
||||
"7e287fc885d9fdcf42da3a12f38572c1": "Vyžadována autorizace",
|
||||
"7ea04ea91aac3cb7ce0ddd96b7ff1fa4": "{{accessToken}} je vyžadován pro odhlášení",
|
||||
"80a32e80cbed65eba2103201a7c94710": "Model nebyl nalezen: {0}",
|
||||
"830cb6c862f8f364e9064cea0026f701": "Načtení vztahu hasOne {0}.",
|
||||
"83cbdc2560ba9f09155ccfc63e08f1a1": "Vlastnost `{0}` nelze překonfigurovat pro `{1}`",
|
||||
"855eb8db89b4921c42072832d33d2dc2": "Neplatné heslo.",
|
||||
"855ecd4a64885ba272d782435f72a4d4": "Neznámé ID \"{0}\" \"{1}\".",
|
||||
"860d1a0b8bd340411fb32baa72867989": "Přenos nepodporuje přesměrování HTTP.",
|
||||
"86254879d01a60826a851066987703f2": "Přidejte související položku podle ID pro {0}.",
|
||||
"895b1f941d026870b3cc8e6af087c197": "{{username}} nebo {{email}} je povinné",
|
||||
"8ae418c605b6a45f2651be9b1677c180": "Neplatná vzdálená metoda: `{0}`",
|
||||
"8bab6720ecc58ec6412358c858a53484": "Hromadná aktualizace se nezdařila, konektor upravil neočekávaný počet záznamů: {0}",
|
||||
"8ecab7f534de38360bd1b1c88e880123": "Podřízené modely `{0}` nebudou dědit nově definované vzdálené metody {1}.",
|
||||
"93ba9a1d03da3b7696332d3f155c5bb7": "\t HTML:{0}",
|
||||
"97795efe0c3eb7f35ce8cf8cfe70682b": "Konfiguraci `{0}` chybí vlastnost {{`dataSource`}}.\nPoužijte `null` nebo `false` k označení modelů, které nejsou připojeny k žádnému zdroji dat.",
|
||||
"9e3cbc1d5a9347cdcf6d1b4a6dbb55b7": "Načte vztah belongsTo {0}.",
|
||||
"a50d10fc6e0959b220e085454c40381e": "Uživatel nebyl nalezen: {0}",
|
||||
"b6f740aeb6f2eb9bee9cb049dbfe6a28": "Neznámý \"{0}\" {{key}} \"{1}\".",
|
||||
"ba96498b10c179f9cd75f75c8def4f70": "{{realm}} je povinný",
|
||||
"c0057a569ff9d3b509bac61a4b2f605d": "Odstraní všechny {0} tohoto modelu.",
|
||||
"c2b5d51f007178170ca3952d59640ca4": "Nelze odstranit {0} změn: \n {1}",
|
||||
"c4ee6d177c974532c3552d2f98eb72ea": "Middleware {0} byl odebrán ve verzi 3.0. Další podrobnosti naleznete v části {1}.",
|
||||
"c61a5a02ba3801a892308f70f5d55a14": "Metadata vzdálené komunikace {{\"isStatic\"}} jsou zamítnuta. Místo toho zadejte {{\"prototype.name\"}} v názvu metody pro {{isStatic=false}}.",
|
||||
"c68a93f0a9524fed4ff64372fc90c55f": "Je třeba zadat platný e-mail",
|
||||
"cd0412f2f33a4a2a316acc834f3f21a6": "Je třeba zadat {{id}} nebo {{data}}",
|
||||
"d5552322de5605c58b62f47ad26d2716": "{{`app.boot`}} byl odebrán, místo toho použijte nový modul {{loopback-boot}}",
|
||||
"d6f43b266533b04d442bdb3955622592": "Vytvoří novou instanci v {0} tohoto modelu.",
|
||||
"da13d3cdf21330557254670dddd8c5c7": "Vypočte {0} z {1}.",
|
||||
"dc568bee32deb0f6eaf63e73b20e8ceb": "Ignorování neobjektového nastavení \"methods\" \"{0}\".",
|
||||
"e4434de4bb8f5a3cd1d416e4d80d7e0b": "Neznámé \"{0}\" {{id}} \"{1}\".",
|
||||
"e92aa25b6b864e3454b65a7c422bd114": "Hromadná aktualizace se nezdařila, konektor odstranil neočekávaný počet záznamů: {0}",
|
||||
"ea63d226b6968e328bdf6876010786b5": "Nelze použít hromadné aktualizace, konektor správně nehlásí počet odstraněných záznamů.",
|
||||
"ead044e2b4bce74b4357f8a03fb78ec4": "Nelze volat {0}.{1}(). Metoda {2} nebyla nastavena. {{KeyValueModel}} nebyl správně připojen ke {{DataSource}}!",
|
||||
"ecb06666ef95e5db27a5ac1d6a17923b": "\t K:{0}",
|
||||
"f0aed00a3d3d0b97d6594e4b70e0c201": "\t PŘENOS:{0}",
|
||||
"f0bd73df8714cefb925e3b8da2f4c5f6": "výsledek: {0}",
|
||||
"f1d4ac54357cc0932f385d56814ba7e4": "Konflikt",
|
||||
"f66ae3cf379b2fce28575a3282defe1a": "Odstraní {0} tohoto modelu.",
|
||||
"f8e26bcca62a47f579562f1cd2c785ff": "Přepracujte aplikaci tak, aby používala oficiální řešení pro vložení argumentu \"options\" z kontextu požadavku,\nviz {0}"
|
||||
}
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
{
|
||||
"03f79fa268fe199de2ce4345515431c1": "Kein Änderungssatz gefunden für {0} mit ID {1}",
|
||||
"04bd8af876f001ceaf443aad6a9002f9": "Für die Authentifizierung muss Modell {0} definiert sein.",
|
||||
"095afbf2f1f0e5be678f5dac5c54e717": "Zugriff verweigert",
|
||||
"0caffe1d763c8cca6a61814abe33b776": "E-Mail ist erforderlich",
|
||||
"0da38687fed24275c1547e815914a8e3": "Zugehöriges Element nach ID für {0} löschen.",
|
||||
"0e21aad369dd09e1965c11949303cefd": "Die Remote-Anbindungs-Metadaten {{\"isStatic\"}} für {0}.{1} entsprechen nicht dem namensbasierten Stil der neuen Methode.",
|
||||
"10e01c895dc0b2fecc385f9f462f1ca6": "eine Liste mit Farben ist verfügbar unter {{http://localhost:3000/colors}}",
|
||||
"1b2a6076dccbe91a56f1672eb3b8598c": "Der Antworthauptteil enthält Eigenschaften des bei der Anmeldung erstellten {{AccessToken}}.\nAbhängig vom Wert des Parameters 'include' kann der Hauptteil zusätzliche Eigenschaften enthalten:\n\n - user - U+007BUserU+007D - Daten des derzeit angemeldeten Benutzers. {{(`include=user`)}}\n\n",
|
||||
"1d7833c3ca2f05fdad8fad7537531c40": "\t BETREFF:{0}",
|
||||
"1e85f822b547a75d7d385048030e4ecb": "Erstellt: {0}",
|
||||
"22fe62fa8d595b72c62208beddaa2a56": "Zugehöriges Element nach ID für {0} aktualisieren.",
|
||||
"275f22ab95671f095640ca99194b7635": "\t VON:{0}",
|
||||
"2860bccdf9ef1e350c1a38932ed12173": "{0} wurde in Version 3.0 entfernt. Siehe {1} für weitere Details.",
|
||||
"2d3071e3b18681c80a090dc0efbdb349": "{0} mit ID {1} konnte nicht gefunden werden",
|
||||
"316e5b82c203cf3de31a449ee07d0650": "Erwartet wurde boolescher Wert, {0} empfangen",
|
||||
"320c482401afa1207c04343ab162e803": "Ungültiger Prinzipaltyp: {0}",
|
||||
"3438fab56cc7ab92dfd88f0497e523e0": "Die relations-Eigenschaft der Konfiguration '{0}' muss ein Objekt sein",
|
||||
"3591f1d3e115b46f9f195df5ca548a6a": "Modell nicht gefunden: Modell '{0}' bietet das unbekannte Modell '{1}' an.",
|
||||
"35e5252c62d80f8c54a5290d30f4c7d0": "Bestätigen Sie Ihre E-Mail-Adresse, indem Sie diesen Link in einem Web-Browser öffnen:\n\t{0}",
|
||||
"37bcd4b50dfae98734772e39ffb1ea3d": "Das eingegebene Kennwort war zu lang. Maximale Länge: {0} (eingegeben: {1})",
|
||||
"3aae63bb7e8e046641767571c1591441": "Anmeldung fehlgeschlagen, da die E-Mail-Adresse nicht bestätigt wurde",
|
||||
"3aecb24fa8bdd3f79d168761ca8a6729": "Unbekannte {{middleware}}-Phase {0}",
|
||||
"3ca45aa6f705c46a4c598a900716f086": "{0} verwendet Modelleinstellung {1}, die nicht mehr verfügbar ist.",
|
||||
"3caaa84fc103d6d5612173ae6d43b245": "Ungültiges Token: {0}",
|
||||
"3d617953470be16d0c2b32f0bcfbb5ee": "Vielen Dank für die Registrierung",
|
||||
"3d63008ccfb2af1db2142e8cc2716ace": "Warnung: Keine E-Mail-Transportmethode für das Senden von E-Mails angegeben. Richten Sie eine Transportmethode für das Senden von E-Mails ein.",
|
||||
"4203ab415ec66a78d3164345439ba76e": "{0}.{1}() kann nicht aufgerufen werden. Die Methode {2} wurde nicht konfiguriert. {{PersistedModel}} wurde nicht ordnungsgemäß an eine {{DataSource}} angehängt!",
|
||||
"42a36bac5cf03c4418d664500c81047a": "{{DataSource}}-Option {{\"defaultForType\"}} wird nicht mehr unterstützt",
|
||||
"44a6c8b1ded4ed653d19ddeaaf89a606": "E-Mail nicht gefunden",
|
||||
"4a4f04a4e480fc5d4ee73b84d9a4b904": "E-Mail senden:",
|
||||
"4b494de07f524703ac0879addbd64b13": "E-Mail-Adresse wurde nicht bestätigt",
|
||||
"528325f3cbf1b0ab9a08447515daac9a": "{0} von diesem Modell aktualisieren.",
|
||||
"543d19bad5e47ee1e9eb8af688e857b4": "Fremdschlüssel für {0}.",
|
||||
"57b87ae0e65f6ab7a2e3e6cbdfca49a4": "Kann Datenquelle {0} nicht erstellen: {1}",
|
||||
"5858e63efaa0e4ad86b61c0459ea32fa": "Sie müssen das {{Email}}-Modell mit einem {{Mail}}-Konnektor verbinden",
|
||||
"598ff0255ffd1d1b71e8de55dbe2c034": "Vorhandensein von {0}-Beziehung zu einem Element nach ID prüfen.",
|
||||
"5a36cc6ba0cc27c754f6c5ed6015ea3c": "{0}-Beziehung zu einem Element nach ID entfernen.",
|
||||
"5e81ad3847a290dc650b47618b9cbc7e": "Anmeldung fehlgeschlagen",
|
||||
"5fa3afb425819ebde958043e598cb664": "Modell mit {{id}} {0} konnte nicht gefunden werden",
|
||||
"61e5deebaf44d68f4e6a508f30cc31a3": "Beziehung '{0} ist für Modell {1} nicht vorhanden",
|
||||
"62e8b0a733417978bab22c8dacf5d7e6": "Massenaktualisierungen können nicht angewendet werden, der Konnektor meldet die Anzahl aktualisierter Datensätze nicht richtig.",
|
||||
"63a091ced88001ab6acb58f61ec041c5": "\t TEXT:{0}",
|
||||
"651f0b3cbba001635152ec3d3d954d0a": "Zugehöriges Element nach ID für {0} suchen.",
|
||||
"6bc376432cd9972cf991aad3de371e78": "Fehlende Daten für Änderung: {0}",
|
||||
"705c2d456a3e204c4af56e671ec3225c": "{{accessToken}} konnte nicht gefunden werden",
|
||||
"734a7bebb65e10899935126ba63dd51f": "Die options-Eigenschaft der Konfiguration '{0}' muss ein Objekt sein",
|
||||
"779467f467862836e19f494a37d6ab77": "Die acls-Eigenschaft der Konfiguration '{0}' muss eine Reihe von Objekten sein",
|
||||
"7bc7b301ad9c4fc873029d57fb9740fe": "Abfrage von {0} von {1}.",
|
||||
"7c837b88fd0e509bd3fc722d7ddf0711": "Fremdschlüssel für {0}",
|
||||
"7d5e7ed0efaedf3f55f380caae0df8b8": "Meine erste mobile Anwendung",
|
||||
"7e0fca41d098607e1c9aa353c67e0fa1": "Ungültiges Zugriffstoken",
|
||||
"7e287fc885d9fdcf42da3a12f38572c1": "Berechtigung erforderlich",
|
||||
"7ea04ea91aac3cb7ce0ddd96b7ff1fa4": "{{accessToken}} muss sich abmelden",
|
||||
"80a32e80cbed65eba2103201a7c94710": "Modell nicht gefunden: {0}",
|
||||
"830cb6c862f8f364e9064cea0026f701": "Ruft hasOne-Beziehung {0} ab.",
|
||||
"83cbdc2560ba9f09155ccfc63e08f1a1": "Eigenschaft '{0}' kann für {1} nicht rekonfiguriert werden",
|
||||
"855eb8db89b4921c42072832d33d2dc2": "Ungültiges Kennwort.",
|
||||
"855ecd4a64885ba272d782435f72a4d4": "\"{0}\" unbekannt, ID \"{1}\".",
|
||||
"860d1a0b8bd340411fb32baa72867989": "Die Transportmethode unterstützt keine HTTP-Umleitungen.",
|
||||
"86254879d01a60826a851066987703f2": "Zugehöriges Element nach ID für {0} hinzufügen.",
|
||||
"895b1f941d026870b3cc8e6af087c197": "{{username}} oder {{email}} ist erforderlich",
|
||||
"8ae418c605b6a45f2651be9b1677c180": "Ungültige Remote-Methode: '{0}'",
|
||||
"8bab6720ecc58ec6412358c858a53484": "Massenaktualisierung fehlgeschlagen, der Konnektor hat eine unerwartete Anzahl an Datensätzen geändert: {0}",
|
||||
"8ecab7f534de38360bd1b1c88e880123": "Untergeordnete Modelle von `{0}` übernehmen nicht die neu definierten Remote-Methoden {1}.",
|
||||
"93ba9a1d03da3b7696332d3f155c5bb7": "\t HTML:{0}",
|
||||
"97795efe0c3eb7f35ce8cf8cfe70682b": "Der Konfiguration von {0} fehlt die {{`dataSource`}}-Eigenschaft.\nVerwenden Sie 'null' oder 'false', um Modelle zu kennzeichnen, die mit keiner Datenquelle verbunden sind.",
|
||||
"9e3cbc1d5a9347cdcf6d1b4a6dbb55b7": "Ruft belongsTo-Beziehung {0} ab.",
|
||||
"a50d10fc6e0959b220e085454c40381e": "Benutzer nicht gefunden: {0}",
|
||||
"b6f740aeb6f2eb9bee9cb049dbfe6a28": "\"{0}\" unbekannt, {{key}} \"{1}\".",
|
||||
"ba96498b10c179f9cd75f75c8def4f70": "{{realm}} ist erforderlich",
|
||||
"c0057a569ff9d3b509bac61a4b2f605d": "Löscht alle {0} von diesem Modell.",
|
||||
"c2b5d51f007178170ca3952d59640ca4": "{0} Änderungen können nicht behoben werden:\n{1}",
|
||||
"c4ee6d177c974532c3552d2f98eb72ea": "Middleware {0} wurde in Version 3.0 entfernt. Siehe {1} für weitere Details.",
|
||||
"c61a5a02ba3801a892308f70f5d55a14": "Metadaten {{\"isStatic\"}} für Remote-Anbindung sind veraltet. Bitte geben Sie {{\"prototype.name\"}} anstelle von {{isStatic=false}} beim Methodennamen an.",
|
||||
"c68a93f0a9524fed4ff64372fc90c55f": "Eine gültige E-Mail-Adresse muss angegeben werden",
|
||||
"cd0412f2f33a4a2a316acc834f3f21a6": "muss {{id}} oder {{data}} angeben",
|
||||
"d5552322de5605c58b62f47ad26d2716": "{{`app.boot`}} wurde entfernt, verwenden Sie stattdessen das neue Modul {{loopback-boot}}",
|
||||
"d6f43b266533b04d442bdb3955622592": "Erstellt eine neue Instanz in {0} von diesem Modell.",
|
||||
"da13d3cdf21330557254670dddd8c5c7": "Zählt {0} von {1}.",
|
||||
"dc568bee32deb0f6eaf63e73b20e8ceb": "Nicht-Objekt-Einstellung \"{0}\" von \"methods\" wird ignoriert.",
|
||||
"e4434de4bb8f5a3cd1d416e4d80d7e0b": "\"{0}\" unbekannt, {{id}} \"{1}\".",
|
||||
"e92aa25b6b864e3454b65a7c422bd114": "Massenaktualisierung fehlgeschlagen, der Konnektor hat eine unerwartete Anzahl an Datensätzen gelöscht: {0}",
|
||||
"ea63d226b6968e328bdf6876010786b5": "Massenaktualisierungen können nicht angewendet werden, der Konnektor meldet die Anzahl gelöschter Datensätze nicht richtig.",
|
||||
"ead044e2b4bce74b4357f8a03fb78ec4": "{0}.{1}() kann nicht aufgerufen werden. Die Methode {2} wurde nicht konfiguriert. {{KeyValueModel}} wurde nicht ordnungsgemäß an eine {{DataSource}} angehängt!",
|
||||
"ecb06666ef95e5db27a5ac1d6a17923b": "\t AN:{0}",
|
||||
"f0aed00a3d3d0b97d6594e4b70e0c201": "\t TRANSPORTMETHODE:{0}",
|
||||
"f0bd73df8714cefb925e3b8da2f4c5f6": "Ergebnis:{0}",
|
||||
"f1d4ac54357cc0932f385d56814ba7e4": "Konflikt",
|
||||
"f66ae3cf379b2fce28575a3282defe1a": "Löscht {0} von diesem Modell.",
|
||||
"f8e26bcca62a47f579562f1cd2c785ff": "Überarbeiten Sie Ihre App, damit sie die offizielle Lösung für eine Injection des Arguments \"options\" aus dem Anforderungskontext verwendet,\nsiehe {0}"
|
||||
}
|
||||
|
|
@ -0,0 +1,92 @@
|
|||
{
|
||||
"03f79fa268fe199de2ce4345515431c1": "No change record found for {0} with id {1}",
|
||||
"04bd8af876f001ceaf443aad6a9002f9": "Authentication requires model {0} to be defined.",
|
||||
"095afbf2f1f0e5be678f5dac5c54e717": "Access Denied",
|
||||
"0caffe1d763c8cca6a61814abe33b776": "Email is required",
|
||||
"0da38687fed24275c1547e815914a8e3": "Delete a related item by id for {0}.",
|
||||
"0e21aad369dd09e1965c11949303cefd": "Remoting metadata for {0}.{1} {{\"isStatic\"}} does not match new method name-based style.",
|
||||
"10e01c895dc0b2fecc385f9f462f1ca6": "a list of colors is available at {{http://localhost:3000/colors}}",
|
||||
"1b2a6076dccbe91a56f1672eb3b8598c": "The response body contains properties of the {{AccessToken}} created on login.\nDepending on the value of `include` parameter, the body may contain additional properties:\n\n - `user` - `U+007BUserU+007D` - Data of the currently logged in user. {{(`include=user`)}}\n\n",
|
||||
"1d7833c3ca2f05fdad8fad7537531c40": "\t SUBJECT:{0}",
|
||||
"1e85f822b547a75d7d385048030e4ecb": "Created: {0}",
|
||||
"22fe62fa8d595b72c62208beddaa2a56": "Update a related item by id for {0}.",
|
||||
"275f22ab95671f095640ca99194b7635": "\t FROM:{0}",
|
||||
"2860bccdf9ef1e350c1a38932ed12173": "{0} was removed in version 3.0. See {1} for more details.",
|
||||
"2d3071e3b18681c80a090dc0efbdb349": "could not find {0} with id {1}",
|
||||
"316e5b82c203cf3de31a449ee07d0650": "Expected boolean, got {0}",
|
||||
"320c482401afa1207c04343ab162e803": "Invalid principal type: {0}",
|
||||
"3438fab56cc7ab92dfd88f0497e523e0": "The relations property of `{0}` configuration must be an object",
|
||||
"3591f1d3e115b46f9f195df5ca548a6a": "Model not found: model `{0}` is extending an unknown model `{1}`.",
|
||||
"35e5252c62d80f8c54a5290d30f4c7d0": "Please verify your email by opening this link in a web browser:\n\t{0}",
|
||||
"37bcd4b50dfae98734772e39ffb1ea3d": "The password entered was too long. Max length is {0} (entered {1})",
|
||||
"3aae63bb7e8e046641767571c1591441": "login failed as the email has not been verified",
|
||||
"3aecb24fa8bdd3f79d168761ca8a6729": "Unknown {{middleware}} phase {0}",
|
||||
"3ca45aa6f705c46a4c598a900716f086": "{0} is using model setting {1} which is no longer available.",
|
||||
"3caaa84fc103d6d5612173ae6d43b245": "Invalid token: {0}",
|
||||
"3d617953470be16d0c2b32f0bcfbb5ee": "Thanks for Registering",
|
||||
"3d63008ccfb2af1db2142e8cc2716ace": "Warning: No email transport specified for sending email. Setup a transport to send mail messages.",
|
||||
"4203ab415ec66a78d3164345439ba76e": "Cannot call {0}.{1}(). The {2} method has not been setup. The {{PersistedModel}} has not been correctly attached to a {{DataSource}}!",
|
||||
"42a36bac5cf03c4418d664500c81047a": "{{DataSource}} option {{\"defaultForType\"}} is no longer supported",
|
||||
"44a6c8b1ded4ed653d19ddeaaf89a606": "Email not found",
|
||||
"4a4f04a4e480fc5d4ee73b84d9a4b904": "Sending Mail:",
|
||||
"4b494de07f524703ac0879addbd64b13": "Email has not been verified",
|
||||
"528325f3cbf1b0ab9a08447515daac9a": "Update {0} of this model.",
|
||||
"543d19bad5e47ee1e9eb8af688e857b4": "Foreign key for {0}.",
|
||||
"57b87ae0e65f6ab7a2e3e6cbdfca49a4": "Cannot create data source {0}: {1}",
|
||||
"5858e63efaa0e4ad86b61c0459ea32fa": "You must connect the {{Email}} Model to a {{Mail}} connector",
|
||||
"598ff0255ffd1d1b71e8de55dbe2c034": "Check the existence of {0} relation to an item by id.",
|
||||
"5a36cc6ba0cc27c754f6c5ed6015ea3c": "Remove the {0} relation to an item by id.",
|
||||
"5e81ad3847a290dc650b47618b9cbc7e": "login failed",
|
||||
"5fa3afb425819ebde958043e598cb664": "could not find a model with {{id}} {0}",
|
||||
"61e5deebaf44d68f4e6a508f30cc31a3": "Relation `{0}` does not exist for model `{1}`",
|
||||
"62e8b0a733417978bab22c8dacf5d7e6": "Cannot apply bulk updates, the connector does not correctly report the number of updated records.",
|
||||
"63a091ced88001ab6acb58f61ec041c5": "\t TEXT:{0}",
|
||||
"651f0b3cbba001635152ec3d3d954d0a": "Find a related item by id for {0}.",
|
||||
"6bc376432cd9972cf991aad3de371e78": "Missing data for change: {0}",
|
||||
"705c2d456a3e204c4af56e671ec3225c": "Could not find {{accessToken}}",
|
||||
"734a7bebb65e10899935126ba63dd51f": "The options property of `{0}` configuration must be an object",
|
||||
"779467f467862836e19f494a37d6ab77": "The acls property of `{0}` configuration must be an array of objects",
|
||||
"7bc7b301ad9c4fc873029d57fb9740fe": "Queries {0} of {1}.",
|
||||
"7c837b88fd0e509bd3fc722d7ddf0711": "Foreign key for {0}",
|
||||
"7d5e7ed0efaedf3f55f380caae0df8b8": "My first mobile application",
|
||||
"7e0fca41d098607e1c9aa353c67e0fa1": "Invalid Access Token",
|
||||
"7e287fc885d9fdcf42da3a12f38572c1": "Authorization Required",
|
||||
"7ea04ea91aac3cb7ce0ddd96b7ff1fa4": "{{accessToken}} is required to logout",
|
||||
"80a32e80cbed65eba2103201a7c94710": "Model not found: {0}",
|
||||
"830cb6c862f8f364e9064cea0026f701": "Fetches hasOne relation {0}.",
|
||||
"83cbdc2560ba9f09155ccfc63e08f1a1": "Property `{0}` cannot be reconfigured for `{1}`",
|
||||
"855eb8db89b4921c42072832d33d2dc2": "Invalid password.",
|
||||
"855ecd4a64885ba272d782435f72a4d4": "Unknown \"{0}\" id \"{1}\".",
|
||||
"860d1a0b8bd340411fb32baa72867989": "The transport does not support HTTP redirects.",
|
||||
"86254879d01a60826a851066987703f2": "Add a related item by id for {0}.",
|
||||
"895b1f941d026870b3cc8e6af087c197": "{{username}} or {{email}} is required",
|
||||
"8ae418c605b6a45f2651be9b1677c180": "Invalid remote method: `{0}`",
|
||||
"8bab6720ecc58ec6412358c858a53484": "Bulk update failed, the connector has modified unexpected number of records: {0}",
|
||||
"8ecab7f534de38360bd1b1c88e880123": "Child models of `{0}` will not inherit newly defined remote methods {1}.",
|
||||
"93ba9a1d03da3b7696332d3f155c5bb7": "\t HTML:{0}",
|
||||
"97795efe0c3eb7f35ce8cf8cfe70682b": "The configuration of `{0}` is missing {{`dataSource`}} property.\nUse `null` or `false` to mark models not attached to any data source.",
|
||||
"9e3cbc1d5a9347cdcf6d1b4a6dbb55b7": "Fetches belongsTo relation {0}.",
|
||||
"a50d10fc6e0959b220e085454c40381e": "User not found: {0}",
|
||||
"b6f740aeb6f2eb9bee9cb049dbfe6a28": "Unknown \"{0}\" {{key}} \"{1}\".",
|
||||
"ba96498b10c179f9cd75f75c8def4f70": "{{realm}} is required",
|
||||
"c0057a569ff9d3b509bac61a4b2f605d": "Deletes all {0} of this model.",
|
||||
"c2b5d51f007178170ca3952d59640ca4": "Cannot rectify {0} changes:\n{1}",
|
||||
"c4ee6d177c974532c3552d2f98eb72ea": "{0} middleware was removed in version 3.0. See {1} for more details.",
|
||||
"c61a5a02ba3801a892308f70f5d55a14": "Remoting metadata {{\"isStatic\"}} is deprecated. Please specify {{\"prototype.name\"}} in method name instead for {{isStatic=false}}.",
|
||||
"c68a93f0a9524fed4ff64372fc90c55f": "Must provide a valid email",
|
||||
"cd0412f2f33a4a2a316acc834f3f21a6": "must specify an {{id}} or {{data}}",
|
||||
"d5552322de5605c58b62f47ad26d2716": "{{`app.boot`}} was removed, use the new module {{loopback-boot}} instead",
|
||||
"d6f43b266533b04d442bdb3955622592": "Creates a new instance in {0} of this model.",
|
||||
"da13d3cdf21330557254670dddd8c5c7": "Counts {0} of {1}.",
|
||||
"dc568bee32deb0f6eaf63e73b20e8ceb": "Ignoring non-object \"methods\" setting of \"{0}\".",
|
||||
"e4434de4bb8f5a3cd1d416e4d80d7e0b": "Unknown \"{0}\" {{id}} \"{1}\".",
|
||||
"e92aa25b6b864e3454b65a7c422bd114": "Bulk update failed, the connector has deleted unexpected number of records: {0}",
|
||||
"ea63d226b6968e328bdf6876010786b5": "Cannot apply bulk updates, the connector does not correctly report the number of deleted records.",
|
||||
"ead044e2b4bce74b4357f8a03fb78ec4": "Cannot call {0}.{1}(). The {2} method has not been setup. The {{KeyValueModel}} has not been correctly attached to a {{DataSource}}!",
|
||||
"ecb06666ef95e5db27a5ac1d6a17923b": "\t TO:{0}",
|
||||
"f0aed00a3d3d0b97d6594e4b70e0c201": "\t TRANSPORT:{0}",
|
||||
"f0bd73df8714cefb925e3b8da2f4c5f6": "result:{0}",
|
||||
"f1d4ac54357cc0932f385d56814ba7e4": "Conflict",
|
||||
"f66ae3cf379b2fce28575a3282defe1a": "Deletes {0} of this model.",
|
||||
"f8e26bcca62a47f579562f1cd2c785ff": "Please rework your app to use the offical solution for injecting \"options\" argument from request context,\nsee {0}"
|
||||
}
|
|
@ -0,0 +1,93 @@
|
|||
{
|
||||
"03f79fa268fe199de2ce4345515431c1": "No se ha encontrado ningún registro de cambio para {0} con el id {1}",
|
||||
"04bd8af876f001ceaf443aad6a9002f9": "La autenticación requiere la definición del modelo {0}.",
|
||||
"095afbf2f1f0e5be678f5dac5c54e717": "Acceso denegado",
|
||||
"0caffe1d763c8cca6a61814abe33b776": "El correo electrónico es obligatorio",
|
||||
"0da38687fed24275c1547e815914a8e3": "Suprimir un elemento relacionado por id para {0}.",
|
||||
"0e21aad369dd09e1965c11949303cefd": "Los metadatos de interacción remota para {0}.{1} {{\"isStatic\"}} no coinciden con el estilo basado en nombre del nuevo método.",
|
||||
"10e01c895dc0b2fecc385f9f462f1ca6": "una lista de colores está disponible en {{http://localhost:3000/colors}}",
|
||||
"1b2a6076dccbe91a56f1672eb3b8598c": "El cuerpo de respuesta contiene propiedades de la {{AccessToken}} creada durante el inicio de la sesión.\nDependiendo del valor del parámetro `include`, el cuerpo puede contener propiedades adicionales:\n\n - `user` - `U+007BUserU+007D` - Datos del usuario conectado actualmente. {{(`include=user`)}}\n\n",
|
||||
"1d7833c3ca2f05fdad8fad7537531c40": "\t ASUNTO:{0}",
|
||||
"1e85f822b547a75d7d385048030e4ecb": "Creado: {0}",
|
||||
"22fe62fa8d595b72c62208beddaa2a56": "Actualizar un elemento relacionado por id para {0}.",
|
||||
"275f22ab95671f095640ca99194b7635": "\t DESDE:{0}",
|
||||
"2860bccdf9ef1e350c1a38932ed12173": "{0} se ha eliminado en la versión 3.0. Consulte {1} para obtener detalles.",
|
||||
"2d3071e3b18681c80a090dc0efbdb349": "no se ha encontrado {0} con el ID {1}",
|
||||
"316e5b82c203cf3de31a449ee07d0650": "Se esperaba un booleano, se ha obtenido {0}",
|
||||
"320c482401afa1207c04343ab162e803": "Tipo de principal no válido: {0}",
|
||||
"3438fab56cc7ab92dfd88f0497e523e0": "La configuración de la propiedad relations de `{0}` debe ser un objeto",
|
||||
"3591f1d3e115b46f9f195df5ca548a6a": "Modelo no encontrado: el modelo `{0}` está ampliando un modelo desconocido `{1}`.",
|
||||
"35e5252c62d80f8c54a5290d30f4c7d0": "Verifique su correo electrónico abriendo este enlace en un navegador:\n\t {0}",
|
||||
"37bcd4b50dfae98734772e39ffb1ea3d": "La contraseña especificada es demasiado larga. La longitud máxima es {0} (se ha especificado {1})",
|
||||
"3aae63bb7e8e046641767571c1591441": "el inicio de sesión ha fallado porque el correo electrónico no ha sido verificado",
|
||||
"3aecb24fa8bdd3f79d168761ca8a6729": "Fase de {{middleware}} desconocida {0}",
|
||||
"3ca45aa6f705c46a4c598a900716f086": "{0} utiliza el valor de modelo {1}, que ya no está disponible.",
|
||||
"3caaa84fc103d6d5612173ae6d43b245": "La señal no es válida: {0}",
|
||||
"3d617953470be16d0c2b32f0bcfbb5ee": "Gracias por registrarse",
|
||||
"3d63008ccfb2af1db2142e8cc2716ace": "Aviso: No se ha especificado ningún transporte de correo electrónico para enviar correo electrónico. Configure un transporte para enviar mensajes de correo.",
|
||||
"4203ab415ec66a78d3164345439ba76e": "No se puede llamar a {0}.{1}(). El método {2} no se ha configurado. {{PersistedModel}} no se ha conectado correctamente a un {{DataSource}}.",
|
||||
"42a36bac5cf03c4418d664500c81047a": "La opción de {{DataSource}} {{\"defaultForType\"}} ya no está soportada",
|
||||
"44a6c8b1ded4ed653d19ddeaaf89a606": "Correo electrónico no encontrado",
|
||||
"4a4f04a4e480fc5d4ee73b84d9a4b904": "Enviando correo:",
|
||||
"4b494de07f524703ac0879addbd64b13": "No se ha verificado el correo electrónico",
|
||||
"528325f3cbf1b0ab9a08447515daac9a": "Actualizar {0} de este modelo.",
|
||||
"543d19bad5e47ee1e9eb8af688e857b4": "Clave foránea para {0}.",
|
||||
"57b87ae0e65f6ab7a2e3e6cbdfca49a4": "No se puede crear el origen de datos {0}: {1}",
|
||||
"5858e63efaa0e4ad86b61c0459ea32fa": "Debe conectar el modelo de {{Email}} a un conector de {{Mail}}",
|
||||
"598ff0255ffd1d1b71e8de55dbe2c034": "Comprobar la existencia de la relación {0} con un elemento por id.",
|
||||
"5a36cc6ba0cc27c754f6c5ed6015ea3c": "Eliminar la relación {0} con un elemento por id.",
|
||||
"5e81ad3847a290dc650b47618b9cbc7e": "el inicio de sesión ha fallado",
|
||||
"5fa3afb425819ebde958043e598cb664": "no se ha encontrado un modelo con {{id}} {0}",
|
||||
"61e5deebaf44d68f4e6a508f30cc31a3": "La relación `{0}` no existe para el modelo `{1}`",
|
||||
"62e8b0a733417978bab22c8dacf5d7e6": "No pueden aplicarse actualizaciones masivas, el conector no notifica correctamente el número de registros actualizados.",
|
||||
"63a091ced88001ab6acb58f61ec041c5": "\t TEXTO:{0}",
|
||||
"651f0b3cbba001635152ec3d3d954d0a": "Buscar un elemento relacionado por id para {0}.",
|
||||
"6bc376432cd9972cf991aad3de371e78": "Faltan datos para el cambio: {0}",
|
||||
"705c2d456a3e204c4af56e671ec3225c": "No se ha encontrado {{accessToken}}",
|
||||
"734a7bebb65e10899935126ba63dd51f": "La configuración de la propiedad de options de `{0}` debe ser un objeto",
|
||||
"779467f467862836e19f494a37d6ab77": "La configuración de la propiedad acls de `{0}` debe ser una matriz de objetos",
|
||||
"7bc7b301ad9c4fc873029d57fb9740fe": "{0} consultas de {1}.",
|
||||
"7c837b88fd0e509bd3fc722d7ddf0711": "Clave foránea para {0}",
|
||||
"7d5e7ed0efaedf3f55f380caae0df8b8": "Mi primera aplicación móvil",
|
||||
"7e0fca41d098607e1c9aa353c67e0fa1": "Señal de acceso no válida",
|
||||
"7e287fc885d9fdcf42da3a12f38572c1": "Autorización necesaria",
|
||||
"7ea04ea91aac3cb7ce0ddd96b7ff1fa4": "Es necesario {{accessToken}} para cerrar la sesión",
|
||||
"80a32e80cbed65eba2103201a7c94710": "No se ha encontrado el modelo: {0}",
|
||||
"830cb6c862f8f364e9064cea0026f701": "Capta la relación hasOne {0}.",
|
||||
"83cbdc2560ba9f09155ccfc63e08f1a1": "La propiedad `{0}` no puede reconfigurarse para `{1}`",
|
||||
"855eb8db89b4921c42072832d33d2dc2": "Contraseña no válida.",
|
||||
"855ecd4a64885ba272d782435f72a4d4": "Id de \"{0}\" desconocido \"{1}\".",
|
||||
"860d1a0b8bd340411fb32baa72867989": "El transporte no admite redirecciones HTTP.",
|
||||
"86254879d01a60826a851066987703f2": "Añadir un elemento relacionado por id para {0}.",
|
||||
"895b1f941d026870b3cc8e6af087c197": "{{username}} o {{email}} es obligatorio",
|
||||
"8ae418c605b6a45f2651be9b1677c180": "Método remoto no válido: `{0}`",
|
||||
"8bab6720ecc58ec6412358c858a53484": "La actualización masiva ha fallado, el conector ha modificado un número de registros inesperado: {0}",
|
||||
"8ecab7f534de38360bd1b1c88e880123": "Los modelos hijo de `{0}` no heredarán los métodos remotos definidos recientemente {1}.",
|
||||
"93ba9a1d03da3b7696332d3f155c5bb7": "\t HTML:{0}",
|
||||
"97795efe0c3eb7f35ce8cf8cfe70682b": "En la configuración de `{0}` falta la propiedad {{`dataSource`}}.\nUtilice `null` o `false` para marcar los modelos no conectados a ningún origen de datos.",
|
||||
"9e3cbc1d5a9347cdcf6d1b4a6dbb55b7": "Capta la relación belongsTo {0}.",
|
||||
"a50d10fc6e0959b220e085454c40381e": "No se ha encontrado el usuario: {0}",
|
||||
"b6f740aeb6f2eb9bee9cb049dbfe6a28": "{{key}} de \"{0}\" desconocido \"{1}\".",
|
||||
"ba96498b10c179f9cd75f75c8def4f70": "{{realm}} es obligatorio",
|
||||
"c0057a569ff9d3b509bac61a4b2f605d": "Suprime todos los {0} de este modelo.",
|
||||
"c2b5d51f007178170ca3952d59640ca4": "No se pueden rectificar los cambios de {0}:\n{1}",
|
||||
"c4ee6d177c974532c3552d2f98eb72ea": "El middleware {0} se ha eliminado en la versión 3.0. Consulte {1} para obtener detalles.",
|
||||
"c61a5a02ba3801a892308f70f5d55a14": "Los metadatos de interacción remota {{\"isStatic\"}} están en desuso. Especifique {{\"prototype.name\"}} en el nombre de método en lugar de {{isStatic=false}}.",
|
||||
"c68a93f0a9524fed4ff64372fc90c55f": "Debe proporcionar un correo electrónico válido",
|
||||
"cd0412f2f33a4a2a316acc834f3f21a6": "debe especificar un {{id}} o {{data}}",
|
||||
"d5552322de5605c58b62f47ad26d2716": "{{`app.boot`}} se ha eliminado, utilice el nuevo módulo {{loopback-boot}} en su lugar",
|
||||
"d6f43b266533b04d442bdb3955622592": "Crea una nueva instancia en {0} de este modelo.",
|
||||
"da13d3cdf21330557254670dddd8c5c7": "Recuentos {0} de {1}.",
|
||||
"dc568bee32deb0f6eaf63e73b20e8ceb": "Se ignora el valor \"methods\" no de objeto de \"{0}\".",
|
||||
"e4434de4bb8f5a3cd1d416e4d80d7e0b": "{{id}} de \"{0}\" desconocido \"{1}\".",
|
||||
"e92aa25b6b864e3454b65a7c422bd114": "La actualización masiva ha fallado, el conector ha suprimido un número de registros inesperado: {0}",
|
||||
"ea63d226b6968e328bdf6876010786b5": "No pueden aplicarse actualizaciones masivas, el conector no notifica correctamente el número de registros suprimidos.",
|
||||
"ead044e2b4bce74b4357f8a03fb78ec4": "No se puede llamar a {0}.{1}(). El método {2} no se ha configurado. {{KeyValueModel}} no se ha conectado correctamente a un {{DataSource}}.",
|
||||
"ecb06666ef95e5db27a5ac1d6a17923b": "\t A:{0}",
|
||||
"f0aed00a3d3d0b97d6594e4b70e0c201": "\t TRANSPORTE:{0}",
|
||||
"f0bd73df8714cefb925e3b8da2f4c5f6": "resultado:{0}",
|
||||
"f1d4ac54357cc0932f385d56814ba7e4": "Conflicto",
|
||||
"f66ae3cf379b2fce28575a3282defe1a": "Suprime {0} de este modelo.",
|
||||
"f8e26bcca62a47f579562f1cd2c785ff": "Reconfigure su aplicación para que utilice la solución oficial para inyectar el argumento \"options\" del contexto de solicitud, \nconsulte {0}"
|
||||
}
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
{
|
||||
"03f79fa268fe199de2ce4345515431c1": "Aucun enregistrement de changement trouvé pour {0} avec l'id {1}",
|
||||
"04bd8af876f001ceaf443aad6a9002f9": "L'authentification exige que le modèle {0} soit défini.",
|
||||
"095afbf2f1f0e5be678f5dac5c54e717": "Accès refusé",
|
||||
"0caffe1d763c8cca6a61814abe33b776": "L'adresse électronique est obligatoire",
|
||||
"0da38687fed24275c1547e815914a8e3": "Supprimez un élément lié par id pour {0}.",
|
||||
"0e21aad369dd09e1965c11949303cefd": "Les métadonnées remoting pour {0}.{1} {{\"isStatic\"}} ne correspondent pas au style basé sur le nom de la nouvelle méthode.",
|
||||
"10e01c895dc0b2fecc385f9f462f1ca6": "une liste de couleurs est disponible sur {{http://localhost:3000/colors}}",
|
||||
"1b2a6076dccbe91a56f1672eb3b8598c": "Le corps de réponse contient les propriétés de {{AccessToken}} créées lors de la connexion.\nEn fonction de la valeur du paramètre `include`, le corps peut contenir des propriétés supplémentaires :\n\n - `user` - `U+007BUserU+007D` - Données de l'utilisateur connecté. {{(`include=user`)}}\n\n",
|
||||
"1d7833c3ca2f05fdad8fad7537531c40": "\t OBJET :{0}",
|
||||
"1e85f822b547a75d7d385048030e4ecb": "Création de : {0}",
|
||||
"22fe62fa8d595b72c62208beddaa2a56": "Mettez à jour un élément lié par id pour {0}.",
|
||||
"275f22ab95671f095640ca99194b7635": "\t DE :{0}",
|
||||
"2860bccdf9ef1e350c1a38932ed12173": "{0} a été supprimé dans la version 3.0. Pour plus de détails, voir {1}.",
|
||||
"2d3071e3b18681c80a090dc0efbdb349": "impossible de trouver {0} avec l'id {1}",
|
||||
"316e5b82c203cf3de31a449ee07d0650": "Valeur booléenne attendue, {0} obtenu",
|
||||
"320c482401afa1207c04343ab162e803": "Type de principal non valide : {0}",
|
||||
"3438fab56cc7ab92dfd88f0497e523e0": "La propriété relations de la configuration `{0}` doit être un objet",
|
||||
"3591f1d3e115b46f9f195df5ca548a6a": "Modèle introuvable : le modèle `{0}` étend un modèle inconnu `{1}`.",
|
||||
"35e5252c62d80f8c54a5290d30f4c7d0": "Vérifiez votre courrier électronique en ouvrant ce lien dans un navigateur Web :\n\t{0}",
|
||||
"37bcd4b50dfae98734772e39ffb1ea3d": "Le mot de passe saisi était trop long. La longueur maximale est {0} ({1} caractères saisis)",
|
||||
"3aae63bb7e8e046641767571c1591441": "la connexion a échoué car l'adresse électronique n'a pas été vérifiée",
|
||||
"3aecb24fa8bdd3f79d168761ca8a6729": "Phase {{middleware}} inconnue {0}",
|
||||
"3ca45aa6f705c46a4c598a900716f086": "{0} utilise le paramètre de modèle {1} qui n'est plus disponible.",
|
||||
"3caaa84fc103d6d5612173ae6d43b245": "Jeton non valide : {0}",
|
||||
"3d617953470be16d0c2b32f0bcfbb5ee": "Merci pour votre inscription",
|
||||
"3d63008ccfb2af1db2142e8cc2716ace": "Avertissement : Aucun transport de courrier électronique n'est spécifié pour l'envoi d'un message électronique. Configurez un transport pour envoyer des messages électroniques.",
|
||||
"4203ab415ec66a78d3164345439ba76e": "Impossible d'appeler {0}.{1}(). La méthode {2} n'a pas été configurée. {{PersistedModel}} n'a pas été associé correctement à {{DataSource}} !",
|
||||
"42a36bac5cf03c4418d664500c81047a": "L'option {{DataSource}} {{\"defaultForType\"}} n'est plus prise en charge",
|
||||
"44a6c8b1ded4ed653d19ddeaaf89a606": "Adresse électronique introuvable",
|
||||
"4a4f04a4e480fc5d4ee73b84d9a4b904": "Envoi d'un message électronique :",
|
||||
"4b494de07f524703ac0879addbd64b13": "Le courrier électronique n'a pas été vérifié",
|
||||
"528325f3cbf1b0ab9a08447515daac9a": "Mettez à jour {0} de ce modèle.",
|
||||
"543d19bad5e47ee1e9eb8af688e857b4": "Clé externe pour {0}.",
|
||||
"57b87ae0e65f6ab7a2e3e6cbdfca49a4": "Impossible de créer la source de données {0} : {1}",
|
||||
"5858e63efaa0e4ad86b61c0459ea32fa": "Vous devez connecter le modèle {{Email}} à un connecteur {{Mail}}",
|
||||
"598ff0255ffd1d1b71e8de55dbe2c034": "Vérifiez l'existence de la relation {0} à un élément par id.",
|
||||
"5a36cc6ba0cc27c754f6c5ed6015ea3c": "Supprimez la relation {0} à un élément par id.",
|
||||
"5e81ad3847a290dc650b47618b9cbc7e": "échec de la connexion",
|
||||
"5fa3afb425819ebde958043e598cb664": "impossible de trouver un modèle avec {{id}} {0}",
|
||||
"61e5deebaf44d68f4e6a508f30cc31a3": "La relation `{0}` n'existe pas pour le modèle `{1}`",
|
||||
"62e8b0a733417978bab22c8dacf5d7e6": "Impossible d'appliquer des mises à jour en bloc ; le connecteur ne signale pas correctement le nombre d'enregistrements mis à jour.",
|
||||
"63a091ced88001ab6acb58f61ec041c5": "\t TEXTE :{0}",
|
||||
"651f0b3cbba001635152ec3d3d954d0a": "Recherchez un élément lié par id pour {0}.",
|
||||
"6bc376432cd9972cf991aad3de371e78": "Données manquantes pour le changement : {0}",
|
||||
"705c2d456a3e204c4af56e671ec3225c": "{{accessToken}} introuvable",
|
||||
"734a7bebb65e10899935126ba63dd51f": "La propriété options de la configuration `{0}` doit être un objet",
|
||||
"779467f467862836e19f494a37d6ab77": "La propriété acls de la configuration `{0}` doit être un tableau d'objets",
|
||||
"7bc7b301ad9c4fc873029d57fb9740fe": "Demandes {0} de {1}.",
|
||||
"7c837b88fd0e509bd3fc722d7ddf0711": "Clé externe pour {0}",
|
||||
"7d5e7ed0efaedf3f55f380caae0df8b8": "Ma première application mobile",
|
||||
"7e0fca41d098607e1c9aa353c67e0fa1": "Jeton d'accès non valide",
|
||||
"7e287fc885d9fdcf42da3a12f38572c1": "Autorisation requise",
|
||||
"7ea04ea91aac3cb7ce0ddd96b7ff1fa4": "{{accessToken}} est nécessaire pour la déconnexion",
|
||||
"80a32e80cbed65eba2103201a7c94710": "Modèle introuvable : {0}",
|
||||
"830cb6c862f8f364e9064cea0026f701": "Extrait la relation hasOne {0}.",
|
||||
"83cbdc2560ba9f09155ccfc63e08f1a1": "La propriété `{0}` ne peut pas être reconfigurée pour `{1}`",
|
||||
"855eb8db89b4921c42072832d33d2dc2": "Mot de passe non valide.",
|
||||
"855ecd4a64885ba272d782435f72a4d4": "ID \"{0}\" inconnu \"{1}\".",
|
||||
"860d1a0b8bd340411fb32baa72867989": "Le transport ne prend pas en charge les réacheminements HTTP.",
|
||||
"86254879d01a60826a851066987703f2": "Ajoutez un élément lié par id pour {0}.",
|
||||
"895b1f941d026870b3cc8e6af087c197": "{{username}} ou {{email}} est obligatoire",
|
||||
"8ae418c605b6a45f2651be9b1677c180": "Méthode distante non valide : `{0}`",
|
||||
"8bab6720ecc58ec6412358c858a53484": "La mise à jour en bloc a échoué ; le connecteur a modifié un nombre inattendu d'enregistrements : {0}",
|
||||
"8ecab7f534de38360bd1b1c88e880123": "Les modèles enfant de `{0}` n'hériteront pas des méthodes distantes nouvellement définies {1}.",
|
||||
"93ba9a1d03da3b7696332d3f155c5bb7": "\t HTML :{0}",
|
||||
"97795efe0c3eb7f35ce8cf8cfe70682b": "La propriété {{`dataSource`}} est manquante dans la configuration de `{0}`.\nUtilisez `null` ou `false` pour marquer les modèles non associés à une source de données.",
|
||||
"9e3cbc1d5a9347cdcf6d1b4a6dbb55b7": "Extrait la relation belongsTo {0}.",
|
||||
"a50d10fc6e0959b220e085454c40381e": "Utilisateur introuvable : {0}",
|
||||
"b6f740aeb6f2eb9bee9cb049dbfe6a28": "\"{0}\" {{key}} \"{1}\" inconnu.",
|
||||
"ba96498b10c179f9cd75f75c8def4f70": "{{realm}} est obligatoire",
|
||||
"c0057a569ff9d3b509bac61a4b2f605d": "Supprime tous les {0} de ce modèle.",
|
||||
"c2b5d51f007178170ca3952d59640ca4": "Impossible de rectifier les modifications {0} :\n{1}",
|
||||
"c4ee6d177c974532c3552d2f98eb72ea": "Le middleware {0} a été supprimé dans la version 3.0. Pour plus de détails, voir {1}.",
|
||||
"c61a5a02ba3801a892308f70f5d55a14": "Les métadonnées Remoting {{\"isStatic\"}} sont obsolètes. Spécifiez {{\"prototype.name\"}} dans le nom de la méthode plutôt pour {{isStatic=false}}.",
|
||||
"c68a93f0a9524fed4ff64372fc90c55f": "Obligation de fournir une adresse électronique valide",
|
||||
"cd0412f2f33a4a2a316acc834f3f21a6": "obligation de spécifier {{id}} ou {{data}}",
|
||||
"d5552322de5605c58b62f47ad26d2716": "{{`app.boot`}} a été supprimé ; utilisez à la place le nouveau module {{loopback-boot}}",
|
||||
"d6f43b266533b04d442bdb3955622592": "Crée une instance dans {0} de ce modèle.",
|
||||
"da13d3cdf21330557254670dddd8c5c7": "Compte {0} de {1}.",
|
||||
"dc568bee32deb0f6eaf63e73b20e8ceb": "Le paramètre \"methods\" non objet de \"{0}\" est ignoré.",
|
||||
"e4434de4bb8f5a3cd1d416e4d80d7e0b": "\"{0}\" {{id}} \"{1}\" inconnu.",
|
||||
"e92aa25b6b864e3454b65a7c422bd114": "La mise à jour en bloc a échoué ; le connecteur a supprimé un nombre inattendu d'enregistrements : {0}",
|
||||
"ea63d226b6968e328bdf6876010786b5": "Impossible d'appliquer des mises à jour en bloc ; le connecteur ne signale pas correctement le nombre d'enregistrements supprimés.",
|
||||
"ead044e2b4bce74b4357f8a03fb78ec4": "Impossible d'appeler {0}.{1}(). La méthode {2} n'a pas été configurée. {{KeyValueModel}} n'a pas été associé correctement à {{DataSource}} !",
|
||||
"ecb06666ef95e5db27a5ac1d6a17923b": "\t A :{0}",
|
||||
"f0aed00a3d3d0b97d6594e4b70e0c201": "\t TRANSPORT :{0}",
|
||||
"f0bd73df8714cefb925e3b8da2f4c5f6": "résultat :{0}",
|
||||
"f1d4ac54357cc0932f385d56814ba7e4": "Conflit",
|
||||
"f66ae3cf379b2fce28575a3282defe1a": "Supprime {0} de ce modèle.",
|
||||
"f8e26bcca62a47f579562f1cd2c785ff": "Transformez votre application pour utiliser la solution officielle d'injection de l'argument \"options\" à partir du contexte de demande. \nVoir {0}"
|
||||
}
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
{
|
||||
"03f79fa268fe199de2ce4345515431c1": "Nessun record di modifica trovato per {0} con id {1}",
|
||||
"04bd8af876f001ceaf443aad6a9002f9": "L'autenticazione richiede che sia definito il modello {0}.",
|
||||
"095afbf2f1f0e5be678f5dac5c54e717": "Accesso negato",
|
||||
"0caffe1d763c8cca6a61814abe33b776": "L'email è obbligatoria",
|
||||
"0da38687fed24275c1547e815914a8e3": "Eliminare un elemento correlato in base all'ID per {0}.",
|
||||
"0e21aad369dd09e1965c11949303cefd": "L'esecuzione della copia in remoto dei metadata per {0}.{1} {{\"isStatic\"}} non corrisponde al nuovo stile basato sul nome del metodo.",
|
||||
"10e01c895dc0b2fecc385f9f462f1ca6": "un elenco dei colori è disponibile all'indirizzo {{http://localhost:3000/colors}}",
|
||||
"1b2a6076dccbe91a56f1672eb3b8598c": "Il corpo della risposta contiene proprietà del {{AccessToken}} creato all'accesso.\nIn base al valore del parametro `include`, il corpo può contenere ulteriori proprietà:\n\n - `user` - `U+007BUserU+007D` - Dati dell'utente attualmente collegato.. {{(`include=user`)}}\n\n",
|
||||
"1d7833c3ca2f05fdad8fad7537531c40": "\t OGGETTO:{0}",
|
||||
"1e85f822b547a75d7d385048030e4ecb": "Creato: {0}",
|
||||
"22fe62fa8d595b72c62208beddaa2a56": "Aggiornare un elemento correlato in base all'ID per {0}.",
|
||||
"275f22ab95671f095640ca99194b7635": "\t DA:{0}",
|
||||
"2860bccdf9ef1e350c1a38932ed12173": "{0} è stato rimosso nella versione 3.0. Consultare {1} per ulteriori dettagli.",
|
||||
"2d3071e3b18681c80a090dc0efbdb349": "impossibile trovare {0} con id {1}",
|
||||
"316e5b82c203cf3de31a449ee07d0650": "Previsto valore booleano, ricevuto {0}",
|
||||
"320c482401afa1207c04343ab162e803": "Tipo principal non valido: {0}",
|
||||
"3438fab56cc7ab92dfd88f0497e523e0": "La proprietà relations della configurazione `{0}` deve essere un oggetto",
|
||||
"3591f1d3e115b46f9f195df5ca548a6a": "Modello non trovato: il modello `{0}` estende un modello sconosciuto `{1}`.",
|
||||
"35e5252c62d80f8c54a5290d30f4c7d0": "Verificare la e-mail aprendo questo link in un browser web:\n\t{0}",
|
||||
"37bcd4b50dfae98734772e39ffb1ea3d": "La password immessa è troppo lunga. La lunghezza massima è {0} (immessa {1})",
|
||||
"3aae63bb7e8e046641767571c1591441": "login non riuscito perché l'email non è stata verificata",
|
||||
"3aecb24fa8bdd3f79d168761ca8a6729": "Fase {{middleware}} sconosciuta {0}",
|
||||
"3ca45aa6f705c46a4c598a900716f086": "{0} utilizza l'impostazione del modello {1} che non è più disponibile.",
|
||||
"3caaa84fc103d6d5612173ae6d43b245": "Token non valido: {0}",
|
||||
"3d617953470be16d0c2b32f0bcfbb5ee": "Grazie per essersi registrati",
|
||||
"3d63008ccfb2af1db2142e8cc2716ace": "Avvertenza: nessun trasporto email specificato per l'invio della email. Configurare un trasporto per inviare messaggi email.",
|
||||
"4203ab415ec66a78d3164345439ba76e": "Impossibile chiamare {0}.{1}(). Il metodo {2} non è stato configurato. {{PersistedModel}} non è stato correttamente collegato ad una {{DataSource}}!",
|
||||
"42a36bac5cf03c4418d664500c81047a": "L'opzione di {{DataSource}} {{\"defaultForType\"}} non è più supportata",
|
||||
"44a6c8b1ded4ed653d19ddeaaf89a606": "Email non trovata",
|
||||
"4a4f04a4e480fc5d4ee73b84d9a4b904": "Invio email:",
|
||||
"4b494de07f524703ac0879addbd64b13": "La e-mail non è stata verificata",
|
||||
"528325f3cbf1b0ab9a08447515daac9a": "Aggiornare {0} di questo modello.",
|
||||
"543d19bad5e47ee1e9eb8af688e857b4": "Chiave esterna per {0}.",
|
||||
"57b87ae0e65f6ab7a2e3e6cbdfca49a4": "Impossibile creare l'origine dati {0}: {1}",
|
||||
"5858e63efaa0e4ad86b61c0459ea32fa": "È necessario collegare il modello {{Email}} ad un connettore {{Mail}}",
|
||||
"598ff0255ffd1d1b71e8de55dbe2c034": "Verificare l'esistenza della relazione {0} ad un elemento in base all'ID.",
|
||||
"5a36cc6ba0cc27c754f6c5ed6015ea3c": "Rimuovere la relazione {0} ad un elemento in base all'ID.",
|
||||
"5e81ad3847a290dc650b47618b9cbc7e": "login non riuscito",
|
||||
"5fa3afb425819ebde958043e598cb664": "impossibile trovare un modello con {{id}} {0}",
|
||||
"61e5deebaf44d68f4e6a508f30cc31a3": "La relazione `{0}` non esiste per il modello `{1}`",
|
||||
"62e8b0a733417978bab22c8dacf5d7e6": "Impossibile applicare gli aggiornamenti in massa, il connettore non indica correttamente il numero di record aggiornati.",
|
||||
"63a091ced88001ab6acb58f61ec041c5": "\t TESTO:{0}",
|
||||
"651f0b3cbba001635152ec3d3d954d0a": "Trovare un elemento correlato in base all'ID per {0}.",
|
||||
"6bc376432cd9972cf991aad3de371e78": "Dati mancanti per la modifica: {0}",
|
||||
"705c2d456a3e204c4af56e671ec3225c": "Impossibile trovare {{accessToken}}",
|
||||
"734a7bebb65e10899935126ba63dd51f": "La proprietà options della configurazione `{0}` deve essere un oggetto",
|
||||
"779467f467862836e19f494a37d6ab77": "La proprietà acls della configurazione `{0}` deve essere un array di oggetti",
|
||||
"7bc7b301ad9c4fc873029d57fb9740fe": "Query {0} di {1}.",
|
||||
"7c837b88fd0e509bd3fc722d7ddf0711": "Chiave esterna per {0}",
|
||||
"7d5e7ed0efaedf3f55f380caae0df8b8": "Prima applicazione mobile personale",
|
||||
"7e0fca41d098607e1c9aa353c67e0fa1": "Token di accesso non valido",
|
||||
"7e287fc885d9fdcf42da3a12f38572c1": "Autorizzazione richiesta",
|
||||
"7ea04ea91aac3cb7ce0ddd96b7ff1fa4": "{{accessToken}} deve scollegarsi",
|
||||
"80a32e80cbed65eba2103201a7c94710": "Modello non trovato: {0}",
|
||||
"830cb6c862f8f364e9064cea0026f701": "Recupera la relazione hasOne {0}.",
|
||||
"83cbdc2560ba9f09155ccfc63e08f1a1": "Impossibile riconfigurare la proprietà `{0}` per `{1}`",
|
||||
"855eb8db89b4921c42072832d33d2dc2": "Password non valida.",
|
||||
"855ecd4a64885ba272d782435f72a4d4": "ID sconosciuto \"{0}\" \"{1}\".",
|
||||
"860d1a0b8bd340411fb32baa72867989": "Il trasporto non supporta i reindirizzamenti HTTP.",
|
||||
"86254879d01a60826a851066987703f2": "Aggiungere un elemento correlato in base all'ID per {0}.",
|
||||
"895b1f941d026870b3cc8e6af087c197": "Sono richiesti {{username}} o {{email}}",
|
||||
"8ae418c605b6a45f2651be9b1677c180": "Metodo remoto non valido: `{0}`",
|
||||
"8bab6720ecc58ec6412358c858a53484": "Aggiornamento in massa non riuscito, il connettore ha modificato un numero non previsto di record: {0}",
|
||||
"8ecab7f534de38360bd1b1c88e880123": "I modelli child di `{0}` non erediteranno i metodi remoti definiti di recente {1}.",
|
||||
"93ba9a1d03da3b7696332d3f155c5bb7": "\t HTML:{0}",
|
||||
"97795efe0c3eb7f35ce8cf8cfe70682b": "La configurazione di `{0}` non contiene la proprietà {{`dataSource`}}.\nUtilizzare `null` o `false` per contrassegnare i modelli non collegati ad alcuna origine dati.",
|
||||
"9e3cbc1d5a9347cdcf6d1b4a6dbb55b7": "Recupera la relazione belongsTo {0}.",
|
||||
"a50d10fc6e0959b220e085454c40381e": "Utente non trovato: {0}",
|
||||
"b6f740aeb6f2eb9bee9cb049dbfe6a28": "{{key}} \"{0}\" sconosciuto \"{1}\".",
|
||||
"ba96498b10c179f9cd75f75c8def4f70": "{{realm}} è obbligatorio",
|
||||
"c0057a569ff9d3b509bac61a4b2f605d": "Elimina tutti i {0} di questo modello.",
|
||||
"c2b5d51f007178170ca3952d59640ca4": "Impossibile correggere {0} modifiche:\n{1}",
|
||||
"c4ee6d177c974532c3552d2f98eb72ea": "Middleware {0} è stato rimosso nella versione 3.0. Consultare {1} per ulteriori dettagli.",
|
||||
"c61a5a02ba3801a892308f70f5d55a14": "L'impostazione dei metadati {{\"isStatic\"}} in remoto è un'operazione obsoleta. Specificare {{\"prototype.name\"}} nel nome del metodo invece di {{isStatic=false}}.",
|
||||
"c68a93f0a9524fed4ff64372fc90c55f": "È necessario fornire una email valida",
|
||||
"cd0412f2f33a4a2a316acc834f3f21a6": "è necessario specificare {{id}} o {{data}}",
|
||||
"d5552322de5605c58b62f47ad26d2716": "{{`app.boot`}} è stato rimosso, utilizzare il nuovo modulo {{loopback-boot}}",
|
||||
"d6f43b266533b04d442bdb3955622592": "Crea una nuova istanza di questo modello in {0}.",
|
||||
"da13d3cdf21330557254670dddd8c5c7": "{0} di {1}.",
|
||||
"dc568bee32deb0f6eaf63e73b20e8ceb": "L'impostazione \"methods\" non oggetto di \"{0}\" viene ignorata.",
|
||||
"e4434de4bb8f5a3cd1d416e4d80d7e0b": "{{id}} \"{0}\" sconosciuto \"{1}\".",
|
||||
"e92aa25b6b864e3454b65a7c422bd114": "Aggiornamento in massa non riuscito, il connettore ha eliminato un numero non previsto di record: {0}",
|
||||
"ea63d226b6968e328bdf6876010786b5": "Impossibile applicare gli aggiornamenti in massa, il connettore non indica correttamente il numero di record eliminati.",
|
||||
"ead044e2b4bce74b4357f8a03fb78ec4": "Impossibile chiamare {0}.{1}(). Il metodo {2} non è stato configurato. {{KeyValueModel}} non è stato correttamente collegato ad una {{DataSource}}!",
|
||||
"ecb06666ef95e5db27a5ac1d6a17923b": "\t A:{0}",
|
||||
"f0aed00a3d3d0b97d6594e4b70e0c201": "\t TRASPORTO:{0}",
|
||||
"f0bd73df8714cefb925e3b8da2f4c5f6": "risultato:{0}",
|
||||
"f1d4ac54357cc0932f385d56814ba7e4": "Conflitto",
|
||||
"f66ae3cf379b2fce28575a3282defe1a": "Elimina {0} di questo modello.",
|
||||
"f8e26bcca62a47f579562f1cd2c785ff": "Impostare l'app in modo da utilizzare la soluzione ufficiale per inserire l'argomento \"options\" dal contesto della richiesta,\nconsultare {0}"
|
||||
}
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
{
|
||||
"03f79fa268fe199de2ce4345515431c1": "ID {1} の {0} の変更レコードが見つかりませんでした",
|
||||
"04bd8af876f001ceaf443aad6a9002f9": "認証では、モデル {0} を定義する必要があります。",
|
||||
"095afbf2f1f0e5be678f5dac5c54e717": "アクセス拒否",
|
||||
"0caffe1d763c8cca6a61814abe33b776": "E メールは必須です",
|
||||
"0da38687fed24275c1547e815914a8e3": "ID を指定して {0} の関連項目を削除します。",
|
||||
"0e21aad369dd09e1965c11949303cefd": "{0}.{1} のリモート・メタデータ {{\"isStatic\"}} は、新規メソッドの名前ベースの方式と一致しません。",
|
||||
"10e01c895dc0b2fecc385f9f462f1ca6": "カラー・リストは {{http://localhost:3000/colors}} で利用できます",
|
||||
"1b2a6076dccbe91a56f1672eb3b8598c": "応答本文には、ログイン時に作成された {{AccessToken}} のプロパティーが含まれます。\n`include` パラメーターの値によっては、本文に追加のプロパティーが含まれる場合があります:\n\n - `user` - `U+007BUserU+007D` - 現在ログインしているユーザーのデータ。 {{(`include=user`)}}\n\n",
|
||||
"1d7833c3ca2f05fdad8fad7537531c40": "\t 件名:{0}",
|
||||
"1e85f822b547a75d7d385048030e4ecb": "作成済み: {0}",
|
||||
"22fe62fa8d595b72c62208beddaa2a56": "ID を指定して {0} の関連項目を更新します。",
|
||||
"275f22ab95671f095640ca99194b7635": "\t 送信元:{0}",
|
||||
"2860bccdf9ef1e350c1a38932ed12173": "{0} は、バージョン 3.0 で削除されました。 詳しくは、{1} を参照してください。",
|
||||
"2d3071e3b18681c80a090dc0efbdb349": "ID {1} の {0} が見つかりませんでした",
|
||||
"316e5b82c203cf3de31a449ee07d0650": "ブール値が必要ですが、{0} が取得されました",
|
||||
"320c482401afa1207c04343ab162e803": "無効なプリンシパル・タイプ: {0}",
|
||||
"3438fab56cc7ab92dfd88f0497e523e0": "`{0}` 構成の関係プロパティーはオブジェクトでなければなりません",
|
||||
"3591f1d3e115b46f9f195df5ca548a6a": "モデルが見つかりません: モデル `{0}` は不明のモデル `{1}` を拡張しています。",
|
||||
"35e5252c62d80f8c54a5290d30f4c7d0": "Web ブラウザーで次のリンクを開いて、E メールを検証してください: \n\t{0}",
|
||||
"37bcd4b50dfae98734772e39ffb1ea3d": "入力したパスワードが長すぎます。最大長は {0} です ({1} が入力されました)",
|
||||
"3aae63bb7e8e046641767571c1591441": "E メールが検証されていないため、ログインに失敗しました",
|
||||
"3aecb24fa8bdd3f79d168761ca8a6729": "不明な {{middleware}} フェーズ {0}",
|
||||
"3ca45aa6f705c46a4c598a900716f086": "{0} は、使用できなくなったモデル設定 {1} を使用しています。",
|
||||
"3caaa84fc103d6d5612173ae6d43b245": "無効なトークン: {0}",
|
||||
"3d617953470be16d0c2b32f0bcfbb5ee": "ご登録いただき、ありがとうございます。",
|
||||
"3d63008ccfb2af1db2142e8cc2716ace": "警告: E メール送信用の E メール・トランスポートが指定されていません。 メール・メッセージを送信するためのトランスポートをセットアップしてください。",
|
||||
"4203ab415ec66a78d3164345439ba76e": "{0}.{1}() を呼び出せません。 {2} メソッドがセットアップされていません。 {{PersistedModel}} は {{DataSource}} に正しく付加されていません。",
|
||||
"42a36bac5cf03c4418d664500c81047a": "{{DataSource}} オプション {{\"defaultForType\"}} はサポートされなくなりました",
|
||||
"44a6c8b1ded4ed653d19ddeaaf89a606": "E メールが見つかりません",
|
||||
"4a4f04a4e480fc5d4ee73b84d9a4b904": "メールの送信:",
|
||||
"4b494de07f524703ac0879addbd64b13": "E メールが検証されていません",
|
||||
"528325f3cbf1b0ab9a08447515daac9a": "このモデルの {0} を更新します。",
|
||||
"543d19bad5e47ee1e9eb8af688e857b4": "{0} の外部キー。",
|
||||
"57b87ae0e65f6ab7a2e3e6cbdfca49a4": "データ・ソース {0}: {1} を作成できません",
|
||||
"5858e63efaa0e4ad86b61c0459ea32fa": "{{Email}} モデルを {{Mail}} コネクターに接続する必要があります",
|
||||
"598ff0255ffd1d1b71e8de55dbe2c034": "ID を指定して項目との {0} 関係があることを確認します。",
|
||||
"5a36cc6ba0cc27c754f6c5ed6015ea3c": "ID を指定して項目との {0} 関係を削除します。",
|
||||
"5e81ad3847a290dc650b47618b9cbc7e": "ログインに失敗しました",
|
||||
"5fa3afb425819ebde958043e598cb664": "{{id}} {0} のモデルが見つかりませんでした",
|
||||
"61e5deebaf44d68f4e6a508f30cc31a3": "モデル `{1}` には関係 `{0}` が存在しません",
|
||||
"62e8b0a733417978bab22c8dacf5d7e6": "一括更新を適用できません。コネクターは更新されたレコードの数を正しく報告していません。",
|
||||
"63a091ced88001ab6acb58f61ec041c5": "\t テキスト:{0}",
|
||||
"651f0b3cbba001635152ec3d3d954d0a": "ID を指定して {0} の関連項目を検索します。",
|
||||
"6bc376432cd9972cf991aad3de371e78": "変更用のデータがありません: {0}",
|
||||
"705c2d456a3e204c4af56e671ec3225c": "{{accessToken}} が見つかりませんでした",
|
||||
"734a7bebb65e10899935126ba63dd51f": "`{0}` 構成のオプション・プロパティーはオブジェクトでなければなりません",
|
||||
"779467f467862836e19f494a37d6ab77": "`{0}` 構成の ACL プロパティーはオブジェクトの配列でなければなりません",
|
||||
"7bc7b301ad9c4fc873029d57fb9740fe": "{1} の {0} を照会します。",
|
||||
"7c837b88fd0e509bd3fc722d7ddf0711": "{0} の外部キー",
|
||||
"7d5e7ed0efaedf3f55f380caae0df8b8": "最初のモバイル・アプリケーション",
|
||||
"7e0fca41d098607e1c9aa353c67e0fa1": "無効なアクセス・トークン",
|
||||
"7e287fc885d9fdcf42da3a12f38572c1": "許可が必要です",
|
||||
"7ea04ea91aac3cb7ce0ddd96b7ff1fa4": "ログアウトするには {{accessToken}} が必要です",
|
||||
"80a32e80cbed65eba2103201a7c94710": "モデルが見つかりません: {0}",
|
||||
"830cb6c862f8f364e9064cea0026f701": "hasOne 関係 {0} をフェッチします。",
|
||||
"83cbdc2560ba9f09155ccfc63e08f1a1": "`{1}` のプロパティー `{0}` を再構成できません",
|
||||
"855eb8db89b4921c42072832d33d2dc2": "パスワードが無効です。",
|
||||
"855ecd4a64885ba272d782435f72a4d4": "\"{0}\" ID \"{1}\" が不明です。",
|
||||
"860d1a0b8bd340411fb32baa72867989": "トランスポートでは HTTP リダイレクトはサポートされません。",
|
||||
"86254879d01a60826a851066987703f2": "ID を指定して {0} の関連項目を追加します。",
|
||||
"895b1f941d026870b3cc8e6af087c197": "{{username}} または {{email}} が必要です",
|
||||
"8ae418c605b6a45f2651be9b1677c180": "無効なリモート・メソッド: `{0}`",
|
||||
"8bab6720ecc58ec6412358c858a53484": "一括更新が失敗しました。コネクターは予期しない数のレコードを変更しました: {0}",
|
||||
"8ecab7f534de38360bd1b1c88e880123": "`{0}` の子モデルは、新しく定義されたリモート・メソッド {1} を継承しません。",
|
||||
"93ba9a1d03da3b7696332d3f155c5bb7": "\t HTML:{0}",
|
||||
"97795efe0c3eb7f35ce8cf8cfe70682b": "`{0}` の構成は {{`dataSource`}} プロパティーがありません。\nどのデータ・ソースにも付加されていないモデルにマークを付けるには `null` または `false` を使用します。",
|
||||
"9e3cbc1d5a9347cdcf6d1b4a6dbb55b7": "belongsTo 関係 {0} をフェッチします。",
|
||||
"a50d10fc6e0959b220e085454c40381e": "ユーザーが見つかりません: {0}",
|
||||
"b6f740aeb6f2eb9bee9cb049dbfe6a28": "\"{0}\" {{key}} \"{1}\" が不明です。",
|
||||
"ba96498b10c179f9cd75f75c8def4f70": "{{realm}} は必須です",
|
||||
"c0057a569ff9d3b509bac61a4b2f605d": "このモデルのすべての {0} を削除します。",
|
||||
"c2b5d51f007178170ca3952d59640ca4": "{0} の変更を修正できません:\n{1}",
|
||||
"c4ee6d177c974532c3552d2f98eb72ea": "{0} ミドルウェアは、バージョン 3.0 で削除されました。 詳しくは、{1} を参照してください。",
|
||||
"c61a5a02ba3801a892308f70f5d55a14": "リモート・メタデータ {{\"isStatic\"}} は非推奨です。 メソッド名では {{isStatic=false}} の代わりに {{\"prototype.name\"}} を指定してください。",
|
||||
"c68a93f0a9524fed4ff64372fc90c55f": "有効な E メールを指定する必要があります",
|
||||
"cd0412f2f33a4a2a316acc834f3f21a6": "{{id}} または {{data}} を指定する必要があります",
|
||||
"d5552322de5605c58b62f47ad26d2716": "{{`app.boot`}} は削除されました。代わりに新規モジュール {{loopback-boot}} を使用してください",
|
||||
"d6f43b266533b04d442bdb3955622592": "このモデルの {0} に新規インスタンスを作成します。",
|
||||
"da13d3cdf21330557254670dddd8c5c7": "{1} の {0} をカウントします。",
|
||||
"dc568bee32deb0f6eaf63e73b20e8ceb": "\"{0}\" の非オブジェクト「メソッド」設定を無視します。",
|
||||
"e4434de4bb8f5a3cd1d416e4d80d7e0b": "\"{0}\" {{id}} \"{1}\" が不明です。",
|
||||
"e92aa25b6b864e3454b65a7c422bd114": "一括更新が失敗しました。コネクターは予期しない数のレコードを削除しました: {0}",
|
||||
"ea63d226b6968e328bdf6876010786b5": "一括更新を適用できません。コネクターは削除されたレコードの数を正しく報告していません。",
|
||||
"ead044e2b4bce74b4357f8a03fb78ec4": "{0}.{1}() を呼び出せません。 {2} メソッドがセットアップされていません。 {{KeyValueModel}} は {{DataSource}} に正しく付加されていません。",
|
||||
"ecb06666ef95e5db27a5ac1d6a17923b": "\t 宛先:{0}",
|
||||
"f0aed00a3d3d0b97d6594e4b70e0c201": "\t トランスポート:{0}",
|
||||
"f0bd73df8714cefb925e3b8da2f4c5f6": "結果:{0}",
|
||||
"f1d4ac54357cc0932f385d56814ba7e4": "競合",
|
||||
"f66ae3cf379b2fce28575a3282defe1a": "このモデルの {0} を削除します。",
|
||||
"f8e26bcca62a47f579562f1cd2c785ff": "要求コンテキストからの \"options\" 引数を注入するための公式な解決策を使用するようにアプリケーションを作り直してください。\n{0} を参照してください。"
|
||||
}
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
{
|
||||
"03f79fa268fe199de2ce4345515431c1": "ID가 {1}인 {0}에 대한 변경 레코드를 찾을 수 없음",
|
||||
"04bd8af876f001ceaf443aad6a9002f9": "인증을 위해 {0} 모델이 정의되어야 함",
|
||||
"095afbf2f1f0e5be678f5dac5c54e717": "액세스 거부",
|
||||
"0caffe1d763c8cca6a61814abe33b776": "이메일은 필수입니다.",
|
||||
"0da38687fed24275c1547e815914a8e3": "{0}에 대해 ID로 관련 항목을 삭제하십시오.",
|
||||
"0e21aad369dd09e1965c11949303cefd": "{0}.{1} {{\"isStatic\"}}의 원격 메타데이터가 새 메소드 이름 기반 스타일과 일치하지 않습니다.",
|
||||
"10e01c895dc0b2fecc385f9f462f1ca6": "색상 목록은 {{http://localhost:3000/colors}}에 있음",
|
||||
"1b2a6076dccbe91a56f1672eb3b8598c": "응답 본문에 로그인 시 작성한 {{AccessToken}} 특성이 포함됩니다.\n`include` 매개변수 값에 따라 본문에 추가 특성이 포함될 수 있습니다. \n\n - `user` - `U+007BUserU+007D` - 현재 로그인된 사용자의 데이터. {{(`include=user`)}}\n\n",
|
||||
"1d7833c3ca2f05fdad8fad7537531c40": "\t 제목:{0}",
|
||||
"1e85f822b547a75d7d385048030e4ecb": "작성 날짜: {0}",
|
||||
"22fe62fa8d595b72c62208beddaa2a56": "{0}에 대해 ID로 관련 항목을 업데이트하십시오.",
|
||||
"275f22ab95671f095640ca99194b7635": "\t 발신인:{0}",
|
||||
"2860bccdf9ef1e350c1a38932ed12173": "버전 3.0에서 {0}이(가) 제거되었습니다. 자세한 정보는 {1}을(를) 참조하십시오.",
|
||||
"2d3071e3b18681c80a090dc0efbdb349": "ID {1}(으)로 {0}을(를) 찾을 수 없음",
|
||||
"316e5b82c203cf3de31a449ee07d0650": "예상 부울, 실제 {0}",
|
||||
"320c482401afa1207c04343ab162e803": "올바르지 않은 프린시펄 유형: {0}",
|
||||
"3438fab56cc7ab92dfd88f0497e523e0": "`{0}` 구성의 관계 특성은 오브젝트여야 함",
|
||||
"3591f1d3e115b46f9f195df5ca548a6a": "모델을 찾을 수 없음: 모델 `{0}`은(는) 알 수 없는 모델 `{1}`의 확장입니다.",
|
||||
"35e5252c62d80f8c54a5290d30f4c7d0": "웹 브라우저에서 이 링크를 열어 이메일을 확인하십시오.\n\t{0}",
|
||||
"37bcd4b50dfae98734772e39ffb1ea3d": "입력한 비밀번호가 너무 깁니다. 최대 길이는 {0}입니다({1}자 입력함).",
|
||||
"3aae63bb7e8e046641767571c1591441": "이메일이 확인되지 않아서 로그인에 실패했습니다.",
|
||||
"3aecb24fa8bdd3f79d168761ca8a6729": "알 수 없는 {{middleware}} 단계 {0}",
|
||||
"3ca45aa6f705c46a4c598a900716f086": "{0}에서 더 이상 사용할 수 없는 모델 설정 {1}을(를) 사용하고 있습니다.",
|
||||
"3caaa84fc103d6d5612173ae6d43b245": "올바르지 않은 토큰: {0}",
|
||||
"3d617953470be16d0c2b32f0bcfbb5ee": "등록해 주셔서 감사합니다.",
|
||||
"3d63008ccfb2af1db2142e8cc2716ace": "경고: 이메일 발송을 위해 이메일 전송이 지정되지 않았습니다. 메일 메시지를 보내려면 전송을 설정하십시오.",
|
||||
"4203ab415ec66a78d3164345439ba76e": "{0}.{1}()을(를) 호출할 수 없습니다. {2} 메소드가 설정되지 않았습니다. {{PersistedModel}}이(가) {{DataSource}}에 재대로 첨부되지 않았습니다!",
|
||||
"42a36bac5cf03c4418d664500c81047a": "{{DataSource}} 옵션 {{\"defaultForType\"}}이(가) 더 이상 지원되지 않음",
|
||||
"44a6c8b1ded4ed653d19ddeaaf89a606": "이메일을 찾을 수 없음",
|
||||
"4a4f04a4e480fc5d4ee73b84d9a4b904": "메일 발송 중:",
|
||||
"4b494de07f524703ac0879addbd64b13": "이메일이 확인되지 않았습니다.",
|
||||
"528325f3cbf1b0ab9a08447515daac9a": "이 모델의 {0}을(를) 업데이트하십시오.",
|
||||
"543d19bad5e47ee1e9eb8af688e857b4": "{0}의 외부 키입니다.",
|
||||
"57b87ae0e65f6ab7a2e3e6cbdfca49a4": "데이터 소스 {0}을(를) 작성할 수 없음: {1}",
|
||||
"5858e63efaa0e4ad86b61c0459ea32fa": "{{Email}} 모델을 {{Mail}} 커넥터에 연결해야 합니다.",
|
||||
"598ff0255ffd1d1b71e8de55dbe2c034": "ID로 항목에 대한 {0} 관계의 존재를 확인하십시오.",
|
||||
"5a36cc6ba0cc27c754f6c5ed6015ea3c": "ID로 항목에 대한 {0} 관계를 제거하십시오.",
|
||||
"5e81ad3847a290dc650b47618b9cbc7e": "로그인 실패",
|
||||
"5fa3afb425819ebde958043e598cb664": "{{id}} {0}인 모델을 찾을 수 없음",
|
||||
"61e5deebaf44d68f4e6a508f30cc31a3": "모델 `{1}`에 대해 관계 `{0}`이(가) 없습니다.",
|
||||
"62e8b0a733417978bab22c8dacf5d7e6": "벌크 업데이트를 적용할 수 없습니다. 커넥터가 업데이트된 레코드 수를 제대로 보고하지 않습니다.",
|
||||
"63a091ced88001ab6acb58f61ec041c5": "\t 텍스트:{0}",
|
||||
"651f0b3cbba001635152ec3d3d954d0a": "{0}에 대해 ID로 관련 항목을 찾으십시오.",
|
||||
"6bc376432cd9972cf991aad3de371e78": "변경을 위한 데이터 누락: {0}",
|
||||
"705c2d456a3e204c4af56e671ec3225c": "{{accessToken}}을(를) 찾을 수 없음",
|
||||
"734a7bebb65e10899935126ba63dd51f": "`{0}` 구성의 옵션 특성은 오브젝트여야 함",
|
||||
"779467f467862836e19f494a37d6ab77": "`{0}` 구성의 acls 특성은 오브젝트 배열이어야 함",
|
||||
"7bc7b301ad9c4fc873029d57fb9740fe": "{1}의 {0}을(를) 조회합니다.",
|
||||
"7c837b88fd0e509bd3fc722d7ddf0711": "{0}의 외부 키",
|
||||
"7d5e7ed0efaedf3f55f380caae0df8b8": "내 첫 번째 모바일 애플리케이션",
|
||||
"7e0fca41d098607e1c9aa353c67e0fa1": "올바르지 않은 액세스 토큰",
|
||||
"7e287fc885d9fdcf42da3a12f38572c1": "권한 필수",
|
||||
"7ea04ea91aac3cb7ce0ddd96b7ff1fa4": "{{accessToken}}이(가) 로그아웃해야 함",
|
||||
"80a32e80cbed65eba2103201a7c94710": "모델을 찾을 수 없음: {0}",
|
||||
"830cb6c862f8f364e9064cea0026f701": "페치에 하나의 관계 {0}이(가) 있습니다.",
|
||||
"83cbdc2560ba9f09155ccfc63e08f1a1": "`{1}`에 대해 `{0}` 특성을 다시 구성할 수 없음",
|
||||
"855eb8db89b4921c42072832d33d2dc2": "올바르지 않은 비밀번호입니다.",
|
||||
"855ecd4a64885ba272d782435f72a4d4": "알 수 없는 \"{0}\" ID \"{1}\".",
|
||||
"860d1a0b8bd340411fb32baa72867989": "전송에서 HTTP 경로 재지원을 지원하지 않습니다.",
|
||||
"86254879d01a60826a851066987703f2": "{0}에 대해 ID로 관련 항목을 추가하십시오.",
|
||||
"895b1f941d026870b3cc8e6af087c197": "{{username}} 또는 {{email}}은(는) 필수입니다.",
|
||||
"8ae418c605b6a45f2651be9b1677c180": "올바르지 않은 원격 메소드: `{0}`",
|
||||
"8bab6720ecc58ec6412358c858a53484": "벌크 업데이트에 실패했습니다. 커넥터가 예상치 못한 수의 레코드를 수정했습니다. {0}",
|
||||
"8ecab7f534de38360bd1b1c88e880123": "'{0}'의 하위 모델은 새로 정의된 원격 메소드 {1}을(를) 상속하지 않습니다.",
|
||||
"93ba9a1d03da3b7696332d3f155c5bb7": "\t HTML:{0}",
|
||||
"97795efe0c3eb7f35ce8cf8cfe70682b": "`{0}`의 구성에 {{`dataSource`}} 특성이 누락되었습니다.\n데이터 소스에 첨부되지 않은 모델을 표시하려면 `null` 또는 `false`를 사용하십시오.",
|
||||
"9e3cbc1d5a9347cdcf6d1b4a6dbb55b7": "페치가 관계 {0}에 속합니다.",
|
||||
"a50d10fc6e0959b220e085454c40381e": "사용자를 찾을 수 없음: {0}",
|
||||
"b6f740aeb6f2eb9bee9cb049dbfe6a28": "알 수 없는 \"{0}\" {{key}} \"{1}\".",
|
||||
"ba96498b10c179f9cd75f75c8def4f70": "{{realm}}은(는) 필수입니다.",
|
||||
"c0057a569ff9d3b509bac61a4b2f605d": "이 모델의 모든 {0}을(를) 삭제합니다.",
|
||||
"c2b5d51f007178170ca3952d59640ca4": "{0} 변경사항을 교정할 수 없음:\n{1}",
|
||||
"c4ee6d177c974532c3552d2f98eb72ea": "버전 3.0에서 {0} 미들웨어가 제거되었습니다. 자세한 정보는 {1}을(를) 참조하십시오.",
|
||||
"c61a5a02ba3801a892308f70f5d55a14": "원격 메타데이터 {{\"isStatic\"}}이(가) 더 이상 사용되지 않습니다. {{isStatic=false}}인 경우 메소드 이름에 대신 {{\"prototype.name\"}}을(를) 지정하십시오.",
|
||||
"c68a93f0a9524fed4ff64372fc90c55f": "올바른 이메일을 제공해야 함",
|
||||
"cd0412f2f33a4a2a316acc834f3f21a6": "{{id}} 또는 {{data}}을(를) 지정해야 함",
|
||||
"d5552322de5605c58b62f47ad26d2716": "{{`app.boot`}}이(가) 제거되었습니다. 대신 새 모듈 {{loopback-boot}}을(를) 사용하십시오.",
|
||||
"d6f43b266533b04d442bdb3955622592": "이 모델의 {0}에서 새 인스턴스를 작성합니다.",
|
||||
"da13d3cdf21330557254670dddd8c5c7": "{1}의 {0}을(를) 계수합니다.",
|
||||
"dc568bee32deb0f6eaf63e73b20e8ceb": "\"{0}\"의 비오브젝트 \"methods\" 설정 무시",
|
||||
"e4434de4bb8f5a3cd1d416e4d80d7e0b": "알 수 없는 \"{0}\" {{id}} \"{1}\".",
|
||||
"e92aa25b6b864e3454b65a7c422bd114": "벌크 업데이트에 실패했습니다. 커넥터가 예상치 못한 수의 레코드를 삭제했습니다. {0}",
|
||||
"ea63d226b6968e328bdf6876010786b5": "벌크 업데이트를 적용할 수 없습니다. 커넥터가 삭제된 레코드 수를 제대로 보고하지 않습니다.",
|
||||
"ead044e2b4bce74b4357f8a03fb78ec4": "{0}.{1}()을(를) 호출할 수 없습니다. {2} 메소드가 설정되지 않았습니다. {{KeyValueModel}}이(가) {{DataSource}}에 재대로 첨부되지 않았습니다!",
|
||||
"ecb06666ef95e5db27a5ac1d6a17923b": "\t 수신인:{0}",
|
||||
"f0aed00a3d3d0b97d6594e4b70e0c201": "\t 전송:{0}",
|
||||
"f0bd73df8714cefb925e3b8da2f4c5f6": "결과: {0}",
|
||||
"f1d4ac54357cc0932f385d56814ba7e4": "충돌",
|
||||
"f66ae3cf379b2fce28575a3282defe1a": "이 모델의 {0}을(를) 삭제합니다.",
|
||||
"f8e26bcca62a47f579562f1cd2c785ff": "요청 컨텍스트의 \"options\" 인수 삽입에 정식 솔루션을 사용하도록 앱을 다시 작업하십시오.\n{0} 참조"
|
||||
}
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
{
|
||||
"03f79fa268fe199de2ce4345515431c1": "Geen wijzigingsrecord gevonden voor {0} met ID {1}",
|
||||
"04bd8af876f001ceaf443aad6a9002f9": "Voor verificatie moet model {0} worden gedefinieerd.",
|
||||
"095afbf2f1f0e5be678f5dac5c54e717": "Toegang geweigerd",
|
||||
"0caffe1d763c8cca6a61814abe33b776": "E-mail is vereist",
|
||||
"0da38687fed24275c1547e815914a8e3": "Gerelateerd item wissen op basis van ID voor {0}.",
|
||||
"0e21aad369dd09e1965c11949303cefd": "Het niet-lokaal zetten van metagegevens voor {0}.{1} {{\"isStatic\"}} komt niet overeen met nieuwe op naam gebaseerde stijl van de methode.",
|
||||
"10e01c895dc0b2fecc385f9f462f1ca6": "een lijst van kleuren is beschikbaar op {{http://localhost:3000/colors}}",
|
||||
"1b2a6076dccbe91a56f1672eb3b8598c": "De lopende tekst van het antwoord bevat eigenschappen van het {{AccessToken}} dat is gemaakt bij aanmelding.\nAfhankelijk van de waarde van de parameter 'include' kan de lopende tekst aanvullende eigenschappen bevatten:\n\n - 'user' - 'U+007BUserU+007D' - Gegevens van de aangemelde gebruiker. {{(`include=user`)}}\n\n",
|
||||
"1d7833c3ca2f05fdad8fad7537531c40": "\t ONDERWERP: {0}",
|
||||
"1e85f822b547a75d7d385048030e4ecb": "Gemaakt: {0}",
|
||||
"22fe62fa8d595b72c62208beddaa2a56": "Gerelateerd item bijwerken op basis van ID voor {0}.",
|
||||
"275f22ab95671f095640ca99194b7635": "\t VAN: {0}",
|
||||
"2860bccdf9ef1e350c1a38932ed12173": "{0} is versie 3.0 verwijderd. Zie {1} voor meer informatie.",
|
||||
"2d3071e3b18681c80a090dc0efbdb349": "kan {0} met ID {1} niet vinden",
|
||||
"316e5b82c203cf3de31a449ee07d0650": "Booleaanse waarde verwacht, {0} ontvangen",
|
||||
"320c482401afa1207c04343ab162e803": "Ongeldig type principal: {0}",
|
||||
"3438fab56cc7ab92dfd88f0497e523e0": "De relaties-eigenschap van de '{0}'-configuratie moet een object zijn",
|
||||
"3591f1d3e115b46f9f195df5ca548a6a": "Model niet gevonden: model '{0}' is een uitbreiding van onbekend model '{1}'.",
|
||||
"35e5252c62d80f8c54a5290d30f4c7d0": "Controleer uw e-mail door deze link te openen in een webbrowser:\n\t{0}",
|
||||
"37bcd4b50dfae98734772e39ffb1ea3d": "Het opgegeven wachtwoord is te lang. Max lengte is {0} (opgegeven {1})",
|
||||
"3aae63bb7e8e046641767571c1591441": "Aanmelding mislukt omdat e-mail niet is gecontroleerd",
|
||||
"3aecb24fa8bdd3f79d168761ca8a6729": "Onbekende {{middleware}}-fase {0}",
|
||||
"3ca45aa6f705c46a4c598a900716f086": "{0} werkt met modelinstelling {1}, maar deze is niet meer beschikbaar.",
|
||||
"3caaa84fc103d6d5612173ae6d43b245": "Ongeldig token: {0}",
|
||||
"3d617953470be16d0c2b32f0bcfbb5ee": "Hartelijk dank voor uw registratie",
|
||||
"3d63008ccfb2af1db2142e8cc2716ace": "Waarschuwing: Geen e-mailtransport opgegeven voor verzending van e-mail. Configureer een transport om e-mailberichten te verzenden.",
|
||||
"4203ab415ec66a78d3164345439ba76e": "Kan {0}.{1}() niet aanroepen. De methode {2} is niet geconfigureerd. De {{PersistedModel}} is niet correct gekoppeld aan een {{DataSource}}!",
|
||||
"42a36bac5cf03c4418d664500c81047a": "{{DataSource}}-optie {{\"defaultForType\"}} wordt niet meer ondersteund",
|
||||
"44a6c8b1ded4ed653d19ddeaaf89a606": "E-mail is niet gevonden",
|
||||
"4a4f04a4e480fc5d4ee73b84d9a4b904": "Mail verzenden:",
|
||||
"4b494de07f524703ac0879addbd64b13": "E-mail is niet geverifieerd",
|
||||
"528325f3cbf1b0ab9a08447515daac9a": "{0} van dit model bijwerken.",
|
||||
"543d19bad5e47ee1e9eb8af688e857b4": "Externe sleutel voor {0}.",
|
||||
"57b87ae0e65f6ab7a2e3e6cbdfca49a4": "Probleem bij maken van gegevensbron {0}: {1}",
|
||||
"5858e63efaa0e4ad86b61c0459ea32fa": "U moet verbinding maken tussen het model {{Email}} en een {{Mail}}-connector",
|
||||
"598ff0255ffd1d1b71e8de55dbe2c034": "Bestaan van {0}-relatie met item controleren op basis van ID.",
|
||||
"5a36cc6ba0cc27c754f6c5ed6015ea3c": "Verwijder de {0}-relatie met een item op basis van ID.",
|
||||
"5e81ad3847a290dc650b47618b9cbc7e": "Aanmelden is mislukt",
|
||||
"5fa3afb425819ebde958043e598cb664": "geen model gevonden met {{id}} {0}",
|
||||
"61e5deebaf44d68f4e6a508f30cc31a3": "Relatie '{0}' voor model '{1}' bestaat niet",
|
||||
"62e8b0a733417978bab22c8dacf5d7e6": "Bulkupdates kunnen niet worden toegepast, de connector meldt niet het juiste aantal bijgewerkte records.",
|
||||
"63a091ced88001ab6acb58f61ec041c5": "\t TEKST: {0}",
|
||||
"651f0b3cbba001635152ec3d3d954d0a": "Gerelateerd item zoeken op basis van ID voor {0}.",
|
||||
"6bc376432cd9972cf991aad3de371e78": "Ontbrekende gegevens voor wijziging: {0}",
|
||||
"705c2d456a3e204c4af56e671ec3225c": "{{accessToken}} is niet gevonden",
|
||||
"734a7bebb65e10899935126ba63dd51f": "De opties-eigenschap van de '{0}'-configuratie moet een object zijn",
|
||||
"779467f467862836e19f494a37d6ab77": "De acls-eigenschap van de '{0}'-configuratie moet een array objecten zijn",
|
||||
"7bc7b301ad9c4fc873029d57fb9740fe": "Query's {0} van {1}.",
|
||||
"7c837b88fd0e509bd3fc722d7ddf0711": "Externe sleutel voor {0}",
|
||||
"7d5e7ed0efaedf3f55f380caae0df8b8": "Mijn eerste mobiele toepassing",
|
||||
"7e0fca41d098607e1c9aa353c67e0fa1": "Ongeldig toegangstoken",
|
||||
"7e287fc885d9fdcf42da3a12f38572c1": "Verplichte verificatie",
|
||||
"7ea04ea91aac3cb7ce0ddd96b7ff1fa4": "{{accessToken}} is vereist voor afmelding",
|
||||
"80a32e80cbed65eba2103201a7c94710": "Model is niet gevonden: {0}",
|
||||
"830cb6c862f8f364e9064cea0026f701": "Haalt hasOne-relatie {0} op.",
|
||||
"83cbdc2560ba9f09155ccfc63e08f1a1": "Eigenschap '{0}' mag niet opnieuw worden geconfigureerd voor '{1}'",
|
||||
"855eb8db89b4921c42072832d33d2dc2": "Ongeldig wachtwoord.",
|
||||
"855ecd4a64885ba272d782435f72a4d4": "Onbekend \"{0}\"-ID \"{1}\".",
|
||||
"860d1a0b8bd340411fb32baa72867989": "Transport biedt geen ondersteuning voor HTTP-omleidingen.",
|
||||
"86254879d01a60826a851066987703f2": "Gerelateerd item toevoegen op basis van ID voor {0}.",
|
||||
"895b1f941d026870b3cc8e6af087c197": "{{username}} of {{email}} is verplicht",
|
||||
"8ae418c605b6a45f2651be9b1677c180": "Ongeldige niet-lokale methode: '{0}'",
|
||||
"8bab6720ecc58ec6412358c858a53484": "Bulkupdate is mislukt; connector heeft een onverwacht aantal records gewijzigd: {0}",
|
||||
"8ecab7f534de38360bd1b1c88e880123": "Onderliggende modellen van '{0}' nemen de nieuw gedefinieerde niet-lokale methoden {1} niet over.",
|
||||
"93ba9a1d03da3b7696332d3f155c5bb7": "\t HTML: {0}",
|
||||
"97795efe0c3eb7f35ce8cf8cfe70682b": "De eigenschap {{`dataSource`}} ontbreekt in de configuratie van '{0}'.\nGebruik 'null' of 'false' om modellen te markeren die niet gekoppeld zijn aan een gegevensbron.",
|
||||
"9e3cbc1d5a9347cdcf6d1b4a6dbb55b7": "Haalt belongsTo-relatie {0} op.",
|
||||
"a50d10fc6e0959b220e085454c40381e": "Gebruiker is niet gevonden: {0}",
|
||||
"b6f740aeb6f2eb9bee9cb049dbfe6a28": "Onbekend \"{0}\" {{key}} \"{1}\".",
|
||||
"ba96498b10c179f9cd75f75c8def4f70": "{{realm}} is verplicht",
|
||||
"c0057a569ff9d3b509bac61a4b2f605d": "Verwijdert alle {0} van dit model.",
|
||||
"c2b5d51f007178170ca3952d59640ca4": "Wijzigingen van {0} kunnen niet worden hersteld:\n{1}",
|
||||
"c4ee6d177c974532c3552d2f98eb72ea": "{0} middleware is versie 3.0 verwijderd. Zie {1} voor meer informatie.",
|
||||
"c61a5a02ba3801a892308f70f5d55a14": "Extern plaatsen (remoting) van metagegevens {{\"isStatic\"}} is gedeprecieerd. Geef {{\"prototype.name\"}} op in naam van methode in plaats van {{isStatic=false}}.",
|
||||
"c68a93f0a9524fed4ff64372fc90c55f": "U moet een geldig e-mailadres opgeven",
|
||||
"cd0412f2f33a4a2a316acc834f3f21a6": "U moet een {{id}} of {{data}} opgeven",
|
||||
"d5552322de5605c58b62f47ad26d2716": "{{`app.boot`}} is verwijderd; gebruik in plaats daarvan de nieuwe module {{loopback-boot}}",
|
||||
"d6f43b266533b04d442bdb3955622592": "Maakt een nieuwe instance in {0} van dit model.",
|
||||
"da13d3cdf21330557254670dddd8c5c7": "Aantal {0} van {1}.",
|
||||
"dc568bee32deb0f6eaf63e73b20e8ceb": "Niet-object \"methods\"-instelling \"{0}\" wordt genegeerd.",
|
||||
"e4434de4bb8f5a3cd1d416e4d80d7e0b": "Onbekend \"{0}\" {{id}} \"{1}\".",
|
||||
"e92aa25b6b864e3454b65a7c422bd114": "Bulkupdate is mislukt; connector heeft een onverwacht aantal records gewist: {0}",
|
||||
"ea63d226b6968e328bdf6876010786b5": "Bulkupdates kunnen niet worden toegepast, de connector meldt niet het juiste aantal gewiste records.",
|
||||
"ead044e2b4bce74b4357f8a03fb78ec4": "Kan {0}.{1}() niet aanroepen. De methode {2} is niet geconfigureerd. De {{KeyValueModel}} is niet correct gekoppeld aan een {{DataSource}}!",
|
||||
"ecb06666ef95e5db27a5ac1d6a17923b": "\t AAN: {0}",
|
||||
"f0aed00a3d3d0b97d6594e4b70e0c201": "\t TRANSPORT: {0}",
|
||||
"f0bd73df8714cefb925e3b8da2f4c5f6": "resultaat:{0}",
|
||||
"f1d4ac54357cc0932f385d56814ba7e4": "Conflict",
|
||||
"f66ae3cf379b2fce28575a3282defe1a": "Verwijdert {0} van dit model.",
|
||||
"f8e26bcca62a47f579562f1cd2c785ff": "Herzie uw app zodanig dat deze gebruikmaakt van de officiële oplossing voor het injecteren van het argument \"options\" vanuit de aanvraagcontext. \nZie {0}"
|
||||
}
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
{
|
||||
"03f79fa268fe199de2ce4345515431c1": "Nie znaleziono rekordu zmiany dla elementu {0} o identyfikatorze {1}",
|
||||
"04bd8af876f001ceaf443aad6a9002f9": "Uwierzytelnianie wymaga zdefiniowania modelu {0}.",
|
||||
"095afbf2f1f0e5be678f5dac5c54e717": "Odmowa dostępu",
|
||||
"0caffe1d763c8cca6a61814abe33b776": "Adres e-mail jest wymagany",
|
||||
"0da38687fed24275c1547e815914a8e3": "Usuń pokrewny element wg identyfikatora dla {0}.",
|
||||
"0e21aad369dd09e1965c11949303cefd": "Zdalne metadane dla {0}.{1} {{\"isStatic\"}} nie pasują do opartego na nazwie stylu nowej metody.",
|
||||
"10e01c895dc0b2fecc385f9f462f1ca6": "lista kolorów jest dostępna pod adresem {{http://localhost:3000/colors}}",
|
||||
"1b2a6076dccbe91a56f1672eb3b8598c": "Treść odpowiedzi zawiera właściwości elementu {{AccessToken}} utworzonego przy logowaniu.\nW zależności od wartości parametru `include`, treść może zawierać dodatkowe właściwości:\n\n - `user` - `U+007BUserU+007D` — dane aktualnie zalogowanego użytkownika. {{(`include=user`)}}\n\n",
|
||||
"1d7833c3ca2f05fdad8fad7537531c40": "\t TEMAT:{0}",
|
||||
"1e85f822b547a75d7d385048030e4ecb": "Utworzono: {0}",
|
||||
"22fe62fa8d595b72c62208beddaa2a56": "Zaktualizuj pokrewny element wg identyfikatora dla {0}.",
|
||||
"275f22ab95671f095640ca99194b7635": "\t OD:{0}",
|
||||
"2860bccdf9ef1e350c1a38932ed12173": "Element {0} został usunięty w wersji 3.0. Więcej informacji na ten temat zawiera sekcja {1}.",
|
||||
"2d3071e3b18681c80a090dc0efbdb349": "nie można znaleźć elementu {0} o identyfikatorze {1}",
|
||||
"316e5b82c203cf3de31a449ee07d0650": "Oczekiwano wartości boolowskiej, otrzymano {0}",
|
||||
"320c482401afa1207c04343ab162e803": "Niepoprawny typ elementu głównego: {0}",
|
||||
"3438fab56cc7ab92dfd88f0497e523e0": "Właściwość relacji konfiguracji `{0}` musi być obiektem",
|
||||
"3591f1d3e115b46f9f195df5ca548a6a": "Nie znaleziono modelu: model `{0}` rozszerza nieznany model `{1}`.",
|
||||
"35e5252c62d80f8c54a5290d30f4c7d0": "Zweryfikuj swój adres e-mail, otwierając ten odsyłacz w przeglądarce WWW:\n\t{0}",
|
||||
"37bcd4b50dfae98734772e39ffb1ea3d": "Wprowadzone hasło było zbyt długie. Maksymalna liczba znaków: {0} (wprowadzono: {1}).",
|
||||
"3aae63bb7e8e046641767571c1591441": "logowanie nie powiodło się, ponieważ adres e-mail nie został zweryfikowany",
|
||||
"3aecb24fa8bdd3f79d168761ca8a6729": "Nieznana faza {{middleware}} {0}",
|
||||
"3ca45aa6f705c46a4c598a900716f086": "{0} używa ustawienia modelu {1}, które nie jest już dostępne.",
|
||||
"3caaa84fc103d6d5612173ae6d43b245": "Niepoprawny znacznik: {0}",
|
||||
"3d617953470be16d0c2b32f0bcfbb5ee": "Dziękujemy za zarejestrowanie",
|
||||
"3d63008ccfb2af1db2142e8cc2716ace": "Ostrzeżenie: nie określono transportu poczty elektronicznej na potrzeby wysyłania wiadomości e-mail. Skonfiguruj transport w celu wysyłania wiadomości e-mail.",
|
||||
"4203ab415ec66a78d3164345439ba76e": "Nie można wywołać metody {0}.{1}(). Metoda {2} nie została skonfigurowana. Model {{PersistedModel}} nie został poprawnie przyłączony do źródła danych {{DataSource}}!",
|
||||
"42a36bac5cf03c4418d664500c81047a": "Opcja {{\"defaultForType\"}} źródła danych {{DataSource}} nie jest już obsługiwana",
|
||||
"44a6c8b1ded4ed653d19ddeaaf89a606": "Nie znaleziono adresu e-mail",
|
||||
"4a4f04a4e480fc5d4ee73b84d9a4b904": "Wysyłanie poczty:",
|
||||
"4b494de07f524703ac0879addbd64b13": "Adres e-mail nie został zweryfikowany",
|
||||
"528325f3cbf1b0ab9a08447515daac9a": "Zaktualizuj element {0} tego modelu.",
|
||||
"543d19bad5e47ee1e9eb8af688e857b4": "Klucz obcy dla {0}.",
|
||||
"57b87ae0e65f6ab7a2e3e6cbdfca49a4": "Nie można utworzyć źródła danych {0}: {1}",
|
||||
"5858e63efaa0e4ad86b61c0459ea32fa": "Należy połączyć model {{Email}} z konektorem {{Mail}}",
|
||||
"598ff0255ffd1d1b71e8de55dbe2c034": "Sprawdź istnienie relacji {0} z elementem wg identyfikatora.",
|
||||
"5a36cc6ba0cc27c754f6c5ed6015ea3c": "Usuń relację {0} z elementem wg identyfikatora.",
|
||||
"5e81ad3847a290dc650b47618b9cbc7e": "logowanie nie powiodło się",
|
||||
"5fa3afb425819ebde958043e598cb664": "nie można znaleźć modelu o identyfikatorze {{id}} {0}",
|
||||
"61e5deebaf44d68f4e6a508f30cc31a3": "Relacja `{0}` nie istnieje dla modelu `{1}'",
|
||||
"62e8b0a733417978bab22c8dacf5d7e6": "Nie można zastosować aktualizacji masowej, ponieważ konektor nie raportuje poprawnie liczby zaktualizowanych rekordów.",
|
||||
"63a091ced88001ab6acb58f61ec041c5": "\t TEKST:{0}",
|
||||
"651f0b3cbba001635152ec3d3d954d0a": "Znajdź pokrewny element wg identyfikatora dla {0}.",
|
||||
"6bc376432cd9972cf991aad3de371e78": "Brak danych do zmiany: {0}",
|
||||
"705c2d456a3e204c4af56e671ec3225c": "Nie można znaleźć obiektu {{accessToken}}",
|
||||
"734a7bebb65e10899935126ba63dd51f": "Właściwość options konfiguracji `{0}` musi być obiektem",
|
||||
"779467f467862836e19f494a37d6ab77": "Właściwość acls konfiguracji `{0}` musi być tablicą obiektów",
|
||||
"7bc7b301ad9c4fc873029d57fb9740fe": "Odpytuje element {0} w {1}.",
|
||||
"7c837b88fd0e509bd3fc722d7ddf0711": "Klucz obcy dla {0}",
|
||||
"7d5e7ed0efaedf3f55f380caae0df8b8": "Moja pierwsza aplikacja dla urządzeń przenośnych",
|
||||
"7e0fca41d098607e1c9aa353c67e0fa1": "Niepoprawny znacznik dostępu",
|
||||
"7e287fc885d9fdcf42da3a12f38572c1": "Wymagana autoryzacja",
|
||||
"7ea04ea91aac3cb7ce0ddd96b7ff1fa4": "{{accessToken}} jest wymagany do wylogowania się",
|
||||
"80a32e80cbed65eba2103201a7c94710": "Nie znaleziono modelu: {0}",
|
||||
"830cb6c862f8f364e9064cea0026f701": "Pobiera relację hasOne {0}.",
|
||||
"83cbdc2560ba9f09155ccfc63e08f1a1": "Nie można zrekonfigurować właściwości `{0}` dla `{1}`",
|
||||
"855eb8db89b4921c42072832d33d2dc2": "Niepoprawne hasło.",
|
||||
"855ecd4a64885ba272d782435f72a4d4": "Nieznany identyfikator \"{0}\" \"{1}\".",
|
||||
"860d1a0b8bd340411fb32baa72867989": "transport nie obsługuje przekierowań HTTP.",
|
||||
"86254879d01a60826a851066987703f2": "Dodaj pokrewny element wg identyfikatora dla {0}.",
|
||||
"895b1f941d026870b3cc8e6af087c197": "Wymagana jest {{username}} lub {{email}}",
|
||||
"8ae418c605b6a45f2651be9b1677c180": "Niepoprawna metoda zdalna: `{0}`",
|
||||
"8bab6720ecc58ec6412358c858a53484": "Aktualizacja masowa nie powiodła się, konektor zmodyfikował nieoczekiwaną liczbę rekordów: {0}",
|
||||
"8ecab7f534de38360bd1b1c88e880123": "Elementy potomne obiektu `{0}` nie odziedziczą nowo zdefiniowanych metod zdalnych {1}.",
|
||||
"93ba9a1d03da3b7696332d3f155c5bb7": "\t HTML:{0}",
|
||||
"97795efe0c3eb7f35ce8cf8cfe70682b": "W konfiguracji elementu `{0}` brakuje właściwości {{`dataSource`}}.\nUżyj wartości `null`, aby oznaczyć modele, które nie są przyłączone do żadnego źródła danych.",
|
||||
"9e3cbc1d5a9347cdcf6d1b4a6dbb55b7": "Pobiera relację belongsTo {0}.",
|
||||
"a50d10fc6e0959b220e085454c40381e": "Nie znaleziono użytkownika: {0}",
|
||||
"b6f740aeb6f2eb9bee9cb049dbfe6a28": "Nieznany klucz {{key}} \"{0}\" \"{1}\".",
|
||||
"ba96498b10c179f9cd75f75c8def4f70": "Właściwość {{realm}} jest wymagana.",
|
||||
"c0057a569ff9d3b509bac61a4b2f605d": "Usuwa wszystkie {0} tego modelu.",
|
||||
"c2b5d51f007178170ca3952d59640ca4": "Nie można skorygować {0} zmian: \n {1}",
|
||||
"c4ee6d177c974532c3552d2f98eb72ea": "Warstwa pośrednia {0} została usunięta w wersji 3.0. Więcej informacji na ten temat zawiera sekcja {1}.",
|
||||
"c61a5a02ba3801a892308f70f5d55a14": "Metadane zdalnego dostępu {{\"isStatic\"}} są nieaktualne. Określ właściwość {{\"prototype.name\"}} w nazwie metody zamiast właściwości {{isStatic=false}}.",
|
||||
"c68a93f0a9524fed4ff64372fc90c55f": "Należy podać poprawny adres e-mail",
|
||||
"cd0412f2f33a4a2a316acc834f3f21a6": "należy określić {{id}} lub {{data}}",
|
||||
"d5552322de5605c58b62f47ad26d2716": "Moduł {{`app.boot`}} został usunięty; zamiast niego użyj nowego modułu {{loopback-boot}}",
|
||||
"d6f43b266533b04d442bdb3955622592": "Tworzy nową instancję w elemencie {0} tego modelu.",
|
||||
"da13d3cdf21330557254670dddd8c5c7": "Zlicza elementy {0} w {1}.",
|
||||
"dc568bee32deb0f6eaf63e73b20e8ceb": "Ignorowanie niebędącego obiektem ustawienia \"methods\" elementu \"{0}\".",
|
||||
"e4434de4bb8f5a3cd1d416e4d80d7e0b": "Nieznany identyfikator {{id}} \"{0}\" \"{1}\".",
|
||||
"e92aa25b6b864e3454b65a7c422bd114": "Aktualizacja masowa nie powiodła się, konektor usunął nieoczekiwaną liczbę rekordów: {0}",
|
||||
"ea63d226b6968e328bdf6876010786b5": "Nie można zastosować aktualizacji masowej, ponieważ konektor nie raportuje poprawnie liczby usuniętych rekordów.",
|
||||
"ead044e2b4bce74b4357f8a03fb78ec4": "Nie można wywołać metody {0}.{1}(). Metoda {2} nie została skonfigurowana. Model {{KeyValueModel}} nie został poprawnie przyłączony do źródła danych {{DataSource}}!",
|
||||
"ecb06666ef95e5db27a5ac1d6a17923b": "\t DO:{0}",
|
||||
"f0aed00a3d3d0b97d6594e4b70e0c201": "\t TRANSPORT:{0}",
|
||||
"f0bd73df8714cefb925e3b8da2f4c5f6": "wynik:{0}",
|
||||
"f1d4ac54357cc0932f385d56814ba7e4": "Konflikt",
|
||||
"f66ae3cf379b2fce28575a3282defe1a": "Usuwa element {0} tego modelu.",
|
||||
"f8e26bcca62a47f579562f1cd2c785ff": "Zmodyfikuj aplikację w celu użycia oficjalnego rozwiązania do wstrzykiwania argumentu \"options\" z kontekstu żądania,\npatrz sekcja {0}"
|
||||
}
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
{
|
||||
"03f79fa268fe199de2ce4345515431c1": "Nenhum registro de mudança localizado para {0} com o ID {1}",
|
||||
"04bd8af876f001ceaf443aad6a9002f9": "Autenticação requer que modelo {0} seja definido.",
|
||||
"095afbf2f1f0e5be678f5dac5c54e717": "Acesso Negado",
|
||||
"0caffe1d763c8cca6a61814abe33b776": "E-mail é necessário",
|
||||
"0da38687fed24275c1547e815914a8e3": "Excluir um item relacionado por ID para {0}.",
|
||||
"0e21aad369dd09e1965c11949303cefd": "Os metadados remotos para {0}.{1} {{\"isStatic\"}} não correspondem ao novo estilo baseado em nome do método. ",
|
||||
"10e01c895dc0b2fecc385f9f462f1ca6": "uma lista de cores está disponível em {{http://localhost:3000/colors}}",
|
||||
"1b2a6076dccbe91a56f1672eb3b8598c": "O corpo de resposta contém propriedades do {{AccessToken}} criado no login.\nDependendo do valor do parâmetro `include`, o corpo poderá conter propriedades adicionais:\n\n - `user` - `U+007BUserU+007D` - Dados do usuário com login efetuado atualmente. {{(`include=user`)}}\n\n",
|
||||
"1d7833c3ca2f05fdad8fad7537531c40": "\t ASSUNTO:{0}",
|
||||
"1e85f822b547a75d7d385048030e4ecb": "Criado: {0}",
|
||||
"22fe62fa8d595b72c62208beddaa2a56": "Atualizar um item relacionado por ID para {0}.",
|
||||
"275f22ab95671f095640ca99194b7635": "\t DE:{0}",
|
||||
"2860bccdf9ef1e350c1a38932ed12173": "{0} foi removido na versão 3.0. Consulte {1} para obter mais detalhes.",
|
||||
"2d3071e3b18681c80a090dc0efbdb349": "não foi possível localizar {0} com ID {1}",
|
||||
"316e5b82c203cf3de31a449ee07d0650": "Booleano esperado, obteve {0}",
|
||||
"320c482401afa1207c04343ab162e803": "Tipo principal inválido: {0}",
|
||||
"3438fab56cc7ab92dfd88f0497e523e0": "A propriedade de relações da configuração de `{0}` deve ser um objeto",
|
||||
"3591f1d3e115b46f9f195df5ca548a6a": "Modelo não localizado: modelo `{0}` está estendendo um modelo `{1}` desconhecido.",
|
||||
"35e5252c62d80f8c54a5290d30f4c7d0": "Verifique seu e-mail abrindo este link em um navegador da web:\n\t{0}",
|
||||
"37bcd4b50dfae98734772e39ffb1ea3d": "A senha inserida é muito longa. O comprimento máximo é de {0} ({1} inserido)",
|
||||
"3aae63bb7e8e046641767571c1591441": "login com falha pois o e-mail não foi verificado",
|
||||
"3aecb24fa8bdd3f79d168761ca8a6729": "Fase {0} do {{middleware}} desconhecida",
|
||||
"3ca45aa6f705c46a4c598a900716f086": "{0} está usando a configuração do modelo {1} que não está mais disponível.",
|
||||
"3caaa84fc103d6d5612173ae6d43b245": "Token inválido: {0}",
|
||||
"3d617953470be16d0c2b32f0bcfbb5ee": "Obrigado por se Registrar",
|
||||
"3d63008ccfb2af1db2142e8cc2716ace": "Aviso: Nenhum transporte de e-mail especificado para enviar e-mail. Configure um transporte para enviar mensagens de e-mail.",
|
||||
"4203ab415ec66a78d3164345439ba76e": "Não é possível chamar {0}.{1}(). O método {2} não foi configurado. O {{PersistedModel}} não foi conectado corretamente a uma {{DataSource}}!",
|
||||
"42a36bac5cf03c4418d664500c81047a": "Opção {{\"defaultForType\"}} de {{DataSource}} não é mais suportada",
|
||||
"44a6c8b1ded4ed653d19ddeaaf89a606": "E-mail não encontrado",
|
||||
"4a4f04a4e480fc5d4ee73b84d9a4b904": "Enviando E-mail:",
|
||||
"4b494de07f524703ac0879addbd64b13": "E-mail não foi verificado",
|
||||
"528325f3cbf1b0ab9a08447515daac9a": "Atualizar {0} deste modelo.",
|
||||
"543d19bad5e47ee1e9eb8af688e857b4": "Chave estrangeira para {0}.",
|
||||
"57b87ae0e65f6ab7a2e3e6cbdfca49a4": "Não é possível criar origem de dados {0}: {1}",
|
||||
"5858e63efaa0e4ad86b61c0459ea32fa": "Deve-se conectar o Modelo de {{Email}} em um conector de {{Mail}}",
|
||||
"598ff0255ffd1d1b71e8de55dbe2c034": "Verifique a existência da relação de {0} com um item por ID.",
|
||||
"5a36cc6ba0cc27c754f6c5ed6015ea3c": "Remova a relação de {0} com um item por ID.",
|
||||
"5e81ad3847a290dc650b47618b9cbc7e": "falha de login",
|
||||
"5fa3afb425819ebde958043e598cb664": "não foi possível localizar um modelo com {{id}} {0}",
|
||||
"61e5deebaf44d68f4e6a508f30cc31a3": "Relação `{0}` não existe para o modelo `{1}`",
|
||||
"62e8b0a733417978bab22c8dacf5d7e6": "Não é possível aplicar atualizações em massa, o conector não relata o número de registros de atualização corretamente.",
|
||||
"63a091ced88001ab6acb58f61ec041c5": "\t TEXTO:{0}",
|
||||
"651f0b3cbba001635152ec3d3d954d0a": "Localize um item relacionado por ID para {0}.",
|
||||
"6bc376432cd9972cf991aad3de371e78": "Dados ausentes para a mudança: {0}",
|
||||
"705c2d456a3e204c4af56e671ec3225c": "Não foi possível localizar o {{accessToken}}",
|
||||
"734a7bebb65e10899935126ba63dd51f": "A propriedade de opções da configuração de `{0}` deve ser um objeto",
|
||||
"779467f467862836e19f494a37d6ab77": "A propriedade acls da configuração de `{0}` deve ser uma matriz de objetos",
|
||||
"7bc7b301ad9c4fc873029d57fb9740fe": "{0} consultas de {1}.",
|
||||
"7c837b88fd0e509bd3fc722d7ddf0711": "Chave estrangeira para {0}",
|
||||
"7d5e7ed0efaedf3f55f380caae0df8b8": "Meu primeiro aplicativo móvel",
|
||||
"7e0fca41d098607e1c9aa353c67e0fa1": "Token de Acesso Inválido",
|
||||
"7e287fc885d9fdcf42da3a12f38572c1": "Autorização Necessária",
|
||||
"7ea04ea91aac3cb7ce0ddd96b7ff1fa4": "{{accessToken}} é necessário para efetuar logout",
|
||||
"80a32e80cbed65eba2103201a7c94710": "Modelo não localizado: {0}",
|
||||
"830cb6c862f8f364e9064cea0026f701": "Busca relação {0} de hasOne.",
|
||||
"83cbdc2560ba9f09155ccfc63e08f1a1": "A propriedade `{0}` não pode ser reconfigurada para `{1}`",
|
||||
"855eb8db89b4921c42072832d33d2dc2": "Senha inválida.",
|
||||
"855ecd4a64885ba272d782435f72a4d4": "ID \"{1}\" de \"{0}\" desconhecido.",
|
||||
"860d1a0b8bd340411fb32baa72867989": "O transporte não suporta redirecionamentos de HTTP.",
|
||||
"86254879d01a60826a851066987703f2": "Inclua um item relacionado por ID para {0}.",
|
||||
"895b1f941d026870b3cc8e6af087c197": "{{username}} ou {{email}} é necessário",
|
||||
"8ae418c605b6a45f2651be9b1677c180": "Método remoto inválido: `{0}`",
|
||||
"8bab6720ecc58ec6412358c858a53484": "Atualização em massa falhou, o conector modificou um número inesperado de registros: {0}",
|
||||
"8ecab7f534de38360bd1b1c88e880123": "Modelos filhos de `{0}` não herdarão métodos remotos recém-definidos {1}.",
|
||||
"93ba9a1d03da3b7696332d3f155c5bb7": "\t HTML:{0}",
|
||||
"97795efe0c3eb7f35ce8cf8cfe70682b": "A configuração de `{0}` não possui a propriedade {{`dataSource`}}.\nUse `null` ou `false` para marcar modelos não conectados a nenhuma origem de dados.",
|
||||
"9e3cbc1d5a9347cdcf6d1b4a6dbb55b7": "Busca relação {0} de belongsTo.",
|
||||
"a50d10fc6e0959b220e085454c40381e": "Usuário não localizado: {0}",
|
||||
"b6f740aeb6f2eb9bee9cb049dbfe6a28": "\"{0}\" {{key}} \"{1}\" desconhecido.",
|
||||
"ba96498b10c179f9cd75f75c8def4f70": "{{realm}} é obrigatório",
|
||||
"c0057a569ff9d3b509bac61a4b2f605d": "Exclui todos os {0} deste modelo.",
|
||||
"c2b5d51f007178170ca3952d59640ca4": "Não é possível retificar mudanças de {0}:\n{1}",
|
||||
"c4ee6d177c974532c3552d2f98eb72ea": "{0} middleware foi removido na versão 3.0. Consulte {1} para obter mais detalhes.",
|
||||
"c61a5a02ba3801a892308f70f5d55a14": "Metadados {{\"isStatic\"}} remotos estão descontinuados. Especifique {{\"prototype.name\"}} no nome do método em vez de para {{isStatic=false}}.",
|
||||
"c68a93f0a9524fed4ff64372fc90c55f": "Deve-se fornecer um e-mail válido",
|
||||
"cd0412f2f33a4a2a316acc834f3f21a6": "deve-se especificar um {{id}} ou {{data}}",
|
||||
"d5552322de5605c58b62f47ad26d2716": "{{`app.boot`}} foi removido, use o novo módulo {{loopback-boot}} no lugar",
|
||||
"d6f43b266533b04d442bdb3955622592": "Cria uma nova instância no {0} deste modelo.",
|
||||
"da13d3cdf21330557254670dddd8c5c7": "{0} contagens de {1}.",
|
||||
"dc568bee32deb0f6eaf63e73b20e8ceb": "Ignorando configuração de \"methods\" de não objeto de \"{0}\".",
|
||||
"e4434de4bb8f5a3cd1d416e4d80d7e0b": "\"{0}\" {{id}} \"{1}\" desconhecido.",
|
||||
"e92aa25b6b864e3454b65a7c422bd114": "Atualização em massa falhou, o conector excluiu um número inesperado de registros: {0}",
|
||||
"ea63d226b6968e328bdf6876010786b5": "Não é possível aplicar atualizações em massa, o conector não relata o número de registros excluídos corretamente.",
|
||||
"ead044e2b4bce74b4357f8a03fb78ec4": "Não é possível chamar {0}.{1}(). O método {2} não foi configurado. O {{KeyValueModel}} não foi conectado corretamente a uma {{DataSource}}!",
|
||||
"ecb06666ef95e5db27a5ac1d6a17923b": "\t PARA:{0}",
|
||||
"f0aed00a3d3d0b97d6594e4b70e0c201": "\t TRANSPORTE:{0}",
|
||||
"f0bd73df8714cefb925e3b8da2f4c5f6": "resultado:{0}",
|
||||
"f1d4ac54357cc0932f385d56814ba7e4": "Conflito",
|
||||
"f66ae3cf379b2fce28575a3282defe1a": "Exclui {0} deste modelo.",
|
||||
"f8e26bcca62a47f579562f1cd2c785ff": "Retrabalhe seu aplicativo para usar a solução oficial para injetar o argumento \"options\" do contexto da solicitação,\nconsulte {0}"
|
||||
}
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
{
|
||||
"03f79fa268fe199de2ce4345515431c1": "Не удалось найти записи изменений для {0} с ИД {1}",
|
||||
"04bd8af876f001ceaf443aad6a9002f9": "Для идентификации необходимо определить модель {0}.",
|
||||
"095afbf2f1f0e5be678f5dac5c54e717": "Доступ запрещен",
|
||||
"0caffe1d763c8cca6a61814abe33b776": "Необходимо указать адрес электронной почты",
|
||||
"0da38687fed24275c1547e815914a8e3": "Удалить связанный элемент по ИД для {0}.",
|
||||
"0e21aad369dd09e1965c11949303cefd": "Метаданные удаленного соединения для {0}.{1} {{\"isStatic\"}} не соответствуют новому стилю на основе имени метода.",
|
||||
"10e01c895dc0b2fecc385f9f462f1ca6": "Список цветов доступен по адресу {{http://localhost:3000/colors}}",
|
||||
"1b2a6076dccbe91a56f1672eb3b8598c": "Тело ответа содержит свойства маркера {{AccessToken}}, созданного при входе в систему.\nВ зависимости от значения параметра `include`, тело может содержать дополнительные свойства:\n\n - `user` - `U+007BUserU+007D` - данные пользователя, вошедшего в систему. {{(`include=user`)}}\n\n",
|
||||
"1d7833c3ca2f05fdad8fad7537531c40": "\t ТЕМА:{0}",
|
||||
"1e85f822b547a75d7d385048030e4ecb": "Кем создано: {0}",
|
||||
"22fe62fa8d595b72c62208beddaa2a56": "Изменить связанный элемент по ИД для {0}.",
|
||||
"275f22ab95671f095640ca99194b7635": "\t От кого:{0}",
|
||||
"2860bccdf9ef1e350c1a38932ed12173": "{0} удален в версии 3.0. Дополнительные сведения приведены в {1}.",
|
||||
"2d3071e3b18681c80a090dc0efbdb349": "не удалось найти {0} с ИД {1}",
|
||||
"316e5b82c203cf3de31a449ee07d0650": "Ожидался тип boolean, получен тип {0}",
|
||||
"320c482401afa1207c04343ab162e803": "Недопустимый тип субъекта: {0}",
|
||||
"3438fab56cc7ab92dfd88f0497e523e0": "Свойство relations конфигурации `{0}` должно быть объектом",
|
||||
"3591f1d3e115b46f9f195df5ca548a6a": "Модель не найдена: модель `{0}` расширяет неизвестную модель `{1}`.",
|
||||
"35e5252c62d80f8c54a5290d30f4c7d0": "Проверьте адрес электронной почты. Для этого откройте ссылку в веб-браузере:\n\t{0}",
|
||||
"37bcd4b50dfae98734772e39ffb1ea3d": "Введен слишком длинный пароль. Максимальная длина составляет {0} символов (введено {1} символов)",
|
||||
"3aae63bb7e8e046641767571c1591441": "Вход в систему не выполнен, так как не проверен адрес электронной почты",
|
||||
"3aecb24fa8bdd3f79d168761ca8a6729": "Неизвестный этап {{middleware}} {0}",
|
||||
"3ca45aa6f705c46a4c598a900716f086": "{0} использует параметр модели {1}, который больше не доступен.",
|
||||
"3caaa84fc103d6d5612173ae6d43b245": "Недопустимый маркер: {0}",
|
||||
"3d617953470be16d0c2b32f0bcfbb5ee": "Спасибо за регистрацию",
|
||||
"3d63008ccfb2af1db2142e8cc2716ace": "Предупреждение: не указан транспортный протокол для отправки электронной почты. Настройте транспортный протокол для отправки сообщений электронной почты.",
|
||||
"4203ab415ec66a78d3164345439ba76e": "Не удалось вызвать {0}. {1} (). Метод {2} не настроен. Модель {{PersistedModel}} неправильно подключена к источнику данных {{DataSource}}!",
|
||||
"42a36bac5cf03c4418d664500c81047a": "Опция {{DataSource}} {{\"defaultForType\"}} больше не поддерживается",
|
||||
"44a6c8b1ded4ed653d19ddeaaf89a606": "Не найден адрес электронной почты",
|
||||
"4a4f04a4e480fc5d4ee73b84d9a4b904": "Отправка почты:",
|
||||
"4b494de07f524703ac0879addbd64b13": "Не проверен адрес электронной почты",
|
||||
"528325f3cbf1b0ab9a08447515daac9a": "Обновить {0} этой модели.",
|
||||
"543d19bad5e47ee1e9eb8af688e857b4": "Внешний ключ для {0}.",
|
||||
"57b87ae0e65f6ab7a2e3e6cbdfca49a4": "Не удалось создать источник данных {0}: {1}",
|
||||
"5858e63efaa0e4ad86b61c0459ea32fa": "Необходимо подключить модель {{Email}} к коннектору {{Mail}}",
|
||||
"598ff0255ffd1d1b71e8de55dbe2c034": "Проверьте существование связи {0} с элементом по ИД.",
|
||||
"5a36cc6ba0cc27c754f6c5ed6015ea3c": "Удалить связь {0} с элементом по ИД.",
|
||||
"5e81ad3847a290dc650b47618b9cbc7e": "Вход в систему не выполнен",
|
||||
"5fa3afb425819ebde958043e598cb664": "Не удалось найти модель с {{id}} {0}",
|
||||
"61e5deebaf44d68f4e6a508f30cc31a3": "Связь `{0}` не существует для модели `{1}`",
|
||||
"62e8b0a733417978bab22c8dacf5d7e6": "Не удалось применить обновления большого объема данных, коннектор неправильно сообщает о числе обновленных записей.",
|
||||
"63a091ced88001ab6acb58f61ec041c5": "\t ТЕКСТ: {0}",
|
||||
"651f0b3cbba001635152ec3d3d954d0a": "Найти связанный элемент по ИД для {0}.",
|
||||
"6bc376432cd9972cf991aad3de371e78": "Отсутствуют данные для изменения: {0}",
|
||||
"705c2d456a3e204c4af56e671ec3225c": "Не удалось найти {{accessToken}}",
|
||||
"734a7bebb65e10899935126ba63dd51f": "Свойство options конфигурации `{0}` должно быть объектом",
|
||||
"779467f467862836e19f494a37d6ab77": "Свойство acls конфигурации `{0}` должно быть массивом объектов",
|
||||
"7bc7b301ad9c4fc873029d57fb9740fe": "{0} запросов из {1}.",
|
||||
"7c837b88fd0e509bd3fc722d7ddf0711": "Внешний ключ для {0}",
|
||||
"7d5e7ed0efaedf3f55f380caae0df8b8": "Мое первое мобильное приложение",
|
||||
"7e0fca41d098607e1c9aa353c67e0fa1": "Недопустимый маркер доступа",
|
||||
"7e287fc885d9fdcf42da3a12f38572c1": "Требуется авторизация",
|
||||
"7ea04ea91aac3cb7ce0ddd96b7ff1fa4": "{{accessToken}} требуется для завершения сеанса",
|
||||
"80a32e80cbed65eba2103201a7c94710": "Модель не найдена: {0}",
|
||||
"830cb6c862f8f364e9064cea0026f701": "Получает связь hasOne {0}.",
|
||||
"83cbdc2560ba9f09155ccfc63e08f1a1": "Не удается повторно выполнить конфигурацию свойства `{0}` для `{1}`",
|
||||
"855eb8db89b4921c42072832d33d2dc2": "Недопустимый пароль.",
|
||||
"855ecd4a64885ba272d782435f72a4d4": "Неизвестный ИД \"{0}\" \"{1}\".",
|
||||
"860d1a0b8bd340411fb32baa72867989": "Транспортный протокол не поддерживает перенаправление HTTP.",
|
||||
"86254879d01a60826a851066987703f2": "Добавить связанный элемент по ИД для {0}.",
|
||||
"895b1f941d026870b3cc8e6af087c197": "Необходимо указать {{username}} или {{email}}",
|
||||
"8ae418c605b6a45f2651be9b1677c180": "Недопустимый удаленный метод: `{0}`",
|
||||
"8bab6720ecc58ec6412358c858a53484": "Обновление большого объема данных не выполнено, коннектор изменил непредвиденное число записей: {0}",
|
||||
"8ecab7f534de38360bd1b1c88e880123": "Дочерние модели ` {0} ' не будут наследовать только что определенные удаленные методы {1}.",
|
||||
"93ba9a1d03da3b7696332d3f155c5bb7": "\t HTML:{0}",
|
||||
"97795efe0c3eb7f35ce8cf8cfe70682b": "В конфигурации `{0}` отсутствует свойство {{`dataSource`}}.\nДля пометки моделей, которые не подключены в источникам данных, используйте значение `null` или `false`.",
|
||||
"9e3cbc1d5a9347cdcf6d1b4a6dbb55b7": "Получает связь belongsTo {0}.",
|
||||
"a50d10fc6e0959b220e085454c40381e": "Пользователь не найден: {0}",
|
||||
"b6f740aeb6f2eb9bee9cb049dbfe6a28": "Неизвестный {{key}} \"{0}\" \"{1}\".",
|
||||
"ba96498b10c179f9cd75f75c8def4f70": "Необходимо указать {{realm}}",
|
||||
"c0057a569ff9d3b509bac61a4b2f605d": "Удалить все {0} этой модели.",
|
||||
"c2b5d51f007178170ca3952d59640ca4": "Не удалось исправить изменения {0}:\n{1}",
|
||||
"c4ee6d177c974532c3552d2f98eb72ea": "Промежуточное ПО {0} удалено в версии 3.0. Дополнительные сведения приведены в {1}.",
|
||||
"c61a5a02ba3801a892308f70f5d55a14": "Метаданные удаленного соединения {{\"isStatic\"}} устарели. В имени метода вместо {{isStatic=false}} укажите {{\"prototype.name\"}}.",
|
||||
"c68a93f0a9524fed4ff64372fc90c55f": "Необходимо указать допустимый адрес электронной почты",
|
||||
"cd0412f2f33a4a2a316acc834f3f21a6": "Необходимо указать {{id}} или {{data}}",
|
||||
"d5552322de5605c58b62f47ad26d2716": "Модуль {{`app.boot`}} был удален, используйте вместо него новый модуль {{loopback-boot}}",
|
||||
"d6f43b266533b04d442bdb3955622592": "Создает новый экземпляр в {0} этой модели.",
|
||||
"da13d3cdf21330557254670dddd8c5c7": "Считает {0} из {1}.",
|
||||
"dc568bee32deb0f6eaf63e73b20e8ceb": "Не относящийся к объекту параметр \"methods\" для \"{0}\" игнорируется.",
|
||||
"e4434de4bb8f5a3cd1d416e4d80d7e0b": "Неизвестный {{id}} \"{0}\" \"{1}\".",
|
||||
"e92aa25b6b864e3454b65a7c422bd114": "Обновление большого объема данных не выполнено, коннектор удалил непредвиденное число записей: {0}",
|
||||
"ea63d226b6968e328bdf6876010786b5": "Не удалось применить обновления большого объема данных, коннектор неправильно сообщает о числе удаленных записей.",
|
||||
"ead044e2b4bce74b4357f8a03fb78ec4": "Не удалось вызвать {0}.{1}(). Метод {2} не настроен. Модель {{KeyValueModel}} неправильно подключена к источнику данных {{DataSource}}!",
|
||||
"ecb06666ef95e5db27a5ac1d6a17923b": "\t Кому:{0}",
|
||||
"f0aed00a3d3d0b97d6594e4b70e0c201": "\t Транспортный протокол:{0}",
|
||||
"f0bd73df8714cefb925e3b8da2f4c5f6": "результат:{0}",
|
||||
"f1d4ac54357cc0932f385d56814ba7e4": "Конфликт",
|
||||
"f66ae3cf379b2fce28575a3282defe1a": "Удалить {0} этой модели.",
|
||||
"f8e26bcca62a47f579562f1cd2c785ff": "Доработайте приложение с целью использования официального решения для добавления аргумента \"options\" из контекста запроса,\nсм. {0}"
|
||||
}
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
{
|
||||
"03f79fa268fe199de2ce4345515431c1": "{0} için {1} tanıtıcılı bir değişiklik kaydı bulunamadı",
|
||||
"04bd8af876f001ceaf443aad6a9002f9": "Kimlik doğrulaması {0} modelinin tanımlanmasını gerektiriyor.",
|
||||
"095afbf2f1f0e5be678f5dac5c54e717": "Erişim Verilmedi",
|
||||
"0caffe1d763c8cca6a61814abe33b776": "E-posta zorunludur",
|
||||
"0da38687fed24275c1547e815914a8e3": "{0} için ilgili bir öğeyi tanıtıcı temelinde siler.",
|
||||
"0e21aad369dd09e1965c11949303cefd": "{0}.{1} {{\"isStatic\"}} ile ilgili uzaktan iletişim meta verisi, yöntem adına dayalı yeni stille eşleşmiyor.",
|
||||
"10e01c895dc0b2fecc385f9f462f1ca6": "renklerin listesine şu adresle erişebilirsiniz: {{http://localhost:3000/colors}}",
|
||||
"1b2a6076dccbe91a56f1672eb3b8598c": "Yanıt gövdesi, oturum açma sırasında yaratılan {{AccessToken}} belirtecine ilişkin özellikleri içerir.\n`include` parametresinin değerine bağlı olarak, gövde ek özellikler içerebilir:\n\n - `user` - `U+007BUserU+007D` - Oturum açmış olan kullanıcıya ilişkin veriler. {{(`include=user`)}}\n\n",
|
||||
"1d7833c3ca2f05fdad8fad7537531c40": "\t KONU:{0}",
|
||||
"1e85f822b547a75d7d385048030e4ecb": "Yaratıldığı tarih: {0}",
|
||||
"22fe62fa8d595b72c62208beddaa2a56": "{0} için ilgili bir öğeyi tanıtıcı temelinde günceller.",
|
||||
"275f22ab95671f095640ca99194b7635": "\t KİMDEN:{0}",
|
||||
"2860bccdf9ef1e350c1a38932ed12173": "{0}, 3.0 sürümünde kaldırıldı. Daha fazla ayrıntı için bkz. {1}.",
|
||||
"2d3071e3b18681c80a090dc0efbdb349": "{1} tanıtıcılı {0} bulunamadı",
|
||||
"316e5b82c203cf3de31a449ee07d0650": "Boole beklenirken {0} alındı",
|
||||
"320c482401afa1207c04343ab162e803": "Geçersiz birincil kullanıcı tipi: {0}",
|
||||
"3438fab56cc7ab92dfd88f0497e523e0": "`{0}` yapılandırmasının ilişkiler (relations) özelliği bir nesne olmalıdır",
|
||||
"3591f1d3e115b46f9f195df5ca548a6a": "Model bulunamadı: `{0}` modeli, bilinmeyen `{1}` modelini genişletiyor.",
|
||||
"35e5252c62d80f8c54a5290d30f4c7d0": "Lütfen bu bağlantıyı bir web tarayıcısında açarak e-postanızı doğrulayın:\n\t{0}",
|
||||
"37bcd4b50dfae98734772e39ffb1ea3d": "Girilen parola çok uzundu. Uzunluk üst sınırı: {0} (girilen: {1})",
|
||||
"3aae63bb7e8e046641767571c1591441": "e-posta doğrulanmadığından oturum açma başarısız oldu",
|
||||
"3aecb24fa8bdd3f79d168761ca8a6729": "Bilinmeyen {{middleware}} aşaması {0}",
|
||||
"3ca45aa6f705c46a4c598a900716f086": "{0}, artık kullanılabilir olmayan {1} model ayarını kullanıyor.",
|
||||
"3caaa84fc103d6d5612173ae6d43b245": "Geçersiz belirteç: {0}",
|
||||
"3d617953470be16d0c2b32f0bcfbb5ee": "Kaydolduğunuz için teşekkürler",
|
||||
"3d63008ccfb2af1db2142e8cc2716ace": "Uyarı: E-posta göndermek için e-posta aktarımı belirtilmedi. Posta iletileri göndermek için aktarım ayarlayın.",
|
||||
"4203ab415ec66a78d3164345439ba76e": "{0}.{1}() çağrılamıyor. {2} yöntemi ayarlanmamış. {{PersistedModel}}, bir veri kaynağına ({{DataSource}}) doğru olarak eklenmedi!",
|
||||
"42a36bac5cf03c4418d664500c81047a": "{{DataSource}} seçeneği {{\"defaultForType\"}} artık desteklenmiyor.",
|
||||
"44a6c8b1ded4ed653d19ddeaaf89a606": "E-posta bulunamadı",
|
||||
"4a4f04a4e480fc5d4ee73b84d9a4b904": "Posta Gönderiliyor:",
|
||||
"4b494de07f524703ac0879addbd64b13": "E-posta doğrulanmadı",
|
||||
"528325f3cbf1b0ab9a08447515daac9a": "Bu modele ilişkin {0} güncellemesi.",
|
||||
"543d19bad5e47ee1e9eb8af688e857b4": "{0} için dış anahtar.",
|
||||
"57b87ae0e65f6ab7a2e3e6cbdfca49a4": "Veri kaynağı {0} yaratılamıyor: {1}",
|
||||
"5858e63efaa0e4ad86b61c0459ea32fa": "{{Email}} modelini bir {{Mail}} bağlayıcısına bağlamalısınız",
|
||||
"598ff0255ffd1d1b71e8de55dbe2c034": "Bir öğeye yönelik {0} ilişkisinin var olup olmadığını tanıtıcı temelinde denetler.",
|
||||
"5a36cc6ba0cc27c754f6c5ed6015ea3c": "Bir öğeye yönelik {0} ilişkisini kaldırır.",
|
||||
"5e81ad3847a290dc650b47618b9cbc7e": "oturum açma başarısız oldu",
|
||||
"5fa3afb425819ebde958043e598cb664": "{{id}} {0} tanıtıcılı bir model bulunamadı",
|
||||
"61e5deebaf44d68f4e6a508f30cc31a3": "`{1}` modeli için `{0}` ilişkisi yok",
|
||||
"62e8b0a733417978bab22c8dacf5d7e6": "Toplu güncelleme uygulanamaz; bağlayıcı, güncellenen kayıtların sayısını doğru olarak bildirmiyor.",
|
||||
"63a091ced88001ab6acb58f61ec041c5": "\t METİN:{0}",
|
||||
"651f0b3cbba001635152ec3d3d954d0a": "{0} için ilgili bir öğeyi tanıtıcı temelinde bulur.",
|
||||
"6bc376432cd9972cf991aad3de371e78": "Değişiklik için veri eksik: {0}",
|
||||
"705c2d456a3e204c4af56e671ec3225c": "{{accessToken}} bulunamadı",
|
||||
"734a7bebb65e10899935126ba63dd51f": "`{0}` yapılandırmasının seçenekler (options) özelliği bir nesne olmalıdır.",
|
||||
"779467f467862836e19f494a37d6ab77": "`{0}` yapılandırmasının erişim denetim listeleri (acls) özelliği bir nesne dizisi olmalıdır.",
|
||||
"7bc7b301ad9c4fc873029d57fb9740fe": "{1} ile ilişkili {0} öğesini sorgular.",
|
||||
"7c837b88fd0e509bd3fc722d7ddf0711": "{0} için dış anahtar",
|
||||
"7d5e7ed0efaedf3f55f380caae0df8b8": "İlk mobil uygulamam",
|
||||
"7e0fca41d098607e1c9aa353c67e0fa1": "Geçersiz Erişim Belirteci",
|
||||
"7e287fc885d9fdcf42da3a12f38572c1": "Yetkilendirme Gerekli",
|
||||
"7ea04ea91aac3cb7ce0ddd96b7ff1fa4": "Oturumu kapatmak için {{accessToken}} zorunludur",
|
||||
"80a32e80cbed65eba2103201a7c94710": "Model bulunamadı: {0}",
|
||||
"830cb6c862f8f364e9064cea0026f701": "{0} hasOne ilişkisini alır.",
|
||||
"83cbdc2560ba9f09155ccfc63e08f1a1": "`{0}` özelliği `{1}` için yeniden yapılandırılamıyor",
|
||||
"855eb8db89b4921c42072832d33d2dc2": "Geçersiz parola.",
|
||||
"855ecd4a64885ba272d782435f72a4d4": "Bilinmeyen \"{0}\" tanıtıcısı \"{1}\".",
|
||||
"860d1a0b8bd340411fb32baa72867989": "Aktarım HTTP yeniden yönlendirmelerini desteklemiyor.",
|
||||
"86254879d01a60826a851066987703f2": "{0} için ilgili bir öğeyi tanıtıcı temelinde ekler.",
|
||||
"895b1f941d026870b3cc8e6af087c197": "{{username}} ya da {{email}} zorunludur",
|
||||
"8ae418c605b6a45f2651be9b1677c180": "Uzak yöntem geçersiz: `{0}`",
|
||||
"8bab6720ecc58ec6412358c858a53484": "Toplu güncelleme başarısız oldu, bağlayıcı beklenmeyen sayıda kaydı değiştirdi: {0}",
|
||||
"8ecab7f534de38360bd1b1c88e880123": "`{0}` alt modelleri, yeni tanımlanan uzak yöntemleri ({1}) devralmayacaktır.",
|
||||
"93ba9a1d03da3b7696332d3f155c5bb7": "\t HTML:{0}",
|
||||
"97795efe0c3eb7f35ce8cf8cfe70682b": "`{0}` yapılandırmasında {{`dataSource`}} özelliği eksik.\nHiçbir veri kaynağına eklenmemiş modelleri işaretlemek için `null` ya da `false` kullanın.",
|
||||
"9e3cbc1d5a9347cdcf6d1b4a6dbb55b7": "{0} belongsTo ilişkisini alır.",
|
||||
"a50d10fc6e0959b220e085454c40381e": "Kullanıcı bulunamadı: {0}",
|
||||
"b6f740aeb6f2eb9bee9cb049dbfe6a28": "Bilinmeyen \"{0}\" {{key}} \"{1}\".",
|
||||
"ba96498b10c179f9cd75f75c8def4f70": "{{realm}} zorunludur",
|
||||
"c0057a569ff9d3b509bac61a4b2f605d": "Bu modele ilişkin tüm {0} öğelerini siler.",
|
||||
"c2b5d51f007178170ca3952d59640ca4": "{0} değişiklik düzeltilemiyor:\n{1}",
|
||||
"c4ee6d177c974532c3552d2f98eb72ea": "{0} ara katman yazılımı, 3.0 sürümünde kaldırıldı. Daha fazla ayrıntı için bkz. {1}.",
|
||||
"c61a5a02ba3801a892308f70f5d55a14": "{{\"isStatic\"}} uzaktan iletişim meta verisi kullanım dışı bırakıldı. Lütfen, yöntem adında {{isStatic=false}} yerine {{\"prototype.name\"}} belirtin.",
|
||||
"c68a93f0a9524fed4ff64372fc90c55f": "Geçerli bir e-posta belirtilmeli",
|
||||
"cd0412f2f33a4a2a316acc834f3f21a6": "bir {{id}} ya da {{data}} belirtmelidir",
|
||||
"d5552322de5605c58b62f47ad26d2716": "{{`app.boot`}} kaldırıldı, onun yerine yeni {{loopback-boot}} modülünü kullanın",
|
||||
"d6f43b266533b04d442bdb3955622592": "Bu modele ilişkin {0} içinde yeni eşgörünüm yaratır.",
|
||||
"da13d3cdf21330557254670dddd8c5c7": "{1} ile ilişkili {0} öğesini sayar.",
|
||||
"dc568bee32deb0f6eaf63e73b20e8ceb": "\"{0}\" öğesinin nesne olmayan \"methods\" atarı yoksayılıyor.",
|
||||
"e4434de4bb8f5a3cd1d416e4d80d7e0b": "Bilinmeyen \"{0}\" {{id}} \"{1}\".",
|
||||
"e92aa25b6b864e3454b65a7c422bd114": "Toplu güncelleme başarısız oldu, bağlayıcı beklenmeyen sayıda kaydı sildi: {0}",
|
||||
"ea63d226b6968e328bdf6876010786b5": "Toplu güncelleme uygulanamaz; bağlayıcı, silinen kayıtların sayısını doğru olarak bildirmiyor.",
|
||||
"ead044e2b4bce74b4357f8a03fb78ec4": "{0}.{1}() çağrılamıyor. {2} yöntemi ayarlanmamış. {{KeyValueModel}}, bir veri kaynağına ({{DataSource}}) doğru olarak eklenmedi!",
|
||||
"ecb06666ef95e5db27a5ac1d6a17923b": "\t KİME:{0}",
|
||||
"f0aed00a3d3d0b97d6594e4b70e0c201": "\t AKTARIM:{0}",
|
||||
"f0bd73df8714cefb925e3b8da2f4c5f6": "sonuç:{0}",
|
||||
"f1d4ac54357cc0932f385d56814ba7e4": "Çakışma",
|
||||
"f66ae3cf379b2fce28575a3282defe1a": "Bu modele ilişkin {0} öğesini siler.",
|
||||
"f8e26bcca62a47f579562f1cd2c785ff": "Lütfen istek bağlamından \"seçenekler\" bağımsız değişkenini eklemek amacıyla resmi çözümü kullanmak için uygulamanız üzerinde yeniden çalışın,\nbkz. {0}"
|
||||
}
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
{
|
||||
"03f79fa268fe199de2ce4345515431c1": "对于标识为 {1} 的 {0},找不到任何更改记录",
|
||||
"04bd8af876f001ceaf443aad6a9002f9": "认证需要定义模型 {0}。",
|
||||
"095afbf2f1f0e5be678f5dac5c54e717": "拒绝访问",
|
||||
"0caffe1d763c8cca6a61814abe33b776": "电子邮件是必需的",
|
||||
"0da38687fed24275c1547e815914a8e3": "按标识删除 {0} 的相关项。",
|
||||
"0e21aad369dd09e1965c11949303cefd": "{0}.{1} {{\"isStatic\"}} 的远程处理元数据不匹配新的基于方法名称的样式。",
|
||||
"10e01c895dc0b2fecc385f9f462f1ca6": "颜色列表位于:{{http://localhost:3000/colors}}",
|
||||
"1b2a6076dccbe91a56f1672eb3b8598c": "响应主体包含在登录时创建的 {{AccessToken}} 的属性。\n根据“include”参数的值,主体可包含其他属性:\n\n - `user` - `U+007BUserU+007D` - 当前已登录用户的数据。 {{(`include=user`)}}\n\n",
|
||||
"1d7833c3ca2f05fdad8fad7537531c40": "\t主题:{0}",
|
||||
"1e85f822b547a75d7d385048030e4ecb": "创建时间:{0}",
|
||||
"22fe62fa8d595b72c62208beddaa2a56": "按标识更新 {0} 的相关项。",
|
||||
"275f22ab95671f095640ca99194b7635": "\t发件人:{0}",
|
||||
"2860bccdf9ef1e350c1a38932ed12173": "V3.0 中移除了 {0}。请参阅 {1} 以获取更多详细信息。",
|
||||
"2d3071e3b18681c80a090dc0efbdb349": "无法找到标识为 {1} 的 {0}",
|
||||
"316e5b82c203cf3de31a449ee07d0650": "期望布尔值,获取 {0}",
|
||||
"320c482401afa1207c04343ab162e803": "无效的主体类型:{0}",
|
||||
"3438fab56cc7ab92dfd88f0497e523e0": "“{0}”配置的关系属性必须是对象。",
|
||||
"3591f1d3e115b46f9f195df5ca548a6a": "找不到模型:模型“{0}”正在扩展未知的模型“{1}”。",
|
||||
"35e5252c62d80f8c54a5290d30f4c7d0": "请通过在 Web 浏览器中打开此链接来验证您的电子邮件:\n\t{0}",
|
||||
"37bcd4b50dfae98734772e39ffb1ea3d": "输入的密码过长。最大长度为 {0}(输入的长度为 {1})",
|
||||
"3aae63bb7e8e046641767571c1591441": "因为尚未验证电子邮件,登录失败",
|
||||
"3aecb24fa8bdd3f79d168761ca8a6729": "未知的 {{middleware}} 阶段 {0}",
|
||||
"3ca45aa6f705c46a4c598a900716f086": "{0} 正在使用目前不再可用的模型设置 {1}。",
|
||||
"3caaa84fc103d6d5612173ae6d43b245": "无效的令牌:{0}",
|
||||
"3d617953470be16d0c2b32f0bcfbb5ee": "感谢您注册",
|
||||
"3d63008ccfb2af1db2142e8cc2716ace": "警告:未指定用于发送电子邮件的电子邮件传输。设置传输以发送电子邮件消息。",
|
||||
"4203ab415ec66a78d3164345439ba76e": "无法调用 {0}.{1}()。尚未设置 {2} 方法。{{PersistedModel}} 未正确附加到 {{DataSource}}!",
|
||||
"42a36bac5cf03c4418d664500c81047a": "不再支持 {{DataSource}} 选项 {{\"defaultForType\"}}",
|
||||
"44a6c8b1ded4ed653d19ddeaaf89a606": "找不到电子邮件",
|
||||
"4a4f04a4e480fc5d4ee73b84d9a4b904": "正在发送电子邮件:",
|
||||
"4b494de07f524703ac0879addbd64b13": "尚未验证电子邮件",
|
||||
"528325f3cbf1b0ab9a08447515daac9a": "更新此模型的 {0}。",
|
||||
"543d19bad5e47ee1e9eb8af688e857b4": "{0} 的外键。",
|
||||
"57b87ae0e65f6ab7a2e3e6cbdfca49a4": "无法创建数据源 {0}:{1}",
|
||||
"5858e63efaa0e4ad86b61c0459ea32fa": "您必须将 {{Email}} 模型连接到 {{Mail}} 连接器",
|
||||
"598ff0255ffd1d1b71e8de55dbe2c034": "按标识检查项的 {0} 关系是否存在。",
|
||||
"5a36cc6ba0cc27c754f6c5ed6015ea3c": "按标识除去项的 {0} 关系。",
|
||||
"5e81ad3847a290dc650b47618b9cbc7e": "登录失败",
|
||||
"5fa3afb425819ebde958043e598cb664": "找不到具有 {{id}} {0} 的模型",
|
||||
"61e5deebaf44d68f4e6a508f30cc31a3": "对于模型“{1}”,关系“{0}”不存在",
|
||||
"62e8b0a733417978bab22c8dacf5d7e6": "无法应用批量更新,连接器未正确报告更新的记录数。",
|
||||
"63a091ced88001ab6acb58f61ec041c5": "\t 文本:{0}",
|
||||
"651f0b3cbba001635152ec3d3d954d0a": "按标识查找 {0} 的相关项。",
|
||||
"6bc376432cd9972cf991aad3de371e78": "缺少更改的数据:{0}",
|
||||
"705c2d456a3e204c4af56e671ec3225c": "无法找到 {{accessToken}}",
|
||||
"734a7bebb65e10899935126ba63dd51f": "“{0}”配置的选项属性必须是对象。",
|
||||
"779467f467862836e19f494a37d6ab77": "“{0}”配置的 acls 属性必须是对象数组。",
|
||||
"7bc7b301ad9c4fc873029d57fb9740fe": "查询 {1} 的 {0}。",
|
||||
"7c837b88fd0e509bd3fc722d7ddf0711": "{0} 的外键",
|
||||
"7d5e7ed0efaedf3f55f380caae0df8b8": "我的第一个移动应用程序",
|
||||
"7e0fca41d098607e1c9aa353c67e0fa1": "无效的访问令牌",
|
||||
"7e287fc885d9fdcf42da3a12f38572c1": "需要授权",
|
||||
"7ea04ea91aac3cb7ce0ddd96b7ff1fa4": "{{accessToken}} 需要注销",
|
||||
"80a32e80cbed65eba2103201a7c94710": "找不到模型:{0}",
|
||||
"830cb6c862f8f364e9064cea0026f701": "访存 hasOne 关系 {0}。",
|
||||
"83cbdc2560ba9f09155ccfc63e08f1a1": "无法针对“{1}”重新配置属性“{0}”。",
|
||||
"855eb8db89b4921c42072832d33d2dc2": "密码无效。",
|
||||
"855ecd4a64885ba272d782435f72a4d4": "未知的“{0}”标识“{1}”。",
|
||||
"860d1a0b8bd340411fb32baa72867989": "传输不支持 HTTP 重定向。",
|
||||
"86254879d01a60826a851066987703f2": "按标识添加 {0} 的相关项。",
|
||||
"895b1f941d026870b3cc8e6af087c197": "{{username}} 或 {{email}} 是必需的",
|
||||
"8ae418c605b6a45f2651be9b1677c180": "无效的远程方法:“{0}”",
|
||||
"8bab6720ecc58ec6412358c858a53484": "批量更新失败,连接器已修改意外数量的记录:{0}",
|
||||
"8ecab7f534de38360bd1b1c88e880123": "“{0}”的子模型不会继承最新定义的远程方法 {1}。",
|
||||
"93ba9a1d03da3b7696332d3f155c5bb7": "\t HTML:{0}",
|
||||
"97795efe0c3eb7f35ce8cf8cfe70682b": "“{0}”的配置缺少 {{`dataSource`}} 属性。\n使用“null”或“false”来标记未附加到任何数据源的模型。",
|
||||
"9e3cbc1d5a9347cdcf6d1b4a6dbb55b7": "访存 belongsTo 关系 {0}。",
|
||||
"a50d10fc6e0959b220e085454c40381e": "找不到用户:{0}",
|
||||
"b6f740aeb6f2eb9bee9cb049dbfe6a28": "未知的“{0}”{{key}}“{1}”。",
|
||||
"ba96498b10c179f9cd75f75c8def4f70": "{{realm}} 是必需的",
|
||||
"c0057a569ff9d3b509bac61a4b2f605d": "删除此模型的所有 {0}。",
|
||||
"c2b5d51f007178170ca3952d59640ca4": "无法纠正 {0} 更改:\n{1}",
|
||||
"c4ee6d177c974532c3552d2f98eb72ea": "V3.0 中移除了 {0} 中间件。请参阅 {1} 以获取更多详细信息。",
|
||||
"c61a5a02ba3801a892308f70f5d55a14": "不推荐使用远程处理元数据 {{\"isStatic\"}}。而是针对 {{isStatic=false}} 在方法名称中指定 {{\"prototype.name\"}}。",
|
||||
"c68a93f0a9524fed4ff64372fc90c55f": "必须提供有效电子邮件",
|
||||
"cd0412f2f33a4a2a316acc834f3f21a6": "必须指定 {{id}} 或 {{data}}",
|
||||
"d5552322de5605c58b62f47ad26d2716": "已移除 {{`app.boot`}},请改用新模块 {{loopback-boot}}",
|
||||
"d6f43b266533b04d442bdb3955622592": "在此模型的 {0} 中创建新实例。",
|
||||
"da13d3cdf21330557254670dddd8c5c7": "计算 {0} 的数量({1})。",
|
||||
"dc568bee32deb0f6eaf63e73b20e8ceb": "忽略“{0}”的非对象“方法”设置。",
|
||||
"e4434de4bb8f5a3cd1d416e4d80d7e0b": "未知的“{0}”{{id}}“{1}”。",
|
||||
"e92aa25b6b864e3454b65a7c422bd114": "批量更新失败,连接器已删除意外数量的记录:{0}",
|
||||
"ea63d226b6968e328bdf6876010786b5": "无法应用批量更新,连接器未正确报告删除的记录数。",
|
||||
"ead044e2b4bce74b4357f8a03fb78ec4": "无法调用 {0}.{1}()。尚未设置 {2} 方法。{{KeyValueModel}} 未正确附加到 {{DataSource}}!",
|
||||
"ecb06666ef95e5db27a5ac1d6a17923b": "\t 收件人:{0}",
|
||||
"f0aed00a3d3d0b97d6594e4b70e0c201": "\t 传输:{0}",
|
||||
"f0bd73df8714cefb925e3b8da2f4c5f6": "结果:{0}",
|
||||
"f1d4ac54357cc0932f385d56814ba7e4": "冲突",
|
||||
"f66ae3cf379b2fce28575a3282defe1a": "删除此模型的 {0}。",
|
||||
"f8e26bcca62a47f579562f1cd2c785ff": "请重新设计您的应用程序以使用正式解决方案插入来自请求上下文的“options”自变量,\n请参阅 {0}"
|
||||
}
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
{
|
||||
"03f79fa268fe199de2ce4345515431c1": "對於 id 為 {1} 的 {0},找不到變更記錄",
|
||||
"04bd8af876f001ceaf443aad6a9002f9": "需要定義模型 {0} 才能鑑別。",
|
||||
"095afbf2f1f0e5be678f5dac5c54e717": "拒絕存取",
|
||||
"0caffe1d763c8cca6a61814abe33b776": "需要電子郵件",
|
||||
"0da38687fed24275c1547e815914a8e3": "依 id 刪除 {0} 的相關項目。",
|
||||
"0e21aad369dd09e1965c11949303cefd": "{0} 的遠端 meta 資料。{1} {{\"isStatic\"}} 不符合新的方法名稱型樣式。",
|
||||
"10e01c895dc0b2fecc385f9f462f1ca6": "{{http://localhost:3000/colors}} 提供顏色清單",
|
||||
"1b2a6076dccbe91a56f1672eb3b8598c": "回應內文包含登入時建立的 {{AccessToken}} 的內容。\n根據 `include` 參數的值而定,內文可能包含其他內容:\n\n - `user` - `U+007BUserU+007D` - 目前登入的使用者的資料。 {{(`include=user`)}}\n\n",
|
||||
"1d7833c3ca2f05fdad8fad7537531c40": "\t 主旨:{0}",
|
||||
"1e85f822b547a75d7d385048030e4ecb": "已建立:{0}",
|
||||
"22fe62fa8d595b72c62208beddaa2a56": "依 id 更新 {0} 的相關項目。",
|
||||
"275f22ab95671f095640ca99194b7635": "\t 寄件者:{0}",
|
||||
"2860bccdf9ef1e350c1a38932ed12173": "3.0 版中已移除 {0}。如需詳細資料,請參閱 {1}。",
|
||||
"2d3071e3b18681c80a090dc0efbdb349": "找不到 id 為 {1} 的 {0}",
|
||||
"316e5b82c203cf3de31a449ee07d0650": "預期為布林,但卻取得 {0}",
|
||||
"320c482401afa1207c04343ab162e803": "無效的主體類型:{0}",
|
||||
"3438fab56cc7ab92dfd88f0497e523e0": "`{0}` 配置的 relations 內容必須是物件",
|
||||
"3591f1d3e115b46f9f195df5ca548a6a": "找不到模型:模型 `{0}` 正在延伸不明模型 `{1}`。",
|
||||
"35e5252c62d80f8c54a5290d30f4c7d0": "請在 Web 瀏覽器中開啟此鏈結來驗證電子郵件:\n\t{0}",
|
||||
"37bcd4b50dfae98734772e39ffb1ea3d": "輸入的密碼太長。長度上限為 {0}(輸入了 {1})",
|
||||
"3aae63bb7e8e046641767571c1591441": "因為尚未驗證電子郵件,所以登入失敗",
|
||||
"3aecb24fa8bdd3f79d168761ca8a6729": "{{middleware}} 階段 {0} 不明",
|
||||
"3ca45aa6f705c46a4c598a900716f086": "{0} 正在使用已不再可用的模型設定 {1}。",
|
||||
"3caaa84fc103d6d5612173ae6d43b245": "無效記號:{0}",
|
||||
"3d617953470be16d0c2b32f0bcfbb5ee": "感謝您登錄",
|
||||
"3d63008ccfb2af1db2142e8cc2716ace": "警告:未指定用於傳送電子郵件的電子郵件傳輸。請設定傳輸來傳送郵件訊息。",
|
||||
"4203ab415ec66a78d3164345439ba76e": "無法呼叫 {0}。{1}()。尚未設定 {2} 方法。{{PersistedModel}} 未正確連接至 {{DataSource}}!",
|
||||
"42a36bac5cf03c4418d664500c81047a": "不再支援 {{DataSource}} 選項 {{\"defaultForType\"}}",
|
||||
"44a6c8b1ded4ed653d19ddeaaf89a606": "找不到電子郵件",
|
||||
"4a4f04a4e480fc5d4ee73b84d9a4b904": "正在傳送郵件:",
|
||||
"4b494de07f524703ac0879addbd64b13": "尚未驗證電子郵件",
|
||||
"528325f3cbf1b0ab9a08447515daac9a": "更新這個模型的 {0}。",
|
||||
"543d19bad5e47ee1e9eb8af688e857b4": "{0} 的外部索引鍵。",
|
||||
"57b87ae0e65f6ab7a2e3e6cbdfca49a4": "無法建立資料來源 {0}:{1}",
|
||||
"5858e63efaa0e4ad86b61c0459ea32fa": "您必須將 {{Email}} 模型連接至 {{Mail}} 連接器",
|
||||
"598ff0255ffd1d1b71e8de55dbe2c034": "依 id 檢查項目的 {0} 關係是否存在。",
|
||||
"5a36cc6ba0cc27c754f6c5ed6015ea3c": "依 id 移除項目的 {0} 關係。",
|
||||
"5e81ad3847a290dc650b47618b9cbc7e": "登入失敗",
|
||||
"5fa3afb425819ebde958043e598cb664": "找不到 {{id}} 為 {0} 的模型",
|
||||
"61e5deebaf44d68f4e6a508f30cc31a3": "模型 `{1}` 的關係 `{0}` 不存在",
|
||||
"62e8b0a733417978bab22c8dacf5d7e6": "無法套用大量更新,連接器未正確報告已更新的記錄數。",
|
||||
"63a091ced88001ab6acb58f61ec041c5": "\t 文字:{0}",
|
||||
"651f0b3cbba001635152ec3d3d954d0a": "依 id 尋找 {0} 的相關項目。",
|
||||
"6bc376432cd9972cf991aad3de371e78": "遺漏變更的資料:{0}",
|
||||
"705c2d456a3e204c4af56e671ec3225c": "找不到 {{accessToken}}",
|
||||
"734a7bebb65e10899935126ba63dd51f": "`{0}` 配置的 options 內容必須是物件",
|
||||
"779467f467862836e19f494a37d6ab77": "`{0}` 配置的 acls 內容必須是物件陣列",
|
||||
"7bc7b301ad9c4fc873029d57fb9740fe": "查詢 {0} 個(共 {1} 個)。",
|
||||
"7c837b88fd0e509bd3fc722d7ddf0711": "{0} 的外部索引鍵",
|
||||
"7d5e7ed0efaedf3f55f380caae0df8b8": "我的第一個行動式應用程式",
|
||||
"7e0fca41d098607e1c9aa353c67e0fa1": "存取記號無效",
|
||||
"7e287fc885d9fdcf42da3a12f38572c1": "需要授權",
|
||||
"7ea04ea91aac3cb7ce0ddd96b7ff1fa4": "需要 {{accessToken}} 才能登出",
|
||||
"80a32e80cbed65eba2103201a7c94710": "找不到模型:{0}",
|
||||
"830cb6c862f8f364e9064cea0026f701": "提取 hasOne 關係 {0}。",
|
||||
"83cbdc2560ba9f09155ccfc63e08f1a1": "無法為 `{1}` 重新配置內容 `{0}`",
|
||||
"855eb8db89b4921c42072832d33d2dc2": "密碼無效。",
|
||||
"855ecd4a64885ba272d782435f72a4d4": "\"{0}\" ID \"{1}\" 不明。",
|
||||
"860d1a0b8bd340411fb32baa72867989": "傳輸不支援 HTTP 重新導向。",
|
||||
"86254879d01a60826a851066987703f2": "依 id 新增 {0} 的相關項目。",
|
||||
"895b1f941d026870b3cc8e6af087c197": "需要 {{username}} 或 {{email}}",
|
||||
"8ae418c605b6a45f2651be9b1677c180": "無效的遠端方法:`{0}`",
|
||||
"8bab6720ecc58ec6412358c858a53484": "大量更新失敗,連接器已修改超乎預期的記錄數:{0}",
|
||||
"8ecab7f534de38360bd1b1c88e880123": "`{0}` 的子項模型將不會繼承新定義的遠端方法 {1}。",
|
||||
"93ba9a1d03da3b7696332d3f155c5bb7": "\t HTML:{0}",
|
||||
"97795efe0c3eb7f35ce8cf8cfe70682b": "`{0}` 的配置遺漏 {{`dataSource`}} 內容。\n請使用 `null` 或 `false` 來標示未連接至任何資料來源的模型。",
|
||||
"9e3cbc1d5a9347cdcf6d1b4a6dbb55b7": "提取 belongsTo 關係 {0}。",
|
||||
"a50d10fc6e0959b220e085454c40381e": "找不到使用者:{0}",
|
||||
"b6f740aeb6f2eb9bee9cb049dbfe6a28": "\"{0}\" {{key}} \"{1}\" 不明。",
|
||||
"ba96498b10c179f9cd75f75c8def4f70": "需要 {{realm}}",
|
||||
"c0057a569ff9d3b509bac61a4b2f605d": "刪除這個模型的所有 {0}。",
|
||||
"c2b5d51f007178170ca3952d59640ca4": "無法更正 {0} 個變更:\n{1}",
|
||||
"c4ee6d177c974532c3552d2f98eb72ea": "3.0 版中已移除 {0} 中介軟體。如需詳細資料,請參閱 {1}。",
|
||||
"c61a5a02ba3801a892308f70f5d55a14": "遠端 meta 資料 {{\"isStatic\"}} 已淘汰。請在方法名稱中指定 {{\"prototype.name\"}} 來代替 {{isStatic=false}}。",
|
||||
"c68a93f0a9524fed4ff64372fc90c55f": "必須提供有效的電子郵件",
|
||||
"cd0412f2f33a4a2a316acc834f3f21a6": "必須指定 {{id}} 或 {{data}}",
|
||||
"d5552322de5605c58b62f47ad26d2716": "已移除 {{`app.boot`}},請改用新的模組 {{loopback-boot}}",
|
||||
"d6f43b266533b04d442bdb3955622592": "在這個模型的 {0} 中建立新實例。",
|
||||
"da13d3cdf21330557254670dddd8c5c7": "計算 {0} 個(共 {1} 個)。",
|
||||
"dc568bee32deb0f6eaf63e73b20e8ceb": "忽略 \"{0}\" 的非物件 \"methods\" 設定。",
|
||||
"e4434de4bb8f5a3cd1d416e4d80d7e0b": "\"{0}\" {{id}} \"{1}\" 不明。",
|
||||
"e92aa25b6b864e3454b65a7c422bd114": "大量更新失敗,連接器已刪除非預期的記錄數:{0}",
|
||||
"ea63d226b6968e328bdf6876010786b5": "無法套用大量更新,連接器未正確報告已刪除的記錄數。",
|
||||
"ead044e2b4bce74b4357f8a03fb78ec4": "無法呼叫 {0}。{1}()。尚未設定 {2} 方法。{{KeyValueModel}} 未正確連接至 {{DataSource}}!",
|
||||
"ecb06666ef95e5db27a5ac1d6a17923b": "\t 收件者:{0}",
|
||||
"f0aed00a3d3d0b97d6594e4b70e0c201": "\t 傳輸:{0}",
|
||||
"f0bd73df8714cefb925e3b8da2f4c5f6": "結果:{0}",
|
||||
"f1d4ac54357cc0932f385d56814ba7e4": "衝突",
|
||||
"f66ae3cf379b2fce28575a3282defe1a": "刪除這個模型的 {0}。",
|
||||
"f8e26bcca62a47f579562f1cd2c785ff": "請重做您的應用程式以使用正式解決方案,從要求環境定義注入 \"options\" 引數,\n請參閱 {0}"
|
||||
}
|
||||
|
|
@ -1,22 +1,42 @@
|
|||
var assert = require('assert');
|
||||
var loopback = require('./loopback');
|
||||
var debug = require('debug')('loopback:security:access-context');
|
||||
// Copyright IBM Corp. 2014,2019. All Rights Reserved.
|
||||
// Node module: loopback
|
||||
// This file is licensed under the MIT License.
|
||||
// License text available at https://opensource.org/licenses/MIT
|
||||
|
||||
'use strict';
|
||||
const assert = require('assert');
|
||||
const loopback = require('./loopback');
|
||||
const debug = require('debug')('loopback:security:access-context');
|
||||
|
||||
const DEFAULT_SCOPES = ['DEFAULT'];
|
||||
|
||||
/**
|
||||
* Access context represents the context for a request to access protected
|
||||
* resources
|
||||
*
|
||||
* NOTE While the method expects an array of principals in the AccessContext instance/object,
|
||||
* it also accepts a single principal defined with the following properties:
|
||||
* ```js
|
||||
* {
|
||||
* // AccessContext instance/object
|
||||
* // ..
|
||||
* principalType: 'somePrincipalType', // APP, ROLE, USER, or custom user model name
|
||||
* principalId: 'somePrincipalId',
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @class
|
||||
* @options {Object} context The context object
|
||||
* @options {AccessContext|Object} context An AccessContext instance or an object
|
||||
* @property {Principal[]} principals An array of principals
|
||||
* @property {Function} model The model class
|
||||
* @property {String} modelName The model name
|
||||
* @property {String} modelId The model id
|
||||
* @property {*} modelId The model id
|
||||
* @property {String} property The model property/method/relation name
|
||||
* @property {String} method The model method to be invoked
|
||||
* @property {String} accessType The access type
|
||||
* @property {AccessToken} accessToken The access token
|
||||
*
|
||||
* @property {String} accessType The access type: READ, REPLICATE, WRITE, or EXECUTE.
|
||||
* @property {AccessToken} accessToken The access token resolved for the request
|
||||
* @property {RemotingContext} remotingContext The request's remoting context
|
||||
* @property {Registry} registry The application or global registry
|
||||
* @returns {AccessContext}
|
||||
* @constructor
|
||||
*/
|
||||
|
@ -26,9 +46,12 @@ function AccessContext(context) {
|
|||
}
|
||||
context = context || {};
|
||||
|
||||
assert(context.registry,
|
||||
'Application registry is mandatory in AccessContext but missing in provided context');
|
||||
this.registry = context.registry;
|
||||
this.principals = context.principals || [];
|
||||
var model = context.model;
|
||||
model = ('string' === typeof model) ? loopback.getModel(model) : model;
|
||||
let model = context.model;
|
||||
model = ('string' === typeof model) ? this.registry.getModel(model) : model;
|
||||
this.model = model;
|
||||
this.modelName = model && model.modelName;
|
||||
|
||||
|
@ -49,23 +72,24 @@ function AccessContext(context) {
|
|||
}
|
||||
|
||||
this.accessType = context.accessType || AccessContext.ALL;
|
||||
|
||||
assert(loopback.AccessToken,
|
||||
'AccessToken model must be defined before AccessContext model');
|
||||
this.accessToken = context.accessToken || loopback.AccessToken.ANONYMOUS;
|
||||
|
||||
var principalType = context.principalType || Principal.USER;
|
||||
var principalId = context.principalId || undefined;
|
||||
var principalName = context.principalName || undefined;
|
||||
if (principalId) {
|
||||
const principalType = context.principalType || Principal.USER;
|
||||
const principalId = context.principalId || undefined;
|
||||
const principalName = context.principalName || undefined;
|
||||
if (principalId != null) {
|
||||
this.addPrincipal(principalType, principalId, principalName);
|
||||
}
|
||||
|
||||
var token = this.accessToken || {};
|
||||
const token = this.accessToken;
|
||||
|
||||
if (token.userId) {
|
||||
this.addPrincipal(Principal.USER, token.userId);
|
||||
if (token.userId != null) {
|
||||
this.addPrincipal(token.principalType || Principal.USER, token.userId);
|
||||
}
|
||||
if (token.appId) {
|
||||
if (token.appId != null) {
|
||||
this.addPrincipal(Principal.APPLICATION, token.appId);
|
||||
}
|
||||
this.remotingContext = context.remotingContext;
|
||||
|
@ -91,7 +115,7 @@ AccessContext.permissionOrder = {
|
|||
ALLOW: 1,
|
||||
ALARM: 2,
|
||||
AUDIT: 3,
|
||||
DENY: 4
|
||||
DENY: 4,
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -102,9 +126,9 @@ AccessContext.permissionOrder = {
|
|||
* @returns {boolean}
|
||||
*/
|
||||
AccessContext.prototype.addPrincipal = function(principalType, principalId, principalName) {
|
||||
var principal = new Principal(principalType, principalId, principalName);
|
||||
for (var i = 0; i < this.principals.length; i++) {
|
||||
var p = this.principals[i];
|
||||
const principal = new Principal(principalType, principalId, principalName);
|
||||
for (let i = 0; i < this.principals.length; i++) {
|
||||
const p = this.principals[i];
|
||||
if (p.equals(principal)) {
|
||||
return false;
|
||||
}
|
||||
|
@ -118,13 +142,35 @@ AccessContext.prototype.addPrincipal = function(principalType, principalId, prin
|
|||
* @returns {*}
|
||||
*/
|
||||
AccessContext.prototype.getUserId = function() {
|
||||
for (var i = 0; i < this.principals.length; i++) {
|
||||
var p = this.principals[i];
|
||||
const user = this.getUser();
|
||||
return user && user.id;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the user
|
||||
* @returns {*}
|
||||
*/
|
||||
AccessContext.prototype.getUser = function() {
|
||||
const BaseUser = this.registry.getModel('User');
|
||||
for (let i = 0; i < this.principals.length; i++) {
|
||||
const p = this.principals[i];
|
||||
const isBuiltinPrincipal = p.type === Principal.APP ||
|
||||
p.type === Principal.ROLE ||
|
||||
p.type == Principal.SCOPE;
|
||||
if (isBuiltinPrincipal) continue;
|
||||
|
||||
// the principalType must either be 'USER'
|
||||
if (p.type === Principal.USER) {
|
||||
return p.id;
|
||||
return {id: p.id, principalType: p.type};
|
||||
}
|
||||
|
||||
// or permit to resolve a valid user model
|
||||
const userModel = this.registry.findModel(p.type);
|
||||
if (!userModel) continue;
|
||||
if (userModel.prototype instanceof BaseUser) {
|
||||
return {id: p.id, principalType: p.type};
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -132,8 +178,8 @@ AccessContext.prototype.getUserId = function() {
|
|||
* @returns {*}
|
||||
*/
|
||||
AccessContext.prototype.getAppId = function() {
|
||||
for (var i = 0; i < this.principals.length; i++) {
|
||||
var p = this.principals[i];
|
||||
for (let i = 0; i < this.principals.length; i++) {
|
||||
const p = this.principals[i];
|
||||
if (p.type === Principal.APPLICATION) {
|
||||
return p.id;
|
||||
}
|
||||
|
@ -146,7 +192,46 @@ AccessContext.prototype.getAppId = function() {
|
|||
* @returns {boolean}
|
||||
*/
|
||||
AccessContext.prototype.isAuthenticated = function() {
|
||||
return !!(this.getUserId() || this.getAppId());
|
||||
return this.getUserId() != null || this.getAppId() != null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the list of scopes required by the current access context.
|
||||
*/
|
||||
AccessContext.prototype.getScopes = function() {
|
||||
if (!this.sharedMethod)
|
||||
return DEFAULT_SCOPES;
|
||||
|
||||
// For backwards compatibility, methods with no scopes defined
|
||||
// are assigned a single "DEFAULT" scope
|
||||
const methodLevel = this.sharedMethod.accessScopes || DEFAULT_SCOPES;
|
||||
|
||||
// TODO add model-level and app-level scopes
|
||||
|
||||
debug('--Context scopes of %s()--', this.sharedMethod.stringName);
|
||||
debug(' method-level: %j', methodLevel);
|
||||
|
||||
return methodLevel;
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if the scope required by the remote method is allowed
|
||||
* by the scopes granted to the requesting access token.
|
||||
* @return {boolean}
|
||||
*/
|
||||
AccessContext.prototype.isScopeAllowed = function() {
|
||||
if (!this.accessToken) return false;
|
||||
|
||||
// For backwards compatibility, tokens with no scopes are treated
|
||||
// as if they have "DEFAULT" scope granted
|
||||
const tokenScopes = this.accessToken.scopes || DEFAULT_SCOPES;
|
||||
|
||||
const resourceScopes = this.getScopes();
|
||||
|
||||
// Scope is allowed when at least one of token's scopes
|
||||
// is found in method's (resource's) scopes.
|
||||
return Array.isArray(tokenScopes) && Array.isArray(resourceScopes) &&
|
||||
resourceScopes.some(s => tokenScopes.indexOf(s) !== -1);
|
||||
};
|
||||
|
||||
/*!
|
||||
|
@ -169,10 +254,12 @@ AccessContext.prototype.debug = function() {
|
|||
debug('property %s', this.property);
|
||||
debug('method %s', this.method);
|
||||
debug('accessType %s', this.accessType);
|
||||
debug('accessScopes %j', this.getScopes());
|
||||
if (this.accessToken) {
|
||||
debug('accessToken:');
|
||||
debug(' id %j', this.accessToken.id);
|
||||
debug(' ttl %j', this.accessToken.ttl);
|
||||
debug(' scopes %j', this.accessToken.scopes || DEFAULT_SCOPES);
|
||||
}
|
||||
debug('getUserId() %s', this.getUserId());
|
||||
debug('isAuthenticated() %s', this.isAuthenticated());
|
||||
|
@ -183,8 +270,9 @@ AccessContext.prototype.debug = function() {
|
|||
* This class represents the abstract notion of a principal, which can be used
|
||||
* to represent any entity, such as an individual, a corporation, and a login id
|
||||
* @param {String} type The principal type
|
||||
* @param {*} id The princiapl id
|
||||
* @param {*} id The principal id
|
||||
* @param {String} [name] The principal name
|
||||
* @param {String} modelName The principal model name
|
||||
* @returns {Principal}
|
||||
* @class
|
||||
*/
|
||||
|
@ -217,32 +305,44 @@ Principal.prototype.equals = function(p) {
|
|||
|
||||
/**
|
||||
* A request to access protected resources.
|
||||
* @param {String} model The model name
|
||||
* @param {String} property
|
||||
*
|
||||
* The method can either be called with the following signature or with a single
|
||||
* argument: an AccessRequest instance or an object containing all the required properties.
|
||||
*
|
||||
* @class
|
||||
* @options {String|AccessRequest|Object} model|req The model name,<br>
|
||||
* or an AccessRequest instance/object.
|
||||
* @param {String} property The property/method/relation name
|
||||
* @param {String} accessType The access type
|
||||
* @param {String} permission The requested permission
|
||||
* @param {String[]} methodNames The names of involved methods
|
||||
* @param {Registry} registry The application or global registry
|
||||
* @returns {AccessRequest}
|
||||
* @class
|
||||
*/
|
||||
function AccessRequest(model, property, accessType, permission, methodNames) {
|
||||
function AccessRequest(model, property, accessType, permission, methodNames, registry) {
|
||||
if (!(this instanceof AccessRequest)) {
|
||||
return new AccessRequest(model, property, accessType);
|
||||
return new AccessRequest(model, property, accessType, permission, methodNames);
|
||||
}
|
||||
if (arguments.length === 1 && typeof model === 'object') {
|
||||
// The argument is an object that contains all required properties
|
||||
var obj = model || {};
|
||||
const obj = model || {};
|
||||
this.model = obj.model || AccessContext.ALL;
|
||||
this.property = obj.property || AccessContext.ALL;
|
||||
this.accessType = obj.accessType || AccessContext.ALL;
|
||||
this.permission = obj.permission || AccessContext.DEFAULT;
|
||||
this.methodNames = methodNames || [];
|
||||
this.methodNames = obj.methodNames || [];
|
||||
this.registry = obj.registry;
|
||||
} else {
|
||||
this.model = model || AccessContext.ALL;
|
||||
this.property = property || AccessContext.ALL;
|
||||
this.accessType = accessType || AccessContext.ALL;
|
||||
this.permission = permission || AccessContext.DEFAULT;
|
||||
this.methodNames = methodNames || [];
|
||||
this.registry = registry;
|
||||
}
|
||||
// do not create AccessRequest without a registry
|
||||
assert(this.registry,
|
||||
'Application registry is mandatory in AccessRequest but missing in provided argument(s)');
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -263,10 +363,10 @@ AccessRequest.prototype.isWildcard = function() {
|
|||
*/
|
||||
|
||||
AccessRequest.prototype.exactlyMatches = function(acl) {
|
||||
var matchesModel = acl.model === this.model;
|
||||
var matchesProperty = acl.property === this.property;
|
||||
var matchesMethodName = this.methodNames.indexOf(acl.property) !== -1;
|
||||
var matchesAccessType = acl.accessType === this.accessType;
|
||||
const matchesModel = acl.model === this.model;
|
||||
const matchesProperty = acl.property === this.property;
|
||||
const matchesMethodName = this.methodNames.indexOf(acl.property) !== -1;
|
||||
const matchesAccessType = acl.accessType === this.accessType;
|
||||
|
||||
if (matchesModel && matchesAccessType) {
|
||||
return matchesProperty || matchesMethodName;
|
||||
|
@ -275,6 +375,28 @@ AccessRequest.prototype.exactlyMatches = function(acl) {
|
|||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Settle the accessRequest's permission if DEFAULT
|
||||
* In most situations, the default permission can be resolved from the nested model
|
||||
* config. An default permission can also be explicitly provided to override it or
|
||||
* cope with AccessRequest instances without a nested model (e.g. model is '*')
|
||||
*
|
||||
* @param {String} defaultPermission (optional) the default permission to apply
|
||||
*/
|
||||
|
||||
AccessRequest.prototype.settleDefaultPermission = function(defaultPermission) {
|
||||
if (this.permission !== 'DEFAULT')
|
||||
return;
|
||||
|
||||
const modelName = this.model;
|
||||
if (!defaultPermission) {
|
||||
const modelClass = this.registry.findModel(modelName);
|
||||
defaultPermission = modelClass && modelClass.settings.defaultPermission;
|
||||
}
|
||||
|
||||
this.permission = defaultPermission || 'ALLOW';
|
||||
};
|
||||
|
||||
/**
|
||||
* Is the request for access allowed?
|
||||
*
|
||||
|
@ -300,3 +422,4 @@ AccessRequest.prototype.debug = function() {
|
|||
module.exports.AccessContext = AccessContext;
|
||||
module.exports.Principal = Principal;
|
||||
module.exports.AccessRequest = AccessRequest;
|
||||
module.exports.DEFAULT_SCOPES = DEFAULT_SCOPES;
|
||||
|
|
|
@ -1,17 +1,24 @@
|
|||
// Copyright IBM Corp. 2013,2019. All Rights Reserved.
|
||||
// Node module: loopback
|
||||
// This file is licensed under the MIT License.
|
||||
// License text available at https://opensource.org/licenses/MIT
|
||||
|
||||
/*!
|
||||
* Module dependencies.
|
||||
*/
|
||||
|
||||
var DataSource = require('loopback-datasource-juggler').DataSource;
|
||||
var Registry = require('./registry');
|
||||
var assert = require('assert');
|
||||
var fs = require('fs');
|
||||
var extend = require('util')._extend;
|
||||
var RemoteObjects = require('strong-remoting');
|
||||
var classify = require('underscore.string/classify');
|
||||
var camelize = require('underscore.string/camelize');
|
||||
var path = require('path');
|
||||
var util = require('util');
|
||||
'use strict';
|
||||
const g = require('./globalize');
|
||||
const DataSource = require('loopback-datasource-juggler').DataSource;
|
||||
const Registry = require('./registry');
|
||||
const assert = require('assert');
|
||||
const fs = require('fs');
|
||||
const extend = require('util')._extend;
|
||||
const RemoteObjects = require('strong-remoting');
|
||||
const classify = require('underscore.string/classify');
|
||||
const camelize = require('underscore.string/camelize');
|
||||
const path = require('path');
|
||||
const util = require('util');
|
||||
|
||||
/**
|
||||
* The `App` object represents a Loopback application.
|
||||
|
@ -42,7 +49,7 @@ function App() {
|
|||
* Export the app prototype.
|
||||
*/
|
||||
|
||||
var app = module.exports = {};
|
||||
const app = module.exports = {};
|
||||
|
||||
/**
|
||||
* Lazily load a set of [remote objects](http://apidocs.strongloop.com/strong-remoting/#remoteobjectsoptions).
|
||||
|
@ -55,7 +62,7 @@ app.remotes = function() {
|
|||
if (this._remotes) {
|
||||
return this._remotes;
|
||||
} else {
|
||||
var options = {};
|
||||
let options = {};
|
||||
|
||||
if (this.get) {
|
||||
options = this.get('remoting');
|
||||
|
@ -71,7 +78,7 @@ app.remotes = function() {
|
|||
|
||||
app.disuse = function(route) {
|
||||
if (this.stack) {
|
||||
for (var i = 0; i < this.stack.length; i++) {
|
||||
for (let i = 0; i < this.stack.length; i++) {
|
||||
if (this.stack[i].route === route) {
|
||||
this.stack.splice(i, 1);
|
||||
}
|
||||
|
@ -94,7 +101,7 @@ app.disuse = function(route) {
|
|||
* app.model(User, { dataSource: 'db' });
|
||||
*```
|
||||
*
|
||||
* @param {Object|String} Model The model to attach.
|
||||
* @param {Object} Model The model to attach.
|
||||
* @options {Object} config The model's configuration.
|
||||
* @property {String|DataSource} dataSource The `DataSource` to which to attach the model.
|
||||
* @property {Boolean} [public] Whether the model should be exposed via REST API.
|
||||
|
@ -104,32 +111,18 @@ app.disuse = function(route) {
|
|||
*/
|
||||
|
||||
app.model = function(Model, config) {
|
||||
var isPublic = true;
|
||||
var registry = this.registry;
|
||||
let isPublic = true;
|
||||
const registry = this.registry;
|
||||
|
||||
if (typeof Model === 'string') {
|
||||
const msg = 'app.model(modelName, settings) is no longer supported. ' +
|
||||
'Use app.registry.createModel(modelName, definition) and ' +
|
||||
'app.model(ModelCtor, config) instead.';
|
||||
throw new Error(msg);
|
||||
}
|
||||
|
||||
if (arguments.length > 1) {
|
||||
config = config || {};
|
||||
if (typeof Model === 'string') {
|
||||
// create & attach the model - backwards compatibility
|
||||
|
||||
// create config for loopback.modelFromConfig
|
||||
var modelConfig = extend({}, config);
|
||||
modelConfig.options = extend({}, config.options);
|
||||
modelConfig.name = Model;
|
||||
|
||||
// modeller does not understand `dataSource` option
|
||||
delete modelConfig.dataSource;
|
||||
|
||||
Model = registry.createModel(modelConfig);
|
||||
|
||||
// delete config options already applied
|
||||
['relations', 'base', 'acls', 'hidden', 'methods'].forEach(function(prop) {
|
||||
delete config[prop];
|
||||
if (config.options) delete config.options[prop];
|
||||
});
|
||||
delete config.properties;
|
||||
}
|
||||
|
||||
configureModel(Model, config, this);
|
||||
isPublic = config.public !== false;
|
||||
} else {
|
||||
|
@ -137,7 +130,7 @@ app.model = function(Model, config) {
|
|||
Model.modelName + ' must be a descendant of loopback.Model');
|
||||
}
|
||||
|
||||
var modelName = Model.modelName;
|
||||
const modelName = Model.modelName;
|
||||
this.models[modelName] =
|
||||
this.models[classify(modelName)] =
|
||||
this.models[camelize(modelName)] = Model;
|
||||
|
@ -145,6 +138,9 @@ app.model = function(Model, config) {
|
|||
this.models().push(Model);
|
||||
|
||||
if (isPublic && Model.sharedClass) {
|
||||
this.remotes().defineObjectType(Model.modelName, function(data) {
|
||||
return new Model(data);
|
||||
});
|
||||
this.remotes().addClass(Model.sharedClass);
|
||||
if (Model.settings.trackChanges && Model.Change) {
|
||||
this.remotes().addClass(Model.Change.sharedClass);
|
||||
|
@ -153,12 +149,60 @@ app.model = function(Model, config) {
|
|||
this.emit('modelRemoted', Model.sharedClass);
|
||||
}
|
||||
|
||||
const self = this;
|
||||
Model.on('remoteMethodDisabled', function(model, methodName) {
|
||||
clearHandlerCache(self);
|
||||
self.emit('remoteMethodDisabled', model, methodName);
|
||||
});
|
||||
Model.on('remoteMethodAdded', function(model) {
|
||||
clearHandlerCache(self);
|
||||
self.emit('remoteMethodAdded', model);
|
||||
});
|
||||
|
||||
Model.shared = isPublic;
|
||||
Model.app = this;
|
||||
Model.emit('attached', this);
|
||||
return Model;
|
||||
};
|
||||
|
||||
/**
|
||||
* Remove all references to a previously registered Model.
|
||||
*
|
||||
* The method emits "modelDeleted" event as a counter-part to "modelRemoted"
|
||||
* event.
|
||||
*
|
||||
* @param {String} modelName The name of the model to remove.
|
||||
*/
|
||||
app.deleteModelByName = function(modelName) {
|
||||
const ModelCtor = this.models[modelName];
|
||||
delete this.models[modelName];
|
||||
delete this.models[classify(modelName)];
|
||||
delete this.models[camelize(modelName)];
|
||||
|
||||
if (ModelCtor) {
|
||||
ModelCtor.removeAllListeners();
|
||||
|
||||
const ix = this._models.indexOf(ModelCtor);
|
||||
if (ix > -1) {
|
||||
this._models.splice(ix, 1);
|
||||
}
|
||||
}
|
||||
|
||||
const remotes = this.remotes();
|
||||
remotes.deleteClassByName(modelName);
|
||||
remotes.deleteTypeByName(modelName);
|
||||
|
||||
if (ModelCtor && ModelCtor.dataSource) {
|
||||
ModelCtor.dataSource.deleteModelByName(modelName);
|
||||
} else {
|
||||
this.registry.modelBuilder.deleteModelByName(modelName);
|
||||
}
|
||||
|
||||
clearHandlerCache(this);
|
||||
|
||||
this.emit('modelDeleted', ModelCtor || modelName);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the models exported by the app. Returns only models defined using `app.model()`
|
||||
*
|
||||
|
@ -174,7 +218,7 @@ app.model = function(Model, config) {
|
|||
* });
|
||||
* ```
|
||||
*
|
||||
* 2. Use `app.model` to access a model by name.
|
||||
* 2. Use `app.models` to access a model by name.
|
||||
* `app.models` has properties for all defined models.
|
||||
*
|
||||
* The following example illustrates accessing the `Product` and `CustomerReceipt` models
|
||||
|
@ -189,8 +233,10 @@ app.model = function(Model, config) {
|
|||
* }
|
||||
* });
|
||||
*
|
||||
* app.model('product', {dataSource: 'db'});
|
||||
* app.model('customer-receipt', {dataSource: 'db'});
|
||||
* var productModel = app.registry.createModel('product');
|
||||
* app.model(productModel, {dataSource: 'db'});
|
||||
* var customerReceiptModel = app.registry.createModel('customer-receipt');
|
||||
* app.model(customerReceiptModel, {dataSource: 'db'});
|
||||
*
|
||||
* // available based on the given name
|
||||
* var Product = app.models.Product;
|
||||
|
@ -219,11 +265,20 @@ app.models = function() {
|
|||
* @param {Object} config The data source config
|
||||
*/
|
||||
app.dataSource = function(name, config) {
|
||||
var ds = dataSourcesFromConfig(name, config, this.connectors, this.registry);
|
||||
this.dataSources[name] =
|
||||
this.dataSources[classify(name)] =
|
||||
this.dataSources[camelize(name)] = ds;
|
||||
return ds;
|
||||
try {
|
||||
const ds = dataSourcesFromConfig(name, config, this.connectors, this.registry);
|
||||
this.dataSources[name] =
|
||||
this.dataSources[classify(name)] =
|
||||
this.dataSources[camelize(name)] = ds;
|
||||
ds.app = this;
|
||||
return ds;
|
||||
} catch (err) {
|
||||
if (err.message) {
|
||||
err.message = g.f('Cannot create data source %s: %s',
|
||||
JSON.stringify(name), err.message);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -252,7 +307,7 @@ app.connector = function(name, connector) {
|
|||
*/
|
||||
|
||||
app.remoteObjects = function() {
|
||||
var result = {};
|
||||
const result = {};
|
||||
|
||||
this.remotes().classes().forEach(function(sharedClass) {
|
||||
result[sharedClass.name] = sharedClass.ctor;
|
||||
|
@ -267,13 +322,13 @@ app.remoteObjects = function() {
|
|||
*/
|
||||
|
||||
app.handler = function(type, options) {
|
||||
var handlers = this._handlers || (this._handlers = {});
|
||||
const handlers = this._handlers || (this._handlers = {});
|
||||
if (handlers[type]) {
|
||||
return handlers[type];
|
||||
}
|
||||
|
||||
var remotes = this.remotes();
|
||||
var handler = this._handlers[type] = remotes.handler(type, options);
|
||||
const remotes = this.remotes();
|
||||
const handler = this._handlers[type] = remotes.handler(type, options);
|
||||
|
||||
remotes.classes().forEach(function(sharedClass) {
|
||||
sharedClass.ctor.emit('mounted', app, sharedClass, remotes);
|
||||
|
@ -293,53 +348,56 @@ app.dataSources = app.datasources = {};
|
|||
*/
|
||||
|
||||
app.enableAuth = function(options) {
|
||||
var AUTH_MODELS = ['User', 'AccessToken', 'ACL', 'Role', 'RoleMapping'];
|
||||
const AUTH_MODELS = ['User', 'AccessToken', 'ACL', 'Role', 'RoleMapping'];
|
||||
|
||||
var remotes = this.remotes();
|
||||
var app = this;
|
||||
const remotes = this.remotes();
|
||||
const app = this;
|
||||
|
||||
if (options && options.dataSource) {
|
||||
var appModels = app.registry.modelBuilder.models;
|
||||
const appModels = app.registry.modelBuilder.models;
|
||||
AUTH_MODELS.forEach(function(m) {
|
||||
var Model = app.registry.findModel(m);
|
||||
const Model = app.registry.findModel(m);
|
||||
if (!Model) {
|
||||
throw new Error(
|
||||
'Authentication requires model ' + m + ' to be defined.');
|
||||
g.f('Authentication requires model %s to be defined.', m),
|
||||
);
|
||||
}
|
||||
|
||||
if (m.dataSource || m.app) return;
|
||||
if (Model.dataSource || Model.app) return;
|
||||
|
||||
for (var name in appModels) {
|
||||
var candidate = appModels[name];
|
||||
var isSubclass = candidate.prototype instanceof Model;
|
||||
var isAttached = !!candidate.dataSource || !!candidate.app;
|
||||
// Find descendants of Model that are attached,
|
||||
// for example "Customer" extending "User" model
|
||||
for (const name in appModels) {
|
||||
const candidate = appModels[name];
|
||||
const isSubclass = candidate.prototype instanceof Model;
|
||||
const isAttached = !!candidate.dataSource || !!candidate.app;
|
||||
if (isSubclass && isAttached) return;
|
||||
}
|
||||
|
||||
app.model(Model, {
|
||||
dataSource: options.dataSource,
|
||||
public: m === 'User'
|
||||
public: m === 'User',
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
remotes.authorization = function(ctx, next) {
|
||||
var method = ctx.method;
|
||||
var req = ctx.req;
|
||||
var Model = method.ctor;
|
||||
var modelInstance = ctx.instance;
|
||||
const method = ctx.method;
|
||||
const req = ctx.req;
|
||||
const Model = method.ctor;
|
||||
const modelInstance = ctx.instance;
|
||||
|
||||
var modelId = modelInstance && modelInstance.id ||
|
||||
const modelId = modelInstance && modelInstance.id ||
|
||||
// replacement for deprecated req.param()
|
||||
(req.params && req.params.id !== undefined ? req.params.id :
|
||||
req.body && req.body.id !== undefined ? req.body.id :
|
||||
req.query && req.query.id !== undefined ? req.query.id :
|
||||
undefined);
|
||||
req.body && req.body.id !== undefined ? req.body.id :
|
||||
req.query && req.query.id !== undefined ? req.query.id :
|
||||
undefined);
|
||||
|
||||
var modelName = Model.modelName;
|
||||
const modelName = Model.modelName;
|
||||
|
||||
var modelSettings = Model.settings || {};
|
||||
var errStatusCode = modelSettings.aclErrorStatus || app.get('aclErrorStatus') || 401;
|
||||
const modelSettings = Model.settings || {};
|
||||
let errStatusCode = modelSettings.aclErrorStatus || app.get('aclErrorStatus') || 401;
|
||||
if (!req.accessToken) {
|
||||
errStatusCode = 401;
|
||||
}
|
||||
|
@ -357,69 +415,166 @@ app.enableAuth = function(options) {
|
|||
} else if (allowed) {
|
||||
next();
|
||||
} else {
|
||||
|
||||
var messages = {
|
||||
const messages = {
|
||||
403: {
|
||||
message: 'Access Denied',
|
||||
code: 'ACCESS_DENIED'
|
||||
message: g.f('Access Denied'),
|
||||
code: 'ACCESS_DENIED',
|
||||
},
|
||||
404: {
|
||||
message: ('could not find ' + modelName + ' with id ' + modelId),
|
||||
code: 'MODEL_NOT_FOUND'
|
||||
message: (g.f('could not find %s with id %s', modelName, modelId)),
|
||||
code: 'MODEL_NOT_FOUND',
|
||||
},
|
||||
401: {
|
||||
message: 'Authorization Required',
|
||||
code: 'AUTHORIZATION_REQUIRED'
|
||||
}
|
||||
message: g.f('Authorization Required'),
|
||||
code: 'AUTHORIZATION_REQUIRED',
|
||||
},
|
||||
};
|
||||
|
||||
var e = new Error(messages[errStatusCode].message || messages[403].message);
|
||||
const e = new Error(messages[errStatusCode].message || messages[403].message);
|
||||
e.statusCode = errStatusCode;
|
||||
e.code = messages[errStatusCode].code || messages[403].code;
|
||||
next(e);
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
};
|
||||
|
||||
this._verifyAuthModelRelations();
|
||||
|
||||
this.isAuthEnabled = true;
|
||||
};
|
||||
|
||||
app._verifyAuthModelRelations = function() {
|
||||
// Allow unit-tests (but also LoopBack users) to disable the warnings
|
||||
if (this.get('_verifyAuthModelRelations') === false) return;
|
||||
|
||||
const AccessToken = this.registry.findModel('AccessToken');
|
||||
const User = this.registry.findModel('User');
|
||||
this.models().forEach(Model => {
|
||||
if (Model === AccessToken || Model.prototype instanceof AccessToken) {
|
||||
scheduleVerification(Model, verifyAccessTokenRelations);
|
||||
}
|
||||
|
||||
if (Model === User || Model.prototype instanceof User) {
|
||||
scheduleVerification(Model, verifyUserRelations);
|
||||
}
|
||||
});
|
||||
|
||||
function scheduleVerification(Model, verifyFn) {
|
||||
if (Model.dataSource) {
|
||||
verifyFn(Model);
|
||||
} else {
|
||||
Model.on('attached', () => verifyFn(Model));
|
||||
}
|
||||
}
|
||||
|
||||
function verifyAccessTokenRelations(Model) {
|
||||
const belongsToUser = Model.relations && Model.relations.user;
|
||||
if (belongsToUser) return;
|
||||
|
||||
const relationsConfig = Model.settings.relations || {};
|
||||
const userName = (relationsConfig.user || {}).model;
|
||||
if (userName) {
|
||||
console.warn(
|
||||
'The model %j configures "belongsTo User-like models" relation ' +
|
||||
'with target model %j. However, the model %j is not attached to ' +
|
||||
'the application and therefore cannot be used by this relation. ' +
|
||||
'This typically happens when the application has a custom ' +
|
||||
'custom User subclass, but does not fix AccessToken relations ' +
|
||||
'to use this new model.\n' +
|
||||
'Learn more at http://ibm.biz/setup-loopback-auth',
|
||||
Model.modelName, userName, userName,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
console.warn(
|
||||
'The model %j does not have "belongsTo User-like model" relation ' +
|
||||
'configured.\n' +
|
||||
'Learn more at http://ibm.biz/setup-loopback-auth',
|
||||
Model.modelName,
|
||||
);
|
||||
}
|
||||
|
||||
function verifyUserRelations(Model) {
|
||||
const hasManyTokens = Model.relations && Model.relations.accessTokens;
|
||||
|
||||
if (hasManyTokens) {
|
||||
// display a temp warning message for users using multiple users config
|
||||
if (hasManyTokens.polymorphic) {
|
||||
console.warn(
|
||||
'The app configuration follows the multiple user models setup ' +
|
||||
'as described in http://ibm.biz/setup-loopback-auth',
|
||||
'The built-in role resolver $owner is not currently compatible ' +
|
||||
'with this configuration and should not be used in production.',
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const relationsConfig = Model.settings.relations || {};
|
||||
const accessTokenName = (relationsConfig.accessTokens || {}).model;
|
||||
if (accessTokenName) {
|
||||
console.warn(
|
||||
'The model %j configures "hasMany AccessToken-like models" relation ' +
|
||||
'with target model %j. However, the model %j is not attached to ' +
|
||||
'the application and therefore cannot be used by this relation. ' +
|
||||
'This typically happens when the application has a custom ' +
|
||||
'AccessToken subclass, but does not fix User relations to use this ' +
|
||||
'new model.\n' +
|
||||
'Learn more at http://ibm.biz/setup-loopback-auth',
|
||||
Model.modelName, accessTokenName, accessTokenName,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
console.warn(
|
||||
'The model %j does not have "hasMany AccessToken-like models" relation ' +
|
||||
'configured.\n' +
|
||||
'Learn more at http://ibm.biz/setup-loopback-auth',
|
||||
Model.modelName,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
app.boot = function(options) {
|
||||
throw new Error(
|
||||
'`app.boot` was removed, use the new module loopback-boot instead');
|
||||
g.f('{{`app.boot`}} was removed, use the new module {{loopback-boot}} instead'),
|
||||
);
|
||||
};
|
||||
|
||||
function dataSourcesFromConfig(name, config, connectorRegistry, registry) {
|
||||
var connectorPath;
|
||||
let connectorPath;
|
||||
|
||||
assert(typeof config === 'object',
|
||||
'cannont create data source without config object');
|
||||
'can not create data source without config object');
|
||||
|
||||
if (typeof config.connector === 'string') {
|
||||
name = config.connector;
|
||||
if (connectorRegistry[name]) {
|
||||
config.connector = connectorRegistry[name];
|
||||
const connectorName = config.connector;
|
||||
if (connectorRegistry[connectorName]) {
|
||||
config.connector = connectorRegistry[connectorName];
|
||||
} else {
|
||||
connectorPath = path.join(__dirname, 'connectors', name + '.js');
|
||||
connectorPath = path.join(__dirname, 'connectors', connectorName + '.js');
|
||||
|
||||
if (fs.existsSync(connectorPath)) {
|
||||
config.connector = require(connectorPath);
|
||||
}
|
||||
}
|
||||
if (config.connector && typeof config.connector === 'object' && !config.connector.name)
|
||||
config.connector.name = connectorName;
|
||||
}
|
||||
|
||||
return registry.createDataSource(config);
|
||||
return registry.createDataSource(name, config);
|
||||
}
|
||||
|
||||
function configureModel(ModelCtor, config, app) {
|
||||
assert(ModelCtor.prototype instanceof ModelCtor.registry.getModel('Model'),
|
||||
ModelCtor.modelName + ' must be a descendant of loopback.Model');
|
||||
|
||||
var dataSource = config.dataSource;
|
||||
let dataSource = config.dataSource;
|
||||
|
||||
if (dataSource) {
|
||||
if (typeof dataSource === 'string') {
|
||||
|
@ -429,61 +584,16 @@ function configureModel(ModelCtor, config, app) {
|
|||
assert(
|
||||
dataSource instanceof DataSource,
|
||||
ModelCtor.modelName + ' is referencing a dataSource that does not exist: "' +
|
||||
config.dataSource + '"'
|
||||
config.dataSource + '"',
|
||||
);
|
||||
}
|
||||
|
||||
config = extend({}, config);
|
||||
config.dataSource = dataSource;
|
||||
|
||||
setSharedMethodSharedProperties(ModelCtor, app, config);
|
||||
|
||||
app.registry.configureModel(ModelCtor, config);
|
||||
}
|
||||
|
||||
function setSharedMethodSharedProperties(model, app, modelConfigs) {
|
||||
var settings = {};
|
||||
|
||||
// apply config.json settings
|
||||
var config = app.get('remoting');
|
||||
var configHasSharedMethodsSettings = config &&
|
||||
config.sharedMethods &&
|
||||
typeof config.sharedMethods === 'object';
|
||||
if (configHasSharedMethodsSettings)
|
||||
util._extend(settings, config.sharedMethods);
|
||||
|
||||
// apply model-config.json settings
|
||||
var modelConfig = modelConfigs.options;
|
||||
var modelConfigHasSharedMethodsSettings = modelConfig &&
|
||||
modelConfig.remoting &&
|
||||
modelConfig.remoting.sharedMethods &&
|
||||
typeof modelConfig.remoting.sharedMethods === 'object';
|
||||
if (modelConfigHasSharedMethodsSettings)
|
||||
util._extend(settings, modelConfig.remoting.sharedMethods);
|
||||
|
||||
// validate setting values
|
||||
Object.keys(settings).forEach(function(setting) {
|
||||
var settingValue = settings[setting];
|
||||
var settingValueType = typeof settingValue;
|
||||
if (settingValueType !== 'boolean')
|
||||
throw new TypeError('Expected boolean, got ' + settingValueType);
|
||||
});
|
||||
|
||||
// set sharedMethod.shared using the merged settings
|
||||
var sharedMethods = model.sharedClass.methods({includeDisabled: true});
|
||||
sharedMethods.forEach(function(sharedMethod) {
|
||||
// use the specific setting if it exists
|
||||
var hasSpecificSetting = settings.hasOwnProperty(sharedMethod.name);
|
||||
if (hasSpecificSetting) {
|
||||
sharedMethod.shared = settings[sharedMethod.name];
|
||||
} else { // otherwise, use the default setting if it exists
|
||||
var hasDefaultSetting = settings.hasOwnProperty('*');
|
||||
if (hasDefaultSetting)
|
||||
sharedMethod.shared = settings['*'];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function clearHandlerCache(app) {
|
||||
app._handlers = undefined;
|
||||
}
|
||||
|
@ -517,15 +627,15 @@ function clearHandlerCache(app) {
|
|||
* as the request handler.
|
||||
*/
|
||||
app.listen = function(cb) {
|
||||
var self = this;
|
||||
const self = this;
|
||||
|
||||
var server = require('http').createServer(this);
|
||||
const server = require('http').createServer(this);
|
||||
|
||||
server.on('listening', function() {
|
||||
self.set('port', this.address().port);
|
||||
|
||||
var listeningOnAll = false;
|
||||
var host = self.get('host');
|
||||
let listeningOnAll = false;
|
||||
let host = self.get('host');
|
||||
if (!host) {
|
||||
listeningOnAll = true;
|
||||
host = this.address().address;
|
||||
|
@ -535,23 +645,26 @@ app.listen = function(cb) {
|
|||
}
|
||||
|
||||
if (!self.get('url')) {
|
||||
if (process.platform === 'win32' && listeningOnAll) {
|
||||
// Windows browsers don't support `0.0.0.0` host in the URL
|
||||
if (listeningOnAll) {
|
||||
// We are replacing it with localhost to build a URL
|
||||
// that can be copied and pasted into the browser.
|
||||
host = 'localhost';
|
||||
}
|
||||
var url = 'http://' + host + ':' + self.get('port') + '/';
|
||||
const url = 'http://' + host + ':' + self.get('port') + '/';
|
||||
self.set('url', url);
|
||||
}
|
||||
});
|
||||
|
||||
var useAppConfig =
|
||||
const useAppConfig =
|
||||
arguments.length === 0 ||
|
||||
(arguments.length == 1 && typeof arguments[0] == 'function');
|
||||
|
||||
if (useAppConfig) {
|
||||
server.listen(this.get('port'), this.get('host'), cb);
|
||||
let port = this.get('port');
|
||||
// NOTE(bajtos) port:undefined no longer works on node@6,
|
||||
// we must pass port:0 explicitly
|
||||
if (port === undefined) port = 0;
|
||||
server.listen(port, this.get('host'), cb);
|
||||
} else {
|
||||
server.listen.apply(server, arguments);
|
||||
}
|
||||
|
|
|
@ -1,5 +1,11 @@
|
|||
var EventEmitter = require('events').EventEmitter;
|
||||
var util = require('util');
|
||||
// Copyright IBM Corp. 2014,2019. All Rights Reserved.
|
||||
// Node module: loopback
|
||||
// This file is licensed under the MIT License.
|
||||
// License text available at https://opensource.org/licenses/MIT
|
||||
|
||||
'use strict';
|
||||
const EventEmitter = require('events').EventEmitter;
|
||||
const util = require('util');
|
||||
|
||||
module.exports = browserExpress;
|
||||
|
||||
|
|
|
@ -1,68 +1,98 @@
|
|||
// Copyright IBM Corp. 2014,2019. All Rights Reserved.
|
||||
// Node module: loopback
|
||||
// This file is licensed under the MIT License.
|
||||
// License text available at https://opensource.org/licenses/MIT
|
||||
|
||||
'use strict';
|
||||
|
||||
const assert = require('assert');
|
||||
|
||||
module.exports = function(registry) {
|
||||
// NOTE(bajtos) we must use static require() due to browserify limitations
|
||||
|
||||
registry.KeyValueModel = createModel(
|
||||
require('../common/models/key-value-model.json'),
|
||||
require('../common/models/key-value-model.js'),
|
||||
);
|
||||
|
||||
registry.Email = createModel(
|
||||
require('../common/models/email.json'),
|
||||
require('../common/models/email.js'));
|
||||
require('../common/models/email.js'),
|
||||
);
|
||||
|
||||
registry.Application = createModel(
|
||||
require('../common/models/application.json'),
|
||||
require('../common/models/application.js'));
|
||||
require('../common/models/application.js'),
|
||||
);
|
||||
|
||||
registry.AccessToken = createModel(
|
||||
require('../common/models/access-token.json'),
|
||||
require('../common/models/access-token.js'));
|
||||
require('../common/models/access-token.js'),
|
||||
);
|
||||
|
||||
registry.User = createModel(
|
||||
require('../common/models/user.json'),
|
||||
require('../common/models/user.js'));
|
||||
require('../common/models/user.js'),
|
||||
);
|
||||
|
||||
registry.RoleMapping = createModel(
|
||||
require('../common/models/role-mapping.json'),
|
||||
require('../common/models/role-mapping.js'));
|
||||
require('../common/models/role-mapping.js'),
|
||||
);
|
||||
|
||||
registry.Role = createModel(
|
||||
require('../common/models/role.json'),
|
||||
require('../common/models/role.js'));
|
||||
require('../common/models/role.js'),
|
||||
);
|
||||
|
||||
registry.ACL = createModel(
|
||||
require('../common/models/acl.json'),
|
||||
require('../common/models/acl.js'));
|
||||
require('../common/models/acl.js'),
|
||||
);
|
||||
|
||||
registry.Scope = createModel(
|
||||
require('../common/models/scope.json'),
|
||||
require('../common/models/scope.js'));
|
||||
require('../common/models/scope.js'),
|
||||
);
|
||||
|
||||
registry.Change = createModel(
|
||||
require('../common/models/change.json'),
|
||||
require('../common/models/change.js'));
|
||||
require('../common/models/change.js'),
|
||||
);
|
||||
|
||||
registry.Checkpoint = createModel(
|
||||
require('../common/models/checkpoint.json'),
|
||||
require('../common/models/checkpoint.js'));
|
||||
|
||||
/*!
|
||||
* Automatically attach these models to dataSources
|
||||
*/
|
||||
|
||||
var dataSourceTypes = {
|
||||
DB: 'db',
|
||||
MAIL: 'mail'
|
||||
};
|
||||
|
||||
registry.Email.autoAttach = dataSourceTypes.MAIL;
|
||||
registry.getModel('PersistedModel').autoAttach = dataSourceTypes.DB;
|
||||
registry.User.autoAttach = dataSourceTypes.DB;
|
||||
registry.AccessToken.autoAttach = dataSourceTypes.DB;
|
||||
registry.Role.autoAttach = dataSourceTypes.DB;
|
||||
registry.RoleMapping.autoAttach = dataSourceTypes.DB;
|
||||
registry.ACL.autoAttach = dataSourceTypes.DB;
|
||||
registry.Scope.autoAttach = dataSourceTypes.DB;
|
||||
registry.Application.autoAttach = dataSourceTypes.DB;
|
||||
require('../common/models/checkpoint.js'),
|
||||
);
|
||||
|
||||
function createModel(definitionJson, customizeFn) {
|
||||
var Model = registry.createModel(definitionJson);
|
||||
// Clone the JSON definition to allow applications
|
||||
// to modify model settings while not affecting
|
||||
// settings of new models created in the local registry
|
||||
// of another app.
|
||||
// This is needed because require() always returns the same
|
||||
// object instance it loaded during the first call.
|
||||
definitionJson = cloneDeepJson(definitionJson);
|
||||
|
||||
const Model = registry.createModel(definitionJson);
|
||||
customizeFn(Model);
|
||||
return Model;
|
||||
}
|
||||
};
|
||||
|
||||
// Because we are cloning objects created by JSON.parse,
|
||||
// the cloning algorithm can stay much simpler than a general-purpose
|
||||
// "cloneDeep" e.g. from lodash.
|
||||
function cloneDeepJson(obj) {
|
||||
const result = Array.isArray(obj) ? [] : {};
|
||||
assert.equal(Object.getPrototypeOf(result), Object.getPrototypeOf(obj));
|
||||
for (const key in obj) {
|
||||
const value = obj[key];
|
||||
if (typeof value === 'object') {
|
||||
result[key] = cloneDeepJson(value);
|
||||
} else {
|
||||
result[key] = value;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,80 @@
|
|||
// Copyright IBM Corp. 2017,2019. All Rights Reserved.
|
||||
// Node module: loopback
|
||||
// This file is licensed under the MIT License.
|
||||
// License text available at https://opensource.org/licenses/MIT
|
||||
|
||||
'use strict';
|
||||
|
||||
const util = require('util');
|
||||
const extend = require('util')._extend;
|
||||
const g = require('./globalize');
|
||||
|
||||
module.exports = function(modelCtor, remotingConfig, modelConfig) {
|
||||
const settings = {};
|
||||
|
||||
// apply config.json settings
|
||||
const configHasSharedMethodsSettings = remotingConfig &&
|
||||
remotingConfig.sharedMethods &&
|
||||
typeof remotingConfig.sharedMethods === 'object';
|
||||
if (configHasSharedMethodsSettings)
|
||||
util._extend(settings, remotingConfig.sharedMethods);
|
||||
|
||||
// apply model-config.json settings
|
||||
const options = modelConfig.options;
|
||||
const modelConfigHasSharedMethodsSettings = options &&
|
||||
options.remoting &&
|
||||
options.remoting.sharedMethods &&
|
||||
typeof options.remoting.sharedMethods === 'object';
|
||||
if (modelConfigHasSharedMethodsSettings)
|
||||
util._extend(settings, options.remoting.sharedMethods);
|
||||
|
||||
// validate setting values
|
||||
Object.keys(settings).forEach(function(setting) {
|
||||
const settingValue = settings[setting];
|
||||
const settingValueType = typeof settingValue;
|
||||
if (settingValueType !== 'boolean')
|
||||
throw new TypeError(g.f('Expected boolean, got %s', settingValueType));
|
||||
});
|
||||
|
||||
// set sharedMethod.shared using the merged settings
|
||||
const sharedMethods = modelCtor.sharedClass.methods({includeDisabled: true});
|
||||
|
||||
// re-map glob style values to regular expressions
|
||||
const tests = Object
|
||||
.keys(settings)
|
||||
.filter(function(setting) {
|
||||
return settings.hasOwnProperty(setting) && setting.indexOf('*') >= 0;
|
||||
})
|
||||
.map(function(setting) {
|
||||
// Turn * into an testable regexp string
|
||||
const glob = escapeRegExp(setting).replace(/\*/g, '(.)*');
|
||||
return {regex: new RegExp(glob), setting: settings[setting]};
|
||||
}) || [];
|
||||
sharedMethods.forEach(function(sharedMethod) {
|
||||
// use the specific setting if it exists
|
||||
const methodName = sharedMethod.isStatic ? sharedMethod.name : 'prototype.' + sharedMethod.name;
|
||||
const hasSpecificSetting = settings.hasOwnProperty(methodName);
|
||||
if (hasSpecificSetting) {
|
||||
if (settings[methodName] === false) {
|
||||
sharedMethod.sharedClass.disableMethodByName(methodName);
|
||||
} else {
|
||||
sharedMethod.shared = true;
|
||||
}
|
||||
} else {
|
||||
tests.forEach(function(glob) {
|
||||
if (glob.regex.test(methodName)) {
|
||||
if (glob.setting === false) {
|
||||
sharedMethod.sharedClass.disableMethodByName(methodName);
|
||||
} else {
|
||||
sharedMethod.shared = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Sanitize all RegExp reserved characters except * for pattern gobbing
|
||||
function escapeRegExp(str) {
|
||||
return str.replace(/[\-\[\]\/\{\}\(\)\+\?\.\\\^\$\|]/g, '\\$&');
|
||||
}
|
|
@ -1,18 +1,24 @@
|
|||
// Copyright IBM Corp. 2013,2019. All Rights Reserved.
|
||||
// Node module: loopback
|
||||
// This file is licensed under the MIT License.
|
||||
// License text available at https://opensource.org/licenses/MIT
|
||||
|
||||
/**
|
||||
* Expose `Connector`.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
module.exports = Connector;
|
||||
|
||||
/**
|
||||
* Module dependencies.
|
||||
*/
|
||||
|
||||
var EventEmitter = require('events').EventEmitter;
|
||||
var debug = require('debug')('connector');
|
||||
var util = require('util');
|
||||
var inherits = util.inherits;
|
||||
var assert = require('assert');
|
||||
const EventEmitter = require('events').EventEmitter;
|
||||
const debug = require('debug')('connector');
|
||||
const util = require('util');
|
||||
const inherits = util.inherits;
|
||||
const assert = require('assert');
|
||||
|
||||
/**
|
||||
* Create a new `Connector` with the given `options`.
|
||||
|
@ -39,7 +45,7 @@ inherits(Connector, EventEmitter);
|
|||
*/
|
||||
|
||||
Connector._createJDBAdapter = function(jdbModule) {
|
||||
var fauxSchema = {};
|
||||
const fauxSchema = {};
|
||||
jdbModule.initialize(fauxSchema, function() {
|
||||
// connected
|
||||
});
|
||||
|
|
|
@ -1,11 +1,18 @@
|
|||
// Copyright IBM Corp. 2013,2019. All Rights Reserved.
|
||||
// Node module: loopback
|
||||
// This file is licensed under the MIT License.
|
||||
// License text available at https://opensource.org/licenses/MIT
|
||||
|
||||
/**
|
||||
* Dependencies.
|
||||
*/
|
||||
|
||||
var mailer = require('nodemailer');
|
||||
var assert = require('assert');
|
||||
var debug = require('debug')('loopback:connector:mail');
|
||||
var loopback = require('../loopback');
|
||||
'use strict';
|
||||
const g = require('../globalize');
|
||||
const mailer = require('nodemailer');
|
||||
const assert = require('assert');
|
||||
const debug = require('debug')('loopback:connector:mail');
|
||||
const loopback = require('../loopback');
|
||||
|
||||
/**
|
||||
* Export the MailConnector class.
|
||||
|
@ -18,14 +25,13 @@ module.exports = MailConnector;
|
|||
*/
|
||||
|
||||
function MailConnector(settings) {
|
||||
|
||||
assert(typeof settings === 'object', 'cannot initialize MailConnector without a settings object');
|
||||
|
||||
var transports = settings.transports;
|
||||
let transports = settings.transports;
|
||||
|
||||
//if transports is not in settings object AND settings.transport exists
|
||||
// if transports is not in settings object AND settings.transport exists
|
||||
if (!transports && settings.transport) {
|
||||
//then wrap single transport in an array and assign to transports
|
||||
// then wrap single transport in an array and assign to transports
|
||||
transports = [settings.transport];
|
||||
}
|
||||
|
||||
|
@ -54,10 +60,11 @@ MailConnector.prototype.DataAccessObject = Mailer;
|
|||
* Example:
|
||||
*
|
||||
* Email.setupTransport({
|
||||
* type: 'SMTP',
|
||||
* type: "SMTP",
|
||||
* host: "smtp.gmail.com", // hostname
|
||||
* secureConnection: true, // use SSL
|
||||
* port: 465, // port for secure SMTP
|
||||
* alias: "gmail", // optional alias for use with 'transport' option when sending
|
||||
* auth: {
|
||||
* user: "gmail.user@gmail.com",
|
||||
* pass: "userpass"
|
||||
|
@ -67,23 +74,21 @@ MailConnector.prototype.DataAccessObject = Mailer;
|
|||
*/
|
||||
|
||||
MailConnector.prototype.setupTransport = function(setting) {
|
||||
var connector = this;
|
||||
const connector = this;
|
||||
connector.transports = connector.transports || [];
|
||||
connector.transportsIndex = connector.transportsIndex || {};
|
||||
|
||||
var transport;
|
||||
var transportType = (setting.type || 'STUB').toLowerCase();
|
||||
if (transportType === 'direct') {
|
||||
transport = mailer.createTransport();
|
||||
} else if (transportType === 'smtp') {
|
||||
let transport;
|
||||
const transportType = (setting.type || 'STUB').toLowerCase();
|
||||
if (transportType === 'smtp') {
|
||||
transport = mailer.createTransport(setting);
|
||||
} else {
|
||||
var transportModuleName = 'nodemailer-' + transportType + '-transport';
|
||||
var transportModule = require(transportModuleName);
|
||||
const transportModuleName = 'nodemailer-' + transportType + '-transport';
|
||||
const transportModule = require(transportModuleName);
|
||||
transport = mailer.createTransport(transportModule(setting));
|
||||
}
|
||||
|
||||
connector.transportsIndex[setting.type] = transport;
|
||||
connector.transportsIndex[setting.alias || setting.type] = transport;
|
||||
connector.transports.push(transport);
|
||||
};
|
||||
|
||||
|
@ -122,7 +127,8 @@ MailConnector.prototype.defaultTransport = function() {
|
|||
* to: "bar@blurdybloop.com, baz@blurdybloop.com", // list of receivers
|
||||
* subject: "Hello ✔", // Subject line
|
||||
* text: "Hello world ✔", // plaintext body
|
||||
* html: "<b>Hello world ✔</b>" // html body
|
||||
* html: "<b>Hello world ✔</b>", // html body
|
||||
* transport: "gmail", // See 'alias' option above in setupTransport
|
||||
* }
|
||||
*
|
||||
* See https://github.com/andris9/Nodemailer for other supported options.
|
||||
|
@ -132,34 +138,35 @@ MailConnector.prototype.defaultTransport = function() {
|
|||
*/
|
||||
|
||||
Mailer.send = function(options, fn) {
|
||||
var dataSource = this.dataSource;
|
||||
var settings = dataSource && dataSource.settings;
|
||||
var connector = dataSource.connector;
|
||||
const dataSource = this.dataSource;
|
||||
const settings = dataSource && dataSource.settings;
|
||||
const connector = dataSource.connector;
|
||||
assert(connector, 'Cannot send mail without a connector!');
|
||||
|
||||
var transport = connector.transportForName(options.transport);
|
||||
let transport = connector.transportForName(options.transport);
|
||||
|
||||
if (!transport) {
|
||||
transport = connector.defaultTransport();
|
||||
}
|
||||
|
||||
if (debug.enabled || settings && settings.debug) {
|
||||
console.log('Sending Mail:');
|
||||
g.log('Sending Mail:');
|
||||
if (options.transport) {
|
||||
console.log('\t TRANSPORT:', options.transport);
|
||||
console.log(g.f('\t TRANSPORT:%s', options.transport));
|
||||
}
|
||||
console.log('\t TO:', options.to);
|
||||
console.log('\t FROM:', options.from);
|
||||
console.log('\t SUBJECT:', options.subject);
|
||||
console.log('\t TEXT:', options.text);
|
||||
console.log('\t HTML:', options.html);
|
||||
g.log('\t TO:%s', options.to);
|
||||
g.log('\t FROM:%s', options.from);
|
||||
g.log('\t SUBJECT:%s', options.subject);
|
||||
g.log('\t TEXT:%s', options.text);
|
||||
g.log('\t HTML:%s', options.html);
|
||||
}
|
||||
|
||||
if (transport) {
|
||||
assert(transport.sendMail, 'You must supply an Email.settings.transports containing a valid transport');
|
||||
assert(transport.sendMail,
|
||||
'You must supply an Email.settings.transports containing a valid transport');
|
||||
transport.sendMail(options, fn);
|
||||
} else {
|
||||
console.warn('Warning: No email transport specified for sending email.' +
|
||||
g.warn('Warning: No email transport specified for sending email.' +
|
||||
' Setup a transport to send mail messages.');
|
||||
process.nextTick(function() {
|
||||
fn(null, options);
|
||||
|
|
|
@ -1,19 +1,25 @@
|
|||
// Copyright IBM Corp. 2013,2019. All Rights Reserved.
|
||||
// Node module: loopback
|
||||
// This file is licensed under the MIT License.
|
||||
// License text available at https://opensource.org/licenses/MIT
|
||||
|
||||
/**
|
||||
* Expose `Memory`.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
module.exports = Memory;
|
||||
|
||||
/**
|
||||
* Module dependencies.
|
||||
*/
|
||||
|
||||
var Connector = require('./base-connector');
|
||||
var debug = require('debug')('memory');
|
||||
var util = require('util');
|
||||
var inherits = util.inherits;
|
||||
var assert = require('assert');
|
||||
var JdbMemory = require('loopback-datasource-juggler/lib/connectors/memory');
|
||||
const Connector = require('./base-connector');
|
||||
const debug = require('debug')('memory');
|
||||
const util = require('util');
|
||||
const inherits = util.inherits;
|
||||
const assert = require('assert');
|
||||
const JdbMemory = require('loopback-datasource-juggler/lib/connectors/memory');
|
||||
|
||||
/**
|
||||
* Create a new `Memory` connector with the given `options`.
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
// Copyright IBM Corp. 2016,2019. All Rights Reserved.
|
||||
// Node module: loopback
|
||||
// This file is licensed under the MIT License.
|
||||
// License text available at https://opensource.org/licenses/MIT
|
||||
|
||||
'use strict';
|
||||
const g = require('./globalize');
|
||||
const juggler = require('loopback-datasource-juggler');
|
||||
const remoting = require('strong-remoting');
|
||||
|
||||
module.exports = function(loopback) {
|
||||
juggler.getCurrentContext =
|
||||
remoting.getCurrentContext =
|
||||
loopback.getCurrentContext = function() {
|
||||
throw new Error(g.f(
|
||||
'%s was removed in version 3.0. See %s for more details.',
|
||||
'loopback.getCurrentContext()',
|
||||
'http://loopback.io/doc/en/lb2/Using-current-context.html',
|
||||
));
|
||||
};
|
||||
|
||||
loopback.runInContext = function(fn) {
|
||||
throw new Error(g.f(
|
||||
'%s was removed in version 3.0. See %s for more details.',
|
||||
'loopback.runInContext()',
|
||||
'http://loopback.io/doc/en/lb2/Using-current-context.html',
|
||||
));
|
||||
};
|
||||
|
||||
loopback.createContext = function(scopeName) {
|
||||
throw new Error(g.f(
|
||||
'%s was removed in version 3.0. See %s for more details.',
|
||||
'loopback.createContext()',
|
||||
'http://loopback.io/doc/en/lb2/Using-current-context.html',
|
||||
));
|
||||
};
|
||||
};
|
|
@ -1,52 +0,0 @@
|
|||
var path = require('path');
|
||||
|
||||
var middlewares = exports;
|
||||
|
||||
function safeRequire(m) {
|
||||
try {
|
||||
return require(m);
|
||||
} catch (err) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function createMiddlewareNotInstalled(memberName, moduleName) {
|
||||
return function() {
|
||||
var msg = 'The middleware loopback.' + memberName + ' is not installed.\n' +
|
||||
'Run `npm install --save ' + moduleName + '` to fix the problem.';
|
||||
throw new Error(msg);
|
||||
};
|
||||
}
|
||||
|
||||
var middlewareModules = {
|
||||
'compress': 'compression',
|
||||
'timeout': 'connect-timeout',
|
||||
'cookieParser': 'cookie-parser',
|
||||
'cookieSession': 'cookie-session',
|
||||
'csrf': 'csurf',
|
||||
'errorHandler': 'errorhandler',
|
||||
'session': 'express-session',
|
||||
'methodOverride': 'method-override',
|
||||
'logger': 'morgan',
|
||||
'responseTime': 'response-time',
|
||||
'favicon': 'serve-favicon',
|
||||
'directory': 'serve-index',
|
||||
// 'static': 'serve-static',
|
||||
'vhost': 'vhost'
|
||||
};
|
||||
|
||||
middlewares.bodyParser = safeRequire('body-parser');
|
||||
middlewares.json = middlewares.bodyParser && middlewares.bodyParser.json;
|
||||
middlewares.urlencoded = middlewares.bodyParser && middlewares.bodyParser.urlencoded;
|
||||
|
||||
for (var m in middlewareModules) {
|
||||
var moduleName = middlewareModules[m];
|
||||
middlewares[m] = safeRequire(moduleName) || createMiddlewareNotInstalled(m, moduleName);
|
||||
}
|
||||
|
||||
// serve-favicon requires a path
|
||||
var favicon = middlewares.favicon;
|
||||
middlewares.favicon = function(icon, options) {
|
||||
icon = icon || path.join(__dirname, '../favicon.ico');
|
||||
return favicon(icon, options);
|
||||
};
|
|
@ -0,0 +1,11 @@
|
|||
// Copyright IBM Corp. 2016,2019. All Rights Reserved.
|
||||
// Node module: loopback
|
||||
// This file is licensed under the MIT License.
|
||||
// License text available at https://opensource.org/licenses/MIT
|
||||
|
||||
'use strict';
|
||||
const path = require('path');
|
||||
const SG = require('strong-globalize');
|
||||
|
||||
SG.SetRootDir(path.join(__dirname, '..'), {autonomousMsgLoading: 'all'});
|
||||
module.exports = SG();
|
139
lib/loopback.js
139
lib/loopback.js
|
@ -1,17 +1,24 @@
|
|||
// Copyright IBM Corp. 2013,2019. All Rights Reserved.
|
||||
// Node module: loopback
|
||||
// This file is licensed under the MIT License.
|
||||
// License text available at https://opensource.org/licenses/MIT
|
||||
|
||||
/*!
|
||||
* Module dependencies.
|
||||
*/
|
||||
|
||||
var express = require('express');
|
||||
var loopbackExpress = require('./server-app');
|
||||
var proto = require('./application');
|
||||
var fs = require('fs');
|
||||
var ejs = require('ejs');
|
||||
var path = require('path');
|
||||
var merge = require('util')._extend;
|
||||
var assert = require('assert');
|
||||
var Registry = require('./registry');
|
||||
var juggler = require('loopback-datasource-juggler');
|
||||
'use strict';
|
||||
const express = require('express');
|
||||
const loopbackExpress = require('./server-app');
|
||||
const proto = require('./application');
|
||||
const fs = require('fs');
|
||||
const ejs = require('ejs');
|
||||
const path = require('path');
|
||||
const merge = require('util')._extend;
|
||||
const assert = require('assert');
|
||||
const Registry = require('./registry');
|
||||
const juggler = require('loopback-datasource-juggler');
|
||||
const configureSharedMethods = require('./configure-shared-methods');
|
||||
|
||||
/**
|
||||
* LoopBack core module. It provides static properties and
|
||||
|
@ -24,7 +31,6 @@ var juggler = require('loopback-datasource-juggler');
|
|||
* ```
|
||||
*
|
||||
* @property {String} version Version of LoopBack framework. Static read-only property.
|
||||
* @property {String} mime
|
||||
* @property {Boolean} isBrowser True if running in a browser environment; false otherwise. Static read-only property.
|
||||
* @property {Boolean} isServer True if running in a server environment; false otherwise. Static read-only property.
|
||||
* @property {Registry} registry The global `Registry` object.
|
||||
|
@ -34,7 +40,7 @@ var juggler = require('loopback-datasource-juggler');
|
|||
* @header loopback
|
||||
*/
|
||||
|
||||
var loopback = module.exports = createApplication;
|
||||
const loopback = module.exports = createApplication;
|
||||
|
||||
/*!
|
||||
* Framework version.
|
||||
|
@ -42,27 +48,21 @@ var loopback = module.exports = createApplication;
|
|||
|
||||
loopback.version = require('../package.json').version;
|
||||
|
||||
/*!
|
||||
* Expose mime.
|
||||
*/
|
||||
|
||||
loopback.mime = express.mime;
|
||||
|
||||
loopback.registry = new Registry();
|
||||
|
||||
Object.defineProperties(loopback, {
|
||||
Model: {
|
||||
get: function() { return this.registry.getModel('Model'); }
|
||||
get: function() { return this.registry.getModel('Model'); },
|
||||
},
|
||||
PersistedModel: {
|
||||
get: function() { return this.registry.getModel('PersistedModel'); }
|
||||
get: function() { return this.registry.getModel('PersistedModel'); },
|
||||
},
|
||||
defaultDataSources: {
|
||||
get: function() { return this.registry.defaultDataSources; }
|
||||
get: function() { return this.registry.defaultDataSources; },
|
||||
},
|
||||
modelBuilder: {
|
||||
get: function() { return this.registry.modelBuilder; }
|
||||
}
|
||||
get: function() { return this.registry.modelBuilder; },
|
||||
},
|
||||
});
|
||||
|
||||
/*!
|
||||
|
@ -73,12 +73,19 @@ Object.defineProperties(loopback, {
|
|||
*/
|
||||
|
||||
function createApplication(options) {
|
||||
var app = loopbackExpress();
|
||||
const app = loopbackExpress();
|
||||
|
||||
merge(app, proto);
|
||||
|
||||
app.loopback = loopback;
|
||||
|
||||
app.on('modelRemoted', function() {
|
||||
app.models().forEach(function(Model) {
|
||||
if (!Model.config) return;
|
||||
configureSharedMethods(Model, app.get('remoting'), Model.config);
|
||||
});
|
||||
});
|
||||
|
||||
// Create a new instance of models registry per each app instance
|
||||
app.models = function() {
|
||||
return proto.models.apply(this, arguments);
|
||||
|
@ -95,10 +102,12 @@ function createApplication(options) {
|
|||
// and thus browserify can process them (include connectors in the bundle)
|
||||
app.connector('memory', loopback.Memory);
|
||||
app.connector('remote', loopback.Remote);
|
||||
app.connector('kv-memory',
|
||||
require('loopback-datasource-juggler/lib/connectors/kv-memory'));
|
||||
|
||||
if (loopback.localRegistry || options && options.localRegistry === true) {
|
||||
// setup the app registry
|
||||
var registry = app.registry = new Registry();
|
||||
const registry = app.registry = new Registry();
|
||||
if (options && options.loadBuiltinModels === true) {
|
||||
require('./builtin-models')(registry);
|
||||
}
|
||||
|
@ -110,8 +119,8 @@ function createApplication(options) {
|
|||
}
|
||||
|
||||
function mixin(source) {
|
||||
for (var key in source) {
|
||||
var desc = Object.getOwnPropertyDescriptor(source, key);
|
||||
for (const key in source) {
|
||||
const desc = Object.getOwnPropertyDescriptor(source, key);
|
||||
|
||||
// Fix for legacy (pre-ES5) browsers like PhantomJS
|
||||
if (!desc) continue;
|
||||
|
@ -123,23 +132,11 @@ function mixin(source) {
|
|||
mixin(require('./runtime'));
|
||||
|
||||
/*!
|
||||
* Expose static express methods like `express.errorHandler`.
|
||||
* Expose static express methods like `express.Router`.
|
||||
*/
|
||||
|
||||
mixin(express);
|
||||
|
||||
/*!
|
||||
* Expose additional middleware like session as loopback.*
|
||||
* This will keep the loopback API compatible with express 3.x
|
||||
*
|
||||
* ***only in node***
|
||||
*/
|
||||
|
||||
if (loopback.isServer) {
|
||||
var middlewares = require('./express-middleware');
|
||||
mixin(middlewares);
|
||||
}
|
||||
|
||||
/*!
|
||||
* Expose additional loopback middleware
|
||||
* for example `loopback.configure` etc.
|
||||
|
@ -164,11 +161,8 @@ if (loopback.isServer) {
|
|||
delete loopback['error-handler'];
|
||||
}
|
||||
|
||||
/*
|
||||
* Expose path to the default favicon file
|
||||
*
|
||||
* ***only in node***
|
||||
*/
|
||||
// Expose path to the default favicon file
|
||||
// ***only in node***
|
||||
|
||||
if (loopback.isServer) {
|
||||
/*!
|
||||
|
@ -205,19 +199,19 @@ loopback.remoteMethod = function(fn, options) {
|
|||
* var render = loopback.template('foo.ejs');
|
||||
* var html = render({foo: 'bar'});
|
||||
*
|
||||
* @param {String} path Path to the template file.
|
||||
* @param {String} file Path to the template file.
|
||||
* @returns {Function}
|
||||
*/
|
||||
|
||||
loopback.template = function(file) {
|
||||
var templates = this._templates || (this._templates = {});
|
||||
var str = templates[file] || (templates[file] = fs.readFileSync(file, 'utf8'));
|
||||
const templates = this._templates || (this._templates = {});
|
||||
const str = templates[file] || (templates[file] = fs.readFileSync(file, 'utf8'));
|
||||
return ejs.compile(str, {
|
||||
filename: file
|
||||
filename: file,
|
||||
});
|
||||
};
|
||||
|
||||
require('../server/current-context')(loopback);
|
||||
require('../lib/current-context')(loopback);
|
||||
|
||||
/**
|
||||
* Create a named vanilla JavaScript class constructor with an attached
|
||||
|
@ -363,51 +357,6 @@ loopback.createDataSource = function(name, options) {
|
|||
loopback.memory = function(name) {
|
||||
return this.registry.memory.apply(this.registry, arguments);
|
||||
};
|
||||
|
||||
/**
|
||||
* Set the default `dataSource` for a given `type`.
|
||||
* @param {String} type The datasource type.
|
||||
* @param {Object|DataSource} dataSource The data source settings or instance
|
||||
* @returns {DataSource} The data source instance.
|
||||
*
|
||||
* @header loopback.setDefaultDataSourceForType(type, dataSource)
|
||||
*/
|
||||
|
||||
loopback.setDefaultDataSourceForType = function(type, dataSource) {
|
||||
return this.registry.setDefaultDataSourceForType.apply(this.registry, arguments);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the default `dataSource` for a given `type`.
|
||||
* @param {String} type The datasource type.
|
||||
* @returns {DataSource} The data source instance
|
||||
*/
|
||||
|
||||
loopback.getDefaultDataSourceForType = function(type) {
|
||||
return this.registry.getDefaultDataSourceForType.apply(this.registry, arguments);
|
||||
};
|
||||
|
||||
/**
|
||||
* Attach any model that does not have a dataSource to
|
||||
* the default dataSource for the type the Model requests
|
||||
*/
|
||||
|
||||
loopback.autoAttach = function() {
|
||||
return this.registry.autoAttach.apply(this.registry, arguments);
|
||||
};
|
||||
|
||||
loopback.autoAttachModel = function(ModelCtor) {
|
||||
return this.registry.autoAttachModel.apply(this.registry, arguments);
|
||||
};
|
||||
|
||||
// temporary alias to simplify migration of code based on <=2.0.0-beta3
|
||||
// TODO(bajtos) Remove this in v3.0
|
||||
Object.defineProperty(loopback, 'DataModel', {
|
||||
get: function() {
|
||||
return this.registry.DataModel;
|
||||
}
|
||||
});
|
||||
|
||||
/*!
|
||||
* Built in models / services
|
||||
*/
|
||||
|
|
663
lib/model.js
663
lib/model.js
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
227
lib/registry.js
227
lib/registry.js
|
@ -1,9 +1,17 @@
|
|||
var assert = require('assert');
|
||||
var extend = require('util')._extend;
|
||||
var juggler = require('loopback-datasource-juggler');
|
||||
var debug = require('debug')('loopback:registry');
|
||||
var DataSource = juggler.DataSource;
|
||||
var ModelBuilder = juggler.ModelBuilder;
|
||||
// Copyright IBM Corp. 2014,2019. All Rights Reserved.
|
||||
// Node module: loopback
|
||||
// This file is licensed under the MIT License.
|
||||
// License text available at https://opensource.org/licenses/MIT
|
||||
|
||||
'use strict';
|
||||
const g = require('./globalize');
|
||||
const assert = require('assert');
|
||||
const extend = require('util')._extend;
|
||||
const juggler = require('loopback-datasource-juggler');
|
||||
const debug = require('debug')('loopback:registry');
|
||||
const DataSource = juggler.DataSource;
|
||||
const ModelBuilder = juggler.ModelBuilder;
|
||||
const deprecated = require('depd')('strong-remoting');
|
||||
|
||||
module.exports = Registry;
|
||||
|
||||
|
@ -88,9 +96,8 @@ function Registry() {
|
|||
*/
|
||||
|
||||
Registry.prototype.createModel = function(name, properties, options) {
|
||||
|
||||
if (arguments.length === 1 && typeof name === 'object') {
|
||||
var config = name;
|
||||
const config = name;
|
||||
name = config.name;
|
||||
properties = config.properties;
|
||||
options = buildModelOptionsFromConfig(config);
|
||||
|
@ -100,41 +107,29 @@ Registry.prototype.createModel = function(name, properties, options) {
|
|||
}
|
||||
|
||||
options = options || {};
|
||||
var BaseModel = options.base || options.super;
|
||||
let BaseModel = options.base || options.super;
|
||||
|
||||
if (typeof BaseModel === 'string') {
|
||||
var baseName = BaseModel;
|
||||
BaseModel = this.getModel(BaseModel);
|
||||
|
||||
if (BaseModel === undefined) {
|
||||
if (baseName === 'DataModel') {
|
||||
console.warn('Model `%s` is extending deprecated `DataModel. ' +
|
||||
'Use `PersistedModel` instead.', name);
|
||||
BaseModel = this.getModel('PersistedModel');
|
||||
} else {
|
||||
console.warn('Model `%s` is extending an unknown model `%s`. ' +
|
||||
'Using `PersistedModel` as the base.', name, baseName);
|
||||
}
|
||||
const baseName = BaseModel;
|
||||
BaseModel = this.findModel(BaseModel);
|
||||
if (!BaseModel) {
|
||||
throw new Error(g.f('Model not found: model `%s` is extending an unknown model `%s`.',
|
||||
name, baseName));
|
||||
}
|
||||
}
|
||||
|
||||
BaseModel = BaseModel || this.getModel('PersistedModel');
|
||||
var model = BaseModel.extend(name, properties, options);
|
||||
const model = BaseModel.extend(name, properties, options);
|
||||
model.registry = this;
|
||||
|
||||
this._defineRemoteMethods(model, options.methods);
|
||||
|
||||
// try to attach
|
||||
try {
|
||||
this.autoAttachModel(model);
|
||||
} catch (e) {}
|
||||
this._defineRemoteMethods(model, model.settings.methods);
|
||||
|
||||
return model;
|
||||
};
|
||||
|
||||
function buildModelOptionsFromConfig(config) {
|
||||
var options = extend({}, config.options);
|
||||
for (var key in config) {
|
||||
const options = extend({}, config.options);
|
||||
for (const key in config) {
|
||||
if (['name', 'properties', 'options'].indexOf(key) !== -1) {
|
||||
// Skip items which have special meaning
|
||||
continue;
|
||||
|
@ -157,7 +152,7 @@ function buildModelOptionsFromConfig(config) {
|
|||
* @param {Object} acl
|
||||
*/
|
||||
function addACL(acls, acl) {
|
||||
for (var i = 0, n = acls.length; i < n; i++) {
|
||||
for (let i = 0, n = acls.length; i < n; i++) {
|
||||
// Check if there is a matching acl to be overriden
|
||||
if (acls[i].property === acl.property &&
|
||||
acls[i].accessType === acl.accessType &&
|
||||
|
@ -181,51 +176,53 @@ function addACL(acls, acl) {
|
|||
*/
|
||||
|
||||
Registry.prototype.configureModel = function(ModelCtor, config) {
|
||||
var settings = ModelCtor.settings;
|
||||
var modelName = ModelCtor.modelName;
|
||||
const settings = ModelCtor.settings;
|
||||
const modelName = ModelCtor.modelName;
|
||||
|
||||
ModelCtor.config = config;
|
||||
|
||||
// Relations
|
||||
if (typeof config.relations === 'object' && config.relations !== null) {
|
||||
var relations = settings.relations = settings.relations || {};
|
||||
const relations = settings.relations = settings.relations || {};
|
||||
Object.keys(config.relations).forEach(function(key) {
|
||||
// FIXME: [rfeng] We probably should check if the relation exists
|
||||
relations[key] = extend(relations[key] || {}, config.relations[key]);
|
||||
});
|
||||
} else if (config.relations != null) {
|
||||
console.warn('The relations property of `%s` configuration ' +
|
||||
g.warn('The relations property of `%s` configuration ' +
|
||||
'must be an object', modelName);
|
||||
}
|
||||
|
||||
// ACLs
|
||||
if (Array.isArray(config.acls)) {
|
||||
var acls = settings.acls = settings.acls || [];
|
||||
const acls = settings.acls = settings.acls || [];
|
||||
config.acls.forEach(function(acl) {
|
||||
addACL(acls, acl);
|
||||
});
|
||||
} else if (config.acls != null) {
|
||||
console.warn('The acls property of `%s` configuration ' +
|
||||
g.warn('The acls property of `%s` configuration ' +
|
||||
'must be an array of objects', modelName);
|
||||
}
|
||||
|
||||
// Settings
|
||||
var excludedProperties = {
|
||||
const excludedProperties = {
|
||||
base: true,
|
||||
'super': true,
|
||||
relations: true,
|
||||
acls: true,
|
||||
dataSource: true
|
||||
dataSource: true,
|
||||
};
|
||||
if (typeof config.options === 'object' && config.options !== null) {
|
||||
for (var p in config.options) {
|
||||
for (const p in config.options) {
|
||||
if (!(p in excludedProperties)) {
|
||||
settings[p] = config.options[p];
|
||||
} else {
|
||||
console.warn('Property `%s` cannot be reconfigured for `%s`',
|
||||
g.warn('Property `%s` cannot be reconfigured for `%s`',
|
||||
p, modelName);
|
||||
}
|
||||
}
|
||||
} else if (config.options != null) {
|
||||
console.warn('The options property of `%s` configuration ' +
|
||||
g.warn('The options property of `%s` configuration ' +
|
||||
'must be an object', modelName);
|
||||
}
|
||||
|
||||
|
@ -233,7 +230,7 @@ Registry.prototype.configureModel = function(ModelCtor, config) {
|
|||
// configuration, so that the datasource picks up updated relations
|
||||
if (config.dataSource) {
|
||||
assert(config.dataSource instanceof DataSource,
|
||||
'Cannot configure ' + ModelCtor.modelName +
|
||||
'Cannot configure ' + ModelCtor.modelName +
|
||||
': config.dataSource must be an instance of DataSource');
|
||||
ModelCtor.attachTo(config.dataSource);
|
||||
debug('Attached model `%s` to dataSource `%s`',
|
||||
|
@ -244,10 +241,21 @@ Registry.prototype.configureModel = function(ModelCtor, config) {
|
|||
} else {
|
||||
debug('Model `%s` is not attached to any DataSource, possibly by a mistake.',
|
||||
modelName);
|
||||
console.warn(
|
||||
'The configuration of `%s` is missing `dataSource` property.\n' +
|
||||
g.warn(
|
||||
'The configuration of `%s` is missing {{`dataSource`}} property.\n' +
|
||||
'Use `null` or `false` to mark models not attached to any data source.',
|
||||
modelName);
|
||||
modelName,
|
||||
);
|
||||
}
|
||||
|
||||
const newMethodNames = config.methods && Object.keys(config.methods);
|
||||
const hasNewMethods = newMethodNames && newMethodNames.length;
|
||||
const hasDescendants = this.getModelByType(ModelCtor) !== ModelCtor;
|
||||
if (hasNewMethods && hasDescendants) {
|
||||
g.warn(
|
||||
'Child models of `%s` will not inherit newly defined remote methods %s.',
|
||||
modelName, newMethodNames,
|
||||
);
|
||||
}
|
||||
|
||||
// Remote methods
|
||||
|
@ -257,19 +265,26 @@ Registry.prototype.configureModel = function(ModelCtor, config) {
|
|||
Registry.prototype._defineRemoteMethods = function(ModelCtor, methods) {
|
||||
if (!methods) return;
|
||||
if (typeof methods !== 'object') {
|
||||
console.warn('Ignoring non-object "methods" setting of "%s".',
|
||||
g.warn('Ignoring non-object "methods" setting of "%s".',
|
||||
ModelCtor.modelName);
|
||||
return;
|
||||
}
|
||||
|
||||
Object.keys(methods).forEach(function(key) {
|
||||
var meta = methods[key];
|
||||
let meta = methods[key];
|
||||
const m = key.match(/^prototype\.(.*)$/);
|
||||
const isStatic = !m;
|
||||
|
||||
if (typeof meta.isStatic !== 'boolean') {
|
||||
console.warn('Remoting metadata for "%s.%s" is missing "isStatic" ' +
|
||||
'flag, the method is registered as an instance method.',
|
||||
ModelCtor.modelName,
|
||||
key);
|
||||
console.warn('This behaviour may change in the next major version.');
|
||||
key = isStatic ? key : m[1];
|
||||
meta = Object.assign({}, meta, {isStatic});
|
||||
} else if (meta.isStatic && m) {
|
||||
throw new Error(g.f('Remoting metadata for %s.%s {{"isStatic"}} does ' +
|
||||
'not match new method name-based style.', ModelCtor.modelName, key));
|
||||
} else {
|
||||
key = isStatic ? key : m[1];
|
||||
deprecated(g.f('Remoting metadata {{"isStatic"}} is deprecated. Please ' +
|
||||
'specify {{"prototype.name"}} in method name instead for {{isStatic=false}}.'));
|
||||
}
|
||||
ModelCtor.remoteMethod(key, meta);
|
||||
});
|
||||
|
@ -298,10 +313,10 @@ Registry.prototype.findModel = function(modelName) {
|
|||
* @header loopback.getModel(modelName)
|
||||
*/
|
||||
Registry.prototype.getModel = function(modelName) {
|
||||
var model = this.findModel(modelName);
|
||||
const model = this.findModel(modelName);
|
||||
if (model) return model;
|
||||
|
||||
throw new Error('Model not found: ' + modelName);
|
||||
throw new Error(g.f('Model not found: %s', modelName));
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -314,8 +329,8 @@ Registry.prototype.getModel = function(modelName) {
|
|||
* @header loopback.getModelByType(modelType)
|
||||
*/
|
||||
Registry.prototype.getModelByType = function(modelType) {
|
||||
var type = typeof modelType;
|
||||
var accepted = ['function', 'string'];
|
||||
const type = typeof modelType;
|
||||
const accepted = ['function', 'string'];
|
||||
|
||||
assert(accepted.indexOf(type) > -1,
|
||||
'The model type must be a constructor or model name');
|
||||
|
@ -324,8 +339,8 @@ Registry.prototype.getModelByType = function(modelType) {
|
|||
modelType = this.getModel(modelType);
|
||||
}
|
||||
|
||||
var models = this.modelBuilder.models;
|
||||
for (var m in models) {
|
||||
const models = this.modelBuilder.models;
|
||||
for (const m in models) {
|
||||
if (models[m].prototype instanceof modelType) {
|
||||
return models[m];
|
||||
}
|
||||
|
@ -344,15 +359,15 @@ Registry.prototype.getModelByType = function(modelType) {
|
|||
*/
|
||||
|
||||
Registry.prototype.createDataSource = function(name, options) {
|
||||
var self = this;
|
||||
const self = this;
|
||||
|
||||
var ds = new DataSource(name, options, self.modelBuilder);
|
||||
const ds = new DataSource(name, options, self.modelBuilder);
|
||||
ds.createModel = function(name, properties, settings) {
|
||||
settings = settings || {};
|
||||
var BaseModel = settings.base || settings.super;
|
||||
let BaseModel = settings.base || settings.super;
|
||||
if (!BaseModel) {
|
||||
// Check the connector types
|
||||
var connectorTypes = ds.getTypes();
|
||||
const connectorTypes = ds.getTypes();
|
||||
if (Array.isArray(connectorTypes) && connectorTypes.indexOf('db') !== -1) {
|
||||
// Only set up the base model to PersistedModel if the connector is DB
|
||||
BaseModel = self.PersistedModel;
|
||||
|
@ -361,13 +376,14 @@ Registry.prototype.createDataSource = function(name, options) {
|
|||
}
|
||||
settings.base = BaseModel;
|
||||
}
|
||||
var ModelCtor = self.createModel(name, properties, settings);
|
||||
const ModelCtor = self.createModel(name, properties, settings);
|
||||
ModelCtor.attachTo(ds);
|
||||
return ModelCtor;
|
||||
};
|
||||
|
||||
if (ds.settings && ds.settings.defaultForType) {
|
||||
this.setDefaultDataSourceForType(ds.settings.defaultForType, ds);
|
||||
const msg = g.f('{{DataSource}} option {{"defaultForType"}} is no longer supported');
|
||||
throw new Error(msg);
|
||||
}
|
||||
|
||||
return ds;
|
||||
|
@ -382,90 +398,15 @@ Registry.prototype.createDataSource = function(name, options) {
|
|||
|
||||
Registry.prototype.memory = function(name) {
|
||||
name = name || 'default';
|
||||
var memory = (
|
||||
let memory = (
|
||||
this._memoryDataSources || (this._memoryDataSources = {})
|
||||
)[name];
|
||||
)[name];
|
||||
|
||||
if (!memory) {
|
||||
memory = this._memoryDataSources[name] = this.createDataSource({
|
||||
connector: 'memory'
|
||||
connector: 'memory',
|
||||
});
|
||||
}
|
||||
|
||||
return memory;
|
||||
};
|
||||
|
||||
/**
|
||||
* Set the default `dataSource` for a given `type`.
|
||||
* @param {String} type The datasource type.
|
||||
* @param {Object|DataSource} dataSource The data source settings or instance
|
||||
* @returns {DataSource} The data source instance.
|
||||
*
|
||||
* @header loopback.setDefaultDataSourceForType(type, dataSource)
|
||||
*/
|
||||
|
||||
Registry.prototype.setDefaultDataSourceForType = function(type, dataSource) {
|
||||
var defaultDataSources = this.defaultDataSources;
|
||||
|
||||
if (!(dataSource instanceof DataSource)) {
|
||||
dataSource = this.createDataSource(dataSource);
|
||||
}
|
||||
|
||||
defaultDataSources[type] = dataSource;
|
||||
return dataSource;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the default `dataSource` for a given `type`.
|
||||
* @param {String} type The datasource type.
|
||||
* @returns {DataSource} The data source instance
|
||||
*/
|
||||
|
||||
Registry.prototype.getDefaultDataSourceForType = function(type) {
|
||||
return this.defaultDataSources && this.defaultDataSources[type];
|
||||
};
|
||||
|
||||
/**
|
||||
* Attach any model that does not have a dataSource to
|
||||
* the default dataSource for the type the Model requests
|
||||
*/
|
||||
|
||||
Registry.prototype.autoAttach = function() {
|
||||
var models = this.modelBuilder.models;
|
||||
assert.equal(typeof models, 'object', 'Cannot autoAttach without a models object');
|
||||
|
||||
Object.keys(models).forEach(function(modelName) {
|
||||
var ModelCtor = models[modelName];
|
||||
|
||||
// Only auto attach if the model doesn't have an explicit data source
|
||||
if (ModelCtor && (!(ModelCtor.dataSource instanceof DataSource))) {
|
||||
this.autoAttachModel(ModelCtor);
|
||||
}
|
||||
}, this);
|
||||
};
|
||||
|
||||
Registry.prototype.autoAttachModel = function(ModelCtor) {
|
||||
if (ModelCtor.autoAttach) {
|
||||
var ds = this.getDefaultDataSourceForType(ModelCtor.autoAttach);
|
||||
|
||||
assert(
|
||||
ds instanceof DataSource,
|
||||
'cannot autoAttach model "' + ModelCtor.modelName +
|
||||
'". No dataSource found of type ' + ModelCtor.autoAttach
|
||||
);
|
||||
|
||||
ModelCtor.attachTo(ds);
|
||||
}
|
||||
};
|
||||
|
||||
// temporary alias to simplify migration of code based on <=2.0.0-beta3
|
||||
Object.defineProperty(Registry.prototype, 'DataModel', {
|
||||
get: function() {
|
||||
var stackLines = new Error().stack.split('\n');
|
||||
console.warn('loopback.DataModel is deprecated, ' +
|
||||
'use loopback.PersistedModel instead.');
|
||||
// Log the location where loopback.DataModel was called
|
||||
console.warn(stackLines[2]);
|
||||
return this.PersistedModel;
|
||||
}
|
||||
});
|
||||
|
|
|
@ -1,10 +1,16 @@
|
|||
// Copyright IBM Corp. 2014,2019. All Rights Reserved.
|
||||
// Node module: loopback
|
||||
// This file is licensed under the MIT License.
|
||||
// License text available at https://opensource.org/licenses/MIT
|
||||
|
||||
/*
|
||||
* This is an internal file that should not be used outside of loopback.
|
||||
* All exported entities can be accessed via the `loopback` object.
|
||||
* @private
|
||||
*/
|
||||
|
||||
var runtime = exports;
|
||||
'use strict';
|
||||
const runtime = exports;
|
||||
|
||||
/**
|
||||
* True if running in a browser environment; false otherwise.
|
||||
|
|
|
@ -1,16 +1,23 @@
|
|||
var assert = require('assert');
|
||||
var express = require('express');
|
||||
var merge = require('util')._extend;
|
||||
var mergePhaseNameLists = require('loopback-phase').mergePhaseNameLists;
|
||||
var debug = require('debug')('loopback:app');
|
||||
var stableSortInPlace = require('stable').inplace;
|
||||
// Copyright IBM Corp. 2014,2019. All Rights Reserved.
|
||||
// Node module: loopback
|
||||
// This file is licensed under the MIT License.
|
||||
// License text available at https://opensource.org/licenses/MIT
|
||||
|
||||
var BUILTIN_MIDDLEWARE = { builtin: true };
|
||||
'use strict';
|
||||
const g = require('./globalize');
|
||||
const assert = require('assert');
|
||||
const express = require('express');
|
||||
const merge = require('util')._extend;
|
||||
const mergePhaseNameLists = require('loopback-phase').mergePhaseNameLists;
|
||||
const debug = require('debug')('loopback:app');
|
||||
const stableSortInPlace = require('stable').inplace;
|
||||
|
||||
var proto = {};
|
||||
const BUILTIN_MIDDLEWARE = {builtin: true};
|
||||
|
||||
const proto = {};
|
||||
|
||||
module.exports = function loopbackExpress() {
|
||||
var app = express();
|
||||
const app = express();
|
||||
app.__expressLazyRouter = app.lazyrouter;
|
||||
merge(app, proto);
|
||||
return app;
|
||||
|
@ -57,23 +64,23 @@ proto.middlewareFromConfig = function(factory, config) {
|
|||
if (config.enabled === false)
|
||||
return;
|
||||
|
||||
var params = config.params;
|
||||
let params = config.params;
|
||||
if (params === undefined) {
|
||||
params = [];
|
||||
} else if (!Array.isArray(params)) {
|
||||
params = [params];
|
||||
}
|
||||
|
||||
var handler = factory.apply(null, params);
|
||||
let handler = factory.apply(null, params);
|
||||
|
||||
// Check if methods/verbs filter exists
|
||||
var verbs = config.methods || config.verbs;
|
||||
let verbs = config.methods || config.verbs;
|
||||
if (Array.isArray(verbs)) {
|
||||
verbs = verbs.map(function(verb) {
|
||||
return verb && verb.toUpperCase();
|
||||
});
|
||||
if (verbs.indexOf('ALL') === -1) {
|
||||
var originalHandler = handler;
|
||||
const originalHandler = handler;
|
||||
if (handler.length <= 3) {
|
||||
// Regular handler
|
||||
handler = function(req, res, next) {
|
||||
|
@ -138,7 +145,7 @@ proto.defineMiddlewarePhases = function(nameOrArray) {
|
|||
mergePhaseNameLists(this._requestHandlingPhases, nameOrArray);
|
||||
} else {
|
||||
// add the new phase before 'routes'
|
||||
var routesIx = this._requestHandlingPhases.indexOf('routes');
|
||||
const routesIx = this._requestHandlingPhases.indexOf('routes');
|
||||
this._requestHandlingPhases.splice(routesIx - 1, 0, nameOrArray);
|
||||
}
|
||||
|
||||
|
@ -174,23 +181,23 @@ proto.middleware = function(name, paths, handler) {
|
|||
paths = '/';
|
||||
}
|
||||
|
||||
var fullPhaseName = name;
|
||||
var handlerName = handler.name || '<anonymous>';
|
||||
const fullPhaseName = name;
|
||||
const handlerName = handler.name || '<anonymous>';
|
||||
|
||||
var m = name.match(/^(.+):(before|after)$/);
|
||||
const m = name.match(/^(.+):(before|after)$/);
|
||||
if (m) {
|
||||
name = m[1];
|
||||
}
|
||||
|
||||
if (this._requestHandlingPhases.indexOf(name) === -1)
|
||||
throw new Error('Unknown middleware phase ' + name);
|
||||
throw new Error(g.f('Unknown {{middleware}} phase %s', name));
|
||||
|
||||
debug('use %s %s %s', fullPhaseName, paths, handlerName);
|
||||
|
||||
this._skipLayerSorting = true;
|
||||
this.use(paths, handler);
|
||||
|
||||
var layer = this._findLayerByHandler(handler);
|
||||
const layer = this._findLayerByHandler(handler);
|
||||
if (layer) {
|
||||
// Set the phase name for sorting
|
||||
layer.phase = fullPhaseName;
|
||||
|
@ -214,17 +221,20 @@ proto.middleware = function(name, paths, handler) {
|
|||
*/
|
||||
proto._findLayerByHandler = function(handler) {
|
||||
// Other handlers can be added to the stack, for example,
|
||||
// NewRelic adds sentinel handler. We need to search the stack
|
||||
for (var k = this._router.stack.length - 1; k >= 0; k--) {
|
||||
if (this._router.stack[k].handle === handler ||
|
||||
// NewRelic replaces the handle and keeps it as __NR_original
|
||||
this._router.stack[k].handle['__NR_original'] === handler
|
||||
) {
|
||||
// NewRelic adds sentinel handler, and AppDynamics adds
|
||||
// some additional proxy info. We need to search the stack
|
||||
for (let k = this._router.stack.length - 1; k >= 0; k--) {
|
||||
const isOriginal = this._router.stack[k].handle === handler;
|
||||
const isNewRelic = this._router.stack[k].handle['__NR_original'] === handler;
|
||||
const isAppDynamics = this._router.stack[k].handle['__appdynamicsProxyInfo__'] &&
|
||||
this._router.stack[k].handle['__appdynamicsProxyInfo__']['orig'] === handler;
|
||||
|
||||
if (isOriginal || isNewRelic || isAppDynamics) {
|
||||
return this._router.stack[k];
|
||||
} else {
|
||||
// Aggressively check if the original handler has been wrapped
|
||||
// into a new function with a property pointing to the original handler
|
||||
for (var p in this._router.stack[k].handle) {
|
||||
for (const p in this._router.stack[k].handle) {
|
||||
if (this._router.stack[k].handle[p] === handler) {
|
||||
return this._router.stack[k];
|
||||
}
|
||||
|
@ -236,12 +246,12 @@ proto._findLayerByHandler = function(handler) {
|
|||
|
||||
// Install our custom PhaseList-based handler into the app
|
||||
proto.lazyrouter = function() {
|
||||
var self = this;
|
||||
const self = this;
|
||||
if (self._router) return;
|
||||
|
||||
self.__expressLazyRouter();
|
||||
|
||||
var router = self._router;
|
||||
const router = self._router;
|
||||
|
||||
// Mark all middleware added by Router ctor as builtin
|
||||
// The sorting algo will keep them at beginning of the list
|
||||
|
@ -251,40 +261,40 @@ proto.lazyrouter = function() {
|
|||
|
||||
router.__expressUse = router.use;
|
||||
router.use = function useAndSort() {
|
||||
var retval = this.__expressUse.apply(this, arguments);
|
||||
const retval = this.__expressUse.apply(this, arguments);
|
||||
self._sortLayersByPhase();
|
||||
return retval;
|
||||
};
|
||||
|
||||
router.__expressRoute = router.route;
|
||||
router.route = function routeAndSort() {
|
||||
var retval = this.__expressRoute.apply(this, arguments);
|
||||
const retval = this.__expressRoute.apply(this, arguments);
|
||||
self._sortLayersByPhase();
|
||||
return retval;
|
||||
};
|
||||
|
||||
self._requestHandlingPhases = [
|
||||
'initial', 'session', 'auth', 'parse',
|
||||
'routes', 'files', 'final'
|
||||
'routes', 'files', 'final',
|
||||
];
|
||||
};
|
||||
|
||||
proto._sortLayersByPhase = function() {
|
||||
if (this._skipLayerSorting) return;
|
||||
|
||||
var phaseOrder = {};
|
||||
const phaseOrder = {};
|
||||
this._requestHandlingPhases.forEach(function(name, ix) {
|
||||
phaseOrder[name + ':before'] = ix * 3;
|
||||
phaseOrder[name] = ix * 3 + 1;
|
||||
phaseOrder[name + ':after'] = ix * 3 + 2;
|
||||
});
|
||||
|
||||
var router = this._router;
|
||||
const router = this._router;
|
||||
stableSortInPlace(router.stack, compareLayers);
|
||||
|
||||
function compareLayers(left, right) {
|
||||
var leftPhase = left.phase;
|
||||
var rightPhase = right.phase;
|
||||
const leftPhase = left.phase;
|
||||
const rightPhase = right.phase;
|
||||
|
||||
if (leftPhase === rightPhase) return 0;
|
||||
|
||||
|
|
137
lib/utils.js
137
lib/utils.js
|
@ -1,17 +1,21 @@
|
|||
// Copyright IBM Corp. 2015,2019. All Rights Reserved.
|
||||
// Node module: loopback
|
||||
// This file is licensed under the MIT License.
|
||||
// License text available at https://opensource.org/licenses/MIT
|
||||
|
||||
'use strict';
|
||||
|
||||
exports.createPromiseCallback = createPromiseCallback;
|
||||
exports.uploadInChunks = uploadInChunks;
|
||||
exports.downloadInChunks = downloadInChunks;
|
||||
exports.concatResults = concatResults;
|
||||
|
||||
const Promise = require('bluebird');
|
||||
const async = require('async');
|
||||
|
||||
function createPromiseCallback() {
|
||||
var cb;
|
||||
|
||||
if (!global.Promise) {
|
||||
cb = function() {};
|
||||
cb.promise = {};
|
||||
Object.defineProperty(cb.promise, 'then', { get: throwPromiseNotDefined });
|
||||
Object.defineProperty(cb.promise, 'catch', { get: throwPromiseNotDefined });
|
||||
return cb;
|
||||
}
|
||||
|
||||
var promise = new global.Promise(function(resolve, reject) {
|
||||
let cb;
|
||||
const promise = new Promise(function(resolve, reject) {
|
||||
cb = function(err, data) {
|
||||
if (err) return reject(err);
|
||||
return resolve(data);
|
||||
|
@ -24,5 +28,114 @@ function createPromiseCallback() {
|
|||
function throwPromiseNotDefined() {
|
||||
throw new Error(
|
||||
'Your Node runtime does support ES6 Promises. ' +
|
||||
'Set "global.Promise" to your preferred implementation of promises.');
|
||||
'Set "global.Promise" to your preferred implementation of promises.',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Divide an async call with large array into multiple calls using smaller chunks
|
||||
* @param {Array} largeArray - the large array to be chunked
|
||||
* @param {Number} chunkSize - size of each chunks
|
||||
* @param {Function} processFunction - the function to be called multiple times
|
||||
* @param {Function} cb - the callback
|
||||
*/
|
||||
function uploadInChunks(largeArray, chunkSize, processFunction, cb) {
|
||||
const chunkArrays = [];
|
||||
|
||||
if (!chunkSize || chunkSize < 1 || largeArray.length <= chunkSize) {
|
||||
// if chunking not required
|
||||
processFunction(largeArray, cb);
|
||||
} else {
|
||||
// copying so that the largeArray object does not get affected during splice
|
||||
const copyOfLargeArray = [].concat(largeArray);
|
||||
|
||||
// chunking to smaller arrays
|
||||
while (copyOfLargeArray.length > 0) {
|
||||
chunkArrays.push(copyOfLargeArray.splice(0, chunkSize));
|
||||
}
|
||||
|
||||
const tasks = chunkArrays.map(function(chunkArray) {
|
||||
return function(previousResults, chunkCallback) {
|
||||
const lastArg = arguments[arguments.length - 1];
|
||||
|
||||
if (typeof lastArg === 'function') {
|
||||
chunkCallback = lastArg;
|
||||
}
|
||||
|
||||
processFunction(chunkArray, function(err, results) {
|
||||
if (err) {
|
||||
return chunkCallback(err);
|
||||
}
|
||||
|
||||
// if this is the first async waterfall call or if previous results was not defined
|
||||
if (typeof previousResults === 'function' || typeof previousResults === 'undefined' ||
|
||||
previousResults === null) {
|
||||
previousResults = results;
|
||||
} else if (results) {
|
||||
previousResults = concatResults(previousResults, results);
|
||||
}
|
||||
|
||||
chunkCallback(err, previousResults);
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
async.waterfall(tasks, cb);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Page async download calls
|
||||
* @param {Object} filter - filter object used for the async call
|
||||
* @param {Number} chunkSize - size of each chunks
|
||||
* @param {Function} processFunction - the function to be called multiple times
|
||||
* @param {Function} cb - the callback
|
||||
*/
|
||||
function downloadInChunks(filter, chunkSize, processFunction, cb) {
|
||||
let results = [];
|
||||
filter = filter ? JSON.parse(JSON.stringify(filter)) : {};
|
||||
|
||||
if (!chunkSize || chunkSize < 1) {
|
||||
// if chunking not required
|
||||
processFunction(filter, cb);
|
||||
} else {
|
||||
filter.skip = 0;
|
||||
filter.limit = chunkSize;
|
||||
|
||||
processFunction(JSON.parse(JSON.stringify(filter)), pageAndConcatResults);
|
||||
}
|
||||
|
||||
function pageAndConcatResults(err, pagedResults) {
|
||||
if (err) {
|
||||
return cb(err);
|
||||
} else {
|
||||
results = concatResults(results, pagedResults);
|
||||
if (pagedResults.length >= chunkSize) {
|
||||
filter.skip += pagedResults.length;
|
||||
processFunction(JSON.parse(JSON.stringify(filter)), pageAndConcatResults);
|
||||
} else {
|
||||
cb(null, results);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Concat current results into previous results
|
||||
* Assumption made here that the previous results and current results are homogeneous
|
||||
* @param {Object|Array} previousResults
|
||||
* @param {Object|Array} currentResults
|
||||
*/
|
||||
function concatResults(previousResults, currentResults) {
|
||||
if (Array.isArray(currentResults)) {
|
||||
previousResults = previousResults.concat(currentResults);
|
||||
} else if (typeof currentResults === 'object') {
|
||||
Object.keys(currentResults).forEach(function(key) {
|
||||
previousResults[key] = concatResults(previousResults[key], currentResults[key]);
|
||||
});
|
||||
} else {
|
||||
previousResults = currentResults;
|
||||
}
|
||||
|
||||
return previousResults;
|
||||
}
|
||||
|
|
116
package.json
116
package.json
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "loopback",
|
||||
"version": "2.26.2",
|
||||
"version": "3.28.0",
|
||||
"description": "LoopBack: Open Source Framework for Node.js",
|
||||
"homepage": "http://loopback.io",
|
||||
"keywords": [
|
||||
|
@ -29,64 +29,76 @@
|
|||
"mBaaS"
|
||||
],
|
||||
"scripts": {
|
||||
"test": "grunt mocha-and-karma"
|
||||
"lint": "grunt eslint",
|
||||
"coverage": "nyc report --reporter=text-lcov | coveralls",
|
||||
"test": "nyc grunt mocha-and-karma"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"dependencies": {
|
||||
"async": "^0.9.0",
|
||||
"async": "^2.0.1",
|
||||
"bcryptjs": "^2.1.0",
|
||||
"bluebird": "^3.1.1",
|
||||
"body-parser": "^1.12.0",
|
||||
"canonical-json": "0.0.4",
|
||||
"continuation-local-storage": "^3.1.3",
|
||||
"cookie-parser": "^1.3.4",
|
||||
"debug": "^2.1.2",
|
||||
"depd": "^1.0.0",
|
||||
"ejs": "^2.3.1",
|
||||
"errorhandler": "^1.3.4",
|
||||
"express": "^4.12.2",
|
||||
"express": "^4.14.0",
|
||||
"inflection": "^1.6.0",
|
||||
"loopback-connector-remote": "^1.0.3",
|
||||
"loopback-phase": "^1.2.0",
|
||||
"nodemailer": "^1.3.1",
|
||||
"nodemailer-stub-transport": "^0.1.5",
|
||||
"isemail": "^3.2.0",
|
||||
"loopback-connector-remote": "^3.0.0",
|
||||
"loopback-datasource-juggler": "^3.28.0",
|
||||
"loopback-filters": "^1.0.0",
|
||||
"loopback-phase": "^3.0.0",
|
||||
"nodemailer": "^6.4.16",
|
||||
"nodemailer-direct-transport": "^3.3.2",
|
||||
"nodemailer-stub-transport": "^1.1.0",
|
||||
"serve-favicon": "^2.2.0",
|
||||
"stable": "^0.1.5",
|
||||
"strong-remoting": "^2.21.0",
|
||||
"strong-globalize": "^4.1.1",
|
||||
"strong-remoting": "^3.11.0",
|
||||
"uid2": "0.0.3",
|
||||
"underscore.string": "^3.0.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"loopback-datasource-juggler": "^2.19.0"
|
||||
"underscore.string": "^3.3.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"bluebird": "^2.9.9",
|
||||
"browserify": "^10.0.0",
|
||||
"chai": "^2.1.1",
|
||||
"es5-shim": "^4.1.0",
|
||||
"grunt": "^0.4.5",
|
||||
"grunt-browserify": "^3.5.0",
|
||||
"grunt-cli": "^0.1.13",
|
||||
"grunt-contrib-jshint": "^0.11.0",
|
||||
"grunt-contrib-uglify": "^0.9.1",
|
||||
"grunt-contrib-watch": "^0.6.1",
|
||||
"grunt-jscs": "^1.5.0",
|
||||
"grunt-karma": "^0.10.1",
|
||||
"grunt-mocha-test": "^0.12.7",
|
||||
"karma": "^0.12.31",
|
||||
"karma-browserify": "^4.0.0",
|
||||
"karma-chrome-launcher": "^0.1.7",
|
||||
"karma-firefox-launcher": "^0.1.4",
|
||||
"karma-html2js-preprocessor": "^0.1.0",
|
||||
"karma-junit-reporter": "^0.2.2",
|
||||
"karma-mocha": "^0.1.10",
|
||||
"karma-phantomjs-launcher": "^0.1.4",
|
||||
"karma-script-launcher": "^0.1.0",
|
||||
"browserify": "^16.5.0",
|
||||
"chai": "^4.2.0",
|
||||
"cookie-parser": "^1.3.4",
|
||||
"coveralls": "^3.0.2",
|
||||
"dirty-chai": "^2.0.1",
|
||||
"eslint": "^6.5.1",
|
||||
"eslint-config-loopback": "^13.1.0",
|
||||
"express-session": "^1.14.0",
|
||||
"grunt": "^1.0.1",
|
||||
"grunt-browserify": "^5.0.0",
|
||||
"grunt-cli": "^1.2.0",
|
||||
"grunt-contrib-uglify": "^4.0.1",
|
||||
"grunt-contrib-watch": "^1.0.0",
|
||||
"grunt-eslint": "^22.0.0",
|
||||
"grunt-karma": "^3.0.2",
|
||||
"grunt-mocha-test": "^0.13.3",
|
||||
"is-docker": "^2.0.0",
|
||||
"karma": "^4.1.0",
|
||||
"karma-browserify": "^6.0.0",
|
||||
"karma-chrome-launcher": "^3.1.0",
|
||||
"karma-es6-shim": "^1.0.0",
|
||||
"karma-firefox-launcher": "^1.0.0",
|
||||
"karma-html2js-preprocessor": "^1.0.0",
|
||||
"karma-junit-reporter": "^1.2.0",
|
||||
"karma-mocha": "^1.1.1",
|
||||
"karma-script-launcher": "^1.0.0",
|
||||
"loopback-boot": "^2.7.0",
|
||||
"loopback-datasource-juggler": "^2.19.1",
|
||||
"loopback-testing": "~1.1.0",
|
||||
"mocha": "^2.1.0",
|
||||
"sinon": "^1.13.0",
|
||||
"strong-task-emitter": "^0.0.6",
|
||||
"supertest": "^0.15.0"
|
||||
"loopback-context": "^1.0.0",
|
||||
"mocha": "^6.2.1",
|
||||
"nyc": "^14.1.1",
|
||||
"sinon": "^7.5.0",
|
||||
"sinon-chai": "^3.2.0",
|
||||
"strong-error-handler": "^3.0.0",
|
||||
"strong-task-emitter": "^0.0.8",
|
||||
"supertest": "^4.0.2",
|
||||
"which": "^2.0.1"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
@ -95,15 +107,25 @@
|
|||
"browser": {
|
||||
"express": "./lib/browser-express.js",
|
||||
"./lib/server-app.js": "./lib/browser-express.js",
|
||||
"./server/current-context.js": "./browser/current-context.js",
|
||||
"connect": false,
|
||||
"nodemailer": false,
|
||||
"supertest": false,
|
||||
"depd": "loopback-datasource-juggler/lib/browser.depd.js",
|
||||
"bcrypt": false
|
||||
},
|
||||
"config": {
|
||||
"ci": {
|
||||
"debug": "*,-mocha:*,-eslint:*"
|
||||
}
|
||||
},
|
||||
"copyright.owner": "IBM Corp.",
|
||||
"license": "MIT",
|
||||
"optionalDependencies": {
|
||||
"sl-blip": "http://blip.strongloop.com/loopback@2.26.2"
|
||||
"author": "IBM Corp.",
|
||||
"ci": {
|
||||
"downstreamIgnoreList": [
|
||||
"bluemix-service-broker",
|
||||
"gateway-director-bluemix",
|
||||
"plan-manager"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,138 +0,0 @@
|
|||
var juggler = require('loopback-datasource-juggler');
|
||||
var remoting = require('strong-remoting');
|
||||
var cls = require('continuation-local-storage');
|
||||
var domain = require('domain');
|
||||
|
||||
module.exports = function(loopback) {
|
||||
|
||||
/**
|
||||
* Get the current context object. The context is preserved
|
||||
* across async calls, it behaves like a thread-local storage.
|
||||
*
|
||||
* @returns {ChainedContext} The context object or null.
|
||||
*/
|
||||
loopback.getCurrentContext = function() {
|
||||
// A placeholder method, see loopback.createContext() for the real version
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Run the given function in such way that
|
||||
* `loopback.getCurrentContext` returns the
|
||||
* provided context object.
|
||||
*
|
||||
* **NOTE**
|
||||
*
|
||||
* The method is supported on the server only, it does not work
|
||||
* in the browser at the moment.
|
||||
*
|
||||
* @param {Function} fn The function to run, it will receive arguments
|
||||
* (currentContext, currentDomain).
|
||||
* @param {ChainedContext} context An optional context object.
|
||||
* When no value is provided, then the default global context is used.
|
||||
*/
|
||||
loopback.runInContext = function(fn, context) {
|
||||
var currentDomain = domain.create();
|
||||
currentDomain.oldBind = currentDomain.bind;
|
||||
currentDomain.bind = function(callback, context) {
|
||||
return currentDomain.oldBind(ns.bind(callback, context), context);
|
||||
};
|
||||
|
||||
var ns = context || loopback.createContext('loopback');
|
||||
|
||||
currentDomain.run(function() {
|
||||
ns.run(function executeInContext(context) {
|
||||
fn(ns, currentDomain);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a new LoopBackContext instance that can be used
|
||||
* for `loopback.runInContext`.
|
||||
*
|
||||
* **NOTES**
|
||||
*
|
||||
* At the moment, `loopback.getCurrentContext` supports
|
||||
* a single global context instance only. If you call `createContext()`
|
||||
* multiple times, `getCurrentContext` will return the last context
|
||||
* created.
|
||||
*
|
||||
* The method is supported on the server only, it does not work
|
||||
* in the browser at the moment.
|
||||
*
|
||||
* @param {String} scopeName An optional scope name.
|
||||
* @return {ChainedContext} The new context object.
|
||||
*/
|
||||
loopback.createContext = function(scopeName) {
|
||||
// Make the namespace globally visible via the process.context property
|
||||
process.context = process.context || {};
|
||||
var ns = process.context[scopeName];
|
||||
if (!ns) {
|
||||
ns = cls.createNamespace(scopeName);
|
||||
process.context[scopeName] = ns;
|
||||
// Set up loopback.getCurrentContext()
|
||||
loopback.getCurrentContext = function() {
|
||||
return ns && ns.active ? ns : null;
|
||||
};
|
||||
|
||||
chain(juggler);
|
||||
chain(remoting);
|
||||
}
|
||||
return ns;
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a chained context
|
||||
* @param {Object} child The child context
|
||||
* @param {Object} parent The parent context
|
||||
* @private
|
||||
* @constructor
|
||||
*/
|
||||
function ChainedContext(child, parent) {
|
||||
this.child = child;
|
||||
this.parent = parent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the value by name from the context. If it doesn't exist in the child
|
||||
* context, try the parent one
|
||||
* @param {String} name Name of the context property
|
||||
* @returns {*} Value of the context property
|
||||
* @private
|
||||
*/
|
||||
ChainedContext.prototype.get = function(name) {
|
||||
var val = this.child && this.child.get(name);
|
||||
if (val === undefined) {
|
||||
return this.parent && this.parent.get(name);
|
||||
}
|
||||
};
|
||||
|
||||
ChainedContext.prototype.set = function(name, val) {
|
||||
if (this.child) {
|
||||
return this.child.set(name, val);
|
||||
} else {
|
||||
return this.parent && this.parent.set(name, val);
|
||||
}
|
||||
};
|
||||
|
||||
ChainedContext.prototype.reset = function(name, val) {
|
||||
if (this.child) {
|
||||
return this.child.reset(name, val);
|
||||
} else {
|
||||
return this.parent && this.parent.reset(name, val);
|
||||
}
|
||||
};
|
||||
|
||||
function chain(child) {
|
||||
if (typeof child.getCurrentContext === 'function') {
|
||||
var childContext = new ChainedContext(child.getCurrentContext(),
|
||||
loopback.getCurrentContext());
|
||||
child.getCurrentContext = function() {
|
||||
return childContext;
|
||||
};
|
||||
} else {
|
||||
child.getCurrentContext = loopback.getCurrentContext;
|
||||
}
|
||||
}
|
||||
};
|
|
@ -1,52 +1,15 @@
|
|||
var loopback = require('../../lib/loopback');
|
||||
// Copyright IBM Corp. 2014,2019. All Rights Reserved.
|
||||
// Node module: loopback
|
||||
// This file is licensed under the MIT License.
|
||||
// License text available at https://opensource.org/licenses/MIT
|
||||
|
||||
module.exports = context;
|
||||
'use strict';
|
||||
const g = require('../../lib/globalize');
|
||||
|
||||
var name = 'loopback';
|
||||
|
||||
/**
|
||||
* Context middleware.
|
||||
* ```js
|
||||
* var app = loopback();
|
||||
* app.use(loopback.context(options);
|
||||
* app.use(loopback.rest());
|
||||
* app.listen();
|
||||
* ```
|
||||
* @options {Object} [options] Options for context
|
||||
* @property {String} name Context scope name.
|
||||
* @property {Boolean} enableHttpContext Whether HTTP context is enabled. Default is false.
|
||||
* @header loopback.context([options])
|
||||
*/
|
||||
|
||||
function context(options) {
|
||||
options = options || {};
|
||||
var scope = options.name || name;
|
||||
var enableHttpContext = options.enableHttpContext || false;
|
||||
var ns = loopback.createContext(scope);
|
||||
|
||||
// Return the middleware
|
||||
return function contextHandler(req, res, next) {
|
||||
if (req.loopbackContext) {
|
||||
return next();
|
||||
}
|
||||
|
||||
loopback.runInContext(function processRequestInContext(ns, domain) {
|
||||
req.loopbackContext = ns;
|
||||
|
||||
// Bind req/res event emitters to the given namespace
|
||||
ns.bindEmitter(req);
|
||||
ns.bindEmitter(res);
|
||||
|
||||
// Add req/res event emitters to the current domain
|
||||
domain.add(req);
|
||||
domain.add(res);
|
||||
|
||||
// Run the code in the context of the namespace
|
||||
if (enableHttpContext) {
|
||||
// Set up the transport context
|
||||
ns.set('http', {req: req, res: res});
|
||||
}
|
||||
next();
|
||||
});
|
||||
};
|
||||
}
|
||||
module.exports = function() {
|
||||
throw new Error(g.f(
|
||||
'%s middleware was removed in version 3.0. See %s for more details.',
|
||||
'loopback#context',
|
||||
'http://loopback.io/doc/en/lb2/Using-current-context.html',
|
||||
));
|
||||
};
|
||||
|
|
|
@ -1,16 +1,10 @@
|
|||
var expressErrorHandler = require('errorhandler');
|
||||
expressErrorHandler.title = 'Loopback';
|
||||
// Copyright IBM Corp. 2015,2018. All Rights Reserved.
|
||||
// Node module: loopback
|
||||
// This file is licensed under the MIT License.
|
||||
// License text available at https://opensource.org/licenses/MIT
|
||||
|
||||
module.exports = errorHandler;
|
||||
|
||||
function errorHandler(options) {
|
||||
if (!options || options.includeStack !== false) {
|
||||
return expressErrorHandler(options);
|
||||
} else {
|
||||
var baseHandler = expressErrorHandler(options);
|
||||
return function errorHandler(err, req, res, next) {
|
||||
delete err.stack;
|
||||
return baseHandler(err, req, res, next);
|
||||
};
|
||||
}
|
||||
}
|
||||
'use strict';
|
||||
module.exports = function(options) {
|
||||
throw new Error('loopback.errorHandler is no longer available.' +
|
||||
' Please use the module "strong-error-handler" instead.');
|
||||
};
|
||||
|
|
|
@ -1,5 +1,17 @@
|
|||
// Copyright IBM Corp. 2014,2019. All Rights Reserved.
|
||||
// Node module: loopback
|
||||
// This file is licensed under the MIT License.
|
||||
// License text available at https://opensource.org/licenses/MIT
|
||||
|
||||
'use strict';
|
||||
const favicon = require('serve-favicon');
|
||||
const path = require('path');
|
||||
|
||||
/**
|
||||
* Serve the LoopBack favicon.
|
||||
* @header loopback.favicon()
|
||||
*/
|
||||
module.exports = require('../../lib/express-middleware').favicon;
|
||||
module.exports = function(icon, options) {
|
||||
icon = icon || path.join(__dirname, '../../favicon.ico');
|
||||
return favicon(icon, options);
|
||||
};
|
||||
|
|
|
@ -1,10 +1,16 @@
|
|||
// Copyright IBM Corp. 2014,2019. All Rights Reserved.
|
||||
// Node module: loopback
|
||||
// This file is licensed under the MIT License.
|
||||
// License text available at https://opensource.org/licenses/MIT
|
||||
|
||||
/*!
|
||||
* Module dependencies.
|
||||
*/
|
||||
|
||||
var loopback = require('../../lib/loopback');
|
||||
var async = require('async');
|
||||
var deprecate = require('depd')('loopback');
|
||||
'use strict';
|
||||
const g = require('../../lib/globalize');
|
||||
const loopback = require('../../lib/loopback');
|
||||
const async = require('async');
|
||||
|
||||
/*!
|
||||
* Export the middleware.
|
||||
|
@ -19,45 +25,33 @@ module.exports = rest;
|
|||
* ```js
|
||||
* app.use(loopback.rest());
|
||||
* ```
|
||||
* For more information, see [Exposing models over a REST API](http://docs.strongloop.com/display/DOC/Exposing+models+over+a+REST+API).
|
||||
* For more information, see [Exposing models over a REST API](http://loopback.io/doc/en/lb2/Exposing-models-over-REST.html).
|
||||
* @header loopback.rest()
|
||||
*/
|
||||
|
||||
function rest() {
|
||||
var handlers; // Cached handlers
|
||||
let handlers; // Cached handlers
|
||||
|
||||
return function restApiHandler(req, res, next) {
|
||||
var app = req.app;
|
||||
var registry = app.registry;
|
||||
|
||||
// added for https://github.com/strongloop/loopback/issues/1134
|
||||
if (app.get('legacyExplorer') !== false) {
|
||||
deprecate(
|
||||
'Routes "/methods" and "/models" are considered dangerous and should not be used.\n' +
|
||||
'Disable them by setting "legacyExplorer=false" in "server/config.json" or via "app.set()".'
|
||||
);
|
||||
if (req.url === '/routes') {
|
||||
return res.send(app.handler('rest').adapter.allRoutes());
|
||||
} else if (req.url === '/models') {
|
||||
return res.send(app.remotes().toJSON());
|
||||
}
|
||||
}
|
||||
const app = req.app;
|
||||
const registry = app.registry;
|
||||
|
||||
if (!handlers) {
|
||||
handlers = [];
|
||||
var remotingOptions = app.get('remoting') || {};
|
||||
const remotingOptions = app.get('remoting') || {};
|
||||
|
||||
var contextOptions = remotingOptions.context;
|
||||
if (contextOptions !== false) {
|
||||
if (typeof contextOptions !== 'object') {
|
||||
contextOptions = {};
|
||||
}
|
||||
handlers.push(loopback.context(contextOptions));
|
||||
const contextOptions = remotingOptions.context;
|
||||
if (contextOptions !== undefined && contextOptions !== false) {
|
||||
throw new Error(g.f(
|
||||
'%s was removed in version 3.0. See %s for more details.',
|
||||
'remoting.context option',
|
||||
'http://loopback.io/doc/en/lb2/Using-current-context.html',
|
||||
));
|
||||
}
|
||||
|
||||
if (app.isAuthEnabled) {
|
||||
var AccessToken = registry.getModelByType('AccessToken');
|
||||
handlers.push(loopback.token({ model: AccessToken, app: app }));
|
||||
const AccessToken = registry.getModelByType('AccessToken');
|
||||
handlers.push(loopback.token({model: AccessToken, app: app}));
|
||||
}
|
||||
|
||||
handlers.push(function(req, res, next) {
|
||||
|
|
|
@ -1,3 +1,8 @@
|
|||
// Copyright IBM Corp. 2014,2018. All Rights Reserved.
|
||||
// Node module: loopback
|
||||
// This file is licensed under the MIT License.
|
||||
// License text available at https://opensource.org/licenses/MIT
|
||||
|
||||
/**
|
||||
* Serve static assets of a LoopBack application.
|
||||
*
|
||||
|
@ -8,4 +13,5 @@
|
|||
* for the full list of available options.
|
||||
* @header loopback.static(root, [options])
|
||||
*/
|
||||
'use strict';
|
||||
module.exports = require('express').static;
|
||||
|
|
|
@ -1,7 +1,13 @@
|
|||
// Copyright IBM Corp. 2014,2019. All Rights Reserved.
|
||||
// Node module: loopback
|
||||
// This file is licensed under the MIT License.
|
||||
// License text available at https://opensource.org/licenses/MIT
|
||||
|
||||
/*!
|
||||
* Export the middleware.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
module.exports = status;
|
||||
|
||||
/**
|
||||
|
@ -18,12 +24,12 @@ module.exports = status;
|
|||
* @header loopback.status()
|
||||
*/
|
||||
function status() {
|
||||
var started = new Date();
|
||||
const started = new Date();
|
||||
|
||||
return function(req, res) {
|
||||
res.send({
|
||||
started: started,
|
||||
uptime: (Date.now() - Number(started)) / 1000
|
||||
uptime: (Date.now() - Number(started)) / 1000,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,10 +1,17 @@
|
|||
// Copyright IBM Corp. 2014,2019. All Rights Reserved.
|
||||
// Node module: loopback
|
||||
// This file is licensed under the MIT License.
|
||||
// License text available at https://opensource.org/licenses/MIT
|
||||
|
||||
/*!
|
||||
* Module dependencies.
|
||||
*/
|
||||
|
||||
var loopback = require('../../lib/loopback');
|
||||
var assert = require('assert');
|
||||
var debug = require('debug')('loopback:middleware:token');
|
||||
'use strict';
|
||||
const g = require('../../lib/globalize');
|
||||
const loopback = require('../../lib/loopback');
|
||||
const assert = require('assert');
|
||||
const debug = require('debug')('loopback:middleware:token');
|
||||
|
||||
/*!
|
||||
* Export the middleware.
|
||||
|
@ -15,18 +22,34 @@ module.exports = token;
|
|||
/*
|
||||
* Rewrite the url to replace current user literal with the logged in user id
|
||||
*/
|
||||
function rewriteUserLiteral(req, currentUserLiteral) {
|
||||
if (req.accessToken && req.accessToken.userId && currentUserLiteral) {
|
||||
function rewriteUserLiteral(req, currentUserLiteral, next) {
|
||||
if (!currentUserLiteral) return next();
|
||||
const literalRegExp = new RegExp('/' + currentUserLiteral + '(/|$|\\?)', 'g');
|
||||
|
||||
if (req.accessToken && req.accessToken.userId) {
|
||||
// Replace /me/ with /current-user-id/
|
||||
var urlBeforeRewrite = req.url;
|
||||
req.url = req.url.replace(
|
||||
new RegExp('/' + currentUserLiteral + '(/|$|\\?)', 'g'),
|
||||
'/' + req.accessToken.userId + '$1');
|
||||
const urlBeforeRewrite = req.url;
|
||||
req.url = req.url.replace(literalRegExp,
|
||||
'/' + req.accessToken.userId + '$1');
|
||||
|
||||
if (req.url !== urlBeforeRewrite) {
|
||||
debug('req.url has been rewritten from %s to %s', urlBeforeRewrite,
|
||||
req.url);
|
||||
}
|
||||
} else if (!req.accessToken && literalRegExp.test(req.url)) {
|
||||
debug(
|
||||
'URL %s matches current-user literal %s,' +
|
||||
' but no (valid) access token was provided.',
|
||||
req.url, currentUserLiteral,
|
||||
);
|
||||
|
||||
const e = new Error(g.f('Authorization Required'));
|
||||
e.status = e.statusCode = 401;
|
||||
e.code = 'AUTHORIZATION_REQUIRED';
|
||||
return next(e);
|
||||
}
|
||||
|
||||
next();
|
||||
}
|
||||
|
||||
function escapeRegExp(str) {
|
||||
|
@ -62,16 +85,21 @@ function escapeRegExp(str) {
|
|||
* @property {Array} [headers] Array of header names.
|
||||
* @property {Array} [params] Array of param names.
|
||||
* @property {Boolean} [searchDefaultTokenKeys] Use the default search locations for Token in request
|
||||
* @property {Boolean} [enableDoublecheck] Execute middleware although an instance mounted earlier in the chain didn't find a token
|
||||
* @property {Boolean} [overwriteExistingToken] only has effect in combination with `enableDoublecheck`. If truthy, will allow to overwrite an existing accessToken.
|
||||
* @property {Function|String} [model] AccessToken model name or class to use.
|
||||
* @property {String} [currentUserLiteral] String literal for the current user.
|
||||
* @property {Boolean} [bearerTokenBase64Encoded] Defaults to `true`. For `Bearer` token based `Authorization` headers,
|
||||
* decode the value from `Base64`. If set to `false`, the decoding will be skipped and the token id will be the raw value
|
||||
* parsed from the header.
|
||||
* @header loopback.token([options])
|
||||
*/
|
||||
|
||||
function token(options) {
|
||||
options = options || {};
|
||||
var TokenModel;
|
||||
let TokenModel;
|
||||
|
||||
var currentUserLiteral = options.currentUserLiteral;
|
||||
let currentUserLiteral = options.currentUserLiteral;
|
||||
if (currentUserLiteral && (typeof currentUserLiteral !== 'string')) {
|
||||
debug('Set currentUserLiteral to \'me\' as the value is not a string.');
|
||||
currentUserLiteral = 'me';
|
||||
|
@ -80,32 +108,44 @@ function token(options) {
|
|||
currentUserLiteral = escapeRegExp(currentUserLiteral);
|
||||
}
|
||||
|
||||
if (options.bearerTokenBase64Encoded === undefined) {
|
||||
options.bearerTokenBase64Encoded = true;
|
||||
}
|
||||
const enableDoublecheck = !!options.enableDoublecheck;
|
||||
const overwriteExistingToken = !!options.overwriteExistingToken;
|
||||
|
||||
return function(req, res, next) {
|
||||
var app = req.app;
|
||||
var registry = app.registry;
|
||||
const app = req.app;
|
||||
const registry = app.registry;
|
||||
if (!TokenModel) {
|
||||
if (registry === loopback.registry) {
|
||||
TokenModel = options.model || loopback.AccessToken;
|
||||
} else if (options.model) {
|
||||
TokenModel = registry.getModel(options.model);
|
||||
} else {
|
||||
TokenModel = registry.getModel('AccessToken');
|
||||
}
|
||||
TokenModel = registry.getModel(options.model || 'AccessToken');
|
||||
}
|
||||
|
||||
assert(typeof TokenModel === 'function',
|
||||
'loopback.token() middleware requires a AccessToken model');
|
||||
|
||||
if (req.accessToken !== undefined) {
|
||||
rewriteUserLiteral(req, currentUserLiteral);
|
||||
return next();
|
||||
if (!enableDoublecheck) {
|
||||
// req.accessToken is defined already (might also be "null" or "false") and enableDoublecheck
|
||||
// has not been set --> skip searching for credentials
|
||||
return rewriteUserLiteral(req, currentUserLiteral, next);
|
||||
}
|
||||
if (req.accessToken && req.accessToken.id && !overwriteExistingToken) {
|
||||
// req.accessToken.id is defined, which means that some other middleware has identified a valid user.
|
||||
// when overwriteExistingToken is not set to a truthy value, skip searching for credentials.
|
||||
return rewriteUserLiteral(req, currentUserLiteral, next);
|
||||
}
|
||||
// continue normal operation (as if req.accessToken was undefined)
|
||||
}
|
||||
|
||||
TokenModel.findForRequest(req, options, function(err, token) {
|
||||
req.accessToken = token || null;
|
||||
rewriteUserLiteral(req, currentUserLiteral);
|
||||
var ctx = loopback.getCurrentContext();
|
||||
if (ctx) ctx.set('accessToken', token);
|
||||
next(err);
|
||||
|
||||
const ctx = req.loopbackContext;
|
||||
if (ctx && ctx.active) ctx.set('accessToken', token);
|
||||
|
||||
if (err) return next(err);
|
||||
rewriteUserLiteral(req, currentUserLiteral, next);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,8 +1,14 @@
|
|||
// Copyright IBM Corp. 2014,2019. All Rights Reserved.
|
||||
// Node module: loopback
|
||||
// This file is licensed under the MIT License.
|
||||
// License text available at https://opensource.org/licenses/MIT
|
||||
|
||||
/*!
|
||||
* Export the middleware.
|
||||
* See discussion in Connect pull request #954 for more details
|
||||
* https://github.com/senchalabs/connect/pull/954.
|
||||
*/
|
||||
'use strict';
|
||||
module.exports = urlNotFound;
|
||||
|
||||
/**
|
||||
|
@ -12,7 +18,7 @@ module.exports = urlNotFound;
|
|||
*/
|
||||
function urlNotFound() {
|
||||
return function raiseUrlNotFoundError(req, res, next) {
|
||||
var error = new Error('Cannot ' + req.method + ' ' + req.url);
|
||||
const error = new Error('Cannot ' + req.method + ' ' + req.url);
|
||||
error.status = 404;
|
||||
next(error);
|
||||
};
|
||||
|
|
|
@ -1,17 +1,20 @@
|
|||
/*jshint -W030 */
|
||||
// Copyright IBM Corp. 2013,2019. All Rights Reserved.
|
||||
// Node module: loopback
|
||||
// This file is licensed under the MIT License.
|
||||
// License text available at https://opensource.org/licenses/MIT
|
||||
|
||||
var loopback = require('../');
|
||||
var lt = require('loopback-testing');
|
||||
var path = require('path');
|
||||
var ACCESS_CONTROL_APP = path.join(__dirname, 'fixtures', 'access-control');
|
||||
var app = require(path.join(ACCESS_CONTROL_APP, 'server/server.js'));
|
||||
var assert = require('assert');
|
||||
var USER = {email: 'test@test.test', password: 'test'};
|
||||
var CURRENT_USER = {email: 'current@test.test', password: 'test'};
|
||||
var debug = require('debug')('loopback:test:access-control.integration');
|
||||
'use strict';
|
||||
const loopback = require('../');
|
||||
const lt = require('./helpers/loopback-testing-helper');
|
||||
const path = require('path');
|
||||
const ACCESS_CONTROL_APP = path.join(__dirname, 'fixtures', 'access-control');
|
||||
const app = require(path.join(ACCESS_CONTROL_APP, 'server/server.js'));
|
||||
const assert = require('assert');
|
||||
const USER = {email: 'test@test.test', password: 'test'};
|
||||
const CURRENT_USER = {email: 'current@test.test', password: 'test'};
|
||||
const debug = require('debug')('loopback:test:access-control.integration');
|
||||
|
||||
describe('access control - integration', function() {
|
||||
|
||||
lt.beforeEach.withApp(app);
|
||||
|
||||
/*
|
||||
|
@ -61,7 +64,6 @@ describe('access control - integration', function() {
|
|||
*/
|
||||
|
||||
describe('/users', function() {
|
||||
|
||||
lt.beforeEach.givenModel('user', USER, 'randomUser');
|
||||
|
||||
lt.it.shouldBeDeniedWhenCalledAnonymously('GET', '/api/users');
|
||||
|
@ -73,10 +75,12 @@ describe('access control - integration', function() {
|
|||
lt.it.shouldBeDeniedWhenCalledByUser(CURRENT_USER, 'GET', urlForUser);
|
||||
|
||||
lt.it.shouldBeAllowedWhenCalledAnonymously(
|
||||
'POST', '/api/users', newUserData());
|
||||
'POST', '/api/users', newUserData(),
|
||||
);
|
||||
|
||||
lt.it.shouldBeAllowedWhenCalledByUser(
|
||||
CURRENT_USER, 'POST', '/api/users', newUserData());
|
||||
CURRENT_USER, 'POST', '/api/users', newUserData(),
|
||||
);
|
||||
|
||||
lt.it.shouldBeAllowedWhenCalledByUser(CURRENT_USER, 'POST', '/api/users/logout');
|
||||
|
||||
|
@ -106,15 +110,25 @@ describe('access control - integration', function() {
|
|||
this.res.statusCode,
|
||||
this.res.headers,
|
||||
this.res.text);
|
||||
var user = this.res.body;
|
||||
const user = this.res.body;
|
||||
assert.equal(user.password, undefined);
|
||||
});
|
||||
});
|
||||
|
||||
// user has replaceOnPUT = false; so then both PUT and PATCH should be allowed for update
|
||||
lt.describe.whenCalledRemotely('PUT', '/api/users/:id', function() {
|
||||
lt.it.shouldBeAllowed();
|
||||
});
|
||||
|
||||
lt.describe.whenCalledRemotely('PATCH', '/api/users/:id', function() {
|
||||
lt.it.shouldBeAllowed();
|
||||
});
|
||||
});
|
||||
|
||||
lt.it.shouldBeDeniedWhenCalledAnonymously('POST', '/api/users/upsertWithWhere');
|
||||
lt.it.shouldBeDeniedWhenCalledUnauthenticated('POST', '/api/users/upsertWithWhere');
|
||||
lt.it.shouldBeDeniedWhenCalledByUser(CURRENT_USER, 'POST', '/api/users/upsertWithWhere');
|
||||
|
||||
lt.it.shouldBeDeniedWhenCalledAnonymously('DELETE', urlForUser);
|
||||
lt.it.shouldBeDeniedWhenCalledUnauthenticated('DELETE', urlForUser);
|
||||
lt.it.shouldBeDeniedWhenCalledByUser(CURRENT_USER, 'DELETE', urlForUser);
|
||||
|
@ -123,17 +137,46 @@ describe('access control - integration', function() {
|
|||
return '/api/users/' + this.randomUser.id;
|
||||
}
|
||||
|
||||
var userCounter;
|
||||
var userCounter; // eslint-disable-line no-var
|
||||
function newUserData() {
|
||||
userCounter = userCounter ? ++userCounter : 1;
|
||||
|
||||
return {
|
||||
email: 'new-' + userCounter + '@test.test',
|
||||
password: 'test'
|
||||
password: 'test',
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
describe('/banks', function() {
|
||||
const SPECIAL_USER = {email: 'special@test.test', password: 'test'};
|
||||
|
||||
// define dynamic role that would only grant access when the authenticated user's email is equal to
|
||||
// SPECIAL_USER's email
|
||||
|
||||
before(function() {
|
||||
const roleModel = app.registry.getModel('Role');
|
||||
const userModel = app.registry.getModel('user');
|
||||
|
||||
roleModel.registerResolver('$dynamic-role', function(role, context, callback) {
|
||||
if (!(context && context.accessToken && context.accessToken.userId)) {
|
||||
return process.nextTick(function() {
|
||||
if (callback) callback(null, false);
|
||||
});
|
||||
}
|
||||
const accessToken = context.accessToken;
|
||||
userModel.findById(accessToken.userId, function(err, user) {
|
||||
if (err) {
|
||||
return callback(err, false);
|
||||
}
|
||||
if (user && user.email === SPECIAL_USER.email) {
|
||||
return callback(null, true);
|
||||
}
|
||||
return callback(null, false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
lt.beforeEach.givenModel('bank');
|
||||
|
||||
lt.it.shouldBeAllowedWhenCalledAnonymously('GET', '/api/banks');
|
||||
|
@ -155,67 +198,93 @@ describe('access control - integration', function() {
|
|||
lt.it.shouldBeDeniedWhenCalledAnonymously('DELETE', urlForBank);
|
||||
lt.it.shouldBeDeniedWhenCalledUnauthenticated('DELETE', urlForBank);
|
||||
lt.it.shouldBeDeniedWhenCalledByUser(CURRENT_USER, 'DELETE', urlForBank);
|
||||
lt.it.shouldBeAllowedWhenCalledByUser(SPECIAL_USER, 'DELETE', urlForBank);
|
||||
|
||||
lt.it.shouldBeDeniedWhenCalledAnonymously('POST', '/api/banks/upsertWithWhere');
|
||||
lt.it.shouldBeDeniedWhenCalledUnauthenticated('POST', '/api/banks/upsertWithWhere');
|
||||
lt.it.shouldBeDeniedWhenCalledByUser(CURRENT_USER, 'POST', '/api/banks/upsertWithWhere');
|
||||
|
||||
function urlForBank() {
|
||||
return '/api/banks/' + this.bank.id;
|
||||
}
|
||||
});
|
||||
|
||||
describe('/accounts', function() {
|
||||
var count = 0;
|
||||
describe('/accounts with replaceOnPUT true', function() {
|
||||
let count = 0;
|
||||
before(function() {
|
||||
var roleModel = loopback.getModelByType(loopback.Role);
|
||||
const roleModel = loopback.getModelByType(loopback.Role);
|
||||
roleModel.registerResolver('$dummy', function(role, context, callback) {
|
||||
process.nextTick(function() {
|
||||
if (context.remotingContext) {
|
||||
count++;
|
||||
}
|
||||
callback && callback(null, false); // Always true
|
||||
if (callback) callback(null, false); // Always true
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
lt.beforeEach.givenModel('account');
|
||||
lt.beforeEach.givenModel('accountWithReplaceOnPUTtrue');
|
||||
|
||||
lt.it.shouldBeDeniedWhenCalledAnonymously('GET', '/api/accounts');
|
||||
lt.it.shouldBeDeniedWhenCalledUnauthenticated('GET', '/api/accounts');
|
||||
lt.it.shouldBeDeniedWhenCalledByUser(CURRENT_USER, 'GET', '/api/accounts');
|
||||
lt.it.shouldBeDeniedWhenCalledAnonymously('GET', '/api/accounts-replacing');
|
||||
lt.it.shouldBeDeniedWhenCalledUnauthenticated('GET', '/api/accounts-replacing');
|
||||
lt.it.shouldBeDeniedWhenCalledByUser(CURRENT_USER, 'GET', '/api/accounts-replacing');
|
||||
|
||||
lt.it.shouldBeDeniedWhenCalledAnonymously('GET', urlForAccount);
|
||||
lt.it.shouldBeDeniedWhenCalledUnauthenticated('GET', urlForAccount);
|
||||
lt.it.shouldBeDeniedWhenCalledByUser(CURRENT_USER, 'GET', urlForAccount);
|
||||
|
||||
lt.it.shouldBeDeniedWhenCalledAnonymously('POST', '/api/accounts');
|
||||
lt.it.shouldBeDeniedWhenCalledUnauthenticated('POST', '/api/accounts');
|
||||
lt.it.shouldBeDeniedWhenCalledByUser(CURRENT_USER, 'POST', '/api/accounts');
|
||||
lt.it.shouldBeDeniedWhenCalledAnonymously('POST', '/api/accounts-replacing');
|
||||
lt.it.shouldBeDeniedWhenCalledUnauthenticated('POST', '/api/accounts-replacing');
|
||||
lt.it.shouldBeDeniedWhenCalledByUser(CURRENT_USER, 'POST', '/api/accounts-replacing');
|
||||
|
||||
lt.it.shouldBeDeniedWhenCalledAnonymously('POST', urlForReplaceAccountPOST);
|
||||
lt.it.shouldBeDeniedWhenCalledUnauthenticated('POST', urlForReplaceAccountPOST);
|
||||
lt.it.shouldBeDeniedWhenCalledByUser(CURRENT_USER, 'POST', urlForReplaceAccountPOST);
|
||||
|
||||
lt.it.shouldBeDeniedWhenCalledAnonymously('PUT', urlForAccount);
|
||||
lt.it.shouldBeDeniedWhenCalledUnauthenticated('PUT', urlForAccount);
|
||||
lt.it.shouldBeDeniedWhenCalledByUser(CURRENT_USER, 'PUT', urlForAccount);
|
||||
|
||||
lt.describe.whenLoggedInAsUser(CURRENT_USER, function() {
|
||||
beforeEach(function(done) {
|
||||
var self = this;
|
||||
lt.it.shouldBeDeniedWhenCalledAnonymously('PATCH', urlForAccount);
|
||||
lt.it.shouldBeDeniedWhenCalledUnauthenticated('PATCH', urlForAccount);
|
||||
lt.it.shouldBeDeniedWhenCalledByUser(CURRENT_USER, 'PATCH', urlForAccount);
|
||||
|
||||
lt.describe.whenLoggedInAsUser(CURRENT_USER, function() {
|
||||
let actId;
|
||||
beforeEach(function(done) {
|
||||
const self = this;
|
||||
// Create an account under the given user
|
||||
app.models.account.create({
|
||||
app.models.accountWithReplaceOnPUTtrue.create({
|
||||
userId: self.user.id,
|
||||
balance: 100
|
||||
balance: 100,
|
||||
}, function(err, act) {
|
||||
self.url = '/api/accounts/' + act.id;
|
||||
actId = act.id;
|
||||
self.url = '/api/accounts-replacing/' + actId;
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
lt.describe.whenCalledRemotely('PUT', '/api/accounts/:id', function() {
|
||||
lt.describe.whenCalledRemotely('PATCH', '/api/accounts-replacing/:id', function() {
|
||||
lt.it.shouldBeAllowed();
|
||||
});
|
||||
lt.describe.whenCalledRemotely('GET', '/api/accounts/:id', function() {
|
||||
lt.describe.whenCalledRemotely('PUT', '/api/accounts-replacing/:id', function() {
|
||||
lt.it.shouldBeAllowed();
|
||||
});
|
||||
lt.describe.whenCalledRemotely('DELETE', '/api/accounts/:id', function() {
|
||||
lt.describe.whenCalledRemotely('GET', '/api/accounts-replacing/:id', function() {
|
||||
lt.it.shouldBeAllowed();
|
||||
});
|
||||
lt.describe.whenCalledRemotely('DELETE', '/api/accounts-replacing/:id', function() {
|
||||
lt.it.shouldBeDenied();
|
||||
});
|
||||
describe('replace on POST verb', function() {
|
||||
beforeEach(function(done) {
|
||||
this.url = '/api/accounts-replacing/' + actId + '/replace';
|
||||
done();
|
||||
});
|
||||
lt.describe.whenCalledRemotely('POST', '/api/accounts-replacing/:id/replace', function() {
|
||||
lt.it.shouldBeAllowed();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
lt.it.shouldBeDeniedWhenCalledAnonymously('DELETE', urlForAccount);
|
||||
|
@ -223,8 +292,76 @@ describe('access control - integration', function() {
|
|||
lt.it.shouldBeDeniedWhenCalledByUser(CURRENT_USER, 'DELETE', urlForAccount);
|
||||
|
||||
function urlForAccount() {
|
||||
return '/api/accounts/' + this.account.id;
|
||||
return '/api/accounts-replacing/' + this.accountWithReplaceOnPUTtrue.id;
|
||||
}
|
||||
function urlForReplaceAccountPOST() {
|
||||
return '/api/accounts-replacing/' + this.accountWithReplaceOnPUTtrue.id + '/replace';
|
||||
}
|
||||
});
|
||||
|
||||
describe('/accounts with replaceOnPUT false', function() {
|
||||
lt.beforeEach.givenModel('accountWithReplaceOnPUTfalse');
|
||||
lt.it.shouldBeDeniedWhenCalledAnonymously('POST', urlForReplaceAccountPOST);
|
||||
lt.it.shouldBeDeniedWhenCalledUnauthenticated('POST', urlForReplaceAccountPOST);
|
||||
lt.it.shouldBeDeniedWhenCalledByUser(CURRENT_USER, 'POST', urlForReplaceAccountPOST);
|
||||
|
||||
lt.it.shouldBeDeniedWhenCalledAnonymously('PUT', urlForAccount);
|
||||
lt.it.shouldBeDeniedWhenCalledUnauthenticated('PUT', urlForAccount);
|
||||
lt.it.shouldBeDeniedWhenCalledByUser(CURRENT_USER, 'PUT', urlForAccount);
|
||||
|
||||
lt.it.shouldBeDeniedWhenCalledAnonymously('PATCH', urlForAccount);
|
||||
lt.it.shouldBeDeniedWhenCalledUnauthenticated('PATCH', urlForAccount);
|
||||
lt.it.shouldBeDeniedWhenCalledByUser(CURRENT_USER, 'PATCH', urlForAccount);
|
||||
|
||||
lt.describe.whenLoggedInAsUser(CURRENT_USER, function() {
|
||||
let actId;
|
||||
beforeEach(function(done) {
|
||||
const self = this;
|
||||
// Create an account under the given user
|
||||
app.models.accountWithReplaceOnPUTfalse.create({
|
||||
userId: self.user.id,
|
||||
balance: 100,
|
||||
}, function(err, act) {
|
||||
actId = act.id;
|
||||
self.url = '/api/accounts-updating/' + actId;
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
lt.describe.whenCalledRemotely('PATCH', '/api/accounts-updating/:id', function() {
|
||||
lt.it.shouldBeAllowed();
|
||||
});
|
||||
|
||||
lt.describe.whenCalledRemotely('PUT', '/api/accounts-updating/:id', function() {
|
||||
lt.it.shouldBeAllowed();
|
||||
});
|
||||
lt.describe.whenCalledRemotely('GET', '/api/accounts-updating/:id', function() {
|
||||
lt.it.shouldBeAllowed();
|
||||
});
|
||||
lt.describe.whenCalledRemotely('DELETE', '/api/accounts-updating/:id', function() {
|
||||
lt.it.shouldBeDenied();
|
||||
});
|
||||
|
||||
describe('replace on POST verb', function() {
|
||||
beforeEach(function(done) {
|
||||
this.url = '/api/accounts-updating/' + actId + '/replace';
|
||||
done();
|
||||
});
|
||||
lt.describe.whenCalledRemotely('POST', '/api/accounts-updating/:id/replace', function() {
|
||||
lt.it.shouldBeAllowed();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
lt.it.shouldBeDeniedWhenCalledAnonymously('DELETE', urlForAccount);
|
||||
lt.it.shouldBeDeniedWhenCalledUnauthenticated('DELETE', urlForAccount);
|
||||
lt.it.shouldBeDeniedWhenCalledByUser(CURRENT_USER, 'DELETE', urlForAccount);
|
||||
|
||||
function urlForAccount() {
|
||||
return '/api/accounts-updating/' + this.accountWithReplaceOnPUTfalse.id;
|
||||
}
|
||||
function urlForReplaceAccountPOST() {
|
||||
return '/api/accounts-updating/' + this.accountWithReplaceOnPUTfalse.id + '/replace';
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,21 +1,128 @@
|
|||
var loopback = require('../');
|
||||
var extend = require('util')._extend;
|
||||
var Token = loopback.AccessToken.extend('MyToken');
|
||||
var ds = loopback.createDataSource({connector: loopback.Memory});
|
||||
Token.attachTo(ds);
|
||||
var ACL = loopback.ACL;
|
||||
// Copyright IBM Corp. 2013,2019. All Rights Reserved.
|
||||
// Node module: loopback
|
||||
// This file is licensed under the MIT License.
|
||||
// License text available at https://opensource.org/licenses/MIT
|
||||
|
||||
'use strict';
|
||||
const assert = require('assert');
|
||||
const expect = require('./helpers/expect');
|
||||
const cookieParser = require('cookie-parser');
|
||||
const LoopBackContext = require('loopback-context');
|
||||
const contextMiddleware = require('loopback-context').perRequest;
|
||||
const loopback = require('../');
|
||||
const extend = require('util')._extend;
|
||||
const session = require('express-session');
|
||||
const request = require('supertest');
|
||||
|
||||
let Token, ACL, User, TestModel;
|
||||
|
||||
describe('loopback.token(options)', function() {
|
||||
beforeEach(createTestingToken);
|
||||
let app;
|
||||
beforeEach(function(done) {
|
||||
app = loopback({localRegistry: true, loadBuiltinModels: true});
|
||||
app.dataSource('db', {connector: 'memory'});
|
||||
|
||||
it('should populate req.token from the query string', function(done) {
|
||||
ACL = app.registry.getModel('ACL');
|
||||
app.model(ACL, {dataSource: 'db'});
|
||||
|
||||
User = app.registry.getModel('User');
|
||||
app.model(User, {dataSource: 'db'});
|
||||
|
||||
Token = app.registry.createModel({
|
||||
name: 'MyToken',
|
||||
base: 'AccessToken',
|
||||
});
|
||||
app.model(Token, {dataSource: 'db'});
|
||||
|
||||
TestModel = app.registry.createModel({
|
||||
name: 'TestModel',
|
||||
base: 'Model',
|
||||
});
|
||||
TestModel.getToken = function(options, cb) {
|
||||
cb(null, options && options.accessToken || null);
|
||||
};
|
||||
TestModel.remoteMethod('getToken', {
|
||||
accepts: {arg: 'options', type: 'object', http: 'optionsFromRequest'},
|
||||
returns: {arg: 'token', type: 'object'},
|
||||
http: {verb: 'GET', path: '/token'},
|
||||
});
|
||||
app.model(TestModel, {dataSource: 'db'});
|
||||
|
||||
createTestingToken.call(this, done);
|
||||
});
|
||||
|
||||
it('defaults to built-in AccessToken model', function() {
|
||||
const BuiltInToken = app.registry.getModel('AccessToken');
|
||||
app.model(BuiltInToken, {dataSource: 'db'});
|
||||
|
||||
app.enableAuth({dataSource: 'db'});
|
||||
app.use(loopback.token());
|
||||
app.use(loopback.rest());
|
||||
|
||||
return BuiltInToken.create({userId: 123}).then(function(token) {
|
||||
return request(app)
|
||||
.get('/TestModels/token?_format=json')
|
||||
.set('authorization', token.id)
|
||||
.expect(200)
|
||||
.expect('Content-Type', /json/)
|
||||
.then(res => {
|
||||
expect(res.body.token.id).to.eql(token.id);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('uses correct custom AccessToken model from model class param', function() {
|
||||
User.hasMany(Token, {
|
||||
as: 'accessTokens',
|
||||
options: {disableInclude: true},
|
||||
});
|
||||
|
||||
app.enableAuth();
|
||||
app.use(loopback.token({model: Token}));
|
||||
app.use(loopback.rest());
|
||||
|
||||
return Token.create({userId: 123}).then(function(token) {
|
||||
return request(app)
|
||||
.get('/TestModels/token?_format=json')
|
||||
.set('authorization', token.id)
|
||||
.expect(200)
|
||||
.expect('Content-Type', /json/)
|
||||
.then(res => {
|
||||
expect(res.body.token.id).to.eql(token.id);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('uses correct custom AccessToken model from string param', function() {
|
||||
User.hasMany(Token, {
|
||||
as: 'accessTokens',
|
||||
options: {disableInclude: true},
|
||||
});
|
||||
|
||||
app.enableAuth();
|
||||
app.use(loopback.token({model: Token.modelName}));
|
||||
app.use(loopback.rest());
|
||||
|
||||
return Token.create({userId: 123}).then(function(token) {
|
||||
return request(app)
|
||||
.get('/TestModels/token?_format=json')
|
||||
.set('authorization', token.id)
|
||||
.expect(200)
|
||||
.expect('Content-Type', /json/)
|
||||
.then(res => {
|
||||
expect(res.body.token.id).to.eql(token.id);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('populates req.token from the query string', function(done) {
|
||||
createTestAppAndRequest(this.token, done)
|
||||
.get('/?access_token=' + this.token.id)
|
||||
.expect(200)
|
||||
.end(done);
|
||||
});
|
||||
|
||||
it('should populate req.token from an authorization header', function(done) {
|
||||
it('populates req.token from an authorization header', function(done) {
|
||||
createTestAppAndRequest(this.token, done)
|
||||
.get('/')
|
||||
.set('authorization', this.token.id)
|
||||
|
@ -23,7 +130,7 @@ describe('loopback.token(options)', function() {
|
|||
.end(done);
|
||||
});
|
||||
|
||||
it('should populate req.token from an X-Access-Token header', function(done) {
|
||||
it('populates req.token from an X-Access-Token header', function(done) {
|
||||
createTestAppAndRequest(this.token, done)
|
||||
.get('/')
|
||||
.set('X-Access-Token', this.token.id)
|
||||
|
@ -31,43 +138,55 @@ describe('loopback.token(options)', function() {
|
|||
.end(done);
|
||||
});
|
||||
|
||||
it('should not search default keys when searchDefaultTokenKeys is false',
|
||||
function(done) {
|
||||
var tokenId = this.token.id;
|
||||
var app = createTestApp(
|
||||
this.token,
|
||||
{ token: { searchDefaultTokenKeys: false } },
|
||||
done);
|
||||
var agent = request.agent(app);
|
||||
it('does not search default keys when searchDefaultTokenKeys is false',
|
||||
function(done) {
|
||||
const tokenId = this.token.id;
|
||||
const app = createTestApp(
|
||||
this.token,
|
||||
{token: {searchDefaultTokenKeys: false}},
|
||||
done,
|
||||
);
|
||||
const agent = request.agent(app);
|
||||
|
||||
// Set the token cookie
|
||||
agent.get('/token').expect(200).end(function(err, res) {
|
||||
if (err) return done(err);
|
||||
// Set the token cookie
|
||||
agent.get('/token').expect(200).end(function(err, res) {
|
||||
if (err) return done(err);
|
||||
|
||||
// Make a request that sets the token in all places searched by default
|
||||
agent.get('/check-access?access_token=' + tokenId)
|
||||
.set('X-Access-Token', tokenId)
|
||||
.set('authorization', tokenId)
|
||||
// Make a request that sets the token in all places searched by default
|
||||
agent.get('/check-access?access_token=' + tokenId)
|
||||
.set('X-Access-Token', tokenId)
|
||||
.set('authorization', tokenId)
|
||||
// Expect 401 because there is no (non-default) place configured where
|
||||
// the middleware should load the token from
|
||||
.expect(401)
|
||||
.expect(401)
|
||||
.end(done);
|
||||
});
|
||||
});
|
||||
|
||||
it('populates req.token from an authorization header with bearer token with base64',
|
||||
function(done) {
|
||||
let token = this.token.id;
|
||||
token = 'Bearer ' + new Buffer(token).toString('base64');
|
||||
createTestAppAndRequest(this.token, done)
|
||||
.get('/')
|
||||
.set('authorization', token)
|
||||
.expect(200)
|
||||
.end(done);
|
||||
});
|
||||
});
|
||||
|
||||
it('should populate req.token from an authorization header with bearer token', function(done) {
|
||||
var token = this.token.id;
|
||||
token = 'Bearer ' + new Buffer(token).toString('base64');
|
||||
createTestAppAndRequest(this.token, done)
|
||||
it('populates req.token from an authorization header with bearer token', function(done) {
|
||||
let token = this.token.id;
|
||||
token = 'Bearer ' + token;
|
||||
createTestAppAndRequest(this.token, {token: {bearerTokenBase64Encoded: false}}, done)
|
||||
.get('/')
|
||||
.set('authorization', token)
|
||||
.expect(200)
|
||||
.end(done);
|
||||
});
|
||||
|
||||
describe('populating req.toen from HTTP Basic Auth formatted authorization header', function() {
|
||||
describe('populating req.token from HTTP Basic Auth formatted authorization header', function() {
|
||||
it('parses "standalone-token"', function(done) {
|
||||
var token = this.token.id;
|
||||
let token = this.token.id;
|
||||
token = 'Basic ' + new Buffer(token).toString('base64');
|
||||
createTestAppAndRequest(this.token, done)
|
||||
.get('/')
|
||||
|
@ -77,7 +196,7 @@ describe('loopback.token(options)', function() {
|
|||
});
|
||||
|
||||
it('parses "token-and-empty-password:"', function(done) {
|
||||
var token = this.token.id + ':';
|
||||
let token = this.token.id + ':';
|
||||
token = 'Basic ' + new Buffer(token).toString('base64');
|
||||
createTestAppAndRequest(this.token, done)
|
||||
.get('/')
|
||||
|
@ -87,7 +206,7 @@ describe('loopback.token(options)', function() {
|
|||
});
|
||||
|
||||
it('parses "ignored-user:token-is-password"', function(done) {
|
||||
var token = 'username:' + this.token.id;
|
||||
let token = 'username:' + this.token.id;
|
||||
token = 'Basic ' + new Buffer(token).toString('base64');
|
||||
createTestAppAndRequest(this.token, done)
|
||||
.get('/')
|
||||
|
@ -97,7 +216,7 @@ describe('loopback.token(options)', function() {
|
|||
});
|
||||
|
||||
it('parses "token-is-username:ignored-password"', function(done) {
|
||||
var token = this.token.id + ':password';
|
||||
let token = this.token.id + ':password';
|
||||
token = 'Basic ' + new Buffer(token).toString('base64');
|
||||
createTestAppAndRequest(this.token, done)
|
||||
.get('/')
|
||||
|
@ -107,8 +226,8 @@ describe('loopback.token(options)', function() {
|
|||
});
|
||||
});
|
||||
|
||||
it('should populate req.token from a secure cookie', function(done) {
|
||||
var app = createTestApp(this.token, done);
|
||||
it('populates req.token from a secure cookie', function(done) {
|
||||
const app = createTestApp(this.token, done);
|
||||
|
||||
request(app)
|
||||
.get('/token')
|
||||
|
@ -120,9 +239,9 @@ describe('loopback.token(options)', function() {
|
|||
});
|
||||
});
|
||||
|
||||
it('should populate req.token from a header or a secure cookie', function(done) {
|
||||
var app = createTestApp(this.token, done);
|
||||
var id = this.token.id;
|
||||
it('populates req.token from a header or a secure cookie', function(done) {
|
||||
const app = createTestApp(this.token, done);
|
||||
const id = this.token.id;
|
||||
request(app)
|
||||
.get('/token')
|
||||
.end(function(err, res) {
|
||||
|
@ -134,58 +253,92 @@ describe('loopback.token(options)', function() {
|
|||
});
|
||||
});
|
||||
|
||||
it('should rewrite url for the current user literal at the end without query',
|
||||
it('rewrites url for the current user literal at the end without query',
|
||||
function(done) {
|
||||
var app = createTestApp(this.token, done);
|
||||
var id = this.token.id;
|
||||
var userId = this.token.userId;
|
||||
const app = createTestApp(this.token, done);
|
||||
const id = this.token.id;
|
||||
const userId = this.token.userId;
|
||||
request(app)
|
||||
.get('/users/me')
|
||||
.set('authorization', id)
|
||||
.end(function(err, res) {
|
||||
assert(!err);
|
||||
assert.deepEqual(res.body, {userId: userId});
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should rewrite url for the current user literal at the end with query',
|
||||
it('rewrites url for the current user literal at the end with query',
|
||||
function(done) {
|
||||
var app = createTestApp(this.token, done);
|
||||
var id = this.token.id;
|
||||
var userId = this.token.userId;
|
||||
const app = createTestApp(this.token, done);
|
||||
const id = this.token.id;
|
||||
const userId = this.token.userId;
|
||||
request(app)
|
||||
.get('/users/me?state=1')
|
||||
.set('authorization', id)
|
||||
.end(function(err, res) {
|
||||
assert(!err);
|
||||
assert.deepEqual(res.body, {userId: userId, state: 1});
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should rewrite url for the current user literal in the middle',
|
||||
it('rewrites url for the current user literal in the middle',
|
||||
function(done) {
|
||||
var app = createTestApp(this.token, done);
|
||||
var id = this.token.id;
|
||||
var userId = this.token.userId;
|
||||
const app = createTestApp(this.token, done);
|
||||
const id = this.token.id;
|
||||
const userId = this.token.userId;
|
||||
request(app)
|
||||
.get('/users/me/1')
|
||||
.set('authorization', id)
|
||||
.end(function(err, res) {
|
||||
assert(!err);
|
||||
assert.deepEqual(res.body, {userId: userId, state: 1});
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should skip when req.token is already present', function(done) {
|
||||
var tokenStub = { id: 'stub id' };
|
||||
it('generates a 401 on a current user literal route without an authToken',
|
||||
function(done) {
|
||||
const app = createTestApp(null, done);
|
||||
request(app)
|
||||
.get('/users/me')
|
||||
.set('authorization', null)
|
||||
.expect(401)
|
||||
.end(done);
|
||||
});
|
||||
|
||||
it('generates a 401 on a current user literal route with empty authToken',
|
||||
function(done) {
|
||||
const app = createTestApp(null, done);
|
||||
request(app)
|
||||
.get('/users/me')
|
||||
.set('authorization', '')
|
||||
.expect(401)
|
||||
.end(done);
|
||||
});
|
||||
|
||||
it('generates a 401 on a current user literal route with invalid authToken',
|
||||
function(done) {
|
||||
const app = createTestApp(this.token, done);
|
||||
request(app)
|
||||
.get('/users/me')
|
||||
.set('Authorization', 'invald-token-id')
|
||||
.expect(401)
|
||||
.end(done);
|
||||
});
|
||||
|
||||
it('skips when req.token is already present', function(done) {
|
||||
const tokenStub = {id: 'stub id'};
|
||||
app.use(function(req, res, next) {
|
||||
req.accessToken = tokenStub;
|
||||
|
||||
next();
|
||||
});
|
||||
app.use(loopback.token({ model: Token }));
|
||||
app.use(loopback.token({model: Token}));
|
||||
app.get('/', function(req, res, next) {
|
||||
res.send(req.accessToken);
|
||||
});
|
||||
|
@ -195,44 +348,257 @@ describe('loopback.token(options)', function() {
|
|||
.expect(200)
|
||||
.end(function(err, res) {
|
||||
if (err) return done(err);
|
||||
|
||||
expect(res.body).to.eql(tokenStub);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
describe('loading multiple instances of token middleware', function() {
|
||||
it('skips when req.token is already present and no further options are set',
|
||||
function(done) {
|
||||
const tokenStub = {id: 'stub id'};
|
||||
app.use(function(req, res, next) {
|
||||
req.accessToken = tokenStub;
|
||||
|
||||
next();
|
||||
});
|
||||
app.use(loopback.token({model: Token}));
|
||||
app.get('/', function(req, res, next) {
|
||||
res.send(req.accessToken);
|
||||
});
|
||||
|
||||
request(app).get('/')
|
||||
.set('Authorization', this.token.id)
|
||||
.expect(200)
|
||||
.end(function(err, res) {
|
||||
if (err) return done(err);
|
||||
|
||||
expect(res.body).to.eql(tokenStub);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('does not overwrite valid existing token (has "id" property) ' +
|
||||
' when overwriteExistingToken is falsy',
|
||||
function(done) {
|
||||
const tokenStub = {id: 'stub id'};
|
||||
app.use(function(req, res, next) {
|
||||
req.accessToken = tokenStub;
|
||||
|
||||
next();
|
||||
});
|
||||
app.use(loopback.token({
|
||||
model: Token,
|
||||
enableDoublecheck: true,
|
||||
}));
|
||||
app.get('/', function(req, res, next) {
|
||||
res.send(req.accessToken);
|
||||
});
|
||||
|
||||
request(app).get('/')
|
||||
.set('Authorization', this.token.id)
|
||||
.expect(200)
|
||||
.end(function(err, res) {
|
||||
if (err) return done(err);
|
||||
|
||||
expect(res.body).to.eql(tokenStub);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('overwrites invalid existing token (is !== undefined and has no "id" property) ' +
|
||||
' when enableDoublecheck is true',
|
||||
function(done) {
|
||||
const token = this.token;
|
||||
app.use(function(req, res, next) {
|
||||
req.accessToken = null;
|
||||
next();
|
||||
});
|
||||
|
||||
app.use(loopback.token({
|
||||
model: Token,
|
||||
enableDoublecheck: true,
|
||||
}));
|
||||
|
||||
app.get('/', function(req, res, next) {
|
||||
res.send(req.accessToken);
|
||||
});
|
||||
|
||||
request(app).get('/')
|
||||
.set('Authorization', token.id)
|
||||
.expect(200)
|
||||
.end(function(err, res) {
|
||||
if (err) return done(err);
|
||||
expect(res.body).to.eql({
|
||||
id: token.id,
|
||||
ttl: token.ttl,
|
||||
userId: token.userId,
|
||||
created: token.created.toJSON(),
|
||||
});
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('overwrites existing token when enableDoublecheck ' +
|
||||
'and overwriteExistingToken options are truthy',
|
||||
function(done) {
|
||||
const token = this.token;
|
||||
const tokenStub = {id: 'stub id'};
|
||||
app.use(function(req, res, next) {
|
||||
req.accessToken = tokenStub;
|
||||
|
||||
next();
|
||||
});
|
||||
app.use(loopback.token({
|
||||
model: Token,
|
||||
enableDoublecheck: true,
|
||||
overwriteExistingToken: true,
|
||||
}));
|
||||
app.get('/', function(req, res, next) {
|
||||
res.send(req.accessToken);
|
||||
});
|
||||
|
||||
request(app).get('/')
|
||||
.set('Authorization', token.id)
|
||||
.expect(200)
|
||||
.end(function(err, res) {
|
||||
if (err) return done(err);
|
||||
|
||||
expect(res.body).to.eql({
|
||||
id: token.id,
|
||||
ttl: token.ttl,
|
||||
userId: token.userId,
|
||||
created: token.created.toJSON(),
|
||||
});
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('AccessToken', function() {
|
||||
beforeEach(createTestingToken);
|
||||
|
||||
it('should auto-generate id', function() {
|
||||
it('has getIdForRequest method', function() {
|
||||
expect(typeof Token.getIdForRequest).to.eql('function');
|
||||
});
|
||||
|
||||
it('has resolve method', function() {
|
||||
expect(typeof Token.resolve).to.eql('function');
|
||||
});
|
||||
|
||||
it('generates id automatically', function() {
|
||||
assert(this.token.id);
|
||||
assert.equal(this.token.id.length, 64);
|
||||
});
|
||||
|
||||
it('should auto-generate created date', function() {
|
||||
it('generates created date automatically', function() {
|
||||
assert(this.token.created);
|
||||
assert(Object.prototype.toString.call(this.token.created), '[object Date]');
|
||||
});
|
||||
|
||||
it('should be validateable', function(done) {
|
||||
this.token.validate(function(err, isValid) {
|
||||
assert(isValid);
|
||||
done();
|
||||
describe('.validate()', function() {
|
||||
it('accepts valid tokens', function(done) {
|
||||
this.token.validate(function(err, isValid) {
|
||||
assert(isValid);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('rejects eternal TTL by default', function(done) {
|
||||
this.token.ttl = -1;
|
||||
this.token.validate(function(err, isValid) {
|
||||
if (err) return done(err);
|
||||
expect(isValid, 'isValid').to.equal(false);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('allows eternal tokens when enabled by User.allowEternalTokens',
|
||||
function(done) {
|
||||
const Token = givenLocalTokenModel();
|
||||
|
||||
// Overwrite User settings - enable eternal tokens
|
||||
Token.app.models.User.settings.allowEternalTokens = true;
|
||||
|
||||
Token.create({userId: '123', ttl: -1}, function(err, token) {
|
||||
if (err) return done(err);
|
||||
token.validate(function(err, isValid) {
|
||||
if (err) return done(err);
|
||||
expect(isValid, 'isValid').to.equal(true);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('.findForRequest()', function() {
|
||||
beforeEach(createTestingToken);
|
||||
|
||||
it('supports two-arg variant with no options', function(done) {
|
||||
var expectedTokenId = this.token.id;
|
||||
var req = mockRequest({
|
||||
headers: { 'authorization': expectedTokenId }
|
||||
const expectedTokenId = this.token.id;
|
||||
const req = mockRequest({
|
||||
headers: {'authorization': expectedTokenId},
|
||||
});
|
||||
|
||||
Token.findForRequest(req, function(err, token) {
|
||||
if (err) return done(err);
|
||||
|
||||
expect(token.id).to.eql(expectedTokenId);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('allows getIdForRequest() to be overridden', function(done) {
|
||||
const expectedTokenId = this.token.id;
|
||||
const current = Token.getIdForRequest;
|
||||
let called = false;
|
||||
Token.getIdForRequest = function(req, options) {
|
||||
called = true;
|
||||
return expectedTokenId;
|
||||
};
|
||||
const req = mockRequest({
|
||||
headers: {'authorization': 'dummy'},
|
||||
});
|
||||
|
||||
Token.findForRequest(req, function(err, token) {
|
||||
Token.getIdForRequest = current;
|
||||
if (err) return done(err);
|
||||
|
||||
expect(token.id).to.eql(expectedTokenId);
|
||||
expect(called).to.be.true();
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('allows resolve() to be overridden', function(done) {
|
||||
const expectedTokenId = this.token.id;
|
||||
const current = Token.resolve;
|
||||
let called = false;
|
||||
Token.resolve = function(id, cb) {
|
||||
called = true;
|
||||
process.nextTick(function() {
|
||||
cb(null, {id: expectedTokenId});
|
||||
});
|
||||
};
|
||||
const req = mockRequest({
|
||||
headers: {'authorization': expectedTokenId},
|
||||
});
|
||||
|
||||
Token.findForRequest(req, function(err, token) {
|
||||
Token.validate = current;
|
||||
if (err) return done(err);
|
||||
|
||||
expect(token.id).to.eql(expectedTokenId);
|
||||
expect(called).to.be.true();
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
@ -247,14 +613,34 @@ describe('AccessToken', function() {
|
|||
|
||||
// express helpers
|
||||
param: function(name) { return this._params[name]; },
|
||||
header: function(name) { return this.headers[name]; }
|
||||
header: function(name) { return this.headers[name]; },
|
||||
},
|
||||
opts);
|
||||
opts,
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('app.enableAuth()', function() {
|
||||
let app;
|
||||
beforeEach(function setupAuthWithModels() {
|
||||
app = loopback({localRegistry: true, loadBuiltinModels: true});
|
||||
app.dataSource('db', {connector: 'memory'});
|
||||
|
||||
Token = app.registry.createModel({
|
||||
name: 'MyToken',
|
||||
base: 'AccessToken',
|
||||
});
|
||||
app.model(Token, {dataSource: 'db'});
|
||||
|
||||
ACL = app.registry.getModel('ACL');
|
||||
|
||||
// Fix User's "hasMany accessTokens" relation to use our new MyToken model
|
||||
const User = app.registry.getModel('User');
|
||||
User.settings.relations.accessTokens.model = 'MyToken';
|
||||
|
||||
app.enableAuth({dataSource: 'db'});
|
||||
});
|
||||
beforeEach(createTestingToken);
|
||||
|
||||
it('prevents remote call with 401 status on denied ACL', function(done) {
|
||||
|
@ -266,15 +652,17 @@ describe('app.enableAuth()', function() {
|
|||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
var errorResponse = res.body.error;
|
||||
|
||||
const errorResponse = res.body.error;
|
||||
assert(errorResponse);
|
||||
assert.equal(errorResponse.code, 'AUTHORIZATION_REQUIRED');
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('prevent remote call with app setting status on denied ACL', function(done) {
|
||||
createTestAppAndRequest(this.token, {app:{aclErrorStatus:403}}, done)
|
||||
it('denies remote call with app setting status 403', function(done) {
|
||||
createTestAppAndRequest(this.token, {app: {aclErrorStatus: 403}}, done)
|
||||
.del('/tests/123')
|
||||
.expect(403)
|
||||
.set('authorization', this.token.id)
|
||||
|
@ -282,15 +670,17 @@ describe('app.enableAuth()', function() {
|
|||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
var errorResponse = res.body.error;
|
||||
|
||||
const errorResponse = res.body.error;
|
||||
assert(errorResponse);
|
||||
assert.equal(errorResponse.code, 'ACCESS_DENIED');
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('prevent remote call with app setting status on denied ACL', function(done) {
|
||||
createTestAppAndRequest(this.token, {model:{aclErrorStatus:404}}, done)
|
||||
it('denies remote call with app setting status 404', function(done) {
|
||||
createTestAppAndRequest(this.token, {model: {aclErrorStatus: 404}}, done)
|
||||
.del('/tests/123')
|
||||
.expect(404)
|
||||
.set('authorization', this.token.id)
|
||||
|
@ -298,14 +688,16 @@ describe('app.enableAuth()', function() {
|
|||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
var errorResponse = res.body.error;
|
||||
|
||||
const errorResponse = res.body.error;
|
||||
assert(errorResponse);
|
||||
assert.equal(errorResponse.code, 'MODEL_NOT_FOUND');
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('prevent remote call if the accessToken is missing and required', function(done) {
|
||||
it('prevents remote call if the accessToken is missing and required', function(done) {
|
||||
createTestAppAndRequest(null, done)
|
||||
.del('/tests/123')
|
||||
.expect(401)
|
||||
|
@ -314,32 +706,34 @@ describe('app.enableAuth()', function() {
|
|||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
var errorResponse = res.body.error;
|
||||
|
||||
const errorResponse = res.body.error;
|
||||
assert(errorResponse);
|
||||
assert.equal(errorResponse.code, 'AUTHORIZATION_REQUIRED');
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('stores token in the context', function(done) {
|
||||
var TestModel = loopback.createModel('TestModel', { base: 'Model' });
|
||||
const TestModel = app.registry.createModel('TestModel', {base: 'Model'});
|
||||
TestModel.getToken = function(cb) {
|
||||
cb(null, loopback.getCurrentContext().get('accessToken') || null);
|
||||
const ctx = LoopBackContext.getCurrentContext();
|
||||
cb(null, ctx && ctx.get('accessToken') || null);
|
||||
};
|
||||
TestModel.remoteMethod('getToken', {
|
||||
returns: { arg: 'token', type: 'object' },
|
||||
http: { verb: 'GET', path: '/token' }
|
||||
returns: {arg: 'token', type: 'object'},
|
||||
http: {verb: 'GET', path: '/token'},
|
||||
});
|
||||
|
||||
var app = loopback();
|
||||
app.model(TestModel, { dataSource: null });
|
||||
app.model(TestModel, {dataSource: null});
|
||||
|
||||
app.enableAuth();
|
||||
app.use(loopback.context());
|
||||
app.use(loopback.token({ model: Token }));
|
||||
app.use(contextMiddleware());
|
||||
app.use(loopback.token({model: Token}));
|
||||
app.use(loopback.rest());
|
||||
|
||||
var token = this.token;
|
||||
const token = this.token;
|
||||
request(app)
|
||||
.get('/TestModels/token?_format=json')
|
||||
.set('authorization', token.id)
|
||||
|
@ -347,42 +741,71 @@ describe('app.enableAuth()', function() {
|
|||
.expect('Content-Type', /json/)
|
||||
.end(function(err, res) {
|
||||
if (err) return done(err);
|
||||
|
||||
expect(res.body.token.id).to.eql(token.id);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
// See https://github.com/strongloop/loopback-context/issues/6
|
||||
it('checks whether context is active', function(done) {
|
||||
app.enableAuth();
|
||||
app.use(contextMiddleware());
|
||||
app.use(session({
|
||||
secret: 'kitty',
|
||||
saveUninitialized: true,
|
||||
resave: true,
|
||||
}));
|
||||
app.use(loopback.token({model: Token}));
|
||||
app.get('/', function(req, res) { res.send('OK'); });
|
||||
app.use(loopback.rest());
|
||||
|
||||
request(app)
|
||||
.get('/')
|
||||
.set('authorization', this.token.id)
|
||||
.set('cookie', 'connect.sid=s%3AFTyno9_MbGTJuOwdh9bxsYCVxlhlulTZ.' +
|
||||
'PZvp85jzLXZBCBkhCsSfuUjhij%2Fb0B1K2RYZdxSQU0c')
|
||||
.expect(200, 'OK')
|
||||
.end(done);
|
||||
});
|
||||
});
|
||||
|
||||
function createTestingToken(done) {
|
||||
var test = this;
|
||||
const test = this;
|
||||
Token.create({userId: '123'}, function(err, token) {
|
||||
if (err) return done(err);
|
||||
|
||||
test.token = token;
|
||||
|
||||
done();
|
||||
});
|
||||
}
|
||||
|
||||
function createTestAppAndRequest(testToken, settings, done) {
|
||||
var app = createTestApp(testToken, settings, done);
|
||||
const app = createTestApp(testToken, settings, done);
|
||||
return request(app);
|
||||
}
|
||||
|
||||
function createTestApp(testToken, settings, done) {
|
||||
done = arguments[arguments.length - 1];
|
||||
if (settings == done) settings = {};
|
||||
settings = settings || {};
|
||||
if (!done && typeof settings === 'function') {
|
||||
done = settings;
|
||||
settings = {};
|
||||
}
|
||||
|
||||
var appSettings = settings.app || {};
|
||||
var modelSettings = settings.model || {};
|
||||
var tokenSettings = extend({
|
||||
const appSettings = settings.app || {};
|
||||
const modelSettings = settings.model || {};
|
||||
const tokenSettings = extend({
|
||||
model: Token,
|
||||
currentUserLiteral: 'me'
|
||||
currentUserLiteral: 'me',
|
||||
}, settings.token);
|
||||
|
||||
var app = loopback();
|
||||
const app = loopback({localRegistry: true, loadBuiltinModels: true});
|
||||
app.dataSource('db', {connector: 'memory'});
|
||||
|
||||
app.use(loopback.cookieParser('secret'));
|
||||
app.use(cookieParser('secret'));
|
||||
app.use(loopback.token(tokenSettings));
|
||||
app.set('remoting', {errorHandler: {debug: true, log: false}});
|
||||
app.get('/token', function(req, res) {
|
||||
res.cookie('authorization', testToken.id, {signed: true});
|
||||
res.cookie('access_token', testToken.id, {signed: true});
|
||||
|
@ -401,7 +824,7 @@ function createTestApp(testToken, settings, done) {
|
|||
res.status(req.accessToken ? 200 : 401).end();
|
||||
});
|
||||
app.use('/users/:uid', function(req, res) {
|
||||
var result = {userId: req.params.uid};
|
||||
const result = {userId: req.params.uid};
|
||||
if (req.query.state) {
|
||||
result.state = req.query.state;
|
||||
} else if (req.url !== '/') {
|
||||
|
@ -410,32 +833,43 @@ function createTestApp(testToken, settings, done) {
|
|||
res.status(200).send(result);
|
||||
});
|
||||
app.use(loopback.rest());
|
||||
app.enableAuth();
|
||||
app.enableAuth({dataSource: 'db'});
|
||||
|
||||
Object.keys(appSettings).forEach(function(key) {
|
||||
app.set(key, appSettings[key]);
|
||||
});
|
||||
|
||||
var modelOptions = {
|
||||
const modelOptions = {
|
||||
acls: [
|
||||
{
|
||||
principalType: 'ROLE',
|
||||
principalId: '$everyone',
|
||||
accessType: ACL.ALL,
|
||||
permission: ACL.DENY,
|
||||
property: 'deleteById'
|
||||
}
|
||||
]
|
||||
property: 'deleteById',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
Object.keys(modelSettings).forEach(function(key) {
|
||||
modelOptions[key] = modelSettings[key];
|
||||
});
|
||||
|
||||
var TestModel = loopback.PersistedModel.extend('test', {}, modelOptions);
|
||||
|
||||
TestModel.attachTo(loopback.memory());
|
||||
app.model(TestModel);
|
||||
const TestModel = app.registry.createModel('test', {}, modelOptions);
|
||||
app.model(TestModel, {dataSource: 'db'});
|
||||
|
||||
return app;
|
||||
}
|
||||
|
||||
function givenLocalTokenModel() {
|
||||
const app = loopback({localRegistry: true, loadBuiltinModels: true});
|
||||
app.dataSource('db', {connector: 'memory'});
|
||||
|
||||
const User = app.registry.getModel('User');
|
||||
app.model(User, {dataSource: 'db'});
|
||||
|
||||
const Token = app.registry.getModel('AccessToken');
|
||||
app.model(Token, {dataSource: 'db'});
|
||||
|
||||
return Token;
|
||||
}
|
||||
|
|
765
test/acl.test.js
765
test/acl.test.js
|
@ -1,225 +1,371 @@
|
|||
var assert = require('assert');
|
||||
var loopback = require('../index');
|
||||
var Scope = loopback.Scope;
|
||||
var ACL = loopback.ACL;
|
||||
var Role = loopback.Role;
|
||||
var RoleMapping = loopback.RoleMapping;
|
||||
var User = loopback.User;
|
||||
var testModel;
|
||||
// Copyright IBM Corp. 2013,2019. All Rights Reserved.
|
||||
// Node module: loopback
|
||||
// This file is licensed under the MIT License.
|
||||
// License text available at https://opensource.org/licenses/MIT
|
||||
|
||||
function checkResult(err, result) {
|
||||
// console.log(err, result);
|
||||
assert(!err);
|
||||
}
|
||||
'use strict';
|
||||
const assert = require('assert');
|
||||
const expect = require('./helpers/expect');
|
||||
const loopback = require('../index');
|
||||
const Scope = loopback.Scope;
|
||||
const ACL = loopback.ACL;
|
||||
const request = require('supertest');
|
||||
const Promise = require('bluebird');
|
||||
const supertest = require('supertest');
|
||||
const Role = loopback.Role;
|
||||
const RoleMapping = loopback.RoleMapping;
|
||||
const User = loopback.User;
|
||||
const async = require('async');
|
||||
|
||||
var ds = null;
|
||||
before(function() {
|
||||
ds = loopback.createDataSource({connector: loopback.Memory});
|
||||
// Speed up the password hashing algorithm for tests
|
||||
User.settings.saltWorkFactor = 4;
|
||||
|
||||
let ds = null;
|
||||
let testModel;
|
||||
|
||||
describe('ACL model', function() {
|
||||
it('provides DEFAULT_SCOPE constant', () => {
|
||||
expect(ACL).to.have.property('DEFAULT_SCOPE', 'DEFAULT');
|
||||
});
|
||||
});
|
||||
|
||||
describe('security scopes', function() {
|
||||
beforeEach(function() {
|
||||
var ds = this.ds = loopback.createDataSource({connector: loopback.Memory});
|
||||
testModel = loopback.PersistedModel.extend('testModel');
|
||||
ACL.attachTo(ds);
|
||||
Role.attachTo(ds);
|
||||
RoleMapping.attachTo(ds);
|
||||
User.attachTo(ds);
|
||||
Scope.attachTo(ds);
|
||||
testModel.attachTo(ds);
|
||||
});
|
||||
beforeEach(setupTestModels);
|
||||
|
||||
it('should allow access to models for the given scope by wildcard', function() {
|
||||
Scope.create({name: 'userScope', description: 'access user information'}, function(err, scope) {
|
||||
ACL.create({principalType: ACL.SCOPE, principalId: scope.id, model: 'User', property: ACL.ALL,
|
||||
accessType: ACL.ALL, permission: ACL.ALLOW},
|
||||
function(err, resource) {
|
||||
Scope.checkPermission('userScope', 'User', ACL.ALL, ACL.ALL, checkResult);
|
||||
Scope.checkPermission('userScope', 'User', 'name', ACL.ALL, checkResult);
|
||||
Scope.checkPermission('userScope', 'User', 'name', ACL.READ, checkResult);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
it('should allow access to models for the given scope', function() {
|
||||
Scope.create({name: 'testModelScope', description: 'access testModel information'}, function(err, scope) {
|
||||
ACL.create({principalType: ACL.SCOPE, principalId: scope.id,
|
||||
model: 'testModel', property: 'name', accessType: ACL.READ, permission: ACL.ALLOW},
|
||||
function(err, resource) {
|
||||
ACL.create({principalType: ACL.SCOPE, principalId: scope.id,
|
||||
model: 'testModel', property: 'name', accessType: ACL.WRITE, permission: ACL.DENY},
|
||||
function(err, resource) {
|
||||
// console.log(resource);
|
||||
Scope.checkPermission('testModelScope', 'testModel', ACL.ALL, ACL.ALL, function(err, perm) {
|
||||
assert(perm.permission === ACL.DENY); // because name.WRITE == DENY
|
||||
});
|
||||
Scope.checkPermission('testModelScope', 'testModel', 'name', ACL.ALL, function(err, perm) {
|
||||
assert(perm.permission === ACL.DENY); // because name.WRITE == DENY
|
||||
});
|
||||
Scope.checkPermission('testModelScope', 'testModel', 'name', ACL.READ, function(err, perm) {
|
||||
assert(perm.permission === ACL.ALLOW);
|
||||
});
|
||||
Scope.checkPermission('testModelScope', 'testModel', 'name', ACL.WRITE, function(err, perm) {
|
||||
assert(perm.permission === ACL.DENY);
|
||||
});
|
||||
});
|
||||
it('should allow access to models for the given scope by wildcard', function(done) {
|
||||
Scope.create({name: 'userScope', description: 'access user information'},
|
||||
function(err, scope) {
|
||||
ACL.create({
|
||||
principalType: ACL.SCOPE, principalId: scope.id,
|
||||
model: 'User', property: ACL.ALL,
|
||||
accessType: ACL.ALL, permission: ACL.ALLOW,
|
||||
}, function(err, resource) {
|
||||
async.parallel([
|
||||
cb => Scope.checkPermission('userScope', 'User', ACL.ALL, ACL.ALL, cb),
|
||||
cb => Scope.checkPermission('userScope', 'User', 'name', ACL.ALL, cb),
|
||||
cb => Scope.checkPermission('userScope', 'User', 'name', ACL.READ, cb),
|
||||
], (err) => {
|
||||
assert.ifError(err);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
it('should allow access to models for the given scope', function(done) {
|
||||
Scope.create({name: 'testModelScope', description: 'access testModel information'},
|
||||
function(err, scope) {
|
||||
ACL.create({
|
||||
principalType: ACL.SCOPE, principalId: scope.id,
|
||||
model: 'testModel', property: 'name',
|
||||
accessType: ACL.READ, permission: ACL.ALLOW,
|
||||
}, function(err, resource) {
|
||||
ACL.create({principalType: ACL.SCOPE, principalId: scope.id,
|
||||
model: 'testModel', property: 'name',
|
||||
accessType: ACL.WRITE, permission: ACL.DENY,
|
||||
}, function(err, resource) {
|
||||
async.parallel([
|
||||
cb => Scope.checkPermission('testModelScope', 'testModel', ACL.ALL, ACL.ALL, cb),
|
||||
cb => Scope.checkPermission('testModelScope', 'testModel', 'name', ACL.ALL, cb),
|
||||
cb => Scope.checkPermission('testModelScope', 'testModel', 'name', ACL.READ, cb),
|
||||
cb => Scope.checkPermission('testModelScope', 'testModel', 'name', ACL.WRITE, cb),
|
||||
], (err, perms) => {
|
||||
if (err) return done(err);
|
||||
assert.deepEqual(perms.map(p => p.permission), [
|
||||
ACL.DENY,
|
||||
ACL.DENY,
|
||||
ACL.ALLOW,
|
||||
ACL.DENY,
|
||||
]);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('security ACLs', function() {
|
||||
beforeEach(setupTestModels);
|
||||
|
||||
it('supports checkPermission() returning a promise', function() {
|
||||
return ACL.create({
|
||||
principalType: ACL.USER,
|
||||
principalId: 'u001',
|
||||
model: 'testModel',
|
||||
property: ACL.ALL,
|
||||
accessType: ACL.ALL,
|
||||
permission: ACL.ALLOW,
|
||||
})
|
||||
.then(function() {
|
||||
return ACL.checkPermission(ACL.USER, 'u001', 'testModel', 'name', ACL.ALL);
|
||||
})
|
||||
.then(function(access) {
|
||||
assert(access.permission === ACL.ALLOW);
|
||||
});
|
||||
});
|
||||
|
||||
it('supports ACL rules with a wildcard for models', function() {
|
||||
const A_USER_ID = 'a-test-user';
|
||||
|
||||
// By default, access is allowed to all users
|
||||
return assertPermission(ACL.ALLOW, 'initial state')
|
||||
// An ACL rule applying to all models denies access to everybody
|
||||
.then(() => ACL.create({
|
||||
model: '*',
|
||||
property: '*',
|
||||
accessType: '*',
|
||||
principalType: 'ROLE',
|
||||
principalId: '$everyone',
|
||||
permission: 'DENY',
|
||||
}))
|
||||
.then(() => assertPermission(ACL.DENY, 'all denied'))
|
||||
// A rule for a specific model overrides the rule matching all models
|
||||
.then(() => ACL.create({
|
||||
model: testModel.modelName,
|
||||
property: '*',
|
||||
accessType: '*',
|
||||
principalType: ACL.USER,
|
||||
principalId: A_USER_ID,
|
||||
permission: ACL.ALLOW,
|
||||
}))
|
||||
.then(() => assertPermission(ACL.ALLOW, 'only a single model allowed'));
|
||||
|
||||
function assertPermission(expectedPermission, msg) {
|
||||
return ACL.checkAccessForContext({
|
||||
principals: [{type: ACL.USER, id: A_USER_ID}],
|
||||
model: testModel.modelName,
|
||||
accessType: ACL.ALL,
|
||||
}).then(accessContext => {
|
||||
const actual = accessContext.isAllowed() ? ACL.ALLOW : ACL.DENY;
|
||||
expect(actual, msg).to.equal(expectedPermission);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
it('supports checkAccessForContext() returning a promise', function() {
|
||||
const testModel = ds.createModel('testModel', {
|
||||
acls: [
|
||||
{principalType: ACL.USER, principalId: 'u001',
|
||||
accessType: ACL.ALL, permission: ACL.ALLOW},
|
||||
],
|
||||
});
|
||||
|
||||
return ACL.checkAccessForContext({
|
||||
principals: [{type: ACL.USER, id: 'u001'}],
|
||||
model: 'testModel',
|
||||
accessType: ACL.ALL,
|
||||
})
|
||||
.then(function(access) {
|
||||
assert(access.permission === ACL.ALLOW);
|
||||
});
|
||||
});
|
||||
|
||||
it('should order ACL entries based on the matching score', function() {
|
||||
var acls = [
|
||||
let acls = [
|
||||
{
|
||||
'model': 'account',
|
||||
'accessType': '*',
|
||||
'permission': 'DENY',
|
||||
'principalType': 'ROLE',
|
||||
'principalId': '$everyone'
|
||||
'principalId': '$everyone',
|
||||
},
|
||||
{
|
||||
'model': 'account',
|
||||
'accessType': '*',
|
||||
'permission': 'ALLOW',
|
||||
'principalType': 'ROLE',
|
||||
'principalId': '$owner'
|
||||
'principalId': '$owner',
|
||||
},
|
||||
{
|
||||
'model': 'account',
|
||||
'accessType': 'READ',
|
||||
'permission': 'ALLOW',
|
||||
'principalType': 'ROLE',
|
||||
'principalId': '$everyone'
|
||||
'principalId': '$everyone',
|
||||
}];
|
||||
var req = {
|
||||
const req = {
|
||||
model: 'account',
|
||||
property: 'find',
|
||||
accessType: 'WRITE'
|
||||
accessType: 'WRITE',
|
||||
};
|
||||
|
||||
acls = acls.map(function(a) { return new ACL(a); });
|
||||
|
||||
var perm = ACL.resolvePermission(acls, req);
|
||||
assert.deepEqual(perm, { model: 'account',
|
||||
const perm = ACL.resolvePermission(acls, req);
|
||||
// remove the registry from AccessRequest instance to ease asserting
|
||||
delete perm.registry;
|
||||
assert.deepEqual(perm, {model: 'account',
|
||||
property: 'find',
|
||||
accessType: 'WRITE',
|
||||
permission: 'ALLOW',
|
||||
methodNames: []});
|
||||
|
||||
// NOTE: when fixed in chaijs, use this implement rather than modifying
|
||||
// the resolved access request
|
||||
//
|
||||
// expect(perm).to.deep.include({
|
||||
// model: 'account',
|
||||
// property: 'find',
|
||||
// accessType: 'WRITE',
|
||||
// permission: 'ALLOW',
|
||||
// methodNames: [],
|
||||
// });
|
||||
});
|
||||
|
||||
it('should order ACL entries based on the matching score even with wildcard req', function() {
|
||||
let acls = [
|
||||
{
|
||||
'model': 'account',
|
||||
'accessType': '*',
|
||||
'permission': 'DENY',
|
||||
'principalType': 'ROLE',
|
||||
'principalId': '$everyone',
|
||||
},
|
||||
{
|
||||
'model': 'account',
|
||||
'accessType': '*',
|
||||
'permission': 'ALLOW',
|
||||
'principalType': 'ROLE',
|
||||
'principalId': '$owner',
|
||||
}];
|
||||
const req = {
|
||||
model: 'account',
|
||||
property: '*',
|
||||
accessType: 'WRITE',
|
||||
};
|
||||
|
||||
acls = acls.map(function(a) { return new ACL(a); });
|
||||
|
||||
const perm = ACL.resolvePermission(acls, req);
|
||||
// remove the registry from AccessRequest instance to ease asserting.
|
||||
// Check the above test case for more info.
|
||||
delete perm.registry;
|
||||
assert.deepEqual(perm, {model: 'account',
|
||||
property: '*',
|
||||
accessType: 'WRITE',
|
||||
permission: 'ALLOW',
|
||||
methodNames: []});
|
||||
});
|
||||
|
||||
it('should allow access to models for the given principal by wildcard', function() {
|
||||
it('should allow access to models for the given principal by wildcard', function(done) {
|
||||
// jscs:disable validateIndentation
|
||||
ACL.create({principalType: ACL.USER, principalId: 'u001', model: 'User', property: ACL.ALL,
|
||||
accessType: ACL.ALL, permission: ACL.ALLOW}, function(err, acl) {
|
||||
|
||||
ACL.create({principalType: ACL.USER, principalId: 'u001', model: 'User', property: ACL.ALL,
|
||||
accessType: ACL.READ, permission: ACL.DENY}, function(err, acl) {
|
||||
|
||||
ACL.checkPermission(ACL.USER, 'u001', 'User', 'name', ACL.READ, function(err, perm) {
|
||||
assert(perm.permission === ACL.DENY);
|
||||
});
|
||||
|
||||
ACL.checkPermission(ACL.USER, 'u001', 'User', 'name', ACL.ALL, function(err, perm) {
|
||||
assert(perm.permission === ACL.DENY);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
it('should allow access to models by exception', function() {
|
||||
ACL.create({principalType: ACL.USER, principalId: 'u001', model: 'testModel', property: ACL.ALL,
|
||||
accessType: ACL.ALL, permission: ACL.DENY}, function(err, acl) {
|
||||
|
||||
ACL.create({principalType: ACL.USER, principalId: 'u001', model: 'testModel', property: ACL.ALL,
|
||||
accessType: ACL.READ, permission: ACL.ALLOW}, function(err, acl) {
|
||||
|
||||
ACL.create({principalType: ACL.USER, principalId: 'u002', model: 'testModel', property: ACL.ALL,
|
||||
accessType: ACL.EXECUTE, permission: ACL.ALLOW}, function(err, acl) {
|
||||
|
||||
ACL.checkPermission(ACL.USER, 'u001', 'testModel', 'name', ACL.READ, function(err, perm) {
|
||||
assert(perm.permission === ACL.ALLOW);
|
||||
});
|
||||
|
||||
ACL.checkPermission(ACL.USER, 'u001', 'testModel', ACL.ALL, ACL.READ, function(err, perm) {
|
||||
assert(perm.permission === ACL.ALLOW);
|
||||
});
|
||||
|
||||
ACL.checkPermission(ACL.USER, 'u001', 'testModel', 'name', ACL.WRITE, function(err, perm) {
|
||||
assert(perm.permission === ACL.DENY);
|
||||
});
|
||||
|
||||
ACL.checkPermission(ACL.USER, 'u001', 'testModel', 'name', ACL.ALL, function(err, perm) {
|
||||
assert(perm.permission === ACL.DENY);
|
||||
});
|
||||
|
||||
ACL.checkPermission(ACL.USER, 'u002', 'testModel', 'name', ACL.WRITE, function(err, perm) {
|
||||
assert(perm.permission === ACL.ALLOW);
|
||||
});
|
||||
|
||||
ACL.checkPermission(ACL.USER, 'u002', 'testModel', 'name', ACL.READ, function(err, perm) {
|
||||
assert(perm.permission === ACL.ALLOW);
|
||||
});
|
||||
|
||||
ACL.create({
|
||||
principalType: ACL.USER, principalId: 'u001', model: 'User', property: ACL.ALL,
|
||||
accessType: ACL.ALL, permission: ACL.ALLOW,
|
||||
}, function(err, acl) {
|
||||
ACL.create({
|
||||
principalType: ACL.USER, principalId: 'u001', model: 'User', property: ACL.ALL,
|
||||
accessType: ACL.READ, permission: ACL.DENY,
|
||||
}, function(err, acl) {
|
||||
async.parallel([
|
||||
cb => ACL.checkPermission(ACL.USER, 'u001', 'User', 'name', ACL.READ, cb),
|
||||
cb => ACL.checkPermission(ACL.USER, 'u001', 'User', 'name', ACL.ALL, cb),
|
||||
], (err, perms) => {
|
||||
if (err) return done(err);
|
||||
assert.deepEqual(perms.map(p => p.permission), [
|
||||
ACL.DENY,
|
||||
ACL.DENY,
|
||||
]);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
it('should honor defaultPermission from the model', function() {
|
||||
var Customer = ds.createModel('Customer', {
|
||||
it('should allow access to models by exception', function(done) {
|
||||
ACL.create({
|
||||
principalType: ACL.USER, principalId: 'u001', model: 'testModel', property: ACL.ALL,
|
||||
accessType: ACL.ALL, permission: ACL.DENY,
|
||||
}, function(err, acl) {
|
||||
ACL.create({
|
||||
principalType: ACL.USER, principalId: 'u001', model: 'testModel', property: ACL.ALL,
|
||||
accessType: ACL.READ, permission: ACL.ALLOW,
|
||||
}, function(err, acl) {
|
||||
ACL.create({
|
||||
principalType: ACL.USER, principalId: 'u002', model: 'testModel', property: ACL.ALL,
|
||||
accessType: ACL.EXECUTE, permission: ACL.ALLOW,
|
||||
}, function(err, acl) {
|
||||
async.parallel([
|
||||
cb => ACL.checkPermission(ACL.USER, 'u001', 'testModel', 'name', ACL.READ, cb),
|
||||
cb => ACL.checkPermission(ACL.USER, 'u001', 'testModel', ACL.ALL, ACL.READ, cb),
|
||||
cb => ACL.checkPermission(ACL.USER, 'u001', 'testModel', 'name', ACL.WRITE, cb),
|
||||
cb => ACL.checkPermission(ACL.USER, 'u001', 'testModel', 'name', ACL.ALL, cb),
|
||||
cb => ACL.checkPermission(ACL.USER, 'u002', 'testModel', 'name', ACL.WRITE, cb),
|
||||
cb => ACL.checkPermission(ACL.USER, 'u002', 'testModel', 'name', ACL.READ, cb),
|
||||
], (err, perms) => {
|
||||
if (err) return done(err);
|
||||
assert.deepEqual(perms.map(p => p.permission), [
|
||||
ACL.ALLOW,
|
||||
ACL.ALLOW,
|
||||
ACL.DENY,
|
||||
ACL.DENY,
|
||||
ACL.ALLOW,
|
||||
ACL.ALLOW,
|
||||
]);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should honor defaultPermission from the model', function(done) {
|
||||
const Customer = ds.createModel('Customer', {
|
||||
name: {
|
||||
type: String,
|
||||
acls: [
|
||||
{principalType: ACL.USER, principalId: 'u001', accessType: ACL.WRITE, permission: ACL.DENY},
|
||||
{principalType: ACL.USER, principalId: 'u001', accessType: ACL.ALL, permission: ACL.ALLOW}
|
||||
]
|
||||
}
|
||||
{principalType: ACL.USER, principalId: 'u001',
|
||||
accessType: ACL.WRITE, permission: ACL.DENY},
|
||||
{principalType: ACL.USER, principalId: 'u001',
|
||||
accessType: ACL.ALL, permission: ACL.ALLOW},
|
||||
],
|
||||
},
|
||||
}, {
|
||||
acls: [
|
||||
{principalType: ACL.USER, principalId: 'u001', accessType: ACL.ALL, permission: ACL.ALLOW}
|
||||
]
|
||||
{principalType: ACL.USER, principalId: 'u001',
|
||||
accessType: ACL.ALL, permission: ACL.ALLOW},
|
||||
],
|
||||
});
|
||||
|
||||
// ACL default permission is to DENY for model Customer
|
||||
Customer.settings.defaultPermission = ACL.DENY;
|
||||
|
||||
ACL.checkPermission(ACL.USER, 'u001', 'Customer', 'name', ACL.WRITE, function(err, perm) {
|
||||
assert(perm.permission === ACL.DENY);
|
||||
async.parallel([
|
||||
cb => ACL.checkPermission(ACL.USER, 'u001', 'Customer', 'name', ACL.WRITE, cb),
|
||||
cb => ACL.checkPermission(ACL.USER, 'u001', 'Customer', 'name', ACL.READ, cb),
|
||||
cb => ACL.checkPermission(ACL.USER, 'u002', 'Customer', 'name', ACL.WRITE, cb),
|
||||
], (err, perms) => {
|
||||
if (err) return done(err);
|
||||
assert.deepEqual(perms.map(p => p.permission), [
|
||||
ACL.DENY,
|
||||
ACL.ALLOW,
|
||||
ACL.DENY,
|
||||
]);
|
||||
done();
|
||||
});
|
||||
|
||||
ACL.checkPermission(ACL.USER, 'u001', 'Customer', 'name', ACL.READ, function(err, perm) {
|
||||
assert(perm.permission === ACL.ALLOW);
|
||||
});
|
||||
|
||||
ACL.checkPermission(ACL.USER, 'u002', 'Customer', 'name', ACL.WRITE, function(err, perm) {
|
||||
assert(perm.permission === ACL.DENY);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
it('should honor static ACLs from the model', function() {
|
||||
var Customer = ds.createModel('Customer', {
|
||||
it('should honor static ACLs from the model', function(done) {
|
||||
const Customer = ds.createModel('Customer', {
|
||||
name: {
|
||||
type: String,
|
||||
acls: [
|
||||
{principalType: ACL.USER, principalId: 'u001', accessType: ACL.WRITE, permission: ACL.DENY},
|
||||
{principalType: ACL.USER, principalId: 'u001', accessType: ACL.ALL, permission: ACL.ALLOW}
|
||||
]
|
||||
}
|
||||
{principalType: ACL.USER, principalId: 'u001',
|
||||
accessType: ACL.WRITE, permission: ACL.DENY},
|
||||
{principalType: ACL.USER, principalId: 'u001',
|
||||
accessType: ACL.ALL, permission: ACL.ALLOW},
|
||||
],
|
||||
},
|
||||
}, {
|
||||
acls: [
|
||||
{principalType: ACL.USER, principalId: 'u001', accessType: ACL.ALL, permission: ACL.ALLOW},
|
||||
{principalType: ACL.USER, principalId: 'u002', accessType: ACL.EXECUTE, permission: ACL.ALLOW},
|
||||
{principalType: ACL.USER, principalId: 'u003', accessType: ACL.EXECUTE, permission: ACL.DENY}
|
||||
]
|
||||
{principalType: ACL.USER, principalId: 'u001',
|
||||
accessType: ACL.ALL, permission: ACL.ALLOW},
|
||||
{principalType: ACL.USER, principalId: 'u002',
|
||||
accessType: ACL.EXECUTE, permission: ACL.ALLOW},
|
||||
{principalType: ACL.USER, principalId: 'u003',
|
||||
accessType: ACL.EXECUTE, permission: ACL.DENY},
|
||||
],
|
||||
});
|
||||
|
||||
/*
|
||||
|
@ -228,39 +374,36 @@ describe('security ACLs', function() {
|
|||
];
|
||||
*/
|
||||
|
||||
ACL.checkPermission(ACL.USER, 'u001', 'Customer', 'name', ACL.WRITE, function(err, perm) {
|
||||
assert(perm.permission === ACL.DENY);
|
||||
async.parallel([
|
||||
cb => ACL.checkPermission(ACL.USER, 'u001', 'Customer', 'name', ACL.WRITE, cb),
|
||||
cb => ACL.checkPermission(ACL.USER, 'u001', 'Customer', 'name', ACL.READ, cb),
|
||||
cb => ACL.checkPermission(ACL.USER, 'u001', 'Customer', 'name', ACL.ALL, cb),
|
||||
cb => ACL.checkPermission(ACL.USER, 'u002', 'Customer', 'name', ACL.READ, cb),
|
||||
cb => ACL.checkPermission(ACL.USER, 'u003', 'Customer', 'name', ACL.WRITE, cb),
|
||||
], (err, perms) => {
|
||||
if (err) return done(err);
|
||||
assert.deepEqual(perms.map(p => p.permission), [
|
||||
ACL.DENY,
|
||||
ACL.ALLOW,
|
||||
ACL.ALLOW,
|
||||
ACL.ALLOW,
|
||||
ACL.DENY,
|
||||
]);
|
||||
done();
|
||||
});
|
||||
|
||||
ACL.checkPermission(ACL.USER, 'u001', 'Customer', 'name', ACL.READ, function(err, perm) {
|
||||
assert(perm.permission === ACL.ALLOW);
|
||||
});
|
||||
|
||||
ACL.checkPermission(ACL.USER, 'u001', 'Customer', 'name', ACL.ALL, function(err, perm) {
|
||||
assert(perm.permission === ACL.ALLOW);
|
||||
});
|
||||
|
||||
ACL.checkPermission(ACL.USER, 'u002', 'Customer', 'name', ACL.READ, function(err, perm) {
|
||||
assert(perm.permission === ACL.ALLOW);
|
||||
});
|
||||
|
||||
ACL.checkPermission(ACL.USER, 'u003', 'Customer', 'name', ACL.WRITE, function(err, perm) {
|
||||
assert(perm.permission === ACL.DENY);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
it('should filter static ACLs by model/property', function() {
|
||||
var Model1 = ds.createModel('Model1', {
|
||||
const Model1 = ds.createModel('Model1', {
|
||||
name: {
|
||||
type: String,
|
||||
acls: [
|
||||
{principalType: ACL.USER, principalId: 'u001',
|
||||
accessType: ACL.WRITE, permission: ACL.DENY},
|
||||
{principalType: ACL.USER, principalId: 'u001',
|
||||
accessType: ACL.ALL, permission: ACL.ALLOW}
|
||||
]
|
||||
}
|
||||
accessType: ACL.ALL, permission: ACL.ALLOW},
|
||||
],
|
||||
},
|
||||
}, {
|
||||
acls: [
|
||||
{principalType: ACL.USER, principalId: 'u001', property: 'name',
|
||||
|
@ -268,11 +411,11 @@ describe('security ACLs', function() {
|
|||
{principalType: ACL.USER, principalId: 'u002', property: 'findOne',
|
||||
accessType: ACL.ALL, permission: ACL.ALLOW},
|
||||
{principalType: ACL.USER, principalId: 'u003', property: ['findOne', 'findById'],
|
||||
accessType: ACL.ALL, permission: ACL.ALLOW}
|
||||
]
|
||||
accessType: ACL.ALL, permission: ACL.ALLOW},
|
||||
],
|
||||
});
|
||||
|
||||
var staticACLs = ACL.getStaticACLs('Model1', 'name');
|
||||
let staticACLs = ACL.getStaticACLs('Model1', 'name');
|
||||
assert(staticACLs.length === 3);
|
||||
|
||||
staticACLs = ACL.getStaticACLs('Model1', 'findOne');
|
||||
|
@ -283,74 +426,86 @@ describe('security ACLs', function() {
|
|||
assert(staticACLs[0].property === 'findById');
|
||||
});
|
||||
|
||||
it('should check access against LDL, ACL, and Role', function() {
|
||||
// var log = console.log;
|
||||
var log = function() {};
|
||||
it('should check access against LDL, ACL, and Role', function(done) {
|
||||
const log = function() {};
|
||||
|
||||
// Create
|
||||
User.create({name: 'Raymond', email: 'x@y.com', password: 'foobar'}, function(err, user) {
|
||||
|
||||
log('User: ', user.toObject());
|
||||
|
||||
var userId = user.id;
|
||||
const userId = user.id;
|
||||
|
||||
// Define a model with static ACLs
|
||||
var Customer = ds.createModel('Customer', {
|
||||
const Customer = ds.createModel('Customer', {
|
||||
name: {
|
||||
type: String,
|
||||
acls: [
|
||||
{principalType: ACL.USER, principalId: userId, accessType: ACL.WRITE, permission: ACL.DENY},
|
||||
{principalType: ACL.USER, principalId: userId, accessType: ACL.ALL, permission: ACL.ALLOW}
|
||||
]
|
||||
}
|
||||
{principalType: ACL.USER, principalId: userId,
|
||||
accessType: ACL.WRITE, permission: ACL.DENY},
|
||||
{principalType: ACL.USER, principalId: userId,
|
||||
accessType: ACL.ALL, permission: ACL.ALLOW},
|
||||
],
|
||||
},
|
||||
}, {
|
||||
acls: [
|
||||
{principalType: ACL.USER, principalId: userId, accessType: ACL.ALL, permission: ACL.ALLOW}
|
||||
{principalType: ACL.USER, principalId: userId,
|
||||
accessType: ACL.ALL, permission: ACL.ALLOW},
|
||||
],
|
||||
defaultPermission: 'DENY'
|
||||
defaultPermission: 'DENY',
|
||||
});
|
||||
|
||||
ACL.create({principalType: ACL.USER, principalId: userId, model: 'Customer', property: ACL.ALL,
|
||||
accessType: ACL.ALL, permission: ACL.ALLOW}, function(err, acl) {
|
||||
|
||||
ACL.create({
|
||||
principalType: ACL.USER, principalId: userId,
|
||||
model: 'Customer', property: ACL.ALL,
|
||||
accessType: ACL.ALL, permission: ACL.ALLOW,
|
||||
}, function(err, acl) {
|
||||
log('ACL 1: ', acl.toObject());
|
||||
|
||||
Role.create({name: 'MyRole'}, function(err, myRole) {
|
||||
log('Role: ', myRole.toObject());
|
||||
|
||||
myRole.principals.create({principalType: RoleMapping.USER, principalId: userId}, function(err, p) {
|
||||
myRole.principals.create({principalType: RoleMapping.USER, principalId: userId},
|
||||
function(err, p) {
|
||||
log('Principal added to role: ', p.toObject());
|
||||
|
||||
log('Principal added to role: ', p.toObject());
|
||||
ACL.create({
|
||||
principalType: ACL.ROLE, principalId: 'MyRole',
|
||||
model: 'Customer', property: ACL.ALL,
|
||||
accessType: ACL.READ, permission: ACL.DENY,
|
||||
}, function(err, acl) {
|
||||
log('ACL 2: ', acl.toObject());
|
||||
|
||||
ACL.create({principalType: ACL.ROLE, principalId: 'MyRole', model: 'Customer', property: ACL.ALL,
|
||||
accessType: ACL.READ, permission: ACL.DENY}, function(err, acl) {
|
||||
|
||||
log('ACL 2: ', acl.toObject());
|
||||
|
||||
ACL.checkAccessForContext({
|
||||
principals: [
|
||||
{type: ACL.USER, id: userId}
|
||||
],
|
||||
model: 'Customer',
|
||||
property: 'name',
|
||||
accessType: ACL.READ
|
||||
}, function(err, access) {
|
||||
assert(!err && access.permission === ACL.ALLOW);
|
||||
async.parallel([
|
||||
cb => {
|
||||
ACL.checkAccessForContext({
|
||||
principals: [
|
||||
{type: ACL.USER, id: userId},
|
||||
],
|
||||
model: 'Customer',
|
||||
property: 'name',
|
||||
accessType: ACL.READ,
|
||||
}, function(err, access) {
|
||||
assert.ifError(err);
|
||||
assert.equal(access.permission, ACL.ALLOW);
|
||||
cb();
|
||||
});
|
||||
},
|
||||
cb => {
|
||||
ACL.checkAccessForContext({
|
||||
principals: [
|
||||
{type: ACL.ROLE, id: Role.EVERYONE},
|
||||
],
|
||||
model: 'Customer',
|
||||
property: 'name',
|
||||
accessType: ACL.READ,
|
||||
}, function(err, access) {
|
||||
assert.ifError(err);
|
||||
assert.equal(access.permission, ACL.DENY);
|
||||
cb();
|
||||
});
|
||||
}], done);
|
||||
});
|
||||
|
||||
ACL.checkAccessForContext({
|
||||
principals: [
|
||||
{type: ACL.ROLE, id: Role.EVERYONE}
|
||||
],
|
||||
model: 'Customer',
|
||||
property: 'name',
|
||||
accessType: ACL.READ
|
||||
}, function(err, access) {
|
||||
assert(!err && access.permission === ACL.DENY);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -358,31 +513,32 @@ describe('security ACLs', function() {
|
|||
});
|
||||
|
||||
describe('access check', function() {
|
||||
var app;
|
||||
before(function() {
|
||||
app = loopback();
|
||||
it('should occur before other remote hooks', function(done) {
|
||||
const app = loopback();
|
||||
const MyTestModel = app.registry.createModel('MyTestModel');
|
||||
let checkAccessCalled = false;
|
||||
let beforeHookCalled = false;
|
||||
|
||||
app.use(loopback.rest());
|
||||
app.set('remoting', {errorHandler: {debug: true, log: false}});
|
||||
app.enableAuth();
|
||||
app.dataSource('test', {connector: 'memory'});
|
||||
});
|
||||
|
||||
it('should occur before other remote hooks', function(done) {
|
||||
var MyTestModel = app.model('MyTestModel', {base: 'PersistedModel', dataSource: 'test'});
|
||||
var checkAccessCalled = false;
|
||||
var beforeHookCalled = false;
|
||||
app.model(MyTestModel, {dataSource: 'test'});
|
||||
|
||||
// fake / spy on the checkAccess method
|
||||
MyTestModel.checkAccess = function() {
|
||||
var cb = arguments[arguments.length - 1];
|
||||
const cb = arguments[arguments.length - 1];
|
||||
checkAccessCalled = true;
|
||||
var allowed = true;
|
||||
const allowed = true;
|
||||
cb(null, allowed);
|
||||
};
|
||||
|
||||
MyTestModel.beforeRemote('find', function(ctx, next) {
|
||||
// ensure this is called after checkAccess
|
||||
if (!checkAccessCalled) return done(new Error('incorrect order'));
|
||||
|
||||
beforeHookCalled = true;
|
||||
|
||||
next();
|
||||
});
|
||||
|
||||
|
@ -391,7 +547,142 @@ describe('access check', function() {
|
|||
.end(function(err, result) {
|
||||
assert(beforeHookCalled, 'the before hook should be called');
|
||||
assert(checkAccessCalled, 'checkAccess should have been called');
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('authorized roles propagation in RemotingContext', function() {
|
||||
let app, request, accessToken;
|
||||
let models = {};
|
||||
|
||||
beforeEach(setupAppAndRequest);
|
||||
|
||||
it('contains all authorized roles for a principal if query is allowed', function() {
|
||||
return createACLs('MyTestModel', [
|
||||
{permission: ACL.ALLOW, principalId: '$everyone'},
|
||||
{permission: ACL.ALLOW, principalId: '$authenticated'},
|
||||
{permission: ACL.ALLOW, principalId: 'myRole'},
|
||||
])
|
||||
.then(makeAuthorizedHttpRequestOnMyTestModel)
|
||||
.then(function() {
|
||||
const ctx = models.MyTestModel.lastRemotingContext;
|
||||
expect(ctx.args.options.authorizedRoles).to.eql(
|
||||
{
|
||||
$everyone: true,
|
||||
$authenticated: true,
|
||||
myRole: true,
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('does not contain any denied role even if query is allowed', function() {
|
||||
return createACLs('MyTestModel', [
|
||||
{permission: ACL.ALLOW, principalId: '$everyone'},
|
||||
{permission: ACL.DENY, principalId: '$authenticated'},
|
||||
{permission: ACL.ALLOW, principalId: 'myRole'},
|
||||
])
|
||||
.then(makeAuthorizedHttpRequestOnMyTestModel)
|
||||
.then(function() {
|
||||
const ctx = models.MyTestModel.lastRemotingContext;
|
||||
expect(ctx.args.options.authorizedRoles).to.eql(
|
||||
{
|
||||
$everyone: true,
|
||||
myRole: true,
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('honors default permission setting', function() {
|
||||
// default permission is set to DENY for MyTestModel
|
||||
models.MyTestModel.settings.defaultPermission = ACL.DENY;
|
||||
|
||||
return createACLs('MyTestModel', [
|
||||
{permission: ACL.DEFAULT, principalId: '$everyone'},
|
||||
{permission: ACL.DENY, principalId: '$authenticated'},
|
||||
{permission: ACL.ALLOW, principalId: 'myRole'},
|
||||
])
|
||||
.then(makeAuthorizedHttpRequestOnMyTestModel)
|
||||
.then(function() {
|
||||
const ctx = models.MyTestModel.lastRemotingContext;
|
||||
expect(ctx.args.options.authorizedRoles).to.eql(
|
||||
// '$everyone' is not expected as default permission is DENY
|
||||
{myRole: true},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// helpers
|
||||
function setupAppAndRequest() {
|
||||
app = loopback({localRegistry: true, loadBuiltinModels: true});
|
||||
app.use(loopback.rest());
|
||||
app.set('remoting', {errorHandler: {debug: true, log: true}});
|
||||
app.dataSource('db', {connector: 'memory'});
|
||||
request = supertest(app);
|
||||
|
||||
app.enableAuth({dataSource: 'db'});
|
||||
models = app.models;
|
||||
|
||||
// Speed up the password hashing algorithm for tests
|
||||
models.User.settings.saltWorkFactor = 4;
|
||||
|
||||
// creating a custom model
|
||||
const MyTestModel = app.registry.createModel('MyTestModel');
|
||||
app.model(MyTestModel, {dataSource: 'db'});
|
||||
|
||||
// capturing the value of the last remoting context
|
||||
models.MyTestModel.beforeRemote('find', function(ctx, unused, next) {
|
||||
models.MyTestModel.lastRemotingContext = ctx;
|
||||
next();
|
||||
});
|
||||
|
||||
// creating a user, a role and a rolemapping binding that user with that role
|
||||
return Promise.all([
|
||||
models.User.create({username: 'myUser', email: 'myuser@example.com', password: 'pass'}),
|
||||
models.Role.create({name: 'myRole'}),
|
||||
])
|
||||
.spread(function(myUser, myRole) {
|
||||
return Promise.all([
|
||||
myRole.principals.create({principalType: 'USER', principalId: myUser.id}),
|
||||
models.User.login({username: 'myUser', password: 'pass'}),
|
||||
]);
|
||||
})
|
||||
.spread(function(role, token) {
|
||||
accessToken = token;
|
||||
});
|
||||
}
|
||||
|
||||
function createACLs(model, acls) {
|
||||
acls = acls.map(function(acl) {
|
||||
return models.ACL.create({
|
||||
principalType: acl.principalType || ACL.ROLE,
|
||||
principalId: acl.principalId,
|
||||
model: acl.model || model,
|
||||
property: acl.property || ACL.ALL,
|
||||
accessType: acl.accessType || ACL.ALL,
|
||||
permission: acl.permission,
|
||||
});
|
||||
});
|
||||
return Promise.all(acls);
|
||||
}
|
||||
|
||||
function makeAuthorizedHttpRequestOnMyTestModel() {
|
||||
return request.get('/MyTestModels')
|
||||
.set('X-Access-Token', accessToken.id)
|
||||
.expect(200);
|
||||
}
|
||||
});
|
||||
|
||||
function setupTestModels() {
|
||||
ds = this.ds = loopback.createDataSource({connector: loopback.Memory});
|
||||
testModel = loopback.PersistedModel.extend('testModel');
|
||||
ACL.attachTo(ds);
|
||||
Role.attachTo(ds);
|
||||
RoleMapping.attachTo(ds);
|
||||
User.attachTo(ds);
|
||||
Scope.attachTo(ds);
|
||||
testModel.attachTo(ds);
|
||||
}
|
||||
|
|
704
test/app.test.js
704
test/app.test.js
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,128 @@
|
|||
// Copyright IBM Corp. 2017,2018. All Rights Reserved.
|
||||
// Node module: loopback
|
||||
// This file is licensed under the MIT License.
|
||||
// License text available at https://opensource.org/licenses/MIT
|
||||
|
||||
'use strict';
|
||||
|
||||
const loopback = require('../');
|
||||
const supertest = require('supertest');
|
||||
const strongErrorHandler = require('strong-error-handler');
|
||||
const loggers = require('./helpers/error-loggers');
|
||||
|
||||
const logAllServerErrors = loggers.logAllServerErrors;
|
||||
const logServerErrorsOtherThan = loggers.logServerErrorsOtherThan;
|
||||
|
||||
describe('Authorization scopes', () => {
|
||||
const CUSTOM_SCOPE = 'read:custom';
|
||||
|
||||
let app, request, User, testUser, regularToken, scopedToken;
|
||||
beforeEach(givenAppAndRequest);
|
||||
beforeEach(givenRemoteMethodWithCustomScope);
|
||||
beforeEach(givenUser);
|
||||
beforeEach(givenDefaultToken);
|
||||
beforeEach(givenScopedToken);
|
||||
|
||||
it('denies regular token to invoke custom-scoped method', () => {
|
||||
logServerErrorsOtherThan(401, app);
|
||||
return request.get('/users/scoped')
|
||||
.set('Authorization', regularToken.id)
|
||||
.expect(401);
|
||||
});
|
||||
|
||||
it('allows regular tokens to invoke default-scoped method', () => {
|
||||
logAllServerErrors(app);
|
||||
return request.get('/users/' + testUser.id)
|
||||
.set('Authorization', regularToken.id)
|
||||
.expect(200);
|
||||
});
|
||||
|
||||
it('allows scoped token to invoke custom-scoped method', () => {
|
||||
logAllServerErrors(app);
|
||||
return request.get('/users/scoped')
|
||||
.set('Authorization', scopedToken.id)
|
||||
.expect(204);
|
||||
});
|
||||
|
||||
it('denies scoped token to invoke default-scoped method', () => {
|
||||
logServerErrorsOtherThan(401, app);
|
||||
return request.get('/users/' + testUser.id)
|
||||
.set('Authorization', scopedToken.id)
|
||||
.expect(401);
|
||||
});
|
||||
|
||||
describe('token granted both default and custom scope', () => {
|
||||
beforeEach('given token with default and custom scope',
|
||||
() => givenScopedToken(['DEFAULT', CUSTOM_SCOPE]));
|
||||
beforeEach(() => logAllServerErrors(app));
|
||||
|
||||
it('allows invocation of default-scoped method', () => {
|
||||
return request.get('/users/' + testUser.id)
|
||||
.set('Authorization', scopedToken.id)
|
||||
.expect(200);
|
||||
});
|
||||
|
||||
it('allows invocation of custom-scoped method', () => {
|
||||
return request.get('/users/scoped')
|
||||
.set('Authorization', scopedToken.id)
|
||||
.expect(204);
|
||||
});
|
||||
});
|
||||
|
||||
it('allows invocation when at least one method scope is matched', () => {
|
||||
givenRemoteMethodWithCustomScope(['read', 'write']);
|
||||
return givenScopedToken(['read', 'execute']).then(() => {
|
||||
return request.get('/users/scoped')
|
||||
.set('Authorization', scopedToken.id)
|
||||
.expect(204);
|
||||
});
|
||||
});
|
||||
|
||||
function givenAppAndRequest() {
|
||||
app = loopback({localRegistry: true, loadBuiltinModels: true});
|
||||
app.set('remoting', {rest: {handleErrors: false}});
|
||||
app.dataSource('db', {connector: 'memory'});
|
||||
app.enableAuth({dataSource: 'db'});
|
||||
request = supertest(app);
|
||||
|
||||
app.use(loopback.rest());
|
||||
|
||||
User = app.models.User;
|
||||
}
|
||||
|
||||
function givenRemoteMethodWithCustomScope() {
|
||||
// Delete any previously registered instance of the method "scoped"
|
||||
User.sharedClass._methods = User.sharedClass._methods
|
||||
.filter(m => m.name !== 'scoped');
|
||||
|
||||
const accessScopes = arguments[0] || [CUSTOM_SCOPE];
|
||||
User.scoped = function(cb) { cb(); };
|
||||
User.remoteMethod('scoped', {
|
||||
accessScopes,
|
||||
http: {verb: 'GET', path: '/scoped'},
|
||||
});
|
||||
User.settings.acls.push({
|
||||
principalType: 'ROLE',
|
||||
principalId: '$authenticated',
|
||||
permission: 'ALLOW',
|
||||
property: 'scoped',
|
||||
accessType: 'EXECUTE',
|
||||
});
|
||||
}
|
||||
|
||||
function givenUser() {
|
||||
return User.create({email: 'test@example.com', password: 'pass'})
|
||||
.then(u => testUser = u);
|
||||
}
|
||||
|
||||
function givenDefaultToken() {
|
||||
return testUser.createAccessToken(60)
|
||||
.then(t => regularToken = t);
|
||||
}
|
||||
|
||||
function givenScopedToken() {
|
||||
const scopes = arguments[0] || [CUSTOM_SCOPE];
|
||||
return testUser.accessTokens.create({ttl: 60, scopes})
|
||||
.then(t => scopedToken = t);
|
||||
}
|
||||
});
|
|
@ -1,17 +1,30 @@
|
|||
// Copyright IBM Corp. 2015,2019. All Rights Reserved.
|
||||
// Node module: loopback
|
||||
// This file is licensed under the MIT License.
|
||||
// License text available at https://opensource.org/licenses/MIT
|
||||
|
||||
'use strict';
|
||||
const expect = require('./helpers/expect');
|
||||
const sinon = require('sinon');
|
||||
const loopback = require('../');
|
||||
|
||||
describe('PersistedModel.createChangeStream()', function() {
|
||||
describe('configured to source changes locally', function() {
|
||||
before(function() {
|
||||
var test = this;
|
||||
var app = loopback({localRegistry: true});
|
||||
var ds = app.dataSource('ds', {connector: 'memory'});
|
||||
this.Score = app.model('Score', {
|
||||
const test = this;
|
||||
const app = loopback({localRegistry: true});
|
||||
const ds = app.dataSource('ds', {connector: 'memory'});
|
||||
const Score = app.registry.createModel('Score');
|
||||
this.Score = app.model(Score, {
|
||||
dataSource: 'ds',
|
||||
changeDataSource: false // use only local observers
|
||||
changeDataSource: false, // use only local observers
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(verifyObserversRemoval);
|
||||
|
||||
it('should detect create', function(done) {
|
||||
var Score = this.Score;
|
||||
const Score = this.Score;
|
||||
|
||||
Score.createChangeStream(function(err, changes) {
|
||||
changes.on('data', function(change) {
|
||||
|
@ -25,28 +38,30 @@ describe('PersistedModel.createChangeStream()', function() {
|
|||
});
|
||||
|
||||
it('should detect update', function(done) {
|
||||
var Score = this.Score;
|
||||
const Score = this.Score;
|
||||
Score.create({team: 'foo'}, function(err, newScore) {
|
||||
Score.createChangeStream(function(err, changes) {
|
||||
changes.on('data', function(change) {
|
||||
expect(change.type).to.equal('update');
|
||||
changes.destroy();
|
||||
|
||||
done();
|
||||
});
|
||||
newScore.updateAttributes({
|
||||
bat: 'baz'
|
||||
bat: 'baz',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should detect delete', function(done) {
|
||||
var Score = this.Score;
|
||||
const Score = this.Score;
|
||||
Score.create({team: 'foo'}, function(err, newScore) {
|
||||
Score.createChangeStream(function(err, changes) {
|
||||
changes.on('data', function(change) {
|
||||
expect(change.type).to.equal('remove');
|
||||
changes.destroy();
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
|
@ -54,28 +69,86 @@ describe('PersistedModel.createChangeStream()', function() {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should apply "where" and "fields" to create events', function() {
|
||||
const Score = this.Score;
|
||||
const data = [
|
||||
{team: 'baz', player: 'baz', value: 1},
|
||||
{team: 'bar', player: 'baz', value: 2},
|
||||
{team: 'foo', player: 'bar', value: 3},
|
||||
];
|
||||
const options = {where: {player: 'bar'}, fields: ['team', 'value']};
|
||||
const changes = [];
|
||||
let changeStream;
|
||||
|
||||
return Score.createChangeStream(options)
|
||||
.then(stream => {
|
||||
changeStream = stream;
|
||||
changeStream.on('data', function(change) {
|
||||
changes.push(change);
|
||||
});
|
||||
|
||||
return Score.create(data);
|
||||
})
|
||||
.then(scores => {
|
||||
changeStream.destroy();
|
||||
|
||||
expect(changes).to.have.length(1);
|
||||
expect(changes[0]).to.have.property('type', 'create');
|
||||
expect(changes[0].data).to.eql({
|
||||
'team': 'foo',
|
||||
value: 3,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should not emit changes after destroy', function(done) {
|
||||
const Score = this.Score;
|
||||
|
||||
const spy = sinon.spy();
|
||||
|
||||
Score.createChangeStream(function(err, changes) {
|
||||
changes.on('data', function() {
|
||||
spy();
|
||||
changes.destroy();
|
||||
});
|
||||
|
||||
Score.create({team: 'foo'})
|
||||
.then(() => Score.deleteAll())
|
||||
.then(() => {
|
||||
expect(spy.calledOnce);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function verifyObserversRemoval() {
|
||||
const Score = this.Score;
|
||||
expect(Score._observers['after save']).to.be.empty();
|
||||
expect(Score._observers['after delete']).to.be.empty();
|
||||
}
|
||||
});
|
||||
|
||||
// TODO(ritch) implement multi-server support
|
||||
describe.skip('configured to source changes using pubsub', function() {
|
||||
before(function() {
|
||||
var test = this;
|
||||
var app = loopback({localRegistry: true});
|
||||
var db = app.dataSource('ds', {connector: 'memory'});
|
||||
var ps = app.dataSource('ps', {
|
||||
const test = this;
|
||||
const app = loopback({localRegistry: true});
|
||||
const db = app.dataSource('ds', {connector: 'memory'});
|
||||
const ps = app.dataSource('ps', {
|
||||
host: 'localhost',
|
||||
port: '12345',
|
||||
connector: 'pubsub',
|
||||
pubsubAdapter: 'mqtt'
|
||||
pubsubAdapter: 'mqtt',
|
||||
});
|
||||
this.Score = app.model('Score', {
|
||||
dataSource: 'db',
|
||||
changeDataSource: 'ps'
|
||||
changeDataSource: 'ps',
|
||||
});
|
||||
});
|
||||
|
||||
it('should detect a change', function(done) {
|
||||
var Score = this.Score;
|
||||
const Score = this.Score;
|
||||
|
||||
Score.createChangeStream(function(err, changes) {
|
||||
changes.on('data', function(change) {
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue