Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,7 @@ export class Queries {
id: new ObjectID().toHexString(),
name: jobName,
[updateField]: date.toISOString(), // force to iso string for sqlite
created_at: date.toISOString(), // force to iso string for sqlite

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thought: this is so subtle, I wonder if we could catch a missing required field like this at compile time somehow 🤔

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see this as blocking, but I think we could. Knex supports declaration merging via knex/types/tables and CompositeTableType, which lets us define separate row, insert, and update types. We could make created_at required for inserts but optional for updates, causing the original code here to fail type-checking. Ghost already uses this pattern for gift links and member custom fields.

Quick example pasted from Codex:

import type {Knex} from 'knex';

  interface JobRow {
      id: string;
      name: string;
      status: 'started' | 'finished' | 'failed' | 'queued';
      started_at: Date | string | null;
      finished_at: Date | string | null;
      created_at: Date | string;
      updated_at: Date | string | null;
      metadata: string | null;
      queue_entry: number | null;
  }

  type JobInsert =
      Pick<JobRow, 'id' | 'name' | 'created_at'>
      & Partial<Omit<JobRow, 'id' | 'name' | 'created_at'>>;

  declare module 'knex/types/tables' {
      interface Tables {
          jobs: Knex.CompositeTableType<
              JobRow,             // what SELECT returns
              JobInsert,          // what INSERT accepts
              Partial<JobRow>     // what UPDATE accepts
          >;
      }
  }

updated_at: date.toISOString(), // force to iso string for sqlite
status: status
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,17 @@ describe('Email analytics queries', function () {
assert.equal(job.status, 'finished');
});

it('creates missing job with created timestamp', async function () {
const date = new Date('2026-08-11T10:00:00.000Z');

await queries.setJobTimestamp('email-analytics-missing', 'started', date);

const job = await knex('jobs').where('name', 'email-analytics-missing').first();
assert.equal(new Date(job.created_at).toISOString(), date.toISOString());
assert.equal(new Date(job.started_at).toISOString(), date.toISOString());
assert.equal(new Date(job.updated_at).toISOString(), date.toISOString());
});

it('swallows database errors', async function () {
await knex.schema.dropTable('jobs');

Expand Down
Loading