@arcote.tech/arc 0.5.1 → 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.
@@ -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<any>): ArcAggregateElement<Name, Id, Schema, Events, MutateMethods, QueryMethods>;
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 buildUnrestrictedQuery;
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: TokenInstanceAny) => boolean | Record<string, unknown> | Promise<boolean | Record<string, unknown>>;
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<TokenParams> = (params: TokenParams) => ViewProtectionConfig;
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(): import("./view-data").ViewProtection[];
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 protections = this._protections;
2093
- const getReadRestrictions = () => {
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
- const restricted = applyRestrictions(options);
2138
- if (restricted === false)
2233
+ if (isDenied)
2139
2234
  return [];
2140
- const rows = await findRows(restricted);
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
- const restrictions = getReadRestrictions();
2145
- if (restrictions === false)
2239
+ if (isDenied)
2146
2240
  return;
2147
- const mergedWhere = restrictions && Object.keys(restrictions).length > 0 ? { ...where, ...restrictions } : where;
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
- buildUnrestrictedQuery(adapters) {
2157
- const viewName = this.name;
2158
- const schema = this.schema;
2159
- const deserializeRow = (row) => {
2160
- const { _id, ...rest } = row;
2161
- return { _id, ...schema.deserialize(rest) };
2162
- };
2163
- const findRows = async (options) => {
2164
- if (adapters.dataStorage)
2165
- return adapters.dataStorage.getStore(viewName).find(options);
2166
- if (adapters.streamingCache)
2167
- return adapters.streamingCache.getStore(viewName).find(options);
2168
- if (adapters.queryWire)
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.buildUnrestrictedQuery(adapters);
2295
+ const privateQuery = this.buildPrivateQuery(adapters);
2196
2296
  const auth = this.getAuth(adapters);
2197
2297
  const aggregateName = this.name;
2198
- const buildMutateMethodCtx = () => {
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 context2 = this.#fn.buildContext(adapters);
2465
- if (adapters.authAdapter) {
2466
- const decoded = adapters.authAdapter.getDecoded();
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 getReadRestrictions = () => {
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
- const restrictedOptions = applyRestrictions(options);
2775
- if (restrictedOptions === false) {
2890
+ if (denied)
2776
2891
  return [];
2777
- }
2778
2892
  if (adapters.dataStorage) {
2779
- const store = adapters.dataStorage.getStore(viewName);
2780
- return store.find(restrictedOptions);
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
- return adapters.queryWire.query(viewName, restrictedOptions);
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
- const restrictions = getReadRestrictions();
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
- const store = adapters.dataStorage.getStore(viewName);
2796
- const results = await store.find({ where: mergedWhere });
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 results = await adapters.queryWire.query(viewName, {
2801
- where: mergedWhere
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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@arcote.tech/arc",
3
3
  "type": "module",
4
- "version": "0.5.1",
4
+ "version": "0.5.2",
5
5
  "private": false,
6
6
  "author": "Przemysław Krasiński [arcote.tech]",
7
7
  "description": "Arc framework core rewrite with improved event emission and type safety",