@happyvertical/smrt-fields 0.40.57
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/AGENTS.md +179 -0
- package/CLAUDE.md +1 -0
- package/LICENSE +7 -0
- package/README.md +52 -0
- package/dist/index.d.ts +532 -0
- package/dist/index.js +936 -0
- package/dist/index.js.map +1 -0
- package/dist/manifest.json +501 -0
- package/dist/smrt-knowledge.json +347 -0
- package/dist/types.d.ts +203 -0
- package/dist/types.js +16 -0
- package/dist/types.js.map +1 -0
- package/package.json +63 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,936 @@
|
|
|
1
|
+
import { APP_FIELD_POLICY_SCOPE_KEY, FIELD_POLICY_SCOPE_TYPES, FIELD_POLICY_VISIBILITIES } from "./types.js";
|
|
2
|
+
import { ObjectRegistry, SmrtCollection, SmrtObject, crossPackageRef, field, smrt } from "@happyvertical/smrt-core";
|
|
3
|
+
import { TenantIsolationError, assertTenantReadAllowed, getCurrentTenant, isSuperAdminBypass, tenantId } from "@happyvertical/smrt-tenancy";
|
|
4
|
+
import { importWorkspaceModule } from "@happyvertical/smrt-core/utils/import-workspace-module";
|
|
5
|
+
//#region \0rolldown/runtime.js
|
|
6
|
+
var __defProp$2 = Object.defineProperty;
|
|
7
|
+
var __exportAll = (all, no_symbols) => {
|
|
8
|
+
let target = {};
|
|
9
|
+
for (var name in all) __defProp$2(target, name, {
|
|
10
|
+
get: all[name],
|
|
11
|
+
enumerable: true
|
|
12
|
+
});
|
|
13
|
+
if (!no_symbols) __defProp$2(target, Symbol.toStringTag, { value: "Module" });
|
|
14
|
+
return target;
|
|
15
|
+
};
|
|
16
|
+
//#endregion
|
|
17
|
+
//#region src/__smrt-register__.ts
|
|
18
|
+
ObjectRegistry.registerPackageManifest(JSON.parse("{\"version\":\"1.0.0\",\"timestamp\":0,\"packageName\":\"@happyvertical/smrt-fields\",\"packageVersion\":\"0.40.57\",\"objects\":{\"@happyvertical/smrt-fields:FieldPolicyCollection\":{\"name\":\"fieldpolicycollection\",\"className\":\"FieldPolicyCollection\",\"qualifiedName\":\"@happyvertical/smrt-fields:FieldPolicyCollection\",\"collection\":\"fieldpolicies\",\"filePath\":\"/home/runner/_work/smrt/smrt/packages/fields/src/collections/FieldPolicyCollection.ts\",\"packageName\":\"@happyvertical/smrt-fields\",\"fields\":{},\"methods\":{\"getAppRows\":{\"name\":\"getAppRows\",\"async\":true,\"parameters\":[{\"name\":\"objectRef\",\"type\":\"string\",\"optional\":false}],\"returnType\":\"Promise<Map<string, FieldPolicy>>\",\"isStatic\":false,\"isPublic\":true},\"getTenantRows\":{\"name\":\"getTenantRows\",\"async\":true,\"parameters\":[{\"name\":\"objectRef\",\"type\":\"string\",\"optional\":false},{\"name\":\"tenantIds\",\"type\":\"string[]\",\"optional\":false}],\"returnType\":\"Promise<Map<string, Map<string, FieldPolicy>>>\",\"isStatic\":false,\"isPublic\":true},\"getUserRows\":{\"name\":\"getUserRows\",\"async\":true,\"parameters\":[{\"name\":\"objectRef\",\"type\":\"string\",\"optional\":false},{\"name\":\"userId\",\"type\":\"string\",\"optional\":false}],\"returnType\":\"Promise<Map<string, FieldPolicy>>\",\"isStatic\":false,\"isPublic\":true},\"resolveBatch\":{\"name\":\"resolveBatch\",\"async\":true,\"parameters\":[{\"name\":\"options\",\"type\":\"object\",\"optional\":true}],\"returnType\":\"Promise<FieldPolicyBatchResult>\",\"isStatic\":false,\"isPublic\":true}},\"decoratorConfig\":{\"conflictColumns\":[\"object_ref\",\"field_name\",\"scope_type\",\"scope_key\"],\"api\":{\"include\":[\"create\",\"update\",\"delete\",\"resolveBatch\"],\"routes\":{\"resolveBatch\":{\"scope\":\"collection\",\"method\":\"POST\",\"path\":\"resolve\"}}},\"cli\":false,\"mcp\":false,\"tableName\":\"_smrt_field_policies\"},\"extends\":\"SmrtCollection\",\"extendsTypeArg\":\"FieldPolicy\",\"exportName\":\"FieldPolicyCollection\",\"collectionExportName\":\"FieldPolicyCollectionCollection\",\"schema\":{\"tableName\":\"_smrt_field_policies\",\"ddl\":\"CREATE TABLE IF NOT EXISTS \\\"_smrt_field_policies\\\" (\\n \\\"id\\\" UUID PRIMARY KEY NOT NULL,\\n \\\"slug\\\" TEXT NOT NULL,\\n \\\"context\\\" TEXT NOT NULL DEFAULT '',\\n \\\"created_at\\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\\"updated_at\\\" TIMESTAMP NOT NULL DEFAULT current_timestamp\\n);\",\"columns\":{\"id\":{\"type\":\"UUID\",\"primaryKey\":true,\"referenceKind\":\"id\",\"notNull\":true},\"slug\":{\"type\":\"TEXT\",\"notNull\":true},\"context\":{\"type\":\"TEXT\",\"notNull\":true,\"default\":\"\"},\"created_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"updated_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"}},\"indexes\":[{\"name\":\"_smrt_field_policies_id_idx\",\"columns\":[\"id\"]},{\"name\":\"_smrt_field_policies_object_ref_field_name_idx\",\"columns\":[\"object_ref\",\"field_name\",\"scope_type\",\"scope_key\"],\"unique\":true}],\"version\":\"13aa748c\"}},\"@happyvertical/smrt-fields:FieldPolicy\":{\"name\":\"fieldpolicy\",\"className\":\"FieldPolicy\",\"qualifiedName\":\"@happyvertical/smrt-fields:FieldPolicy\",\"collection\":\"fieldpolicies\",\"filePath\":\"/home/runner/_work/smrt/smrt/packages/fields/src/models/FieldPolicy.ts\",\"packageName\":\"@happyvertical/smrt-fields\",\"fields\":{\"objectRef\":{\"type\":\"text\",\"required\":true,\"default\":\"\",\"_meta\":{\"required\":true}},\"fieldName\":{\"type\":\"text\",\"required\":true,\"default\":\"\",\"_meta\":{\"required\":true}},\"scopeType\":{\"type\":\"text\",\"required\":true,\"default\":\"app\",\"_meta\":{\"required\":true}},\"tenantId\":{\"type\":\"text\",\"required\":false,\"_meta\":{\"sqlType\":\"UUID\",\"nullable\":true,\"__tenancy\":{\"isTenantIdField\":true,\"autoFilter\":true,\"required\":false,\"autoPopulate\":true,\"nullable\":true}}},\"userId\":{\"type\":\"crossPackageRef\",\"required\":false,\"related\":\"@happyvertical/smrt-users:User\",\"_meta\":{\"nullable\":true}},\"scopeKey\":{\"type\":\"text\",\"required\":true,\"_meta\":{\"required\":true}},\"defaultValue\":{\"type\":\"text\",\"required\":false,\"_meta\":{\"nullable\":true}},\"visibility\":{\"type\":\"text\",\"required\":false,\"_meta\":{\"nullable\":true}},\"help\":{\"type\":\"text\",\"required\":false,\"_meta\":{\"nullable\":true}},\"label\":{\"type\":\"text\",\"required\":false,\"_meta\":{\"nullable\":true}},\"displayOrder\":{\"type\":\"integer\",\"required\":false,\"_meta\":{\"nullable\":true}},\"locked\":{\"type\":\"boolean\",\"required\":false,\"_meta\":{\"nullable\":true}},\"updatedBy\":{\"type\":\"crossPackageRef\",\"required\":false,\"related\":\"@happyvertical/smrt-users:User\",\"_meta\":{\"nullable\":true}}},\"methods\":{\"getDefaultValue\":{\"name\":\"getDefaultValue\",\"async\":false,\"parameters\":[],\"returnType\":\"any\",\"isStatic\":false,\"isPublic\":true},\"setDefaultValue\":{\"name\":\"setDefaultValue\",\"async\":false,\"parameters\":[{\"name\":\"value\",\"type\":\"any\",\"optional\":false}],\"returnType\":\"void\",\"isStatic\":false,\"isPublic\":true},\"save\":{\"name\":\"save\",\"async\":true,\"parameters\":[],\"returnType\":\"Promise\",\"isStatic\":false,\"isPublic\":true},\"delete\":{\"name\":\"delete\",\"async\":true,\"parameters\":[],\"returnType\":\"Promise<void>\",\"isStatic\":false,\"isPublic\":true}},\"decoratorConfig\":{\"tableName\":\"_smrt_field_policies\",\"conflictColumns\":[\"object_ref\",\"field_name\",\"scope_type\",\"scope_key\"],\"api\":{\"include\":[\"create\",\"update\",\"delete\"]},\"cli\":{\"include\":[\"create\",\"update\",\"delete\"],\"exclude\":[\"getDefaultValue\",\"setDefaultValue\"]},\"mcp\":{\"include\":[]}},\"extends\":\"SmrtObject\",\"exportName\":\"FieldPolicy\",\"collectionExportName\":\"FieldPolicyCollection\",\"validationRules\":[{\"field\":\"objectRef\",\"rule\":\"required\",\"fieldType\":\"text\"},{\"field\":\"fieldName\",\"rule\":\"required\",\"fieldType\":\"text\"},{\"field\":\"scopeType\",\"rule\":\"required\",\"fieldType\":\"text\"},{\"field\":\"scopeKey\",\"rule\":\"required\",\"fieldType\":\"text\"}],\"schema\":{\"tableName\":\"_smrt_field_policies\",\"ddl\":\"CREATE TABLE IF NOT EXISTS \\\"_smrt_field_policies\\\" (\\n \\\"id\\\" UUID PRIMARY KEY NOT NULL,\\n \\\"slug\\\" TEXT NOT NULL,\\n \\\"context\\\" TEXT NOT NULL DEFAULT '',\\n \\\"created_at\\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\\"updated_at\\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\\"object_ref\\\" TEXT NOT NULL DEFAULT '',\\n \\\"field_name\\\" TEXT NOT NULL DEFAULT '',\\n \\\"scope_type\\\" TEXT NOT NULL DEFAULT 'app',\\n \\\"tenant_id\\\" UUID,\\n \\\"user_id\\\" UUID,\\n \\\"scope_key\\\" TEXT NOT NULL,\\n \\\"default_value\\\" TEXT,\\n \\\"visibility\\\" TEXT,\\n \\\"help\\\" TEXT,\\n \\\"label\\\" TEXT,\\n \\\"display_order\\\" INTEGER,\\n \\\"locked\\\" BOOLEAN,\\n \\\"updated_by\\\" UUID\\n);\",\"columns\":{\"id\":{\"type\":\"UUID\",\"primaryKey\":true,\"referenceKind\":\"id\",\"notNull\":true},\"slug\":{\"type\":\"TEXT\",\"notNull\":true},\"context\":{\"type\":\"TEXT\",\"notNull\":true,\"default\":\"\"},\"created_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"updated_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"object_ref\":{\"type\":\"TEXT\",\"notNull\":true,\"unique\":false,\"default\":\"\"},\"field_name\":{\"type\":\"TEXT\",\"notNull\":true,\"unique\":false,\"default\":\"\"},\"scope_type\":{\"type\":\"TEXT\",\"notNull\":true,\"unique\":false,\"default\":\"app\"},\"tenant_id\":{\"type\":\"UUID\",\"referenceKind\":\"tenantId\",\"notNull\":false,\"unique\":false},\"user_id\":{\"type\":\"UUID\",\"referenceKind\":\"crossPackageRef\",\"notNull\":false,\"unique\":false},\"scope_key\":{\"type\":\"TEXT\",\"notNull\":true,\"unique\":false},\"default_value\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"visibility\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"help\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"label\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"display_order\":{\"type\":\"INTEGER\",\"notNull\":false,\"unique\":false},\"locked\":{\"type\":\"BOOLEAN\",\"notNull\":false,\"unique\":false},\"updated_by\":{\"type\":\"UUID\",\"referenceKind\":\"crossPackageRef\",\"notNull\":false,\"unique\":false}},\"indexes\":[{\"name\":\"_smrt_field_policies_id_idx\",\"columns\":[\"id\"]},{\"name\":\"_smrt_field_policies_object_ref_field_name_idx\",\"columns\":[\"object_ref\",\"field_name\",\"scope_type\",\"scope_key\"],\"unique\":true}],\"version\":\"34b355cc\"}}},\"moduleType\":\"smrt\",\"smrtDependencies\":[\"@happyvertical/smrt-core\",\"@happyvertical/smrt-tenancy\",\"@happyvertical/smrt-users\"]}"));
|
|
19
|
+
//#endregion
|
|
20
|
+
//#region src/cache.ts
|
|
21
|
+
var FIELD_POLICY_CACHE_TTL_MS = 3e4;
|
|
22
|
+
var fieldPolicyCache = /* @__PURE__ */ new Map();
|
|
23
|
+
var dbInstanceIds = /* @__PURE__ */ new WeakMap();
|
|
24
|
+
var nextDbId = 1;
|
|
25
|
+
function getDbNamespace(db) {
|
|
26
|
+
if (!db) return "no-db";
|
|
27
|
+
if (typeof db === "string") return `db:${db}`;
|
|
28
|
+
if (typeof db === "object") {
|
|
29
|
+
const dbObject = db;
|
|
30
|
+
if (typeof dbObject.query === "function") {
|
|
31
|
+
if (!dbInstanceIds.has(dbObject)) dbInstanceIds.set(dbObject, `db-instance:${nextDbId++}`);
|
|
32
|
+
const namespace = dbInstanceIds.get(dbObject);
|
|
33
|
+
if (namespace) return namespace;
|
|
34
|
+
return "db-instance:unknown";
|
|
35
|
+
}
|
|
36
|
+
try {
|
|
37
|
+
return `db-config:${JSON.stringify(dbObject)}`;
|
|
38
|
+
} catch {
|
|
39
|
+
return "db-config:opaque";
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return "db:unknown";
|
|
43
|
+
}
|
|
44
|
+
var hierarchyLoaderIds = /* @__PURE__ */ new WeakMap();
|
|
45
|
+
var nextLoaderId = 1;
|
|
46
|
+
function getHierarchyLoaderNamespace(loader) {
|
|
47
|
+
if (typeof loader !== "function") return "loader:default";
|
|
48
|
+
const key = loader;
|
|
49
|
+
if (!hierarchyLoaderIds.has(key)) hierarchyLoaderIds.set(key, `loader:${nextLoaderId++}`);
|
|
50
|
+
return hierarchyLoaderIds.get(key) ?? "loader:unknown";
|
|
51
|
+
}
|
|
52
|
+
function buildCacheKey(objectRef, tenantId, userId, db, hierarchyLoader) {
|
|
53
|
+
return `${getDbNamespace(db)}::${objectRef}::${tenantId ?? "app"}::${userId ?? "user:none"}::${getHierarchyLoaderNamespace(hierarchyLoader)}`;
|
|
54
|
+
}
|
|
55
|
+
function getFieldPolicyCacheTtlMs() {
|
|
56
|
+
return FIELD_POLICY_CACHE_TTL_MS;
|
|
57
|
+
}
|
|
58
|
+
function getCachedFieldPolicy(objectRef, tenantId, userId, db, hierarchyLoader) {
|
|
59
|
+
const cacheKey = buildCacheKey(objectRef, tenantId, userId, db, hierarchyLoader);
|
|
60
|
+
const cached = fieldPolicyCache.get(cacheKey);
|
|
61
|
+
if (!cached) return null;
|
|
62
|
+
if (cached.expiresAt <= Date.now()) {
|
|
63
|
+
fieldPolicyCache.delete(cacheKey);
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
return cached.value;
|
|
67
|
+
}
|
|
68
|
+
function setCachedFieldPolicy(objectRef, tenantId, userId, db, value, hierarchyLoader) {
|
|
69
|
+
fieldPolicyCache.set(buildCacheKey(objectRef, tenantId, userId, db, hierarchyLoader), {
|
|
70
|
+
expiresAt: Date.now() + FIELD_POLICY_CACHE_TTL_MS,
|
|
71
|
+
value
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
function invalidateFieldPolicyCache(objectRef, db) {
|
|
75
|
+
const keyPrefix = `${getDbNamespace(db)}::${objectRef}::`;
|
|
76
|
+
for (const cacheKey of fieldPolicyCache.keys()) if (cacheKey.startsWith(keyPrefix)) fieldPolicyCache.delete(cacheKey);
|
|
77
|
+
}
|
|
78
|
+
function clearFieldPolicyCache() {
|
|
79
|
+
fieldPolicyCache.clear();
|
|
80
|
+
}
|
|
81
|
+
//#endregion
|
|
82
|
+
//#region src/field-definitions.ts
|
|
83
|
+
function requireRegisteredObject(objectRef) {
|
|
84
|
+
if (!objectRef?.includes(":")) throw new Error(`Field policy objectRef must be a qualified class name ("@package/name:ClassName"), got "${objectRef}"`);
|
|
85
|
+
if (!ObjectRegistry.getClassByQualifiedName(objectRef)) throw new Error(`Unknown field policy objectRef "${objectRef}": no class with that qualified name is registered`);
|
|
86
|
+
}
|
|
87
|
+
async function getObjectFieldMap(objectRef) {
|
|
88
|
+
requireRegisteredObject(objectRef);
|
|
89
|
+
return ObjectRegistry.getAllFields(objectRef);
|
|
90
|
+
}
|
|
91
|
+
function sanitizeFieldUIHints(value) {
|
|
92
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return;
|
|
93
|
+
const raw = value;
|
|
94
|
+
const ui = {
|
|
95
|
+
...typeof raw.basic === "boolean" ? { basic: raw.basic } : {},
|
|
96
|
+
...typeof raw.group === "string" ? { group: raw.group } : {},
|
|
97
|
+
...typeof raw.order === "number" && Number.isFinite(raw.order) ? { order: raw.order } : {},
|
|
98
|
+
...typeof raw.locked === "boolean" ? { locked: raw.locked } : {}
|
|
99
|
+
};
|
|
100
|
+
return Object.keys(ui).length > 0 ? ui : void 0;
|
|
101
|
+
}
|
|
102
|
+
function isSensitiveField(field) {
|
|
103
|
+
return field.sensitive === true || field._meta?.sensitive === true;
|
|
104
|
+
}
|
|
105
|
+
function getFieldReadPermission(field) {
|
|
106
|
+
if (typeof field.readPermission === "string") return field.readPermission;
|
|
107
|
+
const metaPermission = field._meta?.readPermission;
|
|
108
|
+
return typeof metaPermission === "string" ? metaPermission : void 0;
|
|
109
|
+
}
|
|
110
|
+
function isTransientField(field) {
|
|
111
|
+
return field.transient === true || field._meta?.transient === true;
|
|
112
|
+
}
|
|
113
|
+
function isRequiredField(field) {
|
|
114
|
+
if (field.nullable === true || field._meta?.nullable === true) return false;
|
|
115
|
+
return field.required === true || field._meta?.required === true;
|
|
116
|
+
}
|
|
117
|
+
function getCodeDefault(field) {
|
|
118
|
+
if (field.default !== void 0) return { value: field.default };
|
|
119
|
+
if (field._meta?.default !== void 0) return { value: field._meta.default };
|
|
120
|
+
}
|
|
121
|
+
function isUsableRequiredDefault(defaultBox) {
|
|
122
|
+
if (!defaultBox) return false;
|
|
123
|
+
return defaultBox.value !== null && defaultBox.value !== "";
|
|
124
|
+
}
|
|
125
|
+
function buildCodeSeedVisibility(fields) {
|
|
126
|
+
let hasBasicMarkers = false;
|
|
127
|
+
const hints = /* @__PURE__ */ new Map();
|
|
128
|
+
for (const [name, field] of fields) {
|
|
129
|
+
const ui = sanitizeFieldUIHints(field._meta?.ui);
|
|
130
|
+
hints.set(name, ui);
|
|
131
|
+
if (ui?.basic === true) hasBasicMarkers = true;
|
|
132
|
+
}
|
|
133
|
+
const visibility = /* @__PURE__ */ new Map();
|
|
134
|
+
for (const [name] of fields) {
|
|
135
|
+
const ui = hints.get(name);
|
|
136
|
+
if (ui?.basic === true) visibility.set(name, "basic");
|
|
137
|
+
else if (ui?.basic === false || hasBasicMarkers) visibility.set(name, "advanced");
|
|
138
|
+
else visibility.set(name, "basic");
|
|
139
|
+
}
|
|
140
|
+
return visibility;
|
|
141
|
+
}
|
|
142
|
+
function buildCodeSeedDelta(field, visibility) {
|
|
143
|
+
const ui = sanitizeFieldUIHints(field._meta?.ui);
|
|
144
|
+
const help = typeof field.description === "string" ? field.description : typeof field._meta?.description === "string" ? field._meta.description : void 0;
|
|
145
|
+
const codeDefault = getCodeDefault(field);
|
|
146
|
+
return {
|
|
147
|
+
...codeDefault ? { default: codeDefault } : {},
|
|
148
|
+
visibility,
|
|
149
|
+
...help !== void 0 ? { help } : {},
|
|
150
|
+
...ui?.order !== void 0 ? { order: ui.order } : {},
|
|
151
|
+
...ui?.locked === true ? { locked: true } : {}
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
function getCodeSeedGroup(field) {
|
|
155
|
+
return sanitizeFieldUIHints(field._meta?.ui)?.group ?? null;
|
|
156
|
+
}
|
|
157
|
+
var UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
158
|
+
function usesTextIdStorage(field) {
|
|
159
|
+
return field.idType === "text" || field._meta?.idType === "text";
|
|
160
|
+
}
|
|
161
|
+
function assertDefaultValueMatchesFieldType(objectRef, fieldName, field, value) {
|
|
162
|
+
const label = `Field policy default for "${objectRef}.${fieldName}"`;
|
|
163
|
+
if (value === null) {
|
|
164
|
+
if (isRequiredField(field)) throw new Error(`${label} may not be null: the field is required, so a null default is never a usable default`);
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
switch (field.type) {
|
|
168
|
+
case "text":
|
|
169
|
+
if (typeof value !== "string") throw new Error(`${label} must be a string (field type "text")`);
|
|
170
|
+
return;
|
|
171
|
+
case "integer":
|
|
172
|
+
if (typeof value !== "number" || !Number.isInteger(value)) throw new Error(`${label} must be an integer (field type "integer")`);
|
|
173
|
+
return;
|
|
174
|
+
case "decimal":
|
|
175
|
+
if (typeof value !== "number" || !Number.isFinite(value)) throw new Error(`${label} must be a finite number (field type "decimal")`);
|
|
176
|
+
return;
|
|
177
|
+
case "boolean":
|
|
178
|
+
if (typeof value !== "boolean") throw new Error(`${label} must be a boolean (field type "boolean")`);
|
|
179
|
+
return;
|
|
180
|
+
case "datetime":
|
|
181
|
+
if (typeof value !== "string" || Number.isNaN(Date.parse(value))) throw new Error(`${label} must be a date-parseable string (field type "datetime")`);
|
|
182
|
+
return;
|
|
183
|
+
case "json": return;
|
|
184
|
+
case "foreignKey":
|
|
185
|
+
case "crossPackageRef":
|
|
186
|
+
if (typeof value !== "string") throw new Error(`${label} must be a string id (field type "${field.type}")`);
|
|
187
|
+
if (!usesTextIdStorage(field) && !UUID_PATTERN.test(value)) throw new Error(`${label} must be a UUID string (the reference column stores native UUIDs; declare idType 'text' on the field to store arbitrary ids)`);
|
|
188
|
+
return;
|
|
189
|
+
default: throw new Error(`${label} is not supported: defaults cannot target "${String(field.type)}" fields`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
//#endregion
|
|
193
|
+
//#region src/models/FieldPolicy.ts
|
|
194
|
+
var __defProp$1 = Object.defineProperty;
|
|
195
|
+
var __getOwnPropDesc$1 = Object.getOwnPropertyDescriptor;
|
|
196
|
+
var __decorateClass$1 = (decorators, target, key, kind) => {
|
|
197
|
+
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$1(target, key) : target;
|
|
198
|
+
for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (kind ? decorator(target, key, result) : decorator(result)) || result;
|
|
199
|
+
if (kind && result) __defProp$1(target, key, result);
|
|
200
|
+
return result;
|
|
201
|
+
};
|
|
202
|
+
var FieldPolicy = class extends SmrtObject {
|
|
203
|
+
objectRef = "";
|
|
204
|
+
fieldName = "";
|
|
205
|
+
scopeType = "app";
|
|
206
|
+
tenantId = null;
|
|
207
|
+
userId = null;
|
|
208
|
+
scopeKey = "";
|
|
209
|
+
defaultValue = null;
|
|
210
|
+
visibility = null;
|
|
211
|
+
help = null;
|
|
212
|
+
label = null;
|
|
213
|
+
displayOrder = null;
|
|
214
|
+
locked = null;
|
|
215
|
+
updatedBy = null;
|
|
216
|
+
constructor(options = {}) {
|
|
217
|
+
super(options);
|
|
218
|
+
if (options.objectRef !== void 0) this.objectRef = options.objectRef;
|
|
219
|
+
if (options.fieldName !== void 0) this.fieldName = options.fieldName;
|
|
220
|
+
if (options.scopeType !== void 0) this.scopeType = options.scopeType;
|
|
221
|
+
if (options.tenantId !== void 0) this.tenantId = options.tenantId;
|
|
222
|
+
if (options.userId !== void 0) this.userId = options.userId;
|
|
223
|
+
if (options.defaultValue !== void 0 && options.defaultValueRaw !== void 0) throw new Error("FieldPolicy accepts either defaultValue (already JSON-encoded) or defaultValueRaw (a plain value to serialize), not both");
|
|
224
|
+
if (options.defaultValueRaw !== void 0) this.setDefaultValue(options.defaultValueRaw);
|
|
225
|
+
else if (options.defaultValue !== void 0) this.defaultValue = options.defaultValue;
|
|
226
|
+
if (options.visibility !== void 0) this.visibility = options.visibility;
|
|
227
|
+
if (options.help !== void 0) this.help = options.help;
|
|
228
|
+
if (options.label !== void 0) this.label = options.label;
|
|
229
|
+
if (options.displayOrder !== void 0) this.displayOrder = options.displayOrder;
|
|
230
|
+
if (options.locked !== void 0) this.locked = options.locked;
|
|
231
|
+
if (options.updatedBy !== void 0) this.updatedBy = options.updatedBy;
|
|
232
|
+
}
|
|
233
|
+
/** Parse the stored JSON default. `undefined` = no stored default (inherit). */
|
|
234
|
+
getDefaultValue() {
|
|
235
|
+
if (this.defaultValue === null || this.defaultValue === void 0) return;
|
|
236
|
+
try {
|
|
237
|
+
return JSON.parse(this.defaultValue);
|
|
238
|
+
} catch {
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
/** Serialize a default value; `undefined` clears the override (inherit). */
|
|
243
|
+
setDefaultValue(value) {
|
|
244
|
+
this.defaultValue = value === void 0 ? null : JSON.stringify(value);
|
|
245
|
+
}
|
|
246
|
+
async save() {
|
|
247
|
+
this.attributeScopeToAmbientContext();
|
|
248
|
+
const previousIdentity = await this.getPersistedIdentity();
|
|
249
|
+
if (previousIdentity) this.assertScopeOwnedByAmbientContext(previousIdentity, "save");
|
|
250
|
+
const ambientContext = getCurrentTenant();
|
|
251
|
+
if (ambientContext) this.updatedBy = ambientContext.userId ?? null;
|
|
252
|
+
this.normalizeDefaultValueForPersistence();
|
|
253
|
+
await this.validateFieldPolicy();
|
|
254
|
+
this.scopeKey = this.computeScopeKey();
|
|
255
|
+
const identityChanged = previousIdentity && (previousIdentity.objectRef !== this.objectRef || previousIdentity.fieldName !== this.fieldName || previousIdentity.scopeType !== this.scopeType || previousIdentity.scopeKey !== this.scopeKey);
|
|
256
|
+
const result = identityChanged && previousIdentity ? await this.saveAfterIdentityChange() : await super.save();
|
|
257
|
+
if (identityChanged && previousIdentity) invalidateFieldPolicyCache(previousIdentity.objectRef, this.db);
|
|
258
|
+
invalidateFieldPolicyCache(this.objectRef, this.db);
|
|
259
|
+
return result;
|
|
260
|
+
}
|
|
261
|
+
async saveAfterIdentityChange() {
|
|
262
|
+
if (typeof this.db.beginTransaction === "function") return this.saveAfterIdentityChangeInTransaction();
|
|
263
|
+
return this.saveAfterIdentityChangeWithDeferredDelete();
|
|
264
|
+
}
|
|
265
|
+
async saveAfterIdentityChangeInTransaction() {
|
|
266
|
+
const originalDb = this._db;
|
|
267
|
+
const originalOptionsDb = this.options.db;
|
|
268
|
+
const tx = await this.db.beginTransaction?.();
|
|
269
|
+
if (!tx) return this.saveAfterIdentityChangeWithDeferredDelete();
|
|
270
|
+
try {
|
|
271
|
+
this._db = tx;
|
|
272
|
+
this.options.db = tx;
|
|
273
|
+
await super.delete();
|
|
274
|
+
const result = await super.save();
|
|
275
|
+
await tx.commit();
|
|
276
|
+
return result;
|
|
277
|
+
} catch (error) {
|
|
278
|
+
try {
|
|
279
|
+
await tx.rollback();
|
|
280
|
+
} catch {}
|
|
281
|
+
throw error;
|
|
282
|
+
} finally {
|
|
283
|
+
this._db = originalDb;
|
|
284
|
+
this.options.db = originalOptionsDb;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
async saveAfterIdentityChangeWithDeferredDelete() {
|
|
288
|
+
const previousId = this.id;
|
|
289
|
+
if (!previousId) return super.save();
|
|
290
|
+
const replacementId = crypto.randomUUID();
|
|
291
|
+
let replacementSaved = false;
|
|
292
|
+
this.id = replacementId;
|
|
293
|
+
try {
|
|
294
|
+
const result = await super.save();
|
|
295
|
+
replacementSaved = true;
|
|
296
|
+
await this.db.delete(this.tableName, { id: previousId });
|
|
297
|
+
return result;
|
|
298
|
+
} catch (error) {
|
|
299
|
+
if (replacementSaved) try {
|
|
300
|
+
await this.db.delete(this.tableName, { id: replacementId });
|
|
301
|
+
} catch {}
|
|
302
|
+
this.id = previousId;
|
|
303
|
+
throw error;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
async delete() {
|
|
307
|
+
const persisted = await this.getPersistedIdentity();
|
|
308
|
+
if (persisted) this.assertScopeOwnedByAmbientContext(persisted, "delete");
|
|
309
|
+
const objectRef = persisted?.objectRef ?? this.objectRef;
|
|
310
|
+
await super.delete();
|
|
311
|
+
invalidateFieldPolicyCache(objectRef, this.db);
|
|
312
|
+
if (this.objectRef && this.objectRef !== objectRef) invalidateFieldPolicyCache(this.objectRef, this.db);
|
|
313
|
+
}
|
|
314
|
+
async validateFieldPolicy() {
|
|
315
|
+
if (!this.objectRef || this.objectRef.trim() === "") throw new Error("FieldPolicy.objectRef is required");
|
|
316
|
+
if (!this.fieldName || this.fieldName.trim() === "") throw new Error("FieldPolicy.fieldName is required");
|
|
317
|
+
if (!FIELD_POLICY_SCOPE_TYPES.includes(this.scopeType)) throw new Error(`FieldPolicy.scopeType must be one of ${FIELD_POLICY_SCOPE_TYPES.join(", ")}; got "${this.scopeType}"`);
|
|
318
|
+
if (this.visibility !== null && !FIELD_POLICY_VISIBILITIES.includes(this.visibility)) throw new Error(`FieldPolicy.visibility must be null or one of ${FIELD_POLICY_VISIBILITIES.join(", ")}; got "${this.visibility}"`);
|
|
319
|
+
if (this.displayOrder !== null && (typeof this.displayOrder !== "number" || !Number.isInteger(this.displayOrder))) throw new Error("FieldPolicy.displayOrder must be null or an integer");
|
|
320
|
+
this.validateScopeConsistency();
|
|
321
|
+
this.validateTenantContextBoundary();
|
|
322
|
+
const fieldDef = (await getObjectFieldMap(this.objectRef)).get(this.fieldName);
|
|
323
|
+
if (!fieldDef) throw new Error(`Unknown field "${this.fieldName}" on "${this.objectRef}"`);
|
|
324
|
+
if (fieldDef._meta?.__smrtSystemField === true) throw new Error(`Field "${this.fieldName}" on "${this.objectRef}" is a framework system field and is not policy-addressable`);
|
|
325
|
+
if (fieldDef.type === "oneToMany" || fieldDef.type === "manyToMany") throw new Error(`Field "${this.fieldName}" on "${this.objectRef}" is a relationship pseudo-field and is not policy-addressable`);
|
|
326
|
+
if (fieldDef.type === "meta") throw new Error(`Field "${this.fieldName}" on "${this.objectRef}" is STI meta storage and is not policy-addressable`);
|
|
327
|
+
if (this.locked !== null && this.scopeType === "user") throw new Error("FieldPolicy.locked may only be set on org rows (app or tenant scope)");
|
|
328
|
+
if (this.defaultValue !== null) {
|
|
329
|
+
this.validateDefaultAgainstSecurityRail(fieldDef);
|
|
330
|
+
const parsed = this.parseDefaultValueOrThrow();
|
|
331
|
+
assertDefaultValueMatchesFieldType(this.objectRef, this.fieldName, fieldDef, parsed);
|
|
332
|
+
}
|
|
333
|
+
const demotesRequiredField = (this.visibility === "advanced" || this.visibility === "hidden") && isRequiredField(fieldDef);
|
|
334
|
+
const ownDefault = demotesRequiredField && this.defaultValue !== null ? { value: this.parseDefaultValueOrThrow() } : void 0;
|
|
335
|
+
const needsOrgDefault = demotesRequiredField && !isUsableRequiredDefault(ownDefault);
|
|
336
|
+
const needsLockCheck = this.scopeType === "user";
|
|
337
|
+
if (needsOrgDefault || needsLockCheck) {
|
|
338
|
+
const orgPolicy = await this.resolveOrgTierFieldPolicy();
|
|
339
|
+
if (needsOrgDefault) {
|
|
340
|
+
if (!isUsableRequiredDefault(orgPolicy?.hasDefault ? { value: orgPolicy.defaultValue } : void 0)) throw new Error(`Cannot set visibility "${this.visibility}" for required field "${this.objectRef}.${this.fieldName}": no resolved default exists at or below this layer`);
|
|
341
|
+
}
|
|
342
|
+
if (needsLockCheck && orgPolicy?.locked) throw new Error(`Field "${this.objectRef}.${this.fieldName}" is locked by org policy; user-scope overrides are not allowed`);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* Effective org-tier (code → app → tenant hierarchy) policy for this row's
|
|
347
|
+
* field, computed by the RESOLVER so write-time checks share the one
|
|
348
|
+
* precedence implementation — including ancestor-tenant cascades via the
|
|
349
|
+
* default hierarchy loader. The chain tenant is the row's own tenant for
|
|
350
|
+
* tenant-scope rows, the ambient context tenant for user-scope rows, and
|
|
351
|
+
* none for app-scope rows (their only lower layer is the code seed).
|
|
352
|
+
*
|
|
353
|
+
* On updates the resolver sees this row's PERSISTED version (there is no
|
|
354
|
+
* self-exclusion), so a save that removes the only default while demoting
|
|
355
|
+
* can pass here; the resolver-side safety net stays authoritative at read
|
|
356
|
+
* time.
|
|
357
|
+
*/
|
|
358
|
+
async resolveOrgTierFieldPolicy() {
|
|
359
|
+
const tenantIdForChain = this.scopeType === "tenant" ? this.tenantId : this.scopeType === "user" ? getCurrentTenant()?.tenantId ?? null : null;
|
|
360
|
+
const { resolveFieldPolicy } = await Promise.resolve().then(() => field_policy_resolver_exports);
|
|
361
|
+
return (await resolveFieldPolicy(this.objectRef, {
|
|
362
|
+
tenantId: tenantIdForChain,
|
|
363
|
+
db: this.options.db ?? this.options.persistence
|
|
364
|
+
})).fields[this.fieldName];
|
|
365
|
+
}
|
|
366
|
+
/**
|
|
367
|
+
* Exactly-one-owner scope shape: app rows carry neither id, tenant rows
|
|
368
|
+
* carry only `tenantId`, user rows carry only `userId` (the user tier is
|
|
369
|
+
* keyed by user alone so preferences follow the user across tenants).
|
|
370
|
+
*/
|
|
371
|
+
validateScopeConsistency() {
|
|
372
|
+
if (this.scopeType === "app") {
|
|
373
|
+
if (this.tenantId !== null || this.userId !== null) throw new Error("App-scope field policy rows must have tenantId and userId null");
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
if (this.scopeType === "tenant") {
|
|
377
|
+
if (!this.tenantId || this.userId !== null) throw new Error("Tenant-scope field policy rows must set tenantId and leave userId null");
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
if (!this.userId || this.tenantId !== null) throw new Error("User-scope field policy rows must set userId and leave tenantId null");
|
|
381
|
+
}
|
|
382
|
+
/** `userId ?? tenantId ?? '__app__'` — the `conflictColumns` scope key. */
|
|
383
|
+
computeScopeKey() {
|
|
384
|
+
return this.userId ?? this.tenantId ?? "__app__";
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* Derive a scope-owner column the transport could not carry (#2047).
|
|
388
|
+
*
|
|
389
|
+
* Core's mass-assignment guard treats `tenantId` as server-managed and
|
|
390
|
+
* strips it from EVERY generated create/update body, and `FieldPolicy` is
|
|
391
|
+
* deliberately not `@TenantScoped`, so the tenancy interceptor never
|
|
392
|
+
* repopulates it. A `POST {scopeType:'tenant', tenantId}` therefore always
|
|
393
|
+
* reached `validateScopeConsistency` with `tenantId === null`, making the
|
|
394
|
+
* org tier write-dead over every generated surface — the model, as the
|
|
395
|
+
* single validation authority for these rows, fills it in instead.
|
|
396
|
+
*
|
|
397
|
+
* This grants nothing: `assertScopeOwnedByAmbientContext` already requires
|
|
398
|
+
* a non-bypass caller's tenant/user rows to name exactly the ambient
|
|
399
|
+
* tenant/user, so the derived value is the ONLY value that could ever have
|
|
400
|
+
* been accepted. An explicit value is never overwritten (a super-admin
|
|
401
|
+
* bypass caller keeps naming other scopes), and with no ambient context
|
|
402
|
+
* nothing is derived — scope-shape validation rejects the row as before.
|
|
403
|
+
* Consequence: a bypass caller writing ANOTHER tenant's row must pass
|
|
404
|
+
* `tenantId` through a server-side model call, because the generated routes
|
|
405
|
+
* still strip it.
|
|
406
|
+
*/
|
|
407
|
+
attributeScopeToAmbientContext() {
|
|
408
|
+
const context = getCurrentTenant();
|
|
409
|
+
if (!context) return;
|
|
410
|
+
if (this.scopeType === "tenant" && this.tenantId === null) this.tenantId = context.tenantId;
|
|
411
|
+
if (this.scopeType === "user" && this.userId === null) this.userId = context.userId ?? null;
|
|
412
|
+
}
|
|
413
|
+
/**
|
|
414
|
+
* Fail-closed write boundary against the ambient tenant context, applied to
|
|
415
|
+
* the NEW scope on save and to the PERSISTED scope on save/delete of an
|
|
416
|
+
* existing row.
|
|
417
|
+
*
|
|
418
|
+
* With NO ambient identity at all (no tenant context entered — e.g. a
|
|
419
|
+
* runtime API deployment whose auth middleware never enters the tenancy
|
|
420
|
+
* ALS), only app-scope rows are accepted: tenant- and user-scope rows are
|
|
421
|
+
* unattributable without a context, so they are rejected outright rather
|
|
422
|
+
* than allowed by default. Inside a non-bypass context, a caller may only
|
|
423
|
+
* touch rows for its own tenant, and user rows only for its own user id —
|
|
424
|
+
* a context that carries NO user id may not touch the user tier at all.
|
|
425
|
+
* App-wide rows then require super-admin bypass (or a context-less/system
|
|
426
|
+
* caller).
|
|
427
|
+
*
|
|
428
|
+
* Package rule: a missing identity component DENIES, it never skips.
|
|
429
|
+
*/
|
|
430
|
+
assertScopeOwnedByAmbientContext(scope, operation) {
|
|
431
|
+
const context = getCurrentTenant();
|
|
432
|
+
if (!context) {
|
|
433
|
+
if (scope.scopeType === "tenant" || scope.scopeType === "user") throw new TenantIsolationError(`${scope.scopeType === "tenant" ? "Tenant" : "User"}-scope field policy ${operation}s require an ambient tenant context (or super-admin bypass); without one the caller identity is unattributable`);
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
if (isSuperAdminBypass()) return;
|
|
437
|
+
if (scope.scopeType === "app") throw new TenantIsolationError(`App-scope field policy ${operation}s are not allowed inside a tenant context (use a system context or super-admin bypass)`, { tenantId: context.tenantId });
|
|
438
|
+
if (scope.scopeType === "tenant" && scope.tenantId !== context.tenantId) throw new TenantIsolationError(`Tenant isolation violation in FieldPolicy.${operation}: context tenant is '${context.tenantId}' but the row belongs to '${scope.tenantId}'`, {
|
|
439
|
+
tenantId: context.tenantId,
|
|
440
|
+
attemptedTenantId: scope.tenantId ?? void 0
|
|
441
|
+
});
|
|
442
|
+
if (scope.scopeType === "user") {
|
|
443
|
+
if (context.userId === void 0) throw new TenantIsolationError(`User-scope field policy ${operation}s require an ambient context that carries a user id; this context cannot attribute a user-scope write`, { tenantId: context.tenantId });
|
|
444
|
+
if (scope.userId !== context.userId) throw new TenantIsolationError(`Tenant isolation violation in FieldPolicy.${operation}: context user is '${context.userId}' but the row belongs to '${scope.userId}'`, { tenantId: context.tenantId });
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
validateTenantContextBoundary() {
|
|
448
|
+
this.assertScopeOwnedByAmbientContext({
|
|
449
|
+
scopeType: this.scopeType,
|
|
450
|
+
tenantId: this.tenantId,
|
|
451
|
+
userId: this.userId
|
|
452
|
+
}, "save");
|
|
453
|
+
}
|
|
454
|
+
validateDefaultAgainstSecurityRail(fieldDef) {
|
|
455
|
+
if (isTransientField(fieldDef)) throw new Error(`Cannot store a default for transient field "${this.objectRef}.${this.fieldName}"`);
|
|
456
|
+
if (isSensitiveField(fieldDef)) throw new Error(`Cannot store a default for sensitive field "${this.objectRef}.${this.fieldName}"`);
|
|
457
|
+
const readPermission = getFieldReadPermission(fieldDef);
|
|
458
|
+
if (readPermission) throw new Error(`Cannot store a default for read-permission-gated field "${this.objectRef}.${this.fieldName}" (requires "${readPermission}")`);
|
|
459
|
+
}
|
|
460
|
+
parseDefaultValueOrThrow() {
|
|
461
|
+
try {
|
|
462
|
+
return JSON.parse(this.defaultValue);
|
|
463
|
+
} catch (error) {
|
|
464
|
+
throw new Error(`FieldPolicy default for "${this.objectRef}.${this.fieldName}" is not valid JSON: ${error instanceof Error ? error.message : String(error)}. The "defaultValue" option is the already-encoded channel; pass a plain value as "defaultValueRaw" (or call setDefaultValue()).`);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
/**
|
|
468
|
+
* The persisted row this save/delete would replace, looked up by primary
|
|
469
|
+
* key and — when that misses — by the NATURAL key.
|
|
470
|
+
*
|
|
471
|
+
* The natural-key fallback is load-bearing for authorization: every
|
|
472
|
+
* generated create arrives with a freshly minted UUID, so a primary-key
|
|
473
|
+
* lookup always misses, yet the `conflictColumns` upsert still overwrites
|
|
474
|
+
* whatever row already occupies `(objectRef, fieldName, scopeType,
|
|
475
|
+
* scopeKey)`. Authorizing on the primary key alone therefore skipped the
|
|
476
|
+
* persisted-scope guard on exactly the path that can replace an existing
|
|
477
|
+
* row's contents.
|
|
478
|
+
*/
|
|
479
|
+
async getPersistedIdentity() {
|
|
480
|
+
const existing = await this.getPersistedRowById() ?? await this.getPersistedRowByNaturalKey();
|
|
481
|
+
if (!existing) return null;
|
|
482
|
+
const row = existing;
|
|
483
|
+
const read = (camel, snake, fallback) => {
|
|
484
|
+
if (row[camel] !== void 0 && row[camel] !== null) return String(row[camel]);
|
|
485
|
+
if (row[snake] !== void 0 && row[snake] !== null) return String(row[snake]);
|
|
486
|
+
return fallback;
|
|
487
|
+
};
|
|
488
|
+
const readNullable = (camel, snake) => {
|
|
489
|
+
const value = row[camel] !== void 0 ? row[camel] : row[snake];
|
|
490
|
+
return value === void 0 || value === null ? null : String(value);
|
|
491
|
+
};
|
|
492
|
+
return {
|
|
493
|
+
objectRef: read("objectRef", "object_ref", this.objectRef),
|
|
494
|
+
fieldName: read("fieldName", "field_name", this.fieldName),
|
|
495
|
+
scopeType: read("scopeType", "scope_type", this.scopeType),
|
|
496
|
+
scopeKey: read("scopeKey", "scope_key", this.scopeKey),
|
|
497
|
+
tenantId: readNullable("tenantId", "tenant_id"),
|
|
498
|
+
userId: readNullable("userId", "user_id")
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
async getPersistedRowById() {
|
|
502
|
+
if (!this.id) return null;
|
|
503
|
+
return await this.db.get(this.tableName, { id: this.id }) ?? null;
|
|
504
|
+
}
|
|
505
|
+
/**
|
|
506
|
+
* The row occupying this row's `conflictColumns` tuple, if any. Column
|
|
507
|
+
* names are the physical snake_case ones: this reads the driver directly
|
|
508
|
+
* rather than through a collection (the model has no collection handle).
|
|
509
|
+
*/
|
|
510
|
+
async getPersistedRowByNaturalKey() {
|
|
511
|
+
if (!this.objectRef || !this.fieldName) return null;
|
|
512
|
+
return await this.db.get(this.tableName, {
|
|
513
|
+
object_ref: this.objectRef,
|
|
514
|
+
field_name: this.fieldName,
|
|
515
|
+
scope_type: this.scopeType,
|
|
516
|
+
scope_key: this.computeScopeKey()
|
|
517
|
+
}) ?? null;
|
|
518
|
+
}
|
|
519
|
+
normalizeDefaultValueForPersistence() {
|
|
520
|
+
const raw = this.defaultValue;
|
|
521
|
+
if (raw === null || raw === void 0) {
|
|
522
|
+
this.defaultValue = null;
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
if (typeof raw === "string") {
|
|
526
|
+
this.defaultValue = raw;
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
this.defaultValue = JSON.stringify(raw);
|
|
530
|
+
}
|
|
531
|
+
};
|
|
532
|
+
__decorateClass$1([field({ required: true })], FieldPolicy.prototype, "objectRef", 2);
|
|
533
|
+
__decorateClass$1([field({ required: true })], FieldPolicy.prototype, "fieldName", 2);
|
|
534
|
+
__decorateClass$1([field({ required: true })], FieldPolicy.prototype, "scopeType", 2);
|
|
535
|
+
__decorateClass$1([tenantId({ nullable: true })], FieldPolicy.prototype, "tenantId", 2);
|
|
536
|
+
__decorateClass$1([crossPackageRef("@happyvertical/smrt-users:User", { nullable: true })], FieldPolicy.prototype, "userId", 2);
|
|
537
|
+
__decorateClass$1([field({
|
|
538
|
+
type: "text",
|
|
539
|
+
required: true
|
|
540
|
+
})], FieldPolicy.prototype, "scopeKey", 2);
|
|
541
|
+
__decorateClass$1([field({
|
|
542
|
+
type: "text",
|
|
543
|
+
nullable: true
|
|
544
|
+
})], FieldPolicy.prototype, "defaultValue", 2);
|
|
545
|
+
__decorateClass$1([field({
|
|
546
|
+
type: "text",
|
|
547
|
+
nullable: true
|
|
548
|
+
})], FieldPolicy.prototype, "visibility", 2);
|
|
549
|
+
__decorateClass$1([field({
|
|
550
|
+
type: "text",
|
|
551
|
+
nullable: true
|
|
552
|
+
})], FieldPolicy.prototype, "help", 2);
|
|
553
|
+
__decorateClass$1([field({
|
|
554
|
+
type: "text",
|
|
555
|
+
nullable: true
|
|
556
|
+
})], FieldPolicy.prototype, "label", 2);
|
|
557
|
+
__decorateClass$1([field({
|
|
558
|
+
type: "integer",
|
|
559
|
+
nullable: true
|
|
560
|
+
})], FieldPolicy.prototype, "displayOrder", 2);
|
|
561
|
+
__decorateClass$1([field({
|
|
562
|
+
type: "boolean",
|
|
563
|
+
nullable: true
|
|
564
|
+
})], FieldPolicy.prototype, "locked", 2);
|
|
565
|
+
__decorateClass$1([crossPackageRef("@happyvertical/smrt-users:User", { nullable: true })], FieldPolicy.prototype, "updatedBy", 2);
|
|
566
|
+
FieldPolicy = __decorateClass$1([smrt({
|
|
567
|
+
tableName: "_smrt_field_policies",
|
|
568
|
+
conflictColumns: [
|
|
569
|
+
"object_ref",
|
|
570
|
+
"field_name",
|
|
571
|
+
"scope_type",
|
|
572
|
+
"scope_key"
|
|
573
|
+
],
|
|
574
|
+
api: { include: [
|
|
575
|
+
"create",
|
|
576
|
+
"update",
|
|
577
|
+
"delete"
|
|
578
|
+
] },
|
|
579
|
+
cli: {
|
|
580
|
+
include: [
|
|
581
|
+
"create",
|
|
582
|
+
"update",
|
|
583
|
+
"delete"
|
|
584
|
+
],
|
|
585
|
+
exclude: ["getDefaultValue", "setDefaultValue"]
|
|
586
|
+
},
|
|
587
|
+
mcp: { include: [] }
|
|
588
|
+
})], FieldPolicy);
|
|
589
|
+
//#endregion
|
|
590
|
+
//#region src/collections/FieldPolicyCollection.ts
|
|
591
|
+
var __defProp = Object.defineProperty;
|
|
592
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
593
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, {
|
|
594
|
+
enumerable: true,
|
|
595
|
+
configurable: true,
|
|
596
|
+
writable: true,
|
|
597
|
+
value
|
|
598
|
+
}) : obj[key] = value;
|
|
599
|
+
var __decorateClass = (decorators, target, key, kind) => {
|
|
600
|
+
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
|
|
601
|
+
for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (kind ? decorator(target, key, result) : decorator(result)) || result;
|
|
602
|
+
if (kind && result) __defProp(target, key, result);
|
|
603
|
+
return result;
|
|
604
|
+
};
|
|
605
|
+
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
606
|
+
var MAX_BATCH_OBJECT_REFS = 100;
|
|
607
|
+
var FieldPolicyCollection = class extends SmrtCollection {
|
|
608
|
+
/** All app-scope rows for an object, keyed by field name. */
|
|
609
|
+
async getAppRows(objectRef) {
|
|
610
|
+
const rows = await this.list({ where: {
|
|
611
|
+
objectRef,
|
|
612
|
+
scopeType: "app"
|
|
613
|
+
} });
|
|
614
|
+
const byField = /* @__PURE__ */ new Map();
|
|
615
|
+
for (const row of rows) byField.set(row.fieldName, row);
|
|
616
|
+
return byField;
|
|
617
|
+
}
|
|
618
|
+
/**
|
|
619
|
+
* All tenant-scope rows for an object across a tenant chain, keyed
|
|
620
|
+
* `tenantId → fieldName → row`.
|
|
621
|
+
*/
|
|
622
|
+
async getTenantRows(objectRef, tenantIds) {
|
|
623
|
+
const byTenant = /* @__PURE__ */ new Map();
|
|
624
|
+
if (tenantIds.length === 0) return byTenant;
|
|
625
|
+
const rows = await this.list({ where: {
|
|
626
|
+
objectRef,
|
|
627
|
+
scopeType: "tenant",
|
|
628
|
+
"tenantId in": tenantIds
|
|
629
|
+
} });
|
|
630
|
+
for (const row of rows) {
|
|
631
|
+
if (!row.tenantId) continue;
|
|
632
|
+
let byField = byTenant.get(row.tenantId);
|
|
633
|
+
if (!byField) {
|
|
634
|
+
byField = /* @__PURE__ */ new Map();
|
|
635
|
+
byTenant.set(row.tenantId, byField);
|
|
636
|
+
}
|
|
637
|
+
byField.set(row.fieldName, row);
|
|
638
|
+
}
|
|
639
|
+
return byTenant;
|
|
640
|
+
}
|
|
641
|
+
/** All user-scope rows for an object and user, keyed by field name. */
|
|
642
|
+
async getUserRows(objectRef, userId) {
|
|
643
|
+
const rows = await this.list({ where: {
|
|
644
|
+
objectRef,
|
|
645
|
+
scopeType: "user",
|
|
646
|
+
userId
|
|
647
|
+
} });
|
|
648
|
+
const byField = /* @__PURE__ */ new Map();
|
|
649
|
+
for (const row of rows) byField.set(row.fieldName, row);
|
|
650
|
+
return byField;
|
|
651
|
+
}
|
|
652
|
+
/**
|
|
653
|
+
* Resolve merged field policy for a set of objectRefs for the CURRENT
|
|
654
|
+
* caller, gated for public consumption.
|
|
655
|
+
*
|
|
656
|
+
* Caller identity comes exclusively from the ambient tenant context
|
|
657
|
+
* (established by the app's auth hook, e.g. smrt-users' session context) —
|
|
658
|
+
* the request body cannot select another tenant or user (fail closed).
|
|
659
|
+
* Server-side consumers wanting explicit identities call
|
|
660
|
+
* `resolveFieldPolicy()` directly instead.
|
|
661
|
+
*
|
|
662
|
+
* Field gating mirrors the REST serializer's derivation (`sensitive` /
|
|
663
|
+
* `readPermission` read from both the top level and `_meta`): sensitive and
|
|
664
|
+
* read-permission-gated fields are ABSENT from the response for every
|
|
665
|
+
* caller (the generated action route cannot convey per-caller grants to
|
|
666
|
+
* this method, so gated fields fail closed), and transient fields are
|
|
667
|
+
* stripped for parity with generated client field definitions.
|
|
668
|
+
*/
|
|
669
|
+
async resolveBatch(options = {}) {
|
|
670
|
+
const rawRefs = options.objectRefs;
|
|
671
|
+
if (!Array.isArray(rawRefs) || rawRefs.length === 0) throw new Error("resolveBatch requires a non-empty \"objectRefs\" string array");
|
|
672
|
+
if (rawRefs.some((ref) => typeof ref !== "string" || ref.trim() === "")) throw new Error("resolveBatch objectRefs must be non-empty strings");
|
|
673
|
+
const objectRefs = [...new Set(rawRefs)];
|
|
674
|
+
if (objectRefs.length > MAX_BATCH_OBJECT_REFS) throw new Error(`resolveBatch accepts at most ${MAX_BATCH_OBJECT_REFS} objectRefs per call (got ${objectRefs.length})`);
|
|
675
|
+
const context = getCurrentTenant();
|
|
676
|
+
const tenantId = context?.tenantId ?? null;
|
|
677
|
+
const userId = context?.userId ?? null;
|
|
678
|
+
const { resolveFieldPolicy } = await Promise.resolve().then(() => field_policy_resolver_exports);
|
|
679
|
+
const policies = {};
|
|
680
|
+
for (const objectRef of objectRefs) {
|
|
681
|
+
const resolved = await resolveFieldPolicy(objectRef, {
|
|
682
|
+
tenantId,
|
|
683
|
+
userId,
|
|
684
|
+
db: this.db
|
|
685
|
+
});
|
|
686
|
+
policies[objectRef] = await this.gateResolvedForPublicResponse(resolved);
|
|
687
|
+
}
|
|
688
|
+
return { policies };
|
|
689
|
+
}
|
|
690
|
+
async gateResolvedForPublicResponse(resolved) {
|
|
691
|
+
const fieldMap = await getObjectFieldMap(resolved.objectRef);
|
|
692
|
+
const fields = {};
|
|
693
|
+
for (const [fieldName, policy] of Object.entries(resolved.fields)) {
|
|
694
|
+
const fieldDef = fieldMap.get(fieldName);
|
|
695
|
+
if (!fieldDef) continue;
|
|
696
|
+
if (isSensitiveField(fieldDef) || getFieldReadPermission(fieldDef) !== void 0 || isTransientField(fieldDef)) continue;
|
|
697
|
+
fields[fieldName] = policy;
|
|
698
|
+
}
|
|
699
|
+
return {
|
|
700
|
+
objectRef: resolved.objectRef,
|
|
701
|
+
fields
|
|
702
|
+
};
|
|
703
|
+
}
|
|
704
|
+
};
|
|
705
|
+
__publicField(FieldPolicyCollection, "_itemClass", FieldPolicy);
|
|
706
|
+
FieldPolicyCollection = __decorateClass([smrt({
|
|
707
|
+
conflictColumns: [
|
|
708
|
+
"object_ref",
|
|
709
|
+
"field_name",
|
|
710
|
+
"scope_type",
|
|
711
|
+
"scope_key"
|
|
712
|
+
],
|
|
713
|
+
api: {
|
|
714
|
+
include: [
|
|
715
|
+
"create",
|
|
716
|
+
"update",
|
|
717
|
+
"delete",
|
|
718
|
+
"resolveBatch"
|
|
719
|
+
],
|
|
720
|
+
routes: { resolveBatch: {
|
|
721
|
+
scope: "collection",
|
|
722
|
+
method: "POST",
|
|
723
|
+
path: "resolve"
|
|
724
|
+
} }
|
|
725
|
+
},
|
|
726
|
+
cli: false,
|
|
727
|
+
mcp: false
|
|
728
|
+
})], FieldPolicyCollection);
|
|
729
|
+
//#endregion
|
|
730
|
+
//#region src/field-policy-resolver.ts
|
|
731
|
+
var field_policy_resolver_exports = /* @__PURE__ */ __exportAll({
|
|
732
|
+
isMissingUsersDependency: () => isMissingUsersDependency,
|
|
733
|
+
resolveFieldPolicy: () => resolveFieldPolicy,
|
|
734
|
+
resolveFieldPolicyExplained: () => resolveFieldPolicyExplained
|
|
735
|
+
});
|
|
736
|
+
async function resolveFieldPolicy(objectRef, options = {}) {
|
|
737
|
+
const explained = await resolveFieldPolicyExplained(objectRef, options);
|
|
738
|
+
return {
|
|
739
|
+
objectRef: explained.objectRef,
|
|
740
|
+
fields: explained.fields
|
|
741
|
+
};
|
|
742
|
+
}
|
|
743
|
+
async function resolveFieldPolicyExplained(objectRef, options = {}) {
|
|
744
|
+
const tenantId = options.tenantId ?? null;
|
|
745
|
+
const userId = options.userId ?? null;
|
|
746
|
+
assertResolutionAllowedInContext(tenantId, userId);
|
|
747
|
+
let collection = null;
|
|
748
|
+
let cacheDb = options.db;
|
|
749
|
+
if (options.db) {
|
|
750
|
+
collection = await FieldPolicyCollection.create({ db: options.db });
|
|
751
|
+
cacheDb = collection.db;
|
|
752
|
+
}
|
|
753
|
+
const fieldMap = await getObjectFieldMap(objectRef);
|
|
754
|
+
const cached = getCachedFieldPolicy(objectRef, tenantId, userId, cacheDb, options.tenantHierarchyLoader);
|
|
755
|
+
if (cached) return cached;
|
|
756
|
+
const policyFields = selectPolicyAddressableFields(fieldMap);
|
|
757
|
+
const codeVisibility = buildCodeSeedVisibility(fieldMap);
|
|
758
|
+
const appRows = collection ? await collection.getAppRows(objectRef) : /* @__PURE__ */ new Map();
|
|
759
|
+
let survivingChain = [];
|
|
760
|
+
let tenantRows = /* @__PURE__ */ new Map();
|
|
761
|
+
if (collection && tenantId) {
|
|
762
|
+
survivingChain = selectSurvivingChainSuffix(await resolveTenantChain(tenantId, options));
|
|
763
|
+
tenantRows = await collection.getTenantRows(objectRef, survivingChain.map((node) => node.id));
|
|
764
|
+
}
|
|
765
|
+
const userRows = collection && userId ? await collection.getUserRows(objectRef, userId) : /* @__PURE__ */ new Map();
|
|
766
|
+
const fields = {};
|
|
767
|
+
const layers = {};
|
|
768
|
+
for (const [fieldName, fieldDef] of policyFields) {
|
|
769
|
+
const contributions = [];
|
|
770
|
+
const codeDelta = buildCodeSeedDelta(fieldDef, codeVisibility.get(fieldName) ?? "basic");
|
|
771
|
+
contributions.push({
|
|
772
|
+
layer: "code",
|
|
773
|
+
delta: codeDelta
|
|
774
|
+
});
|
|
775
|
+
let state = applyDelta({ visibility: "basic" }, codeDelta);
|
|
776
|
+
const appRow = appRows.get(fieldName);
|
|
777
|
+
if (appRow) {
|
|
778
|
+
const delta = rowToDelta(appRow);
|
|
779
|
+
contributions.push({
|
|
780
|
+
layer: "app",
|
|
781
|
+
delta
|
|
782
|
+
});
|
|
783
|
+
state = applyDelta(state, delta);
|
|
784
|
+
}
|
|
785
|
+
for (const node of survivingChain) {
|
|
786
|
+
const row = tenantRows.get(node.id)?.get(fieldName);
|
|
787
|
+
if (row) {
|
|
788
|
+
const delta = rowToDelta(row);
|
|
789
|
+
contributions.push({
|
|
790
|
+
layer: "tenant",
|
|
791
|
+
tenantId: node.id,
|
|
792
|
+
delta
|
|
793
|
+
});
|
|
794
|
+
state = applyDelta(state, delta);
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
const orgLocked = state.locked === true;
|
|
798
|
+
const userRow = userId ? userRows.get(fieldName) : void 0;
|
|
799
|
+
if (userRow && !orgLocked) {
|
|
800
|
+
const delta = rowToDelta(userRow);
|
|
801
|
+
contributions.push({
|
|
802
|
+
layer: "user",
|
|
803
|
+
userId,
|
|
804
|
+
delta
|
|
805
|
+
});
|
|
806
|
+
state = applyDelta(state, delta);
|
|
807
|
+
}
|
|
808
|
+
const required = isRequiredField(fieldDef);
|
|
809
|
+
let visibilityForced = false;
|
|
810
|
+
if (required && !isUsableRequiredDefault(state.default) && state.visibility !== "basic") {
|
|
811
|
+
state = {
|
|
812
|
+
...state,
|
|
813
|
+
visibility: "basic"
|
|
814
|
+
};
|
|
815
|
+
visibilityForced = true;
|
|
816
|
+
}
|
|
817
|
+
fields[fieldName] = {
|
|
818
|
+
fieldName,
|
|
819
|
+
hasDefault: state.default !== void 0,
|
|
820
|
+
defaultValue: state.default?.value,
|
|
821
|
+
visibility: state.visibility,
|
|
822
|
+
help: state.help ?? null,
|
|
823
|
+
label: state.label ?? null,
|
|
824
|
+
order: state.order ?? null,
|
|
825
|
+
group: getCodeSeedGroup(fieldDef),
|
|
826
|
+
locked: state.locked === true,
|
|
827
|
+
required,
|
|
828
|
+
...visibilityForced ? { visibilityForced: true } : {}
|
|
829
|
+
};
|
|
830
|
+
layers[fieldName] = contributions;
|
|
831
|
+
}
|
|
832
|
+
const explained = {
|
|
833
|
+
objectRef,
|
|
834
|
+
fields,
|
|
835
|
+
layers
|
|
836
|
+
};
|
|
837
|
+
setCachedFieldPolicy(objectRef, tenantId, userId, cacheDb, explained, options.tenantHierarchyLoader);
|
|
838
|
+
return explained;
|
|
839
|
+
}
|
|
840
|
+
function assertResolutionAllowedInContext(tenantId, userId) {
|
|
841
|
+
if (tenantId) assertTenantReadAllowed(tenantId, "resolveFieldPolicy");
|
|
842
|
+
if (!userId) return;
|
|
843
|
+
const context = getCurrentTenant();
|
|
844
|
+
if (!context || isSuperAdminBypass()) return;
|
|
845
|
+
if (context.userId === void 0) throw new TenantIsolationError(`Tenant isolation violation in resolveFieldPolicy: the ambient context carries no user id, so user-scope resolution for '${userId}' is not attributable`, { tenantId: context.tenantId });
|
|
846
|
+
if (context.userId !== userId) throw new TenantIsolationError(`Tenant isolation violation in resolveFieldPolicy: context user is '${context.userId}' but resolution requested '${userId}'`, { tenantId: context.tenantId });
|
|
847
|
+
}
|
|
848
|
+
function selectPolicyAddressableFields(fieldMap) {
|
|
849
|
+
const selected = /* @__PURE__ */ new Map();
|
|
850
|
+
for (const [name, field] of fieldMap) {
|
|
851
|
+
if (field._meta?.__smrtSystemField === true) continue;
|
|
852
|
+
if (field.type === "oneToMany" || field.type === "manyToMany" || field.type === "meta") continue;
|
|
853
|
+
selected.set(name, field);
|
|
854
|
+
}
|
|
855
|
+
return selected;
|
|
856
|
+
}
|
|
857
|
+
function rowToDelta(row) {
|
|
858
|
+
const delta = {};
|
|
859
|
+
if (row.defaultValue !== null && row.defaultValue !== void 0) try {
|
|
860
|
+
delta.default = { value: JSON.parse(row.defaultValue) };
|
|
861
|
+
} catch {}
|
|
862
|
+
if (row.visibility !== null && row.visibility !== void 0) delta.visibility = row.visibility;
|
|
863
|
+
if (row.help !== null && row.help !== void 0) delta.help = row.help;
|
|
864
|
+
if (row.label !== null && row.label !== void 0) delta.label = row.label;
|
|
865
|
+
if (row.displayOrder !== null && row.displayOrder !== void 0) delta.order = row.displayOrder;
|
|
866
|
+
if (row.locked !== null && row.locked !== void 0) delta.locked = row.locked;
|
|
867
|
+
return delta;
|
|
868
|
+
}
|
|
869
|
+
function applyDelta(state, delta) {
|
|
870
|
+
return {
|
|
871
|
+
default: delta.default ?? state.default,
|
|
872
|
+
visibility: delta.visibility ?? state.visibility,
|
|
873
|
+
help: delta.help ?? state.help,
|
|
874
|
+
label: delta.label ?? state.label,
|
|
875
|
+
order: delta.order ?? state.order,
|
|
876
|
+
locked: delta.locked ?? state.locked
|
|
877
|
+
};
|
|
878
|
+
}
|
|
879
|
+
function selectSurvivingChainSuffix(chain) {
|
|
880
|
+
let survivingStart = 0;
|
|
881
|
+
for (let index = 1; index < chain.length; index++) if (!(chain[index - 1].cascadePermissions && chain[index].inheritPermissions)) survivingStart = index;
|
|
882
|
+
return chain.slice(survivingStart);
|
|
883
|
+
}
|
|
884
|
+
async function resolveTenantChain(tenantId, options) {
|
|
885
|
+
const provider = await (options.tenantHierarchyLoader || defaultTenantHierarchyLoader)({ db: options.db });
|
|
886
|
+
if (provider) {
|
|
887
|
+
const chain = await provider.getChain(tenantId);
|
|
888
|
+
if (chain.length > 0) return chain;
|
|
889
|
+
}
|
|
890
|
+
return [{
|
|
891
|
+
id: tenantId,
|
|
892
|
+
inheritPermissions: true,
|
|
893
|
+
cascadePermissions: true
|
|
894
|
+
}];
|
|
895
|
+
}
|
|
896
|
+
async function defaultTenantHierarchyLoader(options) {
|
|
897
|
+
try {
|
|
898
|
+
const tenantCollection = await (await importWorkspaceModule({
|
|
899
|
+
packageName: "@happyvertical/smrt-users",
|
|
900
|
+
sourceEntry: "packages/users/src/collections/index.ts",
|
|
901
|
+
purpose: "tenant-aware field policy resolution"
|
|
902
|
+
})).TenantCollection.create(options);
|
|
903
|
+
return { async getChain(tenantId) {
|
|
904
|
+
const tenant = await tenantCollection.get({ id: tenantId });
|
|
905
|
+
if (!tenant) return [];
|
|
906
|
+
return [...await tenantCollection.getAncestorsFromRoot(tenantId), tenant].map((node) => ({
|
|
907
|
+
id: String(node.id),
|
|
908
|
+
inheritPermissions: Boolean(node.inheritPermissions),
|
|
909
|
+
cascadePermissions: Boolean(node.cascadePermissions)
|
|
910
|
+
}));
|
|
911
|
+
} };
|
|
912
|
+
} catch (error) {
|
|
913
|
+
if (isMissingUsersDependency(error)) return null;
|
|
914
|
+
throw error;
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
var MISSING_MODULE_TARGET_PATTERN = /Cannot find (?:package|module) '([^']+)'/;
|
|
918
|
+
function isUsersSpecifier(target) {
|
|
919
|
+
return target === "@happyvertical/smrt-users" || target.startsWith("@happyvertical/smrt-users/");
|
|
920
|
+
}
|
|
921
|
+
function isMissingUsersDependency(error) {
|
|
922
|
+
let current = error;
|
|
923
|
+
const seen = /* @__PURE__ */ new Set();
|
|
924
|
+
while (current instanceof Error && !seen.has(current)) {
|
|
925
|
+
seen.add(current);
|
|
926
|
+
const match = current.message.match(MISSING_MODULE_TARGET_PATTERN);
|
|
927
|
+
if (match && isUsersSpecifier(match[1])) return true;
|
|
928
|
+
if (current.message.includes("Failed to load @happyvertical/smrt-users for")) return true;
|
|
929
|
+
current = current.cause;
|
|
930
|
+
}
|
|
931
|
+
return false;
|
|
932
|
+
}
|
|
933
|
+
//#endregion
|
|
934
|
+
export { APP_FIELD_POLICY_SCOPE_KEY, FIELD_POLICY_SCOPE_TYPES, FIELD_POLICY_VISIBILITIES, FieldPolicy, FieldPolicyCollection, assertDefaultValueMatchesFieldType, buildCodeSeedDelta, buildCodeSeedVisibility, clearFieldPolicyCache, getCodeDefault, getCodeSeedGroup, getFieldPolicyCacheTtlMs, getFieldReadPermission, getObjectFieldMap, invalidateFieldPolicyCache, isRequiredField, isSensitiveField, isTransientField, isUsableRequiredDefault, requireRegisteredObject, resolveFieldPolicy, resolveFieldPolicyExplained, sanitizeFieldUIHints };
|
|
935
|
+
|
|
936
|
+
//# sourceMappingURL=index.js.map
|