@devmoods/postgres-admin 0.20.0 → 0.21.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.
package/dist/index.js CHANGED
@@ -3,17 +3,22 @@ import { promises as fs } from "fs";
3
3
  import * as path from "path";
4
4
  import {
5
5
  BadRequest,
6
- createPostgres,
6
+ createPostgres as createPostgres2,
7
7
  generateCsrfToken,
8
8
  joinStatements,
9
- sql,
9
+ sql as sql2,
10
10
  staticMiddleware,
11
11
  createRouter
12
12
  } from "@devmoods/express-extras";
13
13
  import { type } from "arktype";
14
14
 
15
15
  // src/api/structure.ts
16
- import { NotFound, readEnv } from "@devmoods/express-extras";
16
+ import {
17
+ createPostgres,
18
+ NotFound,
19
+ readEnv,
20
+ sql
21
+ } from "@devmoods/express-extras";
17
22
  import "pg-structure";
18
23
  import { pgStructure } from "pg-structure/dist/main.js";
19
24
  var getDisplayType = (dbType) => {
@@ -58,33 +63,40 @@ var getPrimitiveType = (dbType) => {
58
63
  case "uuid":
59
64
  return "string";
60
65
  case "integer":
61
- case "numeric":
62
66
  case "smallint":
63
- case "bigint":
64
67
  return "number";
68
+ case "numeric":
69
+ case "bigint":
70
+ return "string";
65
71
  case "boolean":
66
72
  return "boolean";
67
73
  default:
68
74
  return "string";
69
75
  }
70
76
  };
