@memberjunction/core 6.1.0-edge.5 → 6.1.0-edge.7

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 (98) hide show
  1. package/dist/generic/JSONType-interfaces/IEntitySubtypeSelectorConfig.d.ts +27 -0
  2. package/dist/generic/JSONType-interfaces/IEntitySubtypeSelectorConfig.d.ts.map +1 -0
  3. package/dist/generic/JSONType-interfaces/IEntitySubtypeSelectorConfig.js +6 -0
  4. package/dist/generic/JSONType-interfaces/IEntitySubtypeSelectorConfig.js.map +1 -0
  5. package/dist/generic/baseEngine.d.ts +52 -1
  6. package/dist/generic/baseEngine.d.ts.map +1 -1
  7. package/dist/generic/baseEngine.js +95 -7
  8. package/dist/generic/baseEngine.js.map +1 -1
  9. package/dist/generic/baseEntity.d.ts +292 -0
  10. package/dist/generic/baseEntity.d.ts.map +1 -1
  11. package/dist/generic/baseEntity.js +878 -98
  12. package/dist/generic/baseEntity.js.map +1 -1
  13. package/dist/generic/baseInfo.d.ts +6 -0
  14. package/dist/generic/baseInfo.d.ts.map +1 -1
  15. package/dist/generic/baseInfo.js +19 -1
  16. package/dist/generic/baseInfo.js.map +1 -1
  17. package/dist/generic/compositeKey.d.ts +108 -0
  18. package/dist/generic/compositeKey.d.ts.map +1 -1
  19. package/dist/generic/compositeKey.js +225 -10
  20. package/dist/generic/compositeKey.js.map +1 -1
  21. package/dist/generic/databaseProviderBase.d.ts +49 -0
  22. package/dist/generic/databaseProviderBase.d.ts.map +1 -1
  23. package/dist/generic/databaseProviderBase.js +90 -11
  24. package/dist/generic/databaseProviderBase.js.map +1 -1
  25. package/dist/generic/embeddedRecord.d.ts +2 -1
  26. package/dist/generic/embeddedRecord.d.ts.map +1 -1
  27. package/dist/generic/embeddedRecord.js +9 -2
  28. package/dist/generic/embeddedRecord.js.map +1 -1
  29. package/dist/generic/entityCompanion.d.ts +6 -1
  30. package/dist/generic/entityCompanion.d.ts.map +1 -1
  31. package/dist/generic/entityCompanion.js +7 -0
  32. package/dist/generic/entityCompanion.js.map +1 -1
  33. package/dist/generic/entityInfo.d.ts +550 -8
  34. package/dist/generic/entityInfo.d.ts.map +1 -1
  35. package/dist/generic/entityInfo.js +695 -20
  36. package/dist/generic/entityInfo.js.map +1 -1
  37. package/dist/generic/entitySubtypeResolver.d.ts +19 -0
  38. package/dist/generic/entitySubtypeResolver.d.ts.map +1 -0
  39. package/dist/generic/entitySubtypeResolver.js +24 -0
  40. package/dist/generic/entitySubtypeResolver.js.map +1 -0
  41. package/dist/generic/entityTransactionScope.d.ts +6 -3
  42. package/dist/generic/entityTransactionScope.d.ts.map +1 -1
  43. package/dist/generic/entityTransactionScope.js +6 -3
  44. package/dist/generic/entityTransactionScope.js.map +1 -1
  45. package/dist/generic/extendedTypeValue.d.ts +47 -0
  46. package/dist/generic/extendedTypeValue.d.ts.map +1 -0
  47. package/dist/generic/extendedTypeValue.js +152 -0
  48. package/dist/generic/extendedTypeValue.js.map +1 -0
  49. package/dist/generic/filters/compositeFilter.d.ts +77 -0
  50. package/dist/generic/filters/compositeFilter.d.ts.map +1 -0
  51. package/dist/generic/filters/compositeFilter.js +324 -0
  52. package/dist/generic/filters/compositeFilter.js.map +1 -0
  53. package/dist/generic/filters/filter.types.d.ts +29 -0
  54. package/dist/generic/filters/filter.types.d.ts.map +1 -0
  55. package/dist/generic/filters/filter.types.js +28 -0
  56. package/dist/generic/filters/filter.types.js.map +1 -0
  57. package/dist/generic/filters/index.d.ts +3 -0
  58. package/dist/generic/filters/index.d.ts.map +1 -0
  59. package/dist/generic/filters/index.js +3 -0
  60. package/dist/generic/filters/index.js.map +1 -0
  61. package/dist/generic/geoFields.d.ts +64 -0
  62. package/dist/generic/geoFields.d.ts.map +1 -0
  63. package/dist/generic/geoFields.js +108 -0
  64. package/dist/generic/geoFields.js.map +1 -0
  65. package/dist/generic/interfaces.d.ts +16 -2
  66. package/dist/generic/interfaces.d.ts.map +1 -1
  67. package/dist/generic/interfaces.js.map +1 -1
  68. package/dist/generic/localCacheManager.d.ts +34 -3
  69. package/dist/generic/localCacheManager.d.ts.map +1 -1
  70. package/dist/generic/localCacheManager.js +53 -4
  71. package/dist/generic/localCacheManager.js.map +1 -1
  72. package/dist/generic/providerBase.d.ts +282 -5
  73. package/dist/generic/providerBase.d.ts.map +1 -1
  74. package/dist/generic/providerBase.js +754 -93
  75. package/dist/generic/providerBase.js.map +1 -1
  76. package/dist/generic/recordChangeFieldSecurity.d.ts +164 -0
  77. package/dist/generic/recordChangeFieldSecurity.d.ts.map +1 -0
  78. package/dist/generic/recordChangeFieldSecurity.js +279 -0
  79. package/dist/generic/recordChangeFieldSecurity.js.map +1 -0
  80. package/dist/generic/relatedRecordBatchLoader.js +2 -2
  81. package/dist/generic/relatedRecordBatchLoader.js.map +1 -1
  82. package/dist/generic/relatedRecordCollection.d.ts +2 -1
  83. package/dist/generic/relatedRecordCollection.d.ts.map +1 -1
  84. package/dist/generic/relatedRecordCollection.js +16 -4
  85. package/dist/generic/relatedRecordCollection.js.map +1 -1
  86. package/dist/generic/saveEntityGraphOperation.d.ts +10 -0
  87. package/dist/generic/saveEntityGraphOperation.d.ts.map +1 -1
  88. package/dist/generic/saveEntityGraphOperation.js +2 -1
  89. package/dist/generic/saveEntityGraphOperation.js.map +1 -1
  90. package/dist/generic/wellKnownUserSource.d.ts +70 -0
  91. package/dist/generic/wellKnownUserSource.d.ts.map +1 -0
  92. package/dist/generic/wellKnownUserSource.js +82 -0
  93. package/dist/generic/wellKnownUserSource.js.map +1 -0
  94. package/dist/index.d.ts +7 -0
  95. package/dist/index.d.ts.map +1 -1
  96. package/dist/index.js +7 -0
  97. package/dist/index.js.map +1 -1
  98. package/package.json +3 -3
