gencow 0.1.172 → 0.1.174
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/bin/gencow.mjs +1 -1
- package/core/index.js +425 -33
- package/dashboard/assets/index-Bj6gz8_l.css +1 -0
- package/dashboard/assets/index-DbrOn9J8.js +403 -0
- package/dashboard/index.html +2 -2
- package/lib/codegen/index.mjs +471 -36
- package/lib/db-command.mjs +33 -11
- package/lib/files-command.mjs +7 -8
- package/lib/jobs-command.mjs +7 -9
- package/lib/readme-codegen.mjs +9 -9
- package/package.json +1 -1
- package/server/index.js +1484 -263
- package/server/index.js.map +4 -4
- package/templateFeature/ai/ai-facade.ts +1 -4
- package/templateFeature/ai/ai.ts +0 -1
- package/dashboard/assets/index-CXQUvEMI.js +0 -398
- package/dashboard/assets/index-D3kkc9W1.css +0 -1
package/bin/gencow.mjs
CHANGED
|
@@ -341,7 +341,7 @@ ${BOLD}Commands (login required):${RESET}
|
|
|
341
341
|
${DIM}--prod Push to production DB (confirmation required)${RESET}
|
|
342
342
|
${GREEN}db:generate${RESET} Generate SQL migration files from schema.ts
|
|
343
343
|
${GREEN}db:seed${RESET} Run gencow/seed.ts on cloud app
|
|
344
|
-
${DIM}--prod Seed production app${RESET}
|
|
344
|
+
${DIM}--prod Seed production app (confirmation required)${RESET}
|
|
345
345
|
${GREEN}static [dir]${RESET} Deploy static files only ${DIM}(dist/, out/, build/)${RESET}
|
|
346
346
|
${DIM}--prod Deploy to production app${RESET}
|
|
347
347
|
${DIM}--force, -f Skip optional dependency scan${RESET}
|
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
|
|
4258
|
-
|
|
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
|
|
4261
|
-
|
|
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:
|
|
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
|
|
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
|
-
|
|
4374
|
-
|
|
4375
|
-
|
|
4376
|
-
|
|
4377
|
-
|
|
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
|
|
4410
|
-
|
|
4411
|
-
|
|
4412
|
-
|
|
4413
|
-
|
|
4414
|
-
|
|
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
|
|
4440
|
-
|
|
4441
|
-
|
|
4442
|
-
|
|
4443
|
-
|
|
4444
|
-
|
|
4445
|
-
|
|
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
|
-
|
|
4449
|
-
|
|
4450
|
-
|
|
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 (
|
|
4471
|
-
|
|
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
|
});
|