@malloy-publisher/server 0.0.250 → 0.2.0

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 (21) hide show
  1. package/dist/app/api-doc.yaml +230 -35
  2. package/dist/app/assets/{EnvironmentPage-CiuWaYTk.js → EnvironmentPage-DiUgLuCH.js} +1 -1
  3. package/dist/app/assets/{HomePage-ZXsmEAjw.js → HomePage-MhhjhnFj.js} +1 -1
  4. package/dist/app/assets/{LightMode-10iLdX5H.js → LightMode-BEyvNUzT.js} +1 -1
  5. package/dist/app/assets/{MainPage-u-IAaBdE.js → MainPage-D4Cfee9o.js} +1 -1
  6. package/dist/app/assets/{MaterializationsPage-AcoL_Rnx.js → MaterializationsPage-nGgGqwSN.js} +1 -1
  7. package/dist/app/assets/{ModelPage-BjB-p4Qn.js → ModelPage-DLAcEw-F.js} +1 -1
  8. package/dist/app/assets/{PackagePage-le809jUO.js → PackagePage-Df0ikH7w.js} +1 -1
  9. package/dist/app/assets/{RouteError-DTlqmNQd.js → RouteError-BFQwtroH.js} +1 -1
  10. package/dist/app/assets/{ThemeEditorPage-DrmSzweH.js → ThemeEditorPage-OoskoQYN.js} +1 -1
  11. package/dist/app/assets/{WorkbookPage-CIRFZXlP.js → WorkbookPage-CpFgvUDC.js} +1 -1
  12. package/dist/app/assets/{core-Cr-9Wkp3.es-YhIv13au.js → core-lb28vQkR.es-BoVSOc75.js} +1 -1
  13. package/dist/app/assets/{index-CM5d1afN.js → index-6ypuFH6g.js} +435 -436
  14. package/dist/app/assets/{index-DP4bilgp.js → index-B8pnU_V_.js} +4 -4
  15. package/dist/app/assets/{index-CSFSsus4.js → index-BImcHmVP.js} +1 -1
  16. package/dist/app/assets/{index-DzaYbhnD.js → index-DSoCUF-f.js} +438 -439
  17. package/dist/app/assets/{index-DdUOWgW6.js → index-DfqzSItA.js} +1 -1
  18. package/dist/app/index.html +1 -1
  19. package/dist/package_load_worker.mjs +237 -392
  20. package/dist/server.mjs +3992 -3120
  21. package/package.json +15 -14
@@ -2482,9 +2482,11 @@ var init_errors = __esm(() => {
2482
2482
  }
2483
2483
  };
2484
2484
  MaterializationEligibilityError = class MaterializationEligibilityError extends Error {
2485
+ reason;
2485
2486
  constructor(error) {
2486
2487
  super(error.message);
2487
2488
  this.name = "MaterializationEligibilityError";
2489
+ this.reason = error.reason;
2488
2490
  }
2489
2491
  };
2490
2492
  FrozenConfigError = class FrozenConfigError extends Error {
@@ -2584,7 +2586,7 @@ function assertNoAuthorizeNearMisses(found) {
2584
2586
  message: `These annotations are not \`authorize\` gates and nothing enforces ` + `them:
2585
2587
  ${unique.map((t) => ` - \`${t}\``).join(`
2586
2588
  `)}
2587
- ` + `Malloy routes an annotation by its prefix, and only ` + `\`#(authorize)\` (or \`##(authorize)\`, or the block form ` + `\`#|(authorize)\`) reaches the authorize route — a space after the ` + `\`#\`, spaces inside the brackets, or anything trailing the closing ` + `bracket makes it a plain tag Malloy hands to something else. Write ` + `\`#(authorize) "<expression>"\` on the \`source:\` statement you mean ` + `to protect. This is refused rather than interpreted: guessing at the ` + `intent would let publisher start enforcing a filter on a package that ` + `has been serving every row.`
2589
+ ` + `Malloy routes an annotation by its prefix, and only ` + `\`#(authorize)\` (or \`##(authorize)\`, or the block form ` + `\`#|(authorize)\`) reaches the authorize route — a space after the ` + `\`#\`, spaces inside the brackets, or anything trailing the closing ` + `bracket makes it a plain tag Malloy hands to something else. Write ` + `\`#(authorize) <expression>\` on its own line directly above the ` + `\`source:\` statement you mean to protect (unquoted — the quoted ` + `string form is retired). This is refused rather than interpreted: ` + `guessing at the ` + `intent would let publisher start enforcing a filter on a package that ` + `has been serving every row.`
2588
2590
  });
2589
2591
  }
2590
2592
  function describeMisplacedAuthorizeAnnotation(f) {
@@ -2605,20 +2607,6 @@ ${positions}
2605
2607
  ` + `A gate only applies where model load looks for one — a \`source:\`'s ` + `own annotation, or one it inherits from an \`extend\`/query-source ` + `base. File-level \`##(authorize)\` is deprecated and no longer ` + `enforced anywhere, so it always lands here: declare \`#(authorize)\` ` + `on each \`source:\` it was meant to protect instead. Every other ` + `position above should move to the \`source:\` statement it is meant ` + `to protect.`
2606
2608
  });
2607
2609
  }
2608
- function buildAuthorizeProbe(exprs, givenDecls = []) {
2609
- const selects = exprs.map((expr, i) => `__auth_${i} is (${expr})`).join(`
2610
- `);
2611
- const givenBlock = givenDecls.length > 0 ? `given:
2612
- ${givenDecls.map((g) => ` ${g.name} :: ${g.type}`).join(`
2613
- `)}
2614
-
2615
- ` : "";
2616
- return `${givenBlock}run: duckdb.sql("SELECT 1 AS __authorize_probe_row") -> {
2617
- select:
2618
- ${selects}
2619
- limit: 1
2620
- }`;
2621
- }
2622
2610
  function referencedGivenNames(expr) {
2623
2611
  const scanned = expr.replace(STRING_LITERAL_PATTERN, "''");
2624
2612
  const names = [];
@@ -2632,249 +2620,6 @@ function referencedGivenNames(expr) {
2632
2620
  }
2633
2621
  return names;
2634
2622
  }
