Page Menu
Home
Sealhub
Search
Configure Global Search
Log In
Files
F10360141
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
18 KB
Referenced Files
None
Subscribers
None
View Options
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
Details
Attached
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)
Attached To
Mode
rS Sealious
Attached
Detach File
Event Timeline
Log In to Comment