// The Genesis platform a GSF 8.15.x app runs beside its own processes: auth // (users, profiles, rights, the key-value store), notify's alerts, the // RESOURCES every platform product registers, and the tables, queries and // request servers its products' screens read (./products.ts). Data comes from // ./platform/*.json, which scripts/platform-preset/generate.ts builds from // real recordings and the products' own dictionaries, scripts and seed files; // behaviour and texts cite the product scripts at the versions the showcase // server pins (server/gradle.properties: GSF 8.15.29, auth 8.15.5, // genesis-notify 8.15.16, reporting 8.15.3, file-server 8.15.9, // reconciliation 8.15.4). import { NackError } from '../protocol/errors.ts'; import type { AuthUser, EventHandler, EventHandlerCtx, FieldDef, MockServerConfig, QueryDef, RequestingUser, RequestReplyDef, ResourceItem, Row, TableDef, } from '../types.ts'; import { authBacking, changePasswordRoute, hasRight } from './auth.ts'; import { fileServerBacking } from './fileServer.ts'; import { statusQueries, SYSTEM_STATUS_BYTES, systemInfoRequestReplies } from './gsf.ts'; import { notifyBacking, type NotifyUsers } from './notify.ts'; import { DEFAULT_NOTIFY_GATEWAYS, dispatchBacking, type NotifyGateway } from './notifyDispatch.ts'; import { ruleBacking } from './notifyRules.ts'; import defaultUser from './platform/default-user.json' with { type: 'json' }; import platformProfileRights from './platform/profile-rights.json' with { type: 'json' }; import platformProfiles from './platform/profiles.json' with { type: 'json' }; import platformRights from './platform/rights.json' with { type: 'json' }; import { includedProducts, ofIncluded, type PlatformProduct, platformTable, productBacking, productResources, typedRequestReply, } from './products.ts'; import { reconciliationBacking } from './reconciliation.ts'; import { reportingBacking } from './reporting.ts'; export interface PlatformUser { username: string; // What a credentials login must send, kept hashed as auth keeps // USER.PASSWORD, outside the USER rows the mock serves. Unset: no password // logs this user in until one is set, which a change of password with a // blank old one does, as auth's PasswordService allows (httpAutoLogin and // issued tokens still work). password?: string; // The profiles the user belongs to (PROFILE_USER). Unless `permission` is // set, the user's rights are these profiles' rights (PROFILE_RIGHT), as // auth's consolidator works them out. profile?: string[]; // The user's rights, replacing the ones the profiles give, for as long as // the user exists: auth's events that work rights out from profiles leave // them as they are. permission?: string[]; // The user's USER and USER_ATTRIBUTES columns (FIRST_NAME, LAST_NAME, // EMAIL_ADDRESS, COMPANY_NAME, STATUS, LAST_LOGIN, ACCESS_TYPE, ...): its // ALL_USERS row. STATUS defaults to ENABLED: a login checks it as auth's // does. user?: Row; } // Rows an app adds to the platform tables, as its data/*.csv do: loaded after // the platform PBCs' own, a row with the same key replaces theirs. export interface PlatformSeed { RIGHT?: Row[]; PROFILE?: Row[]; PROFILE_RIGHT?: Row[]; NOTIFY_ALERT?: Row[]; KEY_VALUE?: Row[]; } export interface PlatformPresetOptions { // The users who can log in. Default: blank-app-seed's admin (password // 'admin'), in every profile the platform PBCs seed except // NOTIFICATION_SERVICE, as the real showcase and Create apps have it. users?: PlatformUser[]; seed?: PlatformSeed; // Listed in RESOURCES beside the platform's. resources?: ResourceItem[]; // Who a connection that never logged in acts as for the per-user resources // (the key-value store, alerts). A client that logs in over HTTP sends no // session token on its WebSocket, so the mock can't tell who it is; the real // server knows from the session cookie. Default: the first user. sessionUser?: string; // The platform products the app includes beside GSF and auth, which are // always in: their resources are listed and backed, the others' aren't. // Default: all of them. products?: readonly PlatformProduct[]; // The gateways the app's genesis-notify.kts declares: what the notify // dispatcher sends through (only a `screen` one writes NOTIFY_ALERT rows) // and CONFIGURED_NOTIFY_ROUTES lists. Default: the product's own, screen // "Screen" and email "Email"; Create's seed and the showcase add teams // "Teams". notifyGateways?: NotifyGateway[]; // The sysdef's NOTIFY_ENTITY_LIST: the tables and views NOTIFY_SYSTEM_ENTITY // offers the rule dialogs. Default: all of them, as an empty list is. notifyEntities?: string[]; // The sysdef's REPORTING_DEFAULT_TIMEZONE: the zone a report's // `$(timestamp:...)` file name is written in. Default: the host's, as the // JVM's is when it's unset. reportingTimeZone?: string; // The sysdef's REPORTING_DATASOURCE_LIST: the request servers a report can // read. Default: all of them. reportingDatasources?: string[]; // How long a reconciliation run stays IN_PROGRESS before it completes (with // no results: the mock matches nothing). Default 1000 ms; Infinity, or // anything past 2^31 - 1 ms, never completes it. reconciliationRunMs?: number; // The batch pipelines the app's reconciliation process defines, which a // PIPELINE datasource can name. Default: none, as the product's own // reconciliation-pipelines.kts defines. reconciliationPipelines?: string[]; } const RECONCILIATION_RUN_MS = 1000; const DEFAULT_PASSWORD = 'admin'; // --- tables -------------------------------------------------------------------- const field = (name: string, type: FieldDef['type'] = 'STRING', more: Partial = {}) => ({ name, type, ...more, }); // USER as GSF declares it (genesis-tables-dictionary.kts:360, v8.15.29) but // for PASSWORD, which auth.ts keeps beside the store. const userTable = (): TableDef => { const table = platformTable('USER'); return { ...table, fields: table.fields?.filter(({ name }) => name !== 'PASSWORD') }; }; // What a PlatformUser's `user` may set. const columnsOf = (table: TableDef) => new Set((table.fields ?? []).map(({ name }) => name)); const USER_COLUMNS = columnsOf(userTable()); const ATTRIBUTE_COLUMNS = columnsOf(platformTable('USER_ATTRIBUTES')); // The users, profiles and rights, in GSF's and auth's own tables (their // audits included): USER, USER_ATTRIBUTES and RIGHT_SUMMARY (GSF v8.15.29), // PROFILE, PROFILE_USER, RIGHT and PROFILE_RIGHT (auth v8.15.5). A user's // RIGHT_SUMMARY rows are their rights, as auth's consolidation keeps them. // KEY_VALUE: auth-tables-dictionary.kts:2. NOTIFY_ALERT: // genesis-notify-tables-dictionary.kts:112 (genesis-notify v8.15.16), types // from genesis-notify-fields-dictionary.kts. function platformTables( seed: Required, users: PlatformUser[], ): Record { return { USER: { ...userTable(), rows: users.map(({ username, user = {} }) => ({ ...pick(user, USER_COLUMNS), USER_NAME: username, STATUS: user.STATUS ?? 'ENABLED', })), }, USER_ATTRIBUTES: { ...platformTable('USER_ATTRIBUTES'), rows: users.map(({ username, user = {} }) => ({ ...pick(user, ATTRIBUTE_COLUMNS), USER_NAME: username, })), }, PROFILE: { ...platformTable('PROFILE'), rows: seed.PROFILE }, PROFILE_USER: { ...platformTable('PROFILE_USER'), rows: users.flatMap(({ username, profile = [] }) => profile.map((PROFILE_NAME) => ({ PROFILE_NAME, USER_NAME: username })), ), }, RIGHT: { ...platformTable('RIGHT'), rows: seed.RIGHT }, PROFILE_RIGHT: { ...platformTable('PROFILE_RIGHT'), rows: seed.PROFILE_RIGHT }, RIGHT_SUMMARY: { ...platformTable('RIGHT_SUMMARY'), rows: users.flatMap((user) => permissionOf(user, seed.PROFILE_RIGHT).map((RIGHT_CODE) => ({ USER_NAME: user.username, RIGHT_CODE, })), ), }, // USER_NAME, KV_KEY, KV_VALUE (RAW). Left undeclared: a declared table // would have the engine check EVENT_KEY_VALUE_PUT's DETAILS against it, // and the real handler skips the schema check (`schemaValidation = false`). KEY_VALUE: { pkField: ['USER_NAME', 'KV_KEY'], rows: seed.KEY_VALUE }, NOTIFY_ALERT: { pkField: 'ALERT_ID', // sequence(ALERT_ID, "AL"), audit = details(..., sequence = "NA") sequencePrefix: 'AL', sequenceFormat: 'genesis', audit: { sequencePrefix: 'NA' }, fields: [ field('ALERT_ID'), field('MESSAGE'), field('HEADER'), field('USER_NAME', 'STRING', { maxSize: 255 }), field('EXPIRY', 'DATETIME'), field('NOTIFY_SEVERITY', 'ENUM', { values: ['Information', 'Warning', 'Serious', 'Critical'], default: 'Information', }), field('CREATED_AT', 'DATETIME'), field('TABLE_ENTITY_ID'), field('DYNAMIC_RULE_ID'), field('TOPIC'), field('ALERT_STATUS', 'ENUM', { values: ['NEW', 'DISMISSED', 'EXPIRED'], default: 'NEW' }), field('SHOW_TOAST', 'BOOLEAN', { default: true }), ], rows: seed.NOTIFY_ALERT, }, }; } function pick(row: Row, columns: Set): Row { return Object.fromEntries(Object.entries(row).filter(([name]) => columns.has(name))); } // A later row with the same key replaces an earlier one (GSF's data load // upserts), keeping the first one's place. function upsert(rows: Row[], extra: Row[] | undefined, key: (row: Row) => string): Row[] { const byKey = new Map(); for (const row of [...rows, ...(extra ?? [])]) byKey.set(key(row), row); return [...byKey.values()]; } // --- the session user ------------------------------------------------------------ // The user an event or request runs as: the connection's login, else the // sessionUser (see PlatformPresetOptions.sessionUser). function sessionUserOf(userName: string | undefined, fallback: string | undefined): string { const user = userName ?? fallback; // GSF never runs a handler without a user: its WebSocket is authenticated. if (user === undefined) throw NackError.standard('NOT_AUTHORISED', 'No user is logged in'); return user; } // --- auth: the key-value store --------------------------------------------------- // A RAW column's value on the wire: its bytes in base64 // (GenesisSetMaskingJsonSerializer: gen.writeBinary). foundation-comms' // kv.service base64-decodes every VALUE it reads. const rawValue = (value: unknown) => Buffer.from(String(value ?? ''), 'utf8').toString('base64'); function keyValueHandlers(fallback: string | undefined): Record { const userOf = (ctx: EventHandlerCtx) => sessionUserOf(ctx.connection.userName, fallback); return { // auth-key-value-eventhandler.kts (auth v8.15.5), KEY_VALUE_PUT: // `schemaValidation = false`, then entityDb.upsertAll of DETAILS.RECORDS // [{KEY, VALUE}] under the event's user. foundation-comms' kvStorage.put // sends them; grid column-state persistence rides on this. VALUE is kept // as sent (KeyValueRecord's Utf8ByteArrayDeserializer stores its UTF-8 // bytes); REQ_KEY_VALUE_READ hands it back in base64. EVENT_KEY_VALUE_PUT: (details, ctx) => { const { store, broadcastTableChange } = ctx; const userName = userOf(ctx); const records = (Array.isArray(details.RECORDS) ? details.RECORDS : []) as Row[]; for (const record of records) { // A record without a KEY fails KeyValueRecord's non-null `key` before // the upsert, and onException NACKs with its text. if (typeof record?.KEY !== 'string') { const keys = records.map((each) => each?.KEY).join(','); throw NackError.nack( `Error when upserting record for user: ${userName} with keys: '${keys}'.`, ); } } for (const { KEY, VALUE } of records) { const existing = store.findRow('KEY_VALUE', { USER_NAME: userName, KV_KEY: KEY }); if (existing) { const key = store.rowKey('KEY_VALUE', existing); store.updateRow('KEY_VALUE', key, { KV_VALUE: VALUE }); broadcastTableChange('KEY_VALUE', 'MODIFY', key); } else { const row = store.insertRow('KEY_VALUE', { USER_NAME: userName, KV_KEY: KEY, KV_VALUE: VALUE, }); broadcastTableChange('KEY_VALUE', 'INSERT', store.rowKey('KEY_VALUE', row)); } } return {}; }, // KEY_VALUE_DELETE: DETAILS.KEYS (KeyValueSelectByKeys), deleted in order; // a key that isn't there fails the delete, and onException NACKs with its // own text. The real handler isn't transactional, so the keys deleted // before the missing one stay deleted there; here the NACK rolls them back // (the engine commits an event only when it acks). EVENT_KEY_VALUE_DELETE: (details, ctx) => { const { store, broadcastTableChange } = ctx; const userName = userOf(ctx); const keys = (Array.isArray(details.KEYS) ? details.KEYS : []) as string[]; for (const key of keys) { const pk = store.rowKey('KEY_VALUE', { USER_NAME: userName, KV_KEY: key }); if (!store.deleteRow('KEY_VALUE', pk)) { throw NackError.nack( `Error when deleting records for user: ${userName} with Keys '${keys.join(',')}'`, ); } broadcastTableChange('KEY_VALUE', 'DELETE', pk); } return {}; }, }; } // The KEY_VALUE records a REQ_KEY_VALUE_* request names: the REQUEST user's, // and only the REQUEST's KEY when it has one (auth-reqrep.kts // filterWithParameters). function keyValues(rows: Row[], request: Row): Row[] { return rows.filter( (row) => row.USER_NAME === request.USER_NAME && (request.KEY === undefined || request.KEY === null || row.KV_KEY === request.KEY), ); } // --- notify: alerts -------------------------------------------------------------- // genesis-notify-eventhandler.kts (genesis-notify v8.15.16). function alertHandlers(fallback: string | undefined): Record { return { // :431 contextEventHandler: onValidate NACKs an // alert that isn't there or is another user's; onCommit sets // ALERT_STATUS DISMISSED (the inbox subscribes to ALERT_STATUS == 'NEW', so // the engine pushes the row out of it as a DELETE). EVENT_DISMISS_NOTIFY_ALERT: (details, ctx) => { const { store, broadcastTableChange } = ctx; const alert = store.findRow('NOTIFY_ALERT', { ALERT_ID: details.ALERT_ID }); if (!alert) throw NackError.nack(`Alert with ID ${details.ALERT_ID} does not exist`); if (alert.USER_NAME !== sessionUserOf(ctx.connection.userName, fallback)) { throw NackError.nack('Dismissing an alert targeted at another user is forbidden'); } store.updateRow('NOTIFY_ALERT', alert.ALERT_ID, { ALERT_STATUS: 'DISMISSED' }); broadcastTableChange('NOTIFY_ALERT', 'MODIFY', alert.ALERT_ID); return {}; }, // :453 eventHandler: the event user's NEW alerts (NotifyAlert // .byUserNameAlertStatus), DISMISSED in batches. EVENT_DISMISS_ALL_NOTIFY_ALERT: (_details, ctx) => { const { store, broadcastTableChange } = ctx; const userName = sessionUserOf(ctx.connection.userName, fallback); for (const alert of store.getAllRows('NOTIFY_ALERT')) { if (alert.USER_NAME !== userName || alert.ALERT_STATUS !== 'NEW') continue; store.updateRow('NOTIFY_ALERT', alert.ALERT_ID, { ALERT_STATUS: 'DISMISSED' }); broadcastTableChange('NOTIFY_ALERT', 'MODIFY', alert.ALERT_ID); } return {}; }, }; } // --- the preset ------------------------------------------------------------------ // A user's rights: `permission`, else their profiles' PROFILE_RIGHT codes, // sorted as the real login ack lists them. function permissionOf({ profile = [], permission }: PlatformUser, profileRights: Row[]): string[] { if (permission) return permission; const codes = profileRights .filter((row) => profile.includes(row.PROFILE_NAME)) .map((row) => row.RIGHT_CODE as string); return [...new Set(codes)].sort(); } const DEFAULT_USER: PlatformUser = { username: defaultUser.USER.USER_NAME, password: DEFAULT_PASSWORD, profile: defaultUser.PROFILE, user: { ...defaultUser.USER, ...defaultUser.USER_ATTRIBUTES }, }; // The login ack's USER object: what the mock sent before this preset existed // (the real ack's USER is null and its USER_DETAILS {FIRST_NAME, LAST_NAME}, // which plan item 1.19 aligns). const LOGIN_USER_FIELDS = new Set(['FIRST_NAME', 'LAST_NAME', 'EMAIL_ADDRESS', 'COMPANY_NAME']); /** * The Genesis platform's data and handlers, to merge with an app's own config: * `createMockServer(mergeConfigs(platformPreset(), appConfig))`. See the * README's "Platform preset". */ export function platformPreset(options: PlatformPresetOptions = {}): MockServerConfig { const included = includedProducts(options.products); const notify = included.has('genesis-notify'); if (!notify && options.seed?.NOTIFY_ALERT?.length) { throw new Error("seed.NOTIFY_ALERT needs the 'genesis-notify' product"); } const users = options.users ?? [DEFAULT_USER]; const fallback = options.sessionUser ?? users[0]?.username; const seed: Required = { RIGHT: upsert(platformRights, options.seed?.RIGHT, (row) => row.CODE), PROFILE: upsert(platformProfiles, options.seed?.PROFILE, (row) => row.NAME), PROFILE_RIGHT: upsert( platformProfileRights, options.seed?.PROFILE_RIGHT, (row) => `${row.PROFILE_NAME}|${row.RIGHT_CODE}`, ), NOTIFY_ALERT: options.seed?.NOTIFY_ALERT ?? [], KEY_VALUE: options.seed?.KEY_VALUE ?? [], }; // What the users log in as at first. Logins read the store // (authDirectory), so they follow what auth's events change; these name the // user httpAutoLogin: true logs in as. const authUsers: AuthUser[] = users.map((platformUser) => ({ username: platformUser.username, password: platformUser.password, profile: [...(platformUser.profile ?? [])].sort(), permission: permissionOf(platformUser, seed.PROFILE_RIGHT), user: platformUser.user && pick(platformUser.user, LOGIN_USER_FIELDS), status: platformUser.user?.STATUS ?? 'ENABLED', })); // NOTIFY_ALERT is genesis-notify's; the rest are GSF's and auth's. const { NOTIFY_ALERT, ...always } = platformTables(seed, users); const tables = notify ? { ...always, NOTIFY_ALERT } : always; // What the included products' screens read, generated from their scripts; // the pieces written out here replace any of the same name. const generated = productBacking(included, tables); const notifyEvents = notifyBacking(); const notifyUsers: NotifyUsers = { userOf: (ctx) => sessionUserOf(ctx.connection.userName, fallback), fallback, hasRight: (store, name, code) => name !== undefined && hasRight(store, name, code), }; const auth = authBacking({ userOf: notifyUsers.userOf, fixedRights: new Map( users.flatMap(({ username, permission }) => permission ? [[username, permission] as const] : [], ), ), passwords: new Map( users.flatMap(({ username, password }) => password === undefined ? [] : [[username, password] as const], ), ), }); const notifyRules = ruleBacking(notifyUsers); const notifyDispatch = dispatchBacking({ users: notifyUsers, gateways: options.notifyGateways ?? DEFAULT_NOTIFY_GATEWAYS, entities: options.notifyEntities, }); const fileServer = fileServerBacking(); const files = included.has('file-server'); const reconciliation = reconciliationBacking({ userOf: notifyUsers.userOf, runMs: options.reconciliationRunMs ?? RECONCILIATION_RUN_MS, pipelines: options.reconciliationPipelines, fileContent: files ? fileServer.fileContent : undefined, }); const reporting = reportingBacking({ userOf: notifyUsers.userOf, timeZone: options.reportingTimeZone, datasources: options.reportingDatasources, notify: notify ? notifyDispatch.raise : undefined, }); return { tables: { ...generated.tables, ...tables }, views: { ...generated.views, ...auth.views, SYSTEM_STATUS_BYTES, // auth-view-dictionary.kts (auth v8.15.5) :50 and :65. PROFILE_RIGHT_VIEW: { base: 'PROFILE_RIGHT', joins: [ { table: 'PROFILE', on: ['PROFILE_NAME', 'NAME'], type: 'INNER' }, { table: 'RIGHT', on: ['RIGHT_CODE', 'CODE'], type: 'INNER' }, ], fields: ['PROFILE_NAME', 'RIGHT_CODE'], }, PROFILE_USER_VIEW: { base: 'PROFILE_USER', joins: [ { table: 'USER', on: ['USER_NAME', 'USER_NAME'], type: 'INNER', backwardsJoin: true }, ], fields: ['PROFILE_NAME', 'USER_NAME'], }, // query(PROFILE) / requestReply(PROFILE) `NAME withPrefix PROFILE` and // query(RIGHT) `CODE withPrefix RIGHT`, `DESCRIPTION withPrefix RIGHT`. PROFILE_PREFIXED: { base: 'PROFILE', pkField: 'PROFILE_NAME', select: { PROFILE_NAME: 'NAME' }, fields: ['PROFILE_NAME', 'DESCRIPTION', 'STATUS'], }, RIGHT_PREFIXED: { base: 'RIGHT', pkField: 'RIGHT_CODE', select: { RIGHT_CODE: 'CODE', RIGHT_DESCRIPTION: 'DESCRIPTION' }, fields: ['RIGHT_CODE', 'RIGHT_DESCRIPTION'], }, }, // The preset's own, over the generated ones, for the included products. queries: { ...generated.queries, ...ofIncluded( { ...ownQueries(fallback), ...statusQueries(), ...notifyRules.queries }, 'DATASERVER', included, ), }, requestReplies: typedWhereTheyAre({ ...generated.requestReplies, ...ofIncluded( { ...ownRequestReplies(fallback), ...systemInfoRequestReplies(), ...auth.requestReplies, ...notifyRules.requestReplies, ...notifyDispatch.requestReplies, ...reporting.requestReplies, ...reconciliation.requestReplies, ...fileServer.requestReplies, }, 'REQUEST_SERVER', included, ), }), eventHandlers: ofIncluded( { ...keyValueHandlers(fallback), ...auth.eventHandlers, ...alertHandlers(fallback), ...notifyEvents.eventHandlers, ...notifyRules.eventHandlers, ...notifyDispatch.eventHandlers, ...reporting.eventHandlers, ...reconciliation.eventHandlers, ...fileServer.eventHandlers, }, 'EVENT_HANDLER', included, ), // The DETAILS classes of the alert events, which their names alone would // make NOTIFY_ALERT table events. eventSchemas: ofIncluded( { EVENT_DISMISS_NOTIFY_ALERT: { table: 'NOTIFY_ALERT', pkOnly: true, className: 'global.genesis.gen.dao.NotifyAlert.ByAlertId', }, EVENT_DISMISS_ALL_NOTIFY_ALERT: { fields: [], className: 'kotlin.Unit' }, ...auth.eventSchemas, ...notifyEvents.eventSchemas, ...notifyRules.eventSchemas, ...notifyDispatch.eventSchemas, ...reporting.eventSchemas, ...reconciliation.eventSchemas, ...fileServer.eventSchemas, }, 'EVENT_HANDLER', included, ), auth: { users: authUsers, directory: auth.directory }, // auth's change of password, which an expired-password login needs, and // file-server's endpoints. httpRoutes: [changePasswordRoute, ...(files ? fileServer.httpRoutes : [])], additionalResources: [...productResources(included), ...(options.resources ?? [])], }; } // The products' typed request servers (requestReply) say so: their // META as GSF builds it, PARAMETRIC_TYPE and MAX_ROWS (RequestReplyDef.typed). function typedWhereTheyAre( requestReplies: Record, ): Record { return Object.fromEntries( Object.entries(requestReplies).map(([name, definition]) => { const typed = typedRequestReply(name); return [name, typed ? { ...definition, typed } : definition]; }), ); } // auth-dataserver.kts (auth v8.15.5) and genesis-notify-rules-dataserver.kts // (genesis-notify v8.15.16): the ones the generated backing can't say, as // they rename columns or filter the alerts by user. function ownQueries(fallback: string | undefined): Record { const ownAlert = (row: Row, user: RequestingUser) => row.USER_NAME === (user.userName ?? fallback); return { ALL_USERS: { source: 'USER_VIEW' }, ALL_PROFILES: { source: 'PROFILE_PREFIXED' }, ALL_RIGHTS: { source: 'RIGHT_PREFIXED' }, ALL_PROFILE_RIGHTS: { source: 'PROFILE_RIGHT_VIEW' }, ALL_PROFILE_USERS: { source: 'PROFILE_USER_VIEW' }, // `permissioning { auth { filterWithUserName { userName == data.userName } } }` ALL_NOTIFY_ALERT_RECORDS: { source: 'NOTIFY_ALERT', auth: { rowFilter: ownAlert } }, // :11, the same with `filter { data.alertStatus == AlertStatus.NEW && // data.showToast }`. ALL_TOAST_NOTIFICATIONS: { source: 'NOTIFY_ALERT', filter: (row) => row.ALERT_STATUS === 'NEW' && row.SHOW_TOAST === true, auth: { rowFilter: ownAlert }, }, }; } // auth-reqrep.kts (auth v8.15.5): the ones that rename columns; and // genesis-notify-rules-reqrep.kts (genesis-notify v8.15.16) :162, which filters // the alerts' audit by user. function ownRequestReplies(fallback: string | undefined): Record { return { // requestReply("NOTIFY_ALERT", NOTIFY_ALERT_AUDIT) { permissioning { auth { // filterWithUserName { userName == data.userName } } } } NOTIFY_ALERT: { source: 'NOTIFY_ALERT_AUDIT', requestFields: ['NOTIFY_ALERT_AUDIT_ID'], auth: { rowFilter: (row, user) => row.USER_NAME === (user.userName ?? fallback) }, }, // :57 request { CODE } reply { CODE DESCRIPTION }, so its rows carry no // RECORD_ID. RIGHT: { source: 'RIGHT', fields: ['CODE', 'DESCRIPTION'] }, // :7 request { NAME withPrefix PROFILE } reply { ..., STATUS, DESCRIPTION } PROFILE: { source: 'PROFILE_PREFIXED', requestFields: ['PROFILE_NAME'], fields: ['PROFILE_NAME', 'STATUS', 'DESCRIPTION'], }, // :212 request { USER_NAME } reply { KV_KEY withAlias "KEY" } KEY_VALUE_LIST: { requestFields: ['USER_NAME'], resolver: ({ request, store }) => keyValues(store.getAllRows('KEY_VALUE'), request).map((row) => ({ KEY: row.KV_KEY })), metadataRows: [{ KEY: 'example' }], }, // :232 request { USER_NAME, KV_KEY withAlias "KEY" } // reply { KV_KEY withAlias "KEY", KV_VALUE withAlias "VALUE" } KEY_VALUE_READ: { requestFields: ['USER_NAME', 'KEY'], resolver: ({ request, store }) => keyValues(store.getAllRows('KEY_VALUE'), request).map((row) => ({ KEY: row.KV_KEY, VALUE: rawValue(row.KV_VALUE), })), metadataRows: [{ KEY: 'example', VALUE: '' }], }, }; }