rastack 0.0.50 → 0.0.52

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.
Files changed (72) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/auth/context.ts +73 -0
  3. package/auth/core.ts +293 -0
  4. package/auth/index.ts +23 -0
  5. package/components/auto-form/AutoForm.tsx +32 -12
  6. package/components/auto-form/use-auto-form.ts +75 -6
  7. package/components/data-table/DataTable.tsx +72 -45
  8. package/components/data-table/use-data-table.ts +57 -3
  9. package/components/index.ts +2 -0
  10. package/db.ts +9 -0
  11. package/dist/admin.js +13 -13
  12. package/dist/compile/analyze.d.ts +1 -7
  13. package/dist/compile/analyze.js +19 -70
  14. package/dist/compile/entities.js +163 -16
  15. package/dist/compile/model.d.ts +7 -3
  16. package/dist/compile/openapi.js +13 -3
  17. package/dist/compile/program.d.ts +2 -1
  18. package/dist/compile/program.js +7 -2
  19. package/dist/define/auth.d.ts +27 -0
  20. package/dist/define/auth.js +42 -0
  21. package/dist/define/db.d.ts +24 -1
  22. package/dist/define/db.js +1 -1
  23. package/dist/define/index.d.ts +82 -18
  24. package/dist/define/index.js +27 -21
  25. package/dist/define/manifest.d.ts +64 -0
  26. package/dist/define/manifest.js +259 -0
  27. package/dist/define/markers.d.ts +80 -0
  28. package/dist/define/markers.js +37 -0
  29. package/dist/define/types.d.ts +10 -0
  30. package/dist/define/types.js +26 -0
  31. package/dist/plugin/core.d.ts +108 -0
  32. package/dist/plugin/core.js +198 -0
  33. package/dist/plugin/index.d.ts +112 -0
  34. package/dist/plugin/index.js +203 -0
  35. package/dist/wasm/rastack_wasm.js +1 -1
  36. package/dist/wasm/rastack_wasm_bg.wasm +0 -0
  37. package/hooks/data.ts +18 -6
  38. package/hooks/entity.ts +11 -5
  39. package/hooks/form/entity-form.ts +12 -8
  40. package/hooks/manifest.ts +50 -14
  41. package/hooks/registry.ts +25 -8
  42. package/package.json +1 -1
  43. package/plugin.ts +8 -0
  44. package/provider/index.ts +1 -5
  45. package/provider/provider.tsx +61 -16
  46. package/provider/types.ts +26 -7
  47. package/provider/warehouse.ts +4 -3
  48. package/provider/wasm.ts +3 -1
  49. package/runtime.ts +1 -0
  50. package/src/compile/analyze.ts +27 -65
  51. package/src/compile/entities.ts +193 -19
  52. package/src/compile/model.ts +8 -9
  53. package/src/compile/openapi.ts +11 -3
  54. package/src/compile/program.ts +10 -3
  55. package/src/define/auth.ts +28 -0
  56. package/src/define/db.ts +26 -4
  57. package/src/define/index.ts +118 -24
  58. package/src/define/manifest.ts +287 -0
  59. package/src/define/markers.ts +83 -0
  60. package/src/define/types.ts +11 -0
  61. package/src/plugin/core.ts +236 -0
  62. package/src/plugin/index.ts +243 -0
  63. package/test/auth.spec.ts +207 -0
  64. package/test/compile.spec.ts +18 -7
  65. package/test/components.spec.ts +116 -0
  66. package/test/entities.spec.ts +123 -6
  67. package/test/plugin.spec.ts +315 -0
  68. package/test/runtime-manifest.spec.ts +309 -0
  69. package/test/typed-hooks.spec.ts +7 -2
  70. package/types.ts +7 -2
  71. package/wasm/rastack_wasm.js +1 -1
  72. package/wasm/rastack_wasm_bg.wasm +0 -0
@@ -1,6 +1,7 @@
1
1
  import * as ts from "typescript";
2
2
  import { ResourceModel } from "./model";
3
3
  import type { EntityTypeExport } from "./entities";