@@ -4,11 +4,20 @@ import { BaseEntity } from "./baseEntity.js";
4
4
  import { RowLevelSecurityFilterInfo, UserInfo } from "./securityInfo.js";
5
5
  import { CompositeKey } from "./compositeKey.js";
6
6
  import { type IEntityConfiguration, type IEntityRelationshipConfiguration, type IEntityFieldConfiguration } from "./entityConfiguration.js";
7
+ import type { IEntitySubtypeSelectorConfig } from "./JSONType-interfaces/IEntitySubtypeSelectorConfig.js";
7
8
  /**
8
- * Valid values for EntityField.ExtendedType.
9
- * Defines semantic meaning beyond the SQL data type (e.g., a string field that holds an email, URL, or geo address).
9
+ * Runtime domain for {@link EntityFieldInfo.ExtendedType}. This array is the single source of
10
+ * truth; {@link EntityFieldExtendedType} is derived from it. CodeGen validates LLM suggestions
11
+ * against {@link EntityFieldInfo.ExtendedTypes} rather than duplicating the list.
12
+ *
13
+ * `Image` — the value is an image URL, a `data:image/...` URI, or raw image base64. UI surfaces
14
+ * render a thumbnail and (in edit mode) allow replacing it with an upload capped at the field's
15
+ * MaxLength.
16
+ * `Color` — the value is a CSS color (hex / rgb / hsl).
17
+ * `JSON` — the value is a JSON document; validated on save and pretty-printed in forms.
10
18
  */
11
- export type EntityFieldExtendedType = 'Code' | 'Email' | 'FaceTime' | 'Geo' | 'GeoLatitude' | 'GeoLongitude' | 'GeoCountry' | 'GeoStateProvince' | 'GeoCity' | 'GeoPostalCode' | 'GeoAddress' | 'HTML' | 'Icon' | 'Markdown' | 'MSTeams' | 'Other' | 'SIP' | 'SMS' | 'Skype' | 'Tel' | 'URL' | 'WhatsApp' | 'ZoomMtg';
19
+ export declare const EntityFieldExtendedTypes: readonly ["Code", "Color", "Email", "FaceTime", "Geo", "GeoLatitude", "GeoLongitude", "GeoCountry", "GeoStateProvince", "GeoCity", "GeoPostalCode", "GeoAddress", "HTML", "Icon", "Image", "JSON", "Markdown", "MSTeams", "Other", "SIP", "SMS", "Skype", "Tel", "URL", "WhatsApp", "ZoomMtg"];
20
+ export type EntityFieldExtendedType = typeof EntityFieldExtendedTypes[number];
12
21
  /**
13
22
  * The possible status values for a record change
14
23
  */
