Skip to main content

Upgrade

Overview

To upgrade io.Manager, you must update the @interopio/manager, @interopio/manager-admin-ui, and @interopio/manager-api packages in your project via npm.

If you aren't using automatic schema migration for PostgreSQL and Microsoft SQL Server databases, you must create the databases manually by using the provided schema creation scripts.

As of version 4.0.0, the @interopio/manager and @interopio/manager-admin-ui packages are compatible with each other only when their major and minor versions match. For example, io.Manager Server 4.0.5 is compatible with io.Manager Admin UI 4.0.2. Pairing io.Manager Server 4.0.0 with io.Manager Admin UI 4.1.0 (or vice versa) isn't supported. This change applies only to the relationship between the io.Manager Server and the io.Manager Admin UI - compatibility with @interopio/manager-api, io.Connect Desktop, and io.Connect Browser is unchanged.

Apart from that pairing, and unless explicitly stated in the changelog or on this page, all io.Manager packages support backward and forward compatibility between each other and with the io.Connect platforms. This means that older versions of io.Connect Desktop and io.Connect Browser will operate properly with newer versions of io.Manager and vice versa.

⚠️ Note that before upgrading, it's highly recommended to review the changelog and this section for details about the release to which you want to upgrade and also for all in-between releases. It's important to consider any breaking changes or necessary migration steps related to the packages or to the databases in order to ensure a smoother transition to the targeted version.

@interopio/manager

4.0.0

  • Dropped support for Node.js 20. io.Manager now requires Node.js 22 LTS or 24 LTS.

  • Dropped support for MongoDB 6. io.Manager now requires MongoDB 7 or later.

  • The io.Manager Server is now compatible with the io.Manager Admin UI only when their major and minor versions match. For example, io.Manager Server 4.0.x is compatible with io.Manager Admin UI 4.0.x (any patch), but io.Manager Server 4.0.0 isn't compatible with io.Manager Admin UI 4.1.0. Make sure to upgrade both packages together so that their major and minor versions stay aligned. Compatibility with @interopio/manager-api, io.Connect Desktop, and io.Connect Browser is unchanged.

  • Removed the custom-store extension point. The "custom" value for the type property of the store top-level key in the configuration object for initializing the io.Manager Server is no longer supported, along with the corresponding CustomStoreConfig configuration object. Existing deployments that previously plugged in a custom store must switch to one of the three built-in database backends - MongoStoreConfig, PostgreSQLStoreConfig, or MSSQLStoreConfig.

  • Removed the usersStore property from the MongoStoreConfig, PostgreSQLStoreConfig, and MSSQLStoreConfig configuration objects. A separate users store can no longer be supplied alongside one of the built-in database backends.

  • Removed the StoreBase, UsersStoreBase, and BLOBStoreBase abstract classes from the public API. Custom implementations of these classes can no longer be supplied to the io.Manager Server.

  • Added a system_state database table for storing server-managed system state (for PostgreSQL and Microsoft SQL Server only).

⚠️ Note that users who don't use automatic schema migration must create the system_state table manually by running the following scripts.

For PostgreSQL:

-- Server-wide state shared by every io.Manager instance, one row per named entry.
CREATE TABLE IF NOT EXISTS system_state
(
    system_state_id SERIAL PRIMARY KEY, -- Primary key. Auto-assigned by the database and not used to identify the row outside it.
    name VARCHAR(255) NOT NULL, -- Name of the entry. Unique.
    value JSONB NOT NULL, -- The entry value. JSON.
    version INT NOT NULL, -- Revision of the entry, raised on every write so concurrent instances cannot overwrite each other.
    CONSTRAINT system_state_name_unique UNIQUE (name)
);

For Microsoft SQL Server:

-- Server-wide state shared by every io.Manager instance, one row per named entry.
IF NOT EXISTS (SELECT * FROM sysobjects WHERE name='system_state' AND xtype='U')
CREATE TABLE system_state
(
    system_state_id INT IDENTITY PRIMARY KEY, -- Primary key. Auto-assigned by the database and not used to identify the row outside it.
    name NVARCHAR(255) NOT NULL, -- Name of the entry. Unique.
    value NVARCHAR(MAX) NOT NULL, -- The entry value. JSON, stored as text.
    version INT NOT NULL, -- Revision of the entry, raised on every write so concurrent instances cannot overwrite each other.
    CONSTRAINT system_state_name_unique UNIQUE (name)
);
  • Sessions and command descriptions are now scoped to io.Connect Desktop or io.Connect Browser. Sessions have separate product and product-specific productVersion fields.

⚠️ Note that automatic schema migration applies these changes for MongoDB, PostgreSQL, and Microsoft SQL Server. If you are managing a PostgreSQL or a Microsoft SQL Server schema manually, run the corresponding script below before starting the upgraded server.

For PostgreSQL:

ALTER TABLE sessions
    ADD COLUMN IF NOT EXISTS product VARCHAR(255), -- The io.Connect product that opened the session.
    ADD COLUMN IF NOT EXISTS "productVersion" VARCHAR(255); -- Version of that product. Determines which commands the session supports.

UPDATE sessions
SET
    product = CASE
        WHEN glue->'core'->'platform'->>'version' IS NOT NULL
            OR glue->>'version' LIKE 'io.connect.browser-%'
            THEN 'ioCB'
        ELSE 'ioCD'
    END,
    "productVersion" = CASE
        WHEN glue->'core'->'platform'->>'version' IS NOT NULL
            THEN glue->'core'->'platform'->>'version'
        WHEN glue->>'version' LIKE 'io.connect.browser-%'
            THEN substring(glue->>'version' from 20)
        ELSE glue->>'version'
    END;

ALTER TABLE sessions
    ALTER COLUMN product SET NOT NULL,
    ALTER COLUMN "productVersion" SET NOT NULL;

ALTER TABLE commands_for_version
    ADD COLUMN IF NOT EXISTS product VARCHAR(255); -- The io.Connect product. Unique together with the version.

ALTER TABLE commands_for_version
    DROP CONSTRAINT IF EXISTS commands_for_version_version_unique;

