Directus 插件 Transactional Exec 的一些例子

按规则生成订单号

Payload

{
    "data": "{{ $trigger.payload }}"
}

Code

const formatter = new Intl.DateTimeFormat('en-GB', {
    year: 'numeric',
    month: '2-digit',
    day: '2-digit',
    timeZone: 'Asia/Shanghai'
});

const main = async function (options, context) {
    const { payload: { data }, transaction } = options;
    const { database } = context;

    return await transaction(database, async (trx) => {
        const serialNumber = await getSerialNumber(trx);
        // Inside a Filter Hook
        return {
            ...data,
            serial_number: serialNumber,
        };
    });
};

return await main(options, context);

async function getSerialNumber(trx) {
    // Timezone Handling: Use Intl to force China Standard Time (CST) 
    // to prevent date flips at 8:00 AM if the server uses UTC.
    const now = new Date();
    const parts = formatter.formatToParts(now);
    const year = parts.find(p => p.type === 'year').value;
    const month = parts.find(p => p.type === 'month').value;
    const day = parts.find(p => p.type === 'day').value;
    const datePart = `${year}${month}${day}`;
    const prefix = `PUR${datePart}`;
    const PAD_LENGTH = 4; // Centralized padding control
    const DAILY_QUOTA = Number('9'.repeat(PAD_LENGTH));

    const row = await trx('purchase_order')
        .where('serial_number', 'like', `${prefix}%`)
        .max('serial_number as maxSerial')
        .first();

    let nextNumber = 1; // Start at 1

    if (row?.maxSerial) {
        // Extract suffix and increment numerically
        const lastDigits = row.maxSerial.replace(prefix, '');
        nextNumber = parseInt(lastDigits, 10) + 1;
    }

    if (nextNumber > DAILY_QUOTA) {
        throw new Error(`Daily quota exceeded for ${prefix}. Capacity is ${DAILY_QUOTA} orders.`);
    }

    const nextDigits = String(nextNumber).padStart(PAD_LENGTH, '0');
    return `${prefix}${nextDigits}`;
}

复用 Services

The Circular Loop Risk: Since the script calls updateOne inside the transaction, it might trigger another "Update" event. If this script is attached to an items.update hook, you could create an infinite loop.

Optimization: In a Filter (Before) hook, it is often better to modify the payload object directly rather than calling services.ItemsService.updateOne. However, if this is an Action or a custom script where the ID already exists, your current approach is fine as long as you ensure you aren't re-triggering the same logic.

Code

const main = async function(options, context) {
    const { payload: { collection, id }, transaction, schema } = options;
    const { services, database, accountability } = context;
    
    await transaction(database, async (trx) => {
        const service = new services.ItemsService(collection, { knex: trx, schema, accountability });

        const item = await service.readOne(id, { 
            fields: ['duration_days'],
        });

        await service.updateOne(id, {
            duration_days: item.duration_days + 1,
        });
    });
    
    return {};
};

return await main(options, context);
发表于 2026 年 5 月 3 日,星期天