Files
helios-evcs/apps/csms/src/db/ocpp-schema.ts

740 lines
25 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import {
pgTable,
timestamp,
varchar,
integer,
text,
index,
serial,
jsonb,
uniqueIndex,
} from 'drizzle-orm/pg-core'
import { user } from './auth-schema.ts'
// ---------------------------------------------------------------------------
// 充电桩 / Charge Point
// OCPP 1.6-J Section 4.2 BootNotification
// ---------------------------------------------------------------------------
export const chargePoint = pgTable('charge_point', {
/** 内部 UUID 主键 */
id: varchar('id').primaryKey(),
/**
* 充电桩唯一标识符chargeBoxIdentity
* TLS/WebSocket 连接路径中的最后一段CiString255Type
*/
chargePointIdentifier: varchar('charge_point_identifier', {
length: 255,
})
.unique()
.notNull(),
/** BootNotification.req: chargePointSerialNumber, CiString25Type */
chargePointSerialNumber: varchar('charge_point_serial_number', {
length: 25,
}),
/** BootNotification.req: chargePointModel, CiString20Type (required) */
chargePointModel: varchar('charge_point_model', { length: 20 }).notNull(),
/** BootNotification.req: chargePointVendor, CiString20Type (required) */
chargePointVendor: varchar('charge_point_vendor', { length: 20 }).notNull(),
/** BootNotification.req: firmwareVersion, CiString50Type */
firmwareVersion: varchar('firmware_version', { length: 50 }),
/** BootNotification.req: iccid (SIM card), CiString20Type */
iccid: varchar('iccid', { length: 20 }),
/** BootNotification.req: imsi, CiString20Type */
imsi: varchar('imsi', { length: 20 }),
/** BootNotification.req: meterSerialNumber, CiString25Type */
meterSerialNumber: varchar('meter_serial_number', { length: 25 }),
/** BootNotification.req: meterType, CiString25Type */
meterType: varchar('meter_type', { length: 25 }),
/**
* BootNotification.conf: status
* Accepted = 正常运行Pending = 等待配置Rejected = 拒绝(不提供服务)
*/
registrationStatus: varchar('registration_status', {
enum: ['Accepted', 'Pending', 'Rejected'],
})
.notNull()
.default('Pending'),
/**
* BootNotification.conf: heartbeatInterval
* CSMS 通知充电桩应以此间隔发送心跳
*/
heartbeatInterval: integer('heartbeat_interval').default(60),
/** 最后一次收到 Heartbeat.req 的时间UTC */
lastHeartbeatAt: timestamp('last_heartbeat_at', { withTimezone: true }),
/** 最后一次收到 BootNotification.req 的时间UTC */
lastBootNotificationAt: timestamp('last_boot_notification_at', {
withTimezone: true,
}),
/**
* 电价(单位:分/kWh即 0.01 CNY/kWh
* 交易结束时按实际用电量从储值卡扣费fee = ceil(energyWh * feePerKwh / 1000)
* 默认为 0即不计费
*/
feePerKwh: integer('fee_per_kwh').notNull().default(0),
createdAt: timestamp('created_at', { withTimezone: true })
.notNull()
.defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true })
.notNull()
.defaultNow()
.$onUpdate(() => new Date()),
})
// ---------------------------------------------------------------------------
// 连接器 / Connector
// OCPP 1.6-J Section 4.6 StatusNotification
// ---------------------------------------------------------------------------
/**
* 连接器 ID 规范OCPP 1.6-J Section 2.2
* - connectorId = 0主控制器StatusNotification 中用于报告整桩状态)
* - connectorId ≥ 1实际插口从 1 开始按顺序编号,不能跳号
*/
export const connector = pgTable(
'connector',
{
id: varchar('id').primaryKey(),
chargePointId: varchar('charge_point_id')
.notNull()
.references(() => chargePoint.id, { onDelete: 'cascade' }),
/** OCPP connectorId0 = 主控制器≥1 = 实际插口) */
connectorId: integer('connector_id').notNull(),
/**
* 当前状态 — OCPP 1.6-J Section 7.7 ChargePointStatus
* Available / Preparing / Charging / SuspendedEVSE / SuspendedEV /
* Finishing / Reserved / Unavailable / Faulted
*/
status: varchar('status', {
enum: [
'Available',
'Preparing',
'Charging',
'SuspendedEVSE',
'SuspendedEV',
'Finishing',
'Reserved',
'Unavailable',
'Faulted',
],
})
.notNull()
.default('Unavailable'),
/** 错误码 — OCPP 1.6-J Section 7.6 ChargePointErrorCode */
errorCode: varchar('error_code', {
enum: [
'NoError',
'ConnectorLockFailure',
'EVCommunicationError',
'GroundFailure',
'HighTemperature',
'InternalError',
'LocalListConflict',
'OtherError',
'OverCurrentFailure',
'OverVoltage',
'PowerMeterFailure',
'PowerSwitchFailure',
'ReaderFailure',
'ResetFailure',
'UnderVoltage',
'WeakSignal',
],
})
.notNull()
.default('NoError'),
/** StatusNotification.req: info, CiString50Type */
info: varchar('info', { length: 50 }),
/** StatusNotification.req: vendorId, CiString255Type */
vendorId: varchar('vendor_id', { length: 255 }),
/** StatusNotification.req: vendorErrorCode, CiString50Type */
vendorErrorCode: varchar('vendor_error_code', { length: 50 }),
/** StatusNotification.req: timestamp充电桩报告的状态时间UTC */
lastStatusAt: timestamp('last_status_at', { withTimezone: true })
.notNull()
.defaultNow(),
createdAt: timestamp('created_at', { withTimezone: true })
.notNull()
.defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true })
.notNull()
.defaultNow()
.$onUpdate(() => new Date()),
},
(table) => ({
chargePointIdIdx: index('idx_connector_charge_point_id').on(
table.chargePointId
),
/** 同一充电桩内 connectorId 唯一 */
chargePointConnectorUniq: uniqueIndex(
'idx_connector_charge_point_connector'
).on(table.chargePointId, table.connectorId),
})
)
// ---------------------------------------------------------------------------
// 连接器状态历史 / Connector Status History
// 完整记录每条 StatusNotification.req用于审计与历史回溯
// ---------------------------------------------------------------------------
export const connectorStatusHistory = pgTable(
'connector_status_history',
{
id: varchar('id').primaryKey(),
connectorId: varchar('connector_id')
.notNull()
.references(() => connector.id, { onDelete: 'cascade' }),
/** OCPP connectorId冗余存储方便直接查询 */
connectorNumber: integer('connector_number').notNull(),
status: varchar('status', {
enum: [
'Available',
'Preparing',
'Charging',
'SuspendedEVSE',
'SuspendedEV',
'Finishing',
'Reserved',
'Unavailable',
'Faulted',
],
}).notNull(),
errorCode: varchar('error_code', {
enum: [
'NoError',
'ConnectorLockFailure',
'EVCommunicationError',
'GroundFailure',
'HighTemperature',
'InternalError',
'LocalListConflict',
'OtherError',
'OverCurrentFailure',
'OverVoltage',
'PowerMeterFailure',
'PowerSwitchFailure',
'ReaderFailure',
'ResetFailure',
'UnderVoltage',
'WeakSignal',
],
}).notNull(),
info: varchar('info', { length: 50 }),
vendorId: varchar('vendor_id', { length: 255 }),
vendorErrorCode: varchar('vendor_error_code', { length: 50 }),
/** StatusNotification.req: timestamp充电桩上报的状态时间 */
statusTimestamp: timestamp('status_timestamp', { withTimezone: true }),
/** CSMS 收到消息的时间 */
receivedAt: timestamp('received_at', { withTimezone: true })
.notNull()
.defaultNow(),
},
(table) => ({
connectorIdIdx: index('idx_status_history_connector_id').on(
table.connectorId
),
statusTimestampIdx: index('idx_status_history_timestamp').on(
table.statusTimestamp
),
receivedAtIdx: index('idx_status_history_received_at').on(
table.receivedAt
),
})
)
// ---------------------------------------------------------------------------
// 认证标签 / IdTag
// OCPP 1.6-J Section 4.1 Authorize / Section 7.15 IdTagInfo
// ---------------------------------------------------------------------------
export const idTag = pgTable('id_tag', {
/**
* RFID 卡号或 App 生成的令牌CiString20Type
* 对应 OCPP Authorize.req.idTag
*/
idTag: varchar('id_tag', { length: 20 }).primaryKey(),
/**
* 父标签用于分组授权CiString20Type
* 当 parentIdTag 在本地列表被接受时,子标签同样被接受
*/
parentIdTag: varchar('parent_id_tag', { length: 20 }),
/**
* 标签当前状态 — OCPP 1.6-J Section 7.14 AuthorizationStatus
* Accepted / Blocked / Expired / Invalid / ConcurrentTx
*/
status: varchar('status', {
enum: ['Accepted', 'Blocked', 'Expired', 'Invalid', 'ConcurrentTx'],
})
.notNull()
.default('Accepted'),
/**
* 过期时间UTC
* 若设置CSMS 在 Authorize.conf 中会返回此字段供充电桩离线缓存使用
*/
expiryDate: timestamp('expiry_date', { withTimezone: true }),
/**
* 关联的平台用户(可选)
* 允许将 RFID 卡与注册用户绑定,支持 Web/App 远程查询充电记录
*/
userId: text('user_id').references(() => user.id, { onDelete: 'set null' }),
/**
* 储值卡余额(单位:分)
* 以整数存储1 分 = 0.01 CNY前端显示时除以 100
*/
balance: integer('balance').notNull().default(0), /**
* 卡面内容排列方式
*/
cardLayout: varchar('card_layout', { enum: ['center', 'around'] }).default('around'),
/**
* 卡底装饰风格
* 对应 faces/ 目录中已注册的卡面组件
*/
cardSkin: varchar('card_skin', { enum: ['line', 'circles', 'glow', 'vip', 'redeye'] }).default('circles'), createdAt: timestamp('created_at', { withTimezone: true })
.notNull()
.defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true })
.notNull()
.defaultNow()
.$onUpdate(() => new Date()),
})
// ---------------------------------------------------------------------------
// 充电事务 / Transaction
// OCPP 1.6-J Section 4.3 StartTransaction / Section 4.4 StopTransaction
// ---------------------------------------------------------------------------
export const transaction = pgTable(
'transaction',
{
/**
* OCPP transactionId整型自增
* OCPP 1.6-J Section 4.3: "The transaction id MAY be a number ... it MUST be unique."
* 使用 serial 保证全局唯一且自动递增
*/
id: serial('id').primaryKey(),
chargePointId: varchar('charge_point_id')
.notNull()
.references(() => chargePoint.id, { onDelete: 'cascade' }),
connectorId: varchar('connector_id')
.notNull()
.references(() => connector.id, { onDelete: 'cascade' }),
/** OCPP connectorId冗余存储方便直接查询 */
connectorNumber: integer('connector_number').notNull(),
/**
* 发起充电的 idTagCiString20Type
* StartTransaction.req.idTag
*/
idTag: varchar('id_tag', { length: 20 }).notNull(),
/**
* CSMS 对 idTag 的鉴权结果StartTransaction.conf 中返回)
* OCPP 1.6-J Section 7.14 AuthorizationStatus
*/
idTagStatus: varchar('id_tag_status', {
enum: ['Accepted', 'Blocked', 'Expired', 'Invalid', 'ConcurrentTx'],
}),
/**
* 充电开始时间充电桩本地时间UTC
* StartTransaction.req.timestamp
*/
startTimestamp: timestamp('start_timestamp', {
withTimezone: true,
}).notNull(),
/**
* 开始时的电表读数Wh
* StartTransaction.req.meterStart
*/
startMeterValue: integer('start_meter_value').notNull(),
/**
* 停止充电的 idTag可能与 idTag 不同,如工作人员强制停止)
* StopTransaction.req.idTagoptional
*/
stopIdTag: varchar('stop_id_tag', { length: 20 }),
/**
* 充电结束时间UTC
* StopTransaction.req.timestamp
*/
stopTimestamp: timestamp('stop_timestamp', { withTimezone: true }),
/**
* 结束时的电表读数Wh
* StopTransaction.req.meterStop
*/
stopMeterValue: integer('stop_meter_value'),
/**
* 停止原因 — OCPP 1.6-J Section 7.20 Reason
* EmergencyStop / EVDisconnected / HardReset / Local / Other /
* PowerLoss / Reboot / Remote / SoftReset / UnlockCommand / DeAuthorized
*/
stopReason: varchar('stop_reason', {
enum: [
'EmergencyStop',
'EVDisconnected',
'HardReset',
'Local',
'Other',
'PowerLoss',
'Reboot',
'Remote',
'SoftReset',
'UnlockCommand',
'DeAuthorized',
],
}),
/**
* 本次充电扣费金额(单位:分)
* 由 StopTransaction 处理时根据实际用电量和充电桩电价计算写入
* null 表示未计费(如免费充电桩或交易异常终止)
*/
chargeAmount: integer('charge_amount'),
/**
* 关联的预约 ID若本次充电由预约触发
* StartTransaction.req.reservationIdoptional
*/
reservationId: integer('reservation_id'),
createdAt: timestamp('created_at', { withTimezone: true })
.notNull()
.defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true })
.notNull()
.defaultNow()
.$onUpdate(() => new Date()),
},
(table) => ({
chargePointIdIdx: index('idx_transaction_charge_point_id').on(
table.chargePointId
),
connectorIdIdx: index('idx_transaction_connector_id').on(
table.connectorId
),
idTagIdx: index('idx_transaction_id_tag').on(table.idTag),
startTimestampIdx: index('idx_transaction_start_timestamp').on(
table.startTimestamp
),
})
)
// ---------------------------------------------------------------------------
// 计量值 / Meter Value
// OCPP 1.6-J Section 4.7 MeterValues也内嵌于 StartTransaction / StopTransaction
// ---------------------------------------------------------------------------
/** SampledValue 类型定义(对应 OCPP 1.6-J Section 7.17 */
export type SampledValue = {
/** 测量值(字符串形式) */
value: string
/**
* 读数上下文 — ReadingContext
* Interruption.Begin / Interruption.End / Sample.Clock /
* Sample.Periodic / Transaction.Begin / Transaction.End / Trigger / Other
*/
context?:
| 'Interruption.Begin'
| 'Interruption.End'
| 'Sample.Clock'
| 'Sample.Periodic'
| 'Transaction.Begin'
| 'Transaction.End'
| 'Trigger'
| 'Other'
/** 数据格式Raw原始数值/ SignedData签名数据 */
format?: 'Raw' | 'SignedData'
/**
* 测量量 — Measurand常用值
* Energy.Active.Import.Register / Power.Active.Import /
* Current.Import / Voltage / SoC / Temperature 等
*/
measurand?: string
/** 相位信息三相系统L1 / L2 / L3 / N / L1-N / L2-N / L3-N / L1-L2 / L2-L3 / L3-L1 */
phase?: string
/** 测量位置Cable / EV / Inlet / Outlet / Body */
location?: string
/** 单位Wh / kWh / varh / kvarh / W / kW / VA / kVA / var / kvar / A / V / K / Celcius / Fahrenheit / Percent */
unit?: string
}
export const meterValue = pgTable(
'meter_value',
{
id: varchar('id').primaryKey(),
/**
* 关联的充电事务(可 null如充电桩空闲时上报的周期性计量值
* MeterValues.req.transactionIdoptional
*/
transactionId: integer('transaction_id').references(() => transaction.id, {
onDelete: 'set null',
}),
connectorId: varchar('connector_id')
.notNull()
.references(() => connector.id, { onDelete: 'cascade' }),
chargePointId: varchar('charge_point_id')
.notNull()
.references(() => chargePoint.id, { onDelete: 'cascade' }),
/** OCPP connectorId冗余存储 */
connectorNumber: integer('connector_number').notNull(),
/**
* 充电桩上报的计量时间戳UTC
* MeterValues.req.meterValue[].timestamp
*/
timestamp: timestamp('timestamp', { withTimezone: true }).notNull(),
/**
* 采样值数组JSONB
* MeterValues.req.meterValue[].sampledValue[]
* 保持 JSONB 格式以支持 measurand 类型不固定的场景
*/
sampledValues: jsonb('sampled_values')
.notNull()
.$type<SampledValue[]>(),
/** CSMS 收到消息的时间 */
receivedAt: timestamp('received_at', { withTimezone: true })
.notNull()
.defaultNow(),
},
(table) => ({
transactionIdIdx: index('idx_meter_value_transaction_id').on(
table.transactionId
),
connectorIdIdx: index('idx_meter_value_connector_id').on(
table.connectorId
),
timestampIdx: index('idx_meter_value_timestamp').on(table.timestamp),
})
)
// ---------------------------------------------------------------------------
// 预约 / Reservation
// OCPP 1.6-J Section 3.11 ReserveNow / Section 3.2 CancelReservation
// ---------------------------------------------------------------------------
export const reservation = pgTable(
'reservation',
{
/**
* OCPP reservationId整型由 CSMS 分配)
* ReserveNow.req.reservationId
*/
id: integer('id').primaryKey(),
chargePointId: varchar('charge_point_id')
.notNull()
.references(() => chargePoint.id, { onDelete: 'cascade' }),
/**
* 关联的内部连接器记录(可 nullconnectorId=0 表示任意可用插口)
*/
connectorId: varchar('connector_id').references(() => connector.id, {
onDelete: 'set null',
}),
/**
* OCPP connectorId0 = 整桩任意插口≥1 = 指定插口)
* ReserveNow.req.connectorId
*/
connectorNumber: integer('connector_number').notNull(),
/**
* 预约到期时间UTC
* ReserveNow.req.expiryDate
*/
expiryDate: timestamp('expiry_date', { withTimezone: true }).notNull(),
/**
* 预约使用的 idTagCiString20Type
* ReserveNow.req.idTag
*/
idTag: varchar('id_tag', { length: 20 }).notNull(),
/**
* 可选的父标签(用于允许同组任意 idTag 使用本预约)
* ReserveNow.req.parentIdTag
*/
parentIdTag: varchar('parent_id_tag', { length: 20 }),
/**
* 预约状态(扩展字段,非 OCPP 原生)
* Active = 有效中Cancelled = 已取消Expired = 已过期Used = 已使用
*/
status: varchar('status', {
enum: ['Active', 'Cancelled', 'Expired', 'Used'],
})
.notNull()
.default('Active'),
createdAt: timestamp('created_at', { withTimezone: true })
.notNull()
.defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true })
.notNull()
.defaultNow()
.$onUpdate(() => new Date()),
},
(table) => ({
chargePointIdIdx: index('idx_reservation_charge_point_id').on(
table.chargePointId
),
statusIdx: index('idx_reservation_status').on(table.status),
expiryDateIdx: index('idx_reservation_expiry_date').on(table.expiryDate),
})
)
// ---------------------------------------------------------------------------
// 本地授权列表 / Local Auth List
// OCPP 1.6-J Section 3.9 SendLocalList / Section 3.8 GetLocalListVersion
// ---------------------------------------------------------------------------
/**
* 充电桩本地授权列表缓存
*
* 充电桩断网时依赖本地列表进行离线授权。CSMS 通过 SendLocalList 将列表
* 推送至充电桩,每次推送携带 listVersion单调递增整数
* CSMS 侧同步维护此表,以便在需要时重新推送或审计。
*/
export const localAuthList = pgTable(
'local_auth_list',
{
id: varchar('id').primaryKey(),
chargePointId: varchar('charge_point_id')
.notNull()
.references(() => chargePoint.id, { onDelete: 'cascade' }),
/**
* 列表版本号(单调递增)
* SendLocalList.req.listVersion
*/
listVersion: integer('list_version').notNull(),
/** idTagCiString20Type */
idTag: varchar('id_tag', { length: 20 }).notNull(),
/** 可选父标签 */
parentIdTag: varchar('parent_id_tag', { length: 20 }),
/** 本地授权状态 — AuthorizationStatus */
idTagStatus: varchar('id_tag_status', {
enum: ['Accepted', 'Blocked', 'Expired', 'Invalid', 'ConcurrentTx'],
}).notNull(),
/** 过期时间UTC可选 */
expiryDate: timestamp('expiry_date', { withTimezone: true }),
updatedAt: timestamp('updated_at', { withTimezone: true })
.notNull()
.defaultNow()
.$onUpdate(() => new Date()),
},
(table) => ({
/** 同一充电桩内同一 idTag 唯一 */
chargePointIdTagUniq: uniqueIndex(
'idx_local_auth_list_charge_point_id_tag'
).on(table.chargePointId, table.idTag),
})
)
// ---------------------------------------------------------------------------
// 充电配置文件 / Charging Profile
// OCPP 1.6-J Section 3.12 SetChargingProfileSmart Charging Profile
// ---------------------------------------------------------------------------
/**
* ChargingSchedulePeriod 类型(对应 OCPP 1.6-J Section 7.8
* 描述某一时间段内的充电限制
*/
export type ChargingSchedulePeriod = {
/** 距 chargingSchedule.startSchedule 的偏移秒数 */
startPeriod: number
/** 最大充电限制(单位由 chargingRateUnit 决定W 或 A */
limit: number
/** 可选允许使用的相数1 或 3 */
numberPhases?: number
}
/**
* ChargingSchedule 类型(对应 OCPP 1.6-J Section 7.9
*/
export type ChargingSchedule = {
/** 总时长(秒,可选)*/
duration?: number
/** 计划开始时间UTC ISO 8601可选Absolute 类型时使用) */
startSchedule?: string
/** 限制单位W瓦特或 A安培 */
chargingRateUnit: 'W' | 'A'
/** 各时间段的限制列表(按 startPeriod 升序排列) */
chargingSchedulePeriod: ChargingSchedulePeriod[]
/** 最小充电速率W / A可选 */
minChargingRate?: number
}
export const chargingProfile = pgTable(
'charging_profile',
{
/**
* OCPP chargingProfileId整型由 CSMS 分配)
* SetChargingProfile.req.csChargingProfiles.chargingProfileId
*/
id: integer('id').primaryKey(),
chargePointId: varchar('charge_point_id')
.notNull()
.references(() => chargePoint.id, { onDelete: 'cascade' }),
/**
* 关联的内部连接器0 表示整桩级别的配置文件,对应 connectorId=0
*/
connectorId: varchar('connector_id').references(() => connector.id, {
onDelete: 'cascade',
}),
/** OCPP connectorId0 = 充电桩级别≥1 = 指定插口) */
connectorNumber: integer('connector_number').notNull(),
/**
* 关联的事务 ID仅 TxProfile 类型时有效)
* SetChargingProfile.req.csChargingProfiles.transactionId
*/
transactionId: integer('transaction_id').references(() => transaction.id, {
onDelete: 'set null',
}),
/**
* 优先级堆叠层级(数值越高,优先级越高)
* SetChargingProfile.req.csChargingProfiles.stackLevel
*/
stackLevel: integer('stack_level').notNull(),
/**
* 配置文件用途 — ChargingProfilePurposeType
* ChargePointMaxProfile限制整桩最大功率
* TxDefaultProfile默认事务充电功率
* TxProfile绑定到特定事务的充电功率
*/
chargingProfilePurpose: varchar('charging_profile_purpose', {
enum: ['ChargePointMaxProfile', 'TxDefaultProfile', 'TxProfile'],
}).notNull(),
/**
* 配置文件类型 — ChargingProfileKindType
* Absolute从指定时间点开始
* Recurring周期性重复Daily / Weekly
* Relative相对于事务开始时间
*/
chargingProfileKind: varchar('charging_profile_kind', {
enum: ['Absolute', 'Recurring', 'Relative'],
}).notNull(),
/**
* 周期类型(仅 Recurring 类型时有效)
* Daily / Weekly
*/
recurrencyKind: varchar('recurrency_kind', {
enum: ['Daily', 'Weekly'],
}),
/** 配置文件生效时间UTC可选 */
validFrom: timestamp('valid_from', { withTimezone: true }),
/** 配置文件失效时间UTC可选 */
validTo: timestamp('valid_to', { withTimezone: true }),
/**
* 充电计划JSONB
* SetChargingProfile.req.csChargingProfiles.chargingSchedule
*/
chargingSchedule: jsonb('charging_schedule')
.notNull()
.$type<ChargingSchedule>(),
createdAt: timestamp('created_at', { withTimezone: true })
.notNull()
.defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true })
.notNull()
.defaultNow()
.$onUpdate(() => new Date()),
},
(table) => ({
chargePointIdIdx: index('idx_charging_profile_charge_point_id').on(
table.chargePointId
),
connectorIdIdx: index('idx_charging_profile_connector_id').on(
table.connectorId
),
purposeStackIdx: index('idx_charging_profile_purpose_stack').on(
table.connectorNumber,
table.chargingProfilePurpose,
table.stackLevel
),
})
)