-- Only rows that have not been re-keyed yet: product is derived from version while version
-- itself is rewritten, so a second pass would classify an already-re-keyed row as ioCD.
UPDATE commands_for_version
SET
    product = CASE
        WHEN version LIKE 'io.connect.browser-%' THEN 'ioCB'
        ELSE 'ioCD'
    END,
    version = CASE
        WHEN version LIKE 'io.connect.browser-%'
            THEN substring(version from 20)
        ELSE version
    END
WHERE product IS NULL;

ALTER TABLE commands_for_version
    ALTER COLUMN product SET NOT NULL;

ALTER TABLE commands_for_version
    ADD CONSTRAINT commands_for_version_product_version_unique
    UNIQUE (product, version);

For Microsoft SQL Server:

IF NOT EXISTS (
    SELECT *
    FROM   sys.columns
    WHERE  object_id = OBJECT_ID(N'[dbo].[sessions]')
    AND name = 'product'
)
ALTER TABLE sessions ADD product NVARCHAR(255) NULL; -- The io.Connect product that opened the session.
GO

IF NOT EXISTS (
    SELECT *
    FROM   sys.columns
    WHERE  object_id = OBJECT_ID(N'[dbo].[sessions]')
    AND name = 'productVersion'
)
ALTER TABLE sessions ADD "productVersion" NVARCHAR(255) NULL; -- Version of that product. Determines which commands the session supports.
GO

UPDATE sessions
SET
    product = CASE
        WHEN JSON_VALUE(glue, '$.core.platform.version') IS NOT NULL
            OR JSON_VALUE(glue, '$.version') LIKE 'io.connect.browser-%'
            THEN 'ioCB'
        ELSE 'ioCD'
    END,
    "productVersion" = CASE
        WHEN JSON_VALUE(glue, '$.core.platform.version') IS NOT NULL
            THEN JSON_VALUE(glue, '$.core.platform.version')
        WHEN JSON_VALUE(glue, '$.version') LIKE 'io.connect.browser-%'
            THEN SUBSTRING(
                JSON_VALUE(glue, '$.version'),
                20,
                LEN(JSON_VALUE(glue, '$.version'))
            )
        ELSE JSON_VALUE(glue, '$.version')
    END;

ALTER TABLE sessions ALTER COLUMN product NVARCHAR(255) NOT NULL;
ALTER TABLE sessions ALTER COLUMN "productVersion" NVARCHAR(255) NOT NULL;

IF NOT EXISTS (
    SELECT *
    FROM   sys.columns
    WHERE  object_id = OBJECT_ID(N'[dbo].[commands_for_version]')
    AND name = 'product'
)
ALTER TABLE commands_for_version ADD product NVARCHAR(255) NULL; -- The io.Connect product. Unique together with the version.
GO

ALTER TABLE commands_for_version
    DROP CONSTRAINT IF EXISTS commands_for_version_version_unique;

-- Only rows that have not been re-keyed yet: product is derived from version while version
-- itself is rewritten, so a second pass would classify an already-re-keyed row as ioCD.
UPDATE commands_for_version
SET
    product = CASE
        WHEN version LIKE 'io.connect.browser-%' THEN 'ioCB'
        ELSE 'ioCD'
    END,
    version = CASE
        WHEN version LIKE 'io.connect.browser-%'
            THEN SUBSTRING(version, 20, LEN(version))
        ELSE version
    END
WHERE product IS NULL;

ALTER TABLE commands_for_version
    ALTER COLUMN product NVARCHAR(255) NOT NULL;

ALTER TABLE commands_for_version
    ADD CONSTRAINT commands_for_version_product_version_unique
    UNIQUE (product, version);
  • Added a layouts_advanced database table for storing advanced Layouts (for PostgreSQL and Microsoft SQL Server only).

⚠️ Note that users who don't use automatic schema migration must create the layouts_advanced table manually by running the following scripts.

For PostgreSQL:

-- Layouts saved by an io.Connect platform, with per-entity access control.
CREATE TABLE IF NOT EXISTS layouts_advanced
(
    layouts_advanced_id SERIAL PRIMARY KEY, -- Primary key. Auto-assigned by the database and not used to identify the row outside it.
    id VARCHAR(255) NOT NULL, -- Public identifier of the layout. Unique.
    type VARCHAR(255) NOT NULL, -- Kind of layout, for example 'Global' or 'Workspace'.
    name VARCHAR(255) NOT NULL, -- Name of the layout. Unique per type per owner while private, and unique per type across all owners once shared or public.
    disabled BOOLEAN NOT NULL, -- Whether the layout is withheld from all users.
    "accessLevel" VARCHAR(255) NOT NULL, -- Who can reach the layout: 'private', 'shared' or 'public'.
    "owner" VARCHAR(255) NOT NULL, -- User the layout belongs to.
    "accessInfo" JSON NULL, -- Users and groups granted access, with the level granted to each. Present only for shared layouts. JSON.
    definition JSON NOT NULL, -- The layout payload as saved by the io.Connect platform. JSON.
    "createdBy" VARCHAR(255) NOT NULL, -- User that created the layout.
    "createdOn" BIGINT NOT NULL, -- Creation time. Epoch milliseconds (UTC).
    "lastModifiedBy" VARCHAR(255) NULL, -- User that last modified the layout.
    "lastModifiedOn" BIGINT NULL, -- Time of the last modification. Epoch milliseconds (UTC).
    CONSTRAINT layouts_advanced_id_unique UNIQUE (id)
);

CREATE UNIQUE INDEX IF NOT EXISTS layouts_advanced__name__type__owner__unique
    ON layouts_advanced (name, type, owner)
    WHERE "accessLevel" = 'private';

CREATE UNIQUE INDEX IF NOT EXISTS layouts_advanced__name__type__nonprivate__unique
    ON layouts_advanced (name, type)
    WHERE "accessLevel" IN ('shared', 'public');

For Microsoft SQL Server:

