gencow 0.1.171 → 0.1.173

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/core/index.js CHANGED
@@ -1016,6 +1016,26 @@ function buildRealtimeCtx(options) {
1016
1016
  eventId: input.eventId ?? createRealtimeEventId()
1017
1017
  });
1018
1018
  },
1019
+ invalidateCrudQuery(input) {
1020
+ _hasEmitted = true;
1021
+ const subscriptionKey = input.subscriptionKey ?? resolveRealtimeQuerySubscriptionKey({ query: input.query, args: input.args });
1022
+ if (!subscriptionKey)
1023
+ throw new Error(`Realtime query "${input.query}" resolved to an invalid subscriptionKey`);
1024
+ const scopeHash = input.scopeHash ?? resolveRealtimeQueryScopeHash({
1025
+ query: input.query,
1026
+ args: input.args,
1027
+ subscriptionKey
1028
+ });
1029
+ dispatchEvent({
1030
+ type: "query_invalidate",
1031
+ query: input.query,
1032
+ subscriptionKey,
1033
+ scopeHash,
1034
+ eventId: input.eventId ?? createRealtimeEventId(),
1035
+ legacyQueryKey: input.legacyQueryKey,
1036
+ crudRealtimeEmitMode: input.crudRealtimeEmitMode ?? options.generatedCrudRealtimeEmitMode ?? "dual"
1037
+ });
1038
+ },
1019
1039
  get _hasEmitted() {
1020
1040
  return _hasEmitted;
1021
1041
  },
