@arcote.tech/arc 0.5.0 → 0.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/context-element/aggregate/aggregate-data.d.ts +3 -0
- package/dist/context-element/aggregate/aggregate-element.d.ts +3 -2
- package/dist/context-element/function/arc-function-data.d.ts +13 -2
- package/dist/context-element/listener/listener.d.ts +8 -0
- package/dist/context-element/view/view-data.d.ts +5 -5
- package/dist/context-element/view/view.d.ts +3 -2
- package/dist/data-storage/index.d.ts +1 -0
- package/dist/data-storage/scoped-data-storage.d.ts +35 -0
- package/dist/index.js +306 -176
- package/package.json +1 -1
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import type { ArcIdAny } from "../../elements/id";
|
|
8
8
|
import type { ArcObjectAny } from "../../elements/object";
|
|
9
|
+
import type { ArcContextElement } from "../context-element";
|
|
9
10
|
import type { Simplify } from "../../utils";
|
|
10
11
|
import type { ArcEventAny } from "../event/event";
|
|
11
12
|
import type { ArcEventInstance } from "../event/instance";
|
|
@@ -34,6 +35,8 @@ export interface AggregateMutateMethodEntry {
|
|
|
34
35
|
readonly handler: Function | false | null;
|
|
35
36
|
readonly result?: any;
|
|
36
37
|
readonly cronExpression?: string;
|
|
38
|
+
readonly queryElements?: ArcContextElement<any>[];
|
|
39
|
+
readonly mutationElements?: ArcContextElement<any>[];
|
|
37
40
|
}
|
|
38
41
|
/**
|
|
39
42
|
* Cron method entry extracted at build time.
|
|
@@ -111,7 +111,7 @@ export declare class ArcAggregateElement<Name extends string = string, Id extend
|
|
|
111
111
|
data: any[];
|
|
112
112
|
version: number;
|
|
113
113
|
});
|
|
114
|
-
protectBy<T extends ArcTokenAny>(token: T, protectionFn: ViewProtectionFn<
|
|
114
|
+
protectBy<T extends ArcTokenAny>(token: T, protectionFn: ViewProtectionFn<T>): ArcAggregateElement<Name, Id, Schema, Events, MutateMethods, QueryMethods>;
|
|
115
115
|
event<const EventName extends string, PayloadShape extends ArcRawShape>(eventName: EventName, payload: PayloadShape, handler: (ctx: ArcViewHandlerContext<Id, Schema>, event: ArcEventInstance<InlineArcEvent<EventName, PayloadShape>>) => Promise<void>): ArcAggregateElement<Name, Id, Schema, [
|
|
116
116
|
...Events,
|
|
117
117
|
AggregateEventEntry<InlineArcEvent<EventName, PayloadShape>, Id, Schema, false>
|
|
@@ -196,8 +196,9 @@ export declare class ArcAggregateElement<Name extends string = string, Id extend
|
|
|
196
196
|
get cronMethods(): AggregateCronMethodEntry[];
|
|
197
197
|
queryContext(adapters: ModelAdapters): AggregateQueryContext<QueryMethods>;
|
|
198
198
|
private buildPrivateQuery;
|
|
199
|
-
private
|
|
199
|
+
private isScopeDenied;
|
|
200
200
|
private getAuth;
|
|
201
|
+
private getScopeRestrictions;
|
|
201
202
|
mutateContext(adapters: ModelAdapters): AggregateMutateMethodContext<MutateMethods>;
|
|
202
203
|
databaseStoreSchema(): DatabaseStoreSchema;
|
|
203
204
|
getSeeds(): {
|
|
@@ -1,18 +1,29 @@
|
|
|
1
1
|
import type { ArcObjectAny } from "../../elements/object";
|
|
2
2
|
import type { ArcToken, ArcTokenAny } from "../../token/token";
|
|
3
|
-
import type { TokenInstanceAny } from "../../token/token-instance";
|
|
3
|
+
import type { TokenInstance, TokenInstanceAny } from "../../token/token-instance";
|
|
4
4
|
import type { $type } from "../../utils/types/get-type";
|
|
5
5
|
import type { ArcContextElement } from "../context-element";
|
|
6
6
|
import type { ElementContext } from "../element-context";
|
|
7
|
+
/**
|
|
8
|
+
* Extract typed TokenInstance from a token definition.
|
|
9
|
+
*
|
|
10
|
+
* Given `ArcToken<"workspace", { workspaceId: ArcId, role: ArcString }, ...>`,
|
|
11
|
+
* returns `TokenInstance<{ workspaceId: string, role: string }, RuleNames>`.
|
|
12
|
+
*/
|
|
13
|
+
export type TypedTokenInstance<T extends ArcTokenAny> = T extends ArcToken<any, infer ParamsShape, any, infer Rules> ? TokenInstance<$type<import("../../elements/object").ArcObject<ParamsShape>>, Rules extends Record<string, any> ? Extract<keyof Rules, string> : string> : TokenInstanceAny;
|
|
7
14
|
/**
|
|
8
15
|
* Unified protection check function.
|
|
9
16
|
*
|
|
17
|
+
* Callback receives a **typed** TokenInstance — `token.params.workspaceId`
|
|
18
|
+
* has full autocomplete and type safety.
|
|
19
|
+
*
|
|
10
20
|
* Return value determines behavior:
|
|
11
21
|
* - `false` → access denied
|
|
12
22
|
* - `true` → access allowed, no data filter
|
|
13
23
|
* - `Record` → access allowed + merged into WHERE clause (query context)
|
|
24
|
+
* + validated on writes (scope enforcement)
|
|
14
25
|
*/
|
|
15
|
-
export type FnProtectionCheck<T extends ArcTokenAny> = (tokenInstance:
|
|
26
|
+
export type FnProtectionCheck<T extends ArcTokenAny> = (tokenInstance: TypedTokenInstance<T>) => boolean | Record<string, unknown> | Promise<boolean | Record<string, unknown>>;
|
|
16
27
|
/**
|
|
17
28
|
* Protection configuration for an ArcFunction.
|
|
18
29
|
*/
|
|
@@ -76,6 +76,14 @@ export declare class ArcListener<const Data extends ArcListenerData> extends Arc
|
|
|
76
76
|
get isAsync(): boolean;
|
|
77
77
|
init(environment: ArcEnvironment, adapters: ModelAdapters): Promise<void>;
|
|
78
78
|
private handleEvent;
|
|
79
|
+
/**
|
|
80
|
+
* Build scoped adapters from event payload.
|
|
81
|
+
*
|
|
82
|
+
* Looks at declared query/mutate elements — if any have protections,
|
|
83
|
+
* extracts the token name and creates an AuthAdapter with event payload
|
|
84
|
+
* as decoded params. This allows aggregate restrictions to apply.
|
|
85
|
+
*/
|
|
86
|
+
private buildScopedAdapters;
|
|
79
87
|
destroy(): void;
|
|
80
88
|
}
|
|
81
89
|
export declare function listener<const Name extends string>(name: Name): ArcListener<{
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { WhereCondition } from "../../data-storage/find-options";
|
|
2
2
|
import type { ArcObjectAny } from "../../elements/object";
|
|
3
|
-
import type { ArcTokenAny } from "../../token/token";
|
|
3
|
+
import type { ArcToken, ArcTokenAny } from "../../token/token";
|
|
4
4
|
import type { ArcContextElement } from "../context-element";
|
|
5
5
|
/**
|
|
6
6
|
* Protection config for views
|
|
@@ -9,11 +9,11 @@ import type { ArcContextElement } from "../context-element";
|
|
|
9
9
|
*/
|
|
10
10
|
export type ViewProtectionConfig = WhereCondition | false;
|
|
11
11
|
/**
|
|
12
|
-
* Protection function for views
|
|
13
|
-
* Receives token params and returns read conditions (where clause)
|
|
14
|
-
* Return false to deny access entirely
|
|
12
|
+
* Protection function for views/aggregates.
|
|
13
|
+
* Receives typed token params and returns read/write conditions (where clause).
|
|
14
|
+
* Return false to deny access entirely.
|
|
15
15
|
*/
|
|
16
|
-
export type ViewProtectionFn<
|
|
16
|
+
export type ViewProtectionFn<T extends ArcTokenAny = ArcTokenAny> = (params: T extends ArcToken<any, infer P, any, any> ? import("../../utils/types/get-type").$type<import("../../elements/object").ArcObject<P>> : Record<string, any>) => ViewProtectionConfig;
|
|
17
17
|
/**
|
|
18
18
|
* View protection configuration
|
|
19
19
|
*/
|
|
@@ -5,7 +5,7 @@ import type { ArcTokenAny } from "../../token/token";
|
|
|
5
5
|
import type { TokenInstanceAny } from "../../token/token-instance";
|
|
6
6
|
import type { Merge } from "../../utils";
|
|
7
7
|
import type { ArcViewHandlerContext, ArcViewItem, ArcViewQueryContext } from "./view-context";
|
|
8
|
-
import type { ArcViewData, ViewProtectionFn } from "./view-data";
|
|
8
|
+
import type { ArcViewData, ViewProtection, ViewProtectionFn } from "./view-data";
|
|
9
9
|
/**
|
|
10
10
|
* Arc View - Read-optimized projection of events
|
|
11
11
|
*
|
|
@@ -104,6 +104,7 @@ export declare class ArcView<const Data extends ArcViewData> extends ArcContextE
|
|
|
104
104
|
* @returns Context object with find/findOne methods
|
|
105
105
|
*/
|
|
106
106
|
queryContext(adapters: any): ArcViewQueryContext<Data["id"], Data["schema"]>;
|
|
107
|
+
private resolveProtection;
|
|
107
108
|
/**
|
|
108
109
|
* Get event handlers for this view
|
|
109
110
|
* Used by the framework to set up event listeners
|
|
@@ -144,7 +145,7 @@ export declare class ArcView<const Data extends ArcViewData> extends ArcContextE
|
|
|
144
145
|
/**
|
|
145
146
|
* Get all protection configurations
|
|
146
147
|
*/
|
|
147
|
-
get protections():
|
|
148
|
+
get protections(): ViewProtection[];
|
|
148
149
|
/**
|
|
149
150
|
* Get protection config for a specific token instance
|
|
150
151
|
*
|
|
@@ -9,5 +9,6 @@ export * from "./query-result-resolver";
|
|
|
9
9
|
export * from "./schema-extraction";
|
|
10
10
|
export * from "./store-state-fork";
|
|
11
11
|
export * from "./store-state-master";
|
|
12
|
+
export * from "./scoped-data-storage";
|
|
12
13
|
export * from "./store-state.abstract";
|
|
13
14
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { DataStorage, type QueryListenerCallback, type StoreStateChange } from "./data-storage.abstract";
|
|
2
|
+
import type { StoreState } from "./store-state.abstract";
|
|
3
|
+
import type { FindOptions } from "./find-options";
|
|
4
|
+
import type { ForkedDataStorage } from "./data-storage-forked";
|
|
5
|
+
import type { ReadTransaction, ReadWriteTransaction } from "./database-adapter";
|
|
6
|
+
export type ScopeRestrictions = Record<string, unknown>;
|
|
7
|
+
export type StorePermission = "read" | "read-write";
|
|
8
|
+
export declare class ScopedStore<Item extends {
|
|
9
|
+
_id: string;
|
|
10
|
+
}> {
|
|
11
|
+
#private;
|
|
12
|
+
constructor(inner: StoreState<Item>, restrictions: ScopeRestrictions, canWrite: boolean);
|
|
13
|
+
get storeName(): string;
|
|
14
|
+
find(options?: FindOptions<Item>, listener?: QueryListenerCallback<Item>): Promise<Item[]>;
|
|
15
|
+
set(item: Item): Promise<void>;
|
|
16
|
+
remove(id: string): Promise<void>;
|
|
17
|
+
modify(id: string, data: Partial<Item>): Promise<{
|
|
18
|
+
from: Item | null;
|
|
19
|
+
to: Item | null;
|
|
20
|
+
}>;
|
|
21
|
+
applyChanges(changes: StoreStateChange<Item>[]): Promise<void>;
|
|
22
|
+
unsubscribe(listener: QueryListenerCallback<Item>): void;
|
|
23
|
+
}
|
|
24
|
+
export declare class ScopedDataStorage extends DataStorage {
|
|
25
|
+
#private;
|
|
26
|
+
constructor(inner: DataStorage, allowedStores: Map<string, StorePermission>, restrictions: ScopeRestrictions);
|
|
27
|
+
getStore<Item extends {
|
|
28
|
+
_id: string;
|
|
29
|
+
}>(storeName: string): StoreState<Item>;
|
|
30
|
+
fork(): ForkedDataStorage;
|
|
31
|
+
getReadTransaction(): Promise<ReadTransaction>;
|
|
32
|
+
getReadWriteTransaction(): Promise<ReadWriteTransaction>;
|
|
33
|
+
}
|
|
34
|
+
export declare function createScopedDataStorage(inner: DataStorage, queryElementNames: string[], mutateElementNames: string[], restrictions: ScopeRestrictions): ScopedDataStorage;
|
|
35
|
+
//# sourceMappingURL=scoped-data-storage.d.ts.map
|
package/dist/index.js
CHANGED
|
@@ -1270,6 +1270,122 @@ function object(element) {
|
|
|
1270
1270
|
return new ArcObject(element);
|
|
1271
1271
|
}
|
|
1272
1272
|
|
|
1273
|
+
// src/data-storage/data-storage.abstract.ts
|
|
1274
|
+
class DataStorage {
|
|
1275
|
+
async commitChanges(changes) {
|
|
1276
|
+
await Promise.all(changes.map(({ store, changes: changes2 }) => this.getStore(store).applyChanges(changes2)));
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
// src/data-storage/scoped-data-storage.ts
|
|
1281
|
+
class ScopedStore {
|
|
1282
|
+
#inner;
|
|
1283
|
+
#restrictions;
|
|
1284
|
+
#canWrite;
|
|
1285
|
+
#storeName;
|
|
1286
|
+
constructor(inner, restrictions, canWrite) {
|
|
1287
|
+
this.#inner = inner;
|
|
1288
|
+
this.#restrictions = restrictions;
|
|
1289
|
+
this.#canWrite = canWrite;
|
|
1290
|
+
this.#storeName = inner.storeName;
|
|
1291
|
+
}
|
|
1292
|
+
get storeName() {
|
|
1293
|
+
return this.#storeName;
|
|
1294
|
+
}
|
|
1295
|
+
async find(options, listener) {
|
|
1296
|
+
const restricted = this.#applyReadRestrictions(options);
|
|
1297
|
+
return this.#inner.find(restricted, listener);
|
|
1298
|
+
}
|
|
1299
|
+
async set(item) {
|
|
1300
|
+
this.#assertWriteAccess();
|
|
1301
|
+
this.#validateScopeFields(item);
|
|
1302
|
+
return this.#inner.set(item);
|
|
1303
|
+
}
|
|
1304
|
+
async remove(id) {
|
|
1305
|
+
this.#assertWriteAccess();
|
|
1306
|
+
return this.#inner.remove(id);
|
|
1307
|
+
}
|
|
1308
|
+
async modify(id, data) {
|
|
1309
|
+
this.#assertWriteAccess();
|
|
1310
|
+
this.#validateScopeFields(data);
|
|
1311
|
+
return this.#inner.modify(id, data);
|
|
1312
|
+
}
|
|
1313
|
+
async applyChanges(changes) {
|
|
1314
|
+
this.#assertWriteAccess();
|
|
1315
|
+
for (const change of changes) {
|
|
1316
|
+
if (change.type === "set") {
|
|
1317
|
+
this.#validateScopeFields(change.data);
|
|
1318
|
+
} else if (change.type === "modify") {
|
|
1319
|
+
this.#validateScopeFields(change.data);
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
return this.#inner.applyChanges(changes);
|
|
1323
|
+
}
|
|
1324
|
+
unsubscribe(listener) {
|
|
1325
|
+
this.#inner.unsubscribe(listener);
|
|
1326
|
+
}
|
|
1327
|
+
#applyReadRestrictions(options) {
|
|
1328
|
+
if (Object.keys(this.#restrictions).length === 0) {
|
|
1329
|
+
return options ?? {};
|
|
1330
|
+
}
|
|
1331
|
+
return {
|
|
1332
|
+
...options,
|
|
1333
|
+
where: { ...options?.where ?? {}, ...this.#restrictions }
|
|
1334
|
+
};
|
|
1335
|
+
}
|
|
1336
|
+
#assertWriteAccess() {
|
|
1337
|
+
if (!this.#canWrite) {
|
|
1338
|
+
throw new Error(`Scope violation: write access denied to store "${this.#storeName}" (read-only)`);
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
#validateScopeFields(data) {
|
|
1342
|
+
for (const [key, value] of Object.entries(this.#restrictions)) {
|
|
1343
|
+
if (key in data && data[key] !== value) {
|
|
1344
|
+
throw new Error(`Scope violation: field "${key}" must be "${value}", got "${data[key]}" in store "${this.#storeName}"`);
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1350
|
+
class ScopedDataStorage extends DataStorage {
|
|
1351
|
+
#inner;
|
|
1352
|
+
#allowedStores;
|
|
1353
|
+
#restrictions;
|
|
1354
|
+
constructor(inner, allowedStores, restrictions) {
|
|
1355
|
+
super();
|
|
1356
|
+
this.#inner = inner;
|
|
1357
|
+
this.#allowedStores = allowedStores;
|
|
1358
|
+
this.#restrictions = restrictions;
|
|
1359
|
+
}
|
|
1360
|
+
getStore(storeName) {
|
|
1361
|
+
const permission = this.#allowedStores.get(storeName);
|
|
1362
|
+
if (!permission) {
|
|
1363
|
+
throw new Error(`Scope violation: access denied to store "${storeName}" (not declared in query/mutate)`);
|
|
1364
|
+
}
|
|
1365
|
+
const inner = this.#inner.getStore(storeName);
|
|
1366
|
+
return new ScopedStore(inner, this.#restrictions, permission === "read-write");
|
|
1367
|
+
}
|
|
1368
|
+
fork() {
|
|
1369
|
+
return this.#inner.fork();
|
|
1370
|
+
}
|
|
1371
|
+
getReadTransaction() {
|
|
1372
|
+
return this.#inner.getReadTransaction();
|
|
1373
|
+
}
|
|
1374
|
+
getReadWriteTransaction() {
|
|
1375
|
+
return this.#inner.getReadWriteTransaction();
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
function createScopedDataStorage(inner, queryElementNames, mutateElementNames, restrictions) {
|
|
1379
|
+
const allowedStores = new Map;
|
|
1380
|
+
for (const name of queryElementNames) {
|
|
1381
|
+
allowedStores.set(name, "read");
|
|
1382
|
+
}
|
|
1383
|
+
for (const name of mutateElementNames) {
|
|
1384
|
+
allowedStores.set(name, "read-write");
|
|
1385
|
+
}
|
|
1386
|
+
return new ScopedDataStorage(inner, allowedStores, restrictions);
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1273
1389
|
// src/elements/abstract-primitive.ts
|
|
1274
1390
|
class ArcPrimitive extends ArcAbstract {
|
|
1275
1391
|
serialize(value) {
|
|
@@ -1460,6 +1576,56 @@ class ArcContextElement extends ArcFragmentBase {
|
|
|
1460
1576
|
}
|
|
1461
1577
|
}
|
|
1462
1578
|
|
|
1579
|
+
// src/context-element/element-context.ts
|
|
1580
|
+
function buildElementContext(queryElements, mutationElements, adapters) {
|
|
1581
|
+
const queryMap = new Map;
|
|
1582
|
+
const mutateMap = new Map;
|
|
1583
|
+
const queryProps = {};
|
|
1584
|
+
for (const element of queryElements) {
|
|
1585
|
+
if (element.queryContext) {
|
|
1586
|
+
const ctx = element.queryContext(adapters);
|
|
1587
|
+
queryProps[element.name] = ctx;
|
|
1588
|
+
queryMap.set(element, ctx);
|
|
1589
|
+
}
|
|
1590
|
+
}
|
|
1591
|
+
const mutateProps = {};
|
|
1592
|
+
for (const element of mutationElements) {
|
|
1593
|
+
if (element.mutateContext) {
|
|
1594
|
+
const ctx = element.mutateContext(adapters);
|
|
1595
|
+
mutateProps[element.name] = ctx;
|
|
1596
|
+
mutateMap.set(element, ctx);
|
|
1597
|
+
}
|
|
1598
|
+
}
|
|
1599
|
+
const queryFn = (element) => {
|
|
1600
|
+
const cached = queryMap.get(element);
|
|
1601
|
+
if (cached)
|
|
1602
|
+
return cached;
|
|
1603
|
+
if (element.queryContext) {
|
|
1604
|
+
const ctx = element.queryContext(adapters);
|
|
1605
|
+
queryMap.set(element, ctx);
|
|
1606
|
+
return ctx;
|
|
1607
|
+
}
|
|
1608
|
+
throw new Error(`Element "${element.name}" has no queryContext`);
|
|
1609
|
+
};
|
|
1610
|
+
Object.assign(queryFn, queryProps);
|
|
1611
|
+
const mutateFn = (element) => {
|
|
1612
|
+
const cached = mutateMap.get(element);
|
|
1613
|
+
if (cached)
|
|
1614
|
+
return cached;
|
|
1615
|
+
if (element.mutateContext) {
|
|
1616
|
+
const ctx = element.mutateContext(adapters);
|
|
1617
|
+
mutateMap.set(element, ctx);
|
|
1618
|
+
return ctx;
|
|
1619
|
+
}
|
|
1620
|
+
throw new Error(`Element "${element.name}" has no mutateContext`);
|
|
1621
|
+
};
|
|
1622
|
+
Object.assign(mutateFn, mutateProps);
|
|
1623
|
+
return {
|
|
1624
|
+
query: queryFn,
|
|
1625
|
+
mutate: mutateFn
|
|
1626
|
+
};
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1463
1629
|
// src/elements/boolean.ts
|
|
1464
1630
|
class ArcBoolean extends ArcPrimitive {
|
|
1465
1631
|
hasToBeTrue() {
|
|
@@ -1697,56 +1863,6 @@ function event(name, payload) {
|
|
|
1697
1863
|
});
|
|
1698
1864
|
}
|
|
1699
1865
|
|
|
1700
|
-
// src/context-element/element-context.ts
|
|
1701
|
-
function buildElementContext(queryElements, mutationElements, adapters) {
|
|
1702
|
-
const queryMap = new Map;
|
|
1703
|
-
const mutateMap = new Map;
|
|
1704
|
-
const queryProps = {};
|
|
1705
|
-
for (const element of queryElements) {
|
|
1706
|
-
if (element.queryContext) {
|
|
1707
|
-
const ctx = element.queryContext(adapters);
|
|
1708
|
-
queryProps[element.name] = ctx;
|
|
1709
|
-
queryMap.set(element, ctx);
|
|
1710
|
-
}
|
|
1711
|
-
}
|
|
1712
|
-
const mutateProps = {};
|
|
1713
|
-
for (const element of mutationElements) {
|
|
1714
|
-
if (element.mutateContext) {
|
|
1715
|
-
const ctx = element.mutateContext(adapters);
|
|
1716
|
-
mutateProps[element.name] = ctx;
|
|
1717
|
-
mutateMap.set(element, ctx);
|
|
1718
|
-
}
|
|
1719
|
-
}
|
|
1720
|
-
const queryFn = (element) => {
|
|
1721
|
-
const cached = queryMap.get(element);
|
|
1722
|
-
if (cached)
|
|
1723
|
-
return cached;
|
|
1724
|
-
if (element.queryContext) {
|
|
1725
|
-
const ctx = element.queryContext(adapters);
|
|
1726
|
-
queryMap.set(element, ctx);
|
|
1727
|
-
return ctx;
|
|
1728
|
-
}
|
|
1729
|
-
throw new Error(`Element "${element.name}" has no queryContext`);
|
|
1730
|
-
};
|
|
1731
|
-
Object.assign(queryFn, queryProps);
|
|
1732
|
-
const mutateFn = (element) => {
|
|
1733
|
-
const cached = mutateMap.get(element);
|
|
1734
|
-
if (cached)
|
|
1735
|
-
return cached;
|
|
1736
|
-
if (element.mutateContext) {
|
|
1737
|
-
const ctx = element.mutateContext(adapters);
|
|
1738
|
-
mutateMap.set(element, ctx);
|
|
1739
|
-
return ctx;
|
|
1740
|
-
}
|
|
1741
|
-
throw new Error(`Element "${element.name}" has no mutateContext`);
|
|
1742
|
-
};
|
|
1743
|
-
Object.assign(mutateFn, mutateProps);
|
|
1744
|
-
return {
|
|
1745
|
-
query: queryFn,
|
|
1746
|
-
mutate: mutateFn
|
|
1747
|
-
};
|
|
1748
|
-
}
|
|
1749
|
-
|
|
1750
1866
|
// src/context-element/function/arc-function.ts
|
|
1751
1867
|
class ArcFunction {
|
|
1752
1868
|
data;
|
|
@@ -1962,7 +2078,9 @@ class ArcAggregateElement extends ArcContextElement {
|
|
|
1962
2078
|
const entry = {
|
|
1963
2079
|
name: methodName,
|
|
1964
2080
|
params: configuredFn.data.params,
|
|
1965
|
-
handler: configuredFn.data.handler
|
|
2081
|
+
handler: configuredFn.data.handler,
|
|
2082
|
+
queryElements: configuredFn.data.queryElements?.length ? configuredFn.data.queryElements : undefined,
|
|
2083
|
+
mutationElements: configuredFn.data.mutationElements?.length ? configuredFn.data.mutationElements : undefined
|
|
1966
2084
|
};
|
|
1967
2085
|
return new ArcAggregateElement(this.name, this._id_factory, this.schema, this._events, this._protections, [...this._mutateMethods, entry], this._queryMethods, this._seeds);
|
|
1968
2086
|
}
|
|
@@ -2089,38 +2207,16 @@ class ArcAggregateElement extends ArcContextElement {
|
|
|
2089
2207
|
}
|
|
2090
2208
|
buildPrivateQuery(adapters) {
|
|
2091
2209
|
const viewName = this.name;
|
|
2092
|
-
const
|
|
2093
|
-
const
|
|
2094
|
-
if (protections.length === 0)
|
|
2095
|
-
return null;
|
|
2096
|
-
if (!adapters.authAdapter)
|
|
2097
|
-
return false;
|
|
2098
|
-
const decoded = adapters.authAdapter.getDecoded();
|
|
2099
|
-
if (!decoded)
|
|
2100
|
-
return false;
|
|
2101
|
-
for (const protection of protections) {
|
|
2102
|
-
if (protection.token.name === decoded.tokenName) {
|
|
2103
|
-
const restrictions = protection.protectionFn(decoded.params);
|
|
2104
|
-
if (restrictions === false)
|
|
2105
|
-
return false;
|
|
2106
|
-
return restrictions ?? {};
|
|
2107
|
-
}
|
|
2108
|
-
}
|
|
2109
|
-
return false;
|
|
2110
|
-
};
|
|
2111
|
-
const applyRestrictions = (options) => {
|
|
2112
|
-
const restrictions = getReadRestrictions();
|
|
2113
|
-
if (restrictions === false)
|
|
2114
|
-
return false;
|
|
2115
|
-
if (!restrictions || Object.keys(restrictions).length === 0) {
|
|
2116
|
-
return options || {};
|
|
2117
|
-
}
|
|
2118
|
-
const where = { ...options?.where || {}, ...restrictions };
|
|
2119
|
-
return { ...options, where };
|
|
2120
|
-
};
|
|
2210
|
+
const restrictions = this.getScopeRestrictions(adapters);
|
|
2211
|
+
const isDenied = this.isScopeDenied(adapters);
|
|
2121
2212
|
const findRows = async (options) => {
|
|
2122
|
-
if (adapters.dataStorage)
|
|
2213
|
+
if (adapters.dataStorage) {
|
|
2214
|
+
if (restrictions) {
|
|
2215
|
+
const scopedStore = new ScopedStore(adapters.dataStorage.getStore(viewName), restrictions, false);
|
|
2216
|
+
return scopedStore.find(options);
|
|
2217
|
+
}
|
|
2123
2218
|
return adapters.dataStorage.getStore(viewName).find(options);
|
|
2219
|
+
}
|
|
2124
2220
|
if (adapters.streamingCache)
|
|
2125
2221
|
return adapters.streamingCache.getStore(viewName).find(options);
|
|
2126
2222
|
if (adapters.queryWire)
|
|
@@ -2134,18 +2230,15 @@ class ArcAggregateElement extends ArcContextElement {
|
|
|
2134
2230
|
};
|
|
2135
2231
|
return {
|
|
2136
2232
|
find: async (options, mapper) => {
|
|
2137
|
-
|
|
2138
|
-
if (restricted === false)
|
|
2233
|
+
if (isDenied)
|
|
2139
2234
|
return [];
|
|
2140
|
-
const rows = await findRows(
|
|
2235
|
+
const rows = await findRows(options || {});
|
|
2141
2236
|
return mapper ? rows.map((row) => new mapper(row, adapters)) : rows.map(deserializeRow);
|
|
2142
2237
|
},
|
|
2143
2238
|
findOne: async (where, mapper) => {
|
|
2144
|
-
|
|
2145
|
-
if (restrictions === false)
|
|
2239
|
+
if (isDenied)
|
|
2146
2240
|
return;
|
|
2147
|
-
const
|
|
2148
|
-
const rows = await findRows({ where: mergedWhere });
|
|
2241
|
+
const rows = await findRows({ where: where || {} });
|
|
2149
2242
|
const row = rows[0];
|
|
2150
2243
|
if (!row)
|
|
2151
2244
|
return;
|
|
@@ -2153,35 +2246,22 @@ class ArcAggregateElement extends ArcContextElement {
|
|
|
2153
2246
|
}
|
|
2154
2247
|
};
|
|
2155
2248
|
}
|
|
2156
|
-
|
|
2157
|
-
const
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
return
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
if (
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
return adapters.queryWire.query(viewName, options);
|
|
2170
|
-
return [];
|
|
2171
|
-
};
|
|
2172
|
-
return {
|
|
2173
|
-
find: async (options, mapper) => {
|
|
2174
|
-
const rows = await findRows(options || {});
|
|
2175
|
-
return mapper ? rows.map((row) => new mapper(row, adapters)) : rows.map(deserializeRow);
|
|
2176
|
-
},
|
|
2177
|
-
findOne: async (where, mapper) => {
|
|
2178
|
-
const rows = await findRows({ where });
|
|
2179
|
-
const row = rows[0];
|
|
2180
|
-
if (!row)
|
|
2181
|
-
return;
|
|
2182
|
-
return mapper ? new mapper(row, adapters) : deserializeRow(row);
|
|
2249
|
+
isScopeDenied(adapters) {
|
|
2250
|
+
const protections = this._protections;
|
|
2251
|
+
if (protections.length === 0)
|
|
2252
|
+
return false;
|
|
2253
|
+
if (!adapters.authAdapter)
|
|
2254
|
+
return true;
|
|
2255
|
+
const decoded = adapters.authAdapter.getDecoded();
|
|
2256
|
+
if (!decoded)
|
|
2257
|
+
return true;
|
|
2258
|
+
for (const protection of protections) {
|
|
2259
|
+
if (protection.token.name === decoded.tokenName) {
|
|
2260
|
+
const result = protection.protectionFn(decoded.params);
|
|
2261
|
+
return result === false;
|
|
2183
2262
|
}
|
|
2184
|
-
}
|
|
2263
|
+
}
|
|
2264
|
+
return true;
|
|
2185
2265
|
}
|
|
2186
2266
|
getAuth(adapters) {
|
|
2187
2267
|
if (adapters.authAdapter) {
|
|
@@ -2190,12 +2270,33 @@ class ArcAggregateElement extends ArcContextElement {
|
|
|
2190
2270
|
}
|
|
2191
2271
|
return { params: {}, tokenName: "" };
|
|
2192
2272
|
}
|
|
2273
|
+
getScopeRestrictions(adapters) {
|
|
2274
|
+
const protections = this._protections;
|
|
2275
|
+
if (protections.length === 0)
|
|
2276
|
+
return null;
|
|
2277
|
+
if (!adapters.authAdapter)
|
|
2278
|
+
return null;
|
|
2279
|
+
const decoded = adapters.authAdapter.getDecoded();
|
|
2280
|
+
if (!decoded)
|
|
2281
|
+
return null;
|
|
2282
|
+
for (const protection of protections) {
|
|
2283
|
+
if (protection.token.name === decoded.tokenName) {
|
|
2284
|
+
const result = protection.protectionFn(decoded.params);
|
|
2285
|
+
if (result === false)
|
|
2286
|
+
return null;
|
|
2287
|
+
return result ?? null;
|
|
2288
|
+
}
|
|
2289
|
+
}
|
|
2290
|
+
console.log(`[Scope:${this.name}] no matching protection`);
|
|
2291
|
+
return null;
|
|
2292
|
+
}
|
|
2193
2293
|
mutateContext(adapters) {
|
|
2194
2294
|
const events = this._events;
|
|
2195
|
-
const privateQuery = this.
|
|
2295
|
+
const privateQuery = this.buildPrivateQuery(adapters);
|
|
2196
2296
|
const auth = this.getAuth(adapters);
|
|
2197
2297
|
const aggregateName = this.name;
|
|
2198
|
-
const
|
|
2298
|
+
const scopeRestrictions = this.getScopeRestrictions(adapters);
|
|
2299
|
+
const buildMutateMethodCtx = (method) => {
|
|
2199
2300
|
const ctx = {};
|
|
2200
2301
|
for (const entry of events) {
|
|
2201
2302
|
const arcEvent = entry.event;
|
|
@@ -2206,6 +2307,13 @@ class ArcAggregateElement extends ArcContextElement {
|
|
|
2206
2307
|
if (!adapters.eventPublisher) {
|
|
2207
2308
|
throw new Error(`Cannot emit event "${arcEvent.name}": no eventPublisher adapter available`);
|
|
2208
2309
|
}
|
|
2310
|
+
if (scopeRestrictions) {
|
|
2311
|
+
for (const [key, value] of Object.entries(scopeRestrictions)) {
|
|
2312
|
+
if (key in payload && payload[key] !== value) {
|
|
2313
|
+
throw new Error(`Scope violation: event "${arcEvent.name}" field "${key}" must be "${value}", got "${payload[key]}"`);
|
|
2314
|
+
}
|
|
2315
|
+
}
|
|
2316
|
+
}
|
|
2209
2317
|
const eventInstance = {
|
|
2210
2318
|
type: `${aggregateName}.${arcEvent.name}`,
|
|
2211
2319
|
payload,
|
|
@@ -2218,6 +2326,11 @@ class ArcAggregateElement extends ArcContextElement {
|
|
|
2218
2326
|
}
|
|
2219
2327
|
ctx.$auth = auth;
|
|
2220
2328
|
ctx.$query = privateQuery;
|
|
2329
|
+
if (method.queryElements?.length || method.mutationElements?.length) {
|
|
2330
|
+
const elementCtx = buildElementContext(method.queryElements ?? [], method.mutationElements ?? [], adapters);
|
|
2331
|
+
ctx.query = elementCtx.query;
|
|
2332
|
+
ctx.mutate = elementCtx.mutate;
|
|
2333
|
+
}
|
|
2221
2334
|
return ctx;
|
|
2222
2335
|
};
|
|
2223
2336
|
const result = {};
|
|
@@ -2233,7 +2346,7 @@ class ArcAggregateElement extends ArcContextElement {
|
|
|
2233
2346
|
} : undefined;
|
|
2234
2347
|
return adapters.commandWire.executeCommand(`${aggregateName}.${method.name}`, params, wireAuth);
|
|
2235
2348
|
}
|
|
2236
|
-
const ctx = buildMutateMethodCtx();
|
|
2349
|
+
const ctx = buildMutateMethodCtx(method);
|
|
2237
2350
|
return method.handler(ctx, params);
|
|
2238
2351
|
};
|
|
2239
2352
|
}
|
|
@@ -2461,9 +2574,10 @@ class ArcListener extends ArcContextElement {
|
|
|
2461
2574
|
async handleEvent(event2, adapters) {
|
|
2462
2575
|
if (!this.data.handler)
|
|
2463
2576
|
return;
|
|
2464
|
-
const
|
|
2465
|
-
|
|
2466
|
-
|
|
2577
|
+
const scopedAdapters = this.buildScopedAdapters(event2, adapters);
|
|
2578
|
+
const context2 = this.#fn.buildContext(scopedAdapters);
|
|
2579
|
+
if (scopedAdapters.authAdapter) {
|
|
2580
|
+
const decoded = scopedAdapters.authAdapter.getDecoded();
|
|
2467
2581
|
if (decoded) {
|
|
2468
2582
|
context2.$auth = {
|
|
2469
2583
|
params: decoded.params,
|
|
@@ -2479,6 +2593,37 @@ class ArcListener extends ArcContextElement {
|
|
|
2479
2593
|
await this.data.handler(context2, event2);
|
|
2480
2594
|
}
|
|
2481
2595
|
}
|
|
2596
|
+
buildScopedAdapters(event2, adapters) {
|
|
2597
|
+
if (adapters.authAdapter?.isAuthenticated()) {
|
|
2598
|
+
return adapters;
|
|
2599
|
+
}
|
|
2600
|
+
const allElements = [
|
|
2601
|
+
...this.data.queryElements,
|
|
2602
|
+
...this.data.mutationElements
|
|
2603
|
+
];
|
|
2604
|
+
let tokenName = null;
|
|
2605
|
+
for (const element of allElements) {
|
|
2606
|
+
const protections = element.__aggregateProtections ?? element.data?.protections;
|
|
2607
|
+
if (protections?.length > 0) {
|
|
2608
|
+
tokenName = protections[0].token.name;
|
|
2609
|
+
break;
|
|
2610
|
+
}
|
|
2611
|
+
}
|
|
2612
|
+
if (!tokenName || !event2.payload) {
|
|
2613
|
+
return adapters;
|
|
2614
|
+
}
|
|
2615
|
+
const scopedAuth = new AuthAdapter;
|
|
2616
|
+
scopedAuth.scopes = new Map([
|
|
2617
|
+
["default", {
|
|
2618
|
+
raw: "",
|
|
2619
|
+
decoded: {
|
|
2620
|
+
tokenName,
|
|
2621
|
+
params: event2.payload
|
|
2622
|
+
}
|
|
2623
|
+
}]
|
|
2624
|
+
]);
|
|
2625
|
+
return { ...adapters, authAdapter: scopedAuth };
|
|
2626
|
+
}
|
|
2482
2627
|
destroy() {
|
|
2483
2628
|
for (const unsubscribe of this.unsubscribers) {
|
|
2484
2629
|
unsubscribe();
|
|
@@ -2739,74 +2884,63 @@ class ArcView extends ArcContextElement {
|
|
|
2739
2884
|
queryContext(adapters) {
|
|
2740
2885
|
const viewName = this.data.name;
|
|
2741
2886
|
const protections = this.data.protections || [];
|
|
2742
|
-
const
|
|
2743
|
-
if (protections.length === 0)
|
|
2744
|
-
return null;
|
|
2745
|
-
if (!adapters.authAdapter) {
|
|
2746
|
-
return false;
|
|
2747
|
-
}
|
|
2748
|
-
const decoded = adapters.authAdapter.getDecoded();
|
|
2749
|
-
if (!decoded) {
|
|
2750
|
-
return false;
|
|
2751
|
-
}
|
|
2752
|
-
for (const protection of protections) {
|
|
2753
|
-
if (protection.token.name === decoded.tokenName) {
|
|
2754
|
-
const restrictions = protection.protectionFn(decoded.params);
|
|
2755
|
-
if (restrictions === false)
|
|
2756
|
-
return false;
|
|
2757
|
-
return restrictions ?? {};
|
|
2758
|
-
}
|
|
2759
|
-
}
|
|
2760
|
-
return false;
|
|
2761
|
-
};
|
|
2762
|
-
const applyRestrictions = (options) => {
|
|
2763
|
-
const restrictions = getReadRestrictions();
|
|
2764
|
-
if (restrictions === false)
|
|
2765
|
-
return false;
|
|
2766
|
-
if (!restrictions || Object.keys(restrictions).length === 0) {
|
|
2767
|
-
return options || {};
|
|
2768
|
-
}
|
|
2769
|
-
const where = { ...options?.where || {}, ...restrictions };
|
|
2770
|
-
return { ...options, where };
|
|
2771
|
-
};
|
|
2887
|
+
const { restrictions, denied } = this.resolveProtection(protections, adapters);
|
|
2772
2888
|
return {
|
|
2773
2889
|
find: async (options) => {
|
|
2774
|
-
|
|
2775
|
-
if (restrictedOptions === false) {
|
|
2890
|
+
if (denied)
|
|
2776
2891
|
return [];
|
|
2777
|
-
}
|
|
2778
2892
|
if (adapters.dataStorage) {
|
|
2779
|
-
|
|
2780
|
-
|
|
2893
|
+
if (restrictions) {
|
|
2894
|
+
const scopedStore = new ScopedStore(adapters.dataStorage.getStore(viewName), restrictions, false);
|
|
2895
|
+
return scopedStore.find(options);
|
|
2896
|
+
}
|
|
2897
|
+
return adapters.dataStorage.getStore(viewName).find(options);
|
|
2781
2898
|
}
|
|
2782
2899
|
if (adapters.queryWire) {
|
|
2783
|
-
|
|
2900
|
+
const where = restrictions ? { ...options?.where || {}, ...restrictions } : options?.where;
|
|
2901
|
+
return adapters.queryWire.query(viewName, { ...options, where });
|
|
2784
2902
|
}
|
|
2785
|
-
console.warn(`View "${viewName}" query: no dataStorage or queryWire available`);
|
|
2786
2903
|
return [];
|
|
2787
2904
|
},
|
|
2788
2905
|
findOne: async (where) => {
|
|
2789
|
-
|
|
2790
|
-
if (restrictions === false) {
|
|
2906
|
+
if (denied)
|
|
2791
2907
|
return;
|
|
2792
|
-
}
|
|
2793
|
-
const mergedWhere = restrictions && Object.keys(restrictions).length > 0 ? { ...where, ...restrictions } : where;
|
|
2794
2908
|
if (adapters.dataStorage) {
|
|
2795
|
-
|
|
2796
|
-
|
|
2909
|
+
if (restrictions) {
|
|
2910
|
+
const scopedStore = new ScopedStore(adapters.dataStorage.getStore(viewName), restrictions, false);
|
|
2911
|
+
const results2 = await scopedStore.find({ where: where || {} });
|
|
2912
|
+
return results2[0];
|
|
2913
|
+
}
|
|
2914
|
+
const results = await adapters.dataStorage.getStore(viewName).find({ where });
|
|
2797
2915
|
return results[0];
|
|
2798
2916
|
}
|
|
2799
2917
|
if (adapters.queryWire) {
|
|
2800
|
-
const
|
|
2801
|
-
|
|
2802
|
-
});
|
|
2918
|
+
const mergedWhere = restrictions ? { ...where, ...restrictions } : where;
|
|
2919
|
+
const results = await adapters.queryWire.query(viewName, { where: mergedWhere });
|
|
2803
2920
|
return results[0];
|
|
2804
2921
|
}
|
|
2805
|
-
console.warn(`View "${viewName}" query: no dataStorage or queryWire available`);
|
|
2806
2922
|
return;
|
|
2807
2923
|
}
|
|
2808
2924
|
};
|
|
2809
2925
|
}
|
|
2926
|
+
resolveProtection(protections, adapters) {
|
|
2927
|
+
if (protections.length === 0)
|
|
2928
|
+
return { restrictions: null, denied: false };
|
|
2929
|
+
if (!adapters.authAdapter)
|
|
2930
|
+
return { restrictions: null, denied: true };
|
|
2931
|
+
const decoded = adapters.authAdapter.getDecoded();
|
|
2932
|
+
if (!decoded)
|
|
2933
|
+
return { restrictions: null, denied: true };
|
|
2934
|
+
for (const protection of protections) {
|
|
2935
|
+
if (protection.token.name === decoded.tokenName) {
|
|
2936
|
+
const result = protection.protectionFn(decoded.params);
|
|
2937
|
+
if (result === false)
|
|
2938
|
+
return { restrictions: null, denied: true };
|
|
2939
|
+
return { restrictions: result ?? null, denied: false };
|
|
2940
|
+
}
|
|
2941
|
+
}
|
|
2942
|
+
return { restrictions: null, denied: true };
|
|
2943
|
+
}
|
|
2810
2944
|
getHandlers() {
|
|
2811
2945
|
return this.data.handler;
|
|
2812
2946
|
}
|
|
@@ -2943,13 +3077,6 @@ function view(name, id2, schema) {
|
|
|
2943
3077
|
protections: []
|
|
2944
3078
|
});
|
|
2945
3079
|
}
|
|
2946
|
-
// src/data-storage/data-storage.abstract.ts
|
|
2947
|
-
class DataStorage {
|
|
2948
|
-
async commitChanges(changes) {
|
|
2949
|
-
await Promise.all(changes.map(({ store, changes: changes2 }) => this.getStore(store).applyChanges(changes2)));
|
|
2950
|
-
}
|
|
2951
|
-
}
|
|
2952
|
-
|
|
2953
3080
|
// src/data-storage/store-state-fork.ts
|
|
2954
3081
|
import { apply } from "mutative";
|
|
2955
3082
|
|
|
@@ -5091,6 +5218,7 @@ export {
|
|
|
5091
5218
|
deepMerge,
|
|
5092
5219
|
date,
|
|
5093
5220
|
customId,
|
|
5221
|
+
createScopedDataStorage,
|
|
5094
5222
|
contextMerge,
|
|
5095
5223
|
context,
|
|
5096
5224
|
command,
|
|
@@ -5113,7 +5241,9 @@ export {
|
|
|
5113
5241
|
StoreState,
|
|
5114
5242
|
SecuredStoreState,
|
|
5115
5243
|
SecuredDataStorage,
|
|
5244
|
+
ScopedStore,
|
|
5116
5245
|
ScopedModel,
|
|
5246
|
+
ScopedDataStorage,
|
|
5117
5247
|
QueryWire,
|
|
5118
5248
|
ObservableDataStorage,
|
|
5119
5249
|
Model,
|
package/package.json
CHANGED