-- Layouts saved by an io.Connect platform, with per-entity access control.
IF NOT EXISTS (SELECT * FROM sysobjects WHERE name='layouts_advanced' AND xtype='U')
CREATE TABLE layouts_advanced
(
    layouts_advanced_id INT IDENTITY PRIMARY KEY, -- Primary key. Auto-assigned by the database and not used to identify the row outside it.
    id NVARCHAR(255) NOT NULL, -- Public identifier of the layout. Unique.
    type NVARCHAR(255) NOT NULL, -- Kind of layout, for example 'Global' or 'Workspace'.
    name NVARCHAR(255) NOT NULL, -- Name of the layout. Unique per type per owner while private, and unique per type across all owners once shared or public.
    disabled BIT NOT NULL, -- Whether the layout is withheld from all users. Boolean, stored as 0 or 1.
    accessLevel NVARCHAR(255) NOT NULL, -- Who can reach the layout: 'private', 'shared' or 'public'.
    owner NVARCHAR(255) NOT NULL, -- User the layout belongs to.
    "accessInfo" NVARCHAR(MAX) NULL, -- Users and groups granted access, with the level granted to each. Present only for shared layouts. JSON, stored as text.
    definition NVARCHAR(MAX) NOT NULL, -- The layout payload as saved by the io.Connect platform. JSON, stored as text.
    "createdBy" NVARCHAR(255) NOT NULL, -- User that created the layout.
    "createdOn" BIGINT NOT NULL, -- Creation time. Epoch milliseconds (UTC).
    "lastModifiedBy" NVARCHAR(255) NULL, -- User that last modified the layout.
    "lastModifiedOn" BIGINT NULL, -- Time of the last modification. Epoch milliseconds (UTC).
    CONSTRAINT layouts_advanced_id_unique UNIQUE (id)
);

IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'layouts_advanced__name__type__owner__unique' AND object_id = OBJECT_ID('layouts_advanced'))
CREATE UNIQUE INDEX layouts_advanced__name__type__owner__unique
    ON layouts_advanced (name, type, owner)
    WHERE accessLevel = 'private';

IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'layouts_advanced__name__type__nonprivate__unique' AND object_id = OBJECT_ID('layouts_advanced'))
CREATE UNIQUE INDEX layouts_advanced__name__type__nonprivate__unique
    ON layouts_advanced (name, type)
    WHERE accessLevel IN ('shared', 'public');
  • Added an others database column to the users table (for PostgreSQL and Microsoft SQL Server only).

⚠️ Note that users who don't use automatic schema migration must create the others column manually by running the following scripts.

For PostgreSQL:

ALTER TABLE users
    ADD COLUMN IF NOT EXISTS others JSONB NULL; -- Per-user timestamps that do not have a column of their own. JSON.

For Microsoft SQL Server:

IF NOT EXISTS (
    SELECT *
    FROM   sys.columns
    WHERE  object_id = OBJECT_ID(N'[dbo].[users]')
    AND name = 'others'
)
ALTER TABLE users
    ADD others NVARCHAR(MAX) NULL; -- Per-user timestamps that do not have a column of their own. JSON, stored as text.
  • Added description and expandsTo database columns to the groups table (for PostgreSQL and Microsoft SQL Server only).

⚠️ Note that users who don't use automatic schema migration must create the description and expandsTo columns manually by running the following scripts.

For PostgreSQL:

ALTER TABLE groups
    ADD COLUMN IF NOT EXISTS description TEXT NULL, -- Description of the group.
    ADD COLUMN IF NOT EXISTS "expandsTo" VARCHAR(255)[] NOT NULL DEFAULT '{}'::varchar[]; -- Groups this group also grants membership of. Empty when it expands into no others. Array of strings.

For Microsoft SQL Server:

IF NOT EXISTS (
    SELECT *
    FROM   sys.columns
    WHERE  object_id = OBJECT_ID(N'[dbo].[groups]')
    AND name = 'description'
)
ALTER TABLE groups
    ADD description NVARCHAR(MAX) NULL; -- Description of the group.

IF NOT EXISTS (
    SELECT *
    FROM   sys.columns
    WHERE  object_id = OBJECT_ID(N'[dbo].[groups]')
    AND name = 'expandsTo'
)
ALTER TABLE groups
    ADD expandsTo NVARCHAR(MAX) NOT NULL CONSTRAINT DF_groups_expandsTo DEFAULT '[]'; -- Groups this group also grants membership of. Empty when it expands into no others. JSON array of strings, stored as text.
  • Added a migrated database column to the layouts table (for PostgreSQL and Microsoft SQL Server only). The Layout migration uses it to mark legacy Layouts that have already been migrated to advanced Layouts.

⚠️ Note that users who don't use automatic schema migration must create the migrated column manually by running the following scripts.

For PostgreSQL:

ALTER TABLE layouts
    ADD COLUMN IF NOT EXISTS migrated BOOLEAN NULL; -- Whether this layout has already been migrated to the layouts_advanced table.

For Microsoft SQL Server:

IF NOT EXISTS (
    SELECT *
    FROM   sys.columns
    WHERE  object_id = OBJECT_ID(N'[dbo].[layouts]')
    AND name = 'migrated'
)
ALTER TABLE layouts
    ADD migrated BIT; -- Whether this layout has already been migrated to the layouts_advanced table. Boolean, stored as 0 or 1.
  • On Microsoft SQL Server, all VARCHAR(255) columns are now NVARCHAR(255), so usernames, Layout names, and every other text field store the full Unicode range. Previously, a character outside the code page of the database collation, such as Cyrillic or Greek, was stored as ?.

⚠️ Note that users who don't use automatic schema migration must convert the columns manually by running the following script, after applying every schema script above it on this page and before creating the username indexes below. Each unique constraint is dropped and recreated around the conversion, because a column can't be altered while a constraint references it.