4
+ export { applyTransitions } from "../define/manifest";
4
5
  /**
5
6
  * Statically analyse resource definitions and produce the canonical model.
6
7
  *
@@ -21,13 +22,6 @@ export declare function analyzeDsl(program: ts.Program, fileNames: string[]): {
21
22
  resources: ResourceModel[];
22
23
  types: EntityTypeExport[];
23
24
  };
24
- /**
25
- * Attach a normalised `transitions` block to a resource and derive what the
26
- * machine implies about its state field: the `states` become the field's
27
- * allowed-value set (`enum` in OpenAPI, the `member` gate in validation), and
28
- * `initial` becomes its default when the field doesn't declare one.
29
- */
30
- export declare function applyTransitions(resource: ResourceModel, raw: unknown): void;
31
25
  /**
32
26
  * Evaluate a *static literal* AST node (string/number/bool/null/array/object)
33
27
  * into a JS value. Anything non-literal (a function, an identifier) yields
@@ -33,14 +33,20 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.applyTransitions = void 0;
36
37
  exports.analyze = analyze;
37
38
  exports.analyzeDsl = analyzeDsl;
38
- exports.applyTransitions = applyTransitions;
39
39
  exports.literalToValue = literalToValue;
40
40
  const path = __importStar(require("path"));
41
41
  const ts = __importStar(require("typescript"));
42
42
  const program_1 = require("./program");
43
- const transitions_1 = require("../validate/transitions");
43
+ const manifest_1 = require("../define/manifest");
44
+ // The option normalisers live beside the runtime manifest builder
45
+ // (`define/manifest.ts`) and are shared with it, so a manifest built from
46
+ // runtime `resource()` values and one compiled from the same source can
47
+ // never disagree. Re-exported here for existing importers.
48
+ var manifest_2 = require("../define/manifest");
49
+ Object.defineProperty(exports, "applyTransitions", { enumerable: true, get: function () { return manifest_2.applyTransitions; } });
44
50
  /**
45
51
  * Statically analyse resource definitions and produce the canonical model.
46
52
  *
@@ -79,7 +85,9 @@ function analyzeDsl(program, fileNames) {
79
85
  });
80
86
  }
81
87
  // Deterministic order: app, then model.
82
- resources.sort((a, b) => a.app === b.app ? a.model.localeCompare(b.model) : a.app.localeCompare(b.app));
88
+ resources.sort((a, b) => a.app === b.app
89
+ ? a.model.localeCompare(b.model)
90
+ : a.app.localeCompare(b.app));
83
91
  types.sort((a, b) => a.key.localeCompare(b.key));
84
92
  return { resources, types };
85
93
  }
@@ -110,7 +118,10 @@ function analyzeResourceCall(call, checker) {
110
118
  const app = stringLiteralArg(call.arguments[0]);
111
119
  const model = stringLiteralArg(call.arguments[1]);
112
120
  const fieldsArg = call.arguments[2];
113
- if (!app || !model || !fieldsArg || !ts.isObjectLiteralExpression(fieldsArg)) {
121
+ if (!app ||
122
+ !model ||
123
+ !fieldsArg ||
124
+ !ts.isObjectLiteralExpression(fieldsArg)) {
114
125
  return undefined;
115
126
  }
116
127
  const fields = [];
@@ -139,35 +150,16 @@ function analyzeResourceCall(call, checker) {
139
150
  model,
140
151
  fields,
141
152
  relations,
142
- sync: normaliseSync(options.sync),
143
- admin: normaliseAdmin(options.admin),
153
+ sync: (0, manifest_1.normaliseSync)(options.sync),
154
+ admin: (0, manifest_1.normaliseAdmin)(options.admin),
144
155
  search: options.search,
145
156
  ordering: options.ordering,
146
157
  permission: options.permission,
147
- access: normaliseAccess(options.access),
158
+ access: (0, manifest_1.normaliseAccess)(options.access),
148
159
  };
149
- applyTransitions(resource, options.transitions);
160
+ (0, manifest_1.applyTransitions)(resource, options.transitions);
150
161
  return resource;
151
162
  }
152
- /**
153
- * Attach a normalised `transitions` block to a resource and derive what the
154
- * machine implies about its state field: the `states` become the field's
155
- * allowed-value set (`enum` in OpenAPI, the `member` gate in validation), and
156
- * `initial` becomes its default when the field doesn't declare one.
157
- */
158
- function applyTransitions(resource, raw) {
159
- const transitions = (0, transitions_1.normalizeTransitions)(raw);
160
- if (!transitions)
161
- return;
162
- resource.transitions = transitions;
163
- const stateField = resource.fields.find((f) => f.name === transitions.field);
164
- if (stateField && stateField.type !== "fk") {
165
- stateField.options = [...transitions.states];
166
- if (stateField.default === undefined && transitions.initial !== undefined) {
167
- stateField.default = transitions.initial;
168
- }
169
- }
170
- }
171
163
  /**
172
164
  * Resolve a single field from its initializer. FK detection is type-driven:
173
165
  * the initializer's *type* is inspected, and a `Ref` type yields a foreign key.
@@ -278,46 +270,3 @@ function literalToValue(node) {
278
270
  }
279
271
  return undefined;
280
272
  }
281
- function normaliseSync(sync) {
282
- if (!sync || typeof sync !== "object")
283
- return undefined;
284
- const mode = sync.mode === "local" ? "local" : "server";
285
- if (mode === "server" && !sync.confirmed) {
286
- // A bare server resource carries no sync semantics.
287
- return undefined;
288
- }
289
- const model = { mode };
290
- if (typeof sync.scope === "string")
291
- model.scope = sync.scope;
292
- if (Array.isArray(sync.confirmed))
293
- model.confirmed = sync.confirmed;
294
- if (sync.load === "lazy")
295
- model.load = "lazy";
296
- if (sync.clientIds === true)
297
- model.clientIds = true;
298
- return model;
299
- }
300
- function normaliseAccess(access) {
301
- if (!access || typeof access !== "object")
302
- return undefined;
303
- const model = {};
304
- if (typeof access.ownerField === "string")
305
- model.ownerField = access.ownerField;
306
- if (access.scope === "owner" || access.scope === "shared") {
307
- model.scope = access.scope;
308
- }
309
- if (Array.isArray(access.adminGroups)) {
310
- model.adminGroups = access.adminGroups.filter((g) => typeof g === "string");
311
- }
312
- return Object.keys(model).length ? model : undefined;
313
- }
314
- function normaliseAdmin(admin) {
315
- if (admin === false)
316
- return false;
317
- if (!admin || typeof admin !== "object")
318
- return undefined;
319
- const model = {};
320
- if (Array.isArray(admin.listDisplay))
321
- model.listDisplay = admin.listDisplay;
322
- return model;
323
- }
@@ -107,8 +107,9 @@ function analyzeEntities(program, fileNames) {
107
107
  for (const decl of candidates) {
108
108
  if (jsdocTags(decl).has("rastackresource"))
109
109
  addRoot(decl);
110
- else if (ts.isClassDeclaration(decl) && isDataClass(decl))
110
+ else if (ts.isClassDeclaration(decl) && isDataClass(decl, checker)) {
111
111
  addRoot(decl);
112
+ }
112
113
  }
113
114
  // 2. Transitive closure over FK references: an entity's entity-typed
114
115
  // property pulls the referenced declaration in as an entity too.
@@ -131,7 +132,9 @@ function analyzeEntities(program, fileNames) {
131
132
  seen.add(decl);
132
133
  included.push(decl);
133
134
  for (const prop of analyzed(decl)) {
134
- if (prop.target && candidates.has(prop.target) && !seen.has(prop.target)) {
135
+ if (prop.target &&
136
+ candidates.has(prop.target) &&
137
+ !seen.has(prop.target)) {
135
138
  queue.push(prop.target);
136
139
  }
137
140
  }
@@ -152,9 +155,14 @@ function analyzeEntities(program, fileNames) {
152
155
  });
153
156
  const fields = [];
154
157
  const relations = [];
158
+ let ownerField;
159
+ let groupField;
155
160
  for (const prop of analyzed(decl)) {
156
161
  if (prop.skip) {
157
- diagnostics.push({ severity: "warning", message: `${key}.${prop.name}: ${prop.skip}` });
162
+ diagnostics.push({
163
+ severity: "warning",
164
+ message: `${key}.${prop.name}: ${prop.skip}`,
165
+ });
158
166
  continue;
159
167
  }
160
168
  if (prop.inverse)
@@ -179,15 +187,57 @@ function analyzeEntities(program, fileNames) {
179
187
  }
180
188
  else if (prop.field) {
181
189
  fields.push(prop.field);
190
+ if (prop.owner) {
191
+ if (ownerField) {
192
+ diagnostics.push({
193
+ severity: "warning",
194
+ message: `${key}: multiple Owner fields — keeping "${ownerField}", ignoring "${prop.name}".`,
195
+ });
196
+ }
197
+ else {
198
+ ownerField = prop.name;
199
+ }
200
+ }
201
+ if (prop.group) {
202
+ if (groupField) {
203
+ diagnostics.push({
204
+ severity: "warning",
205
+ message: `${key}: multiple Group fields — keeping "${groupField}", ignoring "${prop.name}".`,
206
+ });
207
+ }
208
+ else {
209
+ groupField = prop.name;
210
+ }
211
+ }
182
212
  }
183
213
  }
214
+ const policy = entityPolicy(decl, checker, naming);
215
+ let scope = policy.scope;
216
+ if (!scope && groupField)
217
+ scope = "group"; // group datasets partition physically by default
218
+ if (scope === "group" && !groupField) {
219
+ diagnostics.push({
220
+ severity: "warning",
221
+ message: `${key}: GroupScoped without a Group field — add a \`string & Group\` property so rows can be placed in a partition.`,
222
+ });
223
+ }
224
+ const access = {};
225
+ if (ownerField)
226
+ access.ownerField = ownerField;
227
+ if (groupField)
228
+ access.groupField = groupField;
229
+ if (scope)
230
+ access.scope = scope;
231
+ if (policy.adminGroups?.length)
232
+ access.adminGroups = policy.adminGroups;
184
233
  const resource = {
185
234
  app: naming.app,
186
235
  model: naming.model,
187
236
  fields,
188
237
  relations,
189
238
  search: naming.search,
190
- permission: naming.permission,
239
+ permission: policy.permission,
240
+ access: Object.keys(access).length ? access : undefined,
191
241
  };
192
242
  // A class may declare its state machine inline: `static transitions =
193
243
  // {...}`. Statics are never columns, so the block rides alongside the
@@ -195,7 +245,9 @@ function analyzeEntities(program, fileNames) {
195
245
  (0, analyze_1.applyTransitions)(resource, staticTransitions(decl));
196
246
  resources.push(resource);
197
247
  }
198
- resources.sort((a, b) => a.app === b.app ? a.model.localeCompare(b.model) : a.app.localeCompare(b.app));
248
+ resources.sort((a, b) => a.app === b.app
249
+ ? a.model.localeCompare(b.model)
250
+ : a.app.localeCompare(b.app));
199
251
  return { resources, diagnostics, types };
200
252
  }
201
253
  /**
@@ -263,14 +315,23 @@ function entityBehindType(type, scanned) {
263
315
  /**
264
316
  * A *plain data class* — the class-authored equivalent of a `resource()` call,
265
317
  * so it roots itself. It must carry no heritage clause (a subclass of
266
- * `React.Component` or a service base is app code, not a table), not be
267
- * abstract, contain nothing but property declarations, and have at least one
268
- * instance property. Anything richer can still opt in via a hook or the
318
+ * `React.Component` or a service base is app code, not a table) — with one
319
+ * exception: `implements` clauses made **only of auth markers**
320
+ * (`IsAuthenticated`, `AdminGroups<…>`, `OwnerScoped`, …) are policy, not
321
+ * behaviour, and keep the class a plain data class. It must not be abstract,
322
+ * contain nothing but property declarations, and have at least one instance
323
+ * property. Anything richer can still opt in via a hook or the
269
324
  * `@rastackResource` tag.
270
325
  */
