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:
Eric F
2026-06-08 00:38:27 -04:00
parent 468cfeaa50
commit d398a6ced2
7326 changed files with 1177561 additions and 7 deletions

View File

@@ -0,0 +1,6 @@
const path = require('path');
module.exports = {
config: path.resolve('dist', 'config', 'sequelize.bridge.config.js'),
'migrations-path': path.resolve('dist/migrations'),
};

View File

@@ -0,0 +1,295 @@
<!--
SPDX-FileCopyrightText: 2025 Contributors to the CitrineOS Project
SPDX-License-Identifier: Apache-2.0
-->
![CitrineOS Logo](../../logo_white.png#gh-dark-mode-only)
![CitrineOS Logo](../../logo_black.png#gh-light-mode-only)
# CitrineOS Server (`@citrineos/server`)
This is the OCPP server application for CitrineOS — the runnable entrypoint that wires together
[`@citrineos/base`](../../packages/base) and [`@citrineos/core`](../../packages/core) into a deployable
service. It hosts the WebSocket endpoints that charging stations connect to, the OCPP message router, the
HTTP/REST Data and Message APIs, and the Sequelize database migrations.
It is one workspace member of the `citrineos-core` pnpm monorepo. For repository-wide setup (cloning,
`pnpm install`, building, the full-stack Docker Compose files, and the operator UI), see the
[root README](../../README.md).
## Table of Contents
- [Running the Server](#running-the-server)
- [With Docker (backend only)](#with-docker-backend-only)
- [Without Docker](#without-docker)
- [Attaching a Debugger](#attaching-a-debugger)
- [Server Ports](#server-ports)
- [Database Sync vs. Migration](#database-sync-vs-migration)
- [Runtime Configuration](#runtime-configuration)
- [Bootstrap Configuration Environment Variables](#bootstrap-configuration-environment-variables)
- [Generating OCPP Interfaces](#generating-ocpp-interfaces)
- [Validating Custom OCPP DataTransfer Messages](#validating-custom-ocpp-datatransfer-messages)
- [Allow Unknown Charging Stations & Auto-Commissioning](#allow-unknown-charging-stations--auto-commissioning)
- [Hasura Metadata](#hasura-metadata)
- [Testing with EVerest](#testing-with-everest)
## Running the Server
Make sure the workspace has been installed and built first (from the repository root: `pnpm install && pnpm run build`).
### With Docker (backend only)
`apps/Server/docker-compose.yml` brings up the server plus its supporting services — RabbitMQ, PostgreSQL, MinIO,
and Hasura — but **not** the operator UI. The server image is built from local source and the source tree is mounted
as volumes for live reload. Run it from this directory:
```shell
cd apps/Server
docker compose up -d
```
To run the full stack including the operator UI, use one of the root-level Compose files instead — see the
[root README](../../README.md#information-on-docker-setup).
### Without Docker
To start the server directly with pnpm, run from the repository root:
```shell
pnpm run start
```
Or from this directory:
```shell
cd apps/Server
pnpm run start
```
This launches the server via `nodemon` (see `nodemon.json`), which builds the workspace, runs database migrations,
and then starts the process with the Node.js inspector listening on port 9229.
CitrineOS requires configuration to allow your OCPP 1.6 and OCPP 2.0.1 compliant charging stations to connect.
To change the configuration used outside of Docker, adjust the configuration file at
`apps/Server/src/config/envs/local.ts`. Make sure any changes to the local configuration do not make it into your PR.
## Attaching a Debugger
Whether you run the application with Docker or locally with pnpm, you can attach a debugger to port 9229 and set
breakpoints in the TypeScript code directly from your IDE.
To make the process **wait for the debugger to attach** before executing, modify the `nodemon.json` exec command from:
```shell
pnpm run build --prefix ../../ && pnpm run migrate && node --inspect=0.0.0.0:9229 ./dist/index.js
```
to:
```shell
pnpm run build --prefix ../../ && pnpm run migrate && node --inspect-brk=0.0.0.0:9229 ./dist/index.js
```
## Server Ports
When running, the server container exposes the following ports (see `docker-compose.yml`):
- `8080`: webserver HTTP — [Swagger](http://localhost:8080/docs)
- `8081`: websocket server TCP connection without auth
- `8082`: websocket server TCP connection with basic HTTP auth
- `8083`: additional websocket server
- `8443` / `8444`: TLS websocket servers
- `9229`: Node.js debugger
## Database Sync vs. Migration
By default, CitrineOS uses migrations to manage database schema changes. This is the recommended approach for
production environments. The `pnpm run migrate` script (run automatically on start via `nodemon.json`) applies
Sequelize migrations.
For development purposes, you can also use `sync` to automatically synchronize your database schema with the models.
Two sync scripts are available at the repository root:
- `pnpm run sync-db`: synchronizes the database schema with the models without altering existing tables. Useful for
development when you want to quickly update the schema without losing data.
- `pnpm run force-sync-db`: drops all tables and recreates them based on the models. Useful when you want to start
with a fresh database.
**Disclaimer:** Using `sync` in a production environment is not recommended as it can lead to data loss. Always use
migrations for production deployments.
## Runtime Configuration
Values from configuration files (`local.ts`, `docker.ts`, `swarm.docker.ts`) may be overridden at runtime via
environment variables. Environment variables prefixed with `citrineos_` and hierarchically separated by an
underscore will override the corresponding value. For example, the amqp URL:
```json
util: {
(...)
messageBroker: {
amqp: {
url: 'amqp://guest:guest@localhost:5672'
(...)
}
(...)
}
(...)
}
```
may be overridden by setting the environment variable `CITRINEOS_util_messageBroker_amqp_url` (case-insensitive).
## Bootstrap Configuration Environment Variables
All environment variables use the `CITRINEOS_` prefix.
Additional prefixes can be added by passing the `--env-prefix` argument to nodemon (see `start:instance1` in
`package.json`).
Here's the complete list of environment variables used in bootstrapping the application (this is not the full system
configuration):
### Basic Bootstrap Configuration
- `BOOTSTRAP_CITRINEOS_CONFIG_FILENAME` - Name of the main config file (default: `config.json`)
- `BOOTSTRAP_CITRINEOS_CONFIG_DIR` - Directory containing the config file (optional)
- `BOOTSTRAP_CITRINEOS_FILE_ACCESS_TYPE` - Type of file access: `local`, `s3`, or `gcp`
### Database Configuration
Database connection details (moved from system config to bootstrap config for better security and 12-factor compliance):
- `BOOTSTRAP_CITRINEOS_DATABASE_HOST` - Database host (default: `localhost`)
- `BOOTSTRAP_CITRINEOS_DATABASE_PORT` - Database port (default: `5432`)
- `BOOTSTRAP_CITRINEOS_DATABASE_NAME` - Database name (default: `citrine`)
- `BOOTSTRAP_CITRINEOS_DATABASE_DIALECT` - Database dialect (default: `postgres`)
- `BOOTSTRAP_CITRINEOS_DATABASE_USERNAME` - Database username (optional)
- `BOOTSTRAP_CITRINEOS_DATABASE_PASSWORD` - Database password (optional)
- `BOOTSTRAP_CITRINEOS_DATABASE_SYNC` - Enable database sync (via sequelize) (true/false, default: `false`)
- `BOOTSTRAP_CITRINEOS_DATABASE_ALTER` - Enable database alter (via sequelize) (true/false, default: `false`)
- `BOOTSTRAP_CITRINEOS_DATABASE_MAX_RETRIES` - Maximum connection retries (default: `3`)
- `BOOTSTRAP_CITRINEOS_DATABASE_RETRY_DELAY` - Retry delay in milliseconds (default: `1000`)
### Local File Access
When `BOOTSTRAP_CITRINEOS_FILE_ACCESS_TYPE=local`:
- `BOOTSTRAP_CITRINEOS_FILE_ACCESS_LOCAL_DEFAULT_FILE_PATH` - Default file path (default: `/data`)
### S3 File Access
When `BOOTSTRAP_CITRINEOS_FILE_ACCESS_TYPE=s3`:
- `BOOTSTRAP_CITRINEOS_FILE_ACCESS_S3_REGION` - AWS region (optional)
- `BOOTSTRAP_CITRINEOS_FILE_ACCESS_S3_ENDPOINT` - S3 endpoint URL (for MinIO or custom S3)
- `BOOTSTRAP_CITRINEOS_FILE_ACCESS_S3_DEFAULT_BUCKET_NAME` - S3 bucket name (default: `citrineos-s3-bucket`)
- `BOOTSTRAP_CITRINEOS_FILE_ACCESS_S3_FORCE_PATH_STYLE` - Force path style (true/false, default: `true`)
- `BOOTSTRAP_CITRINEOS_FILE_ACCESS_S3_ACCESS_KEY_ID` - S3 access key ID
- `BOOTSTRAP_CITRINEOS_FILE_ACCESS_S3_SECRET_ACCESS_KEY` - S3 secret access key
### GCP File Access
When `BOOTSTRAP_CITRINEOS_FILE_ACCESS_TYPE=gcp`:
- `BOOTSTRAP_CITRINEOS_FILE_ACCESS_GCP_PROJECTID` - Project ID
- `BOOTSTRAP_CITRINEOS_FILE_ACCESS_GCP_CREDENTIALS` - GCP Credentials object (Optional, if not set will use Application Default Credentials such as the `GOOGLE_APPLICATION_CREDENTIALS` environment variable or gcloud CLI credentials)
## Generating OCPP Interfaces
All CitrineOS interfaces for OCPP 1.6, 2.0.1, and 2.1-defined schemas were procedurally generated using a processing
script. Schemas are sourced from official OCPP JSON files.
As of release 1.8.0, the schema files used by CitrineOS are not the raw output of this function; we have added
field-level validation that the official schemas lack.
## Validating Custom OCPP DataTransfer Messages
It is possible to add custom JSON schemas to validate the data fields of DataTransfer messages, which are supported by
all OCPP versions.
In the `apps/Server/src/index.ts` code, there is a function `ajvInstance()` that creates the AJV instance. Here, you
could register DataTransfer schemas:
```
import { MyDataTransferRequestSchema } from './path'
...
ajvInstance.compile(MyDataTransferRequestSchema);
```
Note: The schema's `$id` field must follow this format:
```
${protocol}-${dataTransferRequest.vendorId}${dataTransferRequest.messageId ? `-${dataTransferRequest.messageId}` : ''}
```
'Protocol' is the OCPP websocket subprotocol, i.e. "ocpp1.6", "ocpp2.0.1", or so on.
CitrineOS's validation logic assumes that the data field is a string field with JSON structure, and uses `JSON.parse`
before validation. Other approaches to custom DataTransfer message types are not supported.
## Allow Unknown Charging Stations & Auto-Commissioning
The System Configuration defines websocket servers with certain properties, one of which is 'Allow Unknown Charging
Stations', a boolean that permits charging stations which are not commissioned to connect to CitrineOS.
This triggers an auto-commissioning flow which creates the station on its first connection, and creates evses and
connectors for that station in response to StatusNotifications.
This is not recommended for production; it is exclusively for testing and is enabled by the default configuration only
on the websocket server at port 8081 — which also has no security.
Since not all information on the charger is necessarily available in the OCPP messages, commissioning may be wrong and
will be incomplete. In 1.6 in particular, multi-evse stations will not commission properly because 1.6 does not have a
concept of 'evses'. This will lead to improper behavior if a 1.6 station with multiple evses is auto-commissioned:
CitrineOS will assume each new transaction is on the same evse and will automatically mark older transactions on that
evse as inactive, leading to an inconsistent state with the charging station.
## Hasura Metadata
In order for Hasura to track the existing Citrine tables and relationships, this repository comes with Hasura metadata
already exported into the `apps/Server/hasura-metadata` folder.
Running the Docker container will automatically import this metadata and track all tables and relationships.
Unfortunately, Hasura doesn't currently support importing metadata from a JSON (which is the format if you export your
metadata from the Hasura UI or API).
Refer to this issue for more information: https://github.com/hasura/graphql-engine/issues/8423#issuecomment-1115996153.
Therefore, you must use the Hasura CLI to re-export your metadata, should something change with it. As explained in the
Hasura docs https://hasura.io/docs/2.0/migrations-metadata-seeds/auto-apply-migrations/#auto-apply-metadata,
Hasura provides an image called `hasura/graphql-engine:<version>.cli-migrations-v3` that will process and import the
metadata first before starting the server and runs the Hasura CLI internally. This is the image CitrineOS normally uses
in order to automatically load accurate metadata. However, if you want to capture the current state of your database,
you should use a normal version tag (such as `v2.40.3` instead of `v2.40.3.cli-migrations-v3`). Then proceed to the
Hasura console at `localhost:8090`, go to the data tab, use the sidebar to navigate to the database schema at
default>public, and track all of the tables, relationships, and functions you need. Then proceed with the below
instructions.
You can follow these steps to re-export your metadata via the Hasura CLI in the `graphql-engine` container:
- (if the hasura cli isn't installed):
```
curl -L https://github.com/hasura/graphql-engine/raw/stable/cli/get.sh | bash
```
- (If not yet initialized) Initialize the Hasura project in the `graphql-engine` container (you can do this via the Docker Desktop `exec` view):
```
hasura-cli init
OR
hasura init
enter any name you wish for the project (i.e. citrine)
```
- Export the metadata by executing this command in the `graphql-engine` container:
```
hasura-cli metadata export
OR
hasura metadata export
```
- Find the exported files in the `graphql-engine` container's files in the metadata filepath `<name of project i.e. citrine>/metadata` and pull that metadata backup onto your local machine
- Copy the contents of the copied `metadata` folder into the `apps/Server/hasura-metadata` folder in this repository
## Testing with EVerest
In case you don't have a charger that supports OCPP to experiment with, you can run the EVerest charger simulator
locally and point it at CitrineOS. Helper scripts (`pnpm run start-everest` and `pnpm run start-everest-16`) and full
instructions live in [`everest/README.md`](./everest/README.md).

View File

@@ -0,0 +1,58 @@
// SPDX-FileCopyrightText: 2025 Contributors to the CitrineOS Project
//
// SPDX-License-Identifier: Apache-2.0
import * as path from 'path';
import { promises as fs } from 'fs';
const source = process.argv[2];
const target = process.argv[3];
console.log(`Copying assets from ${source} to ${target}`);
if (!source || !target) {
console.error(
`Error: need valid source and target. Received source: ${source}, target: ${target}`,
);
process.exit(1);
}
async function copyDirectory(src: string, dest: string) {
try {
await fs.mkdir(dest, { recursive: true });
console.log(`Created "${dest}" directory`);
const entries = await fs.readdir(src, { withFileTypes: true });
for (const entry of entries) {
const srcPath = path.join(src, entry.name);
const destPath = path.join(dest, entry.name);
if (entry.isDirectory()) {
await copyDirectory(srcPath, destPath);
} else {
await fs.copyFile(srcPath, destPath);
console.log(`Copied ${srcPath} to ${destPath}`);
}
}
} catch (err) {
console.error('Error copying directory:', err);
process.exit(1);
}
}
async function copy(s: string, t: string) {
const srcDir = path.resolve(s);
const destDir = path.resolve(t);
await copyDirectory(srcDir, destDir);
console.log(`Successfully copied assets from ${srcDir} to ${destDir}`);
}
copy(source, target)
.then(() => {
console.log(`Completed copying assets from ${source} to ${target}`);
process.exit(0);
})
.catch((err) => {
console.error('Unhandled error:', err);
process.exit(1);
});

View File

@@ -0,0 +1,30 @@
# SPDX-FileCopyrightText: 2025 Contributors to the CitrineOS Project
#
# SPDX-License-Identifier: Apache-2.0
# Use a specific base image with platform support
FROM --platform=${BUILDPLATFORM:-linux/amd64} node:24.16.0 AS build
RUN corepack enable
WORKDIR /usr/local/apps/citrineos
COPY . .
RUN pnpm install --frozen-lockfile
RUN pnpm --filter "@citrineos/server..." build
# The final stage, which copies built files and prepares the run environment
# Using a slim image to reduce the final image size
FROM node:24.16.0-slim
RUN corepack enable
COPY --from=build /usr/local/apps/citrineos /usr/local/apps/citrineos
WORKDIR /usr/local/apps/citrineos
RUN chmod +x /usr/local/apps/citrineos/apps/Server/entrypoint.sh
EXPOSE ${PORT}
ENTRYPOINT ["/usr/local/apps/citrineos/apps/Server/entrypoint.sh"]

View File

@@ -0,0 +1,181 @@
# SPDX-FileCopyrightText: 2025 Contributors to the CitrineOS Project
#
# SPDX-License-Identifier: Apache-2.0
services:
amqp-broker:
image: rabbitmq:3-management
ports:
- 15672:15672
- 5672:5672
environment:
RABBITMQ_DEFAULT_USER: "guest"
RABBITMQ_DEFAULT_PASS: "guest"
volumes:
- ./data/rabbitmq:/var/lib/rabbitmq
healthcheck:
test: rabbitmq-diagnostics -q check_port_connectivity
interval: 10s
timeout: 10s
retries: 3
ocpp-db:
image: postgis/postgis:16-3.5
platform: linux/amd64
ports:
- 5432:5432
volumes:
- ./data/postgresql/pgdata:/var/lib/postgresql/data
environment:
POSTGRES_DB: citrine
POSTGRES_USER: citrine
POSTGRES_PASSWORD: "citrine"
healthcheck:
test: "pg_isready --username=citrine"
interval: 5s
timeout: 10s
retries: 5
citrine:
image: citrineos-core-citrine:latest
build:
context: ../../
dockerfile: ./apps/Server/deploy.Dockerfile
volumes:
- ../../pnpm-lock.yaml:/usr/local/apps/citrineos/pnpm-lock.yaml
- ../../package.json:/usr/local/apps/citrineos/package.json
- ../../tsconfig.json:/usr/local/apps/citrineos/tsconfig.json
- ../../tsconfig.build.json:/usr/local/apps/citrineos/tsconfig.build.json
- ./:/usr/local/apps/citrineos/apps/Server
- ../../packages/base:/usr/local/apps/citrineos/packages/base
- ../../packages/core:/usr/local/apps/citrineos/packages/core
- /usr/local/apps/citrineos/node_modules
- /usr/local/apps/citrineos/apps/Server/node_modules
- /usr/local/apps/citrineos/packages/base/node_modules
- /usr/local/apps/citrineos/packages/core/node_modules
- /usr/local/apps/citrineos/dist/
- /usr/local/apps/citrineos/apps/Server/dist/
- /usr/local/apps/citrineos/packages/base/dist/
- /usr/local/apps/citrineos/packages/core/dist/
environment:
APP_NAME: "all"
APP_ENV: "docker"
# Add AWS region (required by the SDK)
AWS_REGION: us-east-1
# Skip authentication completely - use public access
AWS_ACCESS_KEY_ID: minioadmin
AWS_SECRET_ACCESS_KEY: minioadmin
# Database Strategy
DB_STRATEGY: "migrate"
# Bootstrap Configuration Environment Variables
BOOTSTRAP_CITRINEOS_DATABASE_HOST: "ocpp-db"
BOOTSTRAP_CITRINEOS_CONFIG_FILENAME: "config.json"
# CITRINEOS_CONFIG_DIR: '/custom/config/path' # Optional - uncomment if needed
BOOTSTRAP_CITRINEOS_FILE_ACCESS_TYPE: "local"
BOOTSTRAP_CITRINEOS_FILE_ACCESS_LOCAL_DEFAULT_FILE_PATH: "/data"
# No System Config file persistence respected, to avoid local conflicts across version updates - recommended to remove for production
CONFIG_CITRINEOS_WIPE_FILE_ON_START: "true"
depends_on:
ocpp-db:
condition: service_healthy
amqp-broker:
condition: service_healthy
minio-init:
condition: service_completed_successfully
ports:
- 8080:8080
- 8081:8081
- 8082:8082
- 8083:8083
- 8443:8443
- 8444:8444
- 9229:9229
healthcheck:
test: [
"CMD-SHELL",
"node -e \"const net = require(\\\"net\\\"); const client =
net.createConnection(8080, \\\"127.0.0.1\\\", () => { client.end();
process.exit(0); }); client.on(\\\"error\\\", () =>
process.exit(1)); client.setTimeout(5000, () => { client.destroy();
process.exit(1); });\"",
]
interval: 30s
timeout: 10s
retries: 5
start_period: 60s
minio:
image: minio/minio
container_name: minio
ports:
- "9000:9000"
- "9001:9001"
environment:
# Use default credentials for simplicity
MINIO_ROOT_USER: minioadmin
MINIO_ROOT_PASSWORD: minioadmin
# Enable public buckets
MINIO_BROWSER_REDIRECT_URL: http://localhost:9001
MINIO_SERVER_URL: http://localhost:9000
volumes:
- ./data/minio:/data
command: server /data --console-address ":9001"
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
interval: 30s
timeout: 10s
retries: 5
start_period: 20s
# Initialize MinIO
minio-init:
image: minio/mc
depends_on:
minio:
condition: service_healthy
entrypoint: >
/bin/sh -c " # Enable command echoing and error reporting set -x
echo 'Setting up MinIO alias' mc alias set myminio http://minio:9000
minioadmin minioadmin || echo 'FAILED: alias setup' "
graphql-engine:
image: hasura/graphql-engine:v2.40.3.cli-migrations-v3
volumes:
- ./hasura-metadata:/hasura-metadata
ports:
- 8090:8080
restart: always
depends_on:
citrine:
# ensures hasura migrations will run only after citrine is healthy,
# meaning 01_Data/dist/layers/sequelize/util.js was executed and logged
# CitrineOS Logger:DefaultSequelizeInstance Database altered,
# which means the database is ready to be used by hasura
condition: service_healthy
environment:
## postgres database to store Hasura metadata
HASURA_GRAPHQL_DATABASE_URL: postgres://citrine:citrine@ocpp-db:5432/citrine
## enable the console served by server
HASURA_GRAPHQL_ENABLE_CONSOLE: "true" # set to "false" to disable console
## enable debugging mode. It is recommended to disable this in production
HASURA_GRAPHQL_DEV_MODE: "true"
HASURA_GRAPHQL_ENABLED_LOG_TYPES: startup, http-log, webhook-log, websocket-log, query-log
HASURA_GRAPHQL_ENABLE_TELEMETRY: "false"
## uncomment next line to run console offline (i.e load console assets from server instead of CDN)
# HASURA_GRAPHQL_CONSOLE_ASSETS_DIR: /srv/console-assets
## uncomment next line to set an admin secret - warning, without configuration this will cause upstream services to fail
# HASURA_GRAPHQL_ADMIN_SECRET: CitrineOS!
HASURA_GRAPHQL_METADATA_DEFAULTS:
"{\"backend_configs\":{\"dataconnector\":{\"a\
thena\":{\"uri\":\"http://data-connector-agent:8081/api/v1/athena\"},\"\
mariadb\":{\"uri\":\"http://data-connector-agent:8081/api/v1/mariadb\"},\
\"mysql8\":{\"uri\":\"http://data-connector-agent:8081/api/v1/mysql\"},\
\"oracle\":{\"uri\":\"http://data-connector-agent:8081/api/v1/oracle\"},\
\"snowflake\":{\"uri\":\"http://data-connector-agent:8081/api/v1/snowfl\
ake\"}}}}"
healthcheck:
test: ["CMD", "curl", "-f", "http://graphql-engine:8080/healthz"]
interval: 5s
timeout: 10s
retries: 20
start_period: 5s

View File

@@ -0,0 +1,20 @@
#!/bin/sh
set -e
# Default to migrate if DB_STRATEGY is not set
DB_STRATEGY=${DB_STRATEGY:-migrate}
echo "Executing DB strategy: $DB_STRATEGY"
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
if [ "$DB_STRATEGY" = "migrate" ]; then
(cd "$SCRIPT_DIR" && pnpm run migrate)
else
echo "Unknown DB_STRATEGY: $DB_STRATEGY. Defaulting to migrate."
(cd "$SCRIPT_DIR" && pnpm run migrate)
fi
echo "Starting application..."
exec node "$SCRIPT_DIR/dist/index.js"

View File

@@ -0,0 +1,26 @@
// SPDX-FileCopyrightText: 2025 Contributors to the CitrineOS Project
//
// SPDX-License-Identifier: Apache-2.0
import { fileURLToPath } from 'url';
import { dirname } from 'path';
import tseslint from 'typescript-eslint';
import { sharedConfigs, sharedIgnores } from '../../eslint.config.base.js';
const __dirname = dirname(fileURLToPath(import.meta.url));
export default tseslint.config(
...sharedConfigs,
{
languageOptions: {
parserOptions: {
project: ['./tsconfig.eslint.json'],
tsconfigRootDir: __dirname,
},
},
rules: {
'@typescript-eslint/no-floating-promises': 'error',
},
},
sharedIgnores,
);

View File

@@ -0,0 +1,11 @@
ARG EVEREST_IMAGE_TAG="2025.6.1-dt-esdp"
FROM ghcr.io/everest/everest-demo/manager:${EVEREST_IMAGE_TAG}
WORKDIR /workspace
RUN npm i -g http-server
EXPOSE 8888
COPY ./start.sh /tmp/start.sh
RUN chmod +x /tmp/start.sh
ENTRYPOINT ["/tmp/start.sh"]

View File

@@ -0,0 +1,62 @@
# Testing with EVerest
In the case you don't have a charger that supports OCPP 2.0.1 to experiment with, we can recommend using the Linux
Foundation Energy project EVerest. [See here](https://github.com/EVerest) for the repository. They have built an open source version of
charger firmware and also allow for using it as a simulator. They support OCPP 2.0.1 which makes it a great testing
opportunity with CitrineOS. For the long route of setting up EVerst you can follow their documentation and build
the project yourself. [See here for Docs](https://everest.github.io/latest/how-to-guides/getting-started/index.html)
# Running EVerest
In order to alleviate some of the complexities that may arise when starting EVerest, we have created
some helpful commands that should help in getting the EVerest charger simulator running locally and targeting
CitrineOS.
You will notice in the `apps/Server/everest` directory the files created to support running EVerest within Docker.
In addition, we created some helpful package scripts (run from the `apps/Server` directory):
- `pnpm run start-everest` — starts EVerest for OCPP 2.x (defaults to `OCPP_VERSION=2.1`)
- `pnpm run start-everest-16` — starts EVerest for OCPP 1.6
Both scripts `cd` into `everest` and trigger the `docker compose up` command (below) from within the
`apps/Server/everest` directory so that it can pick up the `Dockerfile` and the `docker-compose.yml` files.
You will notice that there are two args that are configurable:
- `EVEREST_IMAGE_TAG` - The image tag that will be used for the EVerest image (ghcr.io/everest/everest-demo/manager).
- `OCPP_VERSION` - The version of OCPP to run EVerest for. `start-everest` handles all 2.x versions for the given 2.x `OCPP_VERSION`, and `start-everest-16` is for OCPP 1.6 only.
After running `pnpm run start-everest` (or `pnpm run start-everest-16`), you should see 3 running EVerest containers
and the `manager` container should have the appropriate EVerest logs.
### EVerest UI
Now that the 3 containers are running in Docker, you should be able to navigate to `[localhost|ip]:1880/ui/` to view
the EVerest simulator UI. There, you should be able to simulate the pause/resume and plug/unplug events among others.
### EVerest NodeRed
You can also view the EVerest NodeRed UI `[localhost|ip]:1880/`, but it is not advisable to make any adjustments here
unless you have a good understanding of this configuration.
### Viewing OCPP logs in EVerest
To view the OCPP logs in EVerest, we have utilized Node `http-server`, which you will see being initialized
in the Dockerfile. We initialize a simple HTTP server on port `8888` and expose this port so that it is
mapped in the compose file allowing you to navigate to `localhost:8888`. This HTTP server is configured to
serve the contents of the `/tmp/everest_ocpp_logs` which is where EVerest stores the OCPP logs in the
Docker container. Conveniently, the logs are in HTML format, so we can easily view them in the browser.
# Running EVerest Manually
You can also use their demo repository that hosts a Docker packaged EVerest image. [See here for Github Repo](https://github.com/EVerest/everest-demo)
To get EVerest running on the side while developing and making changes, you can follow the steps below.
1. Run your CitrineOS instance locally with `docker compose up -d` in the CitrineOS repository.
1. Clone the [EVerest Demo](https://github.com/EVerest/everest-demo) repository and `cd` into the repo.
1. With CitrineOS running execute an "add charger" script at `./citrineos/add-charger.sh` This adds a charger, location and password for the charger to CitrineOS.
1. Bring up EVerest with `docker compose --project-name everest-ac-demo --file "docker-compose.ocpp201.yml" up -d`.
1. Copy over the appropriate device model with `docker cp manager/device_model_storage_citrineos_sp1.db \
everest-ac-demo-manager-1:/ext/source/build/dist/share/everest/modules/OCPP201/device_model_storage.db`.
1. Start EVerst having OCPP2.0.1 support with `docker exec everest-ac-demo-manager-1 sh /ext/source/build/run-scripts/run-sil-ocpp201.sh`.

View File

@@ -0,0 +1,56 @@
# SPDX-FileCopyrightText: 2025 Contributors to the CitrineOS Project
#
# SPDX-License-Identifier: Apache-2.0
version: '3.6'
services:
mqtt-server:
image: ghcr.io/everest/everest-demo/mqtt-server:${EVEREST_IMAGE_TAG}
platform: linux/x86_64
logging:
driver: none
networks:
- everest_net
manager:
build:
dockerfile: Dockerfile
args:
- EVEREST_IMAGE_TAG=${EVEREST_IMAGE_TAG}
platform: linux/x86_64
ports:
- 8888:8888
deploy:
resources:
limits:
cpus: '2'
memory: '4G'
depends_on:
- mqtt-server
environment:
- MQTT_SERVER_ADDRESS=mqtt-server
- EVEREST_IMAGE_TAG=${EVEREST_IMAGE_TAG}
- OCPP_VERSION=${OCPP_VERSION}
sysctls:
- net.ipv6.conf.all.disable_ipv6=0
extra_hosts:
- 'host.docker.internal:host-gateway'
networks:
- everest_net
nodered:
image: ghcr.io/everest/everest-demo/nodered:${EVEREST_IMAGE_TAG}
depends_on:
- mqtt-server
ports:
- 1880:1880
environment:
- MQTT_SERVER_ADDRESS=mqtt-server
- FLOWS=/config/config-sil-two-evse-flow.json
networks:
- everest_net
networks:
everest_net:
driver: bridge
enable_ipv6: true

View File

@@ -0,0 +1,75 @@
#!/bin/sh
# SPDX-FileCopyrightText: 2025 Contributors to the CitrineOS Project
#
# SPDX-License-Identifier: Apache-2.0
_OCPP_VERSION=$OCPP_VERSION
OCPP_VERSION_ENUM="OCPP201"
EVEREST_TARGET_URL="ws://host.docker.internal:8081/cp001"
case "$_OCPP_VERSION" in
"1.6")
OCPP_VERSION_ENUM="OCPP16"
;;
"2.0.1")
OCPP_VERSION_ENUM="OCPP201"
;;
"2.1")
OCPP_VERSION_ENUM="OCPP21"
;;
*)
# NOT in the list
_OCPP_VERSION="2.0.1"
OCPP_VERSION_ENUM="OCPP201"
;;
esac
echo $OCPP_VERSION_ENUM
if [ "$_OCPP_VERSION" != "1.6" ]; then
#There are two different configs in Everest that default their settings that we need to override for Citrine.
#The first is a temp file that may or may not be used by Everest, but to be safe we change the config there.
#The second is the config used by Everest when setting up their image, and this is the primary driver for the configuration of the image.
CONFIG="$(cat <<JSON
[{"configurationSlot": 1, "connectionData": {"messageTimeout": 30, "ocppCsmsUrl": "$EVEREST_TARGET_URL", "ocppInterface": "Wired0", "ocppTransport": "JSON", "ocppVersion": "$OCPP_VERSION_ENUM", "securityProfile": 1}}, {"configurationSlot": 2, "connectionData": {"messageTimeout": 30, "ocppCsmsUrl": "$EVEREST_TARGET_URL", "ocppInterface": "Wired0", "ocppTransport": "JSON", "ocppVersion": "$OCPP_VERSION_ENUM", "securityProfile": 2}}]
JSON
)"
chmod +x /tmp/config.json
jq --argjson config "$CONFIG" '
(.[]
| select(.name == "InternalCtrlr")
| .variables.NetworkConnectionProfiles.attributes.Actual
) = $config
' "/tmp/config.json" > /tmp/config_citrine.json && mv /tmp/config_citrine.json "/tmp/config.json"
chmod -x /tmp/config.json
chmod +x /ext/dist/share/everest/modules/OCPP201/component_config/standardized/InternalCtrlr.json
jq --argjson config "$CONFIG" '
(.
| .properties
| .NetworkConnectionProfiles
| .attributes[]
| select(.type == "Actual")
| .value
) = $config
' "/ext/dist/share/everest/modules/OCPP201/component_config/standardized/InternalCtrlr.json" \
> /tmp/config_citrine_dist.json && mv /tmp/config_citrine_dist.json "/ext/dist/share/everest/modules/OCPP201/component_config/standardized/InternalCtrlr.json"
chmod -x /ext/dist/share/everest/modules/OCPP201/component_config/standardized/InternalCtrlr.json
fi
/entrypoint.sh
http-server /tmp/everest_ocpp_logs -p 8888 &
if [ "$_OCPP_VERSION" = "1.6" ]; then
chmod +x /ext/build/run-scripts/run-sil-ocpp.sh
sed -i "0,/127.0.0.1:8180\/steve\/websocket\/CentralSystemService\// s|127.0.0.1:8180/steve/websocket/CentralSystemService/|${EVEREST_TARGET_URL}|" /ext/dist/share/everest/modules/OCPP/config-docker.json
/ext/build/run-scripts/run-sil-ocpp.sh
else
#Works for all 2.x versions
rm /ext/dist/share/everest/modules/OCPP201/component_config/custom/EVSE_2.json
rm /ext/dist/share/everest/modules/OCPP201/component_config/custom/Connector_2_1.json
chmod +x /ext/build/run-scripts/run-sil-ocpp201-pnc.sh
/ext/build/run-scripts/run-sil-ocpp201-pnc.sh
fi

View File

@@ -0,0 +1,9 @@
# SPDX-FileCopyrightText: 2026 Contributors to the CitrineOS Project
#
# SPDX-License-Identifier: Apache-2.0
actions: []
custom_types:
enums: []
input_objects: []
objects: []
scalars: []

View File

@@ -0,0 +1,4 @@
# SPDX-FileCopyrightText: 2026 Contributors to the CitrineOS Project
#
# SPDX-License-Identifier: Apache-2.0
[]

View File

@@ -0,0 +1,4 @@
# SPDX-FileCopyrightText: 2026 Contributors to the CitrineOS Project
#
# SPDX-License-Identifier: Apache-2.0
{}

View File

@@ -0,0 +1,4 @@
# SPDX-FileCopyrightText: 2026 Contributors to the CitrineOS Project
#
# SPDX-License-Identifier: Apache-2.0
{}

View File

@@ -0,0 +1,4 @@
# SPDX-FileCopyrightText: 2026 Contributors to the CitrineOS Project
#
# SPDX-License-Identifier: Apache-2.0
[]

View File

@@ -0,0 +1,17 @@
# SPDX-FileCopyrightText: 2026 Contributors to the CitrineOS Project
#
# SPDX-License-Identifier: Apache-2.0
- name: default
kind: postgres
configuration:
connection_info:
database_url:
from_env: HASURA_GRAPHQL_DATABASE_URL
isolation_level: read-committed
pool_settings:
connection_lifetime: 600
idle_timeout: 180
max_connections: 50
retries: 1
use_prepared_statements: true
tables: "!include default/tables/tables.yaml"

View File

@@ -0,0 +1,10 @@
# SPDX-FileCopyrightText: 2026 Contributors to the CitrineOS Project
#
# SPDX-License-Identifier: Apache-2.0
table:
name: AsyncJobStatuses
schema: public
object_relationships:
- name: TenantPartner
using:
foreign_key_constraint_on: tenantPartnerId

View File

@@ -0,0 +1,83 @@
# SPDX-FileCopyrightText: 2026 Contributors to the CitrineOS Project
#
# SPDX-License-Identifier: Apache-2.0
table:
name: Authorizations
schema: public
object_relationships:
- name: GroupAuthorization
using:
foreign_key_constraint_on: groupAuthorizationId
- name: Tariff
using:
foreign_key_constraint_on: tariffId
- name: Tenant
using:
foreign_key_constraint_on: tenantId
- name: TenantPartner
using:
foreign_key_constraint_on: tenantPartnerId
array_relationships:
- name: Authorizations
using:
foreign_key_constraint_on:
column: groupAuthorizationId
table:
name: Authorizations
schema: public
- name: LocalListAuthorizations
using:
foreign_key_constraint_on:
column: authorizationId
table:
name: LocalListAuthorizations
schema: public
- name: Transactions
using:
foreign_key_constraint_on:
column: authorizationId
table:
name: Transactions
schema: public
- name: localListAuthorizationsByGroupauthorizationid
using:
foreign_key_constraint_on:
column: groupAuthorizationId
table:
name: LocalListAuthorizations
schema: public
insert_permissions:
- role: user
permission:
check:
tenantId:
_eq: x-hasura-tenant-id
columns: '*'
comment: ""
select_permissions:
- role: user
permission:
columns: '*'
filter:
tenantId:
_eq: x-hasura-tenant-id
allow_aggregations: true
comment: ""
update_permissions:
- role: user
permission:
columns: '*'
filter:
tenantId:
_eq: x-hasura-tenant-id
check:
tenantId:
_eq: x-hasura-tenant-id
comment: ""
delete_permissions:
- role: user
permission:
filter:
tenantId:
_eq: x-hasura-tenant-id
comment: ""

View File

@@ -0,0 +1,53 @@
# SPDX-FileCopyrightText: 2026 Contributors to the CitrineOS Project
#
# SPDX-License-Identifier: Apache-2.0
table:
name: Boots
schema: public
object_relationships:
- name: Tenant
using:
foreign_key_constraint_on: tenantId
array_relationships:
- name: VariableAttributes
using:
foreign_key_constraint_on:
column: bootConfigId
table:
name: VariableAttributes
schema: public
insert_permissions:
- role: user
permission:
check:
tenantId:
_eq: x-hasura-tenant-id
columns: '*'
comment: ""
select_permissions:
- role: user
permission:
columns: '*'
filter:
tenantId:
_eq: x-hasura-tenant-id
allow_aggregations: true
comment: ""
update_permissions:
- role: user
permission:
columns: '*'
filter:
tenantId:
_eq: x-hasura-tenant-id
check:
tenantId:
_eq: x-hasura-tenant-id
comment: ""
delete_permissions:
- role: user
permission:
filter:
tenantId:
_eq: x-hasura-tenant-id
comment: ""

View File

@@ -0,0 +1,70 @@
# SPDX-FileCopyrightText: 2026 Contributors to the CitrineOS Project
#
# SPDX-License-Identifier: Apache-2.0
table:
name: Certificates
schema: public
object_relationships:
- name: Certificate
using:
foreign_key_constraint_on: signedBy
- name: Tenant
using:
foreign_key_constraint_on: tenantId
array_relationships:
- name: Certificates
using:
foreign_key_constraint_on:
column: signedBy
table:
name: Certificates
schema: public
- name: InstallCertificateAttempts
using:
foreign_key_constraint_on:
column: certificateId
table:
name: InstallCertificateAttempts
schema: public
- name: InstalledCertificates
using:
foreign_key_constraint_on:
column: certificateId
table:
name: InstalledCertificates
schema: public
insert_permissions:
- role: user
permission:
check:
tenantId:
_eq: x-hasura-tenant-id
columns: '*'
comment: ""
select_permissions:
- role: user
permission:
columns: '*'
filter:
tenantId:
_eq: x-hasura-tenant-id
allow_aggregations: true
comment: ""
update_permissions:
- role: user
permission:
columns: '*'
filter:
tenantId:
_eq: x-hasura-tenant-id
check:
tenantId:
_eq: x-hasura-tenant-id
comment: ""
delete_permissions:
- role: user
permission:
filter:
tenantId:
_eq: x-hasura-tenant-id
comment: ""

View File

@@ -0,0 +1,45 @@
# SPDX-FileCopyrightText: 2026 Contributors to the CitrineOS Project
#
# SPDX-License-Identifier: Apache-2.0
table:
name: ChangeConfigurations
schema: public
object_relationships:
- name: Tenant
using:
foreign_key_constraint_on: tenantId
insert_permissions:
- role: user
permission:
check:
tenantId:
_eq: x-hasura-tenant-id
columns: '*'
comment: ""
select_permissions:
- role: user
permission:
columns: '*'
filter:
tenantId:
_eq: x-hasura-tenant-id
allow_aggregations: true
comment: ""
update_permissions:
- role: user
permission:
columns: '*'
filter:
tenantId:
_eq: x-hasura-tenant-id
check:
tenantId:
_eq: x-hasura-tenant-id
comment: ""
delete_permissions:
- role: user
permission:
filter:
tenantId:
_eq: x-hasura-tenant-id
comment: ""

View File

@@ -0,0 +1,25 @@
# SPDX-FileCopyrightText: 2026 Contributors to the CitrineOS Project
#
# SPDX-License-Identifier: Apache-2.0
table:
name: ChargingNeeds
schema: public
object_relationships:
- name: Evse
using:
foreign_key_constraint_on: evseId
- name: Tenant
using:
foreign_key_constraint_on: tenantId
- name: Transaction
using:
foreign_key_constraint_on: transactionDatabaseId
select_permissions:
- role: user
permission:
columns: '*'
filter:
tenantId:
_eq: x-hasura-tenant-id
allow_aggregations: true
comment: ""

View File

@@ -0,0 +1,56 @@
# SPDX-FileCopyrightText: 2026 Contributors to the CitrineOS Project
#
# SPDX-License-Identifier: Apache-2.0
table:
name: ChargingProfiles
schema: public
object_relationships:
- name: Tenant
using:
foreign_key_constraint_on: tenantId
- name: Transaction
using:
foreign_key_constraint_on: transactionDatabaseId
array_relationships:
- name: ChargingSchedules
using:
foreign_key_constraint_on:
column: chargingProfileDatabaseId
table:
name: ChargingSchedules
schema: public
insert_permissions:
- role: user
permission:
check:
tenantId:
_eq: x-hasura-tenant-id
columns: '*'
comment: ""
select_permissions:
- role: user
permission:
columns: '*'
filter:
tenantId:
_eq: x-hasura-tenant-id
allow_aggregations: true
comment: ""
update_permissions:
- role: user
permission:
columns: '*'
filter:
tenantId:
_eq: x-hasura-tenant-id
check:
tenantId:
_eq: x-hasura-tenant-id
comment: ""
delete_permissions:
- role: user
permission:
filter:
tenantId:
_eq: x-hasura-tenant-id
comment: ""

View File

@@ -0,0 +1,56 @@
# SPDX-FileCopyrightText: 2026 Contributors to the CitrineOS Project
#
# SPDX-License-Identifier: Apache-2.0
table:
name: ChargingSchedules
schema: public
object_relationships:
- name: ChargingProfile
using:
foreign_key_constraint_on: chargingProfileDatabaseId
- name: Tenant
using:
foreign_key_constraint_on: tenantId
array_relationships:
- name: SalesTariffs
using:
foreign_key_constraint_on:
column: chargingScheduleDatabaseId
table:
name: SalesTariffs
schema: public
insert_permissions:
- role: user
permission:
check:
tenantId:
_eq: x-hasura-tenant-id
columns: '*'
comment: ""
select_permissions:
- role: user
permission:
columns: '*'
filter:
tenantId:
_eq: x-hasura-tenant-id
allow_aggregations: true
comment: ""
update_permissions:
- role: user
permission:
columns: '*'
filter:
tenantId:
_eq: x-hasura-tenant-id
check:
tenantId:
_eq: x-hasura-tenant-id
comment: ""
delete_permissions:
- role: user
permission:
filter:
tenantId:
_eq: x-hasura-tenant-id
comment: ""

View File

@@ -0,0 +1,28 @@
# SPDX-FileCopyrightText: 2026 Contributors to the CitrineOS Project
#
# SPDX-License-Identifier: Apache-2.0
table:
name: ChargingStationNetworkProfiles
schema: public
object_relationships:
- name: ChargingStation
using:
foreign_key_constraint_on: stationId
- name: ServerNetworkProfile
using:
foreign_key_constraint_on: websocketServerConfigId
- name: SetNetworkProfile
using:
foreign_key_constraint_on: setNetworkProfileId
- name: Tenant
using:
foreign_key_constraint_on: tenantId
select_permissions:
- role: user
permission:
columns: '*'
filter:
tenantId:
_eq: x-hasura-tenant-id
allow_aggregations: true
comment: ""

View File

@@ -0,0 +1,22 @@
# SPDX-FileCopyrightText: 2026 Contributors to the CitrineOS Project
#
# SPDX-License-Identifier: Apache-2.0
table:
name: ChargingStationSecurityInfos
schema: public
object_relationships:
- name: ChargingStation
using:
foreign_key_constraint_on: stationId
- name: Tenant
using:
foreign_key_constraint_on: tenantId
select_permissions:
- role: user
permission:
columns: '*'
filter:
tenantId:
_eq: x-hasura-tenant-id
allow_aggregations: true
comment: ""

View File

@@ -0,0 +1,22 @@
# SPDX-FileCopyrightText: 2026 Contributors to the CitrineOS Project
#
# SPDX-License-Identifier: Apache-2.0
table:
name: ChargingStationSequences
schema: public
object_relationships:
- name: ChargingStation
using:
foreign_key_constraint_on: stationId
- name: Tenant
using:
foreign_key_constraint_on: tenantId
select_permissions:
- role: user
permission:
columns: '*'
filter:
tenantId:
_eq: x-hasura-tenant-id
allow_aggregations: true
comment: ""

View File

@@ -0,0 +1,161 @@
# SPDX-FileCopyrightText: 2026 Contributors to the CitrineOS Project
#
# SPDX-License-Identifier: Apache-2.0
table:
name: ChargingStations
schema: public
object_relationships:
- name: Location
using:
foreign_key_constraint_on: locationId
- name: Tenant
using:
foreign_key_constraint_on: tenantId
array_relationships:
- name: ChargingStationNetworkProfiles
using:
foreign_key_constraint_on:
column: stationId
table:
name: ChargingStationNetworkProfiles
schema: public
- name: ChargingStationSecurityInfos
using:
foreign_key_constraint_on:
column: stationId
table:
name: ChargingStationSecurityInfos
schema: public
- name: ChargingStationSequences
using:
foreign_key_constraint_on:
column: stationId
table:
name: ChargingStationSequences
schema: public
- name: Connectors
using:
foreign_key_constraint_on:
column: stationId
table:
name: Connectors
schema: public
- name: DeleteCertificateAttempts
using:
foreign_key_constraint_on:
column: stationId
table:
name: DeleteCertificateAttempts
schema: public
- name: EventData
using:
foreign_key_constraint_on:
column: stationId
table:
name: EventData
schema: public
- name: Evses
using:
foreign_key_constraint_on:
column: stationId
table:
name: Evses
schema: public
- name: InstallCertificateAttempts
using:
foreign_key_constraint_on:
column: stationId
table:
name: InstallCertificateAttempts
schema: public
- name: InstalledCertificates
using:
foreign_key_constraint_on:
column: stationId
table:
name: InstalledCertificates
schema: public
- name: LatestStatusNotifications
using:
foreign_key_constraint_on:
column: stationId
table:
name: LatestStatusNotifications
schema: public
- name: OCPPMessages
using:
foreign_key_constraint_on:
column: stationId
table:
name: OCPPMessages
schema: public
- name: SetNetworkProfiles
using:
foreign_key_constraint_on:
column: stationId
table:
name: SetNetworkProfiles
schema: public
- name: StatusNotifications
using:
foreign_key_constraint_on:
column: stationId
table:
name: StatusNotifications
schema: public
- name: Transactions
using:
foreign_key_constraint_on:
column: stationId
table:
name: Transactions
schema: public
- name: VariableAttributes
using:
foreign_key_constraint_on:
column: stationId
table:
name: VariableAttributes
schema: public
- name: VariableMonitorings
using:
foreign_key_constraint_on:
column: stationId
table:
name: VariableMonitorings
schema: public
insert_permissions:
- role: user
permission:
check:
tenantId:
_eq: x-hasura-tenant-id
columns: '*'
comment: ""
select_permissions:
- role: user
permission:
columns: '*'
filter:
tenantId:
_eq: x-hasura-tenant-id
allow_aggregations: true
comment: ""
update_permissions:
- role: user
permission:
columns: '*'
filter:
tenantId:
_eq: x-hasura-tenant-id
check:
tenantId:
_eq: x-hasura-tenant-id
comment: ""
delete_permissions:
- role: user
permission:
filter:
tenantId:
_eq: x-hasura-tenant-id
comment: ""

View File

@@ -0,0 +1,51 @@
# SPDX-FileCopyrightText: 2026 Contributors to the CitrineOS Project
#
# SPDX-License-Identifier: Apache-2.0
table:
name: ComponentVariables
schema: public
object_relationships:
- name: Component
using:
foreign_key_constraint_on: componentId
- name: Tenant
using:
foreign_key_constraint_on: tenantId
- name: Variable
using:
foreign_key_constraint_on: variableId
insert_permissions:
- role: user
permission:
check:
tenantId:
_eq: x-hasura-tenant-id
columns: '*'
comment: ""
select_permissions:
- role: user
permission:
columns: '*'
filter:
tenantId:
_eq: x-hasura-tenant-id
allow_aggregations: true
comment: ""
update_permissions:
- role: user
permission:
columns: '*'
filter:
tenantId:
_eq: x-hasura-tenant-id
check:
tenantId:
_eq: x-hasura-tenant-id
comment: ""
delete_permissions:
- role: user
permission:
filter:
tenantId:
_eq: x-hasura-tenant-id
comment: ""

View File

@@ -0,0 +1,84 @@
# SPDX-FileCopyrightText: 2026 Contributors to the CitrineOS Project
#
# SPDX-License-Identifier: Apache-2.0
table:
name: Components
schema: public
object_relationships:
- name: EvseType
using:
foreign_key_constraint_on: evseDatabaseId
- name: Tenant
using:
foreign_key_constraint_on: tenantId
array_relationships:
- name: ComponentVariables
using:
foreign_key_constraint_on:
column: componentId
table:
name: ComponentVariables
schema: public
- name: EventData
using:
foreign_key_constraint_on:
column: componentId
table:
name: EventData
schema: public
- name: MessageInfos
using:
foreign_key_constraint_on:
column: displayComponentId
table:
name: MessageInfos
schema: public
- name: VariableAttributes
using:
foreign_key_constraint_on:
column: componentId
table:
name: VariableAttributes
schema: public
- name: VariableMonitorings
using:
foreign_key_constraint_on:
column: componentId
table:
name: VariableMonitorings
schema: public
insert_permissions:
- role: user
permission:
check:
tenantId:
_eq: x-hasura-tenant-id
columns: '*'
comment: ""
select_permissions:
- role: user
permission:
columns: '*'
filter:
tenantId:
_eq: x-hasura-tenant-id
allow_aggregations: true
comment: ""
update_permissions:
- role: user
permission:
columns: '*'
filter:
tenantId:
_eq: x-hasura-tenant-id
check:
tenantId:
_eq: x-hasura-tenant-id
comment: ""
delete_permissions:
- role: user
permission:
filter:
tenantId:
_eq: x-hasura-tenant-id
comment: ""

View File

@@ -0,0 +1,45 @@
# SPDX-FileCopyrightText: 2026 Contributors to the CitrineOS Project
#
# SPDX-License-Identifier: Apache-2.0
table:
name: CompositeSchedules
schema: public
object_relationships:
- name: Tenant
using:
foreign_key_constraint_on: tenantId
insert_permissions:
- role: user
permission:
check:
tenantId:
_eq: x-hasura-tenant-id
columns: '*'
comment: ""
select_permissions:
- role: user
permission:
columns: '*'
filter:
tenantId:
_eq: x-hasura-tenant-id
allow_aggregations: true
comment: ""
update_permissions:
- role: user
permission:
columns: '*'
filter:
tenantId:
_eq: x-hasura-tenant-id
check:
tenantId:
_eq: x-hasura-tenant-id
comment: ""
delete_permissions:
- role: user
permission:
filter:
tenantId:
_eq: x-hasura-tenant-id
comment: ""

View File

@@ -0,0 +1,69 @@
# SPDX-FileCopyrightText: 2026 Contributors to the CitrineOS Project
#
# SPDX-License-Identifier: Apache-2.0
table:
name: Connectors
schema: public
object_relationships:
- name: ChargingStation
using:
foreign_key_constraint_on: stationId
- name: Evse
using:
foreign_key_constraint_on: evseId
- name: Tariff
using:
foreign_key_constraint_on: tariffId
- name: Tenant
using:
foreign_key_constraint_on: tenantId
array_relationships:
- name: StartTransactions
using:
foreign_key_constraint_on:
column: connectorDatabaseId
table:
name: StartTransactions
schema: public
- name: Transactions
using:
foreign_key_constraint_on:
column: connectorId
table:
name: Transactions
schema: public
insert_permissions:
- role: user
permission:
check:
tenantId:
_eq: x-hasura-tenant-id
columns: '*'
comment: ""
select_permissions:
- role: user
permission:
columns: '*'
filter:
tenantId:
_eq: x-hasura-tenant-id
allow_aggregations: true
comment: ""
update_permissions:
- role: user
permission:
columns: '*'
filter:
tenantId:
_eq: x-hasura-tenant-id
check:
tenantId:
_eq: x-hasura-tenant-id
comment: ""
delete_permissions:
- role: user
permission:
filter:
tenantId:
_eq: x-hasura-tenant-id
comment: ""

View File

@@ -0,0 +1,13 @@
# SPDX-FileCopyrightText: 2026 Contributors to the CitrineOS Project
#
# SPDX-License-Identifier: Apache-2.0
table:
name: DeleteCertificateAttempts
schema: public
object_relationships:
- name: ChargingStation
using:
foreign_key_constraint_on: stationId
- name: Tenant
using:
foreign_key_constraint_on: tenantId

View File

@@ -0,0 +1,28 @@
# SPDX-FileCopyrightText: 2026 Contributors to the CitrineOS Project
#
# SPDX-License-Identifier: Apache-2.0
table:
name: EventData
schema: public
object_relationships:
- name: ChargingStation
using:
foreign_key_constraint_on: stationId
- name: Component
using:
foreign_key_constraint_on: componentId
- name: Tenant
using:
foreign_key_constraint_on: tenantId
- name: Variable
using:
foreign_key_constraint_on: variableId
select_permissions:
- role: user
permission:
columns: '*'
filter:
tenantId:
_eq: x-hasura-tenant-id
allow_aggregations: true
comment: ""

View File

@@ -0,0 +1,48 @@
# SPDX-FileCopyrightText: 2026 Contributors to the CitrineOS Project
#
# SPDX-License-Identifier: Apache-2.0
table:
name: EvseTypes
schema: public
object_relationships:
- name: Tenant
using:
foreign_key_constraint_on: tenantId
array_relationships:
- name: Components
using:
foreign_key_constraint_on:
column: evseDatabaseId
table:
name: Components
schema: public
- name: Reservations
using:
foreign_key_constraint_on:
column: evseId
table:
name: Reservations
schema: public
- name: TransactionEvents
using:
foreign_key_constraint_on:
column: evseId
table:
name: TransactionEvents
schema: public
- name: VariableAttributes
using:
foreign_key_constraint_on:
column: evseDatabaseId
table:
name: VariableAttributes
schema: public
select_permissions:
- role: user
permission:
columns: '*'
filter:
tenantId:
_eq: x-hasura-tenant-id
allow_aggregations: true
comment: ""

View File

@@ -0,0 +1,70 @@
# SPDX-FileCopyrightText: 2026 Contributors to the CitrineOS Project
#
# SPDX-License-Identifier: Apache-2.0
table:
name: Evses
schema: public
object_relationships:
- name: ChargingStation
using:
foreign_key_constraint_on: stationId
- name: Tenant
using:
foreign_key_constraint_on: tenantId
array_relationships:
- name: ChargingNeeds
using:
foreign_key_constraint_on:
column: evseId
table:
name: ChargingNeeds
schema: public
- name: Connectors
using:
foreign_key_constraint_on:
column: evseId
table:
name: Connectors
schema: public
- name: Transactions
using:
foreign_key_constraint_on:
column: evseId
table:
name: Transactions
schema: public
insert_permissions:
- role: user
permission:
check:
tenantId:
_eq: x-hasura-tenant-id
columns: '*'
comment: ""
select_permissions:
- role: user
permission:
columns: '*'
filter:
tenantId:
_eq: x-hasura-tenant-id
allow_aggregations: true
comment: ""
update_permissions:
- role: user
permission:
columns: '*'
filter:
tenantId:
_eq: x-hasura-tenant-id
check:
tenantId:
_eq: x-hasura-tenant-id
comment: ""
delete_permissions:
- role: user
permission:
filter:
tenantId:
_eq: x-hasura-tenant-id
comment: ""

View File

@@ -0,0 +1,16 @@
# SPDX-FileCopyrightText: 2026 Contributors to the CitrineOS Project
#
# SPDX-License-Identifier: Apache-2.0
table:
name: InstallCertificateAttempts
schema: public
object_relationships:
- name: Certificate
using:
foreign_key_constraint_on: certificateId
- name: ChargingStation
using:
foreign_key_constraint_on: stationId
- name: Tenant
using:
foreign_key_constraint_on: tenantId

View File

@@ -0,0 +1,25 @@
# SPDX-FileCopyrightText: 2026 Contributors to the CitrineOS Project
#
# SPDX-License-Identifier: Apache-2.0
table:
name: InstalledCertificates
schema: public
object_relationships:
- name: Certificate
using:
foreign_key_constraint_on: certificateId
- name: ChargingStation
using:
foreign_key_constraint_on: stationId
- name: Tenant
using:
foreign_key_constraint_on: tenantId
select_permissions:
- role: user
permission:
columns: '*'
filter:
tenantId:
_eq: x-hasura-tenant-id
allow_aggregations: true
comment: ""

View File

@@ -0,0 +1,25 @@
# SPDX-FileCopyrightText: 2026 Contributors to the CitrineOS Project
#
# SPDX-License-Identifier: Apache-2.0
table:
name: LatestStatusNotifications
schema: public
object_relationships:
- name: ChargingStation
using:
foreign_key_constraint_on: stationId
- name: StatusNotification
using:
foreign_key_constraint_on: statusNotificationId
- name: Tenant
using:
foreign_key_constraint_on: tenantId
select_permissions:
- role: user
permission:
columns: '*'
filter:
tenantId:
_eq: x-hasura-tenant-id
allow_aggregations: true
comment: ""

View File

@@ -0,0 +1,40 @@
# SPDX-FileCopyrightText: 2026 Contributors to the CitrineOS Project
#
# SPDX-License-Identifier: Apache-2.0
table:
name: LocalListAuthorizations
schema: public
object_relationships:
- name: Authorization
using:
foreign_key_constraint_on: authorizationId
- name: Tenant
using:
foreign_key_constraint_on: tenantId
- name: authorizationByGroupauthorizationid
using:
foreign_key_constraint_on: groupAuthorizationId
array_relationships:
- name: LocalListVersionAuthorizations
using:
foreign_key_constraint_on:
column: authorizationId
table:
name: LocalListVersionAuthorizations
schema: public
- name: SendLocalListAuthorizations
using:
foreign_key_constraint_on:
column: authorizationId
table:
name: SendLocalListAuthorizations
schema: public
select_permissions:
- role: user
permission:
columns: '*'
filter:
tenantId:
_eq: x-hasura-tenant-id
allow_aggregations: true
comment: ""

View File

@@ -0,0 +1,25 @@
# SPDX-FileCopyrightText: 2026 Contributors to the CitrineOS Project
#
# SPDX-License-Identifier: Apache-2.0
table:
name: LocalListVersionAuthorizations
schema: public
object_relationships:
- name: LocalListAuthorization
using:
foreign_key_constraint_on: authorizationId
- name: LocalListVersion
using:
foreign_key_constraint_on: localListVersionId
- name: Tenant
using:
foreign_key_constraint_on: tenantId
select_permissions:
- role: user
permission:
columns: '*'
filter:
tenantId:
_eq: x-hasura-tenant-id
allow_aggregations: true
comment: ""

View File

@@ -0,0 +1,27 @@
# SPDX-FileCopyrightText: 2026 Contributors to the CitrineOS Project
#
# SPDX-License-Identifier: Apache-2.0
table:
name: LocalListVersions
schema: public
object_relationships:
- name: Tenant
using:
foreign_key_constraint_on: tenantId
array_relationships:
- name: LocalListVersionAuthorizations
using:
foreign_key_constraint_on:
column: localListVersionId
table:
name: LocalListVersionAuthorizations
schema: public
select_permissions:
- role: user
permission:
columns: '*'
filter:
tenantId:
_eq: x-hasura-tenant-id
allow_aggregations: true
comment: ""

View File

@@ -0,0 +1,60 @@
# SPDX-FileCopyrightText: 2026 Contributors to the CitrineOS Project
#
# SPDX-License-Identifier: Apache-2.0
table:
name: Locations
schema: public
object_relationships:
- name: Tenant
using:
foreign_key_constraint_on: tenantId
array_relationships:
- name: ChargingStations
using:
foreign_key_constraint_on:
column: locationId
table:
name: ChargingStations
schema: public
- name: Transactions
using:
foreign_key_constraint_on:
column: locationId
table:
name: Transactions
schema: public
insert_permissions:
- role: user
permission:
check:
tenantId:
_eq: x-hasura-tenant-id
columns: '*'
comment: ""
select_permissions:
- role: user
permission:
columns: '*'
filter:
tenantId:
_eq: x-hasura-tenant-id
allow_aggregations: true
comment: ""
update_permissions:
- role: user
permission:
columns: '*'
filter:
tenantId:
_eq: x-hasura-tenant-id
check:
tenantId:
_eq: x-hasura-tenant-id
comment: ""
delete_permissions:
- role: user
permission:
filter:
tenantId:
_eq: x-hasura-tenant-id
comment: ""

View File

@@ -0,0 +1,22 @@
# SPDX-FileCopyrightText: 2026 Contributors to the CitrineOS Project
#
# SPDX-License-Identifier: Apache-2.0
table:
name: MessageInfos
schema: public
object_relationships:
- name: Component
using:
foreign_key_constraint_on: displayComponentId
- name: Tenant
using:
foreign_key_constraint_on: tenantId
select_permissions:
- role: user
permission:
columns: '*'
filter:
tenantId:
_eq: x-hasura-tenant-id
allow_aggregations: true
comment: ""

View File

@@ -0,0 +1,31 @@
# SPDX-FileCopyrightText: 2026 Contributors to the CitrineOS Project
#
# SPDX-License-Identifier: Apache-2.0
table:
name: MeterValues
schema: public
object_relationships:
- name: StopTransaction
using:
foreign_key_constraint_on: stopTransactionDatabaseId
- name: Tariff
using:
foreign_key_constraint_on: tariffId
- name: Tenant
using:
foreign_key_constraint_on: tenantId
- name: Transaction
using:
foreign_key_constraint_on: transactionDatabaseId
- name: TransactionEvent
using:
foreign_key_constraint_on: transactionEventId
select_permissions:
- role: user
permission:
columns: '*'
filter:
tenantId:
_eq: x-hasura-tenant-id
allow_aggregations: true
comment: ""

View File

@@ -0,0 +1,33 @@
# SPDX-FileCopyrightText: 2026 Contributors to the CitrineOS Project
#
# SPDX-License-Identifier: Apache-2.0
table:
name: OCPPMessages
schema: public
object_relationships:
- name: ChargingStation
using:
foreign_key_constraint_on: stationId
- name: OCPPMessage
using:
foreign_key_constraint_on: requestMessageId
- name: Tenant
using:
foreign_key_constraint_on: tenantId
array_relationships:
- name: OCPPMessages
using:
foreign_key_constraint_on:
column: requestMessageId
table:
name: OCPPMessages
schema: public
select_permissions:
- role: user
permission:
columns: '*'
filter:
tenantId:
_eq: x-hasura-tenant-id
allow_aggregations: true
comment: ""

View File

@@ -0,0 +1,22 @@
# SPDX-FileCopyrightText: 2026 Contributors to the CitrineOS Project
#
# SPDX-License-Identifier: Apache-2.0
table:
name: Reservations
schema: public
object_relationships:
- name: EvseType
using:
foreign_key_constraint_on: evseId
- name: Tenant
using:
foreign_key_constraint_on: tenantId
select_permissions:
- role: user
permission:
columns: '*'
filter:
tenantId:
_eq: x-hasura-tenant-id
allow_aggregations: true
comment: ""

View File

@@ -0,0 +1,22 @@
# SPDX-FileCopyrightText: 2026 Contributors to the CitrineOS Project
#
# SPDX-License-Identifier: Apache-2.0
table:
name: SalesTariffs
schema: public
object_relationships:
- name: ChargingSchedule
using:
foreign_key_constraint_on: chargingScheduleDatabaseId
- name: Tenant
using:
foreign_key_constraint_on: tenantId
select_permissions:
- role: user
permission:
columns: '*'
filter:
tenantId:
_eq: x-hasura-tenant-id
allow_aggregations: true
comment: ""

View File

@@ -0,0 +1,19 @@
# SPDX-FileCopyrightText: 2026 Contributors to the CitrineOS Project
#
# SPDX-License-Identifier: Apache-2.0
table:
name: SecurityEvents
schema: public
object_relationships:
- name: Tenant
using:
foreign_key_constraint_on: tenantId
select_permissions:
- role: user
permission:
columns: '*'
filter:
tenantId:
_eq: x-hasura-tenant-id
allow_aggregations: true
comment: ""

View File

@@ -0,0 +1,25 @@
# SPDX-FileCopyrightText: 2026 Contributors to the CitrineOS Project
#
# SPDX-License-Identifier: Apache-2.0
table:
name: SendLocalListAuthorizations
schema: public
object_relationships:
- name: LocalListAuthorization
using:
foreign_key_constraint_on: authorizationId
- name: SendLocalList
using:
foreign_key_constraint_on: sendLocalListId
- name: Tenant
using:
foreign_key_constraint_on: tenantId
select_permissions:
- role: user
permission:
columns: '*'
filter:
tenantId:
_eq: x-hasura-tenant-id
allow_aggregations: true
comment: ""

View File

@@ -0,0 +1,27 @@
# SPDX-FileCopyrightText: 2026 Contributors to the CitrineOS Project
#
# SPDX-License-Identifier: Apache-2.0
table:
name: SendLocalLists
schema: public
object_relationships:
- name: Tenant
using:
foreign_key_constraint_on: tenantId
array_relationships:
- name: SendLocalListAuthorizations
using:
foreign_key_constraint_on:
column: sendLocalListId
table:
name: SendLocalListAuthorizations
schema: public
select_permissions:
- role: user
permission:
columns: '*'
filter:
tenantId:
_eq: x-hasura-tenant-id
allow_aggregations: true
comment: ""

View File

@@ -0,0 +1,6 @@
# SPDX-FileCopyrightText: 2026 Contributors to the CitrineOS Project
#
# SPDX-License-Identifier: Apache-2.0
table:
name: SequelizeMeta
schema: public

View File

@@ -0,0 +1,34 @@
# SPDX-FileCopyrightText: 2026 Contributors to the CitrineOS Project
#
# SPDX-License-Identifier: Apache-2.0
table:
name: ServerNetworkProfiles
schema: public
object_relationships:
- name: Tenant
using:
foreign_key_constraint_on: tenantId
array_relationships:
- name: ChargingStationNetworkProfiles
using:
foreign_key_constraint_on:
column: websocketServerConfigId
table:
name: ChargingStationNetworkProfiles
schema: public
- name: SetNetworkProfiles
using:
foreign_key_constraint_on:
column: websocketServerConfigId
table:
name: SetNetworkProfiles
schema: public
select_permissions:
- role: user
permission:
columns: '*'
filter:
tenantId:
_eq: x-hasura-tenant-id
allow_aggregations: true
comment: ""

View File

@@ -0,0 +1,33 @@
# SPDX-FileCopyrightText: 2026 Contributors to the CitrineOS Project
#
# SPDX-License-Identifier: Apache-2.0
table:
name: SetNetworkProfiles
schema: public
object_relationships:
- name: ChargingStation
using:
foreign_key_constraint_on: stationId
- name: ServerNetworkProfile
using:
foreign_key_constraint_on: websocketServerConfigId
- name: Tenant
using:
foreign_key_constraint_on: tenantId
array_relationships:
- name: ChargingStationNetworkProfiles
using:
foreign_key_constraint_on:
column: setNetworkProfileId
table:
name: ChargingStationNetworkProfiles
schema: public
select_permissions:
- role: user
permission:
columns: '*'
filter:
tenantId:
_eq: x-hasura-tenant-id
allow_aggregations: true
comment: ""

View File

@@ -0,0 +1,51 @@
# SPDX-FileCopyrightText: 2026 Contributors to the CitrineOS Project
#
# SPDX-License-Identifier: Apache-2.0
table:
name: StartTransactions
schema: public
object_relationships:
- name: Connector
using:
foreign_key_constraint_on: connectorDatabaseId
- name: Tenant
using:
foreign_key_constraint_on: tenantId
- name: Transaction
using:
foreign_key_constraint_on: transactionDatabaseId
insert_permissions:
- role: user
permission:
check:
tenantId:
_eq: x-hasura-tenant-id
columns: '*'
comment: ""
select_permissions:
- role: user
permission:
columns: '*'
filter:
tenantId:
_eq: x-hasura-tenant-id
allow_aggregations: true
comment: ""
update_permissions:
- role: user
permission:
columns: '*'
filter:
tenantId:
_eq: x-hasura-tenant-id
check:
tenantId:
_eq: x-hasura-tenant-id
comment: ""
delete_permissions:
- role: user
permission:
filter:
tenantId:
_eq: x-hasura-tenant-id
comment: ""

View File

@@ -0,0 +1,30 @@
# SPDX-FileCopyrightText: 2026 Contributors to the CitrineOS Project
#
# SPDX-License-Identifier: Apache-2.0
table:
name: StatusNotifications
schema: public
object_relationships:
- name: ChargingStation
using:
foreign_key_constraint_on: stationId
- name: Tenant
using:
foreign_key_constraint_on: tenantId
array_relationships:
- name: LatestStatusNotifications
using:
foreign_key_constraint_on:
column: statusNotificationId
table:
name: LatestStatusNotifications
schema: public
select_permissions:
- role: user
permission:
columns: '*'
filter:
tenantId:
_eq: x-hasura-tenant-id
allow_aggregations: true
comment: ""

View File

@@ -0,0 +1,30 @@
# SPDX-FileCopyrightText: 2026 Contributors to the CitrineOS Project
#
# SPDX-License-Identifier: Apache-2.0
table:
name: StopTransactions
schema: public
object_relationships:
- name: Tenant
using:
foreign_key_constraint_on: tenantId
- name: Transaction
using:
foreign_key_constraint_on: transactionDatabaseId
array_relationships:
- name: MeterValues
using:
foreign_key_constraint_on:
column: stopTransactionDatabaseId
table:
name: MeterValues
schema: public
select_permissions:
- role: user
permission:
columns: '*'
filter:
tenantId:
_eq: x-hasura-tenant-id
allow_aggregations: true
comment: ""

View File

@@ -0,0 +1,19 @@
# SPDX-FileCopyrightText: 2026 Contributors to the CitrineOS Project
#
# SPDX-License-Identifier: Apache-2.0
table:
name: Subscriptions
schema: public
object_relationships:
- name: Tenant
using:
foreign_key_constraint_on: tenantId
select_permissions:
- role: user
permission:
columns: '*'
filter:
tenantId:
_eq: x-hasura-tenant-id
allow_aggregations: true
comment: ""

View File

@@ -0,0 +1,74 @@
# SPDX-FileCopyrightText: 2026 Contributors to the CitrineOS Project
#
# SPDX-License-Identifier: Apache-2.0
table:
name: Tariffs
schema: public
object_relationships:
- name: Tenant
using:
foreign_key_constraint_on: tenantId
array_relationships:
- name: Authorizations
using:
foreign_key_constraint_on:
column: tariffId
table:
name: Authorizations
schema: public
- name: Connectors
using:
foreign_key_constraint_on:
column: tariffId
table:
name: Connectors
schema: public
- name: MeterValues
using:
foreign_key_constraint_on:
column: tariffId
table:
name: MeterValues
schema: public
- name: Transactions
using:
foreign_key_constraint_on:
column: tariffId
table:
name: Transactions
schema: public
insert_permissions:
- role: user
permission:
check:
tenantId:
_eq: x-hasura-tenant-id
columns: '*'
comment: ""
select_permissions:
- role: user
permission:
columns: '*'
filter:
tenantId:
_eq: x-hasura-tenant-id
allow_aggregations: true
comment: ""
update_permissions:
- role: user
permission:
columns: '*'
filter:
tenantId:
_eq: x-hasura-tenant-id
check:
tenantId:
_eq: x-hasura-tenant-id
comment: ""
delete_permissions:
- role: user
permission:
filter:
tenantId:
_eq: x-hasura-tenant-id
comment: ""

View File

@@ -0,0 +1,60 @@
# SPDX-FileCopyrightText: 2026 Contributors to the CitrineOS Project
#
# SPDX-License-Identifier: Apache-2.0
table:
name: TenantPartners
schema: public
object_relationships:
- name: Tenant
using:
foreign_key_constraint_on: tenantId
array_relationships:
- name: AsyncJobStatuses
using:
foreign_key_constraint_on:
column: tenantPartnerId
table:
name: AsyncJobStatuses
schema: public
- name: Authorizations
using:
foreign_key_constraint_on:
column: tenantPartnerId
table:
name: Authorizations
schema: public
insert_permissions:
- role: user
permission:
check:
tenantId:
_eq: x-hasura-tenant-id
columns: '*'
comment: ""
select_permissions:
- role: user
permission:
columns: '*'
filter:
tenantId:
_eq: x-hasura-tenant-id
allow_aggregations: true
comment: ""
update_permissions:
- role: user
permission:
columns: '*'
filter:
tenantId:
_eq: x-hasura-tenant-id
check:
tenantId:
_eq: x-hasura-tenant-id
comment: ""
delete_permissions:
- role: user
permission:
filter:
tenantId:
_eq: x-hasura-tenant-id
comment: ""

View File

@@ -0,0 +1,366 @@
# SPDX-FileCopyrightText: 2026 Contributors to the CitrineOS Project
#
# SPDX-License-Identifier: Apache-2.0
table:
name: Tenants
schema: public
array_relationships:
- name: Authorizations
using:
foreign_key_constraint_on:
column: tenantId
table:
name: Authorizations
schema: public
- name: Boots
using:
foreign_key_constraint_on:
column: tenantId
table:
name: Boots
schema: public
- name: Certificates
using:
foreign_key_constraint_on:
column: tenantId
table:
name: Certificates
schema: public
- name: ChangeConfigurations
using:
foreign_key_constraint_on:
column: tenantId
table:
name: ChangeConfigurations
schema: public
- name: ChargingNeeds
using:
foreign_key_constraint_on:
column: tenantId
table:
name: ChargingNeeds
schema: public
- name: ChargingProfiles
using:
foreign_key_constraint_on:
column: tenantId
table:
name: ChargingProfiles
schema: public
- name: ChargingSchedules
using:
foreign_key_constraint_on:
column: tenantId
table:
name: ChargingSchedules
schema: public
- name: ChargingStationNetworkProfiles
using:
foreign_key_constraint_on:
column: tenantId
table:
name: ChargingStationNetworkProfiles
schema: public
- name: ChargingStationSecurityInfos
using:
foreign_key_constraint_on:
column: tenantId
table:
name: ChargingStationSecurityInfos
schema: public
- name: ChargingStationSequences
using:
foreign_key_constraint_on:
column: tenantId
table:
name: ChargingStationSequences
schema: public
- name: ChargingStations
using:
foreign_key_constraint_on:
column: tenantId
table:
name: ChargingStations
schema: public
- name: ComponentVariables
using:
foreign_key_constraint_on:
column: tenantId
table:
name: ComponentVariables
schema: public
- name: Components
using:
foreign_key_constraint_on:
column: tenantId
table:
name: Components
schema: public
- name: CompositeSchedules
using:
foreign_key_constraint_on:
column: tenantId
table:
name: CompositeSchedules
schema: public
- name: Connectors
using:
foreign_key_constraint_on:
column: tenantId
table:
name: Connectors
schema: public
- name: DeleteCertificateAttempts
using:
foreign_key_constraint_on:
column: tenantId
table:
name: DeleteCertificateAttempts
schema: public
- name: EventData
using:
foreign_key_constraint_on:
column: tenantId
table:
name: EventData
schema: public
- name: EvseTypes
using:
foreign_key_constraint_on:
column: tenantId
table:
name: EvseTypes
schema: public
- name: Evses
using:
foreign_key_constraint_on:
column: tenantId
table:
name: Evses
schema: public
- name: InstallCertificateAttempts
using:
foreign_key_constraint_on:
column: tenantId
table:
name: InstallCertificateAttempts
schema: public
- name: InstalledCertificates
using:
foreign_key_constraint_on:
column: tenantId
table:
name: InstalledCertificates
schema: public
- name: LatestStatusNotifications
using:
foreign_key_constraint_on:
column: tenantId
table:
name: LatestStatusNotifications
schema: public
- name: LocalListAuthorizations
using:
foreign_key_constraint_on:
column: tenantId
table:
name: LocalListAuthorizations
schema: public
- name: LocalListVersionAuthorizations
using:
foreign_key_constraint_on:
column: tenantId
table:
name: LocalListVersionAuthorizations
schema: public
- name: LocalListVersions
using:
foreign_key_constraint_on:
column: tenantId
table:
name: LocalListVersions
schema: public
- name: Locations
using:
foreign_key_constraint_on:
column: tenantId
table:
name: Locations
schema: public
- name: MessageInfos
using:
foreign_key_constraint_on:
column: tenantId
table:
name: MessageInfos
schema: public
- name: MeterValues
using:
foreign_key_constraint_on:
column: tenantId
table:
name: MeterValues
schema: public
- name: OCPPMessages
using:
foreign_key_constraint_on:
column: tenantId
table:
name: OCPPMessages
schema: public
- name: Reservations
using:
foreign_key_constraint_on:
column: tenantId
table:
name: Reservations
schema: public
- name: SalesTariffs
using:
foreign_key_constraint_on:
column: tenantId
table:
name: SalesTariffs
schema: public
- name: SecurityEvents
using:
foreign_key_constraint_on:
column: tenantId
table:
name: SecurityEvents
schema: public
- name: SendLocalListAuthorizations
using:
foreign_key_constraint_on:
column: tenantId
table:
name: SendLocalListAuthorizations
schema: public
- name: SendLocalLists
using:
foreign_key_constraint_on:
column: tenantId
table:
name: SendLocalLists
schema: public
- name: ServerNetworkProfiles
using:
foreign_key_constraint_on:
column: tenantId
table:
name: ServerNetworkProfiles
schema: public
- name: SetNetworkProfiles
using:
foreign_key_constraint_on:
column: tenantId
table:
name: SetNetworkProfiles
schema: public
- name: StartTransactions
using:
foreign_key_constraint_on:
column: tenantId
table:
name: StartTransactions
schema: public
- name: StatusNotifications
using:
foreign_key_constraint_on:
column: tenantId
table:
name: StatusNotifications
schema: public
- name: StopTransactions
using:
foreign_key_constraint_on:
column: tenantId
table:
name: StopTransactions
schema: public
- name: Subscriptions
using:
foreign_key_constraint_on:
column: tenantId
table:
name: Subscriptions
schema: public
- name: Tariffs
using:
foreign_key_constraint_on:
column: tenantId
table:
name: Tariffs
schema: public
- name: TenantPartners
using:
foreign_key_constraint_on:
column: tenantId
table:
name: TenantPartners
schema: public
- name: TransactionEvents
using:
foreign_key_constraint_on:
column: tenantId
table:
name: TransactionEvents
schema: public
- name: Transactions
using:
foreign_key_constraint_on:
column: tenantId
table:
name: Transactions
schema: public
- name: VariableAttributes
using:
foreign_key_constraint_on:
column: tenantId
table:
name: VariableAttributes
schema: public
- name: VariableCharacteristics
using:
foreign_key_constraint_on:
column: tenantId
table:
name: VariableCharacteristics
schema: public
- name: VariableMonitoringStatuses
using:
foreign_key_constraint_on:
column: tenantId
table:
name: VariableMonitoringStatuses
schema: public
- name: VariableMonitorings
using:
foreign_key_constraint_on:
column: tenantId
table:
name: VariableMonitorings
schema: public
- name: VariableStatuses
using:
foreign_key_constraint_on:
column: tenantId
table:
name: VariableStatuses
schema: public
- name: Variables
using:
foreign_key_constraint_on:
column: tenantId
table:
name: Variables
schema: public
select_permissions:
- role: user
permission:
columns: '*'
filter:
id:
_eq: x-hasura-tenant-id
allow_aggregations: true
comment: ""

View File

@@ -0,0 +1,33 @@
# SPDX-FileCopyrightText: 2026 Contributors to the CitrineOS Project
#
# SPDX-License-Identifier: Apache-2.0
table:
name: TransactionEvents
schema: public
object_relationships:
- name: EvseType
using:
foreign_key_constraint_on: evseId
- name: Tenant
using:
foreign_key_constraint_on: tenantId
- name: Transaction
using:
foreign_key_constraint_on: transactionDatabaseId
array_relationships:
- name: MeterValues
using:
foreign_key_constraint_on:
column: transactionEventId
table:
name: MeterValues
schema: public
select_permissions:
- role: user
permission:
columns: '*'
filter:
tenantId:
_eq: x-hasura-tenant-id
allow_aggregations: true
comment: ""

View File

@@ -0,0 +1,80 @@
# SPDX-FileCopyrightText: 2026 Contributors to the CitrineOS Project
#
# SPDX-License-Identifier: Apache-2.0
table:
name: Transactions
schema: public
object_relationships:
- name: Authorization
using:
foreign_key_constraint_on: authorizationId
- name: ChargingStation
using:
foreign_key_constraint_on: stationId
- name: Connector
using:
foreign_key_constraint_on: connectorId
- name: Evse
using:
foreign_key_constraint_on: evseId
- name: Location
using:
foreign_key_constraint_on: locationId
- name: StartTransaction
using:
foreign_key_constraint_on:
column: transactionDatabaseId
table:
name: StartTransactions
schema: public
- name: StopTransaction
using:
foreign_key_constraint_on:
column: transactionDatabaseId
table:
name: StopTransactions
schema: public
- name: Tariff
using:
foreign_key_constraint_on: tariffId
- name: Tenant
using:
foreign_key_constraint_on: tenantId
array_relationships:
- name: ChargingNeeds
using:
foreign_key_constraint_on:
column: transactionDatabaseId
table:
name: ChargingNeeds
schema: public
- name: ChargingProfiles
using:
foreign_key_constraint_on:
column: transactionDatabaseId
table:
name: ChargingProfiles
schema: public
- name: MeterValues
using:
foreign_key_constraint_on:
column: transactionDatabaseId
table:
name: MeterValues
schema: public
- name: TransactionEvents
using:
foreign_key_constraint_on:
column: transactionDatabaseId
table:
name: TransactionEvents
schema: public
select_permissions:
- role: user
permission:
columns: '*'
filter:
tenantId:
_eq: x-hasura-tenant-id
allow_aggregations: true
comment: ""

View File

@@ -0,0 +1,68 @@
# SPDX-FileCopyrightText: 2026 Contributors to the CitrineOS Project
#
# SPDX-License-Identifier: Apache-2.0
table:
name: VariableAttributes
schema: public
object_relationships:
- name: Boot
using:
foreign_key_constraint_on: bootConfigId
- name: ChargingStation
using:
foreign_key_constraint_on: stationId
- name: Component
using:
foreign_key_constraint_on: componentId
- name: EvseType
using:
foreign_key_constraint_on: evseDatabaseId
- name: Tenant
using:
foreign_key_constraint_on: tenantId
- name: Variable
using:
foreign_key_constraint_on: variableId
array_relationships:
- name: VariableStatuses
using:
foreign_key_constraint_on:
column: variableAttributeId
table:
name: VariableStatuses
schema: public
insert_permissions:
- role: user
permission:
check:
tenantId:
_eq: x-hasura-tenant-id
columns: '*'
comment: ""
select_permissions:
- role: user
permission:
columns: '*'
filter:
tenantId:
_eq: x-hasura-tenant-id
allow_aggregations: true
comment: ""
update_permissions:
- role: user
permission:
columns: '*'
filter:
tenantId:
_eq: x-hasura-tenant-id
check:
tenantId:
_eq: x-hasura-tenant-id
comment: ""
delete_permissions:
- role: user
permission:
filter:
tenantId:
_eq: x-hasura-tenant-id
comment: ""

View File

@@ -0,0 +1,22 @@
# SPDX-FileCopyrightText: 2026 Contributors to the CitrineOS Project
#
# SPDX-License-Identifier: Apache-2.0
table:
name: VariableCharacteristics
schema: public
object_relationships:
- name: Tenant
using:
foreign_key_constraint_on: tenantId
- name: Variable
using:
foreign_key_constraint_on: variableId
select_permissions:
- role: user
permission:
columns: '*'
filter:
tenantId:
_eq: x-hasura-tenant-id
allow_aggregations: true
comment: ""

View File

@@ -0,0 +1,22 @@
# SPDX-FileCopyrightText: 2026 Contributors to the CitrineOS Project
#
# SPDX-License-Identifier: Apache-2.0
table:
name: VariableMonitoringStatuses
schema: public
object_relationships:
- name: Tenant
using:
foreign_key_constraint_on: tenantId
- name: VariableMonitoring
using:
foreign_key_constraint_on: variableMonitoringId
select_permissions:
- role: user
permission:
columns: '*'
filter:
tenantId:
_eq: x-hasura-tenant-id
allow_aggregations: true
comment: ""

View File

@@ -0,0 +1,36 @@
# SPDX-FileCopyrightText: 2026 Contributors to the CitrineOS Project
#
# SPDX-License-Identifier: Apache-2.0
table:
name: VariableMonitorings
schema: public
object_relationships:
- name: ChargingStation
using:
foreign_key_constraint_on: stationId
- name: Component
using:
foreign_key_constraint_on: componentId
- name: Tenant
using:
foreign_key_constraint_on: tenantId
- name: Variable
using:
foreign_key_constraint_on: variableId
array_relationships:
- name: VariableMonitoringStatuses
using:
foreign_key_constraint_on:
column: variableMonitoringId
table:
name: VariableMonitoringStatuses
schema: public
select_permissions:
- role: user
permission:
columns: '*'
filter:
tenantId:
_eq: x-hasura-tenant-id
allow_aggregations: true
comment: ""

View File

@@ -0,0 +1,22 @@
# SPDX-FileCopyrightText: 2026 Contributors to the CitrineOS Project
#
# SPDX-License-Identifier: Apache-2.0
table:
name: VariableStatuses
schema: public
object_relationships:
- name: Tenant
using:
foreign_key_constraint_on: tenantId
- name: VariableAttribute
using:
foreign_key_constraint_on: variableAttributeId
select_permissions:
- role: user
permission:
columns: '*'
filter:
tenantId:
_eq: x-hasura-tenant-id
allow_aggregations: true
comment: ""

View File

@@ -0,0 +1,81 @@
# SPDX-FileCopyrightText: 2026 Contributors to the CitrineOS Project
#
# SPDX-License-Identifier: Apache-2.0
table:
name: Variables
schema: public
object_relationships:
- name: Tenant
using:
foreign_key_constraint_on: tenantId
- name: VariableCharacteristic
using:
foreign_key_constraint_on:
column: variableId
table:
name: VariableCharacteristics
schema: public
array_relationships:
- name: ComponentVariables
using:
foreign_key_constraint_on:
column: variableId
table:
name: ComponentVariables
schema: public
- name: EventData
using:
foreign_key_constraint_on:
column: variableId
table:
name: EventData
schema: public
- name: VariableAttributes
using:
foreign_key_constraint_on:
column: variableId
table:
name: VariableAttributes
schema: public
- name: VariableMonitorings
using:
foreign_key_constraint_on:
column: variableId
table:
name: VariableMonitorings
schema: public
insert_permissions:
- role: user
permission:
check:
tenantId:
_eq: x-hasura-tenant-id
columns: '*'
comment: ""
select_permissions:
- role: user
permission:
columns: '*'
filter:
tenantId:
_eq: x-hasura-tenant-id
allow_aggregations: true
comment: ""
update_permissions:
- role: user
permission:
columns: '*'
filter:
tenantId:
_eq: x-hasura-tenant-id
check:
tenantId:
_eq: x-hasura-tenant-id
comment: ""
delete_permissions:
- role: user
permission:
filter:
tenantId:
_eq: x-hasura-tenant-id
comment: ""

View File

@@ -0,0 +1,6 @@
# SPDX-FileCopyrightText: 2026 Contributors to the CitrineOS Project
#
# SPDX-License-Identifier: Apache-2.0
table:
name: geography_columns
schema: public

View File

@@ -0,0 +1,6 @@
# SPDX-FileCopyrightText: 2026 Contributors to the CitrineOS Project
#
# SPDX-License-Identifier: Apache-2.0
table:
name: geometry_columns
schema: public

View File

@@ -0,0 +1,6 @@
# SPDX-FileCopyrightText: 2026 Contributors to the CitrineOS Project
#
# SPDX-License-Identifier: Apache-2.0
table:
name: spatial_ref_sys
schema: public

View File

@@ -0,0 +1,59 @@
# SPDX-FileCopyrightText: 2026 Contributors to the CitrineOS Project
#
# SPDX-License-Identifier: Apache-2.0
- "!include public_AsyncJobStatuses.yaml"
- "!include public_Authorizations.yaml"
- "!include public_Boots.yaml"
- "!include public_Certificates.yaml"
- "!include public_ChangeConfigurations.yaml"
- "!include public_ChargingNeeds.yaml"
- "!include public_ChargingProfiles.yaml"
- "!include public_ChargingSchedules.yaml"
- "!include public_ChargingStationNetworkProfiles.yaml"
- "!include public_ChargingStationSecurityInfos.yaml"
- "!include public_ChargingStationSequences.yaml"
- "!include public_ChargingStations.yaml"
- "!include public_ComponentVariables.yaml"
- "!include public_Components.yaml"
- "!include public_CompositeSchedules.yaml"
- "!include public_Connectors.yaml"
- "!include public_DeleteCertificateAttempts.yaml"
- "!include public_EventData.yaml"
- "!include public_EvseTypes.yaml"
- "!include public_Evses.yaml"
- "!include public_InstallCertificateAttempts.yaml"
- "!include public_InstalledCertificates.yaml"
- "!include public_LatestStatusNotifications.yaml"
- "!include public_LocalListAuthorizations.yaml"
- "!include public_LocalListVersionAuthorizations.yaml"
- "!include public_LocalListVersions.yaml"
- "!include public_Locations.yaml"
- "!include public_MessageInfos.yaml"
- "!include public_MeterValues.yaml"
- "!include public_OCPPMessages.yaml"
- "!include public_Reservations.yaml"
- "!include public_SalesTariffs.yaml"
- "!include public_SecurityEvents.yaml"
- "!include public_SendLocalListAuthorizations.yaml"
- "!include public_SendLocalLists.yaml"
- "!include public_SequelizeMeta.yaml"
- "!include public_ServerNetworkProfiles.yaml"
- "!include public_SetNetworkProfiles.yaml"
- "!include public_StartTransactions.yaml"
- "!include public_StatusNotifications.yaml"
- "!include public_StopTransactions.yaml"
- "!include public_Subscriptions.yaml"
- "!include public_Tariffs.yaml"
- "!include public_TenantPartners.yaml"
- "!include public_Tenants.yaml"
- "!include public_TransactionEvents.yaml"
- "!include public_Transactions.yaml"
- "!include public_VariableAttributes.yaml"
- "!include public_VariableCharacteristics.yaml"
- "!include public_VariableMonitoringStatuses.yaml"
- "!include public_VariableMonitorings.yaml"
- "!include public_VariableStatuses.yaml"
- "!include public_Variables.yaml"
- "!include public_geography_columns.yaml"
- "!include public_geometry_columns.yaml"
- "!include public_spatial_ref_sys.yaml"

View File

@@ -0,0 +1,4 @@
# SPDX-FileCopyrightText: 2026 Contributors to the CitrineOS Project
#
# SPDX-License-Identifier: Apache-2.0
disabled_for_roles: []

View File

@@ -0,0 +1,4 @@
# SPDX-FileCopyrightText: 2026 Contributors to the CitrineOS Project
#
# SPDX-License-Identifier: Apache-2.0
[]

View File

@@ -0,0 +1,4 @@
# SPDX-FileCopyrightText: 2026 Contributors to the CitrineOS Project
#
# SPDX-License-Identifier: Apache-2.0
{}

View File

@@ -0,0 +1,4 @@
# SPDX-FileCopyrightText: 2026 Contributors to the CitrineOS Project
#
# SPDX-License-Identifier: Apache-2.0
{}

View File

@@ -0,0 +1,4 @@
# SPDX-FileCopyrightText: 2026 Contributors to the CitrineOS Project
#
# SPDX-License-Identifier: Apache-2.0
{}

View File

@@ -0,0 +1,4 @@
# SPDX-FileCopyrightText: 2026 Contributors to the CitrineOS Project
#
# SPDX-License-Identifier: Apache-2.0
[]

View File

@@ -0,0 +1,4 @@
# SPDX-FileCopyrightText: 2026 Contributors to the CitrineOS Project
#
# SPDX-License-Identifier: Apache-2.0
[]

View File

@@ -0,0 +1,4 @@
# SPDX-FileCopyrightText: 2026 Contributors to the CitrineOS Project
#
# SPDX-License-Identifier: Apache-2.0
[]

View File

@@ -0,0 +1,4 @@
# SPDX-FileCopyrightText: 2026 Contributors to the CitrineOS Project
#
# SPDX-License-Identifier: Apache-2.0
version: 3

File diff suppressed because it is too large Load Diff

View File

@@ -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);
},
};

View File

@@ -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 });
},
};

View File

@@ -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);
}
}
},
};

View File

@@ -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);
}
},
};

View File

@@ -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);
}
}
},
};

View File

@@ -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(() => {});
},
};

View File

@@ -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');
},
};

View File

@@ -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');
},
};

View File

@@ -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";
`);
},
};

View File

@@ -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);
}
},
};

View File

@@ -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');
},
};

View File

@@ -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',
});
},
};

View File

@@ -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,
});
},
};

View File

@@ -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');
},
};

View File

@@ -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
},
};

Some files were not shown because too many files have changed in this diff Show More