ALTER TABLE app DROP CONSTRAINT app_name_unique;
ALTER TABLE audit DROP CONSTRAINT audit_id_unique;
ALTER TABLE blobs DROP CONSTRAINT blobs_id_unique;
ALTER TABLE commands DROP CONSTRAINT commands_id_unique;
ALTER TABLE commands_for_version DROP CONSTRAINT commands_for_version_product_version_unique;
ALTER TABLE crashes DROP CONSTRAINT crashes_id_unique;
ALTER TABLE feedback DROP CONSTRAINT feedback_id_unique;
ALTER TABLE groups DROP CONSTRAINT groups_name_unique;
ALTER TABLE layouts DROP CONSTRAINT layouts_id_unique;
ALTER TABLE layouts DROP CONSTRAINT layouts__name__type__owner__unique;
ALTER TABLE machines DROP CONSTRAINT machines_id_unique;
ALTER TABLE prefs DROP CONSTRAINT prefs_id_unique;
ALTER TABLE prefs DROP CONSTRAINT prefs__app__user__unique;
ALTER TABLE sessions DROP CONSTRAINT sessions_id_unique;
ALTER TABLE users DROP CONSTRAINT users_id_unique;

ALTER TABLE app ALTER COLUMN name NVARCHAR(255) NOT NULL;
ALTER TABLE app ALTER COLUMN [createdBy] NVARCHAR(255) NOT NULL;
ALTER TABLE app ALTER COLUMN [lastModifiedBy] NVARCHAR(255) NULL;
ALTER TABLE audit ALTER COLUMN id NVARCHAR(255) NOT NULL;
ALTER TABLE audit ALTER COLUMN [user] NVARCHAR(255) NULL;
ALTER TABLE audit ALTER COLUMN parent NVARCHAR(255) NULL;
ALTER TABLE audit ALTER COLUMN session NVARCHAR(255) NULL;
ALTER TABLE audit ALTER COLUMN server NVARCHAR(255) NULL;
ALTER TABLE audit ALTER COLUMN [entityType] NVARCHAR(255) NOT NULL;
ALTER TABLE audit ALTER COLUMN [entityId] NVARCHAR(255) NULL;
ALTER TABLE audit ALTER COLUMN [entityDisplayName] NVARCHAR(255) NULL;
ALTER TABLE audit ALTER COLUMN operation NVARCHAR(255) NOT NULL;
ALTER TABLE audit ALTER COLUMN [operationComment] NVARCHAR(255) NULL;
ALTER TABLE blobs ALTER COLUMN [type] NVARCHAR(255) NOT NULL;
ALTER TABLE blobs ALTER COLUMN id NVARCHAR(255) NOT NULL;
ALTER TABLE blobs ALTER COLUMN [fileName] NVARCHAR(255) NOT NULL;
ALTER TABLE commands ALTER COLUMN id NVARCHAR(255) NOT NULL;
ALTER TABLE commands ALTER COLUMN [user] NVARCHAR(255) NOT NULL;
ALTER TABLE commands ALTER COLUMN status NVARCHAR(255) NOT NULL;
ALTER TABLE commands ALTER COLUMN session NVARCHAR(255) NOT NULL;
ALTER TABLE commands ALTER COLUMN machine NVARCHAR(255) NOT NULL;
ALTER TABLE commands ALTER COLUMN [createdBy] NVARCHAR(255) NOT NULL;
ALTER TABLE commands ALTER COLUMN command NVARCHAR(255) NOT NULL;
ALTER TABLE commands ALTER COLUMN [commandParams] NVARCHAR(255) NULL;
ALTER TABLE commands ALTER COLUMN [resultType] NVARCHAR(255) NULL;
ALTER TABLE commands_for_version ALTER COLUMN version NVARCHAR(255) NOT NULL;
ALTER TABLE crashes ALTER COLUMN id NVARCHAR(255) NOT NULL;
ALTER TABLE crashes ALTER COLUMN [user] NVARCHAR(255) NULL;
ALTER TABLE feedback ALTER COLUMN id NVARCHAR(255) NOT NULL;
ALTER TABLE feedback ALTER COLUMN [user] NVARCHAR(255) NOT NULL;
ALTER TABLE feedback ALTER COLUMN session NVARCHAR(255) NOT NULL;
ALTER TABLE groups ALTER COLUMN name NVARCHAR(255) NOT NULL;
ALTER TABLE layouts ALTER COLUMN id NVARCHAR(255) NOT NULL;
ALTER TABLE layouts ALTER COLUMN [type] NVARCHAR(255) NOT NULL;
ALTER TABLE layouts ALTER COLUMN name NVARCHAR(255) NOT NULL;
ALTER TABLE layouts ALTER COLUMN [owner] NVARCHAR(255) NULL;
ALTER TABLE layouts ALTER COLUMN [createdBy] NVARCHAR(255) NOT NULL;
ALTER TABLE layouts ALTER COLUMN [lastModifiedBy] NVARCHAR(255) NULL;
ALTER TABLE machines ALTER COLUMN id NVARCHAR(255) NOT NULL;
ALTER TABLE machines ALTER COLUMN [user] NVARCHAR(255) NOT NULL;
ALTER TABLE machines ALTER COLUMN name NVARCHAR(255) NULL;
ALTER TABLE prefs ALTER COLUMN id NVARCHAR(255) NOT NULL;
ALTER TABLE prefs ALTER COLUMN app NVARCHAR(255) NOT NULL;
ALTER TABLE prefs ALTER COLUMN [user] NVARCHAR(255) NOT NULL;
ALTER TABLE sessions ALTER COLUMN id NVARCHAR(255) NOT NULL;
ALTER TABLE sessions ALTER COLUMN machine NVARCHAR(255) NOT NULL;
ALTER TABLE sessions ALTER COLUMN [user] NVARCHAR(255) NOT NULL;
ALTER TABLE sessions ALTER COLUMN [closeReason] NVARCHAR(255) NULL;
ALTER TABLE users ALTER COLUMN id NVARCHAR(255) NOT NULL;
ALTER TABLE users ALTER COLUMN email NVARCHAR(255) NULL;
ALTER TABLE users ALTER COLUMN password NVARCHAR(255) NULL;
ALTER TABLE users ALTER COLUMN [firstName] NVARCHAR(255) NULL;
ALTER TABLE users ALTER COLUMN [lastName] NVARCHAR(255) NULL;

