@reekon-tools/boldr-utils 1.20.3 → 1.21.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/exports.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { evaluateFormula, createFormulaScope, createEnhancedFormulaScope, clearFormulaCache, type EnhancedMapping, type FormulaDefinition, type FormulaEvaluationOptions, } from './formulas/evaluateFormula.js';
1
+ export { evaluateFormula, createFormulaScope, createEnhancedFormulaScope, clearFormulaCache, micrometersToUnitNumber, type EnhancedMapping, type FormulaDefinition, type FormulaEvaluationOptions, } from './formulas/evaluateFormula.js';
2
2
  export { calculateFormula } from './formulas/calculateFormula.js';
3
3
  export { convertMicrometers } from './utils/micrometersToUnit.js';
4
4
  export { parseMeasurement } from './utils/parseMeasurement.js';
@@ -9,6 +9,7 @@ export * from './types/annotation.js';
9
9
  export { getToleranceColor, calculateDeviationPercentage, isWithinTolerance, generateToleranceGradient, createDefaultToleranceThresholds, DEFAULT_TOLERANCE_COLORS, type ToleranceThreshold, type ToleranceConfig, } from './utils/tolerance.js';
10
10
  export { GeneralColors, SecondaryColors, FormColors, TileColors, FormulaColors, YellowColors, RedColors, GreenColors, tailwindColors, } from './theme/colors.js';
11
11
  export { DEFAULT_GROUP_INDEX, isDefaultGroup, findDefaultGroup, isFormGroupCompleted, } from './utils/groups.js';
12
+ export { ROLE_RANK, EMPTY_GRANTS, normalizeRole, roleAtLeast, effectiveRole, canView, canEdit, canManage, canManageOrg, isGuest, groupGrants, resolveAccess, type MaybeRole, type GroupedGrants, type ResolvedAccess, } from './utils/permissions.js';
12
13
  export { ALL_EXPORT_CATEGORIES, ALL_EXPORT_MEASUREMENT_DATA_FIELDS, ALL_EXPORT_OBJECT_TYPES, DEFAULT_EXPORT_CONFIG, EXPORT_EVERYTHING_CONFIG, normalizeExportConfig, type ExportCategory, type ExportConfig, type ExportImageFormat, type ExportMeasurementDataField, type ExportMeasurementFilter, type ExportObjectType, } from './export/config.js';
13
14
  export { classifyFileForExport } from './export/objectKinds.js';
14
15
  export { buildJobCsv, escapeCSVField, hyperlinkCell, type BuildJobCsvInput, type ExportObjectRow, type ExportScope, } from './export/buildJobCsv.js';
package/dist/exports.js CHANGED
@@ -2,7 +2,7 @@
2
2
  // platform-specific `AnnotationCanvas` wrapper lives here. The two index
3
3
  // files (index.ts, index.native.ts) re-export from this and add their own
4
4
  // AnnotationCanvas import.
5
- export { evaluateFormula, createFormulaScope, createEnhancedFormulaScope, clearFormulaCache, } from './formulas/evaluateFormula.js';
5
+ export { evaluateFormula, createFormulaScope, createEnhancedFormulaScope, clearFormulaCache, micrometersToUnitNumber, } from './formulas/evaluateFormula.js';
6
6
  export { calculateFormula } from './formulas/calculateFormula.js';
7
7
  export { convertMicrometers } from './utils/micrometersToUnit.js';
8
8
  export { parseMeasurement } from './utils/parseMeasurement.js';