@@ -256,6 +265,13 @@ export declare class EntityPermissionInfo extends BaseInfo {
256
265
  * materialized before the column existed.
257
266
  */
258
267
  Type: string;
268
+ /**
269
+ * True when this row is a Deny row (`Type = 'Deny'`, compared case- and whitespace-insensitively;
270
+ * a null/blank Type — rows created before the column existed — is Allow). On a Deny row a set
271
+ * `Can*` flag means "deny that operation", so nothing that reads a `Can*` flag as a GRANT may
272
+ * look at a Deny row: `GetUserPermisions` subtracts these, and the RLS readers skip them.
273
+ */
274
+ get IsDeny(): boolean;
259
275
  CanCreate: boolean;
260
276
  CanRead: boolean;
261
277
  CanUpdate: boolean;
@@ -280,6 +296,212 @@ export declare class EntityPermissionInfo extends BaseInfo {
280
296
  RLSFilter(type: EntityPermissionType): RowLevelSecurityFilterInfo;
281
297
  constructor(initData: any);
282
298
  }
299
+ /**
300
+ * The three states a single field-level permission verb can hold, modelled on SQL Server's
301
+ * posture:
302
+ *
303
+ * - `No Access` — neutral, and the default. Grants nothing and blocks nothing; another role's
304
+ * Allow still wins.
305
+ * - `Allow` — grants the action for this role.
306
+ * - `Deny` — trumps everything. One Deny across any of the user's roles wins no matter how
307
+ * many Allows sit beside it.
308
+ */
309
+ export declare const FieldPermissionAccess: {
310
+ readonly Allow: "Allow";
311
+ readonly Deny: "Deny";
312
+ readonly NoAccess: "No Access";
313
+ };
314
+ export type FieldPermissionAccess = typeof FieldPermissionAccess[keyof typeof FieldPermissionAccess];
315
+ /**
316
+ * The three trinary verbs of a single field-permission rule, on their own.
317
+ *
318
+ * Both `EntityFieldPermissionInfo` (metadata) and the generated `MJ: Entity Field Permissions`
319
+ * entity satisfy this structurally, so callers can ask about a stored rule or a prospective one
320
+ * without converting between the two.
321
+ */
322
+ export type FieldPermissionRuleVerbs = {
323
+ ReadAccess: FieldPermissionAccess;
324
+ UpdateAccess: FieldPermissionAccess;
325
+ CreateAccess: FieldPermissionAccess;
326
+ };
327
+ /**
328
+ * A field-permission rule together with the role it binds to — the minimum
329
+ * {@link EntityFieldInfo.AggregateFieldRulesForUser} needs to decide whether a rule applies.
330
+ *
331
+ * Structurally satisfied by `EntityFieldPermissionInfo` (metadata) and by the generated
332
+ * `MJ: Entity Field Permissions` entity alike, so a caller can aggregate stored rules, prospective
333
+ * ones, or a mix of both.
334
+ */
335
+ export type FieldPermissionRuleForRole = FieldPermissionRuleVerbs & {
336
+ RoleID: string;
337
+ };
338
+ /**
339
+ * The transport-only key carrying the server's authoritative answer to "which fields on this
340
+ * entity may the caller of THIS request read".
341
+ *
342
+ * **Why it lists READABLE fields rather than denied ones.** The two carry the same information
343
+ * only while the client already holds the full permission matrix, which it does today — the
344
+ * `MJ_Metadata` dataset ships `MJ: Entity Fields` and `MJ: Entity Field Permissions` unfiltered.
345
+ * That is scheduled to change (MJ issue #3485, metadata filtering for restricted users), and a
346
+ * payload that named DENIED fields would hand back exactly what such filtering exists to withhold:
347
+ * the names of columns you are not allowed to know about. A readable list names only fields the
348
+ * caller may already see, so it discloses nothing under any filtering design.
349
+ *
350
+ * **Why it is needed at all.** The server omits denied fields from the response object, but
351
+ * GraphQL emits every SELECTED field regardless — so a denied field the client asked for arrives
352
+ * as an explicit `null`, indistinguishable from a genuine one. The client cannot settle that from
353
+ * its own metadata: in the window after a permission change (and permanently, once metadata is
354
+ * filtered) the client's copy disagrees with the server's. This key is the server stating it
355
+ * in-band, for the request that actually ran.
356
+ *
357
+ * Suffixed `___` following the established transport-only convention (`OldValues___`,
358
+ * `RestoreContext___`) so it cannot collide with a real column name.
359
+ */
360
+ export declare const ReadableFieldsTransportKey = "ReadableFields___";
361
+ /**
362
+ * True when a rule takes access AWAY rather than granting or abstaining.
363
+ *
364
+ * `Deny` is the only restricting value *within a single rule*. `No Access` is the aggregation's
365
+ * identity element: it can leave a role without access, but it can never remove access one of the
366
+ * user's other roles granted.
367
+ *
368
+ * **This answers a question about one rule, not about a change.** Whether a CHANGE restricts a
369
+ * user is a property of the aggregate across every role they hold — setting each of a user's roles
370
+ * to `No Access` in turn writes no `Deny` anywhere and still ends with the field denied. So this
371
+ * is sound for an INSERT, which can only add rules and therefore cannot remove an existing
372
+ * `Allow`, and is NOT sufficient for an edit or a delete. Those must project the resulting rule
373
+ * set and aggregate it — see {@link EntityFieldInfo.AggregateFieldRulesForUser}.
374
+ */
375
+ export declare function IsRestrictingFieldRule(rule: FieldPermissionRuleVerbs | null | undefined): boolean;
376
+ /**
377
+ * The single wording for "you cannot use this field", modelled on SQL Server's posture of never
378
+ * disclosing whether an object is missing or merely inaccessible.
379
+ *
380
+ * Naming the field is safe — the caller supplied it, so it tells them nothing they did not
381
+ * already know. Naming the REASON is not: confirming "this field exists and is restricted"
382
+ * turns any predicate into an oracle for probing which columns a deployment considers
383
+ * sensitive. The ambiguity also stays correct after
384
+ * [#3485](https://github.com/MemberJunction/MJ/issues/3485) tiers metadata and restricted fields
385
+ * stop shipping to clients at all, at which point "does not exist" becomes literally true from
386
+ * the client's vantage point.
387
+ *
388
+ * Lives here rather than on `ProviderBase` so the write path in `BaseEntity` can reach it
389
+ * without importing the provider layer, which imports `BaseEntity` in turn.
390
+ */
391
+ export declare function FieldSecurityDenialMessage(fieldName: string, entityName: string): string;
392
+ /**
393
+ * The wording for "you may not WRITE this field" — used only when the caller can READ it.
394
+ *
395
+ * Naming the reason here discloses nothing. The caller can see the field and its values, so
396
+ * both facts the ambiguous wording withholds — that the column exists, and that it is
397
+ * restricted for them — are already theirs. All this adds is *which* permission is missing,
398
+ * which they would learn by trying anyway.
399
+ *
400
+ * The two justifications behind {@link FieldSecurityDenialMessage} do not reach this case:
401
+ * predicate probing is a question about columns the caller cannot READ, and the
402
+ * [#3485](https://github.com/MemberJunction/MJ/issues/3485) argument — that "does not exist"
403
+ * becomes literally true once restricted fields stop shipping to clients — is false for a
404
+ * readable field, which keeps shipping. Ambiguity there does not age into truth; it just tells
405
+ * someone that a field they are looking at might not exist.
406
+ *
407
+ * A field the caller cannot read must still use the ambiguous wording. That is not hypothetical:
408
+ * `SetMany` deliberately skips the readability assertion (it is the hydration and resolver-apply
409
+ * path), so server-side code can dirty a read-denied field and reach the update gate.
410
+ */
411
+ export declare function FieldSecurityWriteDenialMessage(fieldName: string, entityName: string): string;
412
+ /**
413
+ * The error thrown when field-level security refuses a request — a caller-authored predicate
414
+ * naming an unreadable field, a typed accessor touching one, or a save modifying a field the
415
+ * caller may not write.
416
+ *
417
+ * A DISTINCT class because its message is the one security rejection that is deliberately safe
418
+ * to show a caller: both {@link FieldSecurityDenialMessage} and
419
+ * {@link FieldSecurityWriteDenialMessage} were designed for exactly that surface and disclose
420
+ * nothing (see their docs). Transport layers that rightly swallow arbitrary resolver errors
421
+ * (whose messages can carry SQL text or internal state) recognize this one and let it through,
422
+ * so the intended wording reaches the wire instead of degenerating into a generic transport
423
+ * error.
424
+ *
425
+ * Recognize it by `name === FieldSecurityError.ErrorName` rather than `instanceof` where
426
+ * bundling might duplicate the class.
427
+ */
428
+ export declare class FieldSecurityError extends Error {
429
+ static readonly ErrorName = "FieldSecurityError";
430
+ /**
431
+ * Defaults to the ambiguous wording, which is correct for every READ denial. Pass `message`
432
+ * only through a named factory such as {@link FieldSecurityError.WriteDenial}, so the choice
433
+ * of wording is always a deliberate, reviewable decision rather than an inline string.
434
+ */
435
+ constructor(fieldName: string, entityName: string, message?: string);
436
+ /**
437
+ * A write refusal on a field the caller CAN read — names the missing permission instead of
438
+ * hiding behind "or it does not exist", which would be actively misleading about a field
439
+ * whose values they are looking at. Callers must confirm readability first; see
440
+ * {@link FieldSecurityWriteDenialMessage}.
441
+ */
442
+ static WriteDenial(fieldName: string, entityName: string): FieldSecurityError;
443
+ }
444
+ /**
445
+ * Field-level (column-level) security settings. Maps an entity FIELD to a role, carrying three
446
+ * independent trinary verbs — Read, Update and Create. One row per (field, role).
447
+ *
448
+ * These rows are only consulted when the parent entity has
449
+ * {@link EntityInfo.EnableFieldLevelSecurity} set. Aggregation across the roles a user holds,
450
+ * per verb: `effective = (any matching row Allows) AND NOT (any matching row Denies)`. See
451
+ * {@link EntityFieldInfo.GetUserFieldPermissions}.
452
+ */
453
+ export declare class EntityFieldPermissionInfo extends BaseInfo {
454
+ ID: string;
455
+ EntityFieldID: string;
456
+ RoleID: string;
457
+ /**
458
+ * Whether this role may READ the field's values. The aggregation normalizes defensively and
459
+ * treats anything unrecognized as `No Access`, so a bad value off the wire fails closed
460
+ * rather than granting.
461
+ */
462
+ ReadAccess: FieldPermissionAccess;
463
+ /**
464
+ * Whether this role may modify the field's value on an EXISTING record.
465
+ *
466
+ * Requires {@link ReadAccess} = Allow — a field a user cannot see is one they cannot
467
+ * change. Enforced per row by a CHECK constraint, and again after aggregation because the
468
+ * constraint cannot see across roles.
469
+ */
470
+ UpdateAccess: FieldPermissionAccess;
471
+ /**
472
+ * Whether this role may supply the field's value when INSERTing a record. Requires
473
+ * {@link ReadAccess} = Allow, on the same two-layer basis as {@link UpdateAccess}.
474
+ *
475
+ * A user who may not create a field does not get an error — the supplied value is dropped
476
+ * and the column takes its default, matching the read path where a denied field is simply
477
+ * absent.
478
+ */
479
+ CreateAccess: FieldPermissionAccess;
480
+ __mj_CreatedAt: Date;
481
+ __mj_UpdatedAt: Date;
482
+ EntityField: string;
483
+ Role: string;
484
+ /**
485
+ * @param initData raw metadata row off the wire. `BaseInfo.copyInitData` only ever reads
486
+ * `Object.keys()` off it, so a plain record is wide enough.
487
+ */
488
+ constructor(initData?: Record<string, unknown> | null);
489
+ }
490
+ /**
491
+ * The effective field-level access a specific user has to a specific field, after trinary
492
+ * aggregation across all of the roles that user holds.
493
+ *
494
+ * Narrower than {@link EntityUserPermissionInfo}: Delete has no field-level meaning — you
495
+ * delete rows, not columns.
496
+ *
497
+ * The three flags are not independent on the way out. Read is required for Update and Create,
498
+ * so a `false` CanRead always arrives with `false` CanUpdate and CanCreate.
499
+ */
500
+ export type EntityFieldUserPermissionInfo = {
501
+ CanRead: boolean;
502
+ CanUpdate: boolean;
503
+ CanCreate: boolean;
504
+ };
283
505
  export declare const EntityFieldTSType: {
284
506
  readonly String: "string";
285
507
  readonly Number: "number";
@@ -424,6 +646,10 @@ export declare class EntityFieldInfo extends BaseInfo {
424
646
  DefaultValue: string;
425
647
  AutoIncrement: boolean;
426
648
  ValueListType: string;
649
+ /**
650
+ * Runtime domain for {@link ExtendedType}. Same array as {@link EntityFieldExtendedTypes}.
651
+ */
652
+ static readonly ExtendedTypes: readonly EntityFieldExtendedType[];
427
653
  ExtendedType: EntityFieldExtendedType | null;
428
654
  DefaultInView: boolean;
429
655
  ViewCellTemplate: string;
@@ -774,6 +1000,128 @@ export declare class EntityFieldInfo extends BaseInfo {
774
1000
  */
775
1001
  get RelatedEntityJoinFieldsConfig(): RelatedEntityJoinFieldConfig | null;
776
1002
  get EntityFieldValues(): EntityFieldValueInfo[];
1003
+ private _FieldPermissions;
1004
+ /**
1005
+ * Field-level (column-level) security records configured for THIS field, across all roles.
1006
+ * Empty for the overwhelming majority of fields — see {@link HasFieldPermissions}.
1007
+ */
1008
+ get FieldPermissions(): EntityFieldPermissionInfo[];
1009
+ /**
1010
+ * True when at least one {@link EntityFieldPermissionInfo} record exists for this field.
1011
+ *
1012
+ * **Not an enforcement gate** — it answers "does any configuration target this field",
1013
+ * which CodeGen's DB-tier emission and the system-user entanglement guard both need. The
1014
+ * access decision is {@link EntityInfo.EnableFieldLevelSecurity} plus the aggregation; on an
1015
+ * enabled entity a field with no records is denied, not open.
1016
+ */
1017
+ get HasFieldPermissions(): boolean;
1018
+ /**
1019
+ * Entities whose fields can never be restricted by field-level security.
1020
+ *
1021
+ * Two distinct reasons, both amounting to "a configuration that cannot be undone through
1022
+ * the product":
1023
+ *
1024
+ * 1. **The security-configuration surface** (Entities, Entity Fields, Entity Permissions,
1025
+ * Entity Field Permissions, Roles). Restricting `CanRead` on the Entity Field Permissions
1026
+ * entity itself would leave the admin screen unable to render the very rows needed to
1027
+ * reverse the restriction — recovery would require direct SQL against the database.
1028
+ * 2. **The identity surface** (Users, User Roles). Role resolution and the auth path read
1029
+ * these on every request; restricting a column here degrades far more than one screen.
1030
+ *
1031
+ * Note this is deliberately a guard on WHICH ENTITIES are restrictable, not an exemption for
1032
+ * particular USERS. No user is above a Deny — that would undercut the entire point of the
1033
+ * feature for the confidentiality use cases (compensation, donor giving) that motivate it.
1034
+ *
1035
+ * Stored lowercased; compare with a trimmed, lowercased entity name.
1036
+ */
1037
+ private static readonly UnrestrictableEntityNames;
1038
+ /**
1039
+ * True when this field belongs to an entity that field-level security may never restrict.
1040
+ * See {@link EntityFieldInfo.UnrestrictableEntityNames} for the rationale.
1041
+ */
1042
+ get IsOnUnrestrictableEntity(): boolean;
1043
+ /**
1044
+ * True for fields that must remain readable regardless of any permission record:
1045
+ * primary keys (hard or soft) and `__mj_` system columns.
1046
+ *
1047
+ * Stripping a primary key from a result breaks entity load, {@link CompositeKey}
1048
+ * construction, relationship resolution, and cache fingerprinting — the failure surfaces
1049
+ * far from the permission record that caused it. This is enforced here AND at save time on
1050
+ * the permission record itself, so a row inserted outside the entity path still cannot take
1051
+ * a primary key out of a result set.
1052
+ */
1053
+ get IsUnrestrictableField(): boolean;
1054
+ /**
1055
+ * Returns the effective field-level access this user has to this field, aggregating the
1056
+ * field's permission records across every role the user holds.
1057
+ *
1058
+ * **PRECONDITION: the caller has already established that the parent entity has
1059
+ * {@link EntityInfo.EnableFieldLevelSecurity} set.** The flag is a required parameter rather
1060
+ * than something this method looks up, because `EntityFieldInfo` holds its entity's NAME and
1061
+ * not a reference to the `EntityInfo` — and a method that silently answered "denied" for a
1062
+ * field on a non-FLS entity would be a trap. Pass `false` and every field comes back fully
1063
+ * open.
1064
+ *
1065
+ * Per verb, across the user's matching roles:
1066
+ * `effective = (any row Allows) AND NOT (any row Denies)`. Deny is absorbing and No Access
1067
+ * is the identity, so three states collapse to that one expression.
1068
+ *
1069
+ * Outcomes:
1070
+ * - **Field security disabled on the entity** → fully open.
1071
+ * - **No records on the field** (enabled) → fully closed. Snapshot initialization creates a
1072
+ * row for every (field, role) that should have one, so a missing row means reconciliation
1073
+ * has not run — failing closed makes that visible.
1074
+ * - **Records exist, none match the user's roles** → fully closed, for want of an Allow.
1075
+ *
1076
+ * **There is no exempt user — not even the MJ system user.** Every account, including the
1077
+ * one the server runs its own background work as, gets its access from the rows. The system
1078
+ * user stays working because it holds the standard roles (UI, Developer, Integration),
1079
+ * snapshot initialization writes them `Allow` rows like any other role holding entity read,
1080
+ * and the save-time configuration guards refuse a `Deny` aimed at a role it holds. That is a
1081
+ * constraint on what can be CONFIGURED, which an administrator can see and reason about —
1082
+ * unlike a runtime bypass, which is invisible at the point where access is decided and has
1083
+ * to be trusted rather than checked.
1084
+ *
1085
+ * PERFORMANCE: this is the per-FIELD primitive. Enforcement points must never call it
1086
+ * inside a per-row loop — `MapFieldNamesToCodeNames` runs once per row, so a naive call
1087
+ * site costs `fields x rows` aggregations. Compute the denied-field Set once per
1088
+ * (entity, user) per request and pass it into the row loop.
1089
+ *
1090
+ * @param user the user whose effective access is being resolved
1091
+ * @param entityFieldSecurityEnabled the parent entity's `EnableFieldLevelSecurity` flag
1092
+ */
1093
+ GetUserFieldPermissions(user: UserInfo, entityFieldSecurityEnabled: boolean): EntityFieldUserPermissionInfo;
1094
+ /**
1095
+ * The "field security does not apply here" answer, named so the policy exits above cannot
1096
+ * drift apart from one another.
1097
+ */
1098
+ private static fullyOpenFieldPermissions;
1099
+ /**
1100
+ * Trinary aggregation across the user's roles. Split out of
1101
+ * {@link GetUserFieldPermissions} so the guards there read as policy and this reads as
1102
+ * arithmetic.
1103
+ */
1104
+ private aggregateUserFieldPermissions;
1105
+ /**
1106
+ * The same aggregation {@link GetUserFieldPermissions} performs, over a rule list the caller
1107
+ * supplies rather than this field's stored one.
1108
+ *
1109
+ * Exists so save-time guards can evaluate a **prospective** outcome — the rules as they would
1110
+ * stand after a proposed insert, edit or delete — instead of classifying a single row in
1111
+ * isolation. That distinction is load-bearing: whether a change restricts a user is a property
1112
+ * of the AGGREGATE across all the roles they hold, not of any one rule. A rule reading
1113
+ * `No Access` restricts nobody on its own, yet setting every one of a user's roles to
1114
+ * `No Access` leaves no `Allow` standing and denies the field outright.
1115
+ *
1116
+ * @param rules the rules to aggregate — any shape carrying a `RoleID` and the three verbs
1117
+ * @param user the user whose roles select which rules apply
1118
+ */
1119
+ static AggregateFieldRulesForUser(rules: readonly FieldPermissionRuleForRole[], user: UserInfo): EntityFieldUserPermissionInfo;
1120
+ /**
1121
+ * Folds one trinary verb into the Allow/Deny buckets. Anything unrecognized is treated as
1122
+ * `No Access`, so a value reaching here outside the CHECK constraint grants nothing.
1123
+ */
1124
+ private static applyAccessToBuckets;
777
1125
  /**
778
1126
  * Returns the ValueListType using the EntityFieldValueListType enum.
779
1127
  */
@@ -948,6 +1296,25 @@ export declare class EntityFieldInfo extends BaseInfo {
948
1296
  get SQLFullType(): string;
949
1297
  get MaxLength(): number;
950
1298
  get ReadOnly(): boolean;
1299
+ /**
1300
+ * True when {@link ExtendedType} is any Geo* tag (`Geo`, `GeoLatitude`, `GeoAddress`, …).
1301
+ * Used by maps, distance, and GeoCodeSyncService. Display-only virtuals still count.
1302
+ */
1303
+ get IsGeoExtendedType(): boolean;
1304
+ /**
1305
+ * A Geo* field that can be written on Save. GeoCodeSyncService only runs when the
1306
+ * entity has at least one of these. Virtual / AllowUpdateAPI=0 fields (PrimaryAddress*,
1307
+ * `__mj_Latitude`, embedded `__mj_Latitude_{FK}`) are display-only — maps still use them.
1308
+ */
1309
+ get IsWritableGeoField(): boolean;
1310
+ /**
1311
+ * Native (table) latitude column — `ExtendedType=GeoLatitude`, or legacy `Geo` named Latitude.
1312
+ */
1313
+ get IsNativeLatitudeField(): boolean;
1314
+ /**
1315
+ * Native (table) longitude column — `ExtendedType=GeoLongitude`, or legacy `Geo` named Long*.
1316
+ */
1317
+ get IsNativeLongitudeField(): boolean;
951
1318
  /**
952
1319
  * Helper method that returns true if the field is one of the special reserved MJ date fields for tracking CreatedAt and UpdatedAt timestamps as well as the DeletedAt timestamp used for entities that
953
1320
  * have DeleteType=Soft. This is only used when the entity has TrackRecordChanges=1 or for entities where DeleteType=Soft
@@ -1292,6 +1659,16 @@ export declare class EntityInfo extends BaseInfo {
1292
1659
  * This flag is set on the **parent** entity and controls whether its children are exclusive.
1293
1660
  */
1294
1661
  AllowMultipleSubtypes: boolean;
1662
+ /**
1663
+ * Optional JSON configuration specifying declarative prospective subtype resolution on an entity.
1664
+ * Stored in the SubtypeSelector column of Entity (shape = IEntitySubtypeSelectorConfig).
1665
+ */
1666
+ SubtypeSelector: string;
1667
+ private _subtypeSelectorConfig;
1668
+ /**
1669
+ * Parsed SubtypeSelector configuration, if configured.
1670
+ */
1671
+ get SubtypeSelectorConfig(): IEntitySubtypeSelectorConfig | null;
1295
1672
  /**
1296
1673
  * Whether to audit when users access records from this entity
1297
1674
  */
@@ -1316,6 +1693,19 @@ export declare class EntityInfo extends BaseInfo {
1316
1693
  * client-side IndexedDB cache. Zero overhead on hot save/query paths.
1317
1694
  */
1318
1695
  AllowCaching: boolean;
1696
+ /**
1697
+ * Whether field-level (column-level) security is enforced for this entity.
1698
+ *
1699
+ * This is the single gate every field-security enforcement point checks first, and it is
1700
+ * explicit — never inferred from whether permission rows happen to exist. It is `false` for
1701
+ * nearly every entity in nearly every deployment, so enforcement collapses to one boolean
1702
+ * test: no field iteration, no aggregation, no allocation.
1703
+ *
1704
+ * Turning it on snapshots the entity's existing entity-level permissions into per-field
1705
+ * rows, so enabling changes no behavior until an administrator tightens a field. Turning it
1706
+ * off leaves the rows in place, inactive, so re-enabling does not lose the configuration.
1707
+ */
1708
+ EnableFieldLevelSecurity: boolean;
1319
1709
  /**
1320
1710
  * Whether this entity is available through the GraphQL API
1321
1711
  */
@@ -1426,11 +1816,17 @@ export declare class EntityInfo extends BaseInfo {
1426
1816
  */
1427
1817
  FullTextSearchFunctionGenerated: boolean;
1428
1818
  /**
1429
- * When true, this entity supports geocoding CodeGen generates geo-aware subclass code,
1430
- * adds __mj_Latitude/__mj_Longitude virtual fields to the base view, and the UI shows
1431
- * a map view toggle. Auto-set by CodeGen when LLM detects geo-capable fields.
1819
+ * When true, this entity participates in geo **read** features: map view, distance
1820
+ * calculations, and similar. That is independent of whether GeoCodeSyncService runs
1821
+ * on Save the service only fires when {@link HasWritableGeoSourceFields} is true.
1822
+ * Auto-set by CodeGen when LLM detects geo-capable fields.
1432
1823
  */
1433
1824
  SupportsGeoCoding: boolean;
1825
+ /**
1826
+ * True when at least one field is a writable Geo* source (street and/or native lat/lng).
1827
+ * Person/Org PrimaryAddress* are virtual display fields and do **not** count.
1828
+ */
1829
+ get HasWritableGeoSourceFields(): boolean;
1434
1830
  /**
1435
1831
  * When true (default), CodeGen can automatically set SupportsGeoCoding based on
1436
1832
  * LLM analysis of entity fields. Set to false to lock the value.
@@ -1636,6 +2032,107 @@ export declare class EntityInfo extends BaseInfo {
1636
2032
  private _encryptedFieldsCache;
1637
2033
  private _datetimeFieldsCache;
1638
2034
  private _nameFieldCache;
2035
+ /**
2036
+ * The set of field names this user may NOT READ on this entity — the per-request primitive
2037
+ * every field-security enforcement point is built on.
2038
+ *
2039
+ * Compute this ONCE per (entity, user) per request and pass the Set into any row loop.
2040
+ * `GetUserFieldPermissions` is the per-FIELD primitive; calling it per row costs
2041
+ * `fields x rows` aggregations (40,000 for a 1,000-row x 40-column result), each of which
2042
+ * re-scans `user.UserRoles` and allocates. A Set lookup costs neither.
2043
+ *
2044
+ * Names are lowercased so callers can match case-insensitively, consistent with
2045
+ * {@link ProjectRowsToFields}. Returns an EMPTY set — never null — both when the entity has
2046
+ * field security switched off and when the user is denied nothing, so callers can treat
2047
+ * `size === 0` as the single "nothing to do" condition.
2048
+ */
2049
+ GetDeniedReadFields(user: UserInfo): Set<string>;
2050
+ /**
2051
+ * Whether this user may READ the named field — the single-field question, answered the same
2052
+ * way {@link GetDeniedReadFields} answers it in bulk.
2053
+ *
2054
+ * Exists for **display code that is about to read a value it did not choose**: a form
2055
+ * toolbar rendering the entity's Name field, an FK control rendering the joined display
2056
+ * column, an IS-A card walking a sibling record's fields. `BaseEntity.Get()` throws for a
2057
+ * denied field, so those call sites have to ask before they read or they take out the whole
2058
+ * screen instead of hiding one value.
2059
+ *
2060
+ * This is a PREDICATE, deliberately — not a value accessor that quietly returns nothing.
2061
+ * The caller still decides what to render in place of the value, which is the part that
2062
+ * differs per surface and should not be hidden inside a getter.
2063
+ *
2064
+ * **Fails open** on a missing user, a missing field name, or an entity with field security
2065
+ * switched off — matching `BaseEntity`'s own gate and `MjFormFieldComponent`. The server is
2066
+ * the real boundary; a UI that blanked out fields because no user had resolved yet would be
2067
+ * worse than one that shows them.
2068
+ *
2069
+ * PERFORMANCE: this delegates to {@link GetDeniedReadFields}, which walks every field on the
2070
+ * entity. Fine for the handful of chrome reads it exists for; do NOT call it per row in a
2071
+ * grid loop — compute the denied set once and test against it.
2072
+ *
2073
+ * @param fieldName the field about to be read
2074
+ * @param user the acting user
2075
+ */
2076
+ IsFieldReadableByUser(fieldName: string | null | undefined, user: UserInfo | null | undefined): boolean;
2077
+ /**
2078
+ * Whether this user may UPDATE the named field. Companion to
2079
+ * {@link IsFieldReadableByUser}, for UI that needs to render a control read-only rather than
2080
+ * let a user type into something the server will reject on save. Fails open on the same
2081
+ * three conditions.
2082
+ */
2083
+ IsFieldUpdatableByUser(fieldName: string | null | undefined, user: UserInfo | null | undefined): boolean;
2084
+ /**
2085
+ * Whether this user may supply a value for the named field when CREATING a record.
2086
+ * Companion to {@link IsFieldReadableByUser}. Fails open on the same three conditions.
2087
+ *
2088
+ * Note the server does not REJECT a create-denied value — it drops it and takes the column
2089
+ * default. So a UI that leaves such a field editable on a new record silently discards what
2090
+ * the user typed, which is the case this exists to prevent.
2091
+ */
2092
+ IsFieldCreatableByUser(fieldName: string | null | undefined, user: UserInfo | null | undefined): boolean;
2093
+ /**
2094
+ * The set of field names this user may NOT UPDATE on this entity. Same per-request
2095
+ * precompute contract as {@link GetDeniedReadFields}.
2096
+ *
2097
+ * A field can be readable and not updatable. The reverse cannot happen — Read is required
2098
+ * for Update — so denied-read is always a subset of denied-update, but ask for the set you
2099
+ * actually need rather than relying on that.
2100
+ */
2101
+ GetDeniedUpdateFields(user: UserInfo): Set<string>;
2102
+ /**
2103
+ * The set of field names this user may NOT supply a value for when CREATING a record. Same
2104
+ * per-request precompute contract as {@link GetDeniedReadFields}.
2105
+ *
2106
+ * Unlike the update set, this does not drive a rejection: a value supplied for a
2107
+ * create-denied field is dropped and the column takes its default.
2108
+ */
2109
+ GetDeniedCreateFields(user: UserInfo): Set<string>;
2110
+ /**
2111
+ * Shared walk behind {@link GetDeniedReadFields} / {@link GetDeniedUpdateFields} /
2112
+ * {@link GetDeniedCreateFields}, short-circuiting on {@link EnableFieldLevelSecurity}.
2113
+ *
2114
+ * Every field is aggregated, including those carrying no permission records — on an enabled
2115
+ * entity those are denied. Unrestrictable fields (primary keys, `__mj_` columns) come back
2116
+ * open, decided inside `GetUserFieldPermissions` rather than skipped here.
2117
+ *
2118
+ * **Carries BOTH `Name` and `CodeName`**, because the callers do not all live in the same key
2119
+ * space and a set holding only one of them silently no-ops in the other. `BaseEntity` and the
2120
+ * predicate gate ask about field *Names*; the row projections
2121
+ * (`ProviderBase.OmitFieldsFromRows`, the Record Changes payload projector) match against a
2122
+ * *row's own keys*, and rows are keyed by `CodeName` — `getRunTimeViewFieldString` emits
2123
+ * `[Name] AS [CodeName]` whenever the two differ, and `CodeNameFromString` replaces every
2124
+ * `[^a-zA-Z0-9_]` with `_`. So for a column named `Base Salary` a Name-only set holds
2125
+ * `base salary` while the rows are keyed `Base_Salary`, nothing matches, and the denied values
2126
+ * are returned in full. Every shipped MJ field name is already a valid identifier, so the two
2127
+ * coincide throughout core and no fixture caught this; it needs a customer entity with a
2128
+ * column like `Base Salary` or `Emp #` — which is the population this feature exists for.
2129
+ *
2130
+ * Widening cannot over-deny. The only way an extra entry could catch an innocent field is if a
2131
+ * DENIED field's `CodeName` equalled a different, permitted field's `Name` — but two fields
2132
+ * that collide on `CodeName` already collide on their generated property, which is not a
2133
+ * schema CodeGen can emit.
2134
+ */
2135
+ private getDeniedFields;
1639
2136
  /**
1640
2137
  * O(1) case-insensitive field lookup by name. Use this instead of `Fields.find(f => f.Name === name)`
1641
2138
  * on hot paths — it builds a lowercased+trimmed `Map` once (lazily) and reuses it.
@@ -1651,6 +2148,12 @@ export declare class EntityInfo extends BaseInfo {
1651
2148
  /**
1652
2149
  * Returns the primary key field for the entity. For entities with a composite primary key, use the PrimaryKeys property which returns all.
1653
2150
  * In the case of a composite primary key, the PrimaryKey property will return the first field in the sequence of the primary key fields.
2151
+ *
2152
+ * This is a single-column convenience for the places MJ is single-column *by design* — foreign-key
2153
+ * targets, keyset `ORDER BY`, IS-A shared keys, and the bare-value shorthand `CompositeKey.LoadFromURLSegment`
2154
+ * accepts. Do not use it to *construct* a load key for an arbitrary entity: that silently drops every
2155
+ * column but the first on a composite key. Build keys with `CompositeKey.FromURLSegment(entityInfo, recordId)`
2156
+ * or `CompositeKey.FromEntityRecord(entityInfo, row)`, which honor all of `PrimaryKeys`.
1654
2157
  */
1655
2158
  get FirstPrimaryKey(): EntityFieldInfo;
1656
2159
  /**
@@ -1885,14 +2388,33 @@ export declare class EntityInfo extends BaseInfo {
1885
2388
  */
1886
2389
  GetUserPermisions(user: UserInfo): EntityUserPermissionInfo;
1887
2390
  /**
1888
- * Determines if a given user, for a given permission type, is exempt from RowLevelSecurity or not
2391
+ * Determines if a given user, for a given permission type, is exempt from RowLevelSecurity or not.
2392
+ *
2393
+ * A permission row confers an exemption only for an operation it GRANTS (the matching `Can*`
2394
+ * flag is true) and leaves unfiltered. A row that does not grant the operation has no filter
2395
+ * for it either, and that absence means "not applicable", not "unrestricted" — so it must not
2396
+ * lift a filter that another of the user's roles binds. Without the `Can*` check, a role that
2397
+ * grants only reads (CanCreate=false, hence CreateRLSFilterID=null) made every holder of that
2398
+ * role exempt from CREATE row-level security, which is the shape of the 'UI' role every
2399
+ * authenticated user holds on nearly every entity.
1889
2400
  * @param user
1890
2401
  * @param type
1891
2402
  * @returns
1892
2403
  */
1893
2404
  UserExemptFromRowLevelSecurity(user: UserInfo, type: EntityPermissionType): boolean;
1894
2405
  /**
1895
- * Returns RLS security info attributes for a given user and permission type
2406
+ * Returns RLS security info attributes for a given user and permission type.
2407
+ *
2408
+ * Only permission rows that GRANT the operation contribute a filter: an Allow row whose matching
2409
+ * `Can*` flag is true. Deny rows are skipped outright — on a Deny row a set `Can*` flag means
2410
+ * "deny that operation", and a user carrying one fails the permission gate before this runs
2411
+ * (`GetUserPermisions` subtracts Deny from Allow), so reading it as a grant would be wrong even
2412
+ * though it is unreachable. The filters of a user's roles are OR'd together by the caller, so a filter collected
2413
+ * from a row that does not grant the operation would WIDEN the clause: a user granted Create by
2414
+ * role A (bound to filter F1) would create against `F1 OR F2` when role B keeps a leftover
2415
+ * `CreateRLSFilterID = F2` beside `CanCreate = false`. `GetUserPermisions` aggregates the flags
2416
+ * across roles, so such a user passes the permission gate on role A alone; nothing else stops
2417
+ * F2 from applying. A user with no granting row gets no clause here — and no permission either.
1896
2418
  * @param user
1897
2419
  * @param type
1898
2420
  * @returns
@@ -2060,6 +2582,26 @@ export declare class RecordDependency {
2060
2582
  * The value of the primary key field in the parent record. MemberJunction supports composite(multi-field) primary keys. However, foreign keys only support links to single-valued primary keys in their linked entity.
2061
2583
  */
2062
2584
  PrimaryKey: CompositeKey;
2585
+ /**
2586
+ * True when this dependency is a **polymorphic (soft) link** rather than a hard foreign key -
2587
+ * that is, when `FieldName` is the `RecordID`-shaped payload column of an `EntityID`/`RecordID`
2588
+ * pair declared via {@link EntityFieldInfo.EntityIDFieldName}.
2589
+ *
2590
+ * The distinction matters because the two kinds of link store the target differently: a hard
2591
+ * foreign key holds the bare primary key value, while a polymorphic link holds the canonical
2592
+ * `CompositeKey.ToRecordID()` encoding (`ID|<guid>`). Anything that *rewrites* the link - record
2593
+ * merge, most importantly - has to write the right one, so this flag is what tells it which.
2594
+ *
2595
+ * Optional, and absent/false means "hard foreign key", so callers written before polymorphic
2596
+ * links were detected keep their existing behavior.
2597
+ */
2598
+ IsSoftLink?: boolean;
2599
+ /**
2600
+ * For a soft link ({@link IsSoftLink}), the name of the sibling discriminator column that says
2601
+ * which entity `FieldName` points at - the value of `EntityIDFieldName` on the payload field.
2602
+ * Undefined for hard foreign keys.
2603
+ */
2604
+ EntityIDFieldName?: string;
2063
2605
  }
2064
2606
  /**
2065
2607
  * Information about a merge request including the entity, the surviving record and the records to merge into the surviving record. Additionally, there is an optional field map that can be used to override field values in the surviving record to values specified.