@@ -1601,16 +1621,28 @@ function resolveTtlMs3(ttlSeconds) {
1601
1621
  function randomNonce3() {
1602
1622
  return randomBytes3(16).toString("base64url");
1603
1623
  }
1624
+ function resolveRealtimeCapabilityNegotiation(value) {
1625
+ const record = value && typeof value === "object" ? value : null;
1626
+ return {
1627
+ crudRealtimeMode: record?.crudRealtimeMode === "scoped" ? "scoped" : "legacy"
1628
+ };
1629
+ }
1604
1630
  function createRealtimeTicket(input) {
1605
1631
  validateRequiredString3(input.appName, "appName");
1606
1632
  validateRequiredString3(input.userId, "userId");
1607
1633
  validateRequiredString3(input.secret, "secret");
1608
1634
  const nowMs = input.nowMs ?? Date.now();
1635
+ const realtime = resolveRealtimeCapabilityNegotiation({
1636
+ crudRealtimeMode: input.realtime?.crudRealtimeMode
1637
+ });
1609
1638
  const payload = {
1610
1639
  v: 1,
1611
1640
  appName: input.appName,
1612
1641
  userId: input.userId,
1613
1642
  sessionId: input.sessionId ?? null,
1643
+ rt: {
1644
+ crudRealtimeMode: realtime.crudRealtimeMode
1645
+ },
1614
1646
  iat: nowMs,
1615
1647
  exp: nowMs + resolveTtlMs3(input.ttlSeconds),
1616
1648
  nonce: input.nonce ?? randomNonce3()
@@ -1653,7 +1685,8 @@ function verifyRealtimeTicket(ticket, input) {
1653
1685
  sessionId: typeof record.sessionId === "string" ? record.sessionId : null,
1654
1686
  nonce: record.nonce,
1655
1687
  issuedAt: record.iat,
1656
- expiresAt: record.exp
1688
+ expiresAt: record.exp,
1689
+ realtime: resolveRealtimeCapabilityNegotiation(record.rt)
1657
1690
  };
1658
1691
  }
1659
1692
 
@@ -2807,6 +2840,9 @@ var RESERVED_TENANT_RUNTIME_ENV_KEYS = /* @__PURE__ */ new Set([
2807
2840
  "GENCOW_STORAGE_REQUIRE_READ_GRANT",
2808
2841
  "GENCOW_STORAGE_BACKEND",
2809
2842
  "GENCOW_STORAGE_LEGACY_LOCAL_FALLBACK",
2843
+ "GENCOW_STORAGE_QUOTA_BYTES",
2844
+ "GENCOW_STORAGE_MAX_FILE_SIZE_BYTES",
2845
+ "GENCOW_STORAGE_API_UPLOAD_MAX_FILE_SIZE_BYTES",
2810
2846
  "GENCOW_MIGRATIONS",
2811
2847
  "GENCOW_APP_NAME",
2812
2848
  "GENCOW_APP_DATA_DIR",
@@ -4051,6 +4087,144 @@ function detectOwnerMeta2(table) {
4051
4087
  return null;
4052
4088
  }
4053
4089
 
4090
+ // ../core/src/createCrud.realtime.ts
4091
+ import { getTableConfig as getTableConfig3 } from "drizzle-orm/pg-core";
4092
+ var CRUD_LIST_BUCKET_QUERY = "__gencow.crud.list";
4093
+ var FORBIDDEN_SCOPE_KEYS = /* @__PURE__ */ new Set(["query", "subscriptionKey", "scopeHash", "__gencow"]);
4094
+ function createCrudRealtimeEventId() {
4095
+ const randomUUID = globalThis.crypto?.randomUUID;
4096
+ if (typeof randomUUID === "function") return `evt_${randomUUID.call(globalThis.crypto)}`;
4097
+ return `evt_${Date.now().toString(36)}_${Math.random().toString(36).slice(2)}`;
4098
+ }
4099
+ function resolveDefaultResourceIdentity(table, tableName) {
4100
+ try {
4101
+ const config = getTableConfig3(table);
4102
+ const schema = typeof config.schema === "string" && config.schema.trim().length > 0 ? config.schema.trim() : "public";
4103
+ return `${schema}.${config.name}`;
4104
+ } catch {
4105
+ return tableName.includes(".") ? tableName : `public.${tableName}`;
4106
+ }
4107
+ }
4108
+ function isPlainObject(value) {
4109
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
4110
+ const proto = Object.getPrototypeOf(value);
4111
+ return proto === Object.prototype || proto === null;
4112
+ }
4113
+ function assertJsonScopeValue(value, path) {
4114
+ if (value === void 0) return;
4115
+ if (value === null) return;
4116
+ if (typeof value === "string" || typeof value === "boolean") return;
4117
+ if (typeof value === "number") {
4118
+ if (!Number.isFinite(value)) throw new Error(`[createCrud] realtime scope "${path}" must be finite`);
4119
+ return;
4120
+ }
4121
+ if (Array.isArray(value)) {
4122
+ value.forEach((entry, index) => {
4123
+ if (entry === void 0) throw new Error(`[createCrud] realtime scope "${path}[${index}]" cannot be undefined`);
4124
+ assertJsonScopeValue(entry, `${path}[${index}]`);
4125
+ });
4126
+ return;
4127
+ }
4128
+ if (isPlainObject(value)) {
4129
+ for (const [key, child] of Object.entries(value)) {
4130
+ assertJsonScopeValue(child, path ? `${path}.${key}` : key);
4131
+ }
4132
+ return;
4133
+ }
4134
+ throw new Error(`[createCrud] realtime scope "${path}" must be JSON-serializable`);
4135
+ }
4136
+ function normalizeCrudScope(value, label) {
4137
+ if (!isPlainObject(value)) {
4138
+ throw new Error(`[createCrud] ${label} must return a non-null plain object scope`);
4139
+ }
4140
+ for (const key of Object.keys(value)) {
4141
+ if (FORBIDDEN_SCOPE_KEYS.has(key)) {
4142
+ throw new Error(`[createCrud] ${label} scope key "${key}" is reserved`);
4143
+ }
4144
+ }
4145
+ assertJsonScopeValue(value, "");
4146
+ return value;
4147
+ }
4148
+ function buildCrudListSubscriptionKey(input) {
4149
+ return buildQuerySubscriptionKey(CRUD_LIST_BUCKET_QUERY, {
4150
+ policy: input.policy,
4151
+ resourceIdentity: input.resourceIdentity,
4152
+ scope: input.scope
4153
+ });
4154
+ }
4155
+ function buildCrudListScopeHash(input) {
4156
+ const subscriptionKey = buildCrudListSubscriptionKey(input);
4157
+ return buildRealtimeQueryScopeHash({
4158
+ query: CRUD_LIST_BUCKET_QUERY,
4159
+ subscriptionKey,
4160
+ scope: {
4161
+ policy: input.policy,
4162
+ resourceIdentity: input.resourceIdentity,
4163
+ scope: input.scope
4164
+ }
4165
+ });
4166
+ }
4167
+ function resolveCrudRealtimeConfig(input) {
4168
+ const resourceIdentity = resolveDefaultResourceIdentity(input.table, input.tableName);
4169
+ const config = input.realtime;
4170
+ if (config === false) {
4171
+ return {
4172
+ enabled: false,
4173
+ listEnabled: false,
4174
+ listPolicy: false,
4175
+ resourceIdentity,
4176
+ explicitScopedList: false
4177
+ };
4178
+ }
4179
+ if (config === void 0) {
4180
+ return {
4181
+ enabled: true,
4182
+ listEnabled: true,
4183
+ listPolicy: input.defaultListPolicy,
4184
+ resourceIdentity,
4185
+ explicitScopedList: false,
4186
+ scope: () => ({}),
4187
+ scopeFromRow: async () => ({})
4188
+ };
4189
+ }
4190
+ if (!isPlainObject(config)) {
4191
+ throw new Error(`[createCrud] realtime must be false or an object`);
4192
+ }
4193
+ if (config.listRealtime === false) {
4194
+ return {
4195
+ enabled: true,
4196
+ listEnabled: false,
4197
+ listPolicy: false,
4198
+ resourceIdentity,
4199
+ explicitScopedList: false
4200
+ };
4201
+ }
4202
+ const policy = config.policy;
4203
+ if (policy !== "public" && policy !== "authorized") {
4204
+ throw new Error(`[createCrud] realtime.policy must be "public" or "authorized" when list realtime is enabled`);
4205
+ }
4206
+ const scope = config.scope;
4207
+ const scopeFromRow = config.scopeFromRow;
4208
+ if (typeof scope !== "function" || typeof scopeFromRow !== "function") {
4209
+ throw new Error(`[createCrud] realtime scope and scopeFromRow are required when list realtime is enabled`);
4210
+ }
4211
+ const authorize = config.authorize;
4212
+ if (policy === "authorized" && typeof authorize !== "function") {
4213
+ throw new Error(`[createCrud] realtime.authorize is required for authorized list realtime`);
4214
+ }
4215
+ const explicitResourceIdentity = typeof config.resourceIdentity === "string" && config.resourceIdentity.trim().length > 0 ? config.resourceIdentity.trim() : resourceIdentity;
4216
+ return {
4217
+ enabled: true,
4218
+ listEnabled: true,
4219
+ listPolicy: policy,
4220
+ resourceIdentity: explicitResourceIdentity,
4221
+ explicitScopedList: true,
4222
+ authorize,
4223
+ scope: (args) => normalizeCrudScope(scope(args), "realtime.scope"),
4224
+ scopeFromRow: async ({ row, ctx }) => normalizeCrudScope(await scopeFromRow({ row, ctx }), "realtime.scopeFromRow")
4225
+ };
4226
+ }
4227
+
4054
4228
  // ../core/src/createCrud.validators.ts
4055
4229
  function detectIdType2(column) {
4056
4230
  const colType = column.dataType;
@@ -4183,7 +4357,6 @@ function buildCrud(table, options, procedure) {
4183
4357
  const tableName = getTableName4(drizzleTable);
4184
4358
  const prefix = options?.prefix || tableName;
4185
4359
  const allowAnonymous = options?.allowAnonymous ?? false;
4186
- const useRealtime = options?.realtime ?? true;
4187
4360
  const defaultLimit = options?.defaultLimit ?? 20;
4188
4361
  const maxLimit = options?.maxLimit ?? 100;
4189
4362
  const pk = anyTable["id"];
@@ -4196,6 +4369,14 @@ function buildCrud(table, options, procedure) {
4196
4369
  const defaultOrderCol = createdAtCol || pk;
4197
4370
  const userIdCol = anyTable["userId"];
4198
4371
  const ownerMeta = detectOwnerMeta2(drizzleTable);
4372
+ const listNeedsAuth = !allowAnonymous || ownerMeta && !ownerMeta.readPublic;
4373
+ const defaultListRealtimePolicy = listNeedsAuth ? "authorized" : "public";
4374
+ const crudRealtime = resolveCrudRealtimeConfig({
4375
+ realtime: options?.realtime,
4376
+ defaultListPolicy: defaultListRealtimePolicy,
4377
+ table: drizzleTable,
4378
+ tableName
4379
+ });
4199
4380
  const disableILike = options?.disableILike === true;
4200
4381
  const cursorPropNames = options?.cursor ? [...options.cursor].map((c) => String(c)) : void 0;
4201
4382
  let sortablePropNames = options?.sortable && options.sortable.length > 0 ? new Set(options.sortable.map((c) => String(c))) : void 0;
@@ -4254,17 +4435,102 @@ function buildCrud(table, options, procedure) {
4254
4435
  }
4255
4436
  return await fn(db);
4256
4437
  }
4257
- function invalidateListRealtime(ctx) {
4258
- ctx.realtime.invalidate(`${prefix}.list`);
4438
+ function resolveListRealtimeBucket(scope) {
4439
+ if (!crudRealtime.listEnabled) return null;
4440
+ return {
4441
+ policy: crudRealtime.listPolicy,
4442
+ resourceIdentity: crudRealtime.resourceIdentity,
4443
+ scope,
4444
+ subscriptionKey: buildCrudListSubscriptionKey({
4445
+ policy: crudRealtime.listPolicy,
4446
+ resourceIdentity: crudRealtime.resourceIdentity,
4447
+ scope
4448
+ }),
4449
+ scopeHash: buildCrudListScopeHash({
4450
+ policy: crudRealtime.listPolicy,
4451
+ resourceIdentity: crudRealtime.resourceIdentity,
4452
+ scope
4453
+ })
4454
+ };
4259
4455
  }
4260
- function invalidateGetRealtime(ctx, id) {
4261
- ctx.realtime.invalidate(buildQuerySubscriptionKey(`${prefix}.get`, { id }));
4456
+ async function invalidateListRealtimeForScopes(ctx, input) {
4457
+ if (!crudRealtime.listEnabled || !enabledMethods.has("list")) return;
4458
+ const seen = /* @__PURE__ */ new Set();
4459
+ let firstError;
4460
+ for (const row of input.rows) {
4461
+ if (row == null) continue;
4462
+ let scope;
4463
+ try {
4464
+ scope = await crudRealtime.scopeFromRow({ row, ctx });
4465
+ } catch (error) {
4466
+ firstError ??= error;
4467
+ continue;
4468
+ }
4469
+ const bucket = resolveListRealtimeBucket(scope);
4470
+ if (!bucket || seen.has(bucket.scopeHash)) continue;
4471
+ seen.add(bucket.scopeHash);
4472
+ invalidateCrudRealtime(ctx, {
4473
+ legacyQueryKey: `${prefix}.list`,
4474
+ query: `${prefix}.list`,
4475
+ subscriptionKey: bucket.subscriptionKey,
4476
+ scopeHash: bucket.scopeHash,
4477
+ eventId: input.eventId
4478
+ });
4479
+ }
4480
+ if (firstError) throw firstError;
4481
+ }
4482
+ function reportCrudRealtimeInvalidationError(input) {
4483
+ console.error("[createCrud] realtime invalidation failed after mutation commit", {
4484
+ prefix,
4485
+ tableName,
4486
+ operation: input.operation,
4487
+ ...input.id !== void 0 ? { id: input.id } : {},
4488
+ error: input.error
4489
+ });
4490
+ }
4491
+ function invalidateGetRealtime(ctx, id, eventId) {
4492
+ invalidateCrudRealtime(ctx, {
4493
+ legacyQueryKey: buildQuerySubscriptionKey(`${prefix}.get`, { id }),
4494
+ query: `${prefix}.get`,
4495
+ args: { id },
4496
+ eventId
4497
+ });
4498
+ }
4499
+ function invalidateCrudRealtime(ctx, input) {
4500
+ const realtime = ctx.realtime;
4501
+ if (typeof realtime.invalidateCrudQuery === "function") {
4502
+ realtime.invalidateCrudQuery(input);
4503
+ return;
4504
+ }
4505
+ if (input.subscriptionKey && input.scopeHash && typeof realtime.invalidateScoped === "function") {
4506
+ realtime.invalidateScoped({
4507
+ query: input.query,
4508
+ args: input.args,
4509
+ subscriptionKey: input.subscriptionKey,
4510
+ scopeHash: input.scopeHash,
4511
+ eventId: input.eventId
4512
+ });
4513
+ return;
4514
+ }
4515
+ realtime.invalidate(input.legacyQueryKey);
4516
+ }
4517
+ async function selectFullRowForRealtime(tx, whereCond) {
4518
+ const [row] = await tx.select().from(anyTable).where(whereCond).limit(1);
4519
+ return row ?? null;
4262
4520
  }
4263
4521
  const pickCols = options?.columns?.pick;
4264
4522
  const omitCols = options?.columns?.omit;
4265
4523
  const listProjection = buildSelectProjection(drizzleCols, pickCols, omitCols);
4266
4524
  const getProjection = listProjection;
4267
4525
  const projectedColumns = listProjection !== void 0 ? listProjection : drizzleCols;
4526
+ function projectResultRow(row) {
4527
+ if (listProjection === void 0 || !row || typeof row !== "object") return row;
4528
+ const projected = {};
4529
+ for (const key of Object.keys(listProjection)) {
4530
+ projected[key] = row[key];
4531
+ }
4532
+ return projected;
4533
+ }
4268
4534
  const listRowOutput = buildRowOutputValidator(projectedColumns);
4269
4535
  const updateOutputSchema = v.nullable(listRowOutput);
4270
4536
  const getOutput = v.nullable(listRowOutput);
@@ -4289,7 +4555,10 @@ function buildCrud(table, options, procedure) {
4289
4555
  cursor: cursorPropNames,
4290
4556
  ...disableILike ? { disableILike: true } : {},
4291
4557
  allowAnonymous,
4292
- realtime: useRealtime
4558
+ realtime: crudRealtime.enabled,
4559
+ listRealtime: crudRealtime.listEnabled,
4560
+ realtimePolicy: crudRealtime.listPolicy,
4561
+ resourceIdentity: crudRealtime.resourceIdentity
4293
4562
  };
4294
4563
  function assertOrderBy(orderByProp) {
4295
4564
  if (!orderByProp) return;
@@ -4310,8 +4579,7 @@ function buildCrud(table, options, procedure) {
4310
4579
  })
4311
4580
  ).output(listOutput).handler(async ({ context, input }) => {
4312
4581
  await assertReadAccess(context);
4313
- const needsAuth = !allowAnonymous || ownerMeta && !ownerMeta.readPublic;
4314
- const user = needsAuth ? context.auth.requireAuth() : null;
4582
+ const user = listNeedsAuth ? context.auth.requireAuth() : null;
4315
4583
  let whereClause = buildWhereConditions(input);
4316
4584
  if (ownerMeta && !ownerMeta.readPublic && user) {
4317
4585
  const ownerFilter = eq3(ownerMeta.column, user.id);
@@ -4370,11 +4638,44 @@ function buildCrud(table, options, procedure) {
4370
4638
  return { data: results, total, nextCursor };
4371
4639
  });
4372
4640
  });
4373
- const getDef = !enabledMethods.has("get") ? void 0 : (allowAnonymous ? query2.allowAnonymous() : query2).name(`${prefix}.get`).input(v.object({ id: idValidator })).output(getOutput).handler(async ({ context, input }) => {
4374
- await assertReadAccess(context);
4375
- const needsAuth = !allowAnonymous || ownerMeta && !ownerMeta.readPublic;
4376
- const user = needsAuth ? context.auth.requireAuth() : null;
4377
- let whereCond = eq3(pk, input.id);
4641
+ if (listDef) {
4642
+ listDef.realtimePolicy = crudRealtime.listEnabled ? crudRealtime.listPolicy : false;
4643
+ }
4644
+ if (listDef && crudRealtime.listEnabled) {
4645
+ defineRealtimeSubscriptions({
4646
+ [listDef.name]: {
4647
+ policy: crudRealtime.listPolicy,
4648
+ args: listDef.inputSchema,
4649
+ subscriptionKey: (args) => {
4650
+ const scope = crudRealtime.scope(args);
4651
+ return buildCrudListSubscriptionKey({
4652
+ policy: crudRealtime.listPolicy,
4653
+ resourceIdentity: crudRealtime.resourceIdentity,
4654
+ scope
4655
+ });
4656
+ },
4657
+ scope: (args) => {
4658
+ const scope = crudRealtime.scope(args);
4659
+ return {
4660
+ policy: crudRealtime.listPolicy,
4661
+ resourceIdentity: crudRealtime.resourceIdentity,
4662
+ scope
4663
+ };
4664
+ },
4665
+ authorize: async (ctx, args) => {
4666
+ if (crudRealtime.explicitScopedList) {
4667
+ await crudRealtime.authorize?.({ args, ctx });
4668
+ } else {
4669
+ await assertReadAccess(ctx);
4670
+ }
4671
+ }
4672
+ }
4673
+ });
4674
+ }
4675
+ const getNeedsAuth = !allowAnonymous || ownerMeta && !ownerMeta.readPublic;
4676
+ const getRealtimePolicy = getNeedsAuth ? "authorized" : "public";
4677
+ function buildGetWhereCondition(id, user) {
4678
+ let whereCond = eq3(pk, id);
4378
4679
  if (ownerMeta && !ownerMeta.readPublic && user) {
4379
4680
  whereCond = and3(whereCond, eq3(ownerMeta.column, user.id));
4380
4681
  }
@@ -4382,12 +4683,46 @@ function buildCrud(table, options, procedure) {
4382
4683
  const sdField = anyTable[options.softDelete.field];
4383
4684
  whereCond = and3(whereCond, eq3(sdField, null));
4384
4685
  }
4686
+ return whereCond;
4687
+ }
4688
+ async function canAuthorizeGeneratedGetRealtime(ctx, id) {
4689
+ await assertReadAccess(ctx);
4690
+ const user = getNeedsAuth ? ctx.auth.requireAuth() : null;
4691
+ const whereCond = buildGetWhereCondition(id, user);
4692
+ const row = await inRlsOrPlainTx(ctx.db, async (tx) => {
4693
+ const [result] = await tx.select({ id: pk }).from(anyTable).where(whereCond).limit(1);
4694
+ return result ?? null;
4695
+ });
4696
+ return row !== null;
4697
+ }
4698
+ const getDef = !enabledMethods.has("get") ? void 0 : (allowAnonymous ? query2.allowAnonymous() : query2).name(`${prefix}.get`).input(v.object({ id: idValidator })).output(getOutput).handler(async ({ context, input }) => {
4699
+ await assertReadAccess(context);
4700
+ const user = getNeedsAuth ? context.auth.requireAuth() : null;
4701
+ const whereCond = buildGetWhereCondition(input.id, user);
4385
4702
  return await inRlsOrPlainTx(context.db, async (tx) => {
4386
4703
  const qb = getProjection !== void 0 ? tx.select(getProjection).from(anyTable) : tx.select().from(anyTable);
4387
4704
  const [result] = await qb.where(whereCond).limit(1);
4388
4705
  return result ?? null;
4389
4706
  });
4390
4707
  });
4708
+ if (getDef) {
4709
+ getDef.realtimePolicy = crudRealtime.enabled ? getRealtimePolicy : false;
4710
+ }
4711
+ if (getDef && crudRealtime.enabled) {
4712
+ defineRealtimeSubscriptions({
4713
+ [getDef.name]: {
4714
+ policy: getRealtimePolicy,
4715
+ args: getDef.inputSchema,
4716
+ authorize: async (ctx, args) => {
4717
+ const allowed = await canAuthorizeGeneratedGetRealtime(
4718
+ ctx,
4719
+ args.id
4720
+ );
4721
+ return allowed ? void 0 : false;
4722
+ }
4723
+ }
4724
+ });
4725
+ }
4391
4726
  const createDef = !enabledMethods.has("create") ? void 0 : (allowAnonymous ? mutation2.allowAnonymous() : mutation2).name(`${prefix}.create`).input(createInputValidator).output(listRowOutput).handler(async ({ context, input }) => {
4392
4727
  await denyUnless(options?.access?.create, context);
4393
4728
  const user = ownerMeta || !allowAnonymous ? context.auth.requireAuth() : null;
@@ -4406,12 +4741,31 @@ function buildCrud(table, options, procedure) {
4406
4741
  if (options?.hooks?.beforeCreate) {
4407
4742
  insertData = await options.hooks.beforeCreate(insertData);
4408
4743
  }
4409
- const [result] = await inRlsOrPlainTx(
4410
- context.db,
4411
- async (tx) => listProjection !== void 0 ? tx.insert(anyTable).values(insertData).returning(listProjection) : tx.insert(anyTable).values(insertData).returning()
4412
- );
4413
- if (useRealtime && enabledMethods.has("list")) {
4414
- invalidateListRealtime(context);
4744
+ const { result, realtimeRow } = await inRlsOrPlainTx(context.db, async (tx) => {
4745
+ if (crudRealtime.explicitScopedList) {
4746
+ const [insertedFullRow] = await tx.insert(anyTable).values(insertData).returning();
4747
+ return {
4748
+ result: projectResultRow(insertedFullRow),
4749
+ realtimeRow: insertedFullRow ?? null
4750
+ };
4751
+ }
4752
+ const [inserted] = listProjection !== void 0 ? await tx.insert(anyTable).values(insertData).returning(listProjection) : await tx.insert(anyTable).values(insertData).returning();
4753
+ return { result: inserted, realtimeRow: inserted ?? null };
4754
+ });
4755
+ if (crudRealtime.enabled && realtimeRow) {
4756
+ const mutationEventId = createCrudRealtimeEventId();
4757
+ try {
4758
+ await invalidateListRealtimeForScopes(context, {
4759
+ rows: [realtimeRow],
4760
+ eventId: mutationEventId
4761
+ });
4762
+ } catch (error) {
4763
+ reportCrudRealtimeInvalidationError({
4764
+ operation: "create",
4765
+ id: realtimeRow?.[pkPropName],
4766
+ error
4767
+ });
4768
+ }
4415
4769
  }
4416
4770
  return result;
4417
4771
  });
@@ -4436,18 +4790,35 @@ function buildCrud(table, options, procedure) {
4436
4790
  if (options?.hooks?.beforeUpdate) {
4437
4791
  updateData = await options.hooks.beforeUpdate(updateData);
4438
4792
  }
4439
- const [result] = await inRlsOrPlainTx(
4440
- context.db,
4441
- async (tx) => listProjection !== void 0 ? tx.update(anyTable).set(updateData).where(updateWhere).returning(listProjection) : tx.update(anyTable).set(updateData).where(updateWhere).returning()
4442
- );
4443
- if (useRealtime) {
4444
- if (enabledMethods.has("list")) {
4445
- invalidateListRealtime(context);
4793
+ const { result, preRealtimeRow, postRealtimeRow } = await inRlsOrPlainTx(context.db, async (tx) => {
4794
+ const preRow = crudRealtime.explicitScopedList ? await selectFullRowForRealtime(tx, updateWhere) : null;
4795
+ const [updated] = listProjection !== void 0 ? await tx.update(anyTable).set(updateData).where(updateWhere).returning(listProjection) : await tx.update(anyTable).set(updateData).where(updateWhere).returning();
4796
+ let postRow = updated ?? null;
4797
+ if (crudRealtime.explicitScopedList && updated) {
4798
+ postRow = await selectFullRowForRealtime(tx, eq3(pk, idVal));
4799
+ }
4800
+ return { result: updated, preRealtimeRow: preRow, postRealtimeRow: postRow };
4801
+ });
4802
+ if (crudRealtime.enabled) {
4803
+ const mutationEventId = createCrudRealtimeEventId();
4804
+ let listInvalidationError;
4805
+ try {
4806
+ await invalidateListRealtimeForScopes(context, {
4807
+ rows: [preRealtimeRow, postRealtimeRow].filter(Boolean),
4808
+ eventId: mutationEventId
4809
+ });
4810
+ } catch (error) {
4811
+ listInvalidationError = error;
4446
4812
  }
4447
4813
  if (enabledMethods.has("get")) {
4448
- const getKey = buildQuerySubscriptionKey(`${prefix}.get`, { id: idVal });
4449
- if (allowAnonymous && !ownerMeta) context.realtime.emit(getKey, result);
4450
- else invalidateGetRealtime(context, idVal);
4814
+ invalidateGetRealtime(context, idVal, mutationEventId);
4815
+ }
4816
+ if (listInvalidationError) {
4817
+ reportCrudRealtimeInvalidationError({
4818
+ operation: "update",
4819
+ id: idVal,
4820
+ error: listInvalidationError
4821
+ });
4451
4822
  }
4452
4823
  }
4453
4824
  return result ?? null;
@@ -4459,16 +4830,37 @@ function buildCrud(table, options, procedure) {
4459
4830
  if (ownerMeta && user) {
4460
4831
  deleteWhere = and3(eq3(pk, input.id), eq3(ownerMeta.column, user.id));
4461
4832
  }
4462
- await inRlsOrPlainTx(context.db, async (tx) => {
4833
+ const preRealtimeRow = await inRlsOrPlainTx(context.db, async (tx) => {
4834
+ const preRow = crudRealtime.explicitScopedList ? await selectFullRowForRealtime(tx, deleteWhere) : null;
4463
4835
  if (options?.softDelete) {
4464
4836
  const sdField = options.softDelete.field;
4465
4837
  await tx.update(anyTable).set({ [sdField]: /* @__PURE__ */ new Date() }).where(deleteWhere);
4466
4838
  } else {
4467
4839
  await tx.delete(anyTable).where(deleteWhere);
4468
4840
  }
4841
+ return preRow;
4469
4842
  });
4470
- if (useRealtime && enabledMethods.has("list")) {
4471
- invalidateListRealtime(context);
4843
+ if (crudRealtime.enabled) {
4844
+ const mutationEventId = createCrudRealtimeEventId();
4845
+ let listInvalidationError;
4846
+ try {
4847
+ await invalidateListRealtimeForScopes(context, {
4848
+ rows: crudRealtime.explicitScopedList ? [preRealtimeRow] : [{ [pkPropName]: input.id }],
4849
+ eventId: mutationEventId
4850
+ });
4851
+ } catch (error) {
4852
+ listInvalidationError = error;
4853
+ }
4854
+ if (enabledMethods.has("get")) {
4855
+ invalidateGetRealtime(context, input.id, mutationEventId);
4856
+ }
4857
+ if (listInvalidationError) {
4858
+ reportCrudRealtimeInvalidationError({
4859
+ operation: "remove",
4860
+ id: input.id,
4861
+ error: listInvalidationError
4862
+ });
4863
+ }
4472
4864
  }
4473
4865
  return { success: true };
4474
4866
  });