Page MenuHomeSealhub

No OneTemporary

diff --git a/src/app/policy-types/and.subtest.ts b/src/app/policy-types/and.subtest.ts
index 415b0f29..868a39d3 100644
--- a/src/app/policy-types/and.subtest.ts
+++ b/src/app/policy-types/and.subtest.ts
@@ -1,127 +1,129 @@
import assert from "assert";
import Bluebird from "bluebird";
import { App, Collection, FieldTypes, Policies } from "../../main";
import { withRunningApp } from "../../test_utils/with-test-app";
import create_policies from "../../test_utils/policy-types/create-policies-with-complex-pipeline";
import And from "./and";
import { assertThrowsAsync } from "../../test_utils/assert-throws-async";
import { TestAppType } from "../../test_utils/test-app";
const [ComplexDenyPipeline, ComplexAllowPipeline] = create_policies.allowDeny();
const collections_to_create = [
{
name:
"collection-and(nested-and(allow, public), nested-or(allow, noone))",
policies: [
new And([new ComplexAllowPipeline(), new Policies.Public()]),
new Policies.Or([new ComplexAllowPipeline(), new Policies.Noone()]),
],
},
{
name: "collection-and(ComplexAllowPipeline, noone)",
policies: [new ComplexAllowPipeline(), new Policies.Noone()],
},
{
name: "collection-and(ComplexAllowPipeline, public)",
policies: [new ComplexAllowPipeline(), new Policies.Public()],
},
{
name: "collection-and(complexDenyPipeline, public)",
policies: [new ComplexDenyPipeline(), new Policies.Public()],
},
];
function extend(t: TestAppType) {
const collections: { [name: string]: Collection } = {};
for (const { name, policies } of collections_to_create) {
collections[name] = new (class extends Collection {
name = name;
fields = {
number: new FieldTypes.SingleReference("numbers"),
};
policies = {
+ list: new And(policies),
show: new And(policies),
create: new Policies.Public(),
};
})();
}
return class extends t {
collections = {
...t.BaseCollections,
+ ...collections,
numbers: new (class extends Collection {
name = "numbers";
fields = {
number: new FieldTypes.Int(),
};
})(),
};
};
}
describe("AndPolicy", () => {
async function setup(app: App) {
let numbers = await Bluebird.map([0, 1, 2], (n) =>
app.collections.numbers.suCreate({
number: n,
})
);
for (const number of numbers) {
- await Bluebird.map(collections_to_create, ({ name }) =>
+ await Bluebird.map(collections_to_create, ({ name }) => {
app.collections[name].suCreate({
number: number.id,
- })
- );
+ });
+ });
}
}
it("return everything for collection-and(nested-and(allow, public), nested-or(allow, noone))", () =>
withRunningApp(extend, async ({ app, rest_api }) => {
await setup(app);
return rest_api
.get(
"/api/v1/collections/collection-and(nested-and(allow, public), nested-or(allow, noone))"
)
.then(({ items }: { items: any[] }) =>
assert.equal(items.length, 3)
);
}));
it("returns nothing for and(ComplexAllowPipeline, noone)", () =>
withRunningApp(extend, async ({ app, rest_api }) => {
await setup(app);
await assertThrowsAsync(
() =>
rest_api.get(
"/api/v1/collections/collection-and(ComplexAllowPipeline, noone)"
),
(e) => {
assert.equal(e.response.data.message, `noone is allowed`);
}
);
}));
it("returns everything for and(ComplexAllowPipeline, public)", () =>
withRunningApp(extend, async ({ app, rest_api }) => {
await setup(app);
return rest_api
.get(
"/api/v1/collections/collection-and(ComplexAllowPipeline, public)"
)
.then(({ items }: { items: any[] }) =>
- assert.equal(items.length, 3)
+ assert.strictEqual(items.length, 3)
);
}));
it("returns nothing for and(complex-deny-pipeline, public)", () =>
withRunningApp(extend, async ({ app, rest_api }) => {
await setup(app);
const { items } = await rest_api.get(
"/api/v1/collections/collection-and(complexDenyPipeline, public)"
);
- assert.equal(items.length, 0);
+ assert.strictEqual(items.length, 0);
}));
});
diff --git a/src/app/policy-types/and.ts b/src/app/policy-types/and.ts
index cf8cd5d2..6a467986 100644
--- a/src/app/policy-types/and.ts
+++ b/src/app/policy-types/and.ts
@@ -1,38 +1,39 @@
import Bluebird from "bluebird";
import { And as AndQuery } from "../../datastore/query";
import { Context } from "../../main";
import Policy, { ReducingPolicy } from "../../chip-types/policy";
import { CollectionItem } from "../../chip-types/collection-item";
export default class And extends ReducingPolicy {
static type_name = "and";
async _getRestrictingQuery(context: Context) {
const queries = await Bluebird.map(this.policies, (strategy) =>
strategy.getRestrictingQuery(context)
);
- return new AndQuery(...queries);
+ const ret = new AndQuery(...queries);
+ return ret;
}
isItemSensitive() {
return Bluebird.map(this.policies, (strategy) =>
strategy.isItemSensitive()
).reduce((a, b) => a || b);
}
async checkerFunction(
context: Context,
item_getter: () => Promise<CollectionItem>
) {
const results = await this.checkAllPolicies(context, item_getter);
const negatives = results.filter((result) => result?.allowed === false);
if (negatives.length > 0) {
return Policy.deny(`${negatives.map((n) => n?.reason).join(", ")}`);
}
return Policy.allow(
`${results
.filter((r) => r !== null)
.map((r) => r?.reason)
.join(", ")}`
);
}
}
diff --git a/src/chip-types/item-list.ts b/src/chip-types/item-list.ts
index 69a43cef..84f0c919 100644
--- a/src/chip-types/item-list.ts
+++ b/src/chip-types/item-list.ts
@@ -1,372 +1,373 @@
import { CollectionItem } from "./collection-item";
import Collection from "./collection";
import { Context, Query } from "../main";
import { BadContext, NotFound, BadSubjectAction } from "../response/errors";
import QueryStage from "../datastore/query-stage";
import sealious_to_mongo_sort_param from "../utils/mongo-sorts";
import { ItemFields } from "./collection-item-body";
type FilterT<T extends Collection> = Partial<ItemFields<T>>;
type PaginationParams = {
page: number;
items: number;
forward_buffer: number;
};
type SortParams<T extends Collection> = {
[key in keyof T["fields"]]: keyof typeof sealious_to_mongo_sort_param;
};
type FormatParam<T extends Collection> = {
[key in keyof T["fields"]]: any;
};
type AllInOneParams<T extends Collection> = {
sort: Parameters<ItemList<T>["sort"]>[0];
filter: Parameters<ItemList<T>["filter"]>[0];
pagination: Parameters<ItemList<T>["paginate"]>[0];
attachments: Parameters<ItemList<T>["attach"]>[0];
format: Parameters<ItemList<T>["format"]>[0];
};
/** Which fields to fetch attachments for. Can be nested, as one
* resource can point to another one and that one can also have
* attachments
*/
export type AttachmentOptions = {
[field_name: string]: any;
};
export default class ItemList<T extends Collection> {
private fields_with_attachments_fetched: string[] = [];
private _attachments_options: AttachmentOptions = {};
private _filter: FilterT<T>;
private _format: FormatParam<T>;
private _ids: string[];
private _search: string;
private _sort: ItemFields<T>;
private context: Context;
private collection: Collection;
private aggregation_stages: QueryStage[] = [];
private await_before_fetch: Promise<any>[] = [];
private is_paginated: boolean = false;
private is_sorted: boolean = false;
private pagination: Partial<PaginationParams> = {};
constructor(collection: T, context: Context) {
this.context = context;
this.collection = collection;
this.await_before_fetch = [
this.collection
.getPolicy("list")
.getRestrictingQuery(context)
- .then((query) => query.toPipeline),
+ .then((query) => query.toPipeline())
+ .then((stages) => this.aggregation_stages.push(...stages)),
];
}
filter(filter?: FilterT<T>): ItemList<T> {
if (this._filter) {
throw new Error("Filter already set");
}
if (!filter) {
return this;
}
this._filter = filter;
for (const field_name in filter) {
this.context.app.Logger.debug3(
"ITEM",
"Setting filter for field:",
{ [field_name]: filter[field_name] }
);
if (!this.collection.fields[field_name]) {
throw new Error(
`Unknown field: '${field_name}' in '${this.collection.name}' collection`
);
}
const promise = this.collection.fields[field_name]
.getAggregationStages(this.context, filter[field_name])
.then((stages) => {
this.aggregation_stages.push(...stages);
this.context.app.Logger.debug3(
"ITEM",
"Adding aggregation stage for field",
{ [field_name]: stages }
);
});
this.await_before_fetch.push(promise);
}
return this;
}
format(format?: FormatParam<T>): this {
if (this._format) {
throw new Error("Already formatted!");
}
if (format) {
this._format = format;
}
return this;
}
paginate(pagination_params?: Partial<PaginationParams>): ItemList<T> {
if (!pagination_params) {
return this;
}
this.pagination = pagination_params;
this.is_paginated = true;
return this;
}
search(term: string): ItemList<T> {
if (this._search) {
throw new Error("Search term already set");
}
this.aggregation_stages.push({
$match: {
$text: {
$search: term.toString(),
$caseSensitive: false,
$diacriticSensitive: false,
},
},
});
return this;
}
ids(ids: string[]): ItemList<T> {
if (this._ids) {
throw new Error("ids already filtered");
}
this.aggregation_stages.push(
...Query.fromSingleMatch({
id: { $in: ids },
}).toPipeline()
);
return this;
}
namedFilter(filter_name: string): ItemList<T> {
this.await_before_fetch.push(
this.collection.named_filters[filter_name]
.getFilteringQuery()
.then((query) => {
this.aggregation_stages.push(...query.toPipeline());
})
);
return this;
}
attach(attachment_options?: AttachmentOptions): ItemList<T> {
if (attachment_options === undefined || !attachment_options) {
return this;
}
this.context.app.Logger.debug3(
"ITEM LIST",
"Attaching fields:",
attachment_options
);
//can be called multiple times
for (const field_name of Object.keys(attachment_options)) {
const field = this.collection.fields[field_name];
if (!field) {
throw new NotFound(
`Given field ${field_name} is not declared in collection!`
);
}
this.fields_with_attachments_fetched.push(field_name);
}
this._attachments_options = attachment_options;
return this;
}
private async fetchAttachments(items: CollectionItem<T>[]) {
const promises: Promise<any>[] = [];
let attachments: { [id: string]: CollectionItem<T> } = {};
for (const field_name of this.fields_with_attachments_fetched) {
this.context.app.Logger.debug2(
"ATTACH",
`Loading attachments for ${field_name}`
);
promises.push(
this.collection.fields[field_name]
.getAttachments(
this.context,
items.map((item) => item.get(field_name)),
this._attachments_options[field_name]
)
.then((attachmentsList) => {
attachments = {
...attachments,
...attachmentsList.flattenWithAttachments(),
};
this.context.app.Logger.debug3(
"ATTACH",
`Fetched attachments for ${field_name}:`,
attachmentsList
);
})
);
}
await Promise.all(promises);
return attachments;
}
async fetch(): Promise<ItemListResult<T>> {
const result = await this.collection
.getPolicy("show")
.check(this.context);
if (result !== null && !result.allowed) {
throw new BadContext(result.reason as string);
}
const aggregation_stages = await this.getAggregationStages();
const documents = await this.context.app.Datastore.aggregate(
this.collection.name,
aggregation_stages,
{}
);
const item_promises: Promise<CollectionItem<T>>[] = [];
for (const document of documents) {
const item = this.collection.createFromDB(document);
item_promises.push(
item.decode(this.context, this._format) as Promise<
CollectionItem<T>
>
);
}
const items = await Promise.all(item_promises);
const attachments = await this.fetchAttachments(items);
return new ItemListResult(
items,
this.fields_with_attachments_fetched,
attachments
);
}
public async getAggregationStages() {
await Promise.all(this.await_before_fetch);
this.await_before_fetch = [];
return [
...this.aggregation_stages,
...this.getSortingStages(),
...this.getPaginationStages(),
];
}
public sort(sort_params?: SortParams<T>) {
if (!sort_params) {
return this;
}
this.is_sorted = true;
this._sort = sort_params;
return this;
}
public setParams(params: Partial<AllInOneParams<T>>) {
return this.filter(params.filter)
.paginate(params.pagination)
.sort(params?.sort)
.attach(params?.attachments)
.format(params?.format);
}
private getSortingStages() {
if (!this.is_sorted) {
return [];
}
const $sort: { [field_name: string]: -1 | 1 } = {};
for (const field_name in this._sort) {
const mongo_sort_param =
sealious_to_mongo_sort_param[this._sort[field_name]];
if (!mongo_sort_param) {
const available_sort_keys = Object.keys(
sealious_to_mongo_sort_param
).join(", ");
throw new BadSubjectAction(
`Unknown sort key: ${this._sort[field_name]}. Available sort keys are: ${available_sort_keys}.`
);
}
$sort[field_name] = mongo_sort_param as -1 | 1;
}
return [{ $sort }];
}
private getPaginationStages(): QueryStage[] {
if (!this.is_paginated) {
return [];
}
const full_pagination_params: PaginationParams = {
page: 1,
items: 10,
forward_buffer: 0,
...this.pagination,
};
const $skip =
(full_pagination_params.page - 1) * full_pagination_params.items;
const $limit =
full_pagination_params.items +
full_pagination_params.forward_buffer || 0;
// prettier-ignore
return [ {$skip}, {$limit} ];
}
}
export class ItemListResult<T extends Collection> {
constructor(
public items: CollectionItem<T>[],
public fields_with_attachments: string[],
public attachments: { [id: string]: CollectionItem<T> } = {}
) {}
// this generator method makes the instance of this class iterable with for..of
*[Symbol.iterator](): Iterator<CollectionItem<T>> {
for (const item of this.items) {
yield item;
}
}
get empty(): boolean {
return this.items.length === 0;
}
serialize() {
return {
items: this.items.map((item) => item.serializeBody()),
attachments: Object.fromEntries(
Object.entries(this.attachments).map(([id, item]) => [
id,
item.serializeBody(),
])
),
fields_with_attachments: this.fields_with_attachments,
};
}
static fromSerialized<T extends Collection>(
collection: T,
serialized: {
items: any[];
attachments: { [id: string]: any };
fields_with_attachments: string[];
}
) {
return new ItemListResult<T>(
serialized.items.map((item_data) =>
CollectionItem.fromSerialized(
collection,
item_data,
serialized.attachments
)
),
serialized.fields_with_attachments,
serialized.attachments
);
}
flattenWithAttachments() {
return {
...this.attachments,
...Object.fromEntries(this.items.map((item) => [item.id, item])),
};
}
}
diff --git a/src/datastore/query-and.ts b/src/datastore/query-and.ts
index 25dc2fb0..c04c3485 100644
--- a/src/datastore/query-and.ts
+++ b/src/datastore/query-and.ts
@@ -1,96 +1,97 @@
import Query from "./query";
import QueryStep, { Match } from "./query-step";
import Graph from "./graph";
import { QueryTypes } from "../main";
export default class And extends Query {
graph: Graph;
aggregation_steps: { [id: string]: QueryStep | QueryStep[] };
received_deny_all: boolean;
constructor(...queries: Query[]) {
super();
this._reset();
for (let query of queries) {
this.addQuery(query);
}
}
_reset() {
this.graph = new Graph();
this.aggregation_steps = {};
this.received_deny_all = false;
}
addQuery(query: Query) {
if (this.received_deny_all) {
return;
}
if (query instanceof QueryTypes.DenyAll) {
this._reset();
this.received_deny_all = true;
}
const steps = query.dump();
for (let step of steps) {
const id = step.hash();
if (this._isInGraph(id)) {
continue;
}
this._addToAggregationSteps(id, step);
this._addDependenciesInGraph(id, step);
}
}
_isInGraph(key: string) {
return key.length === 32 && this.graph.node_ids.includes(key);
}
_addToAggregationSteps(id: string, step: QueryStep) {
this.graph.addNode(id, step.getCost());
this.aggregation_steps[id] = step;
}
_addDependenciesInGraph(id: string, step: QueryStep) {
let dependencies = step
.getUsedFields()
.filter((field) => this._isInGraph(field));
if (step instanceof Match) {
dependencies = dependencies.filter((d1) =>
this._isNotDependencyForAnyInGroup(d1, dependencies)
);
}
for (let dependency of dependencies) {
this.graph.addEdge(dependency, id);
}
}
_isNotDependencyForAnyInGroup(id: string, nodeGroup: string[]) {
return !nodeGroup.some(
(node) => id !== node && this.graph.pathExists(id, node)
);
}
dump() {
const sortedStepIds = this.graph.bestFirstSearch();
return sortedStepIds.reduce((steps, id) => {
if (Array.isArray(this.aggregation_steps[id])) {
steps.push(...(this.aggregation_steps[id] as QueryStep[]));
} else {
steps.push(this.aggregation_steps[id] as QueryStep);
}
return steps;
}, [] as QueryStep[]);
}
toPipeline() {
const sortedStepIds = this.graph.bestFirstSearch();
- return sortedStepIds.reduce((pipeline, id) => {
+ const ret = sortedStepIds.reduce((pipeline, id) => {
if (Array.isArray(this.aggregation_steps[id])) {
for (let step of this.aggregation_steps[id] as QueryStep[]) {
step.pushStage(pipeline);
}
return pipeline;
}
return (this.aggregation_steps[id] as QueryStep).pushStage(
pipeline
);
}, []);
+ return ret;
}
}

File Metadata

Mime Type
text/x-diff
Expires
Sat, Nov 8, 04:45 (1 d, 4 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1034025
Default Alt Text
(18 KB)

Event Timeline