71
- function stripStringValue(value) {
72
- if (value.startsWith("nextval(")) {
73
- return null;
77
+ function getDefault(column) {
78
+ const value = column.default;
79
+ if (typeof value !== "string") {
80
+ return value;
74
81
  }
75
- if (value.includes("now()")) {
76
- return null;
82
+ if (value.startsWith("'") && value.endsWith("'")) {
83
+ return value.slice(1, -1).replaceAll("''", "'");
77
84
  }
78
- return value.slice(1, -1);
79
- }
80
- function getDefault(value) {
81
- if (typeof value === "string") {
82
- return stripStringValue(value);
85
+ if (column.type.name === "boolean") {
86
+ if (value === "true") {
87
+ return true;
88
+ }
89
+ if (value === "false") {
90
+ return false;
91
+ }
83
92
  }
84
- return value;
93
+ if (/^[+-]?(?:\d+\.?\d*|\.\d+)$/.test(value)) {
94
+ return ["numeric", "bigint"].includes(column.type.name) ? value : Number(value);
95
+ }
96
+ return void 0;
85
97
  }
86
- function toJsonSchemaProperty(column) {
87
- const defaultValue = getDefault(column.default);
98
+ function toJsonSchemaProperty(column, tableId) {
99
+ const defaultValue = getDefault(column);
88
100
  const type2 = getPrimitiveType(column.type.name);
89
101
  const description = column.comment;
90
102
  if (column.arrayDimension > 0) {
@@ -92,33 +104,38 @@ function toJsonSchemaProperty(column) {
92
104
  type: "array",
93
105
  title: column.name,
94
106
  items: { type: type2 },
95
- default: defaultValue || [],
96
107
  description
97
108
  };
98
109
  }
99
110
  const $bitiforms = {};
100
111
  if (column.isForeignKey) {
112
+ const foreignKey = column.foreignKeys.at(0);
101
113
  $bitiforms.widget = "ForeignKeyWidget";
102
114
  $bitiforms.widgetProps = {
103
- foreignTable: column.foreignKeys.at(0)?.referencedTable.name,
104
- foreignColumn: column.foreignKeys.at(0)?.referencedColumns.at(0)?.name
115
+ foreignTable: foreignKey ? tableId(foreignKey.referencedTable) : void 0,
116
+ foreignColumn: foreignKey?.referencedColumns.at(0)?.name,
117
+ nullable: !column.notNull
105
118
  };
106
119
  }
107
120
  return {
108
121
  type: type2,
109
122
  title: column.name,
110
- default: defaultValue,
123
+ ...defaultValue === void 0 ? {} : { default: defaultValue },
111
124
  description,
112
125
  $bitiforms
113
126
  };
114
127
  }
115
128
  var tableCache = /* @__PURE__ */ new Map();
116
- function toJsonSchema(table) {
129
+ function toJsonSchema(table, tableId, identityColumns) {
117
130
  const required = [];
118
131
  const properties = {};
119
132
  for (const column of table.columns) {
120
- properties[column.name] = toJsonSchemaProperty(column);
121
- if (column.notNull && !column.isSerial) {
133
+ const columnId = `${table.schema.name}\0${table.name}\0${column.name}`;
134
+ if (column.isGenerated || identityColumns.has(columnId)) {
135
+ continue;
136
+ }
137
+ properties[column.name] = toJsonSchemaProperty(column, tableId);
138
+ if (column.notNull && !column.isSerial && column.default == null) {
122
139
  required.push(column.name);
123
140
  }
124
141
  }
@@ -128,40 +145,70 @@ function toJsonSchema(table) {
128
145
  required
129
146
  };
130
147
  }
131
- async function getTable(tableId) {
132
- if (!tableCache.has(tableId)) {
133
- const { tables } = await readTables();
134
- for (const table of tables) {
135
- tableCache.set(table.tableName, table);
136
- }
148
+ async function getTable(tableId, connectionString = readEnv("POSTGRES_URL")) {
149
+ const cacheKey = `${connectionString}\0${tableId}`;
150
+ if (!tableCache.has(cacheKey)) {
151
+ await readTables(connectionString);
137
152
  }
138
- const value = tableCache.get(tableId);
153
+ const value = tableCache.get(cacheKey);
139
154
  if (value == null) {
140
155
  throw NotFound();
141
156
  }
142
157
  return value;
143
158
  }
144
- async function readTables() {
145
- const connectionString = readEnv("POSTGRES_URL");
146
- const structure = await pgStructure({
147
- connectionString
148
- });
159
+ async function readTables(connectionString = readEnv("POSTGRES_URL")) {
160
+ const [structure, identityRows] = await Promise.all([
161
+ pgStructure({ connectionString }),
162
+ (async () => {
163
+ const postgres = createPostgres({ url: connectionString });
164
+ try {
165
+ return await postgres.all(sql`
166
+ SELECT table_schema, table_name, column_name
167
+ FROM information_schema.columns
168
+ WHERE is_identity = 'YES'
169
+ `);
170
+ } finally {
171
+ await postgres.end();
172
+ }
173
+ })()
174
+ ]);
175
+ const identityColumns = new Set(
176
+ identityRows.map(
177
+ (row) => `${row.table_schema}\0${row.table_name}\0${row.column_name}`
178
+ )
179
+ );
180
+ const tableId = (table) => `${table.schema.name}.${table.name}`;
149
181
  const toMetadata = (column) => ({
150
182
  type: getPrimitiveType(column.type.name),
151
183
  displayType: getDisplayType(column.type.name),
152
184
  dbType: column.type.name,
153
- foreignKeys: column.foreignKeys.mapToArray((fk) => ({
185
+ hasDefault: column.default != null || column.isSerial,
186
+ isGenerated: column.isGenerated,
187
+ isIdentity: identityColumns.has(
188
+ `${column.table.schema.name}\0${column.table.name}\0${column.name}`
189
+ ),
190
+ nullable: !column.notNull,
191
+ foreignKeys: column.foreignKeys.filter(
192
+ (fk) => fk.columns.length === 1 && fk.referencedColumns.length === 1
193
+ ).mapToArray((fk) => ({
154
194
  table: fk.referencedTable.name,
195
+ tableId: tableId(fk.referencedTable),
155
196
  column: fk.referencedColumns.at(0).name
156
197
  }))
157
198
  });
158
199
  const toBelongsTo = (table) => {
159
- return table.belongsToTables.mapToArray((t) => {
160
- const foreignKey = t.getForeignKeysFrom(table.name).mapToArray((fk) => fk.columns.at(0)?.name).at(0);
161
- const belongsToTableColumns = t.columns.mapToArray((c) => c.name);
200
+ return table.foreignKeys.filter(
201
+ (fk) => fk.columns.length === 1 && fk.referencedColumns.length === 1
202
+ ).mapToArray((fk) => {
203
+ const foreignKey = fk.columns.at(0).name;
204
+ const referencedTable = fk.referencedTable;
205
+ const belongsToTableColumns = referencedTable.columns.mapToArray(
206
+ (c) => c.name
207
+ );
162
208
  return {
163
209
  foreignKey,
164
- belongsToTable: t.name,
210
+ belongsToTable: referencedTable.name,
211
+ belongsToTableId: tableId(referencedTable),
165
212
  includeColumnNames: belongsToTableColumns.map(
166
213
  (c) => `${foreignKey.replace(/_id$/, "")}__${c}`
167
214
  )
@@ -172,18 +219,33 @@ async function readTables() {
172
219
  for (const table of structure.tables) {
173
220
  const belongsTo = toBelongsTo(table);
174
221
  const columnsArray = table.columns.mapToArray((column) => column);
222
+ const primaryKeyColumns = table.primaryKey?.columns.mapToArray((column) => column.name) ?? [];
175
223
  tables.push({
224
+ tableId: tableId(table),
225
+ schemaName: table.schema.name,
176
226
  tableName: table.name,
177
- primaryKey: table.primaryKey?.columns.at(0)?.name ?? null,
227
+ primaryKey: primaryKeyColumns.length === 1 ? primaryKeyColumns.at(0) : null,
178
228
  belongsTo,
179
229
  columnNames: columnsArray.map((column) => column.name),
180
230
  relatedColumnNames: belongsTo.flatMap((b) => b.includeColumnNames),
181
231
  columnMetadata: Object.fromEntries(
182
232
  columnsArray.map((column) => [column.name, toMetadata(column)])
183
233
  ),
184
- schema: toJsonSchema(table)
234
+ schema: toJsonSchema(table, tableId, identityColumns)
185
235
  });
186
236
  }
237
+ const cachePrefix = `${connectionString}\0`;
238
+ for (const key of tableCache.keys()) {
239
+ if (key.startsWith(cachePrefix)) {
240
+ tableCache.delete(key);
241
+ }
242
+ }
243
+ for (const table of tables) {
244
+ tableCache.set(`${cachePrefix}${table.tableId}`, table);
245
+ if (table.schemaName === "public") {
246
+ tableCache.set(`${cachePrefix}${table.tableName}`, table);
247
+ }
248
+ }
187
249
  return {
188
250
  databaseName: new URL(connectionString).pathname.slice(1),
189
251
  tables
@@ -191,9 +253,6 @@ async function readTables() {
191
253
  }
192
254
 
193
255
  // src/api/utils.ts
194
- function stripIdentifier(tableName) {
195
- return (tableName ?? "").replace(/[^\w+]/g, "");
196
- }
197
256
  function parseOrderBy(order) {
198
257
  if (!order) {
199
258
  return null;
@@ -224,32 +283,48 @@ function counter() {
224
283
  };
225
284
  }
226
285
 
286
+ // src/basename.ts
287
+ function normalizeBasename(basename) {
288
+ return basename.replace(/^\/+|\/+$/g, "");
289
+ }
290
+
227
291
  // src/api/app.ts
292
+ function escapeHtml(value) {
293
+ return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
294
+ }
295
+ function qualifiedTable(table) {
296
+ return sql2`${sql2.identifier(table.schemaName)}.${sql2.identifier(table.tableName)}`;
297
+ }
228
298
  function createAdminRouter({
229
299
  postgres: defaultPostgres,
230
300
  basename = "/admin",
231
301
  indexHtml = "node_modules/@devmoods/postgres-admin/dist/client/_index.html",
232
302
  uiOptions = {}
233
303
  }) {
234
- const postgres = defaultPostgres ?? createPostgres();
304
+ const postgres = defaultPostgres ?? createPostgres2();
235
305
  const router = createRouter();
236
306
  if (!basename.startsWith("/")) {
237
307
  throw new Error(`Option 'basename' must start with a slash /`);
238
308
  }
239
- const unknownTableRow = type(
240
- "Record<string, string | number | boolean | null>"
241
- );
309
+ const clientBasename = normalizeBasename(basename);
310
+ const unknownTableRow = type("Record<string, unknown>");
242
311
  router.get("/api/tables", {}, async () => {
243
- const { databaseName, tables } = await readTables();
312
+ const { databaseName, tables } = await readTables(
313
+ postgres.connectionString
314
+ );
315
+ const tableCounts = await Promise.all(
316
+ tables.map(async (table) => {
317
+ return [
318
+ table,
319
+ await postgres.get(sql2`SELECT COUNT(*) FROM ${qualifiedTable(table)}`).then((r) => +r.count)
320
+ ];
321
+ })
322
+ );
244
323
  const rowCounts = Object.fromEntries(
245
- await Promise.all(
246
- tables.map(async (table) => {
247
- return [
248
- table.tableName,
249
- await postgres.get(sql`SELECT COUNT(*) FROM ${sql.identifier(table.tableName)}`).then((r) => +r.count)
250
- ];
251
- })
252
- )
324
+ tableCounts.flatMap(([table, count]) => [
325
+ [table.tableId, count],
326
+ ...table.schemaName === "public" ? [[table.tableName, count]] : []
327
+ ])
253
328
  );
254
329
  return {
255
330
  databaseName,
@@ -258,19 +333,19 @@ function createAdminRouter({
258
333
  };
259
334
  });
260
335
  router.get("/api/tables/:table/suggestions/:column", {}, async (req) => {
261
- const table = stripIdentifier(req.params.table);
262
- const definition = await getTable(table);
263
- const column = stripIdentifier(req.params.column);
336
+ const table = req.params.table;
337
+ const definition = await getTable(table, postgres.connectionString);
338
+ const column = req.params.column;
264
339
  if (!definition.columnNames.includes(column)) {
265
340
  throw BadRequest(
266
341
  `Invalid column ${column}. Must be one of: ${definition.columnNames.join(", ")}`
267
342
  );
268
343
  }
269
- const statement = sql`
270
- SELECT DISTINCT ${sql.identifier(column)}, *
271
- FROM ${sql.identifier(table)}
272
- LIMIT 500
344
+ const statement = sql2`
345
+ SELECT *
346
+ FROM ${qualifiedTable(definition)}
273
347
  `;
348
+ statement.append(sql2` ORDER BY ${sql2.identifier(column)} LIMIT 500`);
274
349
  return postgres.all(statement);
275
350
  });
276
351
  router.get(
@@ -284,12 +359,18 @@ function createAdminRouter({
284
359
  })
285
360
  },
286
361
  async (req) => {
287
- const table = stripIdentifier(req.params.table);
362
+ const table = req.params.table;
288
363
  const orders = queryParamToArray(req.query.order);
289
364
  const limit = parseInt(req.query.limit ?? "50");
290
365
  const page = parseInt(req.query.page ?? "1");
291
366
  const search = String(req.query.search || "");
292
- const definition = await getTable(table);
367
+ if (limit < 1 || limit > 500) {
368
+ throw BadRequest("Limit must be between 1 and 500");
369
+ }
370
+ if (page < 1) {
371
+ throw BadRequest("Page must be 1 or greater");
372
+ }
373
+ const definition = await getTable(table, postgres.connectionString);
293
374
  const orderBy = orders.map((order) => parseOrderBy(order)).filter((x) => x != null);
294
375
  const allowedColumnNames = definition.columnNames.concat(
295
376
  definition.relatedColumnNames
@@ -299,13 +380,15 @@ function createAdminRouter({
299
380
  `Invalid order value ${orderBy.map((x) => x.column).join(", ")}. Must be one of: ${allowedColumnNames.join(", ")}`
300
381
  );
301
382
  }
302
- const fieldsToSelect = [sql`${sql.identifier(table)}.*`];
383
+ const fieldsToSelect = [sql2`${sql2.identifier("__source")}.*`];
303
384
  const foreignTables = Object.fromEntries(
304
385
  await Promise.all(
305
386
  definition.columnNames.flatMap((column) => {
306
387
  const metadata = definition.columnMetadata[column];
307
- return metadata.foreignKeys.map((x) => x.table);
308
- }).map(async (t) => [t, await getTable(t)])
388
+ return metadata.foreignKeys.map((x) => x.tableId);
389
+ }).map(
390
+ async (t) => [t, await getTable(t, postgres.connectionString)]
391
+ )
309
392
  )
310
393
  );
311
394
  const aliasCounter = counter();
@@ -313,7 +396,7 @@ function createAdminRouter({
313
396
  for (const column of definition.columnNames) {
314
397
  const metadata = definition.columnMetadata[column];
315
398
  for (const {
316
- table: fkTable,
399
+ tableId: fkTable,
317
400
  column: fkColumn
318
401
  } of metadata.foreignKeys) {
319
402
  const alias = `${fkTable}_${aliasCounter.get(fkTable)}`;
@@ -321,79 +404,89 @@ function createAdminRouter({
321
404
  const prefix = column.replace(/_id$/, "");
322
405
  for (const foreignColumn of foreignColumns) {
323
406
  fieldsToSelect.push(
324
- sql`${sql.identifier(alias)}.${sql.identifier(foreignColumn)} AS ${sql.identifier(`${prefix}__${foreignColumn}`)}`
407
+ sql2`${sql2.identifier(alias)}.${sql2.identifier(foreignColumn)} AS ${sql2.identifier(`${prefix}__${foreignColumn}`)}`
325
408
  );
326
409
  }
327
410
  joins.push(
328
- sql`
329
- LEFT JOIN ${sql.identifier(fkTable)} ${sql.identifier(alias)}
330
- ON ${sql.identifier(alias)}.${sql.identifier(fkColumn)} = ${sql.identifier(table)}.${sql.identifier(column)}`
411
+ sql2`
412
+ LEFT JOIN ${qualifiedTable(foreignTables[fkTable])} ${sql2.identifier(alias)}
413
+ ON ${sql2.identifier(alias)}.${sql2.identifier(fkColumn)} = ${sql2.identifier("__source")}.${sql2.identifier(column)}`
331
414
  );
332
415
  }
333
416
  }
334
- const statement = sql`
417
+ const statement = sql2`
335
418
  SELECT ${joinStatements(fieldsToSelect, ", ")}
336
- FROM ${sql.identifier(table)} ${joinStatements(joins, " ")}
419
+ FROM ${qualifiedTable(definition)} ${sql2.identifier("__source")} ${joinStatements(joins, " ")}
337
420
  `;
338
421
  if (search && search.length > 2) {
339
422
  statement.append(" WHERE ");
340
423
  statement.append(
341
424
  joinStatements(
342
425
  definition.columnNames.map(
343
- (c) => sql`${sql.identifier(table)}.${sql.identifier(c)}::text ILIKE ${`%${search}%`}`
426
+ (c) => sql2`${sql2.identifier("__source")}.${sql2.identifier(c)}::text ILIKE ${`%${search}%`}`
344
427
  ),
345
428
  " OR "
346
429
  )
347
430
  );
348
431
  }
349
432
  if (orderBy.length > 0) {
350
- statement.append(sql` ORDER BY `);
433
+ statement.append(sql2` ORDER BY `);
351
434
  statement.append(
352
435
  joinStatements(
353
- orderBy.map(
354
- (o) => sql`${sql.identifier(table)}.${sql.identifier(o.column)} ${sql.raw(o.direction)}`
355
- ),
436
+ orderBy.map((o) => {
437
+ const column = definition.columnNames.includes(o.column) ? sql2`${sql2.identifier("__source")}.${sql2.identifier(o.column)}` : sql2`${sql2.identifier(o.column)}`;
438
+ return sql2`${column} ${sql2.raw(o.direction)}`;
439
+ }),
356
440
  ", "
357
441
  )
358
442
  );
359
443
  } else if (definition.primaryKey) {
360
444
  statement.append(
361
- sql` ORDER BY ${sql.identifier(table)}.${sql.identifier(definition.primaryKey)}`
445
+ sql2` ORDER BY ${sql2.identifier("__source")}.${sql2.identifier(definition.primaryKey)}`
362
446
  );
363
447
  }
364
- statement.append(sql` LIMIT ${limit} OFFSET ${limit * (page - 1)}`);
448
+ statement.append(sql2` LIMIT ${limit} OFFSET ${limit * (page - 1)}`);
365
449
  return await postgres.all(statement);
366
450
  }
367
451
  );
368
452
  router.get("/api/tables/:table/:pk", {}, async (req) => {
369
- const table = stripIdentifier(req.params.table);
370
- const definition = await getTable(table);
453
+ const table = req.params.table;
454
+ const definition = await getTable(table, postgres.connectionString);
371
455
  const primaryKey = definition.primaryKey;
372
456
  if (!primaryKey) {
373
457
  throw BadRequest(`Table ${table} has no primary key`);
374
458
  }
375
- const statement = sql`
459
+ const statement = sql2`
376
460
  SELECT *
377
- FROM ${sql.identifier(table)}
378
- WHERE ${sql.identifier(primaryKey)} = ${req.params.pk}
461
+ FROM ${qualifiedTable(definition)}
462
+ WHERE ${sql2.identifier(primaryKey)} = ${req.params.pk}
379
463
  `;
380
- return postgres.first(statement);
464
+ return postgres.get(statement);
381
465
  });
382
466
  router.post(
383
467
  "/api/tables/:table",
384
468
  { request: unknownTableRow },
385
469
  async (req) => {
386
- const table = stripIdentifier(req.params.table);
387
- const definition = await getTable(table);
470
+ const table = req.params.table;
471
+ const definition = await getTable(table, postgres.connectionString);
388
472
  const primaryKey = definition.primaryKey;
389
- if (!primaryKey) {
390
- throw BadRequest(`Table ${table} has no primary key`);
473
+ const data = { ...req.body };
474
+ for (const column of definition.columnNames) {
475
+ const metadata = definition.columnMetadata[column];
476
+ if (metadata.isGenerated || metadata.isIdentity) {
477
+ delete data[column];
478
+ }
391
479
  }
392
- const { [primaryKey]: pk, ...data } = req.body;
393
- const statement = sql`
394
- INSERT INTO ${sql.identifier(table)}
395
- ${sql.spreadInsert(data)}
396
- `;
480
+ if (primaryKey && (data[primaryKey] == null || data[primaryKey] === "")) {
481
+ delete data[primaryKey];
482
+ }
483
+ const invalidColumns = Object.keys(data).filter(
484
+ (column) => !definition.columnNames.includes(column)
485
+ );
486
+ if (invalidColumns.length > 0) {
487
+ throw BadRequest(`Invalid columns: ${invalidColumns.join(", ")}`);
488
+ }
489
+ const statement = Object.keys(data).length > 0 ? sql2`INSERT INTO ${qualifiedTable(definition)} ${sql2.spreadInsert(data)} RETURNING *` : sql2`INSERT INTO ${qualifiedTable(definition)} DEFAULT VALUES RETURNING *`;
397
490
  return postgres.first(statement);
398
491
  }
399
492
  );
@@ -401,48 +494,64 @@ function createAdminRouter({
401
494
  "/api/tables/:table/:pk",
402
495
  { request: unknownTableRow },
403
496
  async (req) => {
404
- const table = stripIdentifier(req.params.table);
497
+ const table = req.params.table;
405
498
  const pk = req.params.pk;
406
- const definition = await getTable(table);
499
+ const definition = await getTable(table, postgres.connectionString);
407
500
  const primaryKey = definition.primaryKey;
408
501
  if (!primaryKey) {
409
502
  throw BadRequest(`Table ${table} has no primary key`);
410
503
  }
411
504
  const updatedAtColumns = definition.columnNames.filter(
412
- (c) => ["updated_at", "modified_time"].includes(c)
505
+ (column) => ["updated_at", "modified_time"].includes(column) && !definition.columnMetadata[column].isGenerated && !definition.columnMetadata[column].isIdentity
413
506
  );
414
507
  const data = {
415
508
  ...req.body
416
509
  };
510
+ delete data[primaryKey];
511
+ for (const column of definition.columnNames) {
512
+ const metadata = definition.columnMetadata[column];
513
+ if (metadata.isGenerated || metadata.isIdentity) {
514
+ delete data[column];
515
+ }
516
+ }
517
+ const invalidColumns = Object.keys(data).filter(
518
+ (column) => !definition.columnNames.includes(column)
519
+ );
520
+ if (invalidColumns.length > 0) {
521
+ throw BadRequest(`Invalid columns: ${invalidColumns.join(", ")}`);
522
+ }
417
523
  for (const updateAtColumn of updatedAtColumns) {
418
- data[updateAtColumn] = sql.raw("NOW()");
524
+ data[updateAtColumn] = sql2.raw("NOW()");
525
+ }
526
+ if (Object.keys(data).length === 0) {
527
+ throw BadRequest("No values to update");
419
528
  }
420
529
  return await postgres.transaction(async () => {
421
- await postgres.get(sql`
422
- SELECT * FROM ${sql.identifier(table)}
423
- WHERE ${sql.identifier(primaryKey)} = ${req.params.pk}
530
+ await postgres.get(sql2`
531
+ SELECT * FROM ${qualifiedTable(definition)}
532
+ WHERE ${sql2.identifier(primaryKey)} = ${req.params.pk}
424
533
  FOR UPDATE
425
534
  `);
426
- return postgres.get(sql`
427
- UPDATE ${sql.identifier(table)}
428
- SET ${sql.spreadUpdate(data)}
429
- WHERE ${sql.identifier(primaryKey)} = ${pk}
535
+ return postgres.get(sql2`
536
+ UPDATE ${qualifiedTable(definition)}
537
+ SET ${sql2.spreadUpdate(data)}
538
+ WHERE ${sql2.identifier(primaryKey)} = ${pk}
430
539
  RETURNING *
431
540
  `);
432
541
  });
433
542
  }
434
543
  );
435
544
  router.delete("/api/tables/:table/:pk", {}, async (req) => {
436
- const table = stripIdentifier(req.params.table);
437
- const pk = parseInt(req.params.pk);
438
- const definition = await getTable(table);
545
+ const table = req.params.table;
546
+ const pk = req.params.pk;
547
+ const definition = await getTable(table, postgres.connectionString);
439
548
  const primaryKey = definition.primaryKey;
440
549
  if (!primaryKey) {
441
550
  throw BadRequest(`Table ${table} has no primary key`);
442
551
  }
443
- const statement = sql`
444
- DELETE FROM ${sql.identifier(table)}
445
- WHERE ${sql.identifier(primaryKey)} = ${pk}
552
+ const statement = sql2`
553
+ DELETE FROM ${qualifiedTable(definition)}
554
+ WHERE ${sql2.identifier(primaryKey)} = ${pk}
446
555
  `;
447
556
  await postgres.query(statement);
448
557
  return 204;
@@ -454,7 +563,8 @@ function createAdminRouter({
454
563
  );
455
564
  router.get("*", {}, async (req, res) => {
456
565
  const content = await fs.readFile(indexHtml, "utf-8");
457
- return content.replace(/{{ csrf_token }}/g, generateCsrfToken(req, res)).replace(/{{ basename }}/g, basename.slice(1)).replace(/{{ title }}/g, uiOptions.title ?? "Posterboi");
566
+ const title = escapeHtml(uiOptions.title ?? "Posterboi");
567
+ return content.replace(/{{ csrf_token }}/g, generateCsrfToken(req, res)).replace(/{{ basename }}/g, escapeHtml(clientBasename)).replace(/{{ title }}/g, title);
458
568
  });
459
569
  return router.getRouter();
460
570
  }
@@ -0,0 +1,41 @@
1
+ import 'dotenv/config';
2
+ declare module '@devmoods/config' {
3
+ interface Config extends ConfigFromDefs<typeof appConfig> {
4
+ }
5
+ }
6
+ declare const appConfig: import("@devmoods/config").Defs<{
7
+ CI: boolean;
8
+ PORT: number;
9
+ PUBLIC_URL: string;
10
+ AUTH_TOKEN_EXPIRES: number;
11
+ AUTH_TEMP_TOKEN_EXPIRES: number;
12
+ AUTH_OTP_EXPIRES: number;
13
+ AUTH_OTP_MAX_ATTEMPTS: number;
14
+ AUTH_JWT_SECRET: string;
15
+ AUTH_COOKIE_SAME_SITE: string;
16
+ REDIS_URL: string;
17
+ REDIS_KEY_PREFIX: string;
18
+ SENTRY_DSN: string;
19
+ SENTRY_TRACES_SAMPLE_RATE: number;
20
+ SENTRY_PROFILES_SAMPLE_RATE: number;
21
+ SENTRY_EVENT_LOOP_THRESHOLD: number;
22
+ LOG_LEVEL: string;
23
+ LOG_SQL: boolean;
24
+ NODE_ENV: string;
25
+ CSRF_SECRET: string;
26
+ READINESS_PERIOD_MS: number;
27
+ GRACE_PERIOD_MS: number;
28
+ SLACK_WEBHOOK_URL: string;
29
+ VITE_USE_DEV_SERVER: boolean;
30
+ EMAIL_HOST: string;
31
+ EMAIL_PORT: number;
32
+ EMAIL_USER: string;
33
+ EMAIL_PASS: string;
34
+ EMAIL_SENDER_NAME: string;
35
+ EMAIL_SENDER_ADDRESS: string;
36
+ GOOGLE_AUTH_CLIENT_ID: string;
37
+ GOOGLE_AUTH_CLIENT_SECRET: string;
38
+ POSTGRES_URL: string;
39
+ POSTGRES_PRINT_SQL: boolean;
40
+ }>;
41
+ export {};
@@ -0,0 +1,2 @@
1
+ import 'dotenv/config';
2
+ export default function setup(): Promise<() => Promise<void>>;