Add extracted tools: CitrineOS, OpenOCPP, ShapeShifter
- CitrineOS core extracted (CSMS OCPP 2.0.1) - OpenOCPP extracted (firmware OCPP 1.6J/2.0.1) - ShapeShifter library installed (pip install -e) - ShapeShifter specification extracted - EVerest extracted TODO updated with progress
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,40 @@
|
||||
// SPDX-FileCopyrightText: 2025 Contributors to the CitrineOS Project
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
'use strict';
|
||||
|
||||
/** @type {import('sequelize-cli').Migration} */
|
||||
import { DataTypes, QueryInterface } from 'sequelize';
|
||||
|
||||
const TENANTS_TABLE = `Tenants`;
|
||||
|
||||
export default {
|
||||
up: async (queryInterface: QueryInterface) => {
|
||||
await queryInterface.createTable(TENANTS_TABLE, {
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
autoIncrement: true,
|
||||
primaryKey: true,
|
||||
},
|
||||
name: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
},
|
||||
createdAt: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false,
|
||||
defaultValue: DataTypes.NOW,
|
||||
},
|
||||
updatedAt: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false,
|
||||
defaultValue: DataTypes.NOW,
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
down: async (queryInterface: QueryInterface) => {
|
||||
await queryInterface.dropTable(TENANTS_TABLE);
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
// SPDX-FileCopyrightText: 2025 Contributors to the CitrineOS Project
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
'use strict';
|
||||
|
||||
import { DEFAULT_TENANT_ID } from '@citrineos/base';
|
||||
import { QueryInterface } from 'sequelize';
|
||||
|
||||
const TENANTS_TABLE = `Tenants`;
|
||||
|
||||
export default {
|
||||
up: async (queryInterface: QueryInterface) => {
|
||||
const [[existingTenant]] = await queryInterface.sequelize.query(
|
||||
`SELECT 1 FROM "${TENANTS_TABLE}" WHERE id = ${DEFAULT_TENANT_ID} LIMIT 1`,
|
||||
);
|
||||
|
||||
if (!existingTenant) {
|
||||
await queryInterface.bulkInsert(TENANTS_TABLE, [
|
||||
{
|
||||
id: DEFAULT_TENANT_ID,
|
||||
name: 'Default Tenant',
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
]);
|
||||
}
|
||||
},
|
||||
|
||||
down: async (queryInterface: QueryInterface) => {
|
||||
await queryInterface.bulkDelete(TENANTS_TABLE, { id: DEFAULT_TENANT_ID });
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,94 @@
|
||||
// SPDX-FileCopyrightText: 2025 Contributors to the CitrineOS Project
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
'use strict';
|
||||
|
||||
/** @type {import('sequelize-cli').Migration} */
|
||||
import { DEFAULT_TENANT_ID } from '@citrineos/base';
|
||||
import { DataTypes, QueryInterface } from 'sequelize';
|
||||
|
||||
const TABLES = [
|
||||
'AdditionalInfos',
|
||||
'IdTokens',
|
||||
'IdTokenInfos',
|
||||
'Authorizations',
|
||||
'Boots',
|
||||
'Certificates',
|
||||
'InstalledCertificates',
|
||||
'ChangeConfigurations',
|
||||
'Evses',
|
||||
'Locations',
|
||||
'ChargingStations',
|
||||
'Transactions',
|
||||
'ChargingNeeds',
|
||||
'ChargingProfiles',
|
||||
'ChargingSchedules',
|
||||
'ServerNetworkProfiles',
|
||||
'SetNetworkProfiles',
|
||||
'ChargingStationNetworkProfiles',
|
||||
'ChargingStationSecurityInfos',
|
||||
'ChargingStationSequences',
|
||||
'Components',
|
||||
'Variables',
|
||||
'ComponentVariables',
|
||||
'CompositeSchedules',
|
||||
'Connectors',
|
||||
'EventData',
|
||||
'IdTokenAdditionalInfos',
|
||||
'TransactionEvents',
|
||||
'StopTransactions',
|
||||
'MeterValues',
|
||||
'MessageInfos',
|
||||
'OCPPMessages',
|
||||
'Reservations',
|
||||
'SalesTariffs',
|
||||
'SecurityEvents',
|
||||
'StartTransactions',
|
||||
'StatusNotifications',
|
||||
'LatestStatusNotifications',
|
||||
'Subscriptions',
|
||||
'Tariffs',
|
||||
'VariableAttributes',
|
||||
'VariableCharacteristics',
|
||||
'VariableMonitorings',
|
||||
'VariableMonitoringStatuses',
|
||||
'VariableStatuses',
|
||||
'LocalListAuthorizations',
|
||||
'LocalListVersions',
|
||||
'LocalListVersionAuthorizations',
|
||||
'SendLocalLists',
|
||||
'SendLocalListAuthorizations',
|
||||
];
|
||||
|
||||
const TENANT_COLUMN = 'tenantId';
|
||||
const TENANTS_TABLE = `Tenants`;
|
||||
|
||||
export default {
|
||||
up: async (queryInterface: QueryInterface) => {
|
||||
for (const table of TABLES) {
|
||||
const tableDescription = await queryInterface.describeTable(table);
|
||||
if (!tableDescription[TENANT_COLUMN]) {
|
||||
await queryInterface.addColumn(table, TENANT_COLUMN, {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: DEFAULT_TENANT_ID,
|
||||
references: {
|
||||
model: TENANTS_TABLE,
|
||||
key: 'id',
|
||||
},
|
||||
onUpdate: 'CASCADE', // update tenantId if the tenant primary key is updated (should never happen)
|
||||
onDelete: 'RESTRICT', // ensure tenant row cannot be deleted if there are existing records using it
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
down: async (queryInterface: QueryInterface) => {
|
||||
for (const table of TABLES) {
|
||||
const tableDescription = await queryInterface.describeTable(table);
|
||||
if (tableDescription[TENANT_COLUMN]) {
|
||||
await queryInterface.removeColumn(table, TENANT_COLUMN);
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
// SPDX-FileCopyrightText: 2025 Contributors to the CitrineOS Project
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
'use strict';
|
||||
|
||||
/** @type {import('sequelize-cli').Migration} */
|
||||
import { QueryInterface } from 'sequelize';
|
||||
import { DataType } from 'sequelize-typescript';
|
||||
|
||||
const TABLE_NAME = 'Authorizations';
|
||||
const COLUMN_NAME = 'concurrentTransaction';
|
||||
|
||||
export default {
|
||||
up: async (queryInterface: QueryInterface) => {
|
||||
const tableDescription = await queryInterface.describeTable(TABLE_NAME);
|
||||
if (!tableDescription[COLUMN_NAME]) {
|
||||
await queryInterface.addColumn(TABLE_NAME, COLUMN_NAME, {
|
||||
type: DataType.BOOLEAN,
|
||||
allowNull: true,
|
||||
defaultValue: false,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
down: async (queryInterface: QueryInterface) => {
|
||||
const tableDescription = await queryInterface.describeTable(TABLE_NAME);
|
||||
if (tableDescription[COLUMN_NAME]) {
|
||||
await queryInterface.removeColumn(TABLE_NAME, COLUMN_NAME);
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,48 @@
|
||||
// SPDX-FileCopyrightText: 2025 Contributors to the CitrineOS Project
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
'use strict';
|
||||
|
||||
/** @type {import('sequelize-cli').Migration} */
|
||||
import { AuthorizationWhitelistEnum } from '@citrineos/base';
|
||||
import { QueryInterface } from 'sequelize';
|
||||
import { DataType } from 'sequelize-typescript';
|
||||
|
||||
const TABLE_NAME = 'Authorizations';
|
||||
const COLUMNS = [
|
||||
{
|
||||
name: 'realTimeAuth',
|
||||
attributes: {
|
||||
type: DataType.STRING,
|
||||
allowNull: false,
|
||||
defaultValue: AuthorizationWhitelistEnum.Never,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'realTimeAuthUrl',
|
||||
attributes: {
|
||||
type: DataType.STRING,
|
||||
allowNull: true,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export default {
|
||||
up: async (queryInterface: QueryInterface) => {
|
||||
const tableDescription = await queryInterface.describeTable(TABLE_NAME);
|
||||
for (const column of COLUMNS) {
|
||||
if (!tableDescription[column.name]) {
|
||||
await queryInterface.addColumn(TABLE_NAME, column.name, column.attributes);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
down: async (queryInterface: QueryInterface) => {
|
||||
const tableDescription = await queryInterface.describeTable(TABLE_NAME);
|
||||
for (const column of COLUMNS) {
|
||||
if (tableDescription[column.name]) {
|
||||
await queryInterface.removeColumn(TABLE_NAME, column.name);
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,425 @@
|
||||
// SPDX-FileCopyrightText: 2025 Contributors to the CitrineOS Project
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
import { QueryInterface } from 'sequelize';
|
||||
|
||||
export default {
|
||||
up: async (queryInterface: QueryInterface) => {
|
||||
// 1. Drop any existing temp tables first, then create temp tables to preserve data from related tables
|
||||
await queryInterface.sequelize.query('DROP TABLE IF EXISTS "IdTokens_temp"');
|
||||
await queryInterface.sequelize.query('DROP TABLE IF EXISTS "IdTokenInfos_temp"');
|
||||
await queryInterface.sequelize.query('DROP TABLE IF EXISTS "AdditionalInfos_temp"');
|
||||
await queryInterface.sequelize.query('DROP TABLE IF EXISTS "Authorizations_temp"');
|
||||
|
||||
await queryInterface.sequelize.query(`
|
||||
CREATE TABLE "IdTokens_temp" AS TABLE "IdTokens";
|
||||
`);
|
||||
await queryInterface.sequelize.query(`
|
||||
CREATE TABLE "IdTokenInfos_temp" AS TABLE "IdTokenInfos";
|
||||
`);
|
||||
await queryInterface.sequelize.query(`
|
||||
CREATE TABLE "AdditionalInfos_temp" AS TABLE "AdditionalInfos";
|
||||
`);
|
||||
await queryInterface.sequelize.query(`
|
||||
CREATE TABLE "Authorizations_temp" AS TABLE "Authorizations";
|
||||
`);
|
||||
|
||||
// 2. Alter the Authorizations table: add new flat columns, but do not drop old columns yet
|
||||
// Check if columns exist before adding them
|
||||
const tableDescription = await queryInterface.describeTable('Authorizations');
|
||||
|
||||
if (!tableDescription['idToken']) {
|
||||
await queryInterface.addColumn('Authorizations', 'idToken', {
|
||||
type: 'VARCHAR(255)',
|
||||
allowNull: true,
|
||||
});
|
||||
}
|
||||
if (!tableDescription['idTokenType']) {
|
||||
await queryInterface.addColumn('Authorizations', 'idTokenType', {
|
||||
type: 'VARCHAR(255)',
|
||||
allowNull: true,
|
||||
});
|
||||
}
|
||||
if (!tableDescription['additionalInfo']) {
|
||||
await queryInterface.addColumn('Authorizations', 'additionalInfo', {
|
||||
type: 'JSONB',
|
||||
allowNull: true,
|
||||
});
|
||||
}
|
||||
if (!tableDescription['status']) {
|
||||
await queryInterface.addColumn('Authorizations', 'status', {
|
||||
type: 'VARCHAR(255)',
|
||||
allowNull: true,
|
||||
});
|
||||
}
|
||||
if (!tableDescription['cacheExpiryDateTime']) {
|
||||
await queryInterface.addColumn('Authorizations', 'cacheExpiryDateTime', {
|
||||
type: 'TIMESTAMP WITH TIME ZONE',
|
||||
allowNull: true,
|
||||
});
|
||||
}
|
||||
if (!tableDescription['chargingPriority']) {
|
||||
await queryInterface.addColumn('Authorizations', 'chargingPriority', {
|
||||
type: 'INTEGER',
|
||||
allowNull: true,
|
||||
});
|
||||
}
|
||||
if (!tableDescription['language1']) {
|
||||
await queryInterface.addColumn('Authorizations', 'language1', {
|
||||
type: 'VARCHAR(255)',
|
||||
allowNull: true,
|
||||
});
|
||||
}
|
||||
if (!tableDescription['language2']) {
|
||||
await queryInterface.addColumn('Authorizations', 'language2', {
|
||||
type: 'VARCHAR(255)',
|
||||
allowNull: true,
|
||||
});
|
||||
}
|
||||
if (!tableDescription['personalMessage']) {
|
||||
await queryInterface.addColumn('Authorizations', 'personalMessage', {
|
||||
type: 'JSON',
|
||||
allowNull: true,
|
||||
});
|
||||
}
|
||||
if (!tableDescription['groupIdTokenId']) {
|
||||
await queryInterface.addColumn('Authorizations', 'groupIdTokenId', {
|
||||
type: 'INTEGER',
|
||||
allowNull: true,
|
||||
references: { model: 'Authorizations', key: 'id' },
|
||||
onUpdate: 'CASCADE',
|
||||
onDelete: 'SET NULL',
|
||||
});
|
||||
}
|
||||
// concurrentTransaction already exists from previous migration, skip it
|
||||
if (!tableDescription['customData']) {
|
||||
await queryInterface.addColumn('Authorizations', 'customData', {
|
||||
type: 'JSONB',
|
||||
allowNull: true,
|
||||
});
|
||||
}
|
||||
if (!tableDescription['groupAuthorizationId']) {
|
||||
await queryInterface.addColumn('Authorizations', 'groupAuthorizationId', {
|
||||
type: 'INTEGER',
|
||||
allowNull: true,
|
||||
});
|
||||
}
|
||||
// Ensure personalMessage is JSONB
|
||||
if (
|
||||
tableDescription['personalMessage'] &&
|
||||
tableDescription['personalMessage'].type !== 'JSONB'
|
||||
) {
|
||||
await queryInterface.changeColumn('Authorizations', 'personalMessage', {
|
||||
type: 'JSONB',
|
||||
allowNull: true,
|
||||
});
|
||||
}
|
||||
// Ensure concurrentTransaction is BOOLEAN
|
||||
if (
|
||||
tableDescription['concurrentTransaction'] &&
|
||||
tableDescription['concurrentTransaction'].type !== 'BOOLEAN'
|
||||
) {
|
||||
await queryInterface.changeColumn('Authorizations', 'concurrentTransaction', {
|
||||
type: 'BOOLEAN',
|
||||
allowNull: true,
|
||||
});
|
||||
}
|
||||
|
||||
// 3. Copy/transform data from old columns/related tables into new flat columns
|
||||
await queryInterface.sequelize.query(`
|
||||
UPDATE "Authorizations"
|
||||
SET
|
||||
"idToken" = subq."idToken",
|
||||
"idTokenType" = subq."idTokenType",
|
||||
"additionalInfo" = subq."additionalInfo",
|
||||
"status" = subq."status",
|
||||
"cacheExpiryDateTime" = subq."cacheExpiryDateTime",
|
||||
"chargingPriority" = subq."chargingPriority",
|
||||
"language1" = subq."language1",
|
||||
"language2" = subq."language2",
|
||||
"personalMessage" = subq."personalMessage",
|
||||
"groupIdTokenId" = subq."groupIdTokenId",
|
||||
"concurrentTransaction" = subq."concurrentTransaction",
|
||||
"customData" = NULL
|
||||
FROM (
|
||||
SELECT
|
||||
auth."id" as auth_id,
|
||||
t."idToken",
|
||||
t."type" as "idTokenType",
|
||||
COALESCE(
|
||||
(
|
||||
SELECT jsonb_agg(
|
||||
jsonb_build_object(
|
||||
'additionalIdToken', ai."additionalIdToken",
|
||||
'type', ai."type"
|
||||
)
|
||||
)
|
||||
FROM "AdditionalInfos" ai
|
||||
INNER JOIN "IdTokenAdditionalInfos" itai ON ai."id" = itai."additionalInfoId"
|
||||
WHERE itai."idTokenId" = t."id"
|
||||
),
|
||||
NULL
|
||||
) as "additionalInfo",
|
||||
COALESCE(info."status", 'Accepted') as "status",
|
||||
info."cacheExpiryDateTime",
|
||||
info."chargingPriority",
|
||||
info."language1",
|
||||
info."language2",
|
||||
info."personalMessage",
|
||||
CASE
|
||||
WHEN info."groupIdTokenId" IS NOT NULL THEN (
|
||||
SELECT auth2."id"
|
||||
FROM "Authorizations" auth2
|
||||
WHERE auth2."idTokenId" = info."groupIdTokenId"
|
||||
LIMIT 1
|
||||
)
|
||||
ELSE NULL
|
||||
END as "groupIdTokenId",
|
||||
COALESCE(auth."concurrentTransaction", false) as "concurrentTransaction"
|
||||
FROM "Authorizations" auth
|
||||
INNER JOIN "IdTokens" t ON auth."idTokenId" = t."id"
|
||||
LEFT JOIN "IdTokenInfos" info ON auth."idTokenInfoId" = info."id"
|
||||
) subq
|
||||
WHERE "Authorizations"."id" = subq.auth_id
|
||||
`);
|
||||
|
||||
// 4. Set NOT NULL and default constraints on new columns as needed
|
||||
await queryInterface.changeColumn('Authorizations', 'idToken', {
|
||||
type: 'VARCHAR(255)',
|
||||
allowNull: false,
|
||||
});
|
||||
await queryInterface.changeColumn('Authorizations', 'status', {
|
||||
type: 'VARCHAR(255)',
|
||||
allowNull: false,
|
||||
defaultValue: 'Accepted',
|
||||
});
|
||||
|
||||
// 5. Drop old columns and tables
|
||||
await queryInterface
|
||||
.removeConstraint('Authorizations', 'Authorizations_idTokenId_fkey')
|
||||
.catch(() => {});
|
||||
await queryInterface
|
||||
.removeConstraint('Authorizations', 'Authorizations_idTokenInfoId_fkey')
|
||||
.catch(() => {});
|
||||
await queryInterface.removeColumn('Authorizations', 'idTokenId').catch(() => {});
|
||||
await queryInterface.removeColumn('Authorizations', 'idTokenInfoId').catch(() => {});
|
||||
|
||||
// Drop all foreign key constraints systematically
|
||||
const constraintsToRemove = [
|
||||
// IdTokens table constraints
|
||||
['IdTokenInfos', 'IdTokenInfos_groupIdTokenId_fkey'],
|
||||
['IdTokenAdditionalInfos', 'IdTokenAdditionalInfos_idTokenId_fkey'],
|
||||
['TransactionEvents', 'TransactionEvents_idTokenId_fkey'],
|
||||
['StopTransactions', 'StopTransactions_idTokenDatabaseId_fkey'],
|
||||
['StartTransactions', 'StartTransactions_idTokenDatabaseId_fkey'],
|
||||
['LocalListAuthorizations', 'LocalListAuthorizations_idTokenId_fkey'],
|
||||
// IdTokenInfos table constraints
|
||||
['LocalListAuthorizations', 'LocalListAuthorizations_idTokenInfoId_fkey'],
|
||||
// AdditionalInfos table constraints
|
||||
['IdTokenAdditionalInfos', 'IdTokenAdditionalInfos_additionalInfoId_fkey'],
|
||||
];
|
||||
|
||||
for (const [tableName, constraintName] of constraintsToRemove) {
|
||||
await queryInterface.removeConstraint(tableName, constraintName).catch(() => {});
|
||||
}
|
||||
|
||||
// Drop junction table first
|
||||
await queryInterface.sequelize.query('DROP TABLE IF EXISTS "IdTokenAdditionalInfos"');
|
||||
|
||||
// Drop temp tables and old tables in correct order
|
||||
const tablesToDrop = [
|
||||
'Authorizations_temp',
|
||||
'IdTokens_temp',
|
||||
'IdTokenInfos_temp',
|
||||
'AdditionalInfos_temp',
|
||||
'IdTokenInfos',
|
||||
'AdditionalInfos',
|
||||
'IdTokens',
|
||||
];
|
||||
|
||||
for (const tableName of tablesToDrop) {
|
||||
await queryInterface.sequelize.query(`DROP TABLE IF EXISTS "${tableName}"`);
|
||||
}
|
||||
},
|
||||
|
||||
down: async (queryInterface: QueryInterface) => {
|
||||
// 1. Add back the old columns to Authorizations table
|
||||
await queryInterface.addColumn('Authorizations', 'idTokenId', {
|
||||
type: 'INTEGER',
|
||||
allowNull: true,
|
||||
});
|
||||
await queryInterface.addColumn('Authorizations', 'idTokenInfoId', {
|
||||
type: 'INTEGER',
|
||||
allowNull: true,
|
||||
});
|
||||
|
||||
// 2. Recreate old tables structure
|
||||
await queryInterface.sequelize.query(`
|
||||
CREATE TABLE "IdTokens" (
|
||||
"id" SERIAL PRIMARY KEY,
|
||||
"idToken" VARCHAR(255) NOT NULL,
|
||||
"type" VARCHAR(255),
|
||||
"createdAt" TIMESTAMP WITH TIME ZONE DEFAULT now(),
|
||||
"updatedAt" TIMESTAMP WITH TIME ZONE DEFAULT now()
|
||||
);
|
||||
`);
|
||||
await queryInterface.sequelize.query(`
|
||||
CREATE TABLE "IdTokenInfos" (
|
||||
"id" SERIAL PRIMARY KEY,
|
||||
"info" JSONB,
|
||||
"status" VARCHAR(255),
|
||||
"cacheExpiryDateTime" TIMESTAMP WITH TIME ZONE,
|
||||
"chargingPriority" INTEGER,
|
||||
"language1" VARCHAR(255),
|
||||
"language2" VARCHAR(255),
|
||||
"personalMessage" JSON,
|
||||
"groupIdTokenId" INTEGER,
|
||||
"concurrentTransaction" BOOLEAN,
|
||||
"createdAt" TIMESTAMP WITH TIME ZONE DEFAULT now(),
|
||||
"updatedAt" TIMESTAMP WITH TIME ZONE DEFAULT now()
|
||||
);
|
||||
`);
|
||||
await queryInterface.sequelize.query(`
|
||||
CREATE TABLE "AdditionalInfos" (
|
||||
"id" SERIAL PRIMARY KEY,
|
||||
"additionalIdToken" VARCHAR(255) NOT NULL,
|
||||
"type" VARCHAR(255) NOT NULL,
|
||||
"createdAt" TIMESTAMP WITH TIME ZONE DEFAULT now(),
|
||||
"updatedAt" TIMESTAMP WITH TIME ZONE DEFAULT now(),
|
||||
UNIQUE("additionalIdToken", "type")
|
||||
);
|
||||
`);
|
||||
|
||||
await queryInterface.sequelize.query(`
|
||||
CREATE TABLE "IdTokenAdditionalInfos" (
|
||||
"id" SERIAL PRIMARY KEY,
|
||||
"idTokenId" INTEGER REFERENCES "IdTokens"("id") ON DELETE CASCADE,
|
||||
"additionalInfoId" INTEGER REFERENCES "AdditionalInfos"("id") ON DELETE CASCADE,
|
||||
"createdAt" TIMESTAMP WITH TIME ZONE DEFAULT now(),
|
||||
"updatedAt" TIMESTAMP WITH TIME ZONE DEFAULT now(),
|
||||
UNIQUE("idTokenId", "additionalInfoId")
|
||||
);
|
||||
`);
|
||||
|
||||
// 3. Restore data to the recreated tables from flattened Authorization data
|
||||
// Insert IdTokens from flattened data
|
||||
await queryInterface.sequelize.query(`
|
||||
INSERT INTO "IdTokens" ("idToken", "type", "createdAt", "updatedAt")
|
||||
SELECT DISTINCT "idToken", "idTokenType", "createdAt", "updatedAt"
|
||||
FROM "Authorizations"
|
||||
WHERE "idToken" IS NOT NULL
|
||||
ON CONFLICT ("idToken", "type") DO NOTHING
|
||||
`);
|
||||
|
||||
// Insert IdTokenInfos from flattened data (with proper groupIdTokenId handling)
|
||||
await queryInterface.sequelize.query(`
|
||||
INSERT INTO "IdTokenInfos" (
|
||||
"status", "cacheExpiryDateTime", "chargingPriority", "language1", "language2",
|
||||
"personalMessage", "groupIdTokenId", "concurrentTransaction", "createdAt", "updatedAt"
|
||||
)
|
||||
SELECT DISTINCT
|
||||
"status", "cacheExpiryDateTime", "chargingPriority", "language1", "language2",
|
||||
"personalMessage",
|
||||
CASE
|
||||
WHEN "groupIdTokenId" IS NOT NULL THEN (
|
||||
SELECT t."id"
|
||||
FROM "IdTokens" t
|
||||
INNER JOIN "Authorizations" auth ON auth."idToken" = t."idToken" AND auth."idTokenType" = t."type"
|
||||
WHERE auth."id" = "Authorizations"."groupIdTokenId"
|
||||
LIMIT 1
|
||||
)
|
||||
ELSE NULL
|
||||
END as "groupIdTokenId",
|
||||
"concurrentTransaction", "createdAt", "updatedAt"
|
||||
FROM "Authorizations"
|
||||
WHERE "status" IS NOT NULL
|
||||
`);
|
||||
|
||||
// Insert AdditionalInfos from flattened additionalInfo JSONB array
|
||||
await queryInterface.sequelize.query(`
|
||||
INSERT INTO "AdditionalInfos" ("additionalIdToken", "type", "createdAt", "updatedAt")
|
||||
SELECT DISTINCT
|
||||
(jsonb_array_elements("additionalInfo")->>'additionalIdToken')::VARCHAR(255),
|
||||
(jsonb_array_elements("additionalInfo")->>'type')::VARCHAR(255),
|
||||
"createdAt",
|
||||
"updatedAt"
|
||||
FROM "Authorizations"
|
||||
WHERE "additionalInfo" IS NOT NULL
|
||||
AND jsonb_array_length("additionalInfo") > 0
|
||||
ON CONFLICT ("additionalIdToken", "type") DO NOTHING
|
||||
`);
|
||||
|
||||
// Create IdTokenAdditionalInfo junction table relationships
|
||||
await queryInterface.sequelize.query(`
|
||||
INSERT INTO "IdTokenAdditionalInfos" ("idTokenId", "additionalInfoId", "createdAt", "updatedAt")
|
||||
SELECT DISTINCT
|
||||
t."id" as "idTokenId",
|
||||
ai."id" as "additionalInfoId",
|
||||
a."createdAt",
|
||||
a."updatedAt"
|
||||
FROM "Authorizations" a
|
||||
INNER JOIN "IdTokens" t ON a."idToken" = t."idToken" AND a."idTokenType" = t."type"
|
||||
CROSS JOIN LATERAL jsonb_array_elements(a."additionalInfo") as elem
|
||||
INNER JOIN "AdditionalInfos" ai ON
|
||||
ai."additionalIdToken" = (elem->>'additionalIdToken')::VARCHAR(255) AND
|
||||
ai."type" = (elem->>'type')::VARCHAR(255)
|
||||
WHERE a."additionalInfo" IS NOT NULL
|
||||
AND jsonb_array_length(a."additionalInfo") > 0
|
||||
ON CONFLICT ("idTokenId", "additionalInfoId") DO NOTHING
|
||||
`);
|
||||
|
||||
// 4. Update Authorizations with foreign key references
|
||||
await queryInterface.sequelize.query(`
|
||||
UPDATE "Authorizations"
|
||||
SET
|
||||
"idTokenId" = t."id",
|
||||
"idTokenInfoId" = (
|
||||
SELECT info."id"
|
||||
FROM "IdTokenInfos" info
|
||||
WHERE COALESCE("Authorizations"."status", 'Accepted') = info."status"
|
||||
AND COALESCE("Authorizations"."cacheExpiryDateTime", '1970-01-01'::timestamp) = COALESCE(info."cacheExpiryDateTime", '1970-01-01'::timestamp)
|
||||
AND COALESCE("Authorizations"."chargingPriority", -999) = COALESCE(info."chargingPriority", -999)
|
||||
AND COALESCE("Authorizations"."language1", '') = COALESCE(info."language1", '')
|
||||
AND COALESCE("Authorizations"."language2", '') = COALESCE(info."language2", '')
|
||||
LIMIT 1
|
||||
)
|
||||
FROM "IdTokens" t
|
||||
WHERE "Authorizations"."idToken" = t."idToken"
|
||||
AND COALESCE("Authorizations"."idTokenType", '') = COALESCE(t."type", '')
|
||||
`);
|
||||
|
||||
// 5. Add foreign key constraints back
|
||||
await queryInterface.addConstraint('Authorizations', {
|
||||
fields: ['idTokenId'],
|
||||
type: 'foreign key',
|
||||
name: 'Authorizations_idTokenId_fkey',
|
||||
references: { table: 'IdTokens', field: 'id' },
|
||||
onUpdate: 'CASCADE',
|
||||
onDelete: 'CASCADE',
|
||||
});
|
||||
|
||||
await queryInterface.addConstraint('Authorizations', {
|
||||
fields: ['idTokenInfoId'],
|
||||
type: 'foreign key',
|
||||
name: 'Authorizations_idTokenInfoId_fkey',
|
||||
references: { table: 'IdTokenInfos', field: 'id' },
|
||||
onUpdate: 'CASCADE',
|
||||
onDelete: 'SET NULL',
|
||||
});
|
||||
|
||||
// 6. Remove flattened columns
|
||||
await queryInterface.removeColumn('Authorizations', 'idToken').catch(() => {});
|
||||
await queryInterface.removeColumn('Authorizations', 'idTokenType').catch(() => {});
|
||||
await queryInterface.removeColumn('Authorizations', 'additionalInfo').catch(() => {});
|
||||
await queryInterface.removeColumn('Authorizations', 'status').catch(() => {});
|
||||
await queryInterface.removeColumn('Authorizations', 'cacheExpiryDateTime').catch(() => {});
|
||||
await queryInterface.removeColumn('Authorizations', 'chargingPriority').catch(() => {});
|
||||
await queryInterface.removeColumn('Authorizations', 'language1').catch(() => {});
|
||||
await queryInterface.removeColumn('Authorizations', 'language2').catch(() => {});
|
||||
await queryInterface.removeColumn('Authorizations', 'personalMessage').catch(() => {});
|
||||
await queryInterface.removeColumn('Authorizations', 'groupIdTokenId').catch(() => {});
|
||||
await queryInterface.removeColumn('Authorizations', 'concurrentTransaction').catch(() => {});
|
||||
await queryInterface.removeColumn('Authorizations', 'customData').catch(() => {});
|
||||
await queryInterface.removeColumn('Authorizations', 'groupAuthorizationId').catch(() => {});
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,53 @@
|
||||
// SPDX-FileCopyrightText: 2025 Contributors to the CitrineOS Project
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
import { DEFAULT_TENANT_ID } from '@citrineos/base';
|
||||
import { DataTypes, QueryInterface } from 'sequelize';
|
||||
|
||||
export default {
|
||||
up: async (queryInterface: QueryInterface) => {
|
||||
await queryInterface.createTable('TenantPartners', {
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true,
|
||||
allowNull: false,
|
||||
},
|
||||
partyId: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
},
|
||||
countryCode: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
},
|
||||
tenantId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
references: {
|
||||
model: 'Tenants',
|
||||
key: 'id',
|
||||
},
|
||||
defaultValue: DEFAULT_TENANT_ID,
|
||||
onUpdate: 'CASCADE',
|
||||
onDelete: 'CASCADE',
|
||||
},
|
||||
partnerProfileOCPI: {
|
||||
type: DataTypes.JSONB,
|
||||
allowNull: true,
|
||||
},
|
||||
createdAt: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false,
|
||||
},
|
||||
updatedAt: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false,
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
down: async (queryInterface: QueryInterface) => {
|
||||
await queryInterface.dropTable('TenantPartners');
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,34 @@
|
||||
// SPDX-FileCopyrightText: 2025 Contributors to the CitrineOS Project
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
import { DataTypes, QueryInterface } from 'sequelize';
|
||||
|
||||
export default {
|
||||
up: async (queryInterface: QueryInterface) => {
|
||||
await queryInterface.addColumn('Tenants', 'partyId', {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
defaultValue: 'default',
|
||||
});
|
||||
await queryInterface.addColumn('Tenants', 'countryCode', {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
defaultValue: 'US',
|
||||
});
|
||||
await queryInterface.addColumn('Tenants', 'url', {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
});
|
||||
await queryInterface.addColumn('Tenants', 'serverProfileOCPI', {
|
||||
type: DataTypes.JSONB,
|
||||
allowNull: true,
|
||||
});
|
||||
},
|
||||
|
||||
down: async (queryInterface: QueryInterface) => {
|
||||
await queryInterface.removeColumn('Tenants', 'partyId');
|
||||
await queryInterface.removeColumn('Tenants', 'countryCode');
|
||||
await queryInterface.removeColumn('Tenants', 'url');
|
||||
await queryInterface.removeColumn('Tenants', 'serverProfileOCPI');
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,78 @@
|
||||
// SPDX-FileCopyrightText: 2025 Contributors to the CitrineOS Project
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
'use strict';
|
||||
|
||||
/** @type {import('sequelize-cli').Migration} */
|
||||
import { DataTypes, QueryInterface } from 'sequelize';
|
||||
|
||||
const TABLE_NAME = 'AsyncJobStatuses';
|
||||
|
||||
export default {
|
||||
up: async (queryInterface: QueryInterface) => {
|
||||
await queryInterface.createTable(TABLE_NAME, {
|
||||
jobId: {
|
||||
type: DataTypes.STRING,
|
||||
primaryKey: true,
|
||||
allowNull: false,
|
||||
},
|
||||
jobName: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
},
|
||||
tenantPartnerId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
references: {
|
||||
model: 'TenantPartners',
|
||||
key: 'id',
|
||||
},
|
||||
onUpdate: 'CASCADE',
|
||||
onDelete: 'RESTRICT',
|
||||
},
|
||||
finishedAt: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: true,
|
||||
},
|
||||
stoppedAt: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: true,
|
||||
},
|
||||
stopScheduled: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
allowNull: false,
|
||||
defaultValue: false,
|
||||
},
|
||||
isFailed: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
allowNull: false,
|
||||
defaultValue: false,
|
||||
},
|
||||
paginationParams: {
|
||||
type: DataTypes.JSON,
|
||||
allowNull: false,
|
||||
},
|
||||
totalObjects: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
},
|
||||
createdAt: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false,
|
||||
defaultValue: DataTypes.NOW,
|
||||
},
|
||||
updatedAt: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false,
|
||||
defaultValue: DataTypes.NOW,
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
down: async (queryInterface: QueryInterface) => {
|
||||
await queryInterface.dropTable(TABLE_NAME);
|
||||
await queryInterface.sequelize.query(`
|
||||
DROP TYPE IF EXISTS "enum_AsyncJobStatuses_jobName";
|
||||
`);
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,603 @@
|
||||
// SPDX-FileCopyrightText: 2025 Contributors to the CitrineOS Project
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
'use strict';
|
||||
|
||||
/** @type {import('sequelize-cli').Migration} */
|
||||
import { DataTypes, QueryInterface } from 'sequelize';
|
||||
|
||||
export default {
|
||||
up: async (queryInterface: QueryInterface) => {
|
||||
// Helper to check if a constraint exists (robust for schema/casing)
|
||||
const constraintExists = async (
|
||||
tableName: string,
|
||||
constraintName: string,
|
||||
): Promise<boolean> => {
|
||||
const [results] = await queryInterface.sequelize.query(
|
||||
`SELECT constraint_name FROM information_schema.table_constraints WHERE table_schema = 'public' AND table_name = '${tableName}' AND constraint_name = '${constraintName}';`,
|
||||
);
|
||||
return results.length > 0;
|
||||
};
|
||||
|
||||
// 1. Create EvseTypes table
|
||||
await queryInterface.createTable('EvseTypes', {
|
||||
databaseId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
autoIncrement: true,
|
||||
primaryKey: true,
|
||||
},
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
},
|
||||
connectorId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
},
|
||||
tenantId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
references: {
|
||||
model: 'Tenants',
|
||||
key: 'id',
|
||||
},
|
||||
onUpdate: 'CASCADE',
|
||||
onDelete: 'RESTRICT',
|
||||
},
|
||||
createdAt: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false,
|
||||
defaultValue: DataTypes.NOW,
|
||||
},
|
||||
updatedAt: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false,
|
||||
defaultValue: DataTypes.NOW,
|
||||
},
|
||||
});
|
||||
|
||||
// 2. Add all missing columns
|
||||
await queryInterface.renameColumn('ChargingNeeds', 'evseDatabaseId', 'evseId');
|
||||
await queryInterface.addColumn('Evses', 'stationId', {
|
||||
type: DataTypes.STRING(36),
|
||||
allowNull: true,
|
||||
});
|
||||
await queryInterface.addColumn('TransactionEvents', 'idTokenValue', {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
});
|
||||
await queryInterface.removeColumn('Authorizations', 'groupIdTokenId');
|
||||
await queryInterface.addColumn('Authorizations', 'tenantPartnerId', {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
});
|
||||
await queryInterface.addColumn('Connectors', 'evseId', {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
});
|
||||
await queryInterface.addColumn('Tariffs', 'connectorId', {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
});
|
||||
await queryInterface.addColumn('LocalListAuthorizations', 'groupAuthorizationId', {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
});
|
||||
await queryInterface.removeColumn('LocalListAuthorizations', 'idTokenId');
|
||||
await queryInterface.addColumn('LocalListAuthorizations', 'idToken', {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
});
|
||||
await queryInterface.sequelize.query(
|
||||
'UPDATE "LocalListAuthorizations" SET "idToken" = \'\' WHERE "idToken" IS NULL;',
|
||||
);
|
||||
await queryInterface.changeColumn('LocalListAuthorizations', 'idToken', {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
});
|
||||
await queryInterface.addColumn('LocalListAuthorizations', 'idTokenType', {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
});
|
||||
await queryInterface.addColumn('LocalListAuthorizations', 'additionalInfo', {
|
||||
type: DataTypes.JSONB,
|
||||
allowNull: true,
|
||||
});
|
||||
await queryInterface.addColumn('LocalListAuthorizations', 'status', {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
defaultValue: 'Accepted',
|
||||
});
|
||||
await queryInterface.addColumn('LocalListAuthorizations', 'cacheExpiryDateTime', {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: true,
|
||||
});
|
||||
await queryInterface.addColumn('LocalListAuthorizations', 'chargingPriority', {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
});
|
||||
await queryInterface.addColumn('LocalListAuthorizations', 'language1', {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
});
|
||||
await queryInterface.addColumn('LocalListAuthorizations', 'language2', {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
});
|
||||
await queryInterface.addColumn('LocalListAuthorizations', 'personalMessage', {
|
||||
type: DataTypes.JSON,
|
||||
allowNull: true,
|
||||
});
|
||||
await queryInterface.removeColumn('LocalListAuthorizations', 'idTokenInfoId');
|
||||
await queryInterface.addColumn('LocalListAuthorizations', 'customData', {
|
||||
type: DataTypes.JSONB,
|
||||
allowNull: true,
|
||||
});
|
||||
await queryInterface.addColumn('Evses', 'evseTypeId', {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
});
|
||||
await queryInterface.addColumn('TransactionEvents', 'idTokenType', {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
});
|
||||
await queryInterface.removeColumn('TransactionEvents', 'idTokenId');
|
||||
await queryInterface.addColumn('Evses', 'evseId', {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
});
|
||||
await queryInterface.addColumn('Evses', 'physicalReference', {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
});
|
||||
await queryInterface.addColumn('Evses', 'removed', {
|
||||
type: DataTypes.BOOLEAN,
|
||||
allowNull: true,
|
||||
});
|
||||
await queryInterface.addColumn('StopTransactions', 'idTokenValue', {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
});
|
||||
await queryInterface.addColumn('StopTransactions', 'idTokenType', {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
});
|
||||
await queryInterface.removeColumn('StopTransactions', 'idTokenDatabaseId');
|
||||
// ChargingStation: Add missing columns
|
||||
await queryInterface.addColumn('ChargingStations', 'coordinates', {
|
||||
type: DataTypes.GEOMETRY('POINT'),
|
||||
allowNull: true,
|
||||
});
|
||||
await queryInterface.addColumn('ChargingStations', 'floorLevel', {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
});
|
||||
await queryInterface.addColumn('ChargingStations', 'parkingRestrictions', {
|
||||
type: DataTypes.JSONB,
|
||||
allowNull: true,
|
||||
});
|
||||
await queryInterface.addColumn('ChargingStations', 'capabilities', {
|
||||
type: DataTypes.JSONB,
|
||||
allowNull: true,
|
||||
});
|
||||
await queryInterface.addColumn('Locations', 'publishUpstream', {
|
||||
type: DataTypes.BOOLEAN,
|
||||
defaultValue: true,
|
||||
});
|
||||
await queryInterface.addColumn('Locations', 'timeZone', {
|
||||
type: DataTypes.STRING,
|
||||
defaultValue: 'UTC',
|
||||
});
|
||||
await queryInterface.addColumn('Locations', 'parkingType', {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
});
|
||||
await queryInterface.addColumn('Locations', 'facilities', {
|
||||
type: DataTypes.JSONB,
|
||||
allowNull: true,
|
||||
});
|
||||
await queryInterface.addColumn('Locations', 'openingHours', {
|
||||
type: DataTypes.JSONB,
|
||||
allowNull: true,
|
||||
});
|
||||
// Tariff: Add missing column
|
||||
await queryInterface.addColumn('Tariffs', 'tariffAltText', {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
});
|
||||
|
||||
await queryInterface.addColumn('MeterValues', 'customData', {
|
||||
type: DataTypes.JSONB,
|
||||
allowNull: true,
|
||||
});
|
||||
await queryInterface.addColumn('MeterValues', 'tariffId', {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
references: {
|
||||
model: 'Tariffs',
|
||||
key: 'id',
|
||||
},
|
||||
onUpdate: 'CASCADE',
|
||||
onDelete: 'SET NULL',
|
||||
});
|
||||
await queryInterface.addColumn('MeterValues', 'transactionId', {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
});
|
||||
|
||||
await queryInterface.removeColumn('Transactions', 'evseDatabaseId');
|
||||
await queryInterface.addColumn('Transactions', 'locationId', {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
});
|
||||
await queryInterface.addColumn('Transactions', 'evseId', {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
});
|
||||
await queryInterface.addColumn('Transactions', 'connectorId', {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
});
|
||||
await queryInterface.addColumn('Transactions', 'authorizationId', {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
});
|
||||
await queryInterface.addColumn('Transactions', 'tariffId', {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
});
|
||||
await queryInterface.addColumn('Transactions', 'startTime', {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: true,
|
||||
});
|
||||
await queryInterface.addColumn('Transactions', 'endTime', {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: true,
|
||||
});
|
||||
await queryInterface.addColumn('Transactions', 'customData', {
|
||||
type: DataTypes.JSONB,
|
||||
allowNull: true,
|
||||
});
|
||||
|
||||
// 3. Drop dependent foreign key constraints
|
||||
await queryInterface.sequelize.query(
|
||||
'ALTER TABLE "Transactions" DROP CONSTRAINT IF EXISTS "Transactions_evseDatabaseId_fkey";',
|
||||
);
|
||||
await queryInterface.sequelize.query(
|
||||
'ALTER TABLE "ChargingNeeds" DROP CONSTRAINT IF EXISTS "ChargingNeeds_evseDatabaseId_fkey";',
|
||||
);
|
||||
await queryInterface.sequelize.query(
|
||||
'ALTER TABLE "Components" DROP CONSTRAINT IF EXISTS "Components_evseDatabaseId_fkey";',
|
||||
);
|
||||
await queryInterface.sequelize.query(
|
||||
'ALTER TABLE "TransactionEvents" DROP CONSTRAINT IF EXISTS "TransactionEvents_evseId_fkey";',
|
||||
);
|
||||
await queryInterface.sequelize.query(
|
||||
'ALTER TABLE "Reservations" DROP CONSTRAINT IF EXISTS "Reservations_evseId_fkey";',
|
||||
);
|
||||
await queryInterface.sequelize.query(
|
||||
'ALTER TABLE "VariableAttributes" DROP CONSTRAINT IF EXISTS "VariableAttributes_evseDatabaseId_fkey";',
|
||||
);
|
||||
await queryInterface.sequelize.query(
|
||||
'ALTER TABLE "Components" DROP CONSTRAINT IF EXISTS "Components_evseTypeId_fkey";',
|
||||
);
|
||||
await queryInterface.sequelize.query(
|
||||
'ALTER TABLE "Reservations" DROP CONSTRAINT IF EXISTS "Reservations_evseTypeId_fkey";',
|
||||
);
|
||||
await queryInterface.sequelize.query(
|
||||
'ALTER TABLE "TransactionEvents" DROP CONSTRAINT IF EXISTS "TransactionEvents_evseTypeId_fkey";',
|
||||
);
|
||||
await queryInterface.sequelize.query(
|
||||
'ALTER TABLE "VariableAttributes" DROP CONSTRAINT IF EXISTS "VariableAttributes_evseTypeId_fkey";',
|
||||
);
|
||||
|
||||
// 4. Fix the Evses table
|
||||
await queryInterface.sequelize.query(
|
||||
'ALTER TABLE "Evses" DROP CONSTRAINT IF EXISTS "Evses_pkey";',
|
||||
);
|
||||
|
||||
// Populate EvseTypes from existing data before adding foreign keys
|
||||
await queryInterface.sequelize.query(`
|
||||
INSERT INTO "EvseTypes" ("id", "tenantId", "connectorId", "createdAt", "updatedAt")
|
||||
SELECT "id", "tenantId", "connectorId", NOW(), NOW()
|
||||
FROM "Evses";
|
||||
`);
|
||||
|
||||
// Truncate Evses table after migration
|
||||
await queryInterface.sequelize.query('TRUNCATE TABLE "Evses" CASCADE;');
|
||||
|
||||
await queryInterface.removeColumn('Evses', 'databaseId');
|
||||
await queryInterface.removeColumn('Evses', 'connectorId');
|
||||
await queryInterface.removeColumn('Evses', 'id');
|
||||
// Sequelize does not support adding a primary key via addColumn, so we do it in two steps
|
||||
await queryInterface.addColumn('Evses', 'id', {
|
||||
type: DataTypes.INTEGER,
|
||||
autoIncrement: true,
|
||||
});
|
||||
await queryInterface.sequelize.query(
|
||||
'ALTER TABLE "Evses" ADD CONSTRAINT "Evses_pkey" PRIMARY KEY (id);',
|
||||
);
|
||||
|
||||
await queryInterface.addColumn('Connectors', 'evseTypeConnectorId', {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
});
|
||||
await queryInterface.addColumn('Connectors', 'type', {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
});
|
||||
await queryInterface.addColumn('Connectors', 'format', {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
});
|
||||
await queryInterface.addColumn('Connectors', 'powerType', {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
});
|
||||
await queryInterface.addColumn('Connectors', 'maximumAmperage', {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
});
|
||||
await queryInterface.addColumn('Connectors', 'maximumVoltage', {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
});
|
||||
await queryInterface.addColumn('Connectors', 'maximumPowerWatts', {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
});
|
||||
await queryInterface.addColumn('Connectors', 'termsAndConditionsUrl', {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
});
|
||||
// Foreign key constraints for relationships (add only if not exists, drop if exists first)
|
||||
if (await constraintExists('Connectors', 'Connectors_evseId_fkey')) {
|
||||
await queryInterface.sequelize.query(
|
||||
'ALTER TABLE "Connectors" DROP CONSTRAINT IF EXISTS "Connectors_evseId_fkey";',
|
||||
);
|
||||
}
|
||||
if (!(await constraintExists('Connectors', 'Connectors_evseId_fkey'))) {
|
||||
await queryInterface.sequelize.query(
|
||||
'ALTER TABLE "Connectors" ADD CONSTRAINT "Connectors_evseId_fkey" FOREIGN KEY ("evseId") REFERENCES "Evses" (id) ON UPDATE CASCADE ON DELETE SET NULL;',
|
||||
);
|
||||
}
|
||||
if (!(await constraintExists('Connectors', 'Connectors_stationId_fkey'))) {
|
||||
await queryInterface.sequelize.query(
|
||||
'ALTER TABLE "Connectors" ADD CONSTRAINT "Connectors_stationId_fkey" FOREIGN KEY ("stationId") REFERENCES "ChargingStations" (id) ON UPDATE CASCADE ON DELETE SET NULL;',
|
||||
);
|
||||
}
|
||||
if (!(await constraintExists('ChargingStations', 'ChargingStations_locationId_fkey'))) {
|
||||
await queryInterface.sequelize.query(
|
||||
'ALTER TABLE "ChargingStations" ADD CONSTRAINT "ChargingStations_locationId_fkey" FOREIGN KEY ("locationId") REFERENCES "Locations" (id) ON UPDATE CASCADE ON DELETE SET NULL;',
|
||||
);
|
||||
}
|
||||
if (!(await constraintExists('Authorizations', 'Authorizations_groupAuthorizationId_fkey'))) {
|
||||
await queryInterface.sequelize.query(
|
||||
'ALTER TABLE "Authorizations" ADD CONSTRAINT "Authorizations_groupAuthorizationId_fkey" FOREIGN KEY ("groupAuthorizationId") REFERENCES "Authorizations" (id) ON UPDATE CASCADE ON DELETE SET NULL;',
|
||||
);
|
||||
}
|
||||
if (!(await constraintExists('ChargingNeeds', 'ChargingNeeds_transactionDatabaseId_fkey'))) {
|
||||
await queryInterface.sequelize.query(
|
||||
'ALTER TABLE "ChargingNeeds" ADD CONSTRAINT "ChargingNeeds_transactionDatabaseId_fkey" FOREIGN KEY ("transactionDatabaseId") REFERENCES "Transactions" (id) ON UPDATE CASCADE ON DELETE SET NULL;',
|
||||
);
|
||||
}
|
||||
|
||||
// Rename partnerProfile to partnerProfileOCPI in TenantPartners ---
|
||||
const tenantPartnersTable = 'TenantPartners';
|
||||
const oldColumn = 'partnerProfile';
|
||||
const newColumn = 'partnerProfileOCPI';
|
||||
const tenantPartnersDesc = await queryInterface.describeTable(tenantPartnersTable);
|
||||
if (tenantPartnersDesc[oldColumn] && !tenantPartnersDesc[newColumn]) {
|
||||
await queryInterface.renameColumn(tenantPartnersTable, oldColumn, newColumn);
|
||||
}
|
||||
|
||||
// 5. Re-create all foreign key constraints
|
||||
await queryInterface.sequelize.query(
|
||||
'ALTER TABLE "Transactions" ADD CONSTRAINT "Transactions_evseId_fkey" FOREIGN KEY ("evseId") REFERENCES "Evses" (id) ON UPDATE CASCADE ON DELETE SET NULL;',
|
||||
);
|
||||
await queryInterface.sequelize.query(
|
||||
'ALTER TABLE "ChargingNeeds" ADD CONSTRAINT "ChargingNeeds_evseId_fkey" FOREIGN KEY ("evseId") REFERENCES "Evses" (id) ON UPDATE CASCADE ON DELETE SET NULL;',
|
||||
);
|
||||
await queryInterface.sequelize.query(
|
||||
'ALTER TABLE "Authorizations" ADD CONSTRAINT "Authorizations_tenantPartnerId_fkey" FOREIGN KEY ("tenantPartnerId") REFERENCES "TenantPartners" (id) ON UPDATE CASCADE ON DELETE SET NULL;',
|
||||
);
|
||||
await queryInterface.sequelize.query(
|
||||
'ALTER TABLE "Evses" ADD CONSTRAINT "Evses_stationId_fkey" FOREIGN KEY ("stationId") REFERENCES "ChargingStations" (id) ON UPDATE CASCADE ON DELETE SET NULL;',
|
||||
);
|
||||
await queryInterface.sequelize.query(
|
||||
'ALTER TABLE "Tariffs" ADD CONSTRAINT "Tariffs_connectorId_fkey" FOREIGN KEY ("connectorId") REFERENCES "Connectors" (id) ON UPDATE CASCADE ON DELETE SET NULL;',
|
||||
);
|
||||
await queryInterface.sequelize.query(
|
||||
'ALTER TABLE "Transactions" ADD CONSTRAINT "Transactions_tariffId_fkey" FOREIGN KEY ("tariffId") REFERENCES "Tariffs" (id) ON UPDATE CASCADE ON DELETE SET NULL;',
|
||||
);
|
||||
await queryInterface.sequelize.query(
|
||||
'ALTER TABLE "Transactions" ADD CONSTRAINT "Transactions_authorizationId_fkey" FOREIGN KEY ("authorizationId") REFERENCES "Authorizations" (id) ON UPDATE CASCADE ON DELETE SET NULL;',
|
||||
);
|
||||
await queryInterface.sequelize.query(
|
||||
'ALTER TABLE "Transactions" ADD CONSTRAINT "Transactions_connectorId_fkey" FOREIGN KEY ("connectorId") REFERENCES "Connectors" (id) ON UPDATE CASCADE ON DELETE SET NULL;',
|
||||
);
|
||||
await queryInterface.sequelize.query(
|
||||
'ALTER TABLE "Transactions" ADD CONSTRAINT "Transactions_locationId_fkey" FOREIGN KEY ("locationId") REFERENCES "Locations" (id) ON UPDATE CASCADE ON DELETE SET NULL;',
|
||||
);
|
||||
await queryInterface.sequelize.query(
|
||||
'ALTER TABLE "LocalListAuthorizations" ADD CONSTRAINT "LocalListAuthorizations_groupAuthorizationId_fkey" FOREIGN KEY ("groupAuthorizationId") REFERENCES "Authorizations" (id) ON UPDATE CASCADE ON DELETE SET NULL;',
|
||||
);
|
||||
await queryInterface.sequelize.query(
|
||||
'ALTER TABLE "Components" ADD CONSTRAINT "Components_evseTypeId_fkey" FOREIGN KEY ("evseDatabaseId") REFERENCES "EvseTypes" ("databaseId") ON UPDATE CASCADE ON DELETE SET NULL;',
|
||||
);
|
||||
await queryInterface.sequelize.query(
|
||||
'ALTER TABLE "Reservations" ADD CONSTRAINT "Reservations_evseTypeId_fkey" FOREIGN KEY ("evseId") REFERENCES "EvseTypes" ("databaseId") ON UPDATE CASCADE ON DELETE SET NULL;',
|
||||
);
|
||||
await queryInterface.sequelize.query(
|
||||
'ALTER TABLE "TransactionEvents" ADD CONSTRAINT "TransactionEvents_evseTypeId_fkey" FOREIGN KEY ("evseId") REFERENCES "EvseTypes" ("databaseId") ON UPDATE CASCADE ON DELETE SET NULL;',
|
||||
);
|
||||
await queryInterface.sequelize.query(
|
||||
'ALTER TABLE "VariableAttributes" ADD CONSTRAINT "VariableAttributes_evseTypeId_fkey" FOREIGN KEY ("evseDatabaseId") REFERENCES "EvseTypes" ("databaseId") ON UPDATE CASCADE ON DELETE SET NULL;',
|
||||
);
|
||||
},
|
||||
|
||||
down: async (queryInterface: QueryInterface) => {
|
||||
// 1. Drop all foreign key constraints added in up
|
||||
await queryInterface.sequelize.query(
|
||||
'ALTER TABLE "Transactions" DROP CONSTRAINT IF EXISTS "Transactions_evseId_fkey";',
|
||||
);
|
||||
await queryInterface.sequelize.query(
|
||||
'ALTER TABLE "Transactions" DROP CONSTRAINT IF EXISTS "Transactions_tariffId_fkey";',
|
||||
);
|
||||
await queryInterface.sequelize.query(
|
||||
'ALTER TABLE "Transactions" DROP CONSTRAINT IF EXISTS "Transactions_authorizationId_fkey";',
|
||||
);
|
||||
await queryInterface.sequelize.query(
|
||||
'ALTER TABLE "Transactions" DROP CONSTRAINT IF EXISTS "Transactions_connectorId_fkey";',
|
||||
);
|
||||
await queryInterface.sequelize.query(
|
||||
'ALTER TABLE "Transactions" DROP CONSTRAINT IF EXISTS "Transactions_locationId_fkey";',
|
||||
);
|
||||
await queryInterface.sequelize.query(
|
||||
'ALTER TABLE "ChargingNeeds" DROP CONSTRAINT IF EXISTS "ChargingNeeds_evseId_fkey";',
|
||||
);
|
||||
await queryInterface.sequelize.query(
|
||||
'ALTER TABLE "Authorizations" DROP CONSTRAINT IF EXISTS "Authorizations_tenantPartnerId_fkey";',
|
||||
);
|
||||
await queryInterface.sequelize.query(
|
||||
'ALTER TABLE "Authorizations" DROP CONSTRAINT IF EXISTS "Authorizations_groupAuthorizationId_fkey";',
|
||||
);
|
||||
await queryInterface.sequelize.query(
|
||||
'ALTER TABLE "Evses" DROP CONSTRAINT IF EXISTS "Evses_stationId_fkey";',
|
||||
);
|
||||
await queryInterface.sequelize.query(
|
||||
'ALTER TABLE "Connectors" DROP CONSTRAINT IF EXISTS "Connectors_evseId_fkey";',
|
||||
);
|
||||
await queryInterface.sequelize.query(
|
||||
'ALTER TABLE "Tariffs" DROP CONSTRAINT IF EXISTS "Tariffs_connectorId_fkey";',
|
||||
);
|
||||
await queryInterface.sequelize.query(
|
||||
'ALTER TABLE "LocalListAuthorizations" DROP CONSTRAINT IF EXISTS "LocalListAuthorizations_groupAuthorizationId_fkey";',
|
||||
);
|
||||
await queryInterface.sequelize.query(
|
||||
'ALTER TABLE "Components" DROP CONSTRAINT IF EXISTS "Components_evseTypeId_fkey";',
|
||||
);
|
||||
await queryInterface.sequelize.query(
|
||||
'ALTER TABLE "Reservations" DROP CONSTRAINT IF EXISTS "Reservations_evseTypeId_fkey";',
|
||||
);
|
||||
await queryInterface.sequelize.query(
|
||||
'ALTER TABLE "TransactionEvents" DROP CONSTRAINT IF EXISTS "TransactionEvents_evseTypeId_fkey";',
|
||||
);
|
||||
await queryInterface.sequelize.query(
|
||||
'ALTER TABLE "VariableAttributes" DROP CONSTRAINT IF EXISTS "VariableAttributes_evseTypeId_fkey";',
|
||||
);
|
||||
|
||||
// 2. Remove all columns added in up
|
||||
await queryInterface.renameColumn('ChargingNeeds', 'evseId', 'evseDatabaseId');
|
||||
await queryInterface.removeColumn('Evses', 'stationId');
|
||||
await queryInterface.removeColumn('TransactionEvents', 'idTokenValue');
|
||||
await queryInterface.removeColumn('Transactions', 'evseId');
|
||||
await queryInterface.removeColumn('Authorizations', 'tenantPartnerId');
|
||||
await queryInterface.removeColumn('Connectors', 'evseId');
|
||||
await queryInterface.removeColumn('Tariffs', 'connectorId');
|
||||
await queryInterface.removeColumn('Transactions', 'tariffId');
|
||||
await queryInterface.removeColumn('Transactions', 'authorizationId');
|
||||
await queryInterface.removeColumn('Transactions', 'connectorId');
|
||||
await queryInterface.removeColumn('Transactions', 'locationId');
|
||||
await queryInterface.removeColumn('LocalListAuthorizations', 'groupAuthorizationId');
|
||||
await queryInterface.addColumn('LocalListAuthorizations', 'idTokenId', {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
references: {
|
||||
model: 'IdTokens',
|
||||
key: 'id',
|
||||
},
|
||||
onUpdate: 'CASCADE',
|
||||
onDelete: 'SET NULL',
|
||||
});
|
||||
await queryInterface.removeColumn('LocalListAuthorizations', 'idToken');
|
||||
await queryInterface.addColumn('LocalListAuthorizations', 'idTokenType', {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
});
|
||||
await queryInterface.addColumn('LocalListAuthorizations', 'additionalInfo', {
|
||||
type: DataTypes.JSONB,
|
||||
allowNull: true,
|
||||
});
|
||||
await queryInterface.addColumn('LocalListAuthorizations', 'status', {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
defaultValue: 'Accepted',
|
||||
});
|
||||
await queryInterface.addColumn('LocalListAuthorizations', 'cacheExpiryDateTime', {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: true,
|
||||
});
|
||||
await queryInterface.addColumn('LocalListAuthorizations', 'chargingPriority', {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
});
|
||||
await queryInterface.addColumn('LocalListAuthorizations', 'language1', {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
});
|
||||
await queryInterface.addColumn('LocalListAuthorizations', 'language2', {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
});
|
||||
await queryInterface.addColumn('LocalListAuthorizations', 'personalMessage', {
|
||||
type: DataTypes.JSON,
|
||||
allowNull: true,
|
||||
});
|
||||
await queryInterface.removeColumn('LocalListAuthorizations', 'idTokenInfoId');
|
||||
await queryInterface.addColumn('LocalListAuthorizations', 'customData', {
|
||||
type: DataTypes.JSONB,
|
||||
allowNull: true,
|
||||
});
|
||||
await queryInterface.addColumn('Evses', 'evseTypeId', {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
});
|
||||
await queryInterface.addColumn('TransactionEvents', 'idTokenType', {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
});
|
||||
await queryInterface.removeColumn('TransactionEvents', 'idTokenId');
|
||||
await queryInterface.addColumn('Evses', 'evseId', {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
});
|
||||
await queryInterface.addColumn('Evses', 'physicalReference', {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
});
|
||||
await queryInterface.addColumn('Evses', 'removed', {
|
||||
type: DataTypes.BOOLEAN,
|
||||
allowNull: true,
|
||||
});
|
||||
// ChargingStation: Remove columns
|
||||
await queryInterface.removeColumn('ChargingStations', 'coordinates');
|
||||
await queryInterface.removeColumn('ChargingStations', 'floorLevel');
|
||||
await queryInterface.removeColumn('ChargingStations', 'parkingRestrictions');
|
||||
await queryInterface.removeColumn('ChargingStations', 'capabilities');
|
||||
// Tariff: Remove column
|
||||
await queryInterface.removeColumn('Tariffs', 'tariffAltText');
|
||||
|
||||
// 3. Drop EvseTypes table
|
||||
await queryInterface.dropTable('EvseTypes');
|
||||
|
||||
// 4. Restore Evses table PK/index as needed
|
||||
await queryInterface.addColumn('Evses', 'databaseId', {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
});
|
||||
await queryInterface.sequelize.query(
|
||||
'ALTER TABLE "Evses" DROP CONSTRAINT IF EXISTS "Evses_pkey";',
|
||||
);
|
||||
|
||||
// Revert partnerProfileOCPI to partnerProfile in TenantPartners ---
|
||||
const tenantPartnersTable = 'TenantPartners';
|
||||
const oldColumn = 'partnerProfile';
|
||||
const newColumn = 'partnerProfileOCPI';
|
||||
const tenantPartnersDesc = await queryInterface.describeTable(tenantPartnersTable);
|
||||
if (tenantPartnersDesc[newColumn] && !tenantPartnersDesc[oldColumn]) {
|
||||
await queryInterface.renameColumn(tenantPartnersTable, newColumn, oldColumn);
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,68 @@
|
||||
// SPDX-FileCopyrightText: 2025 Contributors to the CitrineOS Project
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
'use strict';
|
||||
|
||||
/** @type {import('sequelize-cli').Migration} */
|
||||
import { QueryInterface } from 'sequelize';
|
||||
|
||||
export default {
|
||||
up: async (queryInterface: QueryInterface) => {
|
||||
// First, let's get all the stations that have EvseTypes associated with them
|
||||
// through VariableAttributes
|
||||
const [stationEvseTypes] = await queryInterface.sequelize.query(`
|
||||
SELECT DISTINCT
|
||||
cs.id as "stationId",
|
||||
cs."tenantId" as "tenantId",
|
||||
et.id as "evseTypeId",
|
||||
ROW_NUMBER() OVER (PARTITION BY cs.id, et.id ORDER BY va.id) as "evseSequence"
|
||||
FROM "ChargingStations" cs
|
||||
INNER JOIN "VariableAttributes" va ON va."stationId" = cs.id
|
||||
INNER JOIN "EvseTypes" et ON va."evseDatabaseId" = et."databaseId"
|
||||
WHERE cs.id IS NOT NULL
|
||||
AND et.id IS NOT NULL
|
||||
ORDER BY cs.id, et.id
|
||||
`);
|
||||
|
||||
// Now create Evse records for each station-evseType combination
|
||||
let id = 1;
|
||||
const evseInserts = stationEvseTypes.map((row: any, index: number) => {
|
||||
// Generate evseId in the format US*TST*C*01234567*8
|
||||
// Using the station's stationId and a sequence number
|
||||
const paddedSequence = row.evseSequence.toString().padStart(8, '0');
|
||||
const evseId = `US*TST*C*${paddedSequence}*${index % 10}`;
|
||||
|
||||
const evse = {
|
||||
id,
|
||||
stationId: row.stationId,
|
||||
evseTypeId: row.evseTypeId,
|
||||
evseId: evseId,
|
||||
tenantId: row.tenantId,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
id++;
|
||||
return evse;
|
||||
});
|
||||
|
||||
// Bulk insert the new Evse records
|
||||
if (evseInserts.length > 0) {
|
||||
await queryInterface.bulkInsert('Evses', evseInserts);
|
||||
|
||||
console.log(`Created ${evseInserts.length} Evse records from EvseType associations`);
|
||||
} else {
|
||||
console.log('No EvseType associations found to migrate');
|
||||
}
|
||||
},
|
||||
|
||||
down: async (queryInterface: QueryInterface) => {
|
||||
// Remove all Evse records that were created by this migration
|
||||
// We'll identify them by the fact that they have evseIds matching our pattern
|
||||
await queryInterface.sequelize.query(`
|
||||
DELETE FROM "Evses"
|
||||
WHERE "evseId" LIKE 'US*TST*C*%'
|
||||
`);
|
||||
|
||||
console.log('Rolled back Evse creation migration');
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,157 @@
|
||||
// SPDX-FileCopyrightText: 2025 Contributors to the CitrineOS Project
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
'use strict';
|
||||
|
||||
/** @type {import('sequelize-cli').Migration} */
|
||||
import { DataTypes, QueryInterface } from 'sequelize';
|
||||
|
||||
export default {
|
||||
up: async (queryInterface: QueryInterface) => {
|
||||
await queryInterface.changeColumn('InstalledCertificates', 'certificateType', {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
});
|
||||
await queryInterface.sequelize.query(`
|
||||
DROP TYPE "enum_InstalledCertificates_certificateType";
|
||||
`);
|
||||
|
||||
await queryInterface.changeColumn('Connectors', 'status', {
|
||||
type: DataTypes.STRING,
|
||||
});
|
||||
await queryInterface.sequelize.query(`
|
||||
DROP TYPE "enum_Connectors_status";
|
||||
`);
|
||||
// Default value references enum type, since default value is changed before column type, so we had to change the column to STRING first.
|
||||
await queryInterface.changeColumn('Connectors', 'status', {
|
||||
type: DataTypes.STRING,
|
||||
defaultValue: 'Unknown',
|
||||
});
|
||||
|
||||
await queryInterface.changeColumn('Connectors', 'errorCode', {
|
||||
type: DataTypes.STRING,
|
||||
});
|
||||
await queryInterface.sequelize.query(`
|
||||
DROP TYPE "enum_Connectors_errorCode";
|
||||
`);
|
||||
// Default value references enum type, since default value is changed before column type, so we had to change the column to STRING first.
|
||||
await queryInterface.changeColumn('Connectors', 'errorCode', {
|
||||
type: DataTypes.STRING,
|
||||
defaultValue: 'NoError',
|
||||
});
|
||||
},
|
||||
|
||||
down: async (queryInterface: QueryInterface) => {
|
||||
await queryInterface.sequelize.query(`
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_type WHERE typname = 'enum_InstalledCertificates_certificateType'
|
||||
) THEN
|
||||
CREATE TYPE "enum_InstalledCertificates_certificateType" AS ENUM (
|
||||
'V2GRootCertificate',
|
||||
'MORootCertificate',
|
||||
'CSMSRootCertificate',
|
||||
'V2GCertificateChain',
|
||||
'ManufacturerRootCertificate'
|
||||
);
|
||||
END IF;
|
||||
END$$;
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_type WHERE typname = 'enum_Connectors_status'
|
||||
) THEN
|
||||
CREATE TYPE "enum_Connectors_status" AS ENUM (
|
||||
'Available',
|
||||
'Preparing',
|
||||
'Charging',
|
||||
'SuspendedEVSE',
|
||||
'SuspendedEV',
|
||||
'Finishing',
|
||||
'Reserved',
|
||||
'Unavailable',
|
||||
'Faulted'
|
||||
);
|
||||
END IF;
|
||||
END$$;
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_type WHERE typname = 'enum_Connectors_errorCode'
|
||||
) THEN
|
||||
CREATE TYPE "enum_Connectors_errorCode" AS ENUM (
|
||||
'ConnectorLockFailure',
|
||||
'EVCommunicationError',
|
||||
'GroundFailure',
|
||||
'HighTemperature',
|
||||
'InternalError',
|
||||
'LocalListConflict',
|
||||
'NoError',
|
||||
'OtherError',
|
||||
'OverCurrentFailure',
|
||||
'PowerMeterFailure',
|
||||
'PowerSwitchFailure',
|
||||
'ReaderFailure',
|
||||
'ResetFailure',
|
||||
'UnderVoltage',
|
||||
'OverVoltage',
|
||||
'WeakSignal'
|
||||
);
|
||||
END IF;
|
||||
END$$;
|
||||
`);
|
||||
|
||||
await queryInterface.changeColumn('InstalledCertificates', 'certificateType', {
|
||||
type: DataTypes.ENUM(
|
||||
'V2GRootCertificate',
|
||||
'MORootCertificate',
|
||||
'CSMSRootCertificate',
|
||||
'V2GCertificateChain',
|
||||
'ManufacturerRootCertificate',
|
||||
),
|
||||
allowNull: false,
|
||||
});
|
||||
|
||||
await queryInterface.changeColumn('Connectors', 'status', {
|
||||
type: DataTypes.ENUM(
|
||||
'Available',
|
||||
'Preparing',
|
||||
'Charging',
|
||||
'SuspendedEVSE',
|
||||
'SuspendedEV',
|
||||
'Finishing',
|
||||
'Reserved',
|
||||
'Unavailable',
|
||||
'Faulted',
|
||||
),
|
||||
allowNull: false,
|
||||
defaultValue: 'Unknown',
|
||||
});
|
||||
|
||||
await queryInterface.changeColumn('Connectors', 'errorCode', {
|
||||
type: DataTypes.ENUM(
|
||||
'ConnectorLockFailure',
|
||||
'EVCommunicationError',
|
||||
'GroundFailure',
|
||||
'HighTemperature',
|
||||
'InternalError',
|
||||
'LocalListConflict',
|
||||
'NoError',
|
||||
'OtherError',
|
||||
'OverCurrentFailure',
|
||||
'PowerMeterFailure',
|
||||
'PowerSwitchFailure',
|
||||
'ReaderFailure',
|
||||
'ResetFailure',
|
||||
'UnderVoltage',
|
||||
'OverVoltage',
|
||||
'WeakSignal',
|
||||
),
|
||||
allowNull: false,
|
||||
defaultValue: 'NoError',
|
||||
});
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,54 @@
|
||||
// SPDX-FileCopyrightText: 2025 Contributors to the CitrineOS Project
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
'use strict';
|
||||
|
||||
/** @type {import('sequelize-cli').Migration} */
|
||||
import { DataTypes, QueryInterface } from 'sequelize';
|
||||
|
||||
export default {
|
||||
up: async (queryInterface: QueryInterface) => {
|
||||
// Add isUserTenant column
|
||||
await queryInterface.addColumn('Tenants', 'isUserTenant', {
|
||||
type: DataTypes.BOOLEAN,
|
||||
allowNull: false,
|
||||
defaultValue: false,
|
||||
comment: 'Indicates if this tenant is a user tenant',
|
||||
});
|
||||
|
||||
// Make url, partyId, countryCode optional
|
||||
await queryInterface.changeColumn('Tenants', 'url', {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
});
|
||||
|
||||
await queryInterface.changeColumn('Tenants', 'partyId', {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
});
|
||||
|
||||
await queryInterface.changeColumn('Tenants', 'countryCode', {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
});
|
||||
},
|
||||
|
||||
down: async (queryInterface: QueryInterface) => {
|
||||
await queryInterface.removeColumn('Tenants', 'isUserTenant');
|
||||
|
||||
await queryInterface.changeColumn('Tenants', 'url', {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
});
|
||||
|
||||
await queryInterface.changeColumn('Tenants', 'partyId', {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
});
|
||||
|
||||
await queryInterface.changeColumn('Tenants', 'countryCode', {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
});
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,25 @@
|
||||
// SPDX-FileCopyrightText: 2025 Contributors to the CitrineOS Project
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
'use strict';
|
||||
|
||||
/** @type {import('sequelize-cli').Migration} */
|
||||
import { DataTypes, QueryInterface } from 'sequelize';
|
||||
|
||||
export default {
|
||||
up: async (queryInterface: QueryInterface) => {
|
||||
await queryInterface.addColumn('Authorizations', 'realTimeAuthLastAttempt', {
|
||||
type: DataTypes.JSONB,
|
||||
allowNull: true,
|
||||
});
|
||||
await queryInterface.addColumn('Authorizations', 'realTimeAuthTimeout', {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
});
|
||||
},
|
||||
|
||||
down: async (queryInterface: QueryInterface) => {
|
||||
await queryInterface.removeColumn('Authorizations', 'realTimeAuthLastAttempt');
|
||||
await queryInterface.removeColumn('Authorizations', 'realTimeAuthTimeout');
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
// SPDX-FileCopyrightText: 2025 Contributors to the CitrineOS Project
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
'use strict';
|
||||
|
||||
/** @type {import('sequelize-cli').Migration} */
|
||||
import { DataTypes, QueryInterface } from 'sequelize';
|
||||
|
||||
export default {
|
||||
up: async (queryInterface: QueryInterface) => {
|
||||
console.log('Creating citext extension if not exists...');
|
||||
await queryInterface.sequelize.query('CREATE EXTENSION IF NOT EXISTS citext;');
|
||||
|
||||
console.log('Changing column to use CITEXT type...');
|
||||
await queryInterface.changeColumn('Authorizations', 'idToken', {
|
||||
type: DataTypes.CITEXT,
|
||||
allowNull: false,
|
||||
});
|
||||
},
|
||||
|
||||
down: async (queryInterface: QueryInterface) => {
|
||||
await queryInterface.changeColumn('Authorizations', 'idToken', {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
});
|
||||
// Note: Not dropping the extension in case other tables use it
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,73 @@
|
||||
// SPDX-FileCopyrightText: 2025 Contributors to the CitrineOS Project
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
'use strict';
|
||||
|
||||
/** @type {import('sequelize-cli').Migration} */
|
||||
import { QueryInterface } from 'sequelize';
|
||||
|
||||
export default {
|
||||
up: async (queryInterface: QueryInterface) => {
|
||||
// First check if the index already exists
|
||||
console.log('Checking if index idToken_type already exists...');
|
||||
const [indexes] = await queryInterface.sequelize.query(`
|
||||
SELECT indexname
|
||||
FROM pg_indexes
|
||||
WHERE tablename = 'Authorizations'
|
||||
AND indexname = 'idToken_type'
|
||||
`);
|
||||
|
||||
if (indexes.length > 0) {
|
||||
console.log('Index idToken_type already exists, skipping creation');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Checking for duplicate idToken/idTokenType combinations...');
|
||||
|
||||
// Check for duplicates before creating the index
|
||||
const [duplicates] = await queryInterface.sequelize.query(`
|
||||
SELECT "idToken", "idTokenType", COUNT(*) as count
|
||||
FROM "Authorizations"
|
||||
WHERE "idToken" IS NOT NULL
|
||||
GROUP BY "idToken", "idTokenType"
|
||||
HAVING COUNT(*) > 1
|
||||
ORDER BY "idToken", "idTokenType"
|
||||
`);
|
||||
|
||||
if (duplicates.length > 0) {
|
||||
console.error('Cannot create unique index due to duplicate data:');
|
||||
duplicates.forEach((dup: any) => {
|
||||
console.error(
|
||||
` - idToken: "${dup.idToken}", idTokenType: "${dup.idTokenType}", count: ${dup.count}`,
|
||||
);
|
||||
});
|
||||
throw new Error(
|
||||
`Migration failed: Found ${duplicates.length} duplicate idToken/idTokenType combinations. ` +
|
||||
'Please resolve these duplicates before running this migration. ' +
|
||||
'You may need t`o` update or remove duplicate records in the Authorizations table.',
|
||||
);
|
||||
}
|
||||
|
||||
console.log('No duplicates found. Proceeding with index creation...');
|
||||
|
||||
await queryInterface.addIndex('Authorizations', ['idToken', 'idTokenType'], {
|
||||
unique: true,
|
||||
name: 'idToken_type',
|
||||
});
|
||||
console.log('Successfully created unique index: idToken_type');
|
||||
},
|
||||
|
||||
down: async (queryInterface: QueryInterface) => {
|
||||
console.log('Removing unique index on idToken and idTokenType columns...');
|
||||
try {
|
||||
await queryInterface.removeIndex('Authorizations', 'idToken_type');
|
||||
console.log('Successfully removed unique index: idToken_type');
|
||||
} catch (error: any) {
|
||||
if (error.message?.includes('does not exist')) {
|
||||
console.log('Index idToken_type does not exist, skipping removal');
|
||||
return;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
// SPDX-FileCopyrightText: 2025 Contributors to the CitrineOS Project
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
'use strict';
|
||||
|
||||
/** @type {import('sequelize-cli').Migration} */
|
||||
import { DataTypes, QueryInterface } from 'sequelize';
|
||||
|
||||
export default {
|
||||
up: async (queryInterface: QueryInterface) => {
|
||||
await queryInterface.addColumn('ChargingStations', 'use16StatusNotification0', {
|
||||
type: DataTypes.BOOLEAN,
|
||||
allowNull: false,
|
||||
defaultValue: true,
|
||||
});
|
||||
},
|
||||
|
||||
down: async (queryInterface: QueryInterface) => {
|
||||
await queryInterface.removeColumn('ChargingStations', 'use16StatusNotification0');
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
// SPDX-FileCopyrightText: 2025 Contributors to the CitrineOS Project
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
'use strict';
|
||||
|
||||
/** @type {import('sequelize-cli').Migration} */
|
||||
import { DataTypes, QueryInterface } from 'sequelize';
|
||||
|
||||
export default {
|
||||
up: async (queryInterface: QueryInterface) => {
|
||||
await queryInterface.addColumn('Transactions', 'meterStart', {
|
||||
type: DataTypes.DECIMAL,
|
||||
allowNull: true,
|
||||
});
|
||||
},
|
||||
|
||||
down: async (queryInterface: QueryInterface) => {
|
||||
await queryInterface.removeColumn('Transactions', 'meterStart');
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
// SPDX-FileCopyrightText: 2026 Contributors to the CitrineOS Project
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { QueryInterface, DataTypes } from 'sequelize';
|
||||
|
||||
/**
|
||||
* Migration to add latestOcppMessageTimestamp.
|
||||
*/
|
||||
export async function up(queryInterface: QueryInterface): Promise<void> {
|
||||
// Add new latestOcppMessageTimestamp column
|
||||
await queryInterface.addColumn('ChargingStations', 'latestOcppMessageTimestamp', {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: true,
|
||||
});
|
||||
|
||||
// Add index on latestOcppMessageTimestamp for efficient staleness queries
|
||||
await queryInterface.addIndex('ChargingStations', ['latestOcppMessageTimestamp'], {
|
||||
name: 'idx_charging_stations_latest_ocpp_message_timestamp',
|
||||
});
|
||||
}
|
||||
|
||||
export async function down(queryInterface: QueryInterface): Promise<void> {
|
||||
// Remove index
|
||||
await queryInterface.removeIndex(
|
||||
'ChargingStations',
|
||||
'idx_charging_stations_latest_ocpp_message_timestamp',
|
||||
);
|
||||
|
||||
// Remove latestOcppMessageTimestamp column
|
||||
await queryInterface.removeColumn('ChargingStations', 'latestOcppMessageTimestamp');
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
// SPDX-FileCopyrightText: 2026 Contributors to the CitrineOS Project
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { DataTypes, QueryInterface } from 'sequelize';
|
||||
|
||||
/**
|
||||
* Migration to add state and requestMessageId self-referencing foreign key to OCPPMessages table.
|
||||
* This enables linking response messages to their corresponding request messages.
|
||||
*/
|
||||
export async function up(queryInterface: QueryInterface): Promise<void> {
|
||||
// Add new state column
|
||||
await queryInterface.addColumn('OCPPMessages', 'state', {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
});
|
||||
|
||||
// Add new requestMessageId column
|
||||
await queryInterface.addColumn('OCPPMessages', 'requestMessageId', {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
references: {
|
||||
model: 'OCPPMessages',
|
||||
key: 'id',
|
||||
},
|
||||
onUpdate: 'CASCADE',
|
||||
onDelete: 'SET NULL',
|
||||
});
|
||||
|
||||
// Add index on requestMessageId for efficient queries
|
||||
await queryInterface.addIndex('OCPPMessages', ['requestMessageId'], {
|
||||
name: 'idx_ocpp_messages_request_message_id',
|
||||
});
|
||||
}
|
||||
|
||||
export async function down(queryInterface: QueryInterface): Promise<void> {
|
||||
// Remove index
|
||||
await queryInterface.removeIndex('OCPPMessages', 'idx_ocpp_messages_request_message_id');
|
||||
|
||||
// Remove requestMessageId column
|
||||
await queryInterface.removeColumn('OCPPMessages', 'requestMessageId');
|
||||
|
||||
// Remove state column
|
||||
await queryInterface.removeColumn('OCPPMessages', 'state');
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
// SPDX-FileCopyrightText: 2026 Contributors to the CitrineOS Project
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
'use strict';
|
||||
|
||||
/** @type {import('sequelize-cli').Migration} */
|
||||
import { DataTypes, QueryInterface } from 'sequelize';
|
||||
|
||||
export default {
|
||||
up: async (queryInterface: QueryInterface) => {
|
||||
// Check if columns already exist
|
||||
const table = await queryInterface.describeTable('ServerNetworkProfiles');
|
||||
|
||||
// Add dynamicTenantResolution column if it doesn't exist
|
||||
if (!table.dynamicTenantResolution) {
|
||||
await queryInterface.addColumn('ServerNetworkProfiles', 'dynamicTenantResolution', {
|
||||
type: DataTypes.BOOLEAN,
|
||||
allowNull: false,
|
||||
defaultValue: false,
|
||||
comment: 'Enable dynamic tenant resolution at WebSocket upgrade time',
|
||||
});
|
||||
}
|
||||
|
||||
// Add maxConnectionsPerTenant column if it doesn't exist
|
||||
if (!table.maxConnectionsPerTenant) {
|
||||
await queryInterface.addColumn('ServerNetworkProfiles', 'maxConnectionsPerTenant', {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
comment: 'Maximum number of concurrent connections allowed per tenant',
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
down: async (queryInterface: QueryInterface) => {
|
||||
// Rollback: remove the columns
|
||||
await queryInterface.removeColumn('ServerNetworkProfiles', 'dynamicTenantResolution');
|
||||
await queryInterface.removeColumn('ServerNetworkProfiles', 'maxConnectionsPerTenant');
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,15 @@
|
||||
// SPDX-FileCopyrightText: 2025 Contributors to the CitrineOS Project
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
import { DataTypes, QueryInterface } from 'sequelize';
|
||||
|
||||
export async function up(queryInterface: QueryInterface): Promise<void> {
|
||||
await queryInterface.addColumn('Certificates', 'certificateFileHash', {
|
||||
type: DataTypes.STRING,
|
||||
unique: true,
|
||||
});
|
||||
}
|
||||
|
||||
export async function down(queryInterface: QueryInterface): Promise<void> {
|
||||
await queryInterface.removeColumn('Certificates', 'certificateFileHash');
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
// SPDX-FileCopyrightText: 2025 Contributors to the CitrineOS Project
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
import { DataTypes, QueryInterface } from 'sequelize';
|
||||
|
||||
export async function up(queryInterface: QueryInterface): Promise<void> {
|
||||
await queryInterface.addColumn('InstalledCertificates', 'certificateId', {
|
||||
type: DataTypes.INTEGER,
|
||||
references: {
|
||||
model: 'Certificates',
|
||||
key: 'id',
|
||||
},
|
||||
onUpdate: 'CASCADE',
|
||||
onDelete: 'CASCADE',
|
||||
});
|
||||
}
|
||||
|
||||
export async function down(queryInterface: QueryInterface): Promise<void> {
|
||||
await queryInterface.removeColumn('InstalledCertificates', 'certificateId');
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
// SPDX-FileCopyrightText: 2025 Contributors to the CitrineOS Project
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
import { DataTypes, QueryInterface } from 'sequelize';
|
||||
|
||||
export async function up(queryInterface: QueryInterface): Promise<void> {
|
||||
await queryInterface.createTable('InstallCertificateAttempts', {
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true,
|
||||
allowNull: false,
|
||||
},
|
||||
stationId: {
|
||||
type: DataTypes.STRING(36),
|
||||
allowNull: false,
|
||||
references: {
|
||||
model: 'ChargingStations',
|
||||
key: 'id',
|
||||
},
|
||||
},
|
||||
certificateType: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
},
|
||||
certificateId: {
|
||||
type: DataTypes.INTEGER,
|
||||
references: {
|
||||
model: 'Certificates',
|
||||
key: 'id',
|
||||
},
|
||||
onUpdate: 'CASCADE',
|
||||
onDelete: 'CASCADE',
|
||||
},
|
||||
status: {
|
||||
type: DataTypes.STRING,
|
||||
},
|
||||
tenantId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
references: {
|
||||
model: 'Tenants',
|
||||
key: 'id',
|
||||
},
|
||||
onUpdate: 'CASCADE',
|
||||
onDelete: 'RESTRICT',
|
||||
},
|
||||
createdAt: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false,
|
||||
},
|
||||
updatedAt: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false,
|
||||
},
|
||||
});
|
||||
|
||||
await queryInterface.createTable('DeleteCertificateAttempts', {
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true,
|
||||
allowNull: false,
|
||||
},
|
||||
stationId: {
|
||||
type: DataTypes.STRING(36),
|
||||
allowNull: false,
|
||||
references: {
|
||||
model: 'ChargingStations',
|
||||
key: 'id',
|
||||
},
|
||||
},
|
||||
hashAlgorithm: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
},
|
||||
issuerNameHash: {
|
||||
type: DataTypes.STRING,
|
||||
},
|
||||
issuerKeyHash: {
|
||||
type: DataTypes.STRING,
|
||||
},
|
||||
serialNumber: {
|
||||
type: DataTypes.STRING,
|
||||
},
|
||||
status: {
|
||||
type: DataTypes.STRING,
|
||||
},
|
||||
tenantId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
references: {
|
||||
model: 'Tenants',
|
||||
key: 'id',
|
||||
},
|
||||
onUpdate: 'CASCADE',
|
||||
onDelete: 'RESTRICT',
|
||||
},
|
||||
createdAt: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false,
|
||||
},
|
||||
updatedAt: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function down(queryInterface: QueryInterface): Promise<void> {
|
||||
await queryInterface.dropTable('DeleteCertificateAttempts');
|
||||
await queryInterface.dropTable('InstallCertificateAttempts');
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
// SPDX-FileCopyrightText: 2026 Contributors to the CitrineOS Project
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
'use strict';
|
||||
|
||||
/** @type {import('sequelize-cli').Migration} */
|
||||
import { DataTypes, QueryInterface } from 'sequelize';
|
||||
|
||||
export default {
|
||||
up: async (queryInterface: QueryInterface) => {
|
||||
// Check if columns already exist
|
||||
const table = await queryInterface.describeTable('ServerNetworkProfiles');
|
||||
|
||||
// Add tenantPathMapping column if it doesn't exist
|
||||
if (!table.tenantPathMapping) {
|
||||
await queryInterface.addColumn('ServerNetworkProfiles', 'tenantPathMapping', {
|
||||
type: DataTypes.JSONB,
|
||||
allowNull: true,
|
||||
comment: 'Mapping of URL path segments to tenant IDs',
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
down: async (queryInterface: QueryInterface) => {
|
||||
// Rollback: remove the column
|
||||
await queryInterface.removeColumn('ServerNetworkProfiles', 'tenantPathMapping');
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,47 @@
|
||||
// SPDX-FileCopyrightText: 2026 Contributors to the CitrineOS Project
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
'use strict';
|
||||
|
||||
/** @type {import('sequelize-cli').Migration} */
|
||||
import { DataTypes, QueryInterface } from 'sequelize';
|
||||
|
||||
export default {
|
||||
up: async (queryInterface: QueryInterface) => {
|
||||
// Change evseId to allow null
|
||||
await queryInterface.changeColumn('Connectors', 'evseId', {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
});
|
||||
|
||||
// Change connectorId to allow null
|
||||
await queryInterface.changeColumn('Connectors', 'connectorId', {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
});
|
||||
|
||||
// Change evseTypeConnectorId to allow null
|
||||
await queryInterface.changeColumn('Connectors', 'evseTypeConnectorId', {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
});
|
||||
},
|
||||
|
||||
down: async (queryInterface: QueryInterface) => {
|
||||
// Rollback: change columns back to not allow null
|
||||
await queryInterface.changeColumn('Connectors', 'evseId', {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
});
|
||||
|
||||
await queryInterface.changeColumn('Connectors', 'connectorId', {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
});
|
||||
|
||||
await queryInterface.changeColumn('Connectors', 'evseTypeConnectorId', {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
});
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,54 @@
|
||||
// SPDX-FileCopyrightText: 2026 Contributors to the CitrineOS Project
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
'use strict';
|
||||
|
||||
/** @type {import('sequelize-cli').Migration} */
|
||||
import { DataTypes, QueryInterface } from 'sequelize';
|
||||
|
||||
export default {
|
||||
up: async (queryInterface: QueryInterface) => {
|
||||
// Add tariffId FK to Connectors so each connector can reference at most one reusable tariff
|
||||
await queryInterface.addColumn('Connectors', 'tariffId', {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
references: { model: 'Tariffs', key: 'id' },
|
||||
onUpdate: 'CASCADE',
|
||||
onDelete: 'SET NULL',
|
||||
});
|
||||
|
||||
// Migrate existing data: for Tariffs that had a connectorId set, populate
|
||||
// Connectors.tariffId with that tariff's id before dropping the old columns.
|
||||
await queryInterface.sequelize.query(`
|
||||
UPDATE "Connectors" c
|
||||
SET "tariffId" = t.id
|
||||
FROM "Tariffs" t
|
||||
WHERE t."connectorId" = c.id;
|
||||
`);
|
||||
|
||||
// Drop the old FK constraint and station/connector columns from Tariffs
|
||||
await queryInterface.sequelize.query(
|
||||
'ALTER TABLE "Tariffs" DROP CONSTRAINT IF EXISTS "Tariffs_connectorId_fkey";',
|
||||
);
|
||||
await queryInterface.removeColumn('Tariffs', 'connectorId');
|
||||
await queryInterface.removeColumn('Tariffs', 'stationId');
|
||||
},
|
||||
|
||||
down: async (queryInterface: QueryInterface) => {
|
||||
// Restore stationId and connectorId columns on Tariffs
|
||||
await queryInterface.addColumn('Tariffs', 'stationId', {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
});
|
||||
await queryInterface.addColumn('Tariffs', 'connectorId', {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
});
|
||||
await queryInterface.sequelize.query(
|
||||
'ALTER TABLE "Tariffs" ADD CONSTRAINT "Tariffs_connectorId_fkey" FOREIGN KEY ("connectorId") REFERENCES "Connectors" (id) ON UPDATE CASCADE ON DELETE SET NULL;',
|
||||
);
|
||||
|
||||
// Remove tariffId from Connectors
|
||||
await queryInterface.removeColumn('Connectors', 'tariffId');
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
// SPDX-FileCopyrightText: 2026 Contributors to the CitrineOS Project
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
'use strict';
|
||||
|
||||
/** @type {import('sequelize-cli').Migration} */
|
||||
import { DataTypes, QueryInterface } from 'sequelize';
|
||||
|
||||
export default {
|
||||
up: async (queryInterface: QueryInterface) => {
|
||||
// Check if columns already exist
|
||||
const table = await queryInterface.describeTable('Tenants');
|
||||
|
||||
// Add maxChargingStations column if it doesn't exist
|
||||
if (!table.maxChargingStations) {
|
||||
await queryInterface.addColumn('Tenants', 'maxChargingStations', {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
comment: 'Maximum number of charging stations allowed for this tenant',
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
down: async (queryInterface: QueryInterface) => {
|
||||
// Rollback: remove the column
|
||||
await queryInterface.removeColumn('Tenants', 'maxChargingStations');
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,54 @@
|
||||
// SPDX-FileCopyrightText: 2026 Contributors to the CitrineOS Project
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
'use strict';
|
||||
|
||||
/** @type {import('sequelize-cli').Migration} */
|
||||
import { DataTypes, QueryInterface } from 'sequelize';
|
||||
|
||||
export default {
|
||||
up: async (queryInterface: QueryInterface) => {
|
||||
const table = await queryInterface.describeTable('ServerNetworkProfiles');
|
||||
|
||||
if (!table.protocols) {
|
||||
await queryInterface.addColumn('ServerNetworkProfiles', 'protocols', {
|
||||
type: DataTypes.ARRAY(DataTypes.STRING),
|
||||
allowNull: true,
|
||||
});
|
||||
}
|
||||
|
||||
// Migrate existing data: wrap the string value of 'protocol' into a single-element array
|
||||
if (table.protocol) {
|
||||
await queryInterface.sequelize.query(`
|
||||
UPDATE "ServerNetworkProfiles"
|
||||
SET "protocols" = ARRAY["protocol"]
|
||||
WHERE "protocol" IS NOT NULL;
|
||||
`);
|
||||
|
||||
await queryInterface.removeColumn('ServerNetworkProfiles', 'protocol');
|
||||
}
|
||||
},
|
||||
|
||||
down: async (queryInterface: QueryInterface) => {
|
||||
const table = await queryInterface.describeTable('ServerNetworkProfiles');
|
||||
|
||||
if (!table.protocol) {
|
||||
await queryInterface.addColumn('ServerNetworkProfiles', 'protocol', {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
});
|
||||
}
|
||||
|
||||
// Migrate back: take the first element of the 'protocols' array
|
||||
if (table.protocols) {
|
||||
await queryInterface.sequelize.query(`
|
||||
UPDATE "ServerNetworkProfiles"
|
||||
SET "protocol" = "protocols"[1]
|
||||
WHERE "protocols" IS NOT NULL
|
||||
AND array_length("protocols", 1) > 0;
|
||||
`);
|
||||
|
||||
await queryInterface.removeColumn('ServerNetworkProfiles', 'protocols');
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
// SPDX-FileCopyrightText: 2026 Contributors to the CitrineOS Project
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
'use strict';
|
||||
|
||||
/** @type {import('sequelize-cli').Migration} */
|
||||
import { QueryInterface } from 'sequelize';
|
||||
|
||||
export default {
|
||||
up: async (queryInterface: QueryInterface) => {
|
||||
const table = await queryInterface.describeTable('ServerNetworkProfiles');
|
||||
if (table.maxConnectionsPerTenant) {
|
||||
await queryInterface.removeColumn('ServerNetworkProfiles', 'maxConnectionsPerTenant');
|
||||
}
|
||||
},
|
||||
|
||||
down: async (_queryInterface: QueryInterface) => {
|
||||
// Column is intentionally not restored; per-tenant limits are now driven by Tenant.maxChargingStations
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,29 @@
|
||||
// SPDX-FileCopyrightText: 2026 Contributors to the CitrineOS Project
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
'use strict';
|
||||
|
||||
/** @type {import('sequelize-cli').Migration} */
|
||||
import { DataTypes, QueryInterface } from 'sequelize';
|
||||
|
||||
export default {
|
||||
up: async (queryInterface: QueryInterface) => {
|
||||
// Change tenantId to allow null in ServerNetworkProfiles
|
||||
await queryInterface.changeColumn('ServerNetworkProfiles', 'tenantId', {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
onUpdate: 'CASCADE',
|
||||
onDelete: 'RESTRICT',
|
||||
});
|
||||
},
|
||||
|
||||
down: async (queryInterface: QueryInterface) => {
|
||||
// Rollback: change tenantId back to not allow null
|
||||
await queryInterface.changeColumn('ServerNetworkProfiles', 'tenantId', {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
onUpdate: 'CASCADE',
|
||||
onDelete: 'RESTRICT',
|
||||
});
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,15 @@
|
||||
// SPDX-FileCopyrightText: 2025 Contributors to the CitrineOS Project
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
import { DataTypes, QueryInterface } from 'sequelize';
|
||||
|
||||
export async function up(queryInterface: QueryInterface): Promise<void> {
|
||||
await queryInterface.addColumn('VariableMonitorings', 'eventNotificationType', {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
});
|
||||
}
|
||||
|
||||
export async function down(queryInterface: QueryInterface): Promise<void> {
|
||||
await queryInterface.removeColumn('VariableMonitorings', 'eventNotificationType');
|
||||
}
|
||||
@@ -0,0 +1,953 @@
|
||||
// SPDX-FileCopyrightText: 2025 Contributors to the CitrineOS Project
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
'use strict';
|
||||
|
||||
import { DataTypes, type ModelAttributeColumnOptions, QueryInterface, QueryTypes } from 'sequelize';
|
||||
|
||||
export default {
|
||||
up: async (queryInterface: QueryInterface) => {
|
||||
// ── Everything below runs inside a single PostgreSQL transaction. ───────────
|
||||
// PostgreSQL treats DDL as transactional, so any failure causes a full
|
||||
// rollback — no partial state, no data loss.
|
||||
await queryInterface.sequelize.transaction(async (transaction) => {
|
||||
// ── Helpers ──────────────────────────────────────────────────────────────
|
||||
|
||||
/** Fire-and-forget raw SQL inside the transaction. */
|
||||
const q = (sql: string, replacements?: Record<string, any>) =>
|
||||
queryInterface.sequelize.query(sql, {
|
||||
transaction,
|
||||
replacements,
|
||||
type: QueryTypes.RAW,
|
||||
});
|
||||
|
||||
/** SELECT inside the transaction; returns the row array directly. */
|
||||
const qSelect = <T = any>(sql: string, replacements?: Record<string, any>): Promise<T[]> =>
|
||||
queryInterface.sequelize.query(sql, {
|
||||
transaction,
|
||||
replacements,
|
||||
type: QueryTypes.SELECT,
|
||||
}) as Promise<T[]>;
|
||||
|
||||
const addCol = (table: string, col: string, def: ModelAttributeColumnOptions) =>
|
||||
queryInterface.addColumn(table, col, def, { transaction } as any);
|
||||
|
||||
/** Check which columns exist — uses the transaction connection to avoid
|
||||
* deadlocking on a single-connection pool. */
|
||||
const describeTable = async (table: string): Promise<Record<string, any>> => {
|
||||
const rows = await qSelect<{ Field: string }>(
|
||||
`SELECT column_name AS "Field"
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = 'public'
|
||||
AND table_name = :table`,
|
||||
{ table },
|
||||
);
|
||||
const desc: Record<string, any> = {};
|
||||
for (const row of rows) {
|
||||
desc[(row as any).Field] = true;
|
||||
}
|
||||
return desc;
|
||||
};
|
||||
|
||||
const dropConstraintIfExists = (table: string, name: string) =>
|
||||
q(`ALTER TABLE "${table}" DROP CONSTRAINT IF EXISTS "${name}"`);
|
||||
|
||||
const dropIndexIfExists = (name: string) => q(`DROP INDEX IF EXISTS "${name}"`);
|
||||
|
||||
/**
|
||||
* Drop every FK on `table` that uses `column`, by inspecting
|
||||
* information_schema so that we handle any constraint name.
|
||||
*/
|
||||
const dropFkByColumn = async (table: string, column: string) => {
|
||||
const rows = await qSelect<{ constraint_name: string }>(
|
||||
`SELECT tc.constraint_name
|
||||
FROM information_schema.table_constraints tc
|
||||
JOIN information_schema.key_column_usage kcu
|
||||
ON tc.constraint_name = kcu.constraint_name
|
||||
AND tc.table_schema = kcu.table_schema
|
||||
WHERE tc.table_schema = 'public'
|
||||
AND tc.table_name = :table
|
||||
AND tc.constraint_type = 'FOREIGN KEY'
|
||||
AND kcu.column_name = :column`,
|
||||
{ table, column },
|
||||
);
|
||||
for (const row of rows) {
|
||||
await q(
|
||||
`ALTER TABLE "${table}" DROP CONSTRAINT IF EXISTS "${(row as any).constraint_name}"`,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* After UPDATE … SET stationPkId = cs.pkId, verify that every row with a
|
||||
* non-NULL stationId was matched. A mismatch means the database already
|
||||
* had a referential-integrity problem; we refuse to proceed rather than
|
||||
* silently lose the relationship.
|
||||
*/
|
||||
const validateNoOrphans = async (table: string) => {
|
||||
const [row] = await qSelect<{ cnt: string }>(
|
||||
`SELECT COUNT(*) AS cnt
|
||||
FROM "${table}"
|
||||
WHERE "stationId" IS NOT NULL
|
||||
AND "stationPkId" IS NULL`,
|
||||
);
|
||||
const cnt = parseInt((row as any).cnt, 10);
|
||||
if (cnt > 0) {
|
||||
throw new Error(
|
||||
`Data integrity error: ${cnt} row(s) in "${table}" have a non-NULL ` +
|
||||
`stationId that could not be matched to any ChargingStation. ` +
|
||||
`Resolve these orphaned rows before running this migration.`,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// ── Step 1: Drop ALL FK constraints on child tables ───────────────────────
|
||||
// Covers both the original stationId_fkey constraints AND any
|
||||
// stationPkId_fkey constraints left behind by a previous partial run.
|
||||
const allChildFks: [string, string][] = [
|
||||
// ── original stationId_fkey constraints ──────────────────────────────
|
||||
['Transactions', 'Transactions_stationId_fkey'],
|
||||
['ChargingStationNetworkProfiles', 'ChargingStationNetworkProfiles_stationId_fkey'],
|
||||
['ChargingStationSequences', 'ChargingStationSequences_stationId_fkey'],
|
||||
['Connectors', 'Connectors_stationId_fkey'],
|
||||
['Evses', 'Evses_stationId_fkey'],
|
||||
['StatusNotifications', 'StatusNotifications_stationId_fkey'],
|
||||
['LatestStatusNotifications', 'LatestStatusNotifications_stationId_fkey'],
|
||||
['VariableAttributes', 'VariableAttributes_stationId_fkey'],
|
||||
['SetNetworkProfiles', 'SetNetworkProfiles_stationId_fkey'],
|
||||
['OCPPMessages', 'OCPPMessages_stationId_fkey'],
|
||||
['InstalledCertificates', 'InstalledCertificates_stationId_fkey'],
|
||||
['EventData', 'EventData_stationId_fkey'],
|
||||
['VariableMonitorings', 'VariableMonitorings_stationId_fkey'],
|
||||
['InstallCertificateAttempts', 'InstallCertificateAttempts_stationId_fkey'],
|
||||
['DeleteCertificateAttempts', 'DeleteCertificateAttempts_stationId_fkey'],
|
||||
['ChargingStationSecurityInfos', 'ChargingStationSecurityInfos_stationId_fkey'],
|
||||
// ── stationPkId_fkey constraints (left from a previous partial run) ──
|
||||
['Evses', 'Evses_stationPkId_fkey'],
|
||||
['Connectors', 'Connectors_stationPkId_fkey'],
|
||||
['Transactions', 'Transactions_stationPkId_fkey'],
|
||||
['ChargingStationNetworkProfiles', 'ChargingStationNetworkProfiles_stationPkId_fkey'],
|
||||
['ChargingStationSequences', 'ChargingStationSequences_stationPkId_fkey'],
|
||||
['StatusNotifications', 'StatusNotifications_stationPkId_fkey'],
|
||||
['LatestStatusNotifications', 'LatestStatusNotifications_stationPkId_fkey'],
|
||||
['VariableAttributes', 'VariableAttributes_stationPkId_fkey'],
|
||||
['SetNetworkProfiles', 'SetNetworkProfiles_stationPkId_fkey'],
|
||||
['OCPPMessages', 'OCPPMessages_stationPkId_fkey'],
|
||||
['InstalledCertificates', 'InstalledCertificates_stationPkId_fkey'],
|
||||
['EventData', 'EventData_stationPkId_fkey'],
|
||||
['VariableMonitorings', 'VariableMonitorings_stationPkId_fkey'],
|
||||
['InstallCertificateAttempts', 'InstallCertificateAttempts_stationPkId_fkey'],
|
||||
['DeleteCertificateAttempts', 'DeleteCertificateAttempts_stationPkId_fkey'],
|
||||
['ChargingStationSecurityInfos', 'ChargingStationSecurityInfos_stationPkId_fkey'],
|
||||
];
|
||||
|
||||
for (const [table, fkName] of allChildFks) {
|
||||
await dropConstraintIfExists(table, fkName);
|
||||
}
|
||||
|
||||
// ── Step 2: Add pkId to ChargingStations (safe for tables with data) ──────
|
||||
//
|
||||
// Adding a NOT NULL column without a DEFAULT to a non-empty table fails in
|
||||
// PostgreSQL. Safe order:
|
||||
// a) add as nullable
|
||||
// b) create sequence & attach as default
|
||||
// c) fill NULL pkIds (all rows on a clean run, leftover rows on re-run)
|
||||
// d) advance sequence past the current max
|
||||
// e) enforce NOT NULL
|
||||
|
||||
const csDesc = await describeTable('ChargingStations');
|
||||
if (!csDesc['pkId']) {
|
||||
await addCol('ChargingStations', 'pkId', {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true, // populated below before setting NOT NULL
|
||||
});
|
||||
}
|
||||
|
||||
await q(`CREATE SEQUENCE IF NOT EXISTS "ChargingStations_pkId_seq"`);
|
||||
await q(
|
||||
`ALTER TABLE "ChargingStations"
|
||||
ALTER COLUMN "pkId" SET DEFAULT nextval('"ChargingStations_pkId_seq"')`,
|
||||
);
|
||||
|
||||
// Assign a unique integer to every row that doesn't have one yet.
|
||||
await q(
|
||||
`UPDATE "ChargingStations"
|
||||
SET "pkId" = nextval('"ChargingStations_pkId_seq"')
|
||||
WHERE "pkId" IS NULL`,
|
||||
);
|
||||
|
||||
// Advance the sequence past the current maximum so future inserts are safe.
|
||||
await q(
|
||||
`SELECT setval(
|
||||
'"ChargingStations_pkId_seq"',
|
||||
COALESCE((SELECT MAX("pkId") FROM "ChargingStations"), 1)
|
||||
)`,
|
||||
);
|
||||
|
||||
// Now every row has a value — enforce NOT NULL.
|
||||
await q(`ALTER TABLE "ChargingStations" ALTER COLUMN "pkId" SET NOT NULL`);
|
||||
|
||||
// ── Step 3: Swap the primary key ─────────────────────────────────────────
|
||||
|
||||
await q(`ALTER TABLE "ChargingStations" DROP CONSTRAINT IF EXISTS "ChargingStations_pkey"`);
|
||||
await q(`ALTER TABLE "ChargingStations" ADD PRIMARY KEY ("pkId")`);
|
||||
|
||||
await dropConstraintIfExists('ChargingStations', 'ChargingStations_id_tenantId_key');
|
||||
await q(
|
||||
`ALTER TABLE "ChargingStations"
|
||||
ADD CONSTRAINT "ChargingStations_id_tenantId_key" UNIQUE (id, "tenantId")`,
|
||||
);
|
||||
|
||||
// ── Step 4: Migrate each child table ─────────────────────────────────────
|
||||
//
|
||||
// For every child table:
|
||||
// 1. Add stationPkId (nullable) if not present
|
||||
// 2. Populate it by joining to ChargingStations
|
||||
// 3. Validate — fail if any row with stationId couldn't be matched
|
||||
// 4. Drop old stationId FK
|
||||
// 5. Add new stationPkId FK
|
||||
|
||||
const migrateChildTable = async (table: string, onDeleteRule = 'SET NULL') => {
|
||||
const desc = await describeTable(table);
|
||||
if (!desc['stationPkId']) {
|
||||
await addCol(table, 'stationPkId', { type: DataTypes.INTEGER, allowNull: true });
|
||||
}
|
||||
|
||||
await q(
|
||||
`UPDATE "${table}" t
|
||||
SET "stationPkId" = cs."pkId"
|
||||
FROM "ChargingStations" cs
|
||||
WHERE cs.id = t."stationId"
|
||||
AND cs."tenantId" = t."tenantId"`,
|
||||
);
|
||||
|
||||
// Fail fast before we drop the old FK if any relationship can't be resolved.
|
||||
await validateNoOrphans(table);
|
||||
|
||||
await dropFkByColumn(table, 'stationId');
|
||||
|
||||
const fkName = `${table}_stationPkId_fkey`;
|
||||
await dropConstraintIfExists(table, fkName);
|
||||
await q(
|
||||
`ALTER TABLE "${table}"
|
||||
ADD CONSTRAINT "${fkName}"
|
||||
FOREIGN KEY ("stationPkId")
|
||||
REFERENCES "ChargingStations"("pkId")
|
||||
ON UPDATE CASCADE ON DELETE ${onDeleteRule}`,
|
||||
);
|
||||
};
|
||||
|
||||
// Evses
|
||||
await migrateChildTable('Evses', 'CASCADE');
|
||||
await dropConstraintIfExists('Evses', 'stationId_evseTypeId');
|
||||
await dropConstraintIfExists('Evses', 'stationPkId_evseTypeId');
|
||||
await q(
|
||||
`ALTER TABLE "Evses" ADD CONSTRAINT "stationPkId_evseTypeId" UNIQUE ("stationPkId", "evseTypeId")`,
|
||||
);
|
||||
|
||||
// Connectors
|
||||
await migrateChildTable('Connectors', 'CASCADE');
|
||||
await dropConstraintIfExists('Connectors', 'stationId_connectorId');
|
||||
await dropConstraintIfExists('Connectors', 'Connectors_stationId_connectorId_key');
|
||||
await dropConstraintIfExists('Connectors', 'stationPkId_connectorId');
|
||||
await q(
|
||||
`ALTER TABLE "Connectors" ADD CONSTRAINT "stationPkId_connectorId" UNIQUE ("stationPkId", "connectorId")`,
|
||||
);
|
||||
|
||||
// Transactions
|
||||
await migrateChildTable('Transactions', 'SET NULL');
|
||||
await dropConstraintIfExists('Transactions', 'stationId_transactionId');
|
||||
await dropConstraintIfExists('Transactions', 'Transactions_stationId_transactionId_key');
|
||||
await dropConstraintIfExists('Transactions', 'stationPkId_transactionId');
|
||||
await q(
|
||||
`ALTER TABLE "Transactions" ADD CONSTRAINT "stationPkId_transactionId" UNIQUE ("stationPkId", "transactionId")`,
|
||||
);
|
||||
|
||||
// ChargingStationNetworkProfiles — inline because it also needs its own
|
||||
// PK and extra unique constraints cleaned up.
|
||||
{
|
||||
const desc = await describeTable('ChargingStationNetworkProfiles');
|
||||
if (!desc['stationPkId']) {
|
||||
await addCol('ChargingStationNetworkProfiles', 'stationPkId', {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
});
|
||||
}
|
||||
await q(
|
||||
`UPDATE "ChargingStationNetworkProfiles" t
|
||||
SET "stationPkId" = cs."pkId"
|
||||
FROM "ChargingStations" cs
|
||||
WHERE cs.id = t."stationId"
|
||||
AND cs."tenantId" = t."tenantId"`,
|
||||
);
|
||||
await validateNoOrphans('ChargingStationNetworkProfiles');
|
||||
await dropFkByColumn('ChargingStationNetworkProfiles', 'stationId');
|
||||
|
||||
await dropConstraintIfExists(
|
||||
'ChargingStationNetworkProfiles',
|
||||
'ChargingStationNetworkProfiles_pkey',
|
||||
);
|
||||
// The stationId variant is exactly 63 chars — no truncation.
|
||||
await dropConstraintIfExists(
|
||||
'ChargingStationNetworkProfiles',
|
||||
'ChargingStationNetworkProfile_stationId_websocketServerConf_key',
|
||||
);
|
||||
// The stationPkId variant is 65 chars; PostgreSQL truncates stored names
|
||||
// to 63 chars, so we must drop both spellings to be safe.
|
||||
await dropConstraintIfExists(
|
||||
'ChargingStationNetworkProfiles',
|
||||
'ChargingStationNetworkProfile_stationPkId_websocketServerConf_key',
|
||||
);
|
||||
await dropConstraintIfExists(
|
||||
'ChargingStationNetworkProfiles',
|
||||
'ChargingStationNetworkProfile_stationPkId_websocketServerConf_k',
|
||||
);
|
||||
await dropConstraintIfExists(
|
||||
'ChargingStationNetworkProfiles',
|
||||
'stationId_configurationSlot',
|
||||
);
|
||||
await dropConstraintIfExists(
|
||||
'ChargingStationNetworkProfiles',
|
||||
'stationPkId_configurationSlot',
|
||||
);
|
||||
|
||||
await q(
|
||||
`ALTER TABLE "ChargingStationNetworkProfiles"
|
||||
ADD CONSTRAINT "ChargingStationNetworkProfiles_stationPkId_fkey"
|
||||
FOREIGN KEY ("stationPkId")
|
||||
REFERENCES "ChargingStations"("pkId")
|
||||
ON UPDATE CASCADE ON DELETE CASCADE`,
|
||||
);
|
||||
await q(
|
||||
`ALTER TABLE "ChargingStationNetworkProfiles"
|
||||
ADD CONSTRAINT "stationPkId_configurationSlot"
|
||||
UNIQUE ("stationPkId", "configurationSlot")`,
|
||||
);
|
||||
// Use a name that stays within PostgreSQL's 63-char identifier limit.
|
||||
await q(
|
||||
`ALTER TABLE "ChargingStationNetworkProfiles"
|
||||
ADD CONSTRAINT "CSNP_stationPkId_websocketServerConfigId_key"
|
||||
UNIQUE ("stationPkId", "websocketServerConfigId")`,
|
||||
);
|
||||
}
|
||||
|
||||
// ChargingStationSequences
|
||||
await migrateChildTable('ChargingStationSequences', 'CASCADE');
|
||||
await dropConstraintIfExists('ChargingStationSequences', 'stationId_type');
|
||||
await dropConstraintIfExists(
|
||||
'ChargingStationSequences',
|
||||
'ChargingStationSequences_stationId_type_key',
|
||||
);
|
||||
await dropConstraintIfExists('ChargingStationSequences', 'stationPkId_type');
|
||||
await q(
|
||||
`ALTER TABLE "ChargingStationSequences" ADD CONSTRAINT "stationPkId_type" UNIQUE ("stationPkId", type)`,
|
||||
);
|
||||
|
||||
await migrateChildTable('StatusNotifications', 'SET NULL');
|
||||
await migrateChildTable('LatestStatusNotifications', 'SET NULL');
|
||||
|
||||
// VariableAttributes
|
||||
await migrateChildTable('VariableAttributes', 'CASCADE');
|
||||
await dropIndexIfExists('variable_attributes_station_id');
|
||||
await dropConstraintIfExists('VariableAttributes', 'stationId_type_variableId_componentId');
|
||||
await dropConstraintIfExists(
|
||||
'VariableAttributes',
|
||||
'VariableAttributes_stationId_type_variableId_componentId_key',
|
||||
);
|
||||
await dropConstraintIfExists('VariableAttributes', 'stationPkId_type_variableId_componentId');
|
||||
await q(
|
||||
`ALTER TABLE "VariableAttributes"
|
||||
ADD CONSTRAINT "stationPkId_type_variableId_componentId"
|
||||
UNIQUE ("stationPkId", type, "variableId", "componentId")`,
|
||||
);
|
||||
await q(
|
||||
`CREATE UNIQUE INDEX IF NOT EXISTS "variable_attributes_stationPkId"
|
||||
ON "VariableAttributes" ("stationPkId")
|
||||
WHERE (type IS NULL AND "variableId" IS NULL AND "componentId" IS NULL)`,
|
||||
);
|
||||
|
||||
// SetNetworkProfiles
|
||||
{
|
||||
const desc = await describeTable('SetNetworkProfiles');
|
||||
if (!desc['stationPkId']) {
|
||||
await addCol('SetNetworkProfiles', 'stationPkId', {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
});
|
||||
}
|
||||
await q(
|
||||
`UPDATE "SetNetworkProfiles" t
|
||||
SET "stationPkId" = cs."pkId"
|
||||
FROM "ChargingStations" cs
|
||||
WHERE cs.id = t."stationId" AND cs."tenantId" = t."tenantId"`,
|
||||
);
|
||||
await validateNoOrphans('SetNetworkProfiles');
|
||||
await dropFkByColumn('SetNetworkProfiles', 'stationId');
|
||||
await dropConstraintIfExists('SetNetworkProfiles', 'SetNetworkProfiles_stationPkId_fkey');
|
||||
await q(
|
||||
`ALTER TABLE "SetNetworkProfiles"
|
||||
ADD CONSTRAINT "SetNetworkProfiles_stationPkId_fkey"
|
||||
FOREIGN KEY ("stationPkId") REFERENCES "ChargingStations"("pkId")
|
||||
ON UPDATE CASCADE ON DELETE SET NULL`,
|
||||
);
|
||||
}
|
||||
|
||||
// OCPPMessages
|
||||
{
|
||||
const desc = await describeTable('OCPPMessages');
|
||||
if (!desc['stationPkId']) {
|
||||
await addCol('OCPPMessages', 'stationPkId', { type: DataTypes.INTEGER, allowNull: true });
|
||||
}
|
||||
await q(
|
||||
`UPDATE "OCPPMessages" t
|
||||
SET "stationPkId" = cs."pkId"
|
||||
FROM "ChargingStations" cs
|
||||
WHERE cs.id = t."stationId" AND cs."tenantId" = t."tenantId"`,
|
||||
);
|
||||
await validateNoOrphans('OCPPMessages');
|
||||
await dropFkByColumn('OCPPMessages', 'stationId');
|
||||
await dropConstraintIfExists('OCPPMessages', 'OCPPMessages_stationPkId_fkey');
|
||||
await q(
|
||||
`ALTER TABLE "OCPPMessages"
|
||||
ADD CONSTRAINT "OCPPMessages_stationPkId_fkey"
|
||||
FOREIGN KEY ("stationPkId") REFERENCES "ChargingStations"("pkId")
|
||||
ON UPDATE CASCADE ON DELETE SET NULL`,
|
||||
);
|
||||
}
|
||||
|
||||
// InstalledCertificates
|
||||
{
|
||||
const desc = await describeTable('InstalledCertificates');
|
||||
if (!desc['stationPkId']) {
|
||||
await addCol('InstalledCertificates', 'stationPkId', {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
});
|
||||
}
|
||||
await q(
|
||||
`UPDATE "InstalledCertificates" t
|
||||
SET "stationPkId" = cs."pkId"
|
||||
FROM "ChargingStations" cs
|
||||
WHERE cs.id = t."stationId" AND cs."tenantId" = t."tenantId"`,
|
||||
);
|
||||
await validateNoOrphans('InstalledCertificates');
|
||||
await dropFkByColumn('InstalledCertificates', 'stationId');
|
||||
await dropConstraintIfExists(
|
||||
'InstalledCertificates',
|
||||
'InstalledCertificates_stationPkId_fkey',
|
||||
);
|
||||
await q(
|
||||
`ALTER TABLE "InstalledCertificates"
|
||||
ADD CONSTRAINT "InstalledCertificates_stationPkId_fkey"
|
||||
FOREIGN KEY ("stationPkId") REFERENCES "ChargingStations"("pkId")
|
||||
ON UPDATE CASCADE ON DELETE CASCADE`,
|
||||
);
|
||||
}
|
||||
|
||||
// EventData
|
||||
{
|
||||
const desc = await describeTable('EventData');
|
||||
if (!desc['stationPkId']) {
|
||||
await addCol('EventData', 'stationPkId', { type: DataTypes.INTEGER, allowNull: true });
|
||||
}
|
||||
await q(
|
||||
`UPDATE "EventData" t
|
||||
SET "stationPkId" = cs."pkId"
|
||||
FROM "ChargingStations" cs
|
||||
WHERE cs.id = t."stationId" AND cs."tenantId" = t."tenantId"`,
|
||||
);
|
||||
await validateNoOrphans('EventData');
|
||||
await dropFkByColumn('EventData', 'stationId');
|
||||
await dropConstraintIfExists('EventData', 'EventData_stationPkId_fkey');
|
||||
await q(
|
||||
`ALTER TABLE "EventData"
|
||||
ADD CONSTRAINT "EventData_stationPkId_fkey"
|
||||
FOREIGN KEY ("stationPkId") REFERENCES "ChargingStations"("pkId")
|
||||
ON UPDATE CASCADE ON DELETE SET NULL`,
|
||||
);
|
||||
}
|
||||
|
||||
// VariableMonitorings
|
||||
{
|
||||
const desc = await describeTable('VariableMonitorings');
|
||||
if (!desc['stationPkId']) {
|
||||
await addCol('VariableMonitorings', 'stationPkId', {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
});
|
||||
}
|
||||
await q(
|
||||
`UPDATE "VariableMonitorings" t
|
||||
SET "stationPkId" = cs."pkId"
|
||||
FROM "ChargingStations" cs
|
||||
WHERE cs.id = t."stationId" AND cs."tenantId" = t."tenantId"`,
|
||||
);
|
||||
await validateNoOrphans('VariableMonitorings');
|
||||
await dropFkByColumn('VariableMonitorings', 'stationId');
|
||||
await dropConstraintIfExists('VariableMonitorings', 'VariableMonitorings_stationPkId_fkey');
|
||||
await q(
|
||||
`ALTER TABLE "VariableMonitorings"
|
||||
ADD CONSTRAINT "VariableMonitorings_stationPkId_fkey"
|
||||
FOREIGN KEY ("stationPkId") REFERENCES "ChargingStations"("pkId")
|
||||
ON UPDATE CASCADE ON DELETE SET NULL`,
|
||||
);
|
||||
}
|
||||
|
||||
// InstallCertificateAttempts
|
||||
{
|
||||
const desc = await describeTable('InstallCertificateAttempts');
|
||||
if (!desc['stationPkId']) {
|
||||
await addCol('InstallCertificateAttempts', 'stationPkId', {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
});
|
||||
}
|
||||
await q(
|
||||
`UPDATE "InstallCertificateAttempts" t
|
||||
SET "stationPkId" = cs."pkId"
|
||||
FROM "ChargingStations" cs
|
||||
WHERE cs.id = t."stationId" AND cs."tenantId" = t."tenantId"`,
|
||||
);
|
||||
await validateNoOrphans('InstallCertificateAttempts');
|
||||
await dropFkByColumn('InstallCertificateAttempts', 'stationId');
|
||||
await dropConstraintIfExists(
|
||||
'InstallCertificateAttempts',
|
||||
'InstallCertificateAttempts_stationPkId_fkey',
|
||||
);
|
||||
await q(
|
||||
`ALTER TABLE "InstallCertificateAttempts"
|
||||
ADD CONSTRAINT "InstallCertificateAttempts_stationPkId_fkey"
|
||||
FOREIGN KEY ("stationPkId") REFERENCES "ChargingStations"("pkId")
|
||||
ON UPDATE CASCADE ON DELETE SET NULL`,
|
||||
);
|
||||
}
|
||||
|
||||
// DeleteCertificateAttempts
|
||||
{
|
||||
const desc = await describeTable('DeleteCertificateAttempts');
|
||||
if (!desc['stationPkId']) {
|
||||
await addCol('DeleteCertificateAttempts', 'stationPkId', {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
});
|
||||
}
|
||||
await q(
|
||||
`UPDATE "DeleteCertificateAttempts" t
|
||||
SET "stationPkId" = cs."pkId"
|
||||
FROM "ChargingStations" cs
|
||||
WHERE cs.id = t."stationId" AND cs."tenantId" = t."tenantId"`,
|
||||
);
|
||||
await validateNoOrphans('DeleteCertificateAttempts');
|
||||
await dropFkByColumn('DeleteCertificateAttempts', 'stationId');
|
||||
await dropConstraintIfExists(
|
||||
'DeleteCertificateAttempts',
|
||||
'DeleteCertificateAttempts_stationPkId_fkey',
|
||||
);
|
||||
await q(
|
||||
`ALTER TABLE "DeleteCertificateAttempts"
|
||||
ADD CONSTRAINT "DeleteCertificateAttempts_stationPkId_fkey"
|
||||
FOREIGN KEY ("stationPkId") REFERENCES "ChargingStations"("pkId")
|
||||
ON UPDATE CASCADE ON DELETE SET NULL`,
|
||||
);
|
||||
}
|
||||
|
||||
// ChargingStationSecurityInfos
|
||||
{
|
||||
const desc = await describeTable('ChargingStationSecurityInfos');
|
||||
if (!desc['stationPkId']) {
|
||||
await addCol('ChargingStationSecurityInfos', 'stationPkId', {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
});
|
||||
}
|
||||
await q(
|
||||
`UPDATE "ChargingStationSecurityInfos" t
|
||||
SET "stationPkId" = cs."pkId"
|
||||
FROM "ChargingStations" cs
|
||||
WHERE cs.id = t."stationId" AND cs."tenantId" = t."tenantId"`,
|
||||
);
|
||||
await validateNoOrphans('ChargingStationSecurityInfos');
|
||||
await dropFkByColumn('ChargingStationSecurityInfos', 'stationId');
|
||||
await dropConstraintIfExists(
|
||||
'ChargingStationSecurityInfos',
|
||||
'ChargingStationSecurityInfos_stationPkId_fkey',
|
||||
);
|
||||
await q(
|
||||
`ALTER TABLE "ChargingStationSecurityInfos"
|
||||
ADD CONSTRAINT "ChargingStationSecurityInfos_stationPkId_fkey"
|
||||
FOREIGN KEY ("stationPkId") REFERENCES "ChargingStations"("pkId")
|
||||
ON UPDATE CASCADE ON DELETE SET NULL`,
|
||||
);
|
||||
}
|
||||
|
||||
// ── Step 5: Widen unique constraints to include tenantId ──────────────────
|
||||
|
||||
const dropOldUnique = async (table: string, ...names: string[]) => {
|
||||
for (const name of names) await dropConstraintIfExists(table, name);
|
||||
};
|
||||
|
||||
// Constraint names in PostgreSQL are globally unique per schema (they are
|
||||
// backed by indexes). Prefix every name with the table to avoid collisions.
|
||||
|
||||
await dropOldUnique(
|
||||
'ChargingStationSecurityInfos',
|
||||
'ChargingStationSecurityInfos_stationId_key',
|
||||
'stationId_tenantId',
|
||||
'ChargingStationSecurityInfos_stationId_tenantId',
|
||||
);
|
||||
await q(
|
||||
`ALTER TABLE "ChargingStationSecurityInfos" ADD CONSTRAINT "ChargingStationSecurityInfos_stationId_tenantId" UNIQUE ("stationId", "tenantId")`,
|
||||
);
|
||||
|
||||
await dropOldUnique(
|
||||
'Reservations',
|
||||
'stationId_id',
|
||||
'Reservations_id_stationId_key',
|
||||
'stationId_tenantId_id',
|
||||
'Reservations_stationId_tenantId_id',
|
||||
);
|
||||
await q(
|
||||
`ALTER TABLE "Reservations" ADD CONSTRAINT "Reservations_stationId_tenantId_id" UNIQUE ("stationId", "tenantId", id)`,
|
||||
);
|
||||
|
||||
await dropOldUnique(
|
||||
'VariableMonitorings',
|
||||
'stationId_Id',
|
||||
'VariableMonitorings_stationId_id_key',
|
||||
'stationId_tenantId_Id',
|
||||
'VariableMonitorings_stationId_tenantId_id',
|
||||
);
|
||||
await q(
|
||||
`ALTER TABLE "VariableMonitorings" ADD CONSTRAINT "VariableMonitorings_stationId_tenantId_id" UNIQUE ("stationId", "tenantId", id)`,
|
||||
);
|
||||
|
||||
await dropOldUnique(
|
||||
'MessageInfos',
|
||||
'stationId_id',
|
||||
'MessageInfos_stationId_id_key',
|
||||
'stationId_tenantId_id',
|
||||
'MessageInfos_stationId_tenantId_id',
|
||||
);
|
||||
await q(
|
||||
`ALTER TABLE "MessageInfos" ADD CONSTRAINT "MessageInfos_stationId_tenantId_id" UNIQUE ("stationId", "tenantId", id)`,
|
||||
);
|
||||
|
||||
await dropOldUnique(
|
||||
'ChangeConfigurations',
|
||||
'stationId_key',
|
||||
'ChangeConfigurations_stationId_key_key',
|
||||
'stationId_tenantId_key',
|
||||
'ChangeConfigurations_stationId_tenantId_key',
|
||||
);
|
||||
await q(
|
||||
`ALTER TABLE "ChangeConfigurations" ADD CONSTRAINT "ChangeConfigurations_stationId_tenantId_key" UNIQUE ("stationId", "tenantId", key)`,
|
||||
);
|
||||
|
||||
await dropOldUnique(
|
||||
'ChargingProfiles',
|
||||
'stationId_id',
|
||||
'ChargingProfiles_stationId_id_key',
|
||||
'stationId_tenantId_id',
|
||||
'ChargingProfiles_stationId_tenantId_id',
|
||||
);
|
||||
await q(
|
||||
`ALTER TABLE "ChargingProfiles" ADD CONSTRAINT "ChargingProfiles_stationId_tenantId_id" UNIQUE ("stationId", "tenantId", id)`,
|
||||
);
|
||||
|
||||
await dropOldUnique(
|
||||
'ChargingSchedules',
|
||||
'stationId_id',
|
||||
'ChargingSchedules_stationId_id_key',
|
||||
'stationId_tenantId_id',
|
||||
'ChargingSchedules_stationId_tenantId_id',
|
||||
);
|
||||
await q(
|
||||
`ALTER TABLE "ChargingSchedules" ADD CONSTRAINT "ChargingSchedules_stationId_tenantId_id" UNIQUE ("stationId", "tenantId", id)`,
|
||||
);
|
||||
|
||||
await dropOldUnique(
|
||||
'LocalListVersions',
|
||||
'LocalListVersions_stationId_key',
|
||||
'stationId_tenantId',
|
||||
'LocalListVersions_stationId_tenantId',
|
||||
);
|
||||
await q(
|
||||
`ALTER TABLE "LocalListVersions" ADD CONSTRAINT "LocalListVersions_stationId_tenantId" UNIQUE ("stationId", "tenantId")`,
|
||||
);
|
||||
|
||||
await dropOldUnique(
|
||||
'EventData',
|
||||
'stationId_eventId',
|
||||
'EventData_stationId_eventId_key',
|
||||
'stationId_tenantId_eventId',
|
||||
'EventData_stationId_tenantId_eventId',
|
||||
);
|
||||
await q(
|
||||
`ALTER TABLE "EventData" ADD CONSTRAINT "EventData_stationId_tenantId_eventId" UNIQUE ("stationId", "tenantId", "eventId")`,
|
||||
);
|
||||
|
||||
// ── Step 6: Create DB triggers to auto-populate stationPkId ─────────────
|
||||
//
|
||||
// Now that every child table has its stationPkId column, we create a
|
||||
// BEFORE INSERT OR UPDATE trigger on each one. The trigger looks up the
|
||||
// ChargingStation.pkId from (stationId, tenantId) whenever stationPkId is
|
||||
// not already supplied, giving a DB-level safety net on top of the ORM
|
||||
// @BeforeCreate hooks defined on each model.
|
||||
//
|
||||
// Note: this used to live in a separate migration file
|
||||
// (20260330000000-charging-station-pk-id-triggers.ts) that ran *before*
|
||||
// the columns were added, so all trigger creations were silently skipped.
|
||||
// The trigger setup is now consolidated here, at the end of `up`, where
|
||||
// all stationPkId columns are guaranteed to exist.
|
||||
|
||||
const allTablesWithStationPkId = [
|
||||
'Evses',
|
||||
'Connectors',
|
||||
'Transactions',
|
||||
'ChargingStationNetworkProfiles',
|
||||
'ChargingStationSequences',
|
||||
'StatusNotifications',
|
||||
'LatestStatusNotifications',
|
||||
'VariableAttributes',
|
||||
'SetNetworkProfiles',
|
||||
'OCPPMessages',
|
||||
'InstalledCertificates',
|
||||
'ChargingStationSecurityInfos',
|
||||
'VariableMonitorings',
|
||||
'EventData',
|
||||
'InstallCertificateAttempts',
|
||||
'DeleteCertificateAttempts',
|
||||
];
|
||||
|
||||
await q(`
|
||||
CREATE OR REPLACE FUNCTION populate_station_pk_id()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
SELECT "pkId" INTO NEW."stationPkId"
|
||||
FROM "ChargingStations"
|
||||
WHERE "id" = NEW."stationId" AND "tenantId" = NEW."tenantId";
|
||||
|
||||
IF NEW."stationPkId" IS NULL THEN
|
||||
RAISE EXCEPTION 'No ChargingStation found with stationId=% and tenantId=%',
|
||||
NEW."stationId", NEW."tenantId";
|
||||
END IF;
|
||||
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
`);
|
||||
|
||||
for (const tableName of allTablesWithStationPkId) {
|
||||
const triggerName = `trigger_populate_${tableName.toLowerCase()}_station_pk_id`;
|
||||
await q(`DROP TRIGGER IF EXISTS "${triggerName}" ON "${tableName}"`);
|
||||
await q(`
|
||||
CREATE TRIGGER "${triggerName}"
|
||||
BEFORE INSERT OR UPDATE ON "${tableName}"
|
||||
FOR EACH ROW
|
||||
WHEN (NEW."stationPkId" IS NULL)
|
||||
EXECUTE FUNCTION populate_station_pk_id()
|
||||
`);
|
||||
}
|
||||
|
||||
console.log('Migration 20260330100000-add-charging-station-pk-id completed successfully.');
|
||||
}); // ── end transaction ──────────────────────────────────────────────────────
|
||||
},
|
||||
|
||||
down: async (queryInterface: QueryInterface) => {
|
||||
await queryInterface.sequelize.transaction(async (transaction) => {
|
||||
const q = (sql: string) =>
|
||||
queryInterface.sequelize.query(sql, { transaction, type: QueryTypes.RAW });
|
||||
|
||||
// ── Drop triggers and function first ──────────────────────────────────────
|
||||
// Must happen before we remove the stationPkId columns they reference.
|
||||
const allTablesWithStationPkId = [
|
||||
'Evses',
|
||||
'Connectors',
|
||||
'Transactions',
|
||||
'ChargingStationNetworkProfiles',
|
||||
'ChargingStationSequences',
|
||||
'StatusNotifications',
|
||||
'LatestStatusNotifications',
|
||||
'VariableAttributes',
|
||||
'SetNetworkProfiles',
|
||||
'OCPPMessages',
|
||||
'InstalledCertificates',
|
||||
'ChargingStationSecurityInfos',
|
||||
'VariableMonitorings',
|
||||
'EventData',
|
||||
'InstallCertificateAttempts',
|
||||
'DeleteCertificateAttempts',
|
||||
];
|
||||
for (const tableName of allTablesWithStationPkId) {
|
||||
const triggerName = `trigger_populate_${tableName.toLowerCase()}_station_pk_id`;
|
||||
await q(`DROP TRIGGER IF EXISTS "${triggerName}" ON "${tableName}"`);
|
||||
}
|
||||
await q(`DROP FUNCTION IF EXISTS populate_station_pk_id()`);
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
const qSelect = <T = any>(sql: string, replacements?: Record<string, any>): Promise<T[]> =>
|
||||
queryInterface.sequelize.query(sql, {
|
||||
transaction,
|
||||
replacements,
|
||||
type: QueryTypes.SELECT,
|
||||
}) as Promise<T[]>;
|
||||
|
||||
const dropFkIfExists = (table: string, name: string) =>
|
||||
q(`ALTER TABLE "${table}" DROP CONSTRAINT IF EXISTS "${name}"`);
|
||||
|
||||
/** Check which columns exist — uses the transaction connection. */
|
||||
const describeTable = async (table: string): Promise<Record<string, any>> => {
|
||||
const rows = await qSelect<{ Field: string }>(
|
||||
`SELECT column_name AS "Field"
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = 'public'
|
||||
AND table_name = :table`,
|
||||
{ table },
|
||||
);
|
||||
const desc: Record<string, any> = {};
|
||||
for (const row of rows) {
|
||||
desc[(row as any).Field] = true;
|
||||
}
|
||||
return desc;
|
||||
};
|
||||
|
||||
const childTables: [string, string][] = [
|
||||
['Evses', 'Evses_stationPkId_fkey'],
|
||||
['Connectors', 'Connectors_stationPkId_fkey'],
|
||||
['Transactions', 'Transactions_stationPkId_fkey'],
|
||||
['ChargingStationNetworkProfiles', 'ChargingStationNetworkProfiles_stationPkId_fkey'],
|
||||
['ChargingStationSequences', 'ChargingStationSequences_stationPkId_fkey'],
|
||||
['StatusNotifications', 'StatusNotifications_stationPkId_fkey'],
|
||||
['LatestStatusNotifications', 'LatestStatusNotifications_stationPkId_fkey'],
|
||||
['VariableAttributes', 'VariableAttributes_stationPkId_fkey'],
|
||||
['SetNetworkProfiles', 'SetNetworkProfiles_stationPkId_fkey'],
|
||||
['OCPPMessages', 'OCPPMessages_stationPkId_fkey'],
|
||||
['InstalledCertificates', 'InstalledCertificates_stationPkId_fkey'],
|
||||
['EventData', 'EventData_stationPkId_fkey'],
|
||||
['VariableMonitorings', 'VariableMonitorings_stationPkId_fkey'],
|
||||
['InstallCertificateAttempts', 'InstallCertificateAttempts_stationPkId_fkey'],
|
||||
['DeleteCertificateAttempts', 'DeleteCertificateAttempts_stationPkId_fkey'],
|
||||
['ChargingStationSecurityInfos', 'ChargingStationSecurityInfos_stationPkId_fkey'],
|
||||
];
|
||||
|
||||
// Drop unique constraints / indexes that include stationPkId BEFORE
|
||||
// removing the column — PostgreSQL's DROP COLUMN (without CASCADE) will
|
||||
// fail if the column is still referenced by a multi-column constraint.
|
||||
await q(
|
||||
`ALTER TABLE "Evses" DROP CONSTRAINT IF EXISTS "stationPkId_evseTypeId"`,
|
||||
);
|
||||
await q(
|
||||
`ALTER TABLE "Connectors" DROP CONSTRAINT IF EXISTS "stationPkId_connectorId"`,
|
||||
);
|
||||
await q(
|
||||
`ALTER TABLE "Transactions" DROP CONSTRAINT IF EXISTS "stationPkId_transactionId"`,
|
||||
);
|
||||
await q(
|
||||
`ALTER TABLE "ChargingStationSequences" DROP CONSTRAINT IF EXISTS "stationPkId_type"`,
|
||||
);
|
||||
await q(
|
||||
`ALTER TABLE "ChargingStationNetworkProfiles" DROP CONSTRAINT IF EXISTS "stationPkId_configurationSlot"`,
|
||||
);
|
||||
await q(
|
||||
`ALTER TABLE "ChargingStationNetworkProfiles" DROP CONSTRAINT IF EXISTS "CSNP_stationPkId_websocketServerConfigId_key"`,
|
||||
);
|
||||
// Also drop the truncated 63-char variant in case a previous partial run stored it.
|
||||
await q(
|
||||
`ALTER TABLE "ChargingStationNetworkProfiles" DROP CONSTRAINT IF EXISTS "ChargingStationNetworkProfile_stationPkId_websocketServerConf_k"`,
|
||||
);
|
||||
await q(
|
||||
`ALTER TABLE "VariableAttributes" DROP CONSTRAINT IF EXISTS "stationPkId_type_variableId_componentId"`,
|
||||
);
|
||||
await q(`DROP INDEX IF EXISTS "variable_attributes_stationPkId"`);
|
||||
|
||||
// Now drop FK constraints and remove the stationPkId column from each child table.
|
||||
for (const [table, fkName] of childTables) {
|
||||
await dropFkIfExists(table, fkName);
|
||||
const desc = await describeTable(table);
|
||||
if (desc['stationPkId']) {
|
||||
await queryInterface.removeColumn(table, 'stationPkId', { transaction } as any);
|
||||
}
|
||||
}
|
||||
|
||||
// Restore ChargingStations primary key to id
|
||||
await q(
|
||||
`ALTER TABLE "ChargingStations" DROP CONSTRAINT IF EXISTS "ChargingStations_id_tenantId_key"`,
|
||||
);
|
||||
await q(`ALTER TABLE "ChargingStations" DROP CONSTRAINT IF EXISTS "ChargingStations_pkey"`);
|
||||
await q(`ALTER TABLE "ChargingStations" ADD PRIMARY KEY (id)`);
|
||||
|
||||
const csDesc = await describeTable('ChargingStations');
|
||||
if (csDesc['pkId']) {
|
||||
await queryInterface.removeColumn('ChargingStations', 'pkId', { transaction } as any);
|
||||
}
|
||||
await q(`DROP SEQUENCE IF EXISTS "ChargingStations_pkId_seq"`);
|
||||
|
||||
// Restore narrow unique constraints
|
||||
const restoreNarrowUnique = async (
|
||||
table: string,
|
||||
wideName: string,
|
||||
narrowName: string,
|
||||
columns: string[],
|
||||
) => {
|
||||
await q(`ALTER TABLE "${table}" DROP CONSTRAINT IF EXISTS "${wideName}"`);
|
||||
const cols = columns.map((c) => `"${c}"`).join(', ');
|
||||
await q(`ALTER TABLE "${table}" ADD CONSTRAINT "${narrowName}" UNIQUE (${cols})`);
|
||||
};
|
||||
|
||||
await restoreNarrowUnique(
|
||||
'ChargingStationSecurityInfos',
|
||||
'ChargingStationSecurityInfos_stationId_tenantId',
|
||||
'ChargingStationSecurityInfos_stationId_key',
|
||||
['stationId'],
|
||||
);
|
||||
await restoreNarrowUnique(
|
||||
'Reservations',
|
||||
'Reservations_stationId_tenantId_id',
|
||||
'Reservations_stationId_id',
|
||||
['stationId', 'id'],
|
||||
);
|
||||
await restoreNarrowUnique(
|
||||
'VariableMonitorings',
|
||||
'VariableMonitorings_stationId_tenantId_id',
|
||||
'VariableMonitorings_stationId_id',
|
||||
['stationId', 'id'],
|
||||
);
|
||||
await restoreNarrowUnique(
|
||||
'MessageInfos',
|
||||
'MessageInfos_stationId_tenantId_id',
|
||||
'MessageInfos_stationId_id',
|
||||
['stationId', 'id'],
|
||||
);
|
||||
await restoreNarrowUnique(
|
||||
'ChangeConfigurations',
|
||||
'ChangeConfigurations_stationId_tenantId_key',
|
||||
'ChangeConfigurations_stationId_key',
|
||||
['stationId', 'key'],
|
||||
);
|
||||
await restoreNarrowUnique(
|
||||
'ChargingProfiles',
|
||||
'ChargingProfiles_stationId_tenantId_id',
|
||||
'ChargingProfiles_stationId_id',
|
||||
['stationId', 'id'],
|
||||
);
|
||||
await restoreNarrowUnique(
|
||||
'ChargingSchedules',
|
||||
'ChargingSchedules_stationId_tenantId_id',
|
||||
'ChargingSchedules_stationId_id',
|
||||
['stationId', 'id'],
|
||||
);
|
||||
await restoreNarrowUnique(
|
||||
'LocalListVersions',
|
||||
'LocalListVersions_stationId_tenantId',
|
||||
'LocalListVersions_stationId_key',
|
||||
['stationId'],
|
||||
);
|
||||
await restoreNarrowUnique(
|
||||
'EventData',
|
||||
'EventData_stationId_tenantId_eventId',
|
||||
'EventData_stationId_eventId',
|
||||
['stationId', 'eventId'],
|
||||
);
|
||||
}); // ── end transaction ──────────────────────────────────────────────────────
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,79 @@
|
||||
// SPDX-FileCopyrightText: 2025 Contributors to the CitrineOS Project
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
'use strict';
|
||||
|
||||
/** @type {import('sequelize-cli').Migration} */
|
||||
import { QueryInterface } from 'sequelize';
|
||||
|
||||
export default {
|
||||
up: async (queryInterface: QueryInterface) => {
|
||||
// Drop the existing index that does not include tenantId, which prevents
|
||||
// different tenants from using the same idToken+idTokenType combination.
|
||||
console.log('Dropping index idToken_type (missing tenantId)...');
|
||||
try {
|
||||
await queryInterface.removeIndex('Authorizations', 'idToken_type');
|
||||
console.log('Successfully dropped index: idToken_type');
|
||||
} catch (error: any) {
|
||||
if (error.message?.includes('does not exist')) {
|
||||
console.log('Index idToken_type does not exist, skipping removal');
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Checking for duplicate tenantId/idToken/idTokenType combinations...');
|
||||
|
||||
const [duplicates] = await queryInterface.sequelize.query(`
|
||||
SELECT "tenantId", "idToken", "idTokenType", COUNT(*) as count
|
||||
FROM "Authorizations"
|
||||
WHERE "idToken" IS NOT NULL
|
||||
GROUP BY "tenantId", "idToken", "idTokenType"
|
||||
HAVING COUNT(*) > 1
|
||||
ORDER BY "tenantId", "idToken", "idTokenType"
|
||||
`);
|
||||
|
||||
if (duplicates.length > 0) {
|
||||
console.error('Cannot create unique index due to duplicate data:');
|
||||
duplicates.forEach((dup: any) => {
|
||||
console.error(
|
||||
` - tenantId: "${dup.tenantId}", idToken: "${dup.idToken}", idTokenType: "${dup.idTokenType}", count: ${dup.count}`,
|
||||
);
|
||||
});
|
||||
throw new Error(
|
||||
`Migration failed: Found ${duplicates.length} duplicate tenantId/idToken/idTokenType combinations. ` +
|
||||
'Please resolve these duplicates before running this migration. ' +
|
||||
'You may need to update or remove duplicate records in the Authorizations table.',
|
||||
);
|
||||
}
|
||||
|
||||
console.log('No duplicates found. Proceeding with index creation...');
|
||||
|
||||
await queryInterface.addIndex('Authorizations', ['tenantId', 'idToken', 'idTokenType'], {
|
||||
unique: true,
|
||||
name: 'idToken_type',
|
||||
});
|
||||
console.log('Successfully created unique index: idToken_type');
|
||||
},
|
||||
|
||||
down: async (queryInterface: QueryInterface) => {
|
||||
console.log('Reverting to index without tenantId...');
|
||||
|
||||
try {
|
||||
await queryInterface.removeIndex('Authorizations', 'idToken_type');
|
||||
console.log('Successfully dropped index: idToken_type');
|
||||
} catch (error: any) {
|
||||
if (error.message?.includes('does not exist')) {
|
||||
console.log('Index idToken_type does not exist, skipping removal');
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
await queryInterface.addIndex('Authorizations', ['idToken', 'idTokenType'], {
|
||||
unique: true,
|
||||
name: 'idToken_type',
|
||||
});
|
||||
console.log('Successfully recreated original index: idToken_type');
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,137 @@
|
||||
// SPDX-FileCopyrightText: 2026 Contributors to the CitrineOS Project
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
'use strict';
|
||||
|
||||
/** @type {import('sequelize-cli').Migration} */
|
||||
import { QueryInterface } from 'sequelize';
|
||||
|
||||
// The initial migration created 7 partial unique indexes on VariableAttributes using
|
||||
// stationId without tenantId, preventing different tenants from using the same
|
||||
// variable attribute combinations. The 20260330000000-add-charging-station-pk-id
|
||||
// migration replaced one of them (the all-null case) and the unconditional constraint
|
||||
// with stationPkId-based equivalents, but left the remaining 6 partial indexes
|
||||
// using stationId. This migration drops those and recreates them using stationPkId,
|
||||
// which is a FK to ChargingStations(pkId) — globally unique per station-tenant pair —
|
||||
// making the constraints effectively per-tenant.
|
||||
export default {
|
||||
up: async (queryInterface: QueryInterface) => {
|
||||
const dropIndex = async (name: string) => {
|
||||
await queryInterface.sequelize.query(`DROP INDEX IF EXISTS "${name}"`);
|
||||
};
|
||||
|
||||
const oldIndexes = [
|
||||
'variable_attributes_station_id_type',
|
||||
'variable_attributes_station_id_variable_id',
|
||||
'variable_attributes_station_id_component_id',
|
||||
'variable_attributes_station_id_type_variable_id',
|
||||
'variable_attributes_station_id_type_component_id',
|
||||
'variable_attributes_station_id_variable_id_component_id',
|
||||
];
|
||||
|
||||
for (const name of oldIndexes) {
|
||||
console.log(`Dropping index ${name}...`);
|
||||
await dropIndex(name);
|
||||
}
|
||||
|
||||
console.log('Creating stationPkId-based partial unique indexes...');
|
||||
|
||||
await queryInterface.sequelize.query(`
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "variable_attributes_stationPkId_type"
|
||||
ON "VariableAttributes" ("stationPkId", "type")
|
||||
WHERE "variableId" IS NULL AND "componentId" IS NULL
|
||||
`);
|
||||
|
||||
await queryInterface.sequelize.query(`
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "variable_attributes_stationPkId_variableId"
|
||||
ON "VariableAttributes" ("stationPkId", "variableId")
|
||||
WHERE "type" IS NULL AND "componentId" IS NULL
|
||||
`);
|
||||
|
||||
await queryInterface.sequelize.query(`
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "variable_attributes_stationPkId_componentId"
|
||||
ON "VariableAttributes" ("stationPkId", "componentId")
|
||||
WHERE "type" IS NULL AND "variableId" IS NULL
|
||||
`);
|
||||
|
||||
await queryInterface.sequelize.query(`
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "variable_attributes_stationPkId_type_variableId"
|
||||
ON "VariableAttributes" ("stationPkId", "type", "variableId")
|
||||
WHERE "componentId" IS NULL
|
||||
`);
|
||||
|
||||
await queryInterface.sequelize.query(`
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "variable_attributes_stationPkId_type_componentId"
|
||||
ON "VariableAttributes" ("stationPkId", "type", "componentId")
|
||||
WHERE "variableId" IS NULL
|
||||
`);
|
||||
|
||||
await queryInterface.sequelize.query(`
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "variable_attributes_stationPkId_variableId_componentId"
|
||||
ON "VariableAttributes" ("stationPkId", "variableId", "componentId")
|
||||
WHERE "type" IS NULL
|
||||
`);
|
||||
|
||||
console.log('Successfully recreated VariableAttributes partial indexes with stationPkId.');
|
||||
},
|
||||
|
||||
down: async (queryInterface: QueryInterface) => {
|
||||
const dropIndex = async (name: string) => {
|
||||
await queryInterface.sequelize.query(`DROP INDEX IF EXISTS "${name}"`);
|
||||
};
|
||||
|
||||
const newIndexes = [
|
||||
'variable_attributes_stationPkId_type',
|
||||
'variable_attributes_stationPkId_variableId',
|
||||
'variable_attributes_stationPkId_componentId',
|
||||
'variable_attributes_stationPkId_type_variableId',
|
||||
'variable_attributes_stationPkId_type_componentId',
|
||||
'variable_attributes_stationPkId_variableId_componentId',
|
||||
];
|
||||
|
||||
for (const name of newIndexes) {
|
||||
console.log(`Dropping index ${name}...`);
|
||||
await dropIndex(name);
|
||||
}
|
||||
|
||||
console.log('Recreating original stationId-based partial unique indexes...');
|
||||
|
||||
await queryInterface.sequelize.query(`
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "variable_attributes_station_id_type"
|
||||
ON "VariableAttributes" ("stationId", "type")
|
||||
WHERE "variableId" IS NULL AND "componentId" IS NULL
|
||||
`);
|
||||
|
||||
await queryInterface.sequelize.query(`
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "variable_attributes_station_id_variable_id"
|
||||
ON "VariableAttributes" ("stationId", "variableId")
|
||||
WHERE "type" IS NULL AND "componentId" IS NULL
|
||||
`);
|
||||
|
||||
await queryInterface.sequelize.query(`
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "variable_attributes_station_id_component_id"
|
||||
ON "VariableAttributes" ("stationId", "componentId")
|
||||
WHERE "type" IS NULL AND "variableId" IS NULL
|
||||
`);
|
||||
|
||||
await queryInterface.sequelize.query(`
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "variable_attributes_station_id_type_variable_id"
|
||||
ON "VariableAttributes" ("stationId", "type", "variableId")
|
||||
WHERE "componentId" IS NULL
|
||||
`);
|
||||
|
||||
await queryInterface.sequelize.query(`
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "variable_attributes_station_id_type_component_id"
|
||||
ON "VariableAttributes" ("stationId", "type", "componentId")
|
||||
WHERE "variableId" IS NULL
|
||||
`);
|
||||
|
||||
await queryInterface.sequelize.query(`
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "variable_attributes_station_id_variable_id_component_id"
|
||||
ON "VariableAttributes" ("stationId", "variableId", "componentId")
|
||||
WHERE "type" IS NULL
|
||||
`);
|
||||
|
||||
console.log('Successfully restored original stationId-based partial indexes.');
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,50 @@
|
||||
// SPDX-FileCopyrightText: 2026 Contributors to the CitrineOS Project
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
'use strict';
|
||||
|
||||
/** @type {import('sequelize-cli').Migration} */
|
||||
import { QueryInterface } from 'sequelize';
|
||||
|
||||
// The initial migration created a globally unique constraint on correlationId in
|
||||
// SetNetworkProfiles, preventing different tenants from reusing the same correlation
|
||||
// ID value. stationPkId already encodes tenant (it is a FK to ChargingStations(pkId),
|
||||
// which is globally unique per station-tenant pair), so a composite unique on
|
||||
// (stationPkId, correlationId) is sufficient to enforce per-station uniqueness.
|
||||
export default {
|
||||
up: async (queryInterface: QueryInterface) => {
|
||||
console.log('Dropping global unique constraint on SetNetworkProfiles.correlationId...');
|
||||
|
||||
// Drop both the inline constraint name and the explicitly-named index to cover
|
||||
// whichever form exists in this environment.
|
||||
await queryInterface.sequelize.query(
|
||||
`ALTER TABLE "SetNetworkProfiles" DROP CONSTRAINT IF EXISTS "SetNetworkProfiles_correlationId_key"`,
|
||||
);
|
||||
await queryInterface.sequelize.query(
|
||||
`DROP INDEX IF EXISTS "set_network_profiles_correlation_id"`,
|
||||
);
|
||||
|
||||
console.log('Creating per-station unique index on (stationPkId, correlationId)...');
|
||||
await queryInterface.sequelize.query(`
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "set_network_profiles_stationPkId_correlationId"
|
||||
ON "SetNetworkProfiles" ("stationPkId", "correlationId")
|
||||
`);
|
||||
|
||||
console.log('Successfully updated SetNetworkProfiles correlationId uniqueness constraint.');
|
||||
},
|
||||
|
||||
down: async (queryInterface: QueryInterface) => {
|
||||
console.log('Dropping per-station correlationId index from SetNetworkProfiles...');
|
||||
|
||||
await queryInterface.sequelize.query(
|
||||
`DROP INDEX IF EXISTS "set_network_profiles_stationPkId_correlationId"`,
|
||||
);
|
||||
|
||||
console.log('Restoring global unique constraint on correlationId...');
|
||||
await queryInterface.sequelize.query(
|
||||
`ALTER TABLE "SetNetworkProfiles" ADD CONSTRAINT "SetNetworkProfiles_correlationId_key" UNIQUE ("correlationId")`,
|
||||
);
|
||||
|
||||
console.log('Successfully restored original SetNetworkProfiles correlationId constraint.');
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,59 @@
|
||||
// SPDX-FileCopyrightText: 2026 Contributors to the CitrineOS Project
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
'use strict';
|
||||
|
||||
/** @type {import('sequelize-cli').Migration} */
|
||||
import { QueryInterface } from 'sequelize';
|
||||
|
||||
export default {
|
||||
up: async (queryInterface: QueryInterface) => {
|
||||
console.log('Updating Certificates unique constraints to include tenantId...');
|
||||
|
||||
await queryInterface.sequelize.query(
|
||||
`ALTER TABLE "Certificates" DROP CONSTRAINT IF EXISTS "serialNumber_issuerName"`,
|
||||
);
|
||||
await queryInterface.sequelize.query(`
|
||||
ALTER TABLE "Certificates"
|
||||
ADD CONSTRAINT "tenantId_serialNumber_issuerName"
|
||||
UNIQUE ("tenantId", "serialNumber", "issuerName")
|
||||
`);
|
||||
|
||||
// The certificateFileHash constraint was created by addColumn with unique: true,
|
||||
// which auto-names the constraint as Certificates_certificateFileHash_key.
|
||||
await queryInterface.sequelize.query(
|
||||
`ALTER TABLE "Certificates" DROP CONSTRAINT IF EXISTS "Certificates_certificateFileHash_key"`,
|
||||
);
|
||||
await queryInterface.sequelize.query(`
|
||||
ALTER TABLE "Certificates"
|
||||
ADD CONSTRAINT "tenantId_certificateFileHash"
|
||||
UNIQUE ("tenantId", "certificateFileHash")
|
||||
`);
|
||||
|
||||
console.log('Successfully updated Certificates unique constraints.');
|
||||
},
|
||||
|
||||
down: async (queryInterface: QueryInterface) => {
|
||||
console.log('Reverting Certificates unique constraints...');
|
||||
|
||||
await queryInterface.sequelize.query(
|
||||
`ALTER TABLE "Certificates" DROP CONSTRAINT IF EXISTS "tenantId_serialNumber_issuerName"`,
|
||||
);
|
||||
await queryInterface.sequelize.query(`
|
||||
ALTER TABLE "Certificates"
|
||||
ADD CONSTRAINT "serialNumber_issuerName"
|
||||
UNIQUE ("serialNumber", "issuerName")
|
||||
`);
|
||||
|
||||
await queryInterface.sequelize.query(
|
||||
`ALTER TABLE "Certificates" DROP CONSTRAINT IF EXISTS "tenantId_certificateFileHash"`,
|
||||
);
|
||||
await queryInterface.sequelize.query(`
|
||||
ALTER TABLE "Certificates"
|
||||
ADD CONSTRAINT "Certificates_certificateFileHash_key"
|
||||
UNIQUE ("certificateFileHash")
|
||||
`);
|
||||
|
||||
console.log('Successfully reverted Certificates unique constraints.');
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,126 @@
|
||||
// SPDX-FileCopyrightText: 2025 Contributors to the CitrineOS Project
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
'use strict';
|
||||
|
||||
/** @type {import('sequelize-cli').Migration} */
|
||||
import { QueryInterface } from 'sequelize';
|
||||
|
||||
// EvseTypes: the schema-fix migration created this table without any unique
|
||||
// constraints. The model defines (id, connectorId) uniqueness, but it was never
|
||||
// applied via migration and lacks tenantId.
|
||||
//
|
||||
// Components / Variables: the initial migration created inline UNIQUE (name, instance)
|
||||
// constraints and partial indexes on (name) WHERE instance IS NULL, both without
|
||||
// tenantId, preventing different tenants from sharing the same component or variable names.
|
||||
//
|
||||
// This migration adds tenantId to all three tables' unique constraints.
|
||||
export default {
|
||||
up: async (queryInterface: QueryInterface) => {
|
||||
// ── EvseTypes ──────────────────────────────────────────────────────────────
|
||||
console.log('Adding tenantId-inclusive unique constraints to EvseTypes...');
|
||||
|
||||
await queryInterface.sequelize.query(`
|
||||
DO $$ BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'evse_types_tenantId_id_connectorId' AND conrelid = '"EvseTypes"'::regclass) THEN
|
||||
ALTER TABLE "EvseTypes" ADD CONSTRAINT "evse_types_tenantId_id_connectorId" UNIQUE ("tenantId", "id", "connectorId");
|
||||
END IF;
|
||||
END $$
|
||||
`);
|
||||
|
||||
await queryInterface.sequelize.query(`
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "evse_types_tenantId_id"
|
||||
ON "EvseTypes" ("tenantId", "id")
|
||||
WHERE "connectorId" IS NULL
|
||||
`);
|
||||
|
||||
// ── Components ─────────────────────────────────────────────────────────────
|
||||
console.log('Replacing Components unique constraints with tenantId-inclusive versions...');
|
||||
|
||||
// The inline UNIQUE (name, instance) is auto-named by PostgreSQL.
|
||||
await queryInterface.sequelize.query(
|
||||
`ALTER TABLE "Components" DROP CONSTRAINT IF EXISTS "Components_name_instance_key"`,
|
||||
);
|
||||
await queryInterface.sequelize.query(`DROP INDEX IF EXISTS "components_name"`);
|
||||
// Drop the misnamed constraint left behind by a failed prior run.
|
||||
await queryInterface.sequelize.query(
|
||||
`ALTER TABLE "Components" DROP CONSTRAINT IF EXISTS "tenantId_name_instance"`,
|
||||
);
|
||||
|
||||
await queryInterface.sequelize.query(`
|
||||
DO $$ BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'components_tenantId_name_instance' AND conrelid = '"Components"'::regclass) THEN
|
||||
ALTER TABLE "Components" ADD CONSTRAINT "components_tenantId_name_instance" UNIQUE ("tenantId", "name", "instance");
|
||||
END IF;
|
||||
END $$
|
||||
`);
|
||||
await queryInterface.sequelize.query(`
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "components_tenantId_name"
|
||||
ON "Components" ("tenantId", "name")
|
||||
WHERE "instance" IS NULL
|
||||
`);
|
||||
|
||||
// ── Variables ──────────────────────────────────────────────────────────────
|
||||
console.log('Replacing Variables unique constraints with tenantId-inclusive versions...');
|
||||
|
||||
await queryInterface.sequelize.query(
|
||||
`ALTER TABLE "Variables" DROP CONSTRAINT IF EXISTS "Variables_name_instance_key"`,
|
||||
);
|
||||
await queryInterface.sequelize.query(`DROP INDEX IF EXISTS "variables_name"`);
|
||||
|
||||
await queryInterface.sequelize.query(`
|
||||
DO $$ BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'variables_tenantId_name_instance' AND conrelid = '"Variables"'::regclass) THEN
|
||||
ALTER TABLE "Variables" ADD CONSTRAINT "variables_tenantId_name_instance" UNIQUE ("tenantId", "name", "instance");
|
||||
END IF;
|
||||
END $$
|
||||
`);
|
||||
await queryInterface.sequelize.query(`
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "variables_tenantId_name"
|
||||
ON "Variables" ("tenantId", "name")
|
||||
WHERE "instance" IS NULL
|
||||
`);
|
||||
|
||||
console.log('Successfully updated EvseTypes, Components, and Variables unique constraints.');
|
||||
},
|
||||
|
||||
down: async (queryInterface: QueryInterface) => {
|
||||
// ── EvseTypes ──────────────────────────────────────────────────────────────
|
||||
await queryInterface.sequelize.query(
|
||||
`ALTER TABLE "EvseTypes" DROP CONSTRAINT IF EXISTS "evse_types_tenantId_id_connectorId"`,
|
||||
);
|
||||
await queryInterface.sequelize.query(`DROP INDEX IF EXISTS "evse_types_tenantId_id"`);
|
||||
|
||||
// ── Components ─────────────────────────────────────────────────────────────
|
||||
await queryInterface.sequelize.query(
|
||||
`ALTER TABLE "Components" DROP CONSTRAINT IF EXISTS "components_tenantId_name_instance"`,
|
||||
);
|
||||
await queryInterface.sequelize.query(`DROP INDEX IF EXISTS "components_tenantId_name"`);
|
||||
|
||||
await queryInterface.sequelize.query(
|
||||
`ALTER TABLE "Components" ADD CONSTRAINT "Components_name_instance_key" UNIQUE ("name", "instance")`,
|
||||
);
|
||||
await queryInterface.sequelize.query(`
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "components_name"
|
||||
ON "Components" ("name")
|
||||
WHERE "instance" IS NULL
|
||||
`);
|
||||
|
||||
// ── Variables ──────────────────────────────────────────────────────────────
|
||||
await queryInterface.sequelize.query(
|
||||
`ALTER TABLE "Variables" DROP CONSTRAINT IF EXISTS "variables_tenantId_name_instance"`,
|
||||
);
|
||||
await queryInterface.sequelize.query(`DROP INDEX IF EXISTS "variables_tenantId_name"`);
|
||||
|
||||
await queryInterface.sequelize.query(
|
||||
`ALTER TABLE "Variables" ADD CONSTRAINT "Variables_name_instance_key" UNIQUE ("name", "instance")`,
|
||||
);
|
||||
await queryInterface.sequelize.query(`
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "variables_name"
|
||||
ON "Variables" ("name")
|
||||
WHERE "instance" IS NULL
|
||||
`);
|
||||
|
||||
console.log('Successfully reverted EvseTypes, Components, and Variables unique constraints.');
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,70 @@
|
||||
// SPDX-FileCopyrightText: 2026 Contributors to the CitrineOS Project
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
'use strict';
|
||||
|
||||
/** @type {import('sequelize-cli').Migration} */
|
||||
import { DataTypes, QueryInterface } from 'sequelize';
|
||||
|
||||
export default {
|
||||
up: async (queryInterface: QueryInterface) => {
|
||||
await queryInterface.addColumn('Tariffs', 'tariffId', {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
});
|
||||
await queryInterface.addColumn('Tariffs', 'validFrom', {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: true,
|
||||
});
|
||||
await queryInterface.addColumn('Tariffs', 'description', {
|
||||
type: DataTypes.JSONB,
|
||||
allowNull: true,
|
||||
});
|
||||
await queryInterface.addColumn('Tariffs', 'energy', {
|
||||
type: DataTypes.JSONB,
|
||||
allowNull: true,
|
||||
});
|
||||
await queryInterface.addColumn('Tariffs', 'chargingTime', {
|
||||
type: DataTypes.JSONB,
|
||||
allowNull: true,
|
||||
});
|
||||
await queryInterface.addColumn('Tariffs', 'idleTime', {
|
||||
type: DataTypes.JSONB,
|
||||
allowNull: true,
|
||||
});
|
||||
await queryInterface.addColumn('Tariffs', 'fixedFee', {
|
||||
type: DataTypes.JSONB,
|
||||
allowNull: true,
|
||||
});
|
||||
await queryInterface.addColumn('Tariffs', 'reservationTime', {
|
||||
type: DataTypes.JSONB,
|
||||
allowNull: true,
|
||||
});
|
||||
await queryInterface.addColumn('Tariffs', 'reservationFixed', {
|
||||
type: DataTypes.JSONB,
|
||||
allowNull: true,
|
||||
});
|
||||
await queryInterface.addColumn('Tariffs', 'minCost', {
|
||||
type: DataTypes.JSONB,
|
||||
allowNull: true,
|
||||
});
|
||||
await queryInterface.addColumn('Tariffs', 'maxCost', {
|
||||
type: DataTypes.JSONB,
|
||||
allowNull: true,
|
||||
});
|
||||
},
|
||||
|
||||
down: async (queryInterface: QueryInterface) => {
|
||||
await queryInterface.removeColumn('Tariffs', 'tariffId');
|
||||
await queryInterface.removeColumn('Tariffs', 'validFrom');
|
||||
await queryInterface.removeColumn('Tariffs', 'description');
|
||||
await queryInterface.removeColumn('Tariffs', 'energy');
|
||||
await queryInterface.removeColumn('Tariffs', 'chargingTime');
|
||||
await queryInterface.removeColumn('Tariffs', 'idleTime');
|
||||
await queryInterface.removeColumn('Tariffs', 'fixedFee');
|
||||
await queryInterface.removeColumn('Tariffs', 'reservationTime');
|
||||
await queryInterface.removeColumn('Tariffs', 'reservationFixed');
|
||||
await queryInterface.removeColumn('Tariffs', 'minCost');
|
||||
await queryInterface.removeColumn('Tariffs', 'maxCost');
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
// SPDX-FileCopyrightText: 2026 Contributors to the CitrineOS Project
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
'use strict';
|
||||
|
||||
/** @type {import('sequelize-cli').Migration} */
|
||||
import { DataTypes, QueryInterface } from 'sequelize';
|
||||
|
||||
export default {
|
||||
up: async (queryInterface: QueryInterface) => {
|
||||
await queryInterface.addColumn('Authorizations', 'tariffId', {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
references: {
|
||||
model: 'Tariffs',
|
||||
key: 'id',
|
||||
},
|
||||
onUpdate: 'CASCADE',
|
||||
onDelete: 'SET NULL',
|
||||
});
|
||||
},
|
||||
|
||||
down: async (queryInterface: QueryInterface) => {
|
||||
await queryInterface.removeColumn('Authorizations', 'tariffId');
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
// SPDX-FileCopyrightText: 2026 Contributors to the CitrineOS Project
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
'use strict';
|
||||
|
||||
/** @type {import('sequelize-cli').Migration} */
|
||||
import { QueryInterface } from 'sequelize';
|
||||
|
||||
export default {
|
||||
up: async (queryInterface: QueryInterface) => {
|
||||
await queryInterface.addConstraint('Tariffs', {
|
||||
fields: ['tariffId', 'tenantId'],
|
||||
type: 'unique',
|
||||
name: 'tariffId_tenantId',
|
||||
});
|
||||
},
|
||||
|
||||
down: async (queryInterface: QueryInterface) => {
|
||||
await queryInterface.removeConstraint('Tariffs', 'tariffId_tenantId');
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
// SPDX-FileCopyrightText: 2026 Contributors to the CitrineOS Project
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
'use strict';
|
||||
|
||||
/** @type {import('sequelize-cli').Migration} */
|
||||
import { DataTypes, QueryInterface } from 'sequelize';
|
||||
|
||||
export default {
|
||||
up: async (queryInterface: QueryInterface) => {
|
||||
await queryInterface.addColumn('Authorizations', 'isPrepaid', {
|
||||
type: DataTypes.BOOLEAN,
|
||||
allowNull: false,
|
||||
defaultValue: false,
|
||||
});
|
||||
await queryInterface.addColumn('Authorizations', 'prepaidBalance', {
|
||||
type: DataTypes.DECIMAL,
|
||||
allowNull: true,
|
||||
});
|
||||
},
|
||||
|
||||
down: async (queryInterface: QueryInterface) => {
|
||||
await queryInterface.removeColumn('Authorizations', 'isPrepaid');
|
||||
await queryInterface.removeColumn('Authorizations', 'prepaidBalance');
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,66 @@
|
||||
// SPDX-FileCopyrightText: 2026 Contributors to the CitrineOS Project
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
'use strict';
|
||||
|
||||
/** @type {import('sequelize-cli').Migration} */
|
||||
import { QueryInterface, QueryTypes } from 'sequelize';
|
||||
|
||||
export default {
|
||||
up: async (queryInterface: QueryInterface) => {
|
||||
console.log('Fixing Certificates.signedBy column type from VARCHAR to INTEGER...');
|
||||
|
||||
await queryInterface.sequelize.transaction(async (transaction) => {
|
||||
const q = (sql: string) =>
|
||||
queryInterface.sequelize.query(sql, { transaction, type: QueryTypes.RAW });
|
||||
|
||||
// Drop the self-referencing FK before altering the column type.
|
||||
// PostgreSQL cannot change the type of a column that is part of a FK constraint.
|
||||
await q(`ALTER TABLE "Certificates" DROP CONSTRAINT IF EXISTS "Certificates_signedBy_fkey"`);
|
||||
|
||||
// Cast the existing values to INTEGER. Any existing rows with a non-numeric
|
||||
// signedBy value will raise an error here, which is the correct behaviour
|
||||
// (it would indicate pre-existing data corruption).
|
||||
await q(`
|
||||
ALTER TABLE "Certificates"
|
||||
ALTER COLUMN "signedBy" TYPE INTEGER
|
||||
USING "signedBy"::INTEGER
|
||||
`);
|
||||
|
||||
// Re-add the self-referencing FK now that both sides are INTEGER.
|
||||
await q(`
|
||||
ALTER TABLE "Certificates"
|
||||
ADD CONSTRAINT "Certificates_signedBy_fkey"
|
||||
FOREIGN KEY ("signedBy")
|
||||
REFERENCES "Certificates" ("id")
|
||||
ON UPDATE CASCADE
|
||||
ON DELETE NO ACTION
|
||||
`);
|
||||
});
|
||||
|
||||
console.log('Successfully fixed Certificates.signedBy column type.');
|
||||
},
|
||||
|
||||
down: async (queryInterface: QueryInterface) => {
|
||||
console.log('Reverting Certificates.signedBy column type from INTEGER to VARCHAR...');
|
||||
|
||||
await queryInterface.sequelize.transaction(async (transaction) => {
|
||||
const q = (sql: string) =>
|
||||
queryInterface.sequelize.query(sql, { transaction, type: QueryTypes.RAW });
|
||||
|
||||
await q(`ALTER TABLE "Certificates" DROP CONSTRAINT IF EXISTS "Certificates_signedBy_fkey"`);
|
||||
|
||||
await q(`
|
||||
ALTER TABLE "Certificates"
|
||||
ALTER COLUMN "signedBy" TYPE VARCHAR
|
||||
USING "signedBy"::VARCHAR
|
||||
`);
|
||||
|
||||
// Note: the original broken schema had a VARCHAR FK referencing an INTEGER id,
|
||||
// which is why this constraint could never be created. The down migration
|
||||
// reverts the column type only; the broken FK is intentionally not restored.
|
||||
});
|
||||
|
||||
console.log('Successfully reverted Certificates.signedBy column type.');
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,593 @@
|
||||
// SPDX-FileCopyrightText: 2026 Contributors to the CitrineOS Project
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
'use strict';
|
||||
|
||||
import { QueryInterface, QueryTypes } from 'sequelize';
|
||||
|
||||
const primaryKeyTables = [
|
||||
'Evses',
|
||||
'Connectors',
|
||||
'Transactions',
|
||||
'ChargingStationNetworkProfiles',
|
||||
'ChargingStationSequences',
|
||||
'StatusNotifications',
|
||||
'LatestStatusNotifications',
|
||||
'VariableAttributes',
|
||||
'SetNetworkProfiles',
|
||||
'OCPPMessages',
|
||||
'InstalledCertificates',
|
||||
'ChargingStationSecurityInfos',
|
||||
'VariableMonitorings',
|
||||
'EventData',
|
||||
'InstallCertificateAttempts',
|
||||
'DeleteCertificateAttempts',
|
||||
];
|
||||
|
||||
const ocppConnectionTables = [
|
||||
'ChangeConfigurations',
|
||||
'ChargingProfiles',
|
||||
'ChargingSchedules',
|
||||
'CompositeSchedules',
|
||||
'LocalListVersions',
|
||||
'SendLocalLists',
|
||||
'MessageInfos',
|
||||
'Reservations',
|
||||
'SecurityEvents',
|
||||
'Subscriptions',
|
||||
'StartTransactions',
|
||||
'StopTransactions',
|
||||
'TransactionEvents',
|
||||
];
|
||||
|
||||
/** @type {import('sequelize-cli').Migration} */
|
||||
export default {
|
||||
up: async (queryInterface: QueryInterface) => {
|
||||
const renameConstraint = async (table: string, oldName: string, newName: string) => {
|
||||
const rows = await queryInterface.sequelize.query(
|
||||
`SELECT COUNT(*) AS count FROM information_schema.table_constraints
|
||||
WHERE table_schema = 'public' AND table_name = :table AND constraint_name = :name`,
|
||||
{ replacements: { table, name: oldName }, type: QueryTypes.SELECT },
|
||||
);
|
||||
if ((rows as any)[0].count > 0) {
|
||||
await queryInterface.sequelize.query(
|
||||
`ALTER TABLE "${table}" RENAME CONSTRAINT "${oldName}" TO "${newName}"`,
|
||||
{ type: QueryTypes.RAW },
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const renameIndex = async (oldName: string, newName: string) => {
|
||||
const rows = await queryInterface.sequelize.query(
|
||||
`SELECT COUNT(*) AS count FROM pg_indexes
|
||||
WHERE schemaname = 'public' AND indexname = :name`,
|
||||
{ replacements: { name: oldName }, type: QueryTypes.SELECT },
|
||||
);
|
||||
if ((rows as any)[0].count > 0) {
|
||||
await queryInterface.sequelize.query(`ALTER INDEX "${oldName}" RENAME TO "${newName}"`, {
|
||||
type: QueryTypes.RAW,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const renameColumn = async (table: string, oldName: string, newName: string) => {
|
||||
await queryInterface.sequelize.query(
|
||||
`DO $$ BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema = 'public' AND table_name = '${table}' AND column_name = '${newName}') THEN
|
||||
ALTER TABLE "${table}" RENAME COLUMN "${oldName}" TO "${newName}";
|
||||
END IF;
|
||||
END $$;`,
|
||||
{ type: QueryTypes.RAW },
|
||||
);
|
||||
};
|
||||
|
||||
// Drop all triggers that reference old column names
|
||||
for (const tableName of primaryKeyTables) {
|
||||
const triggerName = `trigger_populate_${tableName.toLowerCase()}_station_pk_id`;
|
||||
await queryInterface.sequelize.query(
|
||||
`DROP TRIGGER IF EXISTS "${triggerName}" ON "${tableName}"`,
|
||||
{ type: QueryTypes.RAW },
|
||||
);
|
||||
}
|
||||
await queryInterface.sequelize.query(`DROP FUNCTION IF EXISTS populate_station_pk_id()`);
|
||||
|
||||
// Rename columns
|
||||
await renameColumn('ChargingStations', 'id', 'ocppConnectionName');
|
||||
await renameColumn('ChargingStations', 'pkId', 'id');
|
||||
await queryInterface.sequelize.query(
|
||||
`ALTER SEQUENCE IF EXISTS "ChargingStations_pkId_seq" RENAME TO "ChargingStations_id_seq"`,
|
||||
{ type: QueryTypes.RAW },
|
||||
);
|
||||
await queryInterface.sequelize.query(
|
||||
`ALTER TABLE "ChargingStations" ALTER COLUMN "id" SET DEFAULT nextval('"ChargingStations_id_seq"')`,
|
||||
{ type: QueryTypes.RAW },
|
||||
);
|
||||
|
||||
// Child tables: "stationId" (string) → "ocppConnectionName", then "stationPkId" (int FK and PK for ChargingStation) → "stationId"
|
||||
for (const tableName of primaryKeyTables) {
|
||||
await renameColumn(tableName, 'stationId', 'ocppConnectionName');
|
||||
await renameColumn(tableName, 'stationPkId', 'stationId');
|
||||
}
|
||||
|
||||
// Tables with only the string station identifier (no int FK): "stationId" → "ocppConnectionName"
|
||||
for (const tableName of ocppConnectionTables) {
|
||||
await renameColumn(tableName, 'stationId', 'ocppConnectionName');
|
||||
}
|
||||
|
||||
// Rename constraints and indexes
|
||||
await renameConstraint(
|
||||
'ChargingStations',
|
||||
'ChargingStations_id_tenantId_key',
|
||||
'ChargingStations_stationName_tenantId_key',
|
||||
);
|
||||
|
||||
const fkRenames: [string, string, string][] = [
|
||||
['Evses', 'Evses_stationPkId_fkey', 'Evses_stationId_fkey'],
|
||||
['Connectors', 'Connectors_stationPkId_fkey', 'Connectors_stationId_fkey'],
|
||||
['Transactions', 'Transactions_stationPkId_fkey', 'Transactions_stationId_fkey'],
|
||||
[
|
||||
'ChargingStationNetworkProfiles',
|
||||
'ChargingStationNetworkProfiles_stationPkId_fkey',
|
||||
'ChargingStationNetworkProfiles_stationId_fkey',
|
||||
],
|
||||
[
|
||||
'ChargingStationSequences',
|
||||
'ChargingStationSequences_stationPkId_fkey',
|
||||
'ChargingStationSequences_stationId_fkey',
|
||||
],
|
||||
[
|
||||
'StatusNotifications',
|
||||
'StatusNotifications_stationPkId_fkey',
|
||||
'StatusNotifications_stationId_fkey',
|
||||
],
|
||||
[
|
||||
'LatestStatusNotifications',
|
||||
'LatestStatusNotifications_stationPkId_fkey',
|
||||
'LatestStatusNotifications_stationId_fkey',
|
||||
],
|
||||
[
|
||||
'VariableAttributes',
|
||||
'VariableAttributes_stationPkId_fkey',
|
||||
'VariableAttributes_stationId_fkey',
|
||||
],
|
||||
[
|
||||
'SetNetworkProfiles',
|
||||
'SetNetworkProfiles_stationPkId_fkey',
|
||||
'SetNetworkProfiles_stationId_fkey',
|
||||
],
|
||||
['OCPPMessages', 'OCPPMessages_stationPkId_fkey', 'OCPPMessages_stationId_fkey'],
|
||||
[
|
||||
'InstalledCertificates',
|
||||
'InstalledCertificates_stationPkId_fkey',
|
||||
'InstalledCertificates_stationId_fkey',
|
||||
],
|
||||
['EventData', 'EventData_stationPkId_fkey', 'EventData_stationId_fkey'],
|
||||
[
|
||||
'VariableMonitorings',
|
||||
'VariableMonitorings_stationPkId_fkey',
|
||||
'VariableMonitorings_stationId_fkey',
|
||||
],
|
||||
[
|
||||
'InstallCertificateAttempts',
|
||||
'InstallCertificateAttempts_stationPkId_fkey',
|
||||
'InstallCertificateAttempts_stationId_fkey',
|
||||
],
|
||||
[
|
||||
'DeleteCertificateAttempts',
|
||||
'DeleteCertificateAttempts_stationPkId_fkey',
|
||||
'DeleteCertificateAttempts_stationId_fkey',
|
||||
],
|
||||
[
|
||||
'ChargingStationSecurityInfos',
|
||||
'ChargingStationSecurityInfos_stationPkId_fkey',
|
||||
'ChargingStationSecurityInfos_stationId_fkey',
|
||||
],
|
||||
];
|
||||
for (const [table, oldName, newName] of fkRenames) {
|
||||
await renameConstraint(table, oldName, newName);
|
||||
}
|
||||
|
||||
// Unique constraints where stationPkId was the int FK column
|
||||
await renameConstraint('Evses', 'stationPkId_evseTypeId', 'stationId_evseTypeId');
|
||||
await renameConstraint('Connectors', 'stationPkId_connectorId', 'stationId_connectorId');
|
||||
await renameConstraint('Transactions', 'stationPkId_transactionId', 'stationId_transactionId');
|
||||
await renameConstraint(
|
||||
'ChargingStationNetworkProfiles',
|
||||
'stationPkId_configurationSlot',
|
||||
'stationId_configurationSlot',
|
||||
);
|
||||
await renameConstraint(
|
||||
'ChargingStationNetworkProfiles',
|
||||
'CSNP_stationPkId_websocketServerConfigId_key',
|
||||
'CSNP_stationId_websocketServerConfigId_key',
|
||||
);
|
||||
await renameConstraint('ChargingStationSequences', 'stationPkId_type', 'stationId_type');
|
||||
await renameConstraint(
|
||||
'VariableAttributes',
|
||||
'stationPkId_type_variableId_componentId',
|
||||
'stationId_type_variableId_componentId',
|
||||
);
|
||||
|
||||
await renameConstraint(
|
||||
'SetNetworkProfiles',
|
||||
'stationPkId_correlationId',
|
||||
'stationId_correlationId',
|
||||
);
|
||||
|
||||
await renameConstraint(
|
||||
'ChargingStationSecurityInfos',
|
||||
'ChargingStationSecurityInfos_stationId_tenantId',
|
||||
'ChargingStationSecurityInfos_stationName_tenantId',
|
||||
);
|
||||
await renameConstraint(
|
||||
'EventData',
|
||||
'EventData_stationId_tenantId_eventId',
|
||||
'EventData_stationName_tenantId_eventId',
|
||||
);
|
||||
await renameConstraint(
|
||||
'EventData',
|
||||
'EventData_stationId_eventId',
|
||||
'EventData_stationName_eventId',
|
||||
);
|
||||
await renameConstraint(
|
||||
'VariableMonitorings',
|
||||
'VariableMonitorings_stationId_tenantId_id',
|
||||
'VariableMonitorings_stationName_tenantId_id',
|
||||
);
|
||||
|
||||
await renameIndex('variable_attributes_stationPkId', 'variable_attributes_stationId');
|
||||
await renameIndex('variable_attributes_stationPkId_type', 'variable_attributes_stationId_type');
|
||||
await renameIndex(
|
||||
'variable_attributes_stationPkId_variableId',
|
||||
'variable_attributes_stationId_variableId',
|
||||
);
|
||||
await renameIndex(
|
||||
'variable_attributes_stationPkId_componentId',
|
||||
'variable_attributes_stationId_componentId',
|
||||
);
|
||||
await renameIndex(
|
||||
'variable_attributes_stationPkId_type_variableId',
|
||||
'variable_attributes_stationId_type_variableId',
|
||||
);
|
||||
await renameIndex(
|
||||
'variable_attributes_stationPkId_type_componentId',
|
||||
'variable_attributes_stationId_type_componentId',
|
||||
);
|
||||
await renameIndex(
|
||||
'variable_attributes_stationPkId_variableId_componentId',
|
||||
'variable_attributes_stationId_variableId_componentId',
|
||||
);
|
||||
|
||||
// Recreate trigger function with new column names
|
||||
await queryInterface.sequelize.query(`
|
||||
CREATE OR REPLACE FUNCTION populate_station_id()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
SELECT "id" INTO NEW."stationId"
|
||||
FROM "ChargingStations"
|
||||
WHERE "ocppConnectionName" = NEW."ocppConnectionName" AND "tenantId" = NEW."tenantId";
|
||||
|
||||
IF NEW."stationId" IS NULL THEN
|
||||
RAISE EXCEPTION 'No ChargingStation found with ocppConnectionName=% and tenantId=%',
|
||||
NEW."ocppConnectionName", NEW."tenantId";
|
||||
END IF;
|
||||
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
`);
|
||||
|
||||
for (const tableName of primaryKeyTables) {
|
||||
const triggerName = `trigger_populate_${tableName.toLowerCase()}_station_id`;
|
||||
await queryInterface.sequelize.query(
|
||||
`DROP TRIGGER IF EXISTS "${triggerName}" ON "${tableName}"`,
|
||||
);
|
||||
await queryInterface.sequelize.query(`
|
||||
CREATE TRIGGER "${triggerName}"
|
||||
BEFORE INSERT OR UPDATE ON "${tableName}"
|
||||
FOR EACH ROW
|
||||
WHEN (NEW."stationId" IS NULL)
|
||||
EXECUTE FUNCTION populate_station_id()
|
||||
`);
|
||||
}
|
||||
|
||||
console.log('Migration 20260427000000-rename-charging-station-columns completed successfully.');
|
||||
},
|
||||
|
||||
down: async (queryInterface: QueryInterface) => {
|
||||
await queryInterface.sequelize.transaction(async (transaction) => {
|
||||
const qSelect = <T = any>(sql: string, replacements?: Record<string, any>): Promise<T[]> =>
|
||||
queryInterface.sequelize.query(sql, {
|
||||
transaction,
|
||||
replacements,
|
||||
type: QueryTypes.SELECT,
|
||||
}) as Promise<T[]>;
|
||||
|
||||
const renameConstraint = async (table: string, oldName: string, newName: string) => {
|
||||
const [row] = await qSelect<{ count: string }>(
|
||||
`SELECT COUNT(*) AS count FROM information_schema.table_constraints
|
||||
WHERE table_schema = 'public' AND table_name = :table AND constraint_name = :name`,
|
||||
{ table, name: oldName },
|
||||
);
|
||||
if (parseInt((row as any).count, 10) > 0) {
|
||||
await queryInterface.sequelize.query(
|
||||
`ALTER TABLE "${table}" RENAME CONSTRAINT "${oldName}" TO "${newName}"`,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const renameIndex = async (oldName: string, newName: string) => {
|
||||
const [row] = await qSelect<{ count: string }>(
|
||||
`SELECT COUNT(*) AS count FROM pg_indexes
|
||||
WHERE schemaname = 'public' AND indexname = :name`,
|
||||
{ name: oldName },
|
||||
);
|
||||
if (parseInt((row as any).count, 10) > 0) {
|
||||
await queryInterface.sequelize.query(`ALTER INDEX "${oldName}" RENAME TO "${newName}"`);
|
||||
}
|
||||
};
|
||||
|
||||
const renameColumn = async (table: string, oldName: string, newName: string) => {
|
||||
await queryInterface.sequelize.query(
|
||||
`DO $$ BEGIN
|
||||
IF EXISTS (SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema = 'public' AND table_name = '${table}' AND column_name = '${oldName}') THEN
|
||||
ALTER TABLE "${table}" RENAME COLUMN "${oldName}" TO "${newName}";
|
||||
END IF;
|
||||
END $$;`,
|
||||
{ transaction, type: QueryTypes.RAW },
|
||||
);
|
||||
};
|
||||
|
||||
const primaryKeyTables = [
|
||||
'Evses',
|
||||
'Connectors',
|
||||
'Transactions',
|
||||
'ChargingStationNetworkProfiles',
|
||||
'ChargingStationSequences',
|
||||
'StatusNotifications',
|
||||
'LatestStatusNotifications',
|
||||
'VariableAttributes',
|
||||
'SetNetworkProfiles',
|
||||
'OCPPMessages',
|
||||
'InstalledCertificates',
|
||||
'ChargingStationSecurityInfos',
|
||||
'VariableMonitorings',
|
||||
'EventData',
|
||||
'InstallCertificateAttempts',
|
||||
'DeleteCertificateAttempts',
|
||||
];
|
||||
|
||||
// Drop triggers with new names
|
||||
for (const tableName of primaryKeyTables) {
|
||||
const triggerName = `trigger_populate_${tableName.toLowerCase()}_station_id`;
|
||||
await queryInterface.sequelize.query(
|
||||
`DROP TRIGGER IF EXISTS "${triggerName}" ON "${tableName}"`,
|
||||
);
|
||||
}
|
||||
await queryInterface.sequelize.query(`DROP FUNCTION IF EXISTS populate_station_id()`);
|
||||
|
||||
// Rename columns back
|
||||
await renameColumn('ChargingStations', 'id', 'pkId');
|
||||
await renameColumn('ChargingStations', 'ocppConnectionName', 'id');
|
||||
await queryInterface.sequelize.query(
|
||||
`ALTER SEQUENCE IF EXISTS "ChargingStations_id_seq" RENAME TO "ChargingStations_pkId_seq"`,
|
||||
);
|
||||
await queryInterface.sequelize.query(
|
||||
`ALTER TABLE "ChargingStations" ALTER COLUMN "pkId" SET DEFAULT nextval('"ChargingStations_pkId_seq"')`,
|
||||
);
|
||||
|
||||
for (const tableName of primaryKeyTables) {
|
||||
await renameColumn(tableName, 'stationId', 'stationPkId');
|
||||
await renameColumn(tableName, 'ocppConnectionName', 'stationId');
|
||||
}
|
||||
|
||||
// Tables with only the string station identifier: "ocppConnectionName" → "stationId"
|
||||
const ocppConnectionTables = [
|
||||
'ChangeConfigurations',
|
||||
'ChargingProfiles',
|
||||
'ChargingSchedules',
|
||||
'CompositeSchedules',
|
||||
'LocalListVersions',
|
||||
'SendLocalLists',
|
||||
'MessageInfos',
|
||||
'Reservations',
|
||||
'SecurityEvents',
|
||||
'Subscriptions',
|
||||
'StartTransactions',
|
||||
'StopTransactions',
|
||||
'TransactionEvents',
|
||||
];
|
||||
for (const tableName of ocppConnectionTables) {
|
||||
await renameColumn(tableName, 'ocppConnectionName', 'stationId');
|
||||
}
|
||||
|
||||
// Rename constraints and indexes back
|
||||
|
||||
await renameConstraint(
|
||||
'ChargingStations',
|
||||
'ChargingStations_stationName_tenantId_key',
|
||||
'ChargingStations_id_tenantId_key',
|
||||
);
|
||||
|
||||
const fkRenames: [string, string, string][] = [
|
||||
['Evses', 'Evses_stationId_fkey', 'Evses_stationPkId_fkey'],
|
||||
['Connectors', 'Connectors_stationId_fkey', 'Connectors_stationPkId_fkey'],
|
||||
['Transactions', 'Transactions_stationId_fkey', 'Transactions_stationPkId_fkey'],
|
||||
[
|
||||
'ChargingStationNetworkProfiles',
|
||||
'ChargingStationNetworkProfiles_stationId_fkey',
|
||||
'ChargingStationNetworkProfiles_stationPkId_fkey',
|
||||
],
|
||||
[
|
||||
'ChargingStationSequences',
|
||||
'ChargingStationSequences_stationId_fkey',
|
||||
'ChargingStationSequences_stationPkId_fkey',
|
||||
],
|
||||
[
|
||||
'StatusNotifications',
|
||||
'StatusNotifications_stationId_fkey',
|
||||
'StatusNotifications_stationPkId_fkey',
|
||||
],
|
||||
[
|
||||
'LatestStatusNotifications',
|
||||
'LatestStatusNotifications_stationId_fkey',
|
||||
'LatestStatusNotifications_stationPkId_fkey',
|
||||
],
|
||||
[
|
||||
'VariableAttributes',
|
||||
'VariableAttributes_stationId_fkey',
|
||||
'VariableAttributes_stationPkId_fkey',
|
||||
],
|
||||
[
|
||||
'SetNetworkProfiles',
|
||||
'SetNetworkProfiles_stationId_fkey',
|
||||
'SetNetworkProfiles_stationPkId_fkey',
|
||||
],
|
||||
['OCPPMessages', 'OCPPMessages_stationId_fkey', 'OCPPMessages_stationPkId_fkey'],
|
||||
[
|
||||
'InstalledCertificates',
|
||||
'InstalledCertificates_stationId_fkey',
|
||||
'InstalledCertificates_stationPkId_fkey',
|
||||
],
|
||||
['EventData', 'EventData_stationId_fkey', 'EventData_stationPkId_fkey'],
|
||||
[
|
||||
'VariableMonitorings',
|
||||
'VariableMonitorings_stationId_fkey',
|
||||
'VariableMonitorings_stationPkId_fkey',
|
||||
],
|
||||
[
|
||||
'InstallCertificateAttempts',
|
||||
'InstallCertificateAttempts_stationId_fkey',
|
||||
'InstallCertificateAttempts_stationPkId_fkey',
|
||||
],
|
||||
[
|
||||
'DeleteCertificateAttempts',
|
||||
'DeleteCertificateAttempts_stationId_fkey',
|
||||
'DeleteCertificateAttempts_stationPkId_fkey',
|
||||
],
|
||||
[
|
||||
'ChargingStationSecurityInfos',
|
||||
'ChargingStationSecurityInfos_stationId_fkey',
|
||||
'ChargingStationSecurityInfos_stationPkId_fkey',
|
||||
],
|
||||
];
|
||||
for (const [table, oldName, newName] of fkRenames) {
|
||||
await renameConstraint(table, oldName, newName);
|
||||
}
|
||||
|
||||
await renameConstraint('Evses', 'stationId_evseTypeId', 'stationPkId_evseTypeId');
|
||||
await renameConstraint('Connectors', 'stationId_connectorId', 'stationPkId_connectorId');
|
||||
await renameConstraint(
|
||||
'Transactions',
|
||||
'stationId_transactionId',
|
||||
'stationPkId_transactionId',
|
||||
);
|
||||
await renameConstraint(
|
||||
'ChargingStationNetworkProfiles',
|
||||
'stationId_configurationSlot',
|
||||
'stationPkId_configurationSlot',
|
||||
);
|
||||
await renameConstraint(
|
||||
'ChargingStationNetworkProfiles',
|
||||
'CSNP_stationId_websocketServerConfigId_key',
|
||||
'CSNP_stationPkId_websocketServerConfigId_key',
|
||||
);
|
||||
await renameConstraint('ChargingStationSequences', 'stationId_type', 'stationPkId_type');
|
||||
await renameConstraint(
|
||||
'VariableAttributes',
|
||||
'stationId_type_variableId_componentId',
|
||||
'stationPkId_type_variableId_componentId',
|
||||
);
|
||||
await renameConstraint(
|
||||
'SetNetworkProfiles',
|
||||
'stationId_correlationId',
|
||||
'stationPkId_correlationId',
|
||||
);
|
||||
await renameConstraint(
|
||||
'ChargingStationSecurityInfos',
|
||||
'ChargingStationSecurityInfos_stationName_tenantId',
|
||||
'ChargingStationSecurityInfos_stationId_tenantId',
|
||||
);
|
||||
await renameConstraint(
|
||||
'EventData',
|
||||
'EventData_stationName_tenantId_eventId',
|
||||
'EventData_stationId_tenantId_eventId',
|
||||
);
|
||||
await renameConstraint(
|
||||
'EventData',
|
||||
'EventData_stationName_eventId',
|
||||
'EventData_stationId_eventId',
|
||||
);
|
||||
await renameConstraint(
|
||||
'VariableMonitorings',
|
||||
'VariableMonitorings_stationName_tenantId_id',
|
||||
'VariableMonitorings_stationId_tenantId_id',
|
||||
);
|
||||
|
||||
await renameIndex('variable_attributes_stationId', 'variable_attributes_stationPkId');
|
||||
await renameIndex(
|
||||
'variable_attributes_stationId_type',
|
||||
'variable_attributes_stationPkId_type',
|
||||
);
|
||||
await renameIndex(
|
||||
'variable_attributes_stationId_variableId',
|
||||
'variable_attributes_stationPkId_variableId',
|
||||
);
|
||||
await renameIndex(
|
||||
'variable_attributes_stationId_componentId',
|
||||
'variable_attributes_stationPkId_componentId',
|
||||
);
|
||||
await renameIndex(
|
||||
'variable_attributes_stationId_type_variableId',
|
||||
'variable_attributes_stationPkId_type_variableId',
|
||||
);
|
||||
await renameIndex(
|
||||
'variable_attributes_stationId_type_componentId',
|
||||
'variable_attributes_stationPkId_type_componentId',
|
||||
);
|
||||
await renameIndex(
|
||||
'variable_attributes_stationId_variableId_componentId',
|
||||
'variable_attributes_stationPkId_variableId_componentId',
|
||||
);
|
||||
|
||||
// Restore original trigger function
|
||||
await queryInterface.sequelize.query(`
|
||||
CREATE OR REPLACE FUNCTION populate_station_pk_id()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
SELECT "pkId" INTO NEW."stationPkId"
|
||||
FROM "ChargingStations"
|
||||
WHERE "id" = NEW."stationId" AND "tenantId" = NEW."tenantId";
|
||||
|
||||
IF NEW."stationPkId" IS NULL THEN
|
||||
RAISE EXCEPTION 'No ChargingStation found with stationId=% and tenantId=%',
|
||||
NEW."stationId", NEW."tenantId";
|
||||
END IF;
|
||||
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
`);
|
||||
|
||||
for (const tableName of primaryKeyTables) {
|
||||
const triggerName = `trigger_populate_${tableName.toLowerCase()}_station_pk_id`;
|
||||
await queryInterface.sequelize.query(
|
||||
`DROP TRIGGER IF EXISTS "${triggerName}" ON "${tableName}"`,
|
||||
);
|
||||
await queryInterface.sequelize.query(`
|
||||
CREATE TRIGGER "${triggerName}"
|
||||
BEFORE INSERT OR UPDATE ON "${tableName}"
|
||||
FOR EACH ROW
|
||||
WHEN (NEW."stationPkId" IS NULL)
|
||||
EXECUTE FUNCTION populate_station_pk_id()
|
||||
`);
|
||||
}
|
||||
|
||||
console.log(
|
||||
'Migration 20260427000000-rename-charging-station-columns rolled back successfully.',
|
||||
);
|
||||
});
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
// SPDX-FileCopyrightText: 2026 Contributors to the CitrineOS Project
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
'use strict';
|
||||
|
||||
/** @type {import('sequelize-cli').Migration} */
|
||||
import { DataTypes, QueryInterface } from 'sequelize';
|
||||
|
||||
export default {
|
||||
up: async (queryInterface: QueryInterface) => {
|
||||
await queryInterface.addColumn('Transactions', 'transactionLimit', {
|
||||
type: DataTypes.JSONB,
|
||||
allowNull: true,
|
||||
});
|
||||
},
|
||||
|
||||
down: async (queryInterface: QueryInterface) => {
|
||||
await queryInterface.removeColumn('Transactions', 'transactionLimit');
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,15 @@
|
||||
// SPDX-FileCopyrightText: 2025 Contributors to the CitrineOS Project
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
import { DataTypes, QueryInterface } from 'sequelize';
|
||||
|
||||
export async function up(queryInterface: QueryInterface): Promise<void> {
|
||||
await queryInterface.addColumn('InstallCertificateAttempts', 'requestId', {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
});
|
||||
}
|
||||
|
||||
export async function down(queryInterface: QueryInterface): Promise<void> {
|
||||
await queryInterface.removeColumn('InstallCertificateAttempts', 'requestId');
|
||||
}
|
||||
Reference in New Issue
Block a user