Page Menu
Home
Sealhub
Search
Configure Global Search
Log In
Files
F969841
D200.id692.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
33 KB
Referenced Files
None
Subscribers
None
D200.id692.diff
View Options
diff --git a/lib/app/app.js b/lib/app/app.js
--- a/lib/app/app.js
+++ b/lib/app/app.js
@@ -17,6 +17,7 @@
const WwwServerFactory = locreq("lib/http/http.js");
const DatastoreMongoFactory = locreq("lib/datastore/db.js");
+const MetadataFactory = locreq("lib/app/metadata.js");
const load_base_chips = locreq("lib/app/load-base-chips");
const default_config = locreq("default_config.json");
@@ -26,6 +27,7 @@
class App {
constructor(custom_config, manifest) {
+ this.status = "stopped";
this.Sealious = Sealious;
this.setupEventEmitter();
@@ -55,6 +57,7 @@
load_base_chips(this);
this.Datastore = new DatastoreMongoFactory(this);
+ this.Metadata = MetadataFactory(this);
this.FieldType = Sealious.FieldType.bind(Sealious.FieldType, this);
this.Collection = Sealious.Collection.bind(Sealious.Collection, this);
@@ -107,6 +110,7 @@
}
async start() {
+ this.status = "starting";
assert(
["dev", "production"].includes(
this.ConfigManager.get("core.environment")
@@ -118,13 +122,16 @@
await this.Mail.init();
await this.ChipManager.start_chips();
await this.emit("start");
+ this.status = "running";
return this;
}
async stop() {
+ this.status = "stopping";
await this.emit("stop");
await this.WwwServer.stop();
await this.Datastore.stop();
+ this.status = "stopped";
}
}
diff --git a/lib/app/base-chips/collections/password-reset-intents.subtest.js b/lib/app/base-chips/collections/password-reset-intents.subtest.js
--- a/lib/app/base-chips/collections/password-reset-intents.subtest.js
+++ b/lib/app/base-chips/collections/password-reset-intents.subtest.js
@@ -2,7 +2,7 @@
const axios = require("axios");
const assert = require("assert");
const { promise_timeout } = locreq("test_utils");
-const with_test_app = locreq("test_utils/with-test-app.js");
+const { with_running_app } = locreq("test_utils/with-test-app.js");
describe("password-reset-intents", () => {
async function create_a_user(app) {
@@ -19,7 +19,7 @@
}
it("tells you if the email address doesn't exist", async () =>
- with_test_app(async ({ app, base_url }) => {
+ with_running_app(async ({ app, base_url }) => {
try {
await axios.post(
`${base_url}/api/v1/collections/password-reset-intents`,
@@ -38,7 +38,7 @@
}));
it("allows anyone to create an intent, if the email exists", async () =>
- with_test_app(async ({ app, base_url }) => {
+ with_running_app(async ({ app, base_url }) => {
await create_a_user(app);
const data = (await axios.post(
`${base_url}/api/v1/collections/password-reset-intents`,
@@ -53,7 +53,7 @@
}));
it("tells you if the email address is malformed", async () =>
- with_test_app(async ({ base_url }) => {
+ with_running_app(async ({ base_url }) => {
try {
await axios.post(
`${base_url}/api/v1/collections/password-reset-intents`,
@@ -72,7 +72,7 @@
}));
it("sends an email with the reset password link", async () =>
- with_test_app(async ({ app, base_url, mail_api }) => {
+ with_running_app(async ({ app, base_url, mail_api }) => {
await create_a_user(app);
const data = (await axios.post(
`${base_url}/api/v1/collections/password-reset-intents`,
diff --git a/lib/app/base-chips/collections/users.subtest.js b/lib/app/base-chips/collections/users.subtest.js
--- a/lib/app/base-chips/collections/users.subtest.js
+++ b/lib/app/base-chips/collections/users.subtest.js
@@ -1,11 +1,11 @@
const locreq = require("locreq")(__dirname);
const assert = require("assert");
-const with_test_app = locreq("test_utils/with-test-app.js");
+const { with_running_app } = locreq("test_utils/with-test-app.js");
describe("users", () => {
describe("auto create admin", () => {
it("should automatically create a registration intent for the admin user", async () =>
- with_test_app(async ({ app, mail_api }) => {
+ with_running_app(async ({ app, mail_api }) => {
const registration_intents = await app.run_action(
new app.Sealious.SuperContext(),
["collections", "registration-intents"],
diff --git a/lib/app/base-chips/field-types/field-types.test.js b/lib/app/base-chips/field-types/field-types.test.js
--- a/lib/app/base-chips/field-types/field-types.test.js
+++ b/lib/app/base-chips/field-types/field-types.test.js
@@ -1,3 +1,4 @@
describe("field types", () => {
require("./single_reference.subtest.js");
+ require("./reverse-single-reference.subtest.js");
});
diff --git a/lib/app/base-chips/field-types/reverse-single-reference.js b/lib/app/base-chips/field-types/reverse-single-reference.js
new file mode 100644
--- /dev/null
+++ b/lib/app/base-chips/field-types/reverse-single-reference.js
@@ -0,0 +1,211 @@
+"use strict";
+const locreq = require("locreq")(__dirname);
+const Promise = require("bluebird");
+const Errors = locreq("lib/response/error.js");
+const assert = require("assert");
+
+function params_to_cache_key(collection, field_name, params) {
+ return `${collection.name}___${field_name}-reverse-single-reference(${
+ params.collection.name
+ },${params.collection.field_name}).last_update`;
+}
+
+async function update_cache(
+ app,
+ collection,
+ field_name,
+ params,
+ resource_ids = undefined
+) {
+ let pipeline;
+ const referencing_field_name = params.field_name;
+ const referencing_collection = params.collection;
+ if (resource_ids) {
+ assert(Array.isArray(resource_ids));
+ pipeline = [
+ { $match: { [`body.${referencing_field_name}`]: { $in: resource_ids } } },
+ ];
+ } else {
+ pipeline = [];
+ }
+ pipeline.push({
+ $group: {
+ _id: `$body.${referencing_field_name}`,
+ referenced_by: { $push: `$sealious_id` },
+ },
+ });
+ const to_update = await app.Datastore.aggregate(
+ referencing_collection.name,
+ pipeline
+ );
+ if (resource_ids) {
+ for (let resource_id of resource_ids) {
+ if (to_update.filter(e => e._id === resource_id).length === 0) {
+ to_update.push({ _id: resource_id, referenced_by: [] });
+ }
+ }
+ }
+ for (let entry of to_update) {
+ await app.Datastore.update(
+ collection.name,
+ { sealious_id: entry._id },
+ { $set: { [`body.${field_name}`]: entry.referenced_by } }
+ );
+ }
+}
+
+const reverse_single_reference_factory = app => {
+ return {
+ name: "reverse-single-reference",
+
+ get_description: function() {
+ return "Shows which resources from given collection point to this resource in a given field.";
+ },
+
+ get_default_value: async () => [],
+
+ is_proper_value: function(context, params, new_value) {
+ return context.is_super
+ ? Promise.resolve()
+ : Promise.reject("This is a read-only field");
+ },
+
+ filter_to_query: async function(context, params, field_filter) {
+ if (typeof field_filter !== "object") {
+ return {
+ $eq: field_filter,
+ };
+ }
+ const matches = await app.run_action(
+ context,
+ ["collections", params.collection.name],
+ "show",
+ {
+ filter: field_filter,
+ }
+ );
+ const ids = matches.map(resource => resource.id);
+ return {
+ $in: ids,
+ };
+ },
+
+ format: function(context, params, decoded_value, format) {
+ // format can be "expand" or "deep-expand:<depth>", like "deep-expand:3"
+ if (!format) {
+ return decoded_value; // just the IDs
+ }
+
+ const format_params = format.split(":");
+
+ if (!["expand", "deep-expand"].includes(format_params[0])) {
+ return decoded_value;
+ }
+
+ const query_format = {};
+ if (format_params[0] === "deep-expand" && format_params[1] > 1) {
+ for (const field_name in params.collection.fields) {
+ const field = params.collection.fields[field_name];
+ if (field.type.name === "single_reference") {
+ query_format[field_name] = `deep-expand:${parseInt(
+ format_params[1],
+ 10
+ ) - 1}`;
+ }
+ }
+ }
+ const resource_ids = decoded_value;
+ return Promise.map(resource_ids, async resource_id =>
+ app.run_action(
+ context,
+ ["collections", params.collection.name, resource_id],
+ "show",
+ { format: query_format }
+ )
+ );
+ },
+
+ init: async (collection, field_name, params) => {
+ assert(
+ params.collection instanceof app.Sealious.Collection,
+ "'params.collection' should be an instance of Collection"
+ );
+ assert(
+ params.collection.fields[params.field_name],
+ `Collection '${
+ params.collection.name
+ }' does not contain a field named ${params.field_name}.`
+ );
+ app.on("start", async () => {
+ const last_modified_resource_in_reference_collection = (await app.run_action(
+ new app.Sealious.SuperContext(),
+ ["collections", params.collection.name],
+ "show",
+ {
+ sort: { "last_modified_context.timestamp": "desc" },
+ pagination: { items: 1 },
+ }
+ ))[0];
+
+ if (last_modified_resource_in_reference_collection) {
+ const last_modified_resource_timestamp =
+ last_modified_resource_in_reference_collection.last_modified_context
+ .timestamp;
+ const last_field_cache_update =
+ (await app.Metadata.get(
+ params_to_cache_key(collection, field_name, params)
+ )) || 0;
+ if (last_modified_resource_timestamp > last_field_cache_update) {
+ await update_cache(app, collection, field_name, params);
+ await app.Metadata.set(
+ params_to_cache_key(collection, field_name, params),
+ Date.now()
+ );
+ }
+ }
+ });
+ app.on(
+ new RegExp(`post:collections\.${params.collection.name}:create`),
+ async (path, event_params, resource) => {
+ const referenced_id = resource.body[params.field_name];
+ await update_cache(app, collection, field_name, params, [
+ referenced_id,
+ ]);
+ }
+ );
+ app.on(
+ new RegExp(`post:collections\.${params.collection.name}\..*:delete`),
+ async (path, event_params, resource) => {
+ const deleted_id = path[2];
+ const affected = await app.Datastore.find(collection.name, {
+ [`body.${field_name}`]: deleted_id,
+ });
+ const affected_ids = affected.map(document => document.sealious_id);
+ await update_cache(app, collection, field_name, params, affected_ids);
+ }
+ );
+ app.on(
+ new RegExp(`post:collections\.${params.collection.name}\..*:edit`),
+ async (path, event_params, resource) => {
+ if (!event_params.hasOwnProperty(params.field_name)) return;
+ const edited_id = path[2];
+ const no_longer_referenced = await app.Datastore.find(
+ collection.name,
+ {
+ [`body.${field_name}`]: edited_id,
+ }
+ );
+ const affected_ids = no_longer_referenced.map(
+ document => document.sealious_id
+ );
+ if (event_params[params.field_name]) {
+ affected_ids.push(event_params[params.field_name]);
+ }
+ await update_cache(app, collection, field_name, params, affected_ids);
+ }
+ );
+ },
+ };
+};
+
+module.exports = reverse_single_reference_factory;
diff --git a/lib/app/base-chips/field-types/reverse-single-reference.subtest.js b/lib/app/base-chips/field-types/reverse-single-reference.subtest.js
new file mode 100644
--- /dev/null
+++ b/lib/app/base-chips/field-types/reverse-single-reference.subtest.js
@@ -0,0 +1,185 @@
+const assert = require("assert");
+const Promise = require("bluebird");
+const locreq = require("locreq")(__dirname);
+const axios = require("axios");
+const { create_resource_as } = locreq("test_utils");
+const { with_stopped_app, with_running_app } = locreq(
+ "test_utils/with-test-app.js"
+);
+const DatastoreMongoFactory = locreq("lib/datastore/db.js");
+
+describe("reverse-single-reference", () => {
+ async function create_referencing_collections(app, with_reverse) {
+ const A = app.createChip(app.Sealious.Collection, {
+ name: "A",
+ fields: [
+ {
+ name: "reference_to_b",
+ type: "single_reference",
+ params: { collection: "B" },
+ },
+ {
+ name: "pairity",
+ type: "text",
+ },
+ ],
+ });
+ const B = app.createChip(app.Sealious.Collection, {
+ name: "B",
+ fields: [{ name: "number", type: "int" }],
+ });
+ if (with_reverse) {
+ B.add_field({
+ name: "references_in_a",
+ type: "reverse-single-reference",
+ params: { collection: A, field_name: "reference_to_b" },
+ });
+ }
+ }
+
+ async function create_resources(app) {
+ const numbers = [1, 2, 3];
+ const bs = await Promise.map(numbers, number =>
+ app.run_action(
+ new app.Sealious.SuperContext(),
+ ["collections", "B"],
+ "create",
+ { number }
+ )
+ );
+ for (let b of bs) {
+ for (let i = 1; i <= b.body.number; i++) {
+ await app.run_action(
+ new app.Sealious.SuperContext(),
+ ["collections", "A"],
+ "create",
+ { reference_to_b: b.id, pairity: i % 2 ? "odd" : "even" }
+ );
+ }
+ }
+ }
+
+ async function with_reverse(fn) {
+ return with_stopped_app(async args => {
+ await create_referencing_collections(args.app, "with_reverse" && true);
+ await args.app.start();
+ await create_resources(args.app);
+ await fn(args);
+ });
+ }
+
+ it("recreates the cached values if the field has just been added", async () => {
+ await with_stopped_app(async ({ app, dont_clear_database_on_stop }) => {
+ await create_referencing_collections(app, "with_reverse" && false);
+ await app.start();
+ await create_resources(app);
+ dont_clear_database_on_stop();
+ });
+ await with_stopped_app(async ({ base_url, app, rest_api }) => {
+ await create_referencing_collections(app, "with_reverse" && true);
+ await app.start();
+ const result = (await rest_api.get(
+ "/api/v1/collections/B?filter[number]=1"
+ ))[0];
+ assert(result.body.references_in_a);
+ assert.equal(result.body.references_in_a.length, 1);
+ const result2 = (await rest_api.get(
+ "/api/v1/collections/B?filter[number]=2"
+ ))[0];
+ assert(result2.body.references_in_a);
+ assert.equal(result2.body.references_in_a.length, 2);
+ });
+ });
+
+ it("updates the cached value when a new reference is created", async () => {
+ await with_stopped_app(async ({ app, rest_api }) => {
+ await create_referencing_collections(app, "with_reverse" && true);
+ await app.start();
+ await create_resources(app);
+ const result2 = (await rest_api.get(
+ "/api/v1/collections/B?filter[number]=2"
+ ))[0];
+ assert(result2.body.references_in_a instanceof Array);
+ assert.equal(result2.body.references_in_a.length, 2);
+ });
+ });
+
+ it("updates the cached value when an old reference is deleted", async () =>
+ with_reverse(async ({ app, rest_api }) => {
+ const result2 = (await rest_api.get(
+ "/api/v1/collections/B?filter[number]=2"
+ ))[0];
+ const referencing_id = result2.body.references_in_a[0];
+ await rest_api.delete(`/api/v1/collections/A/${referencing_id}`);
+ const new_result2 = (await rest_api.get(
+ "/api/v1/collections/B?filter[number]=2"
+ ))[0];
+ assert.equal(new_result2.body.references_in_a.length, 1);
+ }));
+
+ it("updates the cached value when an old reference is edited to a new one", async () =>
+ with_reverse(async ({ app, rest_api }) => {
+ const result1 = (await rest_api.get(
+ "/api/v1/collections/B?filter[number]=1"
+ ))[0];
+ const result2 = (await rest_api.get(
+ "/api/v1/collections/B?filter[number]=2"
+ ))[0];
+ const referencing_id = result2.body.references_in_a[0];
+
+ await rest_api.patch(`/api/v1/collections/A/${referencing_id}`, {
+ reference_to_b: result1.id,
+ });
+ const new_result2 = (await rest_api.get(
+ "/api/v1/collections/B?filter[number]=2"
+ ))[0];
+ assert.equal(new_result2.body.references_in_a.length, 1);
+ const new_result1 = (await rest_api.get(
+ "/api/v1/collections/B?filter[number]=1"
+ ))[0];
+ assert.equal(new_result1.body.references_in_a.length, 2);
+ }));
+
+ it("updates the cached value when an old reference is edited to an empty one", async () =>
+ with_reverse(async ({ app, rest_api }) => {
+ const result1 = (await rest_api.get(
+ "/api/v1/collections/B?filter[number]=1"
+ ))[0];
+ const result2 = (await rest_api.get(
+ "/api/v1/collections/B?filter[number]=2"
+ ))[0];
+ const referencing_id = result2.body.references_in_a[0];
+
+ await rest_api.patch(`/api/v1/collections/A/${referencing_id}`, {
+ reference_to_b: "",
+ });
+ const new_result2 = (await rest_api.get(
+ "/api/v1/collections/B?filter[number]=2"
+ ))[0];
+ assert.equal(new_result2.body.references_in_a.length, 1);
+ }));
+
+ it("allows to filter by a value of the referencing resource", async () =>
+ with_reverse(async ({ app, rest_api }) => {
+ let results = await rest_api.get(
+ "/api/v1/collections/B?filter[references_in_a][pairity]=non-existant"
+ );
+ assert.equal(results.length, 0);
+ results = await rest_api.get(
+ "/api/v1/collections/B?filter[references_in_a][pairity]=odd"
+ );
+ assert.equal(results.length, 3);
+ results = await rest_api.get(
+ "/api/v1/collections/B?filter[references_in_a][pairity]=even&filter[number]=3"
+ );
+ assert.equal(results.length, 1);
+ }));
+
+ it("allows to display the full body of the referencing resources", async () =>
+ with_reverse(async ({ app, rest_api }) => {
+ let results = await rest_api.get(
+ "/api/v1/collections/B?format[references_in_a]=expand"
+ );
+ assert(results[0].body.references_in_a[0].body);
+ }));
+});
diff --git a/lib/app/base-chips/field-types/single_reference.subtest.js b/lib/app/base-chips/field-types/single_reference.subtest.js
--- a/lib/app/base-chips/field-types/single_reference.subtest.js
+++ b/lib/app/base-chips/field-types/single_reference.subtest.js
@@ -2,7 +2,7 @@
const locreq = require("locreq")(__dirname);
const axios = require("axios");
const { create_resource_as } = locreq("test_utils");
-const with_test_app = locreq("test_utils/with-test-app.js");
+const { with_running_app } = locreq("test_utils/with-test-app.js");
describe("single_reference", () => {
async function create_referencing_collections(app) {
@@ -28,7 +28,7 @@
}
it("should not allow a value that is not an existing id", async () =>
- with_test_app(async ({ app, base_url }) => {
+ with_running_app(async ({ app, base_url }) => {
await create_referencing_collections(app);
return axios
.post(`${base_url}/api/v1/collections/A`, {
@@ -46,7 +46,7 @@
}));
it("should allow a value that exists in B", async () =>
- with_test_app(async ({ app, base_url }) => {
+ with_running_app(async ({ app, base_url }) => {
create_referencing_collections(app);
const b_id = (await axios.post(`${base_url}/api/v1/collections/B`, {
number: 1,
@@ -57,7 +57,7 @@
}));
it("should not allow a value that exists in B but does not meet the filter criteria", async () =>
- with_test_app(async ({ app, base_url }) => {
+ with_running_app(async ({ app, base_url }) => {
create_referencing_collections(app);
const b_id = (await axios.post(`${base_url}/api/v1/collections/B`, {
number: 0,
@@ -79,7 +79,7 @@
}));
it("should allow a value that exists in B but does not meet the filter criteria", async () =>
- with_test_app(async ({ app, base_url }) => {
+ with_running_app(async ({ app, base_url }) => {
create_referencing_collections(app);
const b_id = (await axios.post(`${base_url}/api/v1/collections/B`, {
number: 1,
diff --git a/lib/app/base-chips/special_filters/IsReferencedByResourcesMatching.subtest.js b/lib/app/base-chips/special_filters/IsReferencedByResourcesMatching.subtest.js
--- a/lib/app/base-chips/special_filters/IsReferencedByResourcesMatching.subtest.js
+++ b/lib/app/base-chips/special_filters/IsReferencedByResourcesMatching.subtest.js
@@ -5,7 +5,7 @@
const { create_resource_as } = locreq("test_utils");
const IsReferencedByResourcesMatching = require("./IsReferencedByResourcesMatching");
-const with_test_app = locreq("test_utils/with-test-app.js");
+const { with_running_app } = locreq("test_utils/with-test-app.js");
describe("IsReferencedByResourcesMatching", () => {
async function setup(app) {
@@ -86,7 +86,7 @@
}
it("returns only users with role matching `allowed_values`", () =>
- with_test_app(async ({ app, base_url }) => {
+ with_running_app(async ({ app, base_url }) => {
await setup(app);
return axios
.get(`${base_url}/api/v1/collections/users/@staff`)
diff --git a/lib/app/base-chips/special_filters/matches.subtest.js b/lib/app/base-chips/special_filters/matches.subtest.js
--- a/lib/app/base-chips/special_filters/matches.subtest.js
+++ b/lib/app/base-chips/special_filters/matches.subtest.js
@@ -5,7 +5,7 @@
const { create_resource_as } = locreq("test_utils");
const matches = require("./matches");
-const with_test_app = locreq("test_utils/with-test-app.js");
+const { with_running_app } = locreq("test_utils/with-test-app.js");
describe("Matches", () => {
async function setup(app) {
@@ -35,7 +35,7 @@
}
it("returns only positive numbers when using @positive filter", () =>
- with_test_app(async ({ app, base_url }) => {
+ with_running_app(async ({ app, base_url }) => {
await setup(app);
return axios
.get(
@@ -50,7 +50,7 @@
}));
it("returns empty array when using both @positive and @negative filters", () =>
- with_test_app(async ({ app, base_url }) => {
+ with_running_app(async ({ app, base_url }) => {
await setup(app);
return axios
.get(`${base_url}/api/v1/collections/numbers/@positive/@negative`)
diff --git a/lib/app/load-base-chips.js b/lib/app/load-base-chips.js
--- a/lib/app/load-base-chips.js
+++ b/lib/app/load-base-chips.js
@@ -45,6 +45,7 @@
"value-existing-in-collection",
"value-not-existing-in-collection",
"secret-token",
+ "reverse-single-reference",
]);
BaseChips.set(CalculatedFieldType, ["map-reduce", "aggregate", "custom"]);
diff --git a/lib/app/metadata.js b/lib/app/metadata.js
new file mode 100644
--- /dev/null
+++ b/lib/app/metadata.js
@@ -0,0 +1,25 @@
+const COLLECTION_NAME = "_metadata";
+
+module.exports = app => ({
+ db_collection_name: COLLECTION_NAME,
+ async get(key) {
+ const matches = await app.Datastore.find(COLLECTION_NAME, { key });
+ if (matches.length) {
+ return matches[0].value;
+ } else {
+ undefined;
+ }
+ },
+ async set(key, value) {
+ const matches = await app.Datastore.find(COLLECTION_NAME, { key });
+ if (matches.length) {
+ await app.Datastore.update(
+ COLLECTION_NAME,
+ { key: key },
+ { $set: { value: value } }
+ );
+ } else {
+ await app.Datastore.insert(COLLECTION_NAME, { key, value });
+ }
+ },
+});
diff --git a/lib/chip-types/collection.js b/lib/chip-types/collection.js
--- a/lib/chip-types/collection.js
+++ b/lib/chip-types/collection.js
@@ -73,8 +73,13 @@
Collection.type_name = "collection";
Collection.pure = {
- add_field: function(app, field_type, fields, field_declaration) {
- const field_object = new Field(app, field_declaration, field_type);
+ add_field: function(app, field_type, fields, field_declaration, collection) {
+ const field_object = new Field(
+ app,
+ field_declaration,
+ field_type,
+ collection
+ );
const field_name = field_object.name;
if (!fields[field_name]) {
fields[field_name] = field_object;
@@ -519,7 +524,7 @@
Collection.prototype = {
add_field(field_declaration) {
- return pure.add_field(this.app, this, this.fields, field_declaration);
+ return pure.add_field(this.app, this, this.fields, field_declaration, this);
},
add_fields(field_declarations_array) {
return pure.add_fields(
diff --git a/lib/chip-types/field-type-default-methods.js b/lib/chip-types/field-type-default-methods.js
--- a/lib/chip-types/field-type-default-methods.js
+++ b/lib/chip-types/field-type-default-methods.js
@@ -5,67 +5,73 @@
const FieldTypeDescription = require("../data-structures/field-type-description.js");
const default_methods = {
- has_index: function(params){
+ init: function() {
+ return null;
+ },
+ has_index: function(params) {
return false;
},
- is_proper_value: function(context, params, new_value, old_value){
+ is_proper_value: function(context, params, new_value, old_value) {
return Promise.resolve();
},
- format: function(context, params, decoded_value, format_params){
+ format: function(context, params, decoded_value, format_params) {
return decoded_value;
},
- encode: function(context, params, value_in_code){
+ encode: function(context, params, value_in_code) {
return value_in_code;
},
- get_description: function(context, params){
+ get_description: function(context, params) {
return new FieldTypeDescription(this.name);
},
- decode: function(context, params, value_in_database){
+ decode: function(context, params, value_in_database) {
return value_in_database;
},
- filter_to_query: function(context, params, query){
- return Promise.resolve(this.encode(context, params, query))
- .then(function(encoded_value){
+ filter_to_query: function(context, params, query) {
+ return Promise.resolve(this.encode(context, params, query)).then(function(
+ encoded_value
+ ) {
return {
$eq: encoded_value,
};
});
},
- full_text_search_enabled: function(){
+ full_text_search_enabled: function() {
return false;
},
- get_aggregation_stages: function(context, params, field_name, query_params){
+ get_aggregation_stages: function(context, params, field_name, query_params) {
const self = this;
- if(!query_params || !query_params.filter) return Promise.resolve([]);
+ if (!query_params || !query_params.filter) return Promise.resolve([]);
const expanded_filter = expandHash(query_params.filter);
let field_filter = expanded_filter[field_name];
- if(field_filter && field_filter.length === 1 && field_filter[0] instanceof Array){
+ if (
+ field_filter &&
+ field_filter.length === 1 &&
+ field_filter[0] instanceof Array
+ ) {
field_filter = field_filter[0]; // to fix an edge case where instead of array of values the array is wrapped within another array
}
- if(!(field_name in expanded_filter)){
+ if (!(field_name in expanded_filter)) {
return Promise.resolve([]);
}
- if(field_name in expanded_filter && field_filter === undefined)
- return Promise.resolve([{$match: {[`body.${field_name}`]: {$exists: false}}}]);
+ if (field_name in expanded_filter && field_filter === undefined)
+ return Promise.resolve([
+ { $match: { [`body.${field_name}`]: { $exists: false } } },
+ ]);
let new_filter = null;
- if(field_filter instanceof Array){
+ if (field_filter instanceof Array) {
new_filter = Promise.all(
- field_filter.map(function(element){
+ field_filter.map(function(element) {
return self.encode(context, params, element);
})
- )
- .then((filters)=> {
- return {$in: filters};
+ ).then(filters => {
+ return { $in: filters };
});
- }else{
+ } else {
new_filter = self.filter_to_query(context, params, field_filter);
}
- return new_filter
- .then(function(filter){
- return [
- {$match: {[`body.${field_name}`]: filter}},
- ];
- });
+ return new_filter.then(function(filter) {
+ return [{ $match: { [`body.${field_name}`]: filter } }];
+ });
},
};
diff --git a/lib/chip-types/field.js b/lib/chip-types/field.js
--- a/lib/chip-types/field.js
+++ b/lib/chip-types/field.js
@@ -3,27 +3,29 @@
const default_methods = require("./field-type-default-methods.js");
const FieldType = locreq("lib/chip-types/field-type.js");
-function Field (app, declaration){
-
+function Field(app, declaration, collection) {
this.name = declaration.name;
this.declaration = declaration;
this.type = new FieldType(app, declaration.type);
this.required = declaration.required || false;
this.params = declaration.params || {};
+ this.type.init(collection, declaration.name, this.params);
const self = this;
- for (const method_name in default_methods){
- this[method_name] = (function(method_name){
- return function(){
- const arguments_array = Object.keys(arguments).map((key)=>arguments[key]);
+ for (const method_name in default_methods) {
+ this[method_name] = (function(method_name) {
+ return function() {
+ const arguments_array = Object.keys(arguments).map(
+ key => arguments[key]
+ );
arguments_array.splice(1, 0, self.params);
return self.type[method_name].apply(self.type, arguments_array);
};
})(method_name);
}
- this.get_specification = function(){
+ this.get_specification = function() {
return {
name: this.name,
type: this.type,
@@ -31,14 +33,17 @@
};
};
- this.get_aggregation_stages = function(context, query_params){
+ this.get_aggregation_stages = function(context, query_params) {
const self = this;
return Promise.resolve(
- self.type.get_aggregation_stages(context, self.params, self.name, query_params)
+ self.type.get_aggregation_stages(
+ context,
+ self.params,
+ self.name,
+ query_params
+ )
);
-
};
-
}
module.exports = Field;
diff --git a/lib/email/templates/simple.test.js b/lib/email/templates/simple.test.js
--- a/lib/email/templates/simple.test.js
+++ b/lib/email/templates/simple.test.js
@@ -1,10 +1,10 @@
const locreq = require("locreq")(__dirname);
const assert = require("assert");
-const with_test_app = locreq("test_utils/with-test-app.js");
+const { with_running_app } = locreq("test_utils/with-test-app.js");
describe("simpleTemplate", () => {
it("sends an email", async () =>
- with_test_app(async ({ app, mail_api }) => {
+ with_running_app(async ({ app, mail_api }) => {
const message = await app.EmailTemplates.Simple(app, {
to: "test@example.com",
subject: "Congratulations!",
diff --git a/lib/http/routes/confirm-password-reset.test.js b/lib/http/routes/confirm-password-reset.test.js
--- a/lib/http/routes/confirm-password-reset.test.js
+++ b/lib/http/routes/confirm-password-reset.test.js
@@ -1,11 +1,11 @@
const locreq = require("locreq")(__dirname);
const axios = require("axios");
const assert = require("assert");
-const with_test_app = locreq("test_utils/with-test-app.js");
+const { with_running_app } = locreq("test_utils/with-test-app.js");
describe("confirm-password-reset", () => {
it("displays an html form", async () =>
- with_test_app(async ({ app, base_url }) => {
+ with_running_app(async ({ app, base_url }) => {
const response = await axios.get(
`${base_url}/confirm-password-reset?token=kupcia&email=dupcia`
);
diff --git a/lib/http/routes/finalize-password-reset.test.js b/lib/http/routes/finalize-password-reset.test.js
--- a/lib/http/routes/finalize-password-reset.test.js
+++ b/lib/http/routes/finalize-password-reset.test.js
@@ -3,7 +3,7 @@
const axios = require("axios");
const tough = require("tough-cookie");
const { promise_timeout, assert_throws_async } = locreq("test_utils");
-const with_test_app = locreq("test_utils/with-test-app.js");
+const { with_running_app } = locreq("test_utils/with-test-app.js");
describe("finalize password reset", () => {
async function create_a_user(app) {
@@ -20,7 +20,7 @@
}
it("allows to change a password (entire flow)", async () =>
- with_test_app(async ({ app, base_url, mail_api }) => {
+ with_running_app(async ({ app, base_url, mail_api }) => {
await create_a_user(app);
const cookieJar = new tough.CookieJar();
const options = {
diff --git a/lib/http/routes/finalize-registration-intent.test.js b/lib/http/routes/finalize-registration-intent.test.js
--- a/lib/http/routes/finalize-registration-intent.test.js
+++ b/lib/http/routes/finalize-registration-intent.test.js
@@ -3,11 +3,11 @@
const axios = require("axios");
const tough = require("tough-cookie");
const { promise_timeout, assert_throws_async } = locreq("test_utils");
-const with_test_app = locreq("test_utils/with-test-app.js");
+const { with_running_app } = locreq("test_utils/with-test-app.js");
describe("finalize registration", () => {
it("allows to register an account (entire flow)", async () =>
- with_test_app(async ({ app, base_url, mail_api }) => {
+ with_running_app(async ({ app, base_url, mail_api }) => {
const cookieJar = new tough.CookieJar();
const options = {
jar: cookieJar,
diff --git a/test_utils/with-test-app.js b/test_utils/with-test-app.js
--- a/test_utils/with-test-app.js
+++ b/test_utils/with-test-app.js
@@ -1,7 +1,12 @@
const locreq = require("locreq")(__dirname);
const axios = require("axios");
-module.exports = async function with_test_app(fn) {
+module.exports = {
+ with_stopped_app: with_test_app.bind(global, "auto_start" && false),
+ with_running_app: with_test_app.bind(global, "auto_start" && true),
+};
+
+async function with_test_app(auto_start, fn) {
let app = null;
const port = 8888;
const base_url = `http://localhost:${port}`;
@@ -41,15 +46,26 @@
}
);
- app.on("stop", async () =>
- Promise.all(
- app.ChipManager.get_all_collections().map(collection_name =>
- app.Datastore.remove(collection_name, {}, "just_one" && false)
- )
- )
- );
+ let clear_database_on_stop = true;
+
+ app.on("stop", async () => {
+ if (clear_database_on_stop) {
+ await Promise.all(
+ app.ChipManager.get_all_collections().map(collection_name =>
+ app.Datastore.remove(collection_name, {}, "just_one" && false)
+ )
+ );
+ await app.Datastore.remove(
+ app.Metadata.db_collection_name,
+ {},
+ "just_one" && false
+ );
+ }
+ });
- await app.start();
+ if (auto_start) {
+ await app.start();
+ }
try {
await axios.delete(`${smtp_api_url}/messages`);
@@ -64,11 +80,19 @@
get_message_by_id: async id =>
(await axios.get(`${smtp_api_url}/messages/${id}.html`)).data,
},
+ dont_clear_database_on_stop: () => (clear_database_on_stop = false),
+ rest_api: {
+ get: async url => (await axios.get(`${base_url}${url}`)).data,
+ delete: async url => (await axios.delete(`${base_url}${url}`)).data,
+ patch: async (url, data) =>
+ (await axios.patch(`${base_url}${url}`, data)).data,
+ },
});
-
- return await app.stop();
} catch (e) {
- await app.stop();
throw e;
+ } finally {
+ if (app.status !== "stopped") {
+ await app.stop();
+ }
}
-};
+}
File Metadata
Details
Attached
Mime Type
text/plain
Expires
Sat, Nov 23, 01:26 (16 h, 43 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
548058
Default Alt Text
D200.id692.diff (33 KB)
Attached To
Mode
D200: Reverse single reference
Attached
Detach File
Event Timeline
Log In to Comment