@@ -14,6 +14,9 @@ export { getToleranceColor, calculateDeviationPercentage, isWithinTolerance, gen
14
14
  // Shared color palette (canonical source for both apps' brand colors).
15
15
  export { GeneralColors, SecondaryColors, FormColors, TileColors, FormulaColors, YellowColors, RedColors, GreenColors, tailwindColors, } from './theme/colors.js';
16
16
  export { DEFAULT_GROUP_INDEX, isDefaultGroup, findDefaultGroup, isFormGroupCompleted, } from './utils/groups.js';
17
+ // Three-level access (organization → project → job) role arithmetic. Keep in
18
+ // step with rock-pro-backend/firestore.rules.
19
+ export { ROLE_RANK, EMPTY_GRANTS, normalizeRole, roleAtLeast, effectiveRole, canView, canEdit, canManage, canManageOrg, isGuest, groupGrants, resolveAccess, } from './utils/permissions.js';
17
20
  // Configurable job CSV export, shared by rock-pro-app and rock-desktop so
18
21
  // both platforms emit identical files.
19
22
  export { ALL_EXPORT_CATEGORIES, ALL_EXPORT_MEASUREMENT_DATA_FIELDS, ALL_EXPORT_OBJECT_TYPES, DEFAULT_EXPORT_CONFIG, EXPORT_EVERYTHING_CONFIG, normalizeExportConfig, } from './export/config.js';
@@ -1,2 +1,2 @@
1
1
  import { ColumnConfig, DecimalTolerance, FractionalTolerance, Formula, Measurement, Units } from '../types/firestore.js';
2
- export declare const calculateFormula: (formula: Formula, formulas: Formula[], columns: Record<string, string>, tableConfig: ColumnConfig[], measurements: Measurement[], unit: Units, fractionalTolerance: FractionalTolerance, decimalTolerance: DecimalTolerance) => string | number | null;
2
+ export declare const calculateFormula: (formula: Formula, formulas: Formula[], columns: Record<string, string>, tableConfig: ColumnConfig[], measurements: Measurement[], unit: Units, _fractionalTolerance?: FractionalTolerance, _decimalTolerance?: DecimalTolerance) => string | number | null;
@@ -1,25 +1,9 @@
1
1
  import { ColumnType, } from '../types/firestore.js';
2
- import { evaluateFormula } from './evaluateFormula.js';
3
- import { convertMicrometers } from '../utils/micrometersToUnit.js';
4
- function parseMixedNumber(str) {
5
- const parts = str.trim().split(' ');
6
- let whole = 0;
7
- let fraction = 0;
8
- if (parts.length === 2) {
9
- whole = parseFloat(parts[0]);
10
- const [num, den] = parts[1].split('/').map(Number);
11
- fraction = num / den;
12
- }
13
- else if (parts[0].includes('/')) {
14
- const [num, den] = parts[0].split('/').map(Number);
15
- fraction = num / den;
16
- }
17
- else {
18
- whole = parseFloat(parts[0]);
19
- }
20
- return whole + fraction;
21
- }
22
- export const calculateFormula = (formula, formulas, columns, tableConfig, measurements, unit, fractionalTolerance, decimalTolerance) => {
2
+ import { evaluateFormula, micrometersToUnitNumber, } from './evaluateFormula.js';
3
+ export const calculateFormula = (formula, formulas, columns, tableConfig, measurements, unit,
4
+ // Unused since measurement values stopped round-tripping through the display
5
+ // string — kept so the 8-arg call sites in the apps don't break.
6
+ _fractionalTolerance, _decimalTolerance) => {
23
7
  // Convert Firestore Map to plain object if necessary
24
8
  let variableToColumnMap = {};
25
9
  if (formula.variableToColumnMap) {
@@ -45,8 +29,11 @@ export const calculateFormula = (formula, formulas, columns, tableConfig, measur
45
29
  const measurement = measurements.find((m) => m.id === measurementId);
46
30
  if (!measurement)
47
31
  continue;
48
- const { value, unit: displayUnit } = convertMicrometers(measurement.value, unit, fractionalTolerance, decimalTolerance);
49
- valueMap[columnId] = parseMixedNumber(value);
32
+ // Straight um -> display-unit number. The old path rendered the display
33
+ // STRING (convertMicrometers) and re-parsed it, which rounded the value
34
+ // to the display tolerance and returned NaN / dropped the inches for the
35
+ // composite feet-inches formats (`5' 3 1/2"`).
36
+ valueMap[columnId] = micrometersToUnitNumber(measurement.value, unit);
50
37
  }
51
38
  else if (column.type === ColumnType.Angle) {
52
39
  const measurement = measurements.find((m) => m.id === measurementId);
@@ -17,6 +17,16 @@ export interface FormulaEvaluationOptions {
17
17
  formulas?: FormulaDefinition[];
18
18
  scope?: Record<string, number>;
19
19
  }
20
+ export declare function normalizeUnitForMathJS(unit: Units | string): string;
21
+ /**
22
+ * Convert a stored micrometer value to the number the formula scope expects
23
+ * for `unit` — the same conversion nested-formula results go through. This is
24
+ * the ONLY sanctioned way to put a measurement into a valueMap: converting via
25
+ * the display STRING (convertMicrometers + a string parser) both rounds the
26
+ * value to the display tolerance and falls apart on composite formats like
27
+ * `5' 3 1/2"`.
28
+ */
29
+ export declare function micrometersToUnitNumber(micrometers: number, unit: Units): number;
20
30
  export declare function evaluateFormula({ expression, mappings, valueMap, unit, formulas, scope, }: FormulaEvaluationOptions): number | string | null;
21
31
  export declare function createFormulaScope({ mappings, valueMap, unit, }: {
22
32
  mappings: Record<string, string>;
@@ -7,7 +7,7 @@ const compile = math.compile.bind(math);
7
7
  const mathUnit = math.unit.bind(math);
8
8
  // Cache for evaluated formulas to improve performance
9
9
  const formulaCache = new Map();
10
- function normalizeUnitForMathJS(unit) {
10
+ export function normalizeUnitForMathJS(unit) {
11
11
  switch (unit) {
12
12
  case Units.Centimeters:
13
13
  case 'cm':
@@ -30,6 +30,17 @@ function normalizeUnitForMathJS(unit) {
30
30
  return 'mm'; // fallback
31
31
  }
32
32
  }
33
+ /**
34
+ * Convert a stored micrometer value to the number the formula scope expects
35
+ * for `unit` — the same conversion nested-formula results go through. This is
36
+ * the ONLY sanctioned way to put a measurement into a valueMap: converting via
37
+ * the display STRING (convertMicrometers + a string parser) both rounds the
38
+ * value to the display tolerance and falls apart on composite formats like
39
+ * `5' 3 1/2"`.
40
+ */
41
+ export function micrometersToUnitNumber(micrometers, unit) {
42
+ return mathUnit(micrometers, 'um').toNumber(normalizeUnitForMathJS(unit));
43
+ }
33
44
  // Normalize mapping to EnhancedMapping format
34
45
  function normalizeMapping(mapping) {
35
46
  if (typeof mapping === 'string') {
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export * from './exports.js';
2
- export { evaluateFormula, createFormulaScope, createEnhancedFormulaScope, clearFormulaCache, type EnhancedMapping, type FormulaDefinition, type FormulaEvaluationOptions, } from './formulas/evaluateFormula.js';
2
+ export { evaluateFormula, createFormulaScope, createEnhancedFormulaScope, clearFormulaCache, micrometersToUnitNumber, type EnhancedMapping, type FormulaDefinition, type FormulaEvaluationOptions, } from './formulas/evaluateFormula.js';
3
3
  export { calculateFormula } from './formulas/calculateFormula.js';
4
4
  export { convertMicrometers } from './utils/micrometersToUnit.js';
5
5
  export { parseMeasurement } from './utils/parseMeasurement.js';
package/dist/index.js CHANGED
@@ -3,7 +3,7 @@
3
3
  // WithSkiaWeb. Metro/RN consumers resolve `index.native.ts` instead via
4
4
  // the `react-native` condition in package.json `exports`.
5
5
  export * from './exports.js';
6
- export { evaluateFormula, createFormulaScope, createEnhancedFormulaScope, clearFormulaCache, } from './formulas/evaluateFormula.js';
6
+ export { evaluateFormula, createFormulaScope, createEnhancedFormulaScope, clearFormulaCache, micrometersToUnitNumber, } from './formulas/evaluateFormula.js';
7
7
  export { calculateFormula } from './formulas/calculateFormula.js';
8
8
  export { convertMicrometers } from './utils/micrometersToUnit.js';
9
9
  export { parseMeasurement } from './utils/parseMeasurement.js';
package/dist/server.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { evaluateFormula, createFormulaScope, createEnhancedFormulaScope, clearFormulaCache, type EnhancedMapping, type FormulaDefinition, type FormulaEvaluationOptions, } from './formulas/evaluateFormula.js';
1
+ export { evaluateFormula, createFormulaScope, createEnhancedFormulaScope, clearFormulaCache, micrometersToUnitNumber, type EnhancedMapping, type FormulaDefinition, type FormulaEvaluationOptions, } from './formulas/evaluateFormula.js';
2
2
  export { calculateFormula } from './formulas/calculateFormula.js';
3
3
  export { convertMicrometers } from './utils/micrometersToUnit.js';
4
4
  export { parseMeasurement } from './utils/parseMeasurement.js';
@@ -7,6 +7,7 @@ export * from './types/layout.js';
7
7
  export * from './types/annotation.js';
8
8
  export { getToleranceColor, getToleranceSecondaryColor, calculateDeviationPercentage, isWithinTolerance, generateToleranceGradient, createDefaultToleranceThresholds, DEFAULT_TOLERANCE_COLORS, DEFAULT_TOLERANCE_SECONDARY_COLORS, type ToleranceThreshold, type ToleranceConfig, } from './utils/tolerance.js';
9
9
  export { DEFAULT_GROUP_INDEX, isDefaultGroup, findDefaultGroup, isFormGroupCompleted, } from './utils/groups.js';
10
+ export { ROLE_RANK, EMPTY_GRANTS, normalizeRole, roleAtLeast, effectiveRole, canView, canEdit, canManage, canManageOrg, isGuest, groupGrants, resolveAccess, type MaybeRole, type GroupedGrants, type ResolvedAccess, } from './utils/permissions.js';
10
11
  export { ALL_EXPORT_CATEGORIES, ALL_EXPORT_MEASUREMENT_DATA_FIELDS, ALL_EXPORT_OBJECT_TYPES, DEFAULT_EXPORT_CONFIG, EXPORT_EVERYTHING_CONFIG, normalizeExportConfig, type ExportCategory, type ExportConfig, type ExportImageFormat, type ExportMeasurementDataField, type ExportMeasurementFilter, type ExportObjectType, } from './export/config.js';
11
12
  export { classifyFileForExport } from './export/objectKinds.js';
12
13
  export { buildJobCsv, escapeCSVField, hyperlinkCell, type BuildJobCsvInput, type ExportObjectRow, type ExportScope, } from './export/buildJobCsv.js';
package/dist/server.js CHANGED
@@ -3,7 +3,7 @@
3
3
  // root barrel, whose module graph statically pulls in `react` and
4
4
  // `@shopify/react-native-skia`. Only modules with no React/Skia anywhere
5
5
  // in their import graph may be re-exported from this file.
6
- export { evaluateFormula, createFormulaScope, createEnhancedFormulaScope, clearFormulaCache, } from './formulas/evaluateFormula.js';
6
+ export { evaluateFormula, createFormulaScope, createEnhancedFormulaScope, clearFormulaCache, micrometersToUnitNumber, } from './formulas/evaluateFormula.js';
7
7
  export { calculateFormula } from './formulas/calculateFormula.js';
8
8
  export { convertMicrometers } from './utils/micrometersToUnit.js';
9
9
  export { parseMeasurement } from './utils/parseMeasurement.js';
@@ -12,6 +12,7 @@ export * from './types/layout.js';
12
12
  export * from './types/annotation.js';
13
13
  export { getToleranceColor, getToleranceSecondaryColor, calculateDeviationPercentage, isWithinTolerance, generateToleranceGradient, createDefaultToleranceThresholds, DEFAULT_TOLERANCE_COLORS, DEFAULT_TOLERANCE_SECONDARY_COLORS, } from './utils/tolerance.js';
14
14
  export { DEFAULT_GROUP_INDEX, isDefaultGroup, findDefaultGroup, isFormGroupCompleted, } from './utils/groups.js';
15
+ export { ROLE_RANK, EMPTY_GRANTS, normalizeRole, roleAtLeast, effectiveRole, canView, canEdit, canManage, canManageOrg, isGuest, groupGrants, resolveAccess, } from './utils/permissions.js';
15
16
  export { ALL_EXPORT_CATEGORIES, ALL_EXPORT_MEASUREMENT_DATA_FIELDS, ALL_EXPORT_OBJECT_TYPES, DEFAULT_EXPORT_CONFIG, EXPORT_EVERYTHING_CONFIG, normalizeExportConfig, } from './export/config.js';
16
17
  export { classifyFileForExport } from './export/objectKinds.js';
17
18
  export { buildJobCsv, escapeCSVField, hyperlinkCell, } from './export/buildJobCsv.js';
@@ -21,7 +21,60 @@ export declare enum OrganizationRole {
21
21
  Admin = "admin",
22
22
  Full = "member",
23
23
  Contributor = "contributor",
24
- Viewer = "viewer"
24
+ Viewer = "viewer",
25
+ /**
26
+ * Synthetic org membership for users invited to a single project or job.
27
+ * Grants read of the org doc and its supporting library (templates, labels,
28
+ * formulas, miter variants, print logos) and nothing under `projects/`.
29
+ * Server-written only; never offered in an org role picker. The same word
30
+ * is used for the project-level marker an `AccessGrant` carries when a user
31
+ * has job access but no project access — "may read this container's doc,
32
+ * nothing inside it".
33
+ */
34
+ Guest = "guest"
35
+ }
36
+ /** Roles an org admin may assign in Team settings. Excludes Guest. */
37
+ export declare const ASSIGNABLE_ORG_ROLES: readonly OrganizationRole[];
38
+ /**
39
+ * Roles grantable on a single project or job. Same wire values as the org
40
+ * roles minus Admin (admin is org management, not data access).
41
+ */
42
+ export type ScopedRole = OrganizationRole.Full | OrganizationRole.Contributor | OrganizationRole.Viewer;
43
+ export declare const SCOPED_ROLES: readonly ScopedRole[];
44
+ /**
45
+ * What the sharing UIs offer. Contributor is deliberately absent (Figma
46
+ * 1028-47443 / 746-37130 show Full and Viewer); add it here to enable it —
47
+ * rules and types already accept it.
48
+ */
49
+ export declare const ACCESS_ROLE_OPTIONS: readonly {
50
+ label: string;
51
+ value: ScopedRole;
52
+ }[];
53
+ /** Role stored on an `AccessGrant`: a scoped role, or the project-level guest marker. */
54
+ export type AccessRole = ScopedRole | OrganizationRole.Guest;
55
+ export type AccessScope = 'project' | 'job';
56
+ /**
57
+ * A per-user grant on a project or a job. Lives at
58
+ * organizations/{orgId}/projects/{projectId}/access/{uid} (scope 'project')
59
+ * organizations/{orgId}/projects/{projectId}/jobs/{jobId}/access/{uid} (scope 'job')
60
+ * Doc id == uid. Written only by Cloud Functions. Discovered by the client
61
+ * with `collectionGroup('access').where('userId', '==', uid)`.
62
+ */
63
+ export interface AccessGrant extends FirestoreDoc {
64
+ userId: string;
65
+ organizationId: string;
66
+ projectId: string;
67
+ jobId?: string;
68
+ scope: AccessScope;
69
+ role: AccessRole;
70
+ email: string;
71
+ firstName: string;
72
+ lastName: string;
73
+ grantedBy: {
74
+ userId: string;
75
+ email: string;
76
+ };
77
+ grantedAt: Date;
25
78
  }
26
79
  export declare enum InvitationStatus {
27
80
  Pending = "pending",
@@ -49,6 +102,16 @@ export interface Invitation extends FirestoreDoc, Timestamps {
49
102
  organizationId: string;
50
103
  role: OrganizationRole;
51
104
  status: InvitationStatus;
105
+ /**
106
+ * Present on project/job invites (`kind === 'access'`); absent on org
107
+ * invites. `projectName` / `jobName` are denormalized for email and UI
108
+ * copy only — never authoritative.
109
+ */
110
+ kind?: 'organization' | 'access';
111
+ projectId?: string;
112
+ jobId?: string;
113
+ projectName?: string;
114
+ jobName?: string;
52
115
  }
53
116
  export interface OrganizationMember extends FirestoreDoc {
54
117
  email: string;
@@ -4,7 +4,38 @@ export var OrganizationRole;
4
4
  OrganizationRole["Full"] = "member";
5
5
  OrganizationRole["Contributor"] = "contributor";
6
6
  OrganizationRole["Viewer"] = "viewer";
7
+ /**
8
+ * Synthetic org membership for users invited to a single project or job.
9
+ * Grants read of the org doc and its supporting library (templates, labels,
10
+ * formulas, miter variants, print logos) and nothing under `projects/`.
11
+ * Server-written only; never offered in an org role picker. The same word
12
+ * is used for the project-level marker an `AccessGrant` carries when a user
13
+ * has job access but no project access — "may read this container's doc,
14
+ * nothing inside it".
15
+ */
16
+ OrganizationRole["Guest"] = "guest";
7
17
  })(OrganizationRole || (OrganizationRole = {}));
18
+ /** Roles an org admin may assign in Team settings. Excludes Guest. */
19
+ export const ASSIGNABLE_ORG_ROLES = [
20
+ OrganizationRole.Admin,
21
+ OrganizationRole.Full,
22
+ OrganizationRole.Contributor,
23
+ OrganizationRole.Viewer,
24
+ ];
25
+ export const SCOPED_ROLES = [
26
+ OrganizationRole.Full,
27
+ OrganizationRole.Contributor,
28
+ OrganizationRole.Viewer,
29
+ ];
30
+ /**
31
+ * What the sharing UIs offer. Contributor is deliberately absent (Figma
32
+ * 1028-47443 / 746-37130 show Full and Viewer); add it here to enable it —
33
+ * rules and types already accept it.
34
+ */
35
+ export const ACCESS_ROLE_OPTIONS = [
36
+ { label: 'Full', value: OrganizationRole.Full },
37
+ { label: 'Viewer', value: OrganizationRole.Viewer },
38
+ ];
8
39
  export var InvitationStatus;
9
40
  (function (InvitationStatus) {
10
41
  InvitationStatus["Pending"] = "pending";
@@ -0,0 +1,63 @@
1
+ import { OrganizationRole, type AccessGrant, type ScopedRole } from '../types/firestore.js';
2
+ /** A role as it may arrive from Firestore: possibly legacy, possibly missing. */
3
+ export type MaybeRole = OrganizationRole | string | null | undefined;
4
+ export declare const ROLE_RANK: Readonly<Record<OrganizationRole, number>>;
5
+ /**
6
+ * Maps a stored role string to the enum. Founder member docs written by early
7
+ * `cloneDemoOrganization` carry the legacy value `'creator'`, which the Cloud
8
+ * Functions treat as admin; normalise it the same way. Unknown values → null.
9
+ */
10
+ export declare function normalizeRole(role: MaybeRole): OrganizationRole | null;
11
+ export declare function roleAtLeast(role: MaybeRole, min: OrganizationRole): boolean;
12
+ /**
13
+ * Highest role wins across the levels that enclose a document. Pass only the
14
+ * levels that apply: `effectiveRole(org)` for org-level resources,
15
+ * `effectiveRole(org, project)` for a project, `effectiveRole(org, project,
16
+ * job)` for anything under a job. Guest at any level is ignored. Returns null
17
+ * when the user holds no data-bearing role.
18
+ */
19
+ export declare function effectiveRole(orgRole: MaybeRole, projectRole?: MaybeRole, jobRole?: MaybeRole): OrganizationRole | null;
20
+ /** Viewer or better. */
21
+ export declare const canView: (role: MaybeRole) => boolean;
22
+ /** Contributor or better — may update existing documents. */
23
+ export declare const canEdit: (role: MaybeRole) => boolean;
24
+ /** Full or better — may create/delete children and manage sharing. */
25
+ export declare const canManage: (role: MaybeRole) => boolean;
26
+ /** Org admin — team management, org settings, deleting the org. */
27
+ export declare const canManageOrg: (orgRole: MaybeRole) => boolean;
28
+ export declare const isGuest: (orgRole: MaybeRole) => boolean;
29
+ /**
30
+ * The grants a user holds inside ONE organization, indexed for O(1) lookup.
31
+ * `guestProjectIds` are projects the user may read the doc of (because they
32
+ * hold a job grant inside) but has no project-level role on.
33
+ */
34
+ export interface GroupedGrants {
35
+ projectRoles: Readonly<Record<string, ScopedRole>>;
36
+ jobRolesByProject: Readonly<Record<string, Readonly<Record<string, ScopedRole>>>>;
37
+ guestProjectIds: readonly string[];
38
+ }
39
+ export declare const EMPTY_GRANTS: GroupedGrants;
40
+ /**
41
+ * Groups the raw docs from `collectionGroup('access')` for one org. Grants for
42
+ * other orgs are ignored, so callers may pass the whole snapshot. A project
43
+ * grant with role `guest` is a marker, recorded in `guestProjectIds` only.
44
+ */
45
+ export declare function groupGrants(grants: readonly Pick<AccessGrant, 'organizationId' | 'projectId' | 'jobId' | 'scope' | 'role'>[], orgId: string): GroupedGrants;
46
+ export interface ResolvedAccess {
47
+ /** Effective (highest-wins) role, or null when the user has no access. */
48
+ role: OrganizationRole | null;
49
+ canView: boolean;
50
+ canEdit: boolean;
51
+ canManage: boolean;
52
+ /**
53
+ * True when the user may read the project DOCUMENT itself (org viewer+,
54
+ * a project role, or the guest marker). Distinct from `canView`, which
55
+ * covers the project's contents.
56
+ */
57
+ canReadProjectDoc: boolean;
58
+ }
59
+ /**
60
+ * Resolves what a user may do at a project or a job. Mirrors the rules:
61
+ * canViewProject/Job, canContributeProject/Job, canEditProject/Job.
62
+ */
63
+ export declare function resolveAccess(orgRole: MaybeRole, grants: GroupedGrants, projectId: string, jobId?: string | null): ResolvedAccess;
@@ -0,0 +1,131 @@
1
+ // Role arithmetic shared by both clients and the backend. Pure: no React, no
2
+ // Firebase, no Skia — this module is re-exported from `server.ts`.
3
+ //
4
+ // Three levels of access exist (organization → project → job). A user's
5
+ // effective role on any document is the HIGHEST of the roles they hold at the
6
+ // levels enclosing it; lower-level grants are additive and never restrict.
7
+ // `Guest` (org role, or the project-level marker on an AccessGrant) ranks
8
+ // below Viewer and contributes nothing to the effective role — it exists so
9
+ // the container document itself is readable.
10
+ //
11
+ // The Firestore rules in rock-pro-backend/firestore.rules encode the same
12
+ // tiers; keep the two in step:
13
+ // view = Viewer+ (rules: canView*)
14
+ // edit = Contributor+ (rules: canContribute*)
15
+ // manage = Full+ (rules: canEdit* / canManage*)
16
+ import { OrganizationRole, } from '../types/firestore.js';
17
+ export const ROLE_RANK = {
18
+ [OrganizationRole.Guest]: 0,
19
+ [OrganizationRole.Viewer]: 1,
20
+ [OrganizationRole.Contributor]: 2,
21
+ [OrganizationRole.Full]: 3,
22
+ [OrganizationRole.Admin]: 4,
23
+ };
24
+ const KNOWN_ROLES = new Set(Object.values(OrganizationRole));
25
+ /**
26
+ * Maps a stored role string to the enum. Founder member docs written by early
27
+ * `cloneDemoOrganization` carry the legacy value `'creator'`, which the Cloud
28
+ * Functions treat as admin; normalise it the same way. Unknown values → null.
29
+ */
30
+ export function normalizeRole(role) {
31
+ if (role == null)
32
+ return null;
33
+ if (role === 'creator')
34
+ return OrganizationRole.Admin;
35
+ return KNOWN_ROLES.has(role) ? role : null;
36
+ }
37
+ export function roleAtLeast(role, min) {
38
+ const r = normalizeRole(role);
39
+ return r != null && ROLE_RANK[r] >= ROLE_RANK[min];
40
+ }
41
+ /**
42
+ * Highest role wins across the levels that enclose a document. Pass only the
43
+ * levels that apply: `effectiveRole(org)` for org-level resources,
44
+ * `effectiveRole(org, project)` for a project, `effectiveRole(org, project,
45
+ * job)` for anything under a job. Guest at any level is ignored. Returns null
46
+ * when the user holds no data-bearing role.
47
+ */
48
+ export function effectiveRole(orgRole, projectRole, jobRole) {
49
+ let best = null;
50
+ for (const raw of [orgRole, projectRole, jobRole]) {
51
+ const r = normalizeRole(raw);
52
+ if (r == null || r === OrganizationRole.Guest)
53
+ continue;
54
+ if (best == null || ROLE_RANK[r] > ROLE_RANK[best])
55
+ best = r;
56
+ }
57
+ return best;
58
+ }
59
+ /** Viewer or better. */
60
+ export const canView = (role) => roleAtLeast(role, OrganizationRole.Viewer);
61
+ /** Contributor or better — may update existing documents. */
62
+ export const canEdit = (role) => roleAtLeast(role, OrganizationRole.Contributor);
63
+ /** Full or better — may create/delete children and manage sharing. */
64
+ export const canManage = (role) => roleAtLeast(role, OrganizationRole.Full);
65
+ /** Org admin — team management, org settings, deleting the org. */
66
+ export const canManageOrg = (orgRole) => roleAtLeast(orgRole, OrganizationRole.Admin);
67
+ export const isGuest = (orgRole) => normalizeRole(orgRole) === OrganizationRole.Guest;
68
+ export const EMPTY_GRANTS = Object.freeze({
69
+ projectRoles: Object.freeze({}),
70
+ jobRolesByProject: Object.freeze({}),
71
+ guestProjectIds: Object.freeze([]),
72
+ });
73
+ function isScopedRole(role) {
74
+ const r = normalizeRole(role);
75
+ return (r === OrganizationRole.Full ||
76
+ r === OrganizationRole.Contributor ||
77
+ r === OrganizationRole.Viewer);
78
+ }
79
+ /**
80
+ * Groups the raw docs from `collectionGroup('access')` for one org. Grants for
81
+ * other orgs are ignored, so callers may pass the whole snapshot. A project
82
+ * grant with role `guest` is a marker, recorded in `guestProjectIds` only.
83
+ */
84
+ export function groupGrants(grants, orgId) {
85
+ var _a;
86
+ const projectRoles = {};
87
+ const jobRolesByProject = {};
88
+ const guest = new Set();
89
+ for (const g of grants) {
90
+ if (g.organizationId !== orgId)
91
+ continue;
92
+ if (g.scope === 'job') {
93
+ if (!g.jobId || !isScopedRole(g.role))
94
+ continue;
95
+ (jobRolesByProject[_a = g.projectId] ?? (jobRolesByProject[_a] = {}))[g.jobId] = g.role;
96
+ }
97
+ else if (g.scope === 'project') {
98
+ if (isScopedRole(g.role))
99
+ projectRoles[g.projectId] = g.role;
100
+ else if (normalizeRole(g.role) === OrganizationRole.Guest)
101
+ guest.add(g.projectId);
102
+ }
103
+ }
104
+ // A real project role supersedes a stale marker.
105
+ for (const id of Object.keys(projectRoles))
106
+ guest.delete(id);
107
+ return {
108
+ projectRoles,
109
+ jobRolesByProject,
110
+ guestProjectIds: [...guest].sort(),
111
+ };
112
+ }
113
+ /**
114
+ * Resolves what a user may do at a project or a job. Mirrors the rules:
115
+ * canViewProject/Job, canContributeProject/Job, canEditProject/Job.
116
+ */
117
+ export function resolveAccess(orgRole, grants, projectId, jobId) {
118
+ const projectRole = grants.projectRoles[projectId];
119
+ const jobRole = jobId
120
+ ? grants.jobRolesByProject[projectId]?.[jobId]
121
+ : undefined;
122
+ const role = effectiveRole(orgRole, projectRole, jobId ? jobRole : undefined);
123
+ return {
124
+ role,
125
+ canView: canView(role),
126
+ canEdit: canEdit(role),
127
+ canManage: canManage(role),
128
+ canReadProjectDoc: canView(effectiveRole(orgRole, projectRole)) ||
129
+ grants.guestProjectIds.includes(projectId),
130
+ };
131
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reekon-tools/boldr-utils",
3
- "version": "1.20.3",
3
+ "version": "1.21.1",
4
4
  "description": "Shared utilities for formulas and measurement conversion used in Reekon apps",
5
5
  "author": "REEKON Tools",
6
6
  "license": "MIT",