2635
- function classifyAuthorizeGate(condition, declaredTypes, declaredDefaults) {
2636
- const fieldUsage = condition.refSummary?.fieldUsage;
2637
- if (!Array.isArray(fieldUsage) || fieldUsage.length === 0) {
2638
- return { shape: "given_only" };
2639
- }
2640
- const givenNames = [];
2641
- const literalAtoms = [];
2642
- let rejection;
2643
- const reject = (cause, detail) => {
2644
- rejection ??= { shape: "rejected", cause, detail };
2645
- return false;
2646
- };
2647
- const givenOperand = (node) => {
2648
- const n = asNode(node);
2649
- return n && n.node === "given" && typeof n.refName === "string" ? n.refName : null;
2650
- };
2651
- const isLiteralOperand = (node) => {
2652
- const n = asNode(node);
2653
- return !!n && (n.node === "numberLiteral" || n.node === "stringLiteral" || n.node === "true" || n.node === "false");
2654
- };
2655
- const literalOperandText = (node) => {
2656
- const n = asNode(node);
2657
- if (!n)
2658
- return null;
2659
- if (n.node === "true" || n.node === "false")
2660
- return n.node;
2661
- if (n.node === "numberLiteral" && typeof n.literal === "string") {
2662
- return n.literal;
2663
- }
2664
- if (n.node === "stringLiteral" && typeof n.literal === "string") {
2665
- return `'${n.literal.replace(/\\/g, "\\\\").replace(/'/g, "\\'")}'`;
2666
- }
2667
- return null;
2668
- };
2669
- const declaredTypeOf = (name) => {
2670
- const declared = declaredTypes.get(name);
2671
- if (declared === undefined) {
2672
- reject("unreachable_given", `\`$${name}\` is not on this model's given surface, so the gate ` + `would bind its declaration default rather than the caller's ` + `value. Declare it here, importing it if it lives elsewhere ` + `(\`import { ${name} } from "…"\`)`);
2673
- return null;
2674
- }
2675
- return declared;
2676
- };
2677
- const walk = (node, depth) => {
2678
- if (depth > MAX_GATE_WALK_DEPTH) {
2679
- return reject("unsupported_node", "the gate nests too deeply to read");
2680
- }
2681
- const n = asNode(node);
2682
- if (!n || typeof n.node !== "string") {
2683
- return reject("unsupported_node", "the gate has an unreadable shape");
2684
- }
2685
- const kind = n.node;
2686
- if (BOOLEAN_NODES.has(kind)) {
2687
- const kids = asNode(n.kids);
2688
- if (!kids) {
2689
- return reject("unsupported_node", `\`${kind}\` has no operands`);
2690
- }
2691
- return walk(kids.left, depth + 1) && walk(kids.right, depth + 1);
2692
- }
2693
- if (TRANSPARENT_NODES.has(kind)) {
2694
- return walk(n.e, depth + 1);
2695
- }
2696
- if (kind === "inGiven") {
2697
- if (n.not === true) {
2698
- return reject("unsupported_node", "a negated membership test (`not in`) is not an access rule; " + "write the gate as the set of rows a caller MAY read");
2699
- }
2700
- const given = givenOperand(n.givenRef);
2701
- if (given === null) {
2702
- return reject("unsupported_node", "`in` must test membership of a declared given");
2703
- }
2704
- const declared = declaredTypeOf(given);
2705
- if (declared === null)
2706
- return false;
2707
- if (!isArrayType(declared)) {
2708
- return reject("scalar_given_rejects_in", `\`$${given}\` is declared \`${declared}\` (a scalar), so ` + `\`in $${given}\` is not a membership test. Compare it with ` + `\`=\` instead`);
2709
- }
2710
- givenNames.push(given);
2711
- return walkFieldOperand(n.e);
2712
- }
2713
- if (SCALAR_COMPARISON_NODES.has(kind)) {
2714
- const kids = asNode(n.kids);
2715
- if (!kids) {
2716
- return reject("unsupported_node", `\`${kind}\` has no operands`);
2717
- }
2718
- const left = givenOperand(kids.left);
2719
- const right = givenOperand(kids.right);
2720
- const given = left ?? right;
2721
- if (given === null) {
2722
- return reject("no_given_reference", `\`${kind}\` must compare a field against a given; a comparison ` + `against a constant is a fixed filter and belongs in the ` + `source's own \`where:\``);
2723
- }
2724
- const declared = declaredTypeOf(given);
2725
- if (declared === null)
2726
- return false;
2727
- if (isArrayType(declared)) {
2728
- return reject("array_given_needs_in", `\`$${given}\` is declared \`${declared}\` (an array), so ` + `comparing it with \`${kind}\` compiles and then fails in the ` + `warehouse. Write \`in $${given}\` — it is also the spelling ` + `that matches no rows when the array is empty`);
2729
- }
2730
- const otherSide = left === null ? kids.left : kids.right;
2731
- if (isLiteralOperand(otherSide)) {
2732
- givenNames.push(given);
2733
- const literalText = literalOperandText(otherSide);
2734
- if (literalText !== null) {
2735
- literalAtoms.push(left !== null ? `$${given} ${kind} ${literalText}` : `${literalText} ${kind} $${given}`);
2736
- }
2737
- return true;
2738
- }
2739
- if (declaredDefaults.has(given)) {
2740
- return reject("field_given_has_default", `\`${kind}\` compares row field data against \`$${given}\`, ` + `which is declared with a default (\`${declaredDefaults.get(given)}\`). ` + `A caller who supplies no value for \`$${given}\` gets that ` + `default, and the comparison then applies to every row — ` + `admitting rows it was meant to exclude. Declare \`$${given}\` ` + `with no default so a caller must supply one explicitly`);
2741
- }
2742
- givenNames.push(given);
2743
- return walkFieldOperand(otherSide);
2744
- }
2745
- return reject("unsupported_node", `\`${kind}\` is not permitted in a gate; a row-level gate is a ` + `boolean combination of \`<field> <operator> $GIVEN\` comparisons`);
2746
- };
2747
- const walkFieldOperand = (node) => {
2748
- const n = asNode(node);
2749
- if (!n || n.node !== "field" || !Array.isArray(n.path)) {
2750
- return reject("unsupported_node", `a gate compares a FIELD against a given; \`${asNode(node)?.node ?? "that operand"}\` is not a field reference`);
2751
- }
2752
- return true;
2753
- };
2754
- let ok;
2755
- try {
2756
- ok = walk(condition.e, 0);
2757
- } catch {
2758
- return {
2759
- shape: "rejected",
2760
- cause: "unsupported_node",
2761
- detail: "the gate's compiled shape could not be read"
2762
- };
2763
- }
2764
- if (!ok) {
2765
- return rejection ?? {
2766
- shape: "rejected",
2767
- cause: "unsupported_node",
2768
- detail: "the gate is not an allowed shape"
2769
- };
2770
- }
2771
- if (givenNames.length === 0) {
2772
- return {
2773
- shape: "rejected",
2774
- cause: "no_given_reference",
2775
- detail: "a row-level gate must compare a field against a given; a gate " + "that references none is a fixed filter and belongs in the " + "source's own `where:`"
2776
- };
2777
- }
2778
- return { shape: "row_level", givenNames, literalAtoms };
2779
- }
2780
- function isArrayType(declaredType) {
2781
- const normalized = declaredType.trim().toLowerCase();
2782
- return normalized === "array" || normalized.endsWith("[]");
2783
- }
2784
- function asNode(node) {
2785
- return node !== null && typeof node === "object" ? node : null;
2786
- }
2787
- function inferGivenType(value) {
2788
- if (typeof value === "string")
2789
- return "string";
2790
- if (typeof value === "number" || typeof value === "bigint")
2791
- return "number";
2792
- if (typeof value === "boolean")
2793
- return "boolean";
2794
- if (value instanceof Date)
2795
- return "timestamp";
2796
- if (Array.isArray(value)) {
2797
- if (value.length === 0)
2798
- return null;
2799
- const elementType = inferGivenType(value[0]);
2800
- return elementType ? `${elementType}[]` : null;
2801
- }
2802
- return null;
2803
- }
2804
- function bindProbeGivens(expr, givens, declaredTypes) {
2805
- const decls = [];
2806
- const bound = {};
2807
- for (const name of referencedGivenNames(expr)) {
2808
- if (!(name in givens))
2809
- continue;
2810
- const value = givens[name];
2811
- const type = declaredTypes?.get(name) ?? inferGivenType(value);
2812
- if (!type)
2813
- continue;
2814
- decls.push({ name, type });
2815
- bound[name] = value;
2816
- }
2817
- return { decls, bound };
2818
- }
2819
- function isProbeTrue(cell) {
2820
- return cell === true || cell === 1 || cell === "true";
2821
- }
2822
- async function runProbe(executor, probeText, givens) {
2823
- const result = await executor.loadQuery(probeText).run({ rowLimit: 1, givens });
2824
- const row = result?.data?.value?.[0];
2825
- return !!(row && isProbeTrue(row.__auth_0));
2826
- }
2827
- async function evaluateAuthorize(executor, exprs, givens, declaredTypes, options) {
2828
- const selfContainedFirst = options?.selfContainedFirst ?? false;
2829
- for (const expr of exprs) {
2830
- if (selfContainedFirst) {
2831
- if (await evaluateSelfContainedFirst(executor, expr, givens, declaredTypes)) {
2832
- return true;
2833
- }
2834
- continue;
2835
- }
2836
- try {
2837
- if (await runProbe(executor, buildAuthorizeProbe([expr]), givens)) {
2838
- return true;
2839
- }
2840
- continue;
2841
- } catch {}
2842
- try {
2843
- const { decls, bound } = bindProbeGivens(expr, givens, declaredTypes);
2844
- if (decls.length === 0)
2845
- continue;
2846
- if (await runProbe(executor, buildAuthorizeProbe([expr], decls), bound)) {
2847
- return true;
2848
- }
2849
- } catch {
2850
- continue;
2851
- }
2852
- }
2853
- return false;
2854
- }
2855
- async function evaluateSelfContainedFirst(executor, expr, givens, declaredTypes) {
2856
- const referenced = referencedGivenNames(expr);
2857
- if (referenced.length === 0) {
2858
- try {
2859
- return await runProbe(executor, buildAuthorizeProbe([expr]), {});
2860
- } catch {
2861
- return false;
2862
- }
2863
- }
2864
- const { decls, bound } = bindProbeGivens(expr, givens, declaredTypes);
2865
- if (decls.length !== referenced.length) {
2866
- return false;
2867
- }
2868
- try {
2869
- return await runProbe(executor, buildAuthorizeProbe([expr], decls), bound);
2870
- } catch {
2871
- try {
2872
- return await runProbe(executor, buildAuthorizeProbe([expr]), givens);
2873
- } catch {
2874
- return false;
2875
- }
2876
- }
2877
- }
2878
2623
  function quoteMalloyIdentifier(name) {
2879
2624
  return "`" + name.replace(/\\/g, "\\\\").replace(/`/g, "\\`") + "`";
2880
2625
  }
@@ -2903,42 +2648,8 @@ async function liftRowLevelCondition(compiler, sourceName, exprs) {
2903
2648
  const prepared = await compiler.loadQuery(buildRowLevelProbe(sourceName, filterText)).getPreparedQuery();
2904
2649
  return liftProbeFilterCondition(prepared, `row-level probe for "${sourceName}"`, filterText);
2905
2650
  }
2906
- async function runOneRowProbeOrThrow(compiler, sourceName, exprs) {
2907
- try {
2908
- await compiler.loadQuery(buildAuthorizeProbe(exprs)).getPreparedQuery();
2909
- } catch (err) {
2910
- const detail = err instanceof Error ? err.message : String(err);
2911
- throw new ModelCompilationError({
2912
- message: `Invalid #(authorize) annotation on source "${sourceName}" [${exprs.join(" | ")}]: ${detail}`
2913
- });
2914
- }
2915
- }
2916
- async function assertNoVacuousDefaultAtom(executor, sourceName, literalAtoms) {
2917
- for (const atom of literalAtoms) {
2918
- let vacuous;
2919
- try {
2920
- vacuous = await runProbe(executor, buildAuthorizeProbe([atom]), {});
2921
- } catch (err) {
2922
- const detail = err instanceof Error ? err.message : String(err);
2923
- if (NO_DEFAULT_GIVEN_PATTERN.test(detail))
2924
- continue;
2925
- throw new ModelCompilationError({
2926
- message: `Invalid #(authorize) annotation on source "${sourceName}": ` + `the atom \`${atom}\` could not be evaluated against its ` + `given's declared default (${detail}).`
2927
- });
2928
- }
2929
- if (vacuous) {
2930
- throw new ModelCompilationError({
2931
- message: `Invalid #(authorize) annotation on source "${sourceName}": ` + `the atom \`${atom}\` evaluates to TRUE when a caller supplies ` + `no givens (its given's own declared default). An OR'd atom ` + `that is true by default makes the whole row filter admit ` + `every row for that caller. Give the given a default this ` + `atom evaluates false against, or declare it with no default ` + `so a caller must supply one explicitly.`
2932
- });
2933
- }
2934
- }
2935
- }
2936
2651
  async function validateAuthorizeProbes(compiler, options) {
2937
- const declaredTypes = options.declaredTypes ?? new Map;
2938
- const declaredDefaults = options.declaredDefaults ?? new Map;
2939
- const provenNoteObjects = new Set;
2940
2652
  const ownNotesOf = options.authorizeOwnNotes ?? new Map;
2941
- const pending = [];
2942
2653
  for (const [sourceName, groups] of options.authorizeMap ?? []) {
2943
2654
  for (const exprs of groups) {
2944
2655
  if (exprs.length === 0)
@@ -2947,51 +2658,39 @@ async function validateAuthorizeProbes(compiler, options) {
2947
2658
  try {
2948
2659
  condition = await liftRowLevelCondition(compiler, sourceName, exprs);
2949
2660
  } catch (err) {
2950
- pending.push({ sourceName, exprs, err });
2951
- continue;
2952
- }
2953
- const classification = classifyAuthorizeGate(condition, declaredTypes, declaredDefaults);
2954
- if (classification.shape === "given_only") {
2955
- await runOneRowProbeOrThrow(compiler, sourceName, exprs);
2956
- for (const note of ownNotesOf.get(sourceName) ?? []) {
2957
- provenNoteObjects.add(note);
2958
- }
2959
- continue;
2960
- }
2961
- if (classification.shape === "rejected") {
2962
- options.onRowLevelGateRejected?.(classification.cause);
2661
+ const detail = err instanceof Error ? err.message : String(err);
2963
2662
  const ownNotes = ownNotesOf.get(sourceName) ?? [];
2964
2663
  if (ownNotes.length === 0) {
2965
- options.onRowLevelGateUnexpressible?.(sourceName, classification.detail);
2664
+ options.onRowLevelGateRejected?.("entry_point_unexpressible");
2665
+ options.onRowLevelGateUnexpressible?.(sourceName, detail);
2966
2666
  continue;
2967
2667
  }
2968
2668
  throw new ModelCompilationError({
2969
- message: `Invalid #(authorize) annotation on source "${sourceName}" ` + `[${exprs.join(" | ")}]: ${classification.detail}`
2669
+ message: `Invalid #(authorize) annotation on source "${sourceName}" ` + `[${exprs.join(" | ")}]: ${detail}`
2970
2670
  });
2971
2671
  }
2972
- await assertNoVacuousDefaultAtom(compiler, sourceName, classification.literalAtoms);
2973
- for (const note of ownNotesOf.get(sourceName) ?? []) {
2974
- provenNoteObjects.add(note);
2975
- }
2976
- }
2977
- }
2978
- for (const failure of pending) {
2979
- const ownNotes = ownNotesOf.get(failure.sourceName) ?? [];
2980
- const inherited = ownNotes.length === 0 || ownNotes.every((note) => provenNoteObjects.has(note));
2981
- if (inherited) {
2982
- const detail = failure.err instanceof Error ? failure.err.message : String(failure.err);
2983
- options.onRowLevelGateRejected?.("entry_point_unexpressible");
2984
- options.onRowLevelGateUnexpressible?.(failure.sourceName, detail);
2985
- continue;
2672
+ options.onOwnRowLevelConditionCompiled?.(sourceName, condition);
2986
2673
  }
2987
- await runOneRowProbeOrThrow(compiler, failure.sourceName, failure.exprs);
2988
2674
  }
2989
2675
  }
2676
+ function isLegacyQuotedPayload(payload) {
2677
+ const trimmed = payload.trim();
2678
+ return WHOLE_BODY_QUOTED_STRING.test(trimmed) || WHOLE_BODY_SINGLE_QUOTED_STRING.test(trimmed);
2679
+ }
2680
+ function unquoteLegacyGatePayload(payload) {
2681
+ const trimmed = payload.trim();
2682
+ const inner = trimmed.slice(1, -1);
2683
+ return trimmed[0] === "'" ? inner.replace(SINGLE_QUOTE_ESCAPE, "$1") : inner.replace(DOUBLE_QUOTE_ESCAPE, "$1");
2684
+ }
2990
2685
  function parseAuthorizeAnnotation(annotation) {
2991
2686
  const content = authorizeNoteContent(annotation);
2992
2687
  if (content === undefined)
2993
2688
  return null;
2994
- return unwrapQuotedExpression(content.trim());
2689
+ const trimmed = content.trim();
2690
+ if (trimmed.length === 0) {
2691
+ throw new Error("authorize annotation has an empty expression body");
2692
+ }
2693
+ return trimmed;
2995
2694
  }
2996
2695
  function collectAuthorizeExprs(annotations) {
2997
2696
  const exprs = [];
@@ -3003,43 +2702,57 @@ function collectAuthorizeExprs(annotations) {
3003
2702
  }
3004
2703
  return exprs;
3005
2704
  }
3006
- function unwrapQuotedExpression(body) {
3007
- if (body.length < 2 || body[0] !== '"') {
3008
- throw new Error(`authorize annotation expression must be a double-quoted string, got: ${body || "(empty)"}`);
3009
- }
3010
- let expr = "";
3011
- let i = 1;
3012
- let closed = false;
3013
- for (;i < body.length; i++) {
3014
- const ch = body[i];
3015
- if (ch === "\\" && i + 1 < body.length) {
3016
- const next = body[i + 1];
3017
- if (next === '"' || next === "\\") {
3018
- expr += next;
3019
- i++;
3020
- continue;
3021
- }
3022
- }
3023
- if (ch === '"') {
3024
- closed = true;
3025
- i++;
3026
- break;
2705
+ function findMultipleAuthorizeGates(authorizeOwnNotes) {
2706
+ const found = [];
2707
+ for (const [sourceName, notes] of authorizeOwnNotes) {
2708
+ if (notes.length > 1) {
2709
+ found.push({ sourceName, texts: notes.map((note) => note.text) });
3027
2710
  }
3028
- expr += ch;
3029
- }
3030
- if (!closed) {
3031
- throw new Error(`authorize annotation has mismatched quotes: ${body}`);
3032
2711
  }
3033
- const rest = body.slice(i).trim();
3034
- if (rest.length > 0) {
3035
- throw new Error(`authorize annotation has unexpected content after the expression: ${rest}`);
3036
- }
3037
- if (expr.trim().length === 0) {
3038
- throw new Error("authorize annotation has an empty expression body");
2712
+ return found;
2713
+ }
2714
+ function assertAtMostOneAuthorizeGate(found) {
2715
+ if (found.length === 0)
2716
+ return;
2717
+ const positions = found.map(({ sourceName, texts }) => ` - source "${sourceName}" declares ${texts.length}:
2718
+ ` + texts.map((t) => ` \`${t.trim().split(/[\r\n]/, 1)[0]}\``).join(`
2719
+ `)).join(`
2720
+ `);
2721
+ throw new ModelCompilationError({
2722
+ message: `A source may declare at most one \`#(authorize)\` annotation:
2723
+ ${positions}
2724
+ ` + `Combine multiple conditions into one expression with \`or\` instead of ` + `repeating the annotation, e.g. ` + "`#(authorize) $ROLE = 'admin' or org_id in $GROUPS`."
2725
+ });
2726
+ }
2727
+ function findLegacyStringGates(authorizeOwnNotes) {
2728
+ const found = [];
2729
+ for (const [sourceName, notes] of authorizeOwnNotes) {
2730
+ const legacyNotes = notes.filter((note) => {
2731
+ const content = authorizeNoteContent(note.text);
2732
+ return content !== undefined && isLegacyQuotedPayload(content);
2733
+ });
2734
+ if (legacyNotes.length === 0)
2735
+ continue;
2736
+ const exprs = collectAuthorizeExprs(legacyNotes.map((note) => note.text));
2737
+ if (exprs.length > 0) {
2738
+ found.push({ sourceName, exprs });
2739
+ }
3039
2740
  }
3040
- return expr;
2741
+ return found;
2742
+ }
2743
+ function assertNoLegacyStringGate(found) {
2744
+ if (found.length === 0)
2745
+ return;
2746
+ const rewrites = found.flatMap(({ sourceName, exprs }) => exprs.map((expr) => ` - source "${sourceName}": replace \`#(authorize) ${expr}\`` + ` with
2747
+ #(authorize) ${unquoteLegacyGatePayload(expr)}`)).join(`
2748
+ `);
2749
+ throw new ModelCompilationError({
2750
+ message: `The string form of \`#(authorize)\` (a Malloy-quoted expression on ` + `the \`source:\` line) is no longer accepted. Replace it with the ` + `unquoted expression, carried by an \`#(authorize)\` annotation on ` + `its own line directly above the \`source:\` line:
2751
+ ${rewrites}
2752
+ ` + `Test that the rewrite gates the same rows and check the row count ` + `matches what the string form served.`
2753
+ });
3041
2754
  }
3042
- var AUTHORIZE_ROUTE = "authorize", AUTHORIZE_TAG_LIKE, AUTHORIZE_ANNOTATION_ANYWHERE, MOTLY_AUTHORIZE_PAYLOAD, MALFORMED_AUTHORIZE_ATTEMPT, GIVEN_REF_PATTERN, STRING_LITERAL_PATTERN, everyMemberOf = () => (...members) => members, ROW_LEVEL_GATE_REJECTION_CAUSES, BOOLEAN_NODES, TRANSPARENT_NODES, SCALAR_COMPARISON_NODES, MAX_GATE_WALK_DEPTH = 64, NO_DEFAULT_GIVEN_PATTERN;
2755
+ var AUTHORIZE_ROUTE = "authorize", AUTHORIZE_TAG_LIKE, AUTHORIZE_ANNOTATION_ANYWHERE, MOTLY_AUTHORIZE_PAYLOAD, MALFORMED_AUTHORIZE_ATTEMPT, GIVEN_REF_PATTERN, SINGLE_QUOTE_ESCAPE, DOUBLE_QUOTE_ESCAPE, STRING_LITERAL_PATTERN, everyMemberOf = () => (...members) => members, ROW_LEVEL_GATE_REJECTION_CAUSES, WHOLE_BODY_QUOTED_STRING, WHOLE_BODY_SINGLE_QUOTED_STRING;
3043
2756
  var init_authorize = __esm(() => {
3044
2757
  init_errors();
3045
2758
  AUTHORIZE_TAG_LIKE = String.raw`##?\|?[ \t]*[([{<]?[ \t]*authorize(?=[)\]}>]|[ \t]|$)`;
@@ -3047,12 +2760,12 @@ var init_authorize = __esm(() => {
3047
2760
  MOTLY_AUTHORIZE_PAYLOAD = /^[ \t]*[([{<][ \t]*authorize[ \t]*[)\]}>]/iu;
3048
2761
  MALFORMED_AUTHORIZE_ATTEMPT = /^##?\|?[ \t]*[([{<]?[ \t]*authorize/iu;
3049
2762
  GIVEN_REF_PATTERN = /\$([A-Za-z_][A-Za-z0-9_]*)/g;
3050
- STRING_LITERAL_PATTERN = /'(?:\\.|[^'\\])*'/g;
3051
- ROW_LEVEL_GATE_REJECTION_CAUSES = everyMemberOf()("array_given_needs_in", "scalar_given_rejects_in", "field_given_has_default", "unsupported_node", "no_given_reference", "unreachable_given", "entry_point_unexpressible");
3052
- BOOLEAN_NODES = new Set(["and", "or"]);
3053
- TRANSPARENT_NODES = new Set(["()"]);
3054
- SCALAR_COMPARISON_NODES = new Set(["=", "!=", ">", ">=", "<", "<="]);
3055
- NO_DEFAULT_GIVEN_PATTERN = /has no value and no default/;
2763
+ SINGLE_QUOTE_ESCAPE = /\\(['\\])/g;
2764
+ DOUBLE_QUOTE_ESCAPE = /\\(["\\])/g;
2765
+ STRING_LITERAL_PATTERN = /'(?:\\.|[^'\\])*'|"(?:\\.|[^"\\])*"/g;
2766
+ ROW_LEVEL_GATE_REJECTION_CAUSES = everyMemberOf()("unreachable_given", "entry_point_unexpressible", "source_line_gate_no_given_reference", "source_line_gate_negated_membership", "legacy_string_gate", "given_usage_unresolvable", "unclassifiable_condition");
2767
+ WHOLE_BODY_QUOTED_STRING = /^"(?:\\.|[^"\\])*"$/;
2768
+ WHOLE_BODY_SINGLE_QUOTED_STRING = /^'(?:\\.|[^'\\])*'$/;
3056
2769
  });
3057
2770
 
3058
2771
  // src/authorize_metrics.ts
@@ -3076,7 +2789,7 @@ function recordRowLevelGateDecision(decision) {
3076
2789
  }
3077
2790
  function recordRowLevelGateRejected(cause) {
3078
2791
  rowLevelRejectionCounter ??= publisherMeter().createCounter("publisher_authorize_row_level_rejected_total", {
3079
- description: "Row-level `#(authorize)` gates refused at package load because their compiled condition is not an allowed shape, or an inherited gate that could not be expressed at one derived entry point. Label: cause (" + ROW_LEVEL_GATE_REJECTION_CAUSES.map((c) => `'${c}'`).join("|") + "). All but 'entry_point_unexpressible' fail the whole model load; that one fires at load without failing itsee the doc above. Alert on any nonzero value since the last publish, not on a rate."
2792
+ description: "Row-level `#(authorize)` gates that were refused, warned about, or could not be resolved. Label: cause (" + ROW_LEVEL_GATE_REJECTION_CAUSES.map((c) => `'${c}'`).join("|") + "). Only 'legacy_string_gate' fails the whole model load. 'entry_point_unexpressible', 'source_line_gate_no_given_reference' and 'source_line_gate_negated_membership' warn at load and leave the model servable. 'unreachable_given', 'given_usage_unresolvable' and 'unclassifiable_condition' are request-time only that entry point denies every request. Alert on any nonzero value since the last publish; the request-time causes also make this a rate signal, so see the doc above before writing an alert."
3080
2793
  });
3081
2794
  rowLevelRejectionCounter.add(1, { cause });
3082
2795
  }
@@ -13639,6 +13352,97 @@ var init_data_styles = __esm(() => {
13639
13352
  init_logger();
13640
13353
  });
13641
13354
 
13355
+ // src/service/gate_dimension.ts
13356
+ import {
13357
+ isJoined,
13358
+ isSourceDef
13359
+ } from "@malloydata/malloy";
13360
+ function resolveFieldUsagePath(struct, path) {
13361
+ let current = struct;
13362
+ for (let i = 0;i < path.length - 1; i++) {
13363
+ const seg = path[i];
13364
+ const joinField = (current.fields ?? []).find((f) => (f.as || f.name) === seg && isJoined(f) && (isSourceDef(f) || f.type === "record" || f.type === "array"));
13365
+ if (!joinField)
13366
+ return;
13367
+ current = joinField;
13368
+ }
13369
+ const leaf = path[path.length - 1];
13370
+ const field = (current.fields ?? []).find((f) => (f.as || f.name) === leaf);
13371
+ return field ? { struct: current, field } : undefined;
13372
+ }
13373
+ function expandRefSummaryGivenIds(struct, refSummary, seen = new Set) {
13374
+ const ids = new Set;
13375
+ for (const g of refSummary?.givenUsage ?? [])
13376
+ ids.add(g.id);
13377
+ for (const usage of refSummary?.fieldUsage ?? []) {
13378
+ if (usage.path.length === 0)
13379
+ continue;
13380
+ const resolved = resolveFieldUsagePath(struct, usage.path);
13381
+ if (!resolved) {
13382
+ return { ok: false, unresolvedPath: usage.path.join(".") };
13383
+ }
13384
+ if (seen.has(resolved.field))
13385
+ continue;
13386
+ seen.add(resolved.field);
13387
+ const nestedRefSummary = resolved.field.refSummary;
13388
+ const nested = expandRefSummaryGivenIds(resolved.struct, nestedRefSummary, seen);
13389
+ if (!nested.ok)
13390
+ return nested;
13391
+ for (const id of nested.givenIds)
13392
+ ids.add(id);
13393
+ }
13394
+ return { ok: true, givenIds: ids };
13395
+ }
13396
+ function containsNegatedMembership(node, depth = 0) {
13397
+ if (depth > 64 || node === null || typeof node !== "object")
13398
+ return false;
13399
+ const n = node;
13400
+ if (n.node === "inGiven" && n.not === true)
13401
+ return true;
13402
+ if (n.node === "not") {
13403
+ let inner = n.e;
13404
+ while (inner && inner.node === "()") {
13405
+ inner = inner.e;
13406
+ }
13407
+ if (inner && inner.node === "inGiven")
13408
+ return true;
13409
+ return containsNegatedMembership(n.e, depth + 1);
13410
+ }
13411
+ if (n.node === "and" || n.node === "or") {
13412
+ const kids = n.kids;
13413
+ return containsNegatedMembership(kids?.left, depth + 1) || containsNegatedMembership(kids?.right, depth + 1);
13414
+ }
13415
+ if (n.node === "()")
13416
+ return containsNegatedMembership(n.e, depth + 1);
13417
+ return false;
13418
+ }
13419
+ function validateSourceLineGateGivenUsage(sourceName, struct, refSummary, conditionExpr, modelDef, onWarning) {
13420
+ const expansion = expandRefSummaryGivenIds(struct, refSummary);
13421
+ if (!expansion.ok) {
13422
+ throw new ModelCompilationError({
13423
+ message: `#(authorize) on source "${sourceName}" references ` + `"${expansion.unresolvedPath}", which could not be resolved to a ` + `field this model can reach. An unresolvable reference is refused, ` + `not treated as referencing no given`
13424
+ });
13425
+ }
13426
+ const givenIds = expansion.givenIds;
13427
+ for (const id of givenIds) {
13428
+ const given = modelDef.givens?.[id];
13429
+ if (given && (given.default !== undefined || given.defaultText !== undefined)) {
13430
+ throw new ModelCompilationError({
13431
+ message: `#(authorize) on source "${sourceName}" references ` + `\`$${given.name}\`, which is declared with a default. A ` + `caller who supplies no value for \`$${given.name}\` gets ` + `that default, which can admit rows the gate was meant to ` + `exclude. Declare \`$${given.name}\` with no default`
13432
+ });
13433
+ }
13434
+ }
13435
+ if (givenIds.size === 0) {
13436
+ onWarning?.("source_line_gate_no_given_reference", `#(authorize) on source "${sourceName}" references no given; it is ` + `a fixed predicate, not an access rule keyed on the caller`);
13437
+ }
13438
+ if (containsNegatedMembership(conditionExpr)) {
13439
+ onWarning?.("source_line_gate_negated_membership", `#(authorize) on source "${sourceName}" negates a membership test; ` + `an empty given then matches every row instead of none`);
13440
+ }
13441
+ }
13442
+ var init_gate_dimension = __esm(() => {
13443
+ init_errors();
13444
+ });
13445
+
13642
13446
  // src/service/annotations.ts
13643
13447
  import { Annotations } from "@malloydata/malloy";
13644
13448
  function isReservedRoute(route) {
@@ -13905,7 +13709,7 @@ var init_filter = __esm(() => {
13905
13709
  });
13906
13710
 
13907
13711
  // src/service/gate_registry_walk.ts
13908
- import { isSourceDef } from "@malloydata/malloy";
13712
+ import { isSourceDef as isSourceDef2 } from "@malloydata/malloy";
13909
13713
  function resolveDeclaredSource(struct, modelDef) {
13910
13714
  if (!modelDef)
13911
13715
  return { kind: "none" };
@@ -13917,7 +13721,7 @@ function resolveDeclaredSource(struct, modelDef) {
13917
13721
  const declared = entry.type === "source_registry_reference" ? modelDef.contents[entry.name] : entry;
13918
13722
  if (declared === struct)
13919
13723
  continue;
13920
- if (!declared || !isSourceDef(declared)) {
13724
+ if (!declared || !isSourceDef2(declared)) {
13921
13725
  sawBrokenEntry = true;
13922
13726
  continue;
13923
13727
  }
@@ -13952,7 +13756,7 @@ function resolveQuerySourceBase(struct, modelDef) {
13952
13756
  return;
13953
13757
  const ref = duck.query?.structRef;
13954
13758
  const base = typeof ref === "string" ? modelDef?.contents[ref] : ref;
13955
- return base && isSourceDef(base) ? base : undefined;
13759
+ return base && isSourceDef2(base) ? base : undefined;
13956
13760
  }
13957
13761
  function resolveCompositeResolvedBase(struct) {
13958
13762
  const duck = struct;
@@ -14011,8 +13815,8 @@ var init_gate_registry_walk = __esm(() => {
14011
13815
 
14012
13816
  // src/service/source_extraction.ts
14013
13817
  import {
14014
- isJoined,
14015
- isSourceDef as isSourceDef2
13818
+ isJoined as isJoined2,
13819
+ isSourceDef as isSourceDef3
14016
13820
  } from "@malloydata/malloy";
14017
13821
  function joinFieldNamesUnresolvableDeclaration(field, modelDef) {
14018
13822
  const ids = [field.referenceID, field.sourceID].filter((id) => !!id);
@@ -14023,26 +13827,40 @@ function joinFieldNamesUnresolvableDeclaration(field, modelDef) {
14023
13827
  if (!entry)
14024
13828
  continue;
14025
13829
  const declared = entry.type === "source_registry_reference" ? modelDef.contents[entry.name] : entry;
14026
- if (declared && isSourceDef2(declared))
13830
+ if (declared && isSourceDef3(declared))
14027
13831
  return false;
14028
13832
  }
14029
13833
  return true;
14030
13834
  }
13835
+ function isEarlierPosition(a, b) {
13836
+ return a.line < b.line || a.line === b.line && a.character < b.character;
13837
+ }
13838
+ function considerAuthorizeNoteOwner(declaredBy, note, candidate) {
13839
+ const location = candidate.location;
13840
+ if (!location || location.url !== note.at.url)
13841
+ return;
13842
+ const currentLocation = declaredBy.get(note)?.location;
13843
+ if (!currentLocation || isEarlierPosition(location.range.start, currentLocation.range.start)) {
13844
+ declaredBy.set(note, candidate);
13845
+ }
13846
+ }
14031
13847
  function extractSourcesFromModelDef(modelDef, givens, onParseError) {
14032
13848
  const filterMap = new Map;
14033
13849
  const authorizeMap = new Map;
14034
13850
  const misplacedAuthorize = [];
14035
13851
  const gatedSourceOwnAuthorizeNotes = new Set;
13852
+ const authorizeNoteDeclaredBy = new Map;
14036
13853
  const nearMissAuthorize = [];
14037
13854
  const sweptStructs = [];
14038
13855
  for (const obj of Object.values(modelDef.contents)) {
14039
- if (!isSourceDef2(obj))
13856
+ if (!isSourceDef3(obj))
14040
13857
  continue;
14041
13858
  const struct = obj;
14042
13859
  sweptStructs.push(obj);
14043
13860
  for (const note of ownLevelNotes(struct.annotations)) {
14044
13861
  if (containsAuthorizeAnnotationTag([note.text])) {
14045
13862
  gatedSourceOwnAuthorizeNotes.add(note);
13863
+ considerAuthorizeNoteOwner(authorizeNoteDeclaredBy, note, struct);
14046
13864
  }
14047
13865
  }
14048
13866
  nearMissAuthorize.push(...collectAuthorizeNearMisses([
@@ -14054,12 +13872,13 @@ function extractSourcesFromModelDef(modelDef, givens, onParseError) {
14054
13872
  const entry = value.entry;
14055
13873
  if (entry.type === "source_registry_reference")
14056
13874
  continue;
14057
- if (!isSourceDef2(entry))
13875
+ if (!isSourceDef3(entry))
14058
13876
  continue;
14059
13877
  sweptStructs.push(entry);
14060
13878
  for (const note of ownLevelNotes(entry.annotations)) {
14061
13879
  if (containsAuthorizeAnnotationTag([note.text])) {
14062
13880
  gatedSourceOwnAuthorizeNotes.add(note);
13881
+ considerAuthorizeNoteOwner(authorizeNoteDeclaredBy, note, entry);
14063
13882
  }
14064
13883
  }
14065
13884
  nearMissAuthorize.push(...collectAuthorizeNearMisses(ownLevelNotes(entry.annotations).map((note) => note.text)));
@@ -14076,10 +13895,11 @@ function extractSourcesFromModelDef(modelDef, givens, onParseError) {
14076
13895
  }
14077
13896
  assertNoAuthorizeNearMisses(nearMissAuthorize);
14078
13897
  const authorizeOwnNotes = new Map;
13898
+ const attributedAuthorizeOwnNotes = new Map;
14079
13899
  if (containsAuthorizeAnnotationTag((modelAnnotations(modelDef).notes ?? []).map((note) => note.text))) {
14080
13900
  misplacedAuthorize.push({ kind: "file" });
14081
13901
  }
14082
- const sources = Object.values(modelDef.contents).filter((obj) => isSourceDef2(obj)).map((sourceObj) => {
13902
+ const sources = Object.values(modelDef.contents).filter((obj) => isSourceDef3(obj)).map((sourceObj) => {
14083
13903
  const struct = sourceObj;
14084
13904
  const sourceName = struct.as || struct.name;
14085
13905
  const annotations = annotationTexts(struct.annotations);
@@ -14117,7 +13937,9 @@ function extractSourcesFromModelDef(modelDef, givens, onParseError) {
14117
13937
  }
14118
13938
  const ownNotes = ownLevelNoteTexts(struct.annotations);
14119
13939
  const ownGates = collectAuthorizeExprs(ownNotes);
14120
- authorizeOwnNotes.set(sourceName, ownLevelNotes(struct.annotations).filter((note) => containsAuthorizeAnnotationTag([note.text])));
13940
+ const ownAuthorizeNotes = ownLevelNotes(struct.annotations).filter((note) => containsAuthorizeAnnotationTag([note.text]));
13941
+ authorizeOwnNotes.set(sourceName, ownAuthorizeNotes);
13942
+ attributedAuthorizeOwnNotes.set(sourceName, ownAuthorizeNotes.filter((note) => authorizeNoteDeclaredBy.get(note) === struct));
14121
13943
  const inheritedGroups = ownGates.length === 0 ? effectiveAncestorGateExprs(struct, modelDef) : [];
14122
13944
  const effectiveGroups = ownGates.length > 0 ? [ownGates] : inheritedGroups;
14123
13945
  let authorize;
@@ -14137,7 +13959,7 @@ function extractSourcesFromModelDef(modelDef, givens, onParseError) {
14137
13959
  continue;
14138
13960
  }
14139
13961
  const fieldName = field.as || field.name;
14140
- if (isJoined(field) && isSourceDef2(field)) {
13962
+ if (isJoined2(field) && isSourceDef3(field)) {
14141
13963
  const joinedStruct = field;
14142
13964
  if (joinFieldNamesUnresolvableDeclaration(joinedStruct, modelDef)) {
14143
13965
  continue;
@@ -14169,7 +13991,8 @@ function extractSourcesFromModelDef(modelDef, givens, onParseError) {
14169
13991
  filterMap,
14170
13992
  authorizeMap,
14171
13993
  misplacedAuthorize,
14172
- authorizeOwnNotes
13994
+ authorizeOwnNotes,
13995
+ attributedAuthorizeOwnNotes
14173
13996
  };
14174
13997
  }
14175
13998
  function extractQueriesFromModelDef(modelDef) {
@@ -14570,9 +14393,11 @@ init_authorize_metrics();
14570
14393
  init_data_styles();
14571
14394
  init_errors();
14572
14395
  init_authorize();
14396
+ init_gate_dimension();
14573
14397
  var import_recursive_readdir = __toESM(require_recursive_readdir(), 1);
14574
14398
  import {
14575
14399
  contextOverlay,
14400
+ isSourceDef as isSourceDef4,
14576
14401
  MalloyConfig,
14577
14402
  MalloyError as MalloyError2,
14578
14403
  modelDefToModelInfo,
@@ -15012,14 +14837,16 @@ function extractSources(modelDef, givens) {
15012
14837
  filterMap,
15013
14838
  authorizeMap,
15014
14839
  misplacedAuthorize,
15015
- authorizeOwnNotes
14840
+ authorizeOwnNotes,
14841
+ attributedAuthorizeOwnNotes
15016
14842
  } = extractSourcesFromModelDef(modelDef, givens);
15017
14843
  return {
15018
14844
  sources,
15019
14845
  filterMap,
15020
14846
  authorizeMap,
15021
14847
  misplacedAuthorize,
15022
- authorizeOwnNotes
14848
+ authorizeOwnNotes,
14849
+ attributedAuthorizeOwnNotes
15023
14850
  };
15024
14851
  }
15025
14852
  function authorizeWarningCollector() {
@@ -15031,12 +14858,6 @@ function authorizeWarningCollector() {
15031
14858
  }
15032
14859
  };
15033
14860
  }
15034
- function givenDeclaredTypes(givens) {
15035
- return new Map((givens ?? []).filter((g) => g.name != null && g.type != null).map((g) => [g.name, g.type]));
15036
- }
15037
- function givenDeclaredDefaults(givens) {
15038
- return new Map((givens ?? []).filter((g) => g.name != null && g.default != null).map((g) => [g.name, g.default]));
15039
- }
15040
14861
  function extractQueries(modelDef) {
15041
14862
  const { queries, misplacedAuthorize } = extractQueriesFromModelDef(modelDef);
15042
14863
  return {
@@ -15074,7 +14895,8 @@ async function compileMalloyModel(job, malloyConfig, modelPath) {
15074
14895
  filterMap,
15075
14896
  authorizeMap,
15076
14897
  misplacedAuthorize,
15077
- authorizeOwnNotes
14898
+ authorizeOwnNotes,
14899
+ attributedAuthorizeOwnNotes
15078
14900
  } = extractSources(modelDef, givens);
15079
14901
  const queryResult = extractQueries(modelDef);
15080
14902
  const queries = queryResult.queries;
@@ -15082,14 +14904,25 @@ async function compileMalloyModel(job, malloyConfig, modelPath) {
15082
14904
  ...misplacedAuthorize,
15083
14905
  ...queryResult.misplacedAuthorize
15084
14906
  ]);
14907
+ const legacyStringGates = findLegacyStringGates(authorizeOwnNotes);
14908
+ legacyStringGates.forEach(() => recordRowLevelGateRejected("legacy_string_gate"));
14909
+ assertNoLegacyStringGate(legacyStringGates);
14910
+ assertAtMostOneAuthorizeGate(findMultipleAuthorizeGates(authorizeOwnNotes));
15085
14911
  const authorizeWarningCollection = authorizeWarningCollector();
15086
14912
  await validateAuthorizeProbes(mm, {
15087
14913
  authorizeMap,
15088
- declaredTypes: givenDeclaredTypes(givens),
15089
- declaredDefaults: givenDeclaredDefaults(givens),
15090
- authorizeOwnNotes,
14914
+ authorizeOwnNotes: attributedAuthorizeOwnNotes,
15091
14915
  onRowLevelGateRejected: recordRowLevelGateRejected,
15092
- onRowLevelGateUnexpressible: authorizeWarningCollection.onRowLevelGateUnexpressible
14916
+ onRowLevelGateUnexpressible: authorizeWarningCollection.onRowLevelGateUnexpressible,
14917
+ onOwnRowLevelConditionCompiled: (sourceName, condition) => {
14918
+ const struct = modelDef.contents[sourceName];
14919
+ if (!struct || !isSourceDef4(struct))
14920
+ return;
14921
+ validateSourceLineGateGivenUsage(sourceName, struct, condition.refSummary, condition.e, modelDef, (cause, detail) => {
14922
+ recordRowLevelGateRejected(cause);
14923
+ authorizeWarningCollection.warnings.push(`Row-level #(authorize) gate warning on "${sourceName}" (${cause}): ${detail}`);
14924
+ });
14925
+ }
15093
14926
  });
15094
14927
  return {
15095
14928
  modelPath,
@@ -15209,13 +15042,25 @@ async function compileNotebookModel(job, malloyConfig, modelPath) {
15209
15042
  ...extracted.misplacedAuthorize,
15210
15043
  ...finalQueryResult.misplacedAuthorize
15211
15044
  ]);
15045
+ const finalLegacyStringGates = findLegacyStringGates(extracted.authorizeOwnNotes);
15046
+ finalLegacyStringGates.forEach(() => recordRowLevelGateRejected("legacy_string_gate"));
15047
+ assertNoLegacyStringGate(finalLegacyStringGates);
15048
+ assertAtMostOneAuthorizeGate(findMultipleAuthorizeGates(extracted.authorizeOwnNotes));
15049
+ const finalCompiledModelDef = finalModelDef;
15212
15050
  await validateAuthorizeProbes(mm, {
15213
15051
  authorizeMap: extracted.authorizeMap,
15214
- declaredTypes: givenDeclaredTypes(finalGivens),
15215
- declaredDefaults: givenDeclaredDefaults(finalGivens),
15216
- authorizeOwnNotes: extracted.authorizeOwnNotes,
15052
+ authorizeOwnNotes: extracted.attributedAuthorizeOwnNotes,
15217
15053
  onRowLevelGateRejected: recordRowLevelGateRejected,
15218
- onRowLevelGateUnexpressible: authorizeWarningCollection.onRowLevelGateUnexpressible
15054
+ onRowLevelGateUnexpressible: authorizeWarningCollection.onRowLevelGateUnexpressible,
15055
+ onOwnRowLevelConditionCompiled: (sourceName, condition) => {
15056
+ const struct = finalCompiledModelDef.contents[sourceName];
15057
+ if (!struct || !isSourceDef4(struct))
15058
+ return;
15059
+ validateSourceLineGateGivenUsage(sourceName, struct, condition.refSummary, condition.e, finalCompiledModelDef, (cause, detail) => {
15060
+ recordRowLevelGateRejected(cause);
15061
+ authorizeWarningCollection.warnings.push(`Row-level #(authorize) gate warning on "${sourceName}" (${cause}): ${detail}`);
15062
+ });
15063
+ }
15219
15064
  });
15220
15065
  }
15221
15066
  return {