{"record":{"id":"943433bf10628e08","repo":"TryGhost/Ghost","slug":"cannot-insert-without-an-id-field","errorCode":null,"errorMessage":"Cannot insert without an id field","messagePattern":"Cannot insert without an id field","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"e2e/data-factory/persistence/adapters/knex.ts","lineNumber":22,"sourceCode":"/**\n * Knex-based persistence adapter for direct database access\n */\nexport class KnexPersistenceAdapter implements PersistenceAdapter {\n    private db: Knex;\n\n    constructor(db: Knex) {\n        this.db = db;\n    }\n\n    async insert<T>(entityType: string, data: T): Promise<T> {\n        // entityType is the table name for Knex\n        await this.db(entityType).insert(data);\n\n        // MySQL doesn't support returning(), so we need to fetch the inserted record\n        // Assuming the data has an 'id' field\n        const id = (data as {id?: string}).id;\n        if (!id) {\n            throw new Error('Cannot insert without an id field');\n        }\n\n        return await this.findById<T>(entityType, id);\n    }\n\n    async update<T>(entityType: string, id: string, data: Partial<T>): Promise<T> {\n        await this.db(entityType)\n            .where('id', id)\n            .update(data);\n\n        return await this.findById<T>(entityType, id);\n    }\n\n    async delete(entityType: string, id: string): Promise<void> {\n        await this.db(entityType)\n            .where('id', id)\n            .del();\n    }","sourceCodeStart":4,"sourceCodeEnd":40,"githubUrl":"https://github.com/TryGhost/Ghost/blob/47d8b0e2ad2fd4757d3bc45f46c3ac165ff8a1fe/e2e/data-factory/persistence/adapters/knex.ts#L4-L40","documentation":"Thrown by KnexPersistenceAdapter.insert() when the data object passed to insert has no 'id' field. Because MySQL doesn't support Knex's returning(), the adapter inserts the row then re-fetches it by id — so an id is mandatory on the payload. This is a factory-build contract requirement: any entity persisted via the Knex adapter must include an id in the built data.","triggerScenarios":"A factory's build() method produces an object without an id field (relying on auto-increment), then create() routes through KnexPersistenceAdapter.insert(). The adapter cannot re-fetch the inserted row because it has no id to query by, so it throws immediately after the raw insert.","commonSituations":"Factory was designed for the ApiPersistenceAdapter (where the server assigns the id) but reused with the Knex adapter without generating an id in build(); MySQL auto-increment assumed but the adapter needs an explicit id; a UUID/id generator was removed from build() during a refactor.","solutions":["Ensure the factory's build() generates an id (e.g. crypto.randomUUID() or a faker-based id) for every entity persisted via the Knex adapter.","If the table uses DB-assigned auto-increment ids, the Knex adapter's insert path is the wrong abstraction — either switch to the API adapter or extend the adapter to read the last-inserted id.","Run 'pnpm build' and 'pnpm test:types' after factory changes to catch missing id generation.","Confirm the table's primary key is actually named 'id' (the adapter hardcodes the 'id' field name)."],"exampleFix":"// before: build() omits id, assuming DB auto-increment\nbuild(options) {\n    return {title: options.title ?? 'Post', status: 'draft'};\n}\n\n// after: generate an id so the Knex adapter can re-fetch\nimport {randomUUID} from 'crypto';\nbuild(options) {\n    return {id: options.id ?? randomUUID(), title: options.title ?? 'Post', status: 'draft'};\n}","handlingStrategy":"validation","validationCode":"import {randomUUID} from 'crypto';\n\nfunction ensureId(data) {\n    if (!data || data.id == null) {\n        throw new Error('Cannot insert via Knex adapter without an id field. Generate one in build().');\n    }\n    return data;\n}\n\n// In the factory:\nbuild(options) {\n    return ensureId({id: options.id ?? randomUUID(), ...otherFields});\n}","typeGuard":"function hasIdField(data) {\n    return data != null && (typeof data.id === 'string' || typeof data.id === 'number') && data.id !== '';\n}","tryCatchPattern":null,"preventionTips":["Always generate an id in build() for entities used with the Knex adapter.","Confirm the table's primary key column is named 'id' (the adapter hardcodes it).","If you need DB auto-increment ids, don't use this adapter's insert — extend it to fetch the last-inserted id."],"tags":["e2e","data-factory","knex-adapter","mysql","test-infrastructure"],"backgroundTag":null,"analyzedSha":"47d8b0e2ad2fd4757d3bc45f46c3ac165ff8a1fe","analyzedAt":"2026-08-13T01:25:26.651Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}