271
- function isDataClass(decl) {
272
- if (decl.heritageClauses?.length)
273
- return false;
326
+ function isDataClass(decl, checker) {
327
+ for (const clause of decl.heritageClauses ?? []) {
328
+ if (clause.token !== ts.SyntaxKind.ImplementsKeyword)
329
+ return false;
330
+ for (const typeNode of clause.types) {
331
+ if (!isAccessMarker(checker.getTypeFromTypeNode(typeNode)))
332
+ return false;
333
+ }
334
+ }
274
335
  if (ts.getCombinedModifierFlags(decl) &
275
336
  (ts.ModifierFlags.Abstract | ts.ModifierFlags.Ambient)) {
276
337
  return false;
@@ -337,7 +398,8 @@ function analyzeProperty(name, member, checker, scanned) {
337
398
  let typeNode = unwrapParens(member.type);
338
399
  if (ts.isUnionTypeNode(typeNode)) {
339
400
  const arms = typeNode.types.filter((t) => {
340
- const isNullish = (ts.isLiteralTypeNode(t) && t.literal.kind === ts.SyntaxKind.NullKeyword) ||
401
+ const isNullish = (ts.isLiteralTypeNode(t) &&
402
+ t.literal.kind === ts.SyntaxKind.NullKeyword) ||
341
403
  t.kind === ts.SyntaxKind.UndefinedKeyword;
342
404
  if (isNullish)
343
405
  nullable = true;
@@ -449,7 +511,12 @@ function scalarProp(name, base, config, nullable) {
449
511
  field.null = true;
450
512
  if (config.default !== undefined)
451
513
  field.default = config.default;
452
- return { name, nullable, field };
514
+ const info = { name, nullable, field };
515
+ if (config.owner)
516
+ info.owner = true;
517
+ if (config.group)
518
+ info.group = true;
519
+ return info;
453
520
  }
454
521
  /**
455
522
  * Config carried in the type system: intersecting a property type with
@@ -479,6 +546,10 @@ function configFromType(type, checker, location) {
479
546
  config.unique = true;
480
547
  if (prop("primaryKey"))
481
548
  config.primaryKey = true;
549
+ if (prop("owner"))
550
+ config.owner = true;
551
+ if (prop("group"))
552
+ config.group = true;
482
553
  const maxLength = prop("maxLength");
483
554
  if (maxLength?.isNumberLiteral())
484
555
  config.maxLength = maxLength.value;
@@ -501,6 +572,10 @@ function configFromTags(tags) {
501
572
  config.unique = true;
502
573
  if (tags.has("primarykey"))
503
574
  config.primaryKey = true;
575
+ if (tags.has("owner"))
576
+ config.owner = true;
577
+ if (tags.has("group"))
578
+ config.group = true;
504
579
  const maxLength = Number(tags.get("maxlength"));
505
580
  if (Number.isFinite(maxLength) && maxLength > 0)
506
581
  config.maxLength = maxLength;
@@ -543,7 +618,8 @@ function arrayElement(node) {
543
618
  return node.elementType;
544
619
  if (ts.isTypeReferenceNode(node) &&
545
620
  ts.isIdentifier(node.typeName) &&
546
- (node.typeName.text === "Array" || node.typeName.text === "ReadonlyArray") &&
621
+ (node.typeName.text === "Array" ||
622
+ node.typeName.text === "ReadonlyArray") &&
547
623
  node.typeArguments?.length === 1) {
548
624
  return node.typeArguments[0];
549
625
  }
@@ -556,7 +632,8 @@ function entityNaming(decl) {
556
632
  // Generated entity interfaces are `I`-prefixed (`IFlight`); strip the prefix
557
633
  // so hook usage of generated types resolves to the same resource.
558
634
  const stripped = /^I[A-Z]/.test(raw) ? raw.slice(1) : raw;
559
- const model = tags.get("rastackmodel") || stripped.charAt(0).toLowerCase() + stripped.slice(1);
635
+ const model = tags.get("rastackmodel") ||
636
+ stripped.charAt(0).toLowerCase() + stripped.slice(1);
560
637
  const app = tags.get("rastackapp") ||
561
638
  path
562
639
  .basename(decl.getSourceFile().fileName)
@@ -565,7 +642,10 @@ function entityNaming(decl) {
565
642
  const naming = { app, model };
566
643
  const search = tags.get("rastacksearch");
567
644
  if (search)
568
- naming.search = search.split(",").map((s) => s.trim()).filter(Boolean);
645
+ naming.search = search
646
+ .split(",")
647
+ .map((s) => s.trim())
648
+ .filter(Boolean);
569
649
  const permission = tags.get("rastackpermission");
570
650
  if (permission === "authenticatedOrReadOnly" ||
571
651
  permission === "authenticated" ||
@@ -574,6 +654,73 @@ function entityNaming(decl) {
574
654
  }
575
655
  return naming;
576
656
  }
657
+ // -- access policy (auth as types) ---------------------------------------------
658
+ /** The phantom keys the `rastack/auth` entity markers carry. */
659
+ const MARKER_PROPS = [
660
+ "__rastackPermission",
661
+ "__rastackAdminGroups",
662
+ "__rastackScope",
663
+ ];
664
+ /** Whether a heritage type is an auth marker (policy, not behaviour). */
665
+ function isAccessMarker(type) {
666
+ return MARKER_PROPS.some((p) => type.getProperty(p));
667
+ }
668
+ /**
669
+ * Read the entity's access policy: the auth markers on its heritage clauses
670
+ * (`implements IsAuthenticated, AdminGroups<"staff">` on a class, `extends`
671
+ * on an interface) merged over the JSDoc tag equivalents
672
+ * (`@rastackPermission`, `@rastackAdminGroups`, `@rastackScope`) — the type
673
+ * wins when both are present, like the `DbConfig` brands do for columns.
674
+ */
675
+ function entityPolicy(decl, checker, naming) {
676
+ const policy = {};
677
+ const tags = jsdocTags(decl);
678
+ const adminGroupsTag = tags.get("rastackadmingroups");
679
+ if (adminGroupsTag) {
680
+ const groups = adminGroupsTag
681
+ .split(",")
682
+ .map((g) => g.trim())
683
+ .filter(Boolean);
684
+ if (groups.length)
685
+ policy.adminGroups = groups;
686
+ }
687
+ const scopeTag = tags.get("rastackscope");
688
+ if (scopeTag === "owner" || scopeTag === "group" || scopeTag === "shared") {
689
+ policy.scope = scopeTag;
690
+ }
691
+ policy.permission = naming.permission; // @rastackPermission, parsed with naming
692
+ for (const clause of decl.heritageClauses ?? []) {
693
+ for (const typeNode of clause.types) {
694
+ const type = checker.getTypeFromTypeNode(typeNode);
695
+ const literals = (prop) => {
696
+ const symbol = type.getProperty(prop);
697
+ if (!symbol)
698
+ return [];
699
+ const carrier = checker
700
+ .getTypeOfSymbolAtLocation(symbol, decl)
701
+ .getNonNullableType();
702
+ const arms = carrier.isUnion() ? carrier.types : [carrier];
703
+ return arms
704
+ .filter((t) => t.isStringLiteral())
705
+ .map((t) => t.value);
706
+ };
707
+ const [permission] = literals("__rastackPermission");
708
+ if (permission === "authenticated" ||
709
+ permission === "authenticatedOrReadOnly" ||
710
+ permission === "public") {
711
+ policy.permission = permission;
712
+ }
713
+ const adminGroups = literals("__rastackAdminGroups");
714
+ if (adminGroups.length)
715
+ policy.adminGroups = adminGroups.sort();
716
+ const [scope] = literals("__rastackScope");
717
+ if (scope === "owner" || scope === "group" || scope === "shared") {
718
+ policy.scope = scope;
719
+ }
720
+ }
721
+ }
722
+ return policy;
723
+ }
577
724
  /** All JSDoc tags on a node, keyed by lower-cased tag name → comment text. */
578
725
  function jsdocTags(node) {
579
726
  const tags = new Map();
@@ -46,12 +46,16 @@ export interface AdminModel {
46
46
  /**
47
47
  * Row-level access control, enforced by `rastack-api-core` on every surface.
48
48
  * `ownerField` names the field stamped with the authenticated subject;
49
- * `scope: "owner"` places the table under a per-identity warehouse prefix
50
- * (`tenants/{sub}/…`) so object-storage IAM can fence data per user.
49
+ * `groupField` names the field holding the owning group (rows visible to its
50
+ * members). `scope: "owner"` places the table under a per-identity warehouse
51
+ * prefix (`tenants/{sub}/…`) so object-storage IAM can fence data per user;
52
+ * `scope: "group"` partitions per group (`groups/{name}/…`) so each group's
53
+ * records are physically stored apart.
51
54
  */
52
55
  export interface AccessModel {
53
56
  ownerField?: string;
54
- scope?: "owner" | "shared";
57
+ groupField?: string;
58
+ scope?: "owner" | "group" | "shared";
55
59
  adminGroups?: string[];
56
60
  }
57
61
  /**
@@ -225,19 +225,29 @@ function syncStamp(operation, resource, verb) {
225
225
  return operation;
226
226
  }
227
227
  /**
228
- * Stamp `x-rastack-access` on an operation of an access-controlled resource so
229
- * downstream tooling (hooks, the local cache) knows the owner field and whether
230
- * the table is tenant-partitioned on object storage.
228
+ * Stamp `x-rastack-permission` / `x-rastack-access` on an operation so
229
+ * downstream tooling knows the resource's full access policy: the permission
230
+ * gate, the owner field (row-level security), tenant scope (the local cache
231
+ * partitions per identity), and the RLS-bypassing admin groups — everything
232
+ * `rastack/auth`'s `policyFromOpenApi` needs to mirror the server's rules in
233
+ * the UI.
231
234
  */
232
235
  function accessStamp(operation, resource) {
236
+ if (resource.permission) {
237
+ operation["x-rastack-permission"] = resource.permission;
238
+ }
233
239
  const access = resource.access;
234
240
  if (!access)
235
241
  return;
236
242
  const stamp = {};
237
243
  if (access.ownerField)
238
244
  stamp.ownerField = access.ownerField;
245
+ if (access.groupField)
246
+ stamp.groupField = access.groupField;
239
247
  if (access.scope)
240
248
  stamp.scope = access.scope;
249
+ if (access.adminGroups?.length)
250
+ stamp.adminGroups = access.adminGroups;
241
251
  if (Object.keys(stamp).length)
242
252
  operation["x-rastack-access"] = stamp;
243
253
  }
@@ -2,7 +2,8 @@ import * as ts from "typescript";
2
2
  /**
3
3
  * Build a `ts.Program` (and thus a `TypeChecker`) over a set of resource
4
4
  * definition files. The authoring subpaths (`rastack/define` for the DSL,
5
- * `rastack/types` for the `DbConfig` column brands) are mapped to the
5
+ * `rastack/db` for the `DbConfig` column brands, `rastack/auth` for the auth
6
+ * markers, `rastack/types` as the alias for both) are mapped to the
6
7
  * in-tree/installed sources via `paths` so both the shipped package name and
7
8
  * the local demo/tests resolve without extra linking.
8
9
  */
@@ -40,7 +40,8 @@ const ts = __importStar(require("typescript"));
40
40
  /**
41
41
  * Build a `ts.Program` (and thus a `TypeChecker`) over a set of resource
42
42
  * definition files. The authoring subpaths (`rastack/define` for the DSL,
43
- * `rastack/types` for the `DbConfig` column brands) are mapped to the
43
+ * `rastack/db` for the `DbConfig` column brands, `rastack/auth` for the auth
44
+ * markers, `rastack/types` as the alias for both) are mapped to the
44
45
  * in-tree/installed sources via `paths` so both the shipped package name and
45
46
  * the local demo/tests resolve without extra linking.
46
47
  */
@@ -61,7 +62,11 @@ function createProgram(fileNames) {
61
62
  paths: {
62
63
  "rastack/define": [defineDir],
63
64
  "rastack/define/*": [defineDir + "/*"],
64
- "rastack/types": [path.join(defineDir, "db")],
65
+ // `rastack/db` = column brands, `rastack/auth` = auth markers + the
66
+ // Owner/Group brands; `rastack/types` is the compatibility alias for both.
67
+ "rastack/db": [path.join(defineDir, "db")],
68
+ "rastack/auth": [path.join(defineDir, "auth")],
69
+ "rastack/types": [path.join(defineDir, "types")],
65
70
  },
66
71
  };
67
72
  return ts.createProgram(fileNames, options);
@@ -0,0 +1,27 @@
1
+ /**
2
+ * The `rastack/auth` *authoring* surface — auth as TypeScript types.
3
+ *
4
+ * Everything needed to declare an entity's access policy in the type system:
5
+ * the entity-level markers (`IsAuthenticated`, `AdminGroups<…>`,
6
+ * `OwnerScoped`, …) and the access-control field brands (`Owner`, `Group`).
7
+ * The compiler maps the `rastack/auth` import specifier here (see
8
+ * `compile/program.ts`); the published package additionally exposes the
9
+ * client runtime (`RastackAuth`, `can`, the React context) under the same
10
+ * subpath from `tools/auth/`.
11
+ *
12
+ * ```ts
13
+ * import { AdminGroups, Group, IsAuthenticated, Owner, OwnerScoped } from "rastack/auth";
14
+ *
15
+ * export class Trip implements IsAuthenticated, AdminGroups<"staff">, OwnerScoped {
16
+ * owner!: string & Owner;
17
+ * destination!: string;
18
+ * }
19
+ *
20
+ * export class Report implements IsAuthenticated, AdminGroups<"staff"> {
21
+ * team!: string & Group; // group-owned — physically partitioned per group
22
+ * title!: string;
23
+ * }
24
+ * ```
25
+ */
26
+ export * from "./markers";
27
+ export type { Owner, Group } from "./db";
@@ -0,0 +1,42 @@
1
+ "use strict";
2
+ /**
3
+ * The `rastack/auth` *authoring* surface — auth as TypeScript types.
4
+ *
5
+ * Everything needed to declare an entity's access policy in the type system:
6
+ * the entity-level markers (`IsAuthenticated`, `AdminGroups<…>`,
7
+ * `OwnerScoped`, …) and the access-control field brands (`Owner`, `Group`).
8
+ * The compiler maps the `rastack/auth` import specifier here (see
9
+ * `compile/program.ts`); the published package additionally exposes the
10
+ * client runtime (`RastackAuth`, `can`, the React context) under the same
11
+ * subpath from `tools/auth/`.
12
+ *
13
+ * ```ts
14
+ * import { AdminGroups, Group, IsAuthenticated, Owner, OwnerScoped } from "rastack/auth";
15
+ *
16
+ * export class Trip implements IsAuthenticated, AdminGroups<"staff">, OwnerScoped {
17
+ * owner!: string & Owner;
18
+ * destination!: string;
19
+ * }
20
+ *
21
+ * export class Report implements IsAuthenticated, AdminGroups<"staff"> {
22
+ * team!: string & Group; // group-owned — physically partitioned per group
23
+ * title!: string;
24
+ * }
25
+ * ```
26
+ */
27
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
28
+ if (k2 === undefined) k2 = k;
29
+ var desc = Object.getOwnPropertyDescriptor(m, k);
30
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
31
+ desc = { enumerable: true, get: function() { return m[k]; } };
32
+ }
33
+ Object.defineProperty(o, k2, desc);
34
+ }) : (function(o, m, k, k2) {
35
+ if (k2 === undefined) k2 = k;
36
+ o[k2] = m[k];
37
+ }));
38
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
39
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
40
+ };
41
+ Object.defineProperty(exports, "__esModule", { value: true });
42
+ __exportStar(require("./markers"), exports);
@@ -7,7 +7,7 @@
7
7
  * off the type — the same way it reads foreign keys.
8
8
  *
9
9
  * ```ts
10
- * import { Default, Int, MaxLength, PrimaryKey, Unique, UUID } from "rastack/types";
10
+ * import { Default, Int, MaxLength, PrimaryKey, Unique, UUID } from "rastack/db";
11
11
  *
12
12
  * export interface Passenger {
13
13
  * id: UUID & PrimaryKey;
@@ -59,6 +59,29 @@ export interface Default<V extends string | number | boolean> extends DbConfig<{
59
59
  default: V;
60
60
  }> {
61
61
  }
62
+ /**
63
+ * Marks the column that records the owning identity (the verified token
64
+ * `sub`): `owner: string & Owner`. Compiles to `access.ownerField` — the row
65
+ * is stamped from the token on create, immutable after, and rows are only
66
+ * visible to their owner (plus `AdminGroups` members). Add the `OwnerScoped`
67
+ * entity marker to *also* partition the table physically per identity.
68
+ */
69
+ export interface Owner extends DbConfig<{
70
+ owner: true;
71
+ }> {
72
+ }
73
+ /**
74
+ * Marks the column that records the owning group (a verified `cognito:groups`
75
+ * value): `team: string & Group`. Compiles to `access.groupField` — rows are
76
+ * only visible to members of their group, and the dataset is **physically
77
+ * partitioned per group** by default (`groups/{name}/…` on object storage),
78
+ * so each group's records live in their own files. Add the `SharedStorage`
79
+ * entity marker to keep group rows in one shared table instead.
80
+ */
81
+ export interface Group extends DbConfig<{
82
+ group: true;
83
+ }> {
84
+ }
62
85
  /** An integer column — a bare `number` property maps to `float`. */
63
86
  export type Int = number & DbConfig<{
64
87
  int: true;
package/dist/define/db.js CHANGED
@@ -8,7 +8,7 @@
8
8
  * off the type — the same way it reads foreign keys.
9
9
  *
10
10
  * ```ts
11
- * import { Default, Int, MaxLength, PrimaryKey, Unique, UUID } from "rastack/types";
11
+ * import { Default, Int, MaxLength, PrimaryKey, Unique, UUID } from "rastack/db";
12
12
  *
13
13
  * export interface Passenger {
14
14
  * id: UUID & PrimaryKey;