ALTER TABLE app ADD CONSTRAINT app_name_unique UNIQUE (name);
ALTER TABLE audit ADD CONSTRAINT audit_id_unique UNIQUE (id);
ALTER TABLE blobs ADD CONSTRAINT blobs_id_unique UNIQUE (id);
ALTER TABLE commands ADD CONSTRAINT commands_id_unique UNIQUE (id);
ALTER TABLE commands_for_version ADD CONSTRAINT commands_for_version_product_version_unique UNIQUE (product, version);
ALTER TABLE crashes ADD CONSTRAINT crashes_id_unique UNIQUE (id);
ALTER TABLE feedback ADD CONSTRAINT feedback_id_unique UNIQUE (id);
ALTER TABLE groups ADD CONSTRAINT groups_name_unique UNIQUE (name);
ALTER TABLE layouts ADD CONSTRAINT layouts_id_unique UNIQUE (id);
ALTER TABLE layouts ADD CONSTRAINT layouts__name__type__owner__unique UNIQUE ([name], [type], [owner]);
ALTER TABLE machines ADD CONSTRAINT machines_id_unique UNIQUE (id);
ALTER TABLE prefs ADD CONSTRAINT prefs_id_unique UNIQUE (id);
ALTER TABLE prefs ADD CONSTRAINT prefs__app__user__unique UNIQUE ([app], [user]);
ALTER TABLE sessions ADD CONSTRAINT sessions_id_unique UNIQUE (id);
ALTER TABLE users ADD CONSTRAINT users_id_unique UNIQUE (id);
  • Added database indexes for every column a username is resolved by - users.id, layouts.owner, prefs.user, and machines.user - in both a case-folded and an exact form, so that both settings of username_case_sensitive use an index instead of scanning the table. On Microsoft SQL Server, the case-folded form is a persisted computed column with an index on it, because Microsoft SQL Server has no expression indexes.

⚠️ Note that users who don't use automatic schema migration must create the indexes manually by running the following scripts, after the NVARCHAR conversion above. On MongoDB, the indexes are created by the server on startup and don't need any manual step.

For PostgreSQL:

CREATE INDEX IF NOT EXISTS users__id__lower ON users (lower(id));
CREATE INDEX IF NOT EXISTS layouts__owner__lower ON layouts (lower("owner"));
CREATE INDEX IF NOT EXISTS layouts__owner ON layouts ("owner");
CREATE INDEX IF NOT EXISTS prefs__user__lower__app ON prefs (lower("user"), app);
CREATE INDEX IF NOT EXISTS prefs__user ON prefs ("user");
CREATE INDEX IF NOT EXISTS machines__user__lower ON machines (lower("user"));
CREATE INDEX IF NOT EXISTS machines__user ON machines ("user");

For Microsoft SQL Server:

IF NOT EXISTS (
    SELECT *
    FROM   sys.columns
    WHERE  object_id = OBJECT_ID(N'[dbo].[users]')
    AND name = 'id_lower'
)
ALTER TABLE users ADD id_lower AS LOWER(id) PERSISTED; -- The identifier, lowercased, for resolving a username when username_case_sensitive is false. Derived by the database.
GO

IF NOT EXISTS (
    SELECT *
    FROM   sys.columns
    WHERE  object_id = OBJECT_ID(N'[dbo].[layouts]')
    AND name = 'owner_lower'
)
ALTER TABLE layouts ADD owner_lower AS LOWER([owner]) PERSISTED; -- The owner, lowercased, for resolving a username when username_case_sensitive is false. Derived by the database.
GO

IF NOT EXISTS (
    SELECT *
    FROM   sys.columns
    WHERE  object_id = OBJECT_ID(N'[dbo].[prefs]')
    AND name = 'user_lower'
)
ALTER TABLE prefs ADD user_lower AS LOWER([user]) PERSISTED; -- The user, lowercased, for resolving a username when username_case_sensitive is false. Derived by the database.
GO

IF NOT EXISTS (
    SELECT *
    FROM   sys.columns
    WHERE  object_id = OBJECT_ID(N'[dbo].[machines]')
    AND name = 'user_lower'
)
ALTER TABLE machines ADD user_lower AS LOWER([user]) PERSISTED; -- The user, lowercased, for resolving a username when username_case_sensitive is false. Derived by the database.
GO

IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'users__id__lower' AND object_id = OBJECT_ID('users'))
    CREATE INDEX users__id__lower ON users (id_lower);

IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'layouts__owner__lower' AND object_id = OBJECT_ID('layouts'))
    CREATE INDEX layouts__owner__lower ON layouts (owner_lower);

IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'layouts__owner' AND object_id = OBJECT_ID('layouts'))
    CREATE INDEX layouts__owner ON layouts ([owner]);

IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'prefs__user__lower__app' AND object_id = OBJECT_ID('prefs'))
    CREATE INDEX prefs__user__lower__app ON prefs (user_lower, app);

IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'prefs__user' AND object_id = OBJECT_ID('prefs'))
    CREATE INDEX prefs__user ON prefs ([user]);

IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'machines__user__lower' AND object_id = OBJECT_ID('machines'))
    CREATE INDEX machines__user__lower ON machines (user_lower);

IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'machines__user' AND object_id = OBJECT_ID('machines'))
    CREATE INDEX machines__user ON machines ([user]);
  • The GET /api/crashes/{id}/dump endpoint of the io.Manager Server REST API now requires authentication and the IO_MANAGER:CRASHES:READ permission group. External callers must send valid credentials according to the configured auth provider, as a user whose permission groups include IO_MANAGER:CRASHES:READ. Alternatively, use the new Crashes.getDump(id): Promise<Blob> method on @interopio/manager-api, which reuses the client's existing authentication and returns the dump as a Blob.

  • Added an encoding database column to the blobs table (for PostgreSQL and Microsoft SQL Server only).

⚠️ Note that users who don't use automatic schema migration must create the encoding column and mark the existing rows manually by running the following scripts.

For PostgreSQL:

ALTER TABLE blobs
    ADD COLUMN IF NOT EXISTS encoding VARCHAR(20); -- Encoding of the content: "raw" when it is the file's own bytes, "base64" when it is those bytes base64-encoded as text. Absent on content stored before the encoding was recorded.

-- Only rows that predate the column are NULL, so re-running this script never
-- reclassifies files stored after the upgrade.
UPDATE blobs
    SET encoding = 'base64'
    WHERE type = 'command_result' AND encoding IS NULL;

For Microsoft SQL Server:

IF NOT EXISTS (
    SELECT *
    FROM   sys.columns
    WHERE  object_id = OBJECT_ID(N'[dbo].[blobs]')
    AND name = 'encoding'
)
ALTER TABLE blobs
    ADD encoding NVARCHAR(20) NULL; -- Encoding of the content: "raw" when it is the file's own bytes, "base64" when it is those bytes base64-encoded as text. Absent on content stored before the encoding was recorded.
GO

-- Only rows that predate the column are NULL, so re-running this script never
-- reclassifies files stored after the upgrade.
UPDATE blobs
    SET encoding = 'base64'
    WHERE type = 'command_result' AND encoding IS NULL;
  • The POST /groups endpoint no longer accepts the name of an existing group - it now returns 400 when a group with that name already exists, instead of succeeding without changing it. If you relied on POST /groups to idempotently ensure a group exists, switch to POST /v2/groups, which creates the group or updates it if it already exists. The new POST /v2/groups/add is create-only and also returns 400 on a duplicate name; use POST /v2/groups/update to update an existing group.

  • The GroupsService interface changed. If you provide a custom Groups service via the groups_service configuration property, update your implementation:

    • Add the new capability flags to the object returned by getSupportedFeatures: canGetGroup, canUpdateGroup, canAddOrUpdateGroup, and canRemoveAll. The io.Manager Server calls a group operation only when its capability flag is true, so set a flag to true only if your implementation supports that operation.
    • The new getGroup, updateGroup, and addOrUpdateGroup methods are optional - they are never called unless you enable their capability flags, so an implementation that doesn't support them can leave them as stubs.
    • The addGroup method now receives a Group object instead of a group name and returns the created Group (previously it received the name and returned void).
    • The getAllGroups method's request parameter is now optional.
    • Each Group you return may include an expandsTo array listing the granular permission groups (or other groups) it expands into; omit it or use an empty array for a group that doesn't expand into others.
getSupportedFeatures(): GroupsFeatures {
    return {
        canGetUserGroups: true,
        canGetAllGroups: true,
        canGetGroup: true,
        canAddGroup: true,
        canUpdateGroup: true,
        canAddOrUpdateGroup: true,
        canRemoveGroup: true,
        canRemoveAll: true,
        canAddUserToGroup: true,
        canRemoveUserFromGroup: true
    };
}

async addGroup(group: Group, audit: AuditBuilder): Promise<Group> {
    // Persist the group (including its `expandsTo`) and return it.
    return group;
}

For a complete example, see Configuration > Server Extension Points > Custom Groups Service.

2.0.0

  • As of version 2.0.0, the @interopio/manager package will be published in the public NPM registry. Previous versions of the package will still be available in the private JFROG registry.

  • io.Manager now requires a license key.️ To acquire a license key, contact us at sales@interop.io. Existing customers should contact their Client Success representative to obtain a license key. For more details, see the Requirements > Licensing section.

The license key can be passed to io.Manager via the licenseKey property of the configuration object for initializing io.Manager, or via the API_LICENSE_KEY environment variable, depending on your deployment approach.

The following example demonstrates providing the license key when initializing io.Manager:

import { start } from "@interopio/manager";

const config = {
    // It's now required to provide a valid license key for io.Manager to operate.
    licenseKey: "my-license-key"
};

const server = await start(config);

The following example demonstrates providing the license key by using an environment variable:

API_LICENSE_KEY=my-license-key
  • Dropped Node.js 18 support. io.Manager now requires Node.js 20 LTS or 22 LTS.

  • The default value for skipProcessExitOnStop is changed to true. If you want process.exit() to be called when server.stop() is called or when the server fails to start, set skipProcessExitOnStop to false.

  • TypeScript type changes. The following changes will affect only clients who are using TypeScript instead of pure JavaScript.

    • When implementing CustomAuthenticator, you now have to import the following types from @interopio/manager instead of @interopio/manager-api:
      • Token
      • User
    • When implementing AuditService, you now have to import the following types from @interopio/manager instead of @interopio/manager-api:
      • AuditLog
      • AuditLogDataResult
      • CleanAuditLogRequest
      • DataRequest
      • User
    • When implementing GroupsService, you now have to import the following types from @interopio/manager instead of @interopio/manager-api:
      • DataRequest
      • Group
      • GroupDataResult
      • GroupsFeatures
      • User
    • The AuditService.getAll() method now returns AuditLogDataResult instead of DataResult<AuditLog>. The types are structurally equivalent.
    • The types AuditLogEntityType and AuditLogOperation have been converted from TypeScript Union types to TypeScript Enums.
    • The @interopio/manager-api package is no longer required for io.Manager Server deployments and was subsequently dropped as a dependency.
  • In CustomAuthenticator implementations, the value of the req.url property will now include the value of the base property provided to io.Manager via configuration. For example, if a request is received for /user and base is set to "api", in version 1.8.0 the value of req.url will be "/user", and in version 2.0.0 the value of req.url will be "/api/user".

  • Implementations of CustomAuthenticator should always call next even when rejecting requests and should not respond to the request directly. Previously, a common pattern for CustomAuthenticator implementations was rejecting the requests directly:

import type { Request, Response } from "express";
import {
    type CustomAuthenticator,
    type User,
    type Token
} from "@interopio/manager";

export class MyAuthenticator implements CustomAuthenticator {
    initialize(): void {};

    authenticate(
        req: Request,
        res: Response,
        next: (err?: Error, info?: User) = void,
        token?: Token
    ): void {

        const user = this.findUser(req);

        if (user) {
            // Successful authentication request.
            next(undefined, user);
        } else {
            // Failed authentication request.
            res.status(401);
            res.send();
            return;
        };
    };

    private findUser(req: Request): User {
        // Custom authentication logic.
        throw new Error("Not implemented.");
    };
};

Rejecting the requests directly could cause memory leaks and is therefore not recommended. Instead, implementations of CustomAuthenticator should import CustomAuthUnauthorizedError and use it in the following way:

import type { Request, Response } from "express";
import {
    type CustomAuthenticator,
    type User,
    type Token,
    CustomAuthUnauthorizedError
} from "@interopio/manager";

export class MyAuthenticator implements CustomAuthenticator {
    initialize(): void {};

    authenticate(
        req: Request,
        res: Response,
        next: (err?: Error, info?: User) = void,
        token?: Token
    ): void {
        const user = this.findUser(req);

        if (user) {
            // Successful authentication request.
            next(undefined, user);
        } else {
            // Failed authentication request.
            next(new CustomAuthUnauthorizedError());
        };
    };

    private findUser(req: Request): User {
        // Custom authentication logic.
        throw new Error("Not implemented.");
    };
};

If you are using an older versions of io.Manager, you can work around this problem by manually defining an error class and passing an instance of it to the next() function.

ℹ️ For an example of defining an error class and passing it to the next() function, see the Custom Authentication example on GitHub.

As of version 2.0.0, io.Manager will produce warnings when it detects that the authentication pipeline is short-circuited by omitting the call to the next() function.

  • Added a traceContext database column to the commands table (for PostgreSQL and Microsoft SQL Server only).

⚠️ Note that users who don't use automatic schema migration must create the traceContext column manually by running the following scripts.

For PostgreSQL:

ALTER TABLE commands
    ADD COLUMN IF NOT EXISTS "traceContext" JSON;

For Microsoft SQL Server:

IF NOT EXISTS (
    SELECT *
    FROM   sys.columns
    WHERE  object_id = OBJECT_ID(N'[dbo].[commands]')
    AND name = 'traceContext'
)
ALTER TABLE commands
    ADD traceContext NVARCHAR(MAX);
  • Changed the type of the commandParams database column of the commands table to JSON (for PostgreSQL only).

⚠️ Note that users who don't use automatic schema migration must change the type of the commandParams column manually by running the following script:

ALTER TABLE commands
    ALTER COLUMN "commandParams" TYPE JSON USING "commandParams"::json;
  • Changed the precision of the weight database column in the glue42SystemConfig table (for PostgreSQL and Microsoft SQL Server only).

⚠️ Note that users who don't use automatic schema migration must change the precision of the weight column manually by running the following scripts.

For PostgreSQL:

ALTER TABLE "glue42SystemConfig"
   ALTER COLUMN weight TYPE numeric(8, 6) USING weight::numeric(8, 6);

For Microsoft SQL Server:

ALTER TABLE glue42SystemConfig
    ALTER COLUMN weight decimal(8, 6);
  • Changed the type of the definition database column of the layouts table to JSON (for PostgreSQL and Microsoft SQL Server only).

⚠️ Note that users who don't use automatic schema migration must change the type of the definition column manually by running the following scripts.

For PostgreSQL:

ALTER TABLE layouts
    ALTER COLUMN definition TYPE JSON USING definition::json;

For Microsoft SQL Server:

ALTER TABLE layouts
    ALTER COLUMN definition NVARCHAR(max) NOT NULL;
  • Changed the type of the comment database column of the crashes table to NVARCHAR(max) (for Microsoft SQL Server only).

⚠️ Note that users who don't use automatic schema migration must change the type of the comment column manually by running the following script:

ALTER TABLE crashes
    ALTER COLUMN comment NVARCHAR(max);
  • Changed the types of the comment, description and attachment database columns of the feedback table to NVARCHAR(max) (for Microsoft SQL Server only).

⚠️ Note that users who don't use automatic schema migration must change the type of the columns manually by running the following script:

ALTER TABLE feedback
    ALTER COLUMN comment NVARCHAR(max);
ALTER TABLE feedback
    ALTER COLUMN description NVARCHAR(max) NOT NULL;
ALTER TABLE feedback
    ALTER COLUMN attachment NVARCHAR(max) NOT NULL;
  • Monitoring configuration changes:

    • Removed false, "none", and "sentry" as values for the monitoring property. Use { type: "none" } or { type: "sentry" } instead.
  • Sentry monitoring configuration changes:

    • Removed the tracesSampleRate property from the Sentry monitoring configuration. Use sentryOptions.tracesSampleRate instead.
    • Removed the dsn property from the Sentry monitoring configuration. Use sentryOptions.dsn instead.
    • Tracing via the Sentry SDK is no longer supported. The recommended way to send traces to Sentry is by using OpenTelemetry traces. See also the OpenTelemetry with Sentry example on GitHub.
  • Removed ["admin"] as a default value for the API_AUTH_EXCLUSIVE_USERS environment variable. This change doesn't affect users who don't use environment variables for configuring io.Manager as the default value was valid only for the API_AUTH_EXCLUSIVE_USERS environment variable.

  • Due to a change in the FDC3 specification, the name and definition.name app properties won't be checked for consistency for FDC3 app definitions. The name and definition.appId properties will still be checked. For more details, see the FDC3 Standard 2.1.

  • When using environment variable configuration with .env files, values from the .env files will override the process environment variables. This means that if an environment variable is both defined in the .env file and passed to the process, the value from the .env file will be used.

1.7.0

  • Added database column others to the last_updated table (for PostgreSQL and Microsoft SQL Server only).

⚠️ Note that users who don't use automatic schema migration must create the others column manually by running the following scripts.

For PostgreSQL:

ALTER TABLE last_updated
    ADD COLUMN IF NOT EXISTS others JSON NULL;

For Microsoft SQL Server:

IF NOT EXISTS (
    SELECT *
    FROM   sys.columns
    WHERE  object_id = OBJECT_ID(N'[dbo].[last_updated]')
      AND name = 'others'
)
ALTER TABLE last_updated
    ADD others NVARCHAR(MAX);

1.6.0

  • Added database column browser to the machines table (for PostgreSQL and Microsoft SQL Server only).

⚠️ Note that users who don't use automatic schema migration must create the browser column manually by running the following scripts.

For PostgreSQL:

ALTER TABLE machines
    ADD COLUMN IF NOT EXISTS browser JSON NULL;

For Microsoft SQL Server:

IF NOT EXISTS (
    SELECT *
    FROM   sys.columns
    WHERE  object_id = OBJECT_ID(N'[dbo].[machines]')
    AND name = 'browser'
)
ALTER TABLE machines
    ADD browser NVARCHAR(MAX);

1.0.0

  • Users can now be compared in a case-insensitive way. You can use the username_case_sensitive property in the configuration object for initializing the io.Manager Server or the API_USERNAME_CASE_SENSITIVE environment variable to switch between the two modes. The new default is case-insensitive. To switch back to case sensitive mode, set the username_case_sensitive property or the API_USERNAME_CASE_SENSITIVE environment variable to true.

  • Removed status endpoint. Use GET / instead.

@interopio/manager-admin-ui

4.0.0

  • The io.Manager Admin UI is now compatible with the io.Manager Server only when their major and minor versions match. For example, io.Manager Admin UI 4.0.x is compatible with io.Manager Server 4.0.x (any patch), but io.Manager Admin UI 4.0.0 isn't compatible with io.Manager Server 4.1.0. Make sure to upgrade both packages together so that their major and minor versions stay aligned. Compatibility with @interopio/manager-api, io.Connect Desktop, and io.Connect Browser is unchanged.

  • The onUnauthorizedResponse() callback on the AuthProvider interface is now also invoked on 403 Forbidden responses, in addition to the existing 401 Unauthorized trigger. Consumers implementing a custom AuthProvider should review the callback and update any logic that previously assumed it fires only on 401 - for example, token-refresh flows that should not run when the user is authenticated but lacks the required permissions.

3.0.0

  • As of version 3.0.0, the @interopio/manager-admin-ui package will be published in the public NPM registry. Previous versions of the package will still be available in the private JFROG registry.

  • Dropped React 17 support. The io.Manager Admin UI now requires React 18. See the React 18 Upgrade Guide and the Admin UI example on GitHub.

  • Removed the users top-level configuration property. Its options will now be implied from the auth top-level configuration property.

  • If using TypeScript, the "jsx" property of the "compilerOptions" object in the tsconfig.json file must be set to "react-jsx".

  • All io.Manager Admin UI style imports have been combined in one:

// This is the only required style import.
import "@interopio/manager-admin-ui/styles.css";

⚠️ Note that for the Admin UI to function properly, you must remove the @interopio/theme-demo-apps style import.

The @interopio/theme-demo-apps dependency can be safely removed from your package.json file. For a complete example, see the Admin UI example on GitHub.

  • For clients using Auth0 authentication: The @auth0/auth0-react package has been updated from v1 to v2. The @auth0/auth0-react v2 package introduces breaking changes to the auth_auth0 property which accepts a value of type Auth0ProviderOptions imported from @auth0/auth0-react. For more details, see the Auth0-React v2 Migration Guide.

  • For clients using the manager-admin-ui docker image: The PUBLIC_URL environment variable has been deprecated and won't have any effect. Setting the REACT_APP_BASE environment variable will be enough for the routing to work correctly.

@interopio/manager-api

5.0.0

  • The type of the attachment field of the AddFeedbackRequest DTO has been changed from string to File | NodeJSReadStream. Callers must pass a File in the browser or a Node.js ReadStream in Node.
  • The type of the upload_file_minidump field of the CreateCrashRequest DTO has been changed from string to File | NodeJSReadStream. Callers must pass a File in the browser or a Node.js ReadStream in Node.
  • The declared return type of the whoAmI() method has been changed from Promise<User> to Promise<WhoAmIResponse>. The method has always returned only the id, email, apps, and groups fields at runtime, which means that the runtime behavior hasn't changed. TypeScript callers that read any other field of User from the result won't compile - read those fields via the users client instead.
  • The following exports have been removed from the @interopio/manager-api package:
    • BaseAPI
    • AuthOptions
    • CustomRequest
    • CustomRequestResponse
    • ClientOptions
    • IOManagerCache
    • IOManagerCacheItem
    • IOManagerLogger
    • IOManagerAsyncSequelizer
    • SanitizedIOManagerApiError
    • IOManagerRequestValidationError
    • CachedClientAPI
    • CachedClientOptions
    • IOManagerConnectionState
    • isIOManagerTimeoutError

4.0.0

  • TypeScript type changes. The following changes will affect only clients who are using TypeScript instead of pure JavaScript.

    • The usage of type DataResult<T> has been replaced with several concrete types:
      • AppPreferenceDataResult
      • AuditLogDataResult
      • CommandDataResult
      • ClientCrashDataResult
      • FeedbackDataResult
      • GroupDataResult
      • LayoutDataResult
      • MachineDataResult
      • UserSessionDataResult
      • SystemConfigEntryDataResult
    • Removed type alias GetAppsRequest - use DataRequest instead.
    • Removed type alias GetCommandsResponse - use CommandDataResult instead.
    • Removed type alias GetUsersResponse - use UserDataResult instead.
    • The types of the following interface properties have been converted from inline TypeScript Union types to TypeScript Enums:
      • CleanSessionsAction.action is now of type CleanSessionsAction.
      • UserAppStatus.explain is now of type UserAppStatusExplain.
      • AuditLog.entityType is now of type AuditLogEntityType.
      • AuditLog.operation is now of type AuditLogOperation.
      • Command.status is now of type CommandStatus.
      • Command.resultType is now of type ResultType.
      • TextFilterCondition.type is now of type TextFilterType.
      • BooleanFilterCondition.type is now of type BooleanFilterType.
      • NumberFilterCondition.type is now of type NumberFilterType.
      • DateFilterCondition.type is now of type DateFilterType.
      • SortOrder.sort is now of type SortOrderEnum.
      • LogicalCondition.operator is now of type LogicalConditionOperator.
    • Removed types SimpleCondition and Filter.
  • The MatchAll export has been renamed to ConfigMatchAll.