@rpcbase/server 0.481.0 → 0.483.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,7 @@
1
- import { loadModel } from "@rpcbase/db";
1
+ import { models } from "@rpcbase/db";
2
2
  import { buildAbilityFromSession, getAccessibleByQuery } from "@rpcbase/db/acl";
3
3
  import { createNotification, sendNotificationsDigestForUser } from "./notifications.js";
4
- import { o as object, b as boolean, n as number, a as array, s as string, r as record, u as unknown, _ as _enum } from "./schemas-D5T9tDtI.js";
4
+ import { o as object, b as boolean, n as number, a as array, s as string, r as record, _ as _enum, u as unknown } from "./schemas-7qqi9OQy.js";
5
5
  const getSessionUser = (ctx) => {
6
6
  const rawSessionUser = ctx.req.session?.user;
7
7
  const userId = typeof rawSessionUser?.id === "string" ? rawSessionUser.id.trim() : "";
@@ -139,10 +139,10 @@ const listNotifications = async (payload, ctx) => {
139
139
  const unreadOnly = parsed.data.unreadOnly === true;
140
140
  const limit = parsed.data.limit ?? 50;
141
141
  const markSeen = parsed.data.markSeen === true;
142
- const SettingsModel = await loadModel("RBNotificationSettings", ctx);
142
+ const SettingsModel = await models.get("RBNotificationSettings", ctx);
143
143
  const settings = await SettingsModel.findOne({ userId }).lean();
144
144
  const disabledTopics = buildDisabledTopics(settings, "inApp");
145
- const NotificationModel = await loadModel("RBNotification", ctx);
145
+ const NotificationModel = await models.get("RBNotification", ctx);
146
146
  const queryFilters = [
147
147
  { userId },
148
148
  getAccessibleByQuery(ability, "read", "RBNotification")
@@ -234,7 +234,7 @@ const markRead = async (_payload, ctx) => {
234
234
  ctx.res.status(400);
235
235
  return { ok: false, error: "missing_notification_id" };
236
236
  }
237
- const NotificationModel = await loadModel("RBNotification", ctx);
237
+ const NotificationModel = await models.get("RBNotification", ctx);
238
238
  const now = /* @__PURE__ */ new Date();
239
239
  try {
240
240
  await NotificationModel.updateOne(
@@ -257,10 +257,10 @@ const markAllRead = async (_payload, ctx) => {
257
257
  ctx.res.status(403);
258
258
  return { ok: false, error: "forbidden" };
259
259
  }
260
- const SettingsModel = await loadModel("RBNotificationSettings", ctx);
260
+ const SettingsModel = await models.get("RBNotificationSettings", ctx);
261
261
  const settings = await SettingsModel.findOne({ userId: session.userId }).lean();
262
262
  const disabledTopics = buildDisabledTopics(settings, "inApp");
263
- const NotificationModel = await loadModel("RBNotification", ctx);
263
+ const NotificationModel = await models.get("RBNotification", ctx);
264
264
  const queryFilters = [
265
265
  { userId: session.userId },
266
266
  { archivedAt: { $exists: false } },
@@ -288,7 +288,7 @@ const archiveNotification = async (_payload, ctx) => {
288
288
  ctx.res.status(400);
289
289
  return { ok: false, error: "missing_notification_id" };
290
290
  }
291
- const NotificationModel = await loadModel("RBNotification", ctx);
291
+ const NotificationModel = await models.get("RBNotification", ctx);
292
292
  try {
293
293
  await NotificationModel.updateOne(
294
294
  { $and: [{ _id: notificationId }, { archivedAt: { $exists: false } }, getAccessibleByQuery(ability, "update", "RBNotification")] },
@@ -310,7 +310,7 @@ const getSettings = async (_payload, ctx) => {
310
310
  ctx.res.status(403);
311
311
  return { ok: false, error: "forbidden" };
312
312
  }
313
- const SettingsModel = await loadModel("RBNotificationSettings", ctx);
313
+ const SettingsModel = await models.get("RBNotificationSettings", ctx);
314
314
  const settings = await SettingsModel.findOne(
315
315
  { $and: [{ userId: session.userId }, getAccessibleByQuery(ability, "read", "RBNotificationSettings")] }
316
316
  ).lean();
@@ -346,7 +346,7 @@ const updateSettings = async (payload, ctx) => {
346
346
  ctx.res.status(400);
347
347
  return { ok: false, error: "invalid_payload" };
348
348
  }
349
- const SettingsModel = await loadModel("RBNotificationSettings", ctx);
349
+ const SettingsModel = await models.get("RBNotificationSettings", ctx);
350
350
  const nextValues = {};
351
351
  if (parsed.data.digestFrequency) {
352
352
  nextValues.digestFrequency = parsed.data.digestFrequency;
@@ -1,6 +1,6 @@
1
- import { loadModel, ZRBRtsChangeOp } from "@rpcbase/db";
1
+ import { models, ZRBRtsChangeOp } from "@rpcbase/db";
2
2
  import { buildAbilityFromSession } from "@rpcbase/db/acl";
3
- import { o as object, a as array, s as string, n as number, b as boolean, _ as _enum } from "./schemas-D5T9tDtI.js";
3
+ import { o as object, a as array, s as string, n as number, b as boolean, _ as _enum } from "./schemas-7qqi9OQy.js";
4
4
  const Route = "/api/rb/rts/changes";
5
5
  const requestSchema = object({
6
6
  sinceSeq: number().int().min(0).default(0),
@@ -72,8 +72,8 @@ const changesHandler = async (payload, ctx) => {
72
72
  const ability = buildAbilityFromSession({ tenantId, session: ctx.req.session });
73
73
  const modelCtx = getModelCtx(ctx, tenantId);
74
74
  const [RtsChange, RtsCounter] = await Promise.all([
75
- loadModel("RBRtsChange", modelCtx),
76
- loadModel("RBRtsCounter", modelCtx)
75
+ models.get("RBRtsChange", modelCtx),
76
+ models.get("RBRtsCounter", modelCtx)
77
77
  ]);
78
78
  const counter = await RtsCounter.findOne({ _id: "rts" }, { seq: 1 }).lean();
79
79
  const latestSeq = Number(counter?.seq ?? 0) || 0;
@@ -1,10 +1,10 @@
1
- import { loadModel, getTenantFilesystemDb } from "@rpcbase/db";
1
+ import { models, getTenantFilesystemDb } from "@rpcbase/db";
2
2
  import { GridFSBucket, ObjectId } from "mongodb";
3
3
  import { JSDOM } from "jsdom";
4
4
  import createDOMPurify from "dompurify";
5
5
  import { g as getTenantId, a as getModelCtx, b as buildUploadsAbility, c as getUploadSessionAccessQuery, e as ensureUploadIndexes, d as getBucketName, f as getUserId, h as getChunkSizeBytes, i as getSessionTtlMs, j as computeSha256Hex, t as toBufferPayload, n as normalizeSha256Hex, k as getMaxClientUploadBytesPerSecond, l as getRawBodyLimitBytes } from "./shared-BJomDDWK.js";
6
6
  import { randomBytes } from "node:crypto";
7
- import { o as object, n as number, b as boolean, s as string, a as array, _ as _enum } from "./schemas-D5T9tDtI.js";
7
+ import { o as object, n as number, b as boolean, s as string, a as array, _ as _enum } from "./schemas-7qqi9OQy.js";
8
8
  const MAX_SVG_BYTES = 128 * 1024;
9
9
  const window = new JSDOM("").window;
10
10
  const DOMPurify = createDOMPurify(window);
@@ -124,8 +124,8 @@ const completeUpload = async (_payload, ctx) => {
124
124
  }
125
125
  const modelCtx = getModelCtx(ctx, tenantId);
126
126
  const [UploadSession, UploadChunk] = await Promise.all([
127
- loadModel("RBUploadSession", modelCtx),
128
- loadModel("RBUploadChunk", modelCtx)
127
+ models.get("RBUploadSession", modelCtx),
128
+ models.get("RBUploadChunk", modelCtx)
129
129
  ]);
130
130
  const ability = buildUploadsAbility(ctx, tenantId);
131
131
  if (!ability.can("update", "RBUploadSession")) {
@@ -352,8 +352,8 @@ const getStatus = async (_payload, ctx) => {
352
352
  }
353
353
  const modelCtx = getModelCtx(ctx, tenantId);
354
354
  const [UploadSession, UploadChunk] = await Promise.all([
355
- loadModel("RBUploadSession", modelCtx),
356
- loadModel("RBUploadChunk", modelCtx)
355
+ models.get("RBUploadSession", modelCtx),
356
+ models.get("RBUploadChunk", modelCtx)
357
357
  ]);
358
358
  const ability = buildUploadsAbility(ctx, tenantId);
359
359
  if (!ability.can("read", "RBUploadSession")) {
@@ -428,8 +428,8 @@ const initUpload = async (payload, ctx) => {
428
428
  const chunksTotal = Math.ceil(totalSize / chunkSize);
429
429
  const modelCtx = getModelCtx(ctx, tenantId);
430
430
  const [UploadSession, UploadChunk] = await Promise.all([
431
- loadModel("RBUploadSession", modelCtx),
432
- loadModel("RBUploadChunk", modelCtx)
431
+ models.get("RBUploadSession", modelCtx),
432
+ models.get("RBUploadChunk", modelCtx)
433
433
  ]);
434
434
  await ensureUploadIndexes(UploadSession, UploadChunk);
435
435
  const uploadId = new ObjectId().toString();
@@ -474,8 +474,8 @@ const uploadChunk = async (payload, ctx) => {
474
474
  }
475
475
  const modelCtx = getModelCtx(ctx, tenantId);
476
476
  const [UploadSession, UploadChunk] = await Promise.all([
477
- loadModel("RBUploadSession", modelCtx),
478
- loadModel("RBUploadChunk", modelCtx)
477
+ models.get("RBUploadSession", modelCtx),
478
+ models.get("RBUploadChunk", modelCtx)
479
479
  ]);
480
480
  const ability = buildUploadsAbility(ctx, tenantId);
481
481
  if (!ability.can("update", "RBUploadSession")) {
package/dist/index.js CHANGED
@@ -2096,7 +2096,7 @@ class ErrorTracking {
2096
2096
  this._rateLimiter.stop();
2097
2097
  }
2098
2098
  }
2099
- const version = "5.18.0";
2099
+ const version = "5.19.0";
2100
2100
  const FeatureFlagError = {
2101
2101
  ERRORS_WHILE_COMPUTING: "errors_while_computing_flags",
2102
2102
  FLAG_MISSING: "flag_missing",
@@ -2374,6 +2374,7 @@ class FeatureFlagsPoller {
2374
2374
  }
2375
2375
  async loadFeatureFlags(forceReload = false) {
2376
2376
  if (this.loadedSuccessfullyOnce && !forceReload) return;
2377
+ if (!forceReload && this.nextFetchAllowedAt && Date.now() < this.nextFetchAllowedAt) return void this.logMsgIfDebug(() => console.debug("[FEATURE FLAGS] Skipping fetch, in backoff period"));
2377
2378
  if (!this.loadingPromise) this.loadingPromise = this._loadFeatureFlags().catch((err) => this.logMsgIfDebug(() => console.debug(`[FEATURE FLAGS] Failed to load feature flags: ${err}`))).finally(() => {
2378
2379
  this.loadingPromise = void 0;
2379
2380
  });
@@ -2386,6 +2387,16 @@ class FeatureFlagsPoller {
2386
2387
  if (!this.shouldBeginExponentialBackoff) return this.pollingInterval;
2387
2388
  return Math.min(SIXTY_SECONDS, this.pollingInterval * 2 ** this.backOffCount);
2388
2389
  }
2390
+ beginBackoff() {
2391
+ this.shouldBeginExponentialBackoff = true;
2392
+ this.backOffCount += 1;
2393
+ this.nextFetchAllowedAt = Date.now() + this.getPollingInterval();
2394
+ }
2395
+ clearBackoff() {
2396
+ this.shouldBeginExponentialBackoff = false;
2397
+ this.backOffCount = 0;
2398
+ this.nextFetchAllowedAt = void 0;
2399
+ }
2389
2400
  async _loadFeatureFlags() {
2390
2401
  if (this.poller) {
2391
2402
  clearTimeout(this.poller);
@@ -2411,12 +2422,10 @@ class FeatureFlagsPoller {
2411
2422
  this.logMsgIfDebug(() => console.debug("[FEATURE FLAGS] Flags not modified (304), using cached data"));
2412
2423
  this.flagsEtag = res.headers?.get("ETag") ?? this.flagsEtag;
2413
2424
  this.loadedSuccessfullyOnce = true;
2414
- this.shouldBeginExponentialBackoff = false;
2415
- this.backOffCount = 0;
2425
+ this.clearBackoff();
2416
2426
  return;
2417
2427
  case 401:
2418
- this.shouldBeginExponentialBackoff = true;
2419
- this.backOffCount += 1;
2428
+ this.beginBackoff();
2420
2429
  throw new ClientError(`Your project key or personal API key is invalid. Setting next polling interval to ${this.getPollingInterval()}ms. More information: https://posthog.com/docs/api#rate-limiting`);
2421
2430
  case 402:
2422
2431
  console.warn("[FEATURE FLAGS] Feature flags quota limit exceeded - unsetting all local flags. Learn more about billing limits at https://posthog.com/docs/billing/limits-alerts");
@@ -2426,12 +2435,10 @@ class FeatureFlagsPoller {
2426
2435
  this.cohorts = {};
2427
2436
  return;
2428
2437
  case 403:
2429
- this.shouldBeginExponentialBackoff = true;
2430
- this.backOffCount += 1;
2438
+ this.beginBackoff();
2431
2439
  throw new ClientError(`Your personal API key does not have permission to fetch feature flag definitions for local evaluation. Setting next polling interval to ${this.getPollingInterval()}ms. Are you sure you're using the correct personal and Project API key pair? More information: https://posthog.com/docs/api/overview`);
2432
2440
  case 429:
2433
- this.shouldBeginExponentialBackoff = true;
2434
- this.backOffCount += 1;
2441
+ this.beginBackoff();
2435
2442
  throw new ClientError(`You are being rate limited. Setting next polling interval to ${this.getPollingInterval()}ms. More information: https://posthog.com/docs/api#rate-limiting`);
2436
2443
  case 200: {
2437
2444
  const responseJson = await res.json() ?? {};
@@ -2443,8 +2450,7 @@ class FeatureFlagsPoller {
2443
2450
  cohorts: responseJson.cohorts || {}
2444
2451
  };
2445
2452
  this.updateFlagState(flagData);
2446
- this.shouldBeginExponentialBackoff = false;
2447
- this.backOffCount = 0;
2453
+ this.clearBackoff();
2448
2454
  if (this.cacheProvider && shouldFetch) try {
2449
2455
  await this.cacheProvider.onFlagDefinitionsReceived(flagData);
2450
2456
  } catch (err) {
@@ -2812,6 +2818,7 @@ class PostHogBackendClient extends PostHogCoreStateless {
2812
2818
  });
2813
2819
  }
2814
2820
  async getFeatureFlag(key, distinctId, options) {
2821
+ if (void 0 !== this._flagOverrides && key in this._flagOverrides) return this._flagOverrides[key];
2815
2822
  const { groups, disableGeoip } = options || {};
2816
2823
  let { onlyEvaluateLocally, sendFeatureFlagEvents, personProperties, groupProperties } = options || {};
2817
2824
  const adjustedProperties = this.addLocalPersonAndGroupProperties(distinctId, groups, personProperties, groupProperties);
@@ -2872,6 +2879,7 @@ class PostHogBackendClient extends PostHogCoreStateless {
2872
2879
  return response;
2873
2880
  }
2874
2881
  async getFeatureFlagPayload(key, distinctId, matchValue, options) {
2882
+ if (void 0 !== this._payloadOverrides && key in this._payloadOverrides) return this._payloadOverrides[key];
2875
2883
  const { groups, disableGeoip } = options || {};
2876
2884
  let { onlyEvaluateLocally, personProperties, groupProperties } = options || {};
2877
2885
  const adjustedProperties = this.addLocalPersonAndGroupProperties(distinctId, groups, personProperties, groupProperties);
@@ -2945,6 +2953,14 @@ class PostHogBackendClient extends PostHogCoreStateless {
2945
2953
  ...remoteEvaluationResult.payloads || {}
2946
2954
  };
2947
2955
  }
2956
+ if (void 0 !== this._flagOverrides) featureFlags = {
2957
+ ...featureFlags,
2958
+ ...this._flagOverrides
2959
+ };
2960
+ if (void 0 !== this._payloadOverrides) featureFlagPayloads = {
2961
+ ...featureFlagPayloads,
2962
+ ...this._payloadOverrides
2963
+ };
2948
2964
  return {
2949
2965
  featureFlags,
2950
2966
  featureFlagPayloads
@@ -2958,6 +2974,53 @@ class PostHogBackendClient extends PostHogCoreStateless {
2958
2974
  async reloadFeatureFlags() {
2959
2975
  await this.featureFlagsPoller?.loadFeatureFlags(true);
2960
2976
  }
2977
+ overrideFeatureFlags(overrides) {
2978
+ const flagArrayToRecord = (flags) => Object.fromEntries(flags.map((f) => [
2979
+ f,
2980
+ true
2981
+ ]));
2982
+ if (false === overrides) {
2983
+ this._flagOverrides = void 0;
2984
+ this._payloadOverrides = void 0;
2985
+ return;
2986
+ }
2987
+ if (Array.isArray(overrides)) {
2988
+ this._flagOverrides = flagArrayToRecord(overrides);
2989
+ return;
2990
+ }
2991
+ if (this._isFeatureFlagOverrideOptions(overrides)) {
2992
+ if ("flags" in overrides) {
2993
+ if (false === overrides.flags) this._flagOverrides = void 0;
2994
+ else if (Array.isArray(overrides.flags)) this._flagOverrides = flagArrayToRecord(overrides.flags);
2995
+ else if (void 0 !== overrides.flags) this._flagOverrides = {
2996
+ ...overrides.flags
2997
+ };
2998
+ }
2999
+ if ("payloads" in overrides) {
3000
+ if (false === overrides.payloads) this._payloadOverrides = void 0;
3001
+ else if (void 0 !== overrides.payloads) this._payloadOverrides = {
3002
+ ...overrides.payloads
3003
+ };
3004
+ }
3005
+ return;
3006
+ }
3007
+ this._flagOverrides = {
3008
+ ...overrides
3009
+ };
3010
+ }
3011
+ _isFeatureFlagOverrideOptions(overrides) {
3012
+ if ("object" != typeof overrides || null === overrides || Array.isArray(overrides)) return false;
3013
+ const obj = overrides;
3014
+ if ("flags" in obj) {
3015
+ const flagsValue = obj["flags"];
3016
+ if (false === flagsValue || Array.isArray(flagsValue) || "object" == typeof flagsValue && null !== flagsValue) return true;
3017
+ }
3018
+ if ("payloads" in obj) {
3019
+ const payloadsValue = obj["payloads"];
3020
+ if (false === payloadsValue || "object" == typeof payloadsValue && null !== payloadsValue) return true;
3021
+ }
3022
+ return false;
3023
+ }
2961
3024
  withContext(data, fn, options) {
2962
3025
  if (!this.context) return fn();
2963
3026
  return this.context.run(data, fn, options);
@@ -1,7 +1,7 @@
1
- import { loadModel, loadRbModel } from "@rpcbase/db";
1
+ import { models } from "@rpcbase/db";
2
2
  import { s as sendEmail } from "./email-DEw8keax.js";
3
3
  const routes = Object.entries({
4
- .../* @__PURE__ */ Object.assign({ "./api/notifications/handler.ts": () => import("./handler-B_mMDLBO.js") })
4
+ .../* @__PURE__ */ Object.assign({ "./api/notifications/handler.ts": () => import("./handler-BwK8qxLn.js") })
5
5
  }).reduce((acc, [path, mod]) => {
6
6
  acc[path.replace("./api/", "@rpcbase/server/notifications/api/")] = mod;
7
7
  return acc;
@@ -18,7 +18,7 @@ const createNotification = async (ctx, input) => {
18
18
  const topic = typeof input.topic === "string" ? input.topic.trim() : "";
19
19
  const body = typeof input.body === "string" ? input.body.trim() : "";
20
20
  const url = typeof input.url === "string" ? input.url.trim() : "";
21
- const NotificationModel = await loadModel("RBNotification", ctx);
21
+ const NotificationModel = await models.get("RBNotification", ctx);
22
22
  const doc = await NotificationModel.create({
23
23
  userId,
24
24
  ...topic ? { topic } : {},
@@ -57,8 +57,8 @@ const sendNotificationsDigestForUser = async (ctx, {
57
57
  userId,
58
58
  force = false
59
59
  }) => {
60
- const SettingsModel = await loadModel("RBNotificationSettings", ctx);
61
- const NotificationModel = await loadModel("RBNotification", ctx);
60
+ const SettingsModel = await models.get("RBNotificationSettings", ctx);
61
+ const NotificationModel = await models.get("RBNotification", ctx);
62
62
  const settings = await SettingsModel.findOne({ userId }).lean();
63
63
  const digestFrequencyRaw = typeof settings?.digestFrequency === "string" ? settings.digestFrequency : "weekly";
64
64
  const digestFrequency = digestFrequencyRaw === "daily" || digestFrequencyRaw === "weekly" || digestFrequencyRaw === "off" ? digestFrequencyRaw : "weekly";
@@ -92,7 +92,7 @@ const sendNotificationsDigestForUser = async (ctx, {
92
92
  );
93
93
  return { ok: true, sent: false, skippedReason: "empty" };
94
94
  }
95
- const UserModel = await loadRbModel("RBUser", ctx);
95
+ const UserModel = await models.getGlobal("RBUser", ctx);
96
96
  const user = await UserModel.findById(userId, { email: 1 }).lean();
97
97
  const email = typeof user?.email === "string" ? user.email.trim() : "";
98
98
  if (!email) {
package/dist/rts/index.js CHANGED
@@ -1,9 +1,9 @@
1
1
  import { randomUUID } from "node:crypto";
2
- import { loadRbModel, loadModel } from "@rpcbase/db";
2
+ import { models } from "@rpcbase/db";
3
3
  import { buildAbilityFromSession, getTenantRolesFromSessionUser, buildAbility, getAccessibleByQuery } from "@rpcbase/db/acl";
4
4
  import { WebSocketServer } from "ws";
5
5
  const routes = Object.entries({
6
- .../* @__PURE__ */ Object.assign({ "./api/changes/handler.ts": () => import("../handler-Dd20DHyz.js") })
6
+ .../* @__PURE__ */ Object.assign({ "./api/changes/handler.ts": () => import("../handler-CedzJJg0.js") })
7
7
  }).reduce((acc, [path, mod]) => {
8
8
  acc[path.replace("./api/", "@rpcbase/server/rts/api/")] = mod;
9
9
  return acc;
@@ -166,7 +166,7 @@ const parseUpgradeMeta = async ({
166
166
  throw new Error("Missing rb-user-id header (reverse-proxy) and no session middleware configured");
167
167
  }
168
168
  const rbCtx = { req: { session: null } };
169
- const User = await loadRbModel("RBUser", rbCtx);
169
+ const User = await models.getGlobal("RBUser", rbCtx);
170
170
  const user = await User.findById(headerUserId, { tenants: 1, tenantRoles: 1 }).lean();
171
171
  const tenantsRaw = user?.tenants;
172
172
  const tenants = Array.isArray(tenantsRaw) ? tenantsRaw.map((t) => String(t)) : [];
@@ -187,7 +187,7 @@ const getTenantModel = async (tenantId, modelName) => {
187
187
  }
188
188
  }
189
189
  };
190
- return loadModel(modelName, ctx);
190
+ return models.get(modelName, ctx);
191
191
  };
192
192
  const normalizeLimit = (limit) => {
193
193
  if (typeof limit !== "number") return QUERY_MAX_LIMIT;