feat(skill): automatically onboard to openapi tooling (#34179)

* feat(skill): automatically onboard to openapi tooling

Signed-off-by: aramissennyeydd <aramis.sennyey@doordash.com>

* test skill against notifications backend

Signed-off-by: aramissennyeydd <aramis.sennyey@doordash.com>

* update URLs to point to main repo and describe multiple client types

Signed-off-by: aramissennyeydd <aramis.sennyey@doordash.com>

* address PR feedback

Signed-off-by: aramissennyeydd <aramis.sennyey@doordash.com>

* revert erroneous changes

Signed-off-by: aramissennyeydd <aramis.sennyey@doordash.com>

* add changeset

Signed-off-by: aramissennyeydd <aramis.sennyey@doordash.com>

* address review feedback: skill doc fixes and entityRef schema dedup

Disambiguate generated entityRef/orderField types and correct skill
guidance on operationId casing and OpenAPI version support.

Signed-off-by: aramissennyeydd <aramis.sennyey@doordash.com>

* note in skill that handler-side validation should be stripped

Reviewers may forget that manual InputError guards and primitive-type
checks become dead weight once the OpenAPI validator runs.

Signed-off-by: aramissennyeydd <aramis.sennyey@doordash.com>

---------

Signed-off-by: aramissennyeydd <aramis.sennyey@doordash.com>
This commit is contained in:
Aramis Sennyey
2026-05-21 23:01:16 -04:00
committed by GitHub
parent a7d7542432
commit ac410b1d26
36 changed files with 2918 additions and 93 deletions
@@ -35,6 +35,7 @@
"scripts": {
"build": "backstage-cli package build",
"clean": "backstage-cli package clean",
"generate": "backstage-repo-tools package schema openapi generate --server",
"lint": "backstage-cli package lint",
"prepack": "backstage-cli package prepack",
"postpack": "backstage-cli package postpack",
@@ -42,6 +43,7 @@
"test": "backstage-cli package test"
},
"dependencies": {
"@backstage/backend-openapi-utils": "workspace:^",
"@backstage/backend-plugin-api": "workspace:^",
"@backstage/catalog-model": "workspace:^",
"@backstage/config": "workspace:^",
@@ -64,6 +66,7 @@
"@backstage/plugin-auth-backend-module-guest-provider": "workspace:^",
"@backstage/plugin-events-backend": "workspace:^",
"@backstage/plugin-signals-backend": "workspace:^",
"@backstage/repo-tools": "workspace:^",
"@types/express": "^4.17.6",
"@types/supertest": "^2.0.8",
"supertest": "^7.0.0"
@@ -0,0 +1,675 @@
openapi: 3.1.0
info:
title: notifications
version: '1'
description: The Backstage backend plugin that powers user notifications.
license:
name: Apache-2.0
url: http://www.apache.org/licenses/LICENSE-2.0.html
contact: {}
servers:
- url: /
components:
examples: {}
headers: {}
parameters:
notificationId:
name: id
in: path
required: true
schema:
type: string
offset:
name: offset
in: query
required: false
allowReserved: true
schema:
type: integer
minimum: 0
limit:
name: limit
in: query
required: false
allowReserved: true
schema:
type: integer
minimum: 0
orderField:
name: orderField
in: query
required: false
allowReserved: true
description: One or more "field,direction" pairs. Direction is "asc" or "desc". Pass multiple to express compound sorts.
schema:
$ref: '#/components/schemas/OrderField'
topic:
name: topic
in: query
required: false
allowReserved: true
schema:
type: string
search:
name: search
in: query
required: false
allowReserved: true
schema:
type: string
read:
name: read
in: query
required: false
allowReserved: true
schema:
type: string
enum:
- 'true'
- 'false'
saved:
name: saved
in: query
required: false
allowReserved: true
schema:
type: string
enum:
- 'true'
- 'false'
createdAfter:
name: createdAfter
in: query
required: false
allowReserved: true
description: ISO 8601 date-time string. Validated by the handler.
schema:
type: string
minimumSeverity:
name: minimumSeverity
in: query
required: false
allowReserved: true
schema:
$ref: '#/components/schemas/NotificationSeverity'
requestBodies: {}
responses:
ErrorResponse:
description: An error response from the backend.
content:
application/json; charset=utf-8:
schema:
$ref: '#/components/schemas/Error'
schemas:
NotificationSeverity:
type: string
enum:
- critical
- high
- normal
- low
NotificationPayload:
type: object
required:
- title
properties:
title:
type: string
description: Notification title
description:
type: string
description: Optional longer description for the notification
link:
type: string
description: Optional link where the notification is pointing to
severity:
$ref: '#/components/schemas/NotificationSeverity'
topic:
type: string
description: Optional notification topic
scope:
type: string
description: Notification scope, can be used to re-send same notifications in case the scope and origin matches.
icon:
type: string
description: Optional notification icon
metadata:
type: object
description: Optional additional customizable metadata.
additionalProperties: true
NotificationResponsePayload:
description: Response variant of NotificationPayload. Optional string fields may be returned as null when unset.
type: object
required:
- title
properties:
title:
type: string
description:
type:
- string
- 'null'
link:
type:
- string
- 'null'
severity:
$ref: '#/components/schemas/NotificationSeverity'
topic:
type:
- string
- 'null'
scope:
type:
- string
- 'null'
icon:
type:
- string
- 'null'
metadata:
type: object
additionalProperties: true
Notification:
type: object
required:
- id
- user
- created
- origin
- payload
properties:
id:
type: string
description: Unique identifier for the notification.
user:
type:
- string
- 'null'
description: User entity reference, or null for broadcast notifications.
created:
type: string
format: date-time
saved:
type:
- string
- 'null'
format: date-time
read:
type:
- string
- 'null'
format: date-time
updated:
type:
- string
- 'null'
format: date-time
origin:
type: string
description: Origin of the notification (the sender's reference).
payload:
$ref: '#/components/schemas/NotificationResponsePayload'
NotificationStatus:
type: object
required:
- unread
- read
properties:
unread:
type: integer
read:
type: integer
TopicSetting:
type: object
required:
- id
- enabled
properties:
id:
type: string
enabled:
type: boolean
OriginSetting:
type: object
required:
- id
- enabled
properties:
id:
type: string
enabled:
type: boolean
topics:
type: array
items:
$ref: '#/components/schemas/TopicSetting'
ChannelSetting:
type: object
required:
- id
- origins
properties:
id:
type: string
enabled:
type: boolean
origins:
type: array
items:
$ref: '#/components/schemas/OriginSetting'
NotificationSettings:
type: object
required:
- channels
properties:
channels:
type: array
items:
$ref: '#/components/schemas/ChannelSetting'
EntityRef:
description: A single entity ref, or a list of entity refs.
oneOf:
- type: string
- type: array
items:
type: string
OrderField:
description: One or more "field,direction" pairs, given as a single string or an array of strings.
oneOf:
- type: string
- type: array
items:
type: string
NotificationRecipients:
oneOf:
- type: object
required:
- type
- entityRef
properties:
type:
type: string
enum:
- entity
entityRef:
$ref: '#/components/schemas/EntityRef'
excludeEntityRef:
$ref: '#/components/schemas/EntityRef'
- type: object
required:
- type
properties:
type:
type: string
enum:
- broadcast
NotificationSendOptions:
type: object
required:
- recipients
- payload
properties:
recipients:
$ref: '#/components/schemas/NotificationRecipients'
payload:
$ref: '#/components/schemas/NotificationPayload'
UpdateNotificationsRequest:
type: object
required:
- ids
properties:
ids:
type: array
items:
type: string
read:
type: boolean
saved:
type: boolean
ListNotificationsResponse:
type: object
required:
- totalCount
- notifications
properties:
totalCount:
type: integer
notifications:
type: array
items:
$ref: '#/components/schemas/Notification'
GetTopicsResponse:
type: object
required:
- topics
properties:
topics:
type: array
items:
type: string
Error:
type: object
properties:
error:
type: object
properties:
name:
type: string
message:
type: string
required:
- name
- message
request:
type: object
properties:
method:
type: string
url:
type: string
required:
- method
- url
response:
type: object
properties:
statusCode:
type: number
required:
- statusCode
required:
- error
- request
- response
securitySchemes:
JWT:
type: http
scheme: bearer
bearerFormat: JWT
paths:
/:
get:
operationId: ListNotificationsLegacy
deprecated: true
description: Deprecated alias for `GET /notifications`.
parameters:
- $ref: '#/components/parameters/offset'
- $ref: '#/components/parameters/limit'
- $ref: '#/components/parameters/orderField'
- $ref: '#/components/parameters/topic'
- $ref: '#/components/parameters/search'
- $ref: '#/components/parameters/read'
- $ref: '#/components/parameters/saved'
- $ref: '#/components/parameters/createdAfter'
- $ref: '#/components/parameters/minimumSeverity'
responses:
'200':
description: A page of notifications for the current user.
content:
application/json:
schema:
$ref: '#/components/schemas/ListNotificationsResponse'
default:
$ref: '#/components/responses/ErrorResponse'
security:
- {}
- JWT: []
post:
operationId: CreateNotificationsLegacy
deprecated: true
description: Deprecated alias for `POST /notifications`.
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/NotificationSendOptions'
responses:
'200':
description: The notifications that were created.
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Notification'
default:
$ref: '#/components/responses/ErrorResponse'
security:
- {}
- JWT: []
/notifications:
get:
operationId: ListNotifications
description: List notifications for the current user.
parameters:
- $ref: '#/components/parameters/offset'
- $ref: '#/components/parameters/limit'
- $ref: '#/components/parameters/orderField'
- $ref: '#/components/parameters/topic'
- $ref: '#/components/parameters/search'
- $ref: '#/components/parameters/read'
- $ref: '#/components/parameters/saved'
- $ref: '#/components/parameters/createdAfter'
- $ref: '#/components/parameters/minimumSeverity'
responses:
'200':
description: A page of notifications for the current user.
content:
application/json:
schema:
$ref: '#/components/schemas/ListNotificationsResponse'
default:
$ref: '#/components/responses/ErrorResponse'
security:
- {}
- JWT: []
post:
operationId: CreateNotifications
description: Send a notification to one or more recipients (service credentials required).
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/NotificationSendOptions'
responses:
'200':
description: The notifications that were created.
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Notification'
default:
$ref: '#/components/responses/ErrorResponse'
security:
- {}
- JWT: []
/update:
post:
operationId: UpdateNotificationsLegacy
deprecated: true
description: Deprecated alias for `POST /notifications/update`.
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/UpdateNotificationsRequest'
responses:
'200':
description: The notifications that were updated.
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Notification'
default:
$ref: '#/components/responses/ErrorResponse'
security:
- {}
- JWT: []
/notifications/update:
post:
operationId: UpdateNotifications
description: Mark notifications as read/unread or saved/unsaved.
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/UpdateNotificationsRequest'
responses:
'200':
description: The notifications that were updated.
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Notification'
default:
$ref: '#/components/responses/ErrorResponse'
security:
- {}
- JWT: []
/status:
get:
operationId: GetStatus
description: Get the unread/read counts for the current user.
responses:
'200':
description: The current user's notification status.
content:
application/json:
schema:
$ref: '#/components/schemas/NotificationStatus'
default:
$ref: '#/components/responses/ErrorResponse'
security:
- {}
- JWT: []
/settings:
get:
operationId: GetSettings
description: Get the current user's notification settings, merged with defaults.
responses:
'200':
description: Effective notification settings for the user.
content:
application/json:
schema:
$ref: '#/components/schemas/NotificationSettings'
default:
$ref: '#/components/responses/ErrorResponse'
security:
- {}
- JWT: []
post:
operationId: UpdateSettings
description: Update the current user's notification settings.
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/NotificationSettings'
responses:
'200':
description: Effective notification settings for the user.
content:
application/json:
schema:
$ref: '#/components/schemas/NotificationSettings'
default:
$ref: '#/components/responses/ErrorResponse'
security:
- {}
- JWT: []
/topics:
get:
operationId: GetTopics
description: List notification topics that the user has received notifications on.
parameters:
- $ref: '#/components/parameters/search'
- $ref: '#/components/parameters/read'
- $ref: '#/components/parameters/saved'
- $ref: '#/components/parameters/createdAfter'
- $ref: '#/components/parameters/minimumSeverity'
responses:
'200':
description: The list of topics.
content:
application/json:
schema:
$ref: '#/components/schemas/GetTopicsResponse'
default:
$ref: '#/components/responses/ErrorResponse'
security:
- {}
- JWT: []
/{id}:
get:
operationId: GetNotificationLegacy
deprecated: true
description: Deprecated alias for `GET /notifications/{id}`.
parameters:
- $ref: '#/components/parameters/notificationId'
responses:
'200':
description: The requested notification.
content:
application/json:
schema:
$ref: '#/components/schemas/Notification'
default:
$ref: '#/components/responses/ErrorResponse'
security:
- {}
- JWT: []
/notifications/{id}:
get:
operationId: GetNotification
description: Get a single notification by id for the current user.
parameters:
- $ref: '#/components/parameters/notificationId'
responses:
'200':
description: The requested notification.
content:
application/json:
schema:
$ref: '#/components/schemas/Notification'
default:
$ref: '#/components/responses/ErrorResponse'
security:
- {}
- JWT: []
@@ -0,0 +1,169 @@
/*
* Copyright 2026 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
//
// ******************************************************************
// * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. *
// ******************************************************************
import { GetTopicsResponse } from '../models/GetTopicsResponse.model';
import { ListNotificationsResponse } from '../models/ListNotificationsResponse.model';
import { Notification } from '../models/Notification.model';
import { NotificationSendOptions } from '../models/NotificationSendOptions.model';
import { NotificationSettings } from '../models/NotificationSettings.model';
import { NotificationSeverity } from '../models/NotificationSeverity.model';
import { NotificationStatus } from '../models/NotificationStatus.model';
import { OrderField } from '../models/OrderField.model';
import { UpdateNotificationsRequest } from '../models/UpdateNotificationsRequest.model';
/**
* @public
*/
export type CreateNotifications = {
body: NotificationSendOptions;
response: Array<Notification> | Error;
};
/**
* @public
*/
export type CreateNotificationsLegacy = {
body: NotificationSendOptions;
response: Array<Notification> | Error;
};
/**
* @public
*/
export type GetNotification = {
path: {
id: string;
};
response: Notification | Error;
};
/**
* @public
*/
export type GetNotificationLegacy = {
path: {
id: string;
};
response: Notification | Error;
};
/**
* @public
*/
export type GetSettings = {
response: NotificationSettings | Error;
};
/**
* @public
*/
export type GetStatus = {
response: NotificationStatus | Error;
};
/**
* @public
*/
export type GetTopics = {
query: {
search?: string;
read?: 'true' | 'false';
saved?: 'true' | 'false';
createdAfter?: string;
minimumSeverity?: NotificationSeverity;
};
response: GetTopicsResponse | Error;
};
/**
* @public
*/
export type ListNotifications = {
query: {
offset?: number;
limit?: number;
orderField?: OrderField;
topic?: string;
search?: string;
read?: 'true' | 'false';
saved?: 'true' | 'false';
createdAfter?: string;
minimumSeverity?: NotificationSeverity;
};
response: ListNotificationsResponse | Error;
};
/**
* @public
*/
export type ListNotificationsLegacy = {
query: {
offset?: number;
limit?: number;
orderField?: OrderField;
topic?: string;
search?: string;
read?: 'true' | 'false';
saved?: 'true' | 'false';
createdAfter?: string;
minimumSeverity?: NotificationSeverity;
};
response: ListNotificationsResponse | Error;
};
/**
* @public
*/
export type UpdateNotifications = {
body: UpdateNotificationsRequest;
response: Array<Notification> | Error;
};
/**
* @public
*/
export type UpdateNotificationsLegacy = {
body: UpdateNotificationsRequest;
response: Array<Notification> | Error;
};
/**
* @public
*/
export type UpdateSettings = {
body: NotificationSettings;
response: NotificationSettings | Error;
};
export type EndpointMap = {
'#post|/notifications': CreateNotifications;
'#post|/': CreateNotificationsLegacy;
'#get|/notifications/{id}': GetNotification;
'#get|/{id}': GetNotificationLegacy;
'#get|/settings': GetSettings;
'#get|/status': GetStatus;
'#get|/topics': GetTopics;
'#get|/notifications': ListNotifications;
'#get|/': ListNotificationsLegacy;
'#post|/notifications/update': UpdateNotifications;
'#post|/update': UpdateNotificationsLegacy;
'#post|/settings': UpdateSettings;
};
@@ -0,0 +1,23 @@
/*
* Copyright 2026 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
//
// ******************************************************************
// * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. *
// ******************************************************************
export * from './Api.server';
@@ -0,0 +1,18 @@
/*
* Copyright 2026 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export * from './apis';
export * from './router';
@@ -0,0 +1,29 @@
/*
* Copyright 2026 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// ******************************************************************
// * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. *
// ******************************************************************
import { OriginSetting } from '../models/OriginSetting.model';
/**
* @public
*/
export interface ChannelSetting {
id: string;
enabled?: boolean;
origins: Array<OriginSetting>;
}
@@ -0,0 +1,25 @@
/*
* Copyright 2026 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// ******************************************************************
// * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. *
// ******************************************************************
/**
* A single entity ref, or a list of entity refs.
* @public
*/
export type EntityRef = Array<string> | string;
@@ -0,0 +1,27 @@
/*
* Copyright 2026 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// ******************************************************************
// * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. *
// ******************************************************************
/**
* @public
*/
export interface ErrorError {
name: string;
message: string;
}
@@ -0,0 +1,27 @@
/*
* Copyright 2026 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// ******************************************************************
// * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. *
// ******************************************************************
/**
* @public
*/
export interface ErrorRequest {
method: string;
url: string;
}
@@ -0,0 +1,26 @@
/*
* Copyright 2026 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// ******************************************************************
// * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. *
// ******************************************************************
/**
* @public
*/
export interface ErrorResponse {
statusCode: number;
}
@@ -0,0 +1,26 @@
/*
* Copyright 2026 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// ******************************************************************
// * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. *
// ******************************************************************
/**
* @public
*/
export interface GetTopicsResponse {
topics: Array<string>;
}
@@ -0,0 +1,28 @@
/*
* Copyright 2026 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// ******************************************************************
// * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. *
// ******************************************************************
import { Notification } from '../models/Notification.model';
/**
* @public
*/
export interface ListNotificationsResponse {
totalCount: number;
notifications: Array<Notification>;
}
@@ -0,0 +1,31 @@
/*
* Copyright 2026 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// ******************************************************************
// * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. *
// ******************************************************************
import { ErrorError } from '../models/ErrorError.model';
import { ErrorRequest } from '../models/ErrorRequest.model';
import { ErrorResponse } from '../models/ErrorResponse.model';
/**
* @public
*/
export interface ModelError {
error: ErrorError;
request: ErrorRequest;
response: ErrorResponse;
}
@@ -0,0 +1,43 @@
/*
* Copyright 2026 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// ******************************************************************
// * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. *
// ******************************************************************
import { NotificationResponsePayload } from '../models/NotificationResponsePayload.model';
/**
* @public
*/
export interface Notification {
/**
* Unique identifier for the notification.
*/
id: string;
/**
* User entity reference, or null for broadcast notifications.
*/
user: string | null;
created: Date;
saved?: Date | null;
read?: Date | null;
updated?: Date | null;
/**
* Origin of the notification (the sender\'s reference).
*/
origin: string;
payload: NotificationResponsePayload;
}
@@ -0,0 +1,55 @@
/*
* Copyright 2026 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// ******************************************************************
// * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. *
// ******************************************************************
import { NotificationSeverity } from '../models/NotificationSeverity.model';
/**
* @public
*/
export interface NotificationPayload {
/**
* Notification title
*/
title: string;
/**
* Optional longer description for the notification
*/
description?: string;
/**
* Optional link where the notification is pointing to
*/
link?: string;
severity?: NotificationSeverity;
/**
* Optional notification topic
*/
topic?: string;
/**
* Notification scope, can be used to re-send same notifications in case the scope and origin matches.
*/
scope?: string;
/**
* Optional notification icon
*/
icon?: string;
/**
* Optional additional customizable metadata.
*/
metadata?: { [key: string]: any };
}
@@ -0,0 +1,28 @@
/*
* Copyright 2026 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// ******************************************************************
// * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. *
// ******************************************************************
import { NotificationRecipientsOneOf } from '../models/NotificationRecipientsOneOf.model';
import { NotificationRecipientsOneOf1 } from '../models/NotificationRecipientsOneOf1.model';
/**
* @public
*/
export type NotificationRecipients =
| NotificationRecipientsOneOf
| NotificationRecipientsOneOf1;
@@ -0,0 +1,34 @@
/*
* Copyright 2026 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// ******************************************************************
// * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. *
// ******************************************************************
import { EntityRef } from '../models/EntityRef.model';
/**
* @public
*/
export interface NotificationRecipientsOneOf {
type: NotificationRecipientsOneOfTypeEnum;
entityRef: EntityRef;
excludeEntityRef?: EntityRef;
}
/**
* @public
*/
export type NotificationRecipientsOneOfTypeEnum = 'entity';
@@ -0,0 +1,31 @@
/*
* Copyright 2026 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// ******************************************************************
// * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. *
// ******************************************************************
/**
* @public
*/
export interface NotificationRecipientsOneOf1 {
type: NotificationRecipientsOneOf1TypeEnum;
}
/**
* @public
*/
export type NotificationRecipientsOneOf1TypeEnum = 'broadcast';
@@ -0,0 +1,35 @@
/*
* Copyright 2026 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// ******************************************************************
// * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. *
// ******************************************************************
import { NotificationSeverity } from '../models/NotificationSeverity.model';
/**
* Response variant of NotificationPayload. Optional string fields may be returned as null when unset.
* @public
*/
export interface NotificationResponsePayload {
title: string;
description?: string | null;
link?: string | null;
severity?: NotificationSeverity;
topic?: string | null;
scope?: string | null;
icon?: string | null;
metadata?: { [key: string]: any };
}
@@ -0,0 +1,29 @@
/*
* Copyright 2026 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// ******************************************************************
// * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. *
// ******************************************************************
import { NotificationPayload } from '../models/NotificationPayload.model';
import { NotificationRecipients } from '../models/NotificationRecipients.model';
/**
* @public
*/
export interface NotificationSendOptions {
recipients: NotificationRecipients;
payload: NotificationPayload;
}
@@ -0,0 +1,27 @@
/*
* Copyright 2026 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// ******************************************************************
// * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. *
// ******************************************************************
import { ChannelSetting } from '../models/ChannelSetting.model';
/**
* @public
*/
export interface NotificationSettings {
channels: Array<ChannelSetting>;
}
@@ -0,0 +1,24 @@
/*
* Copyright 2026 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// ******************************************************************
// * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. *
// ******************************************************************
/**
* @public
*/
export type NotificationSeverity = 'critical' | 'high' | 'normal' | 'low';
@@ -0,0 +1,27 @@
/*
* Copyright 2026 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// ******************************************************************
// * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. *
// ******************************************************************
/**
* @public
*/
export interface NotificationStatus {
unread: number;
read: number;
}
@@ -0,0 +1,25 @@
/*
* Copyright 2026 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// ******************************************************************
// * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. *
// ******************************************************************
/**
* One or more \"field,direction\" pairs, given as a single string or an array of strings.
* @public
*/
export type OrderField = Array<string> | string;
@@ -0,0 +1,29 @@
/*
* Copyright 2026 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// ******************************************************************
// * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. *
// ******************************************************************
import { TopicSetting } from '../models/TopicSetting.model';
/**
* @public
*/
export interface OriginSetting {
id: string;
enabled: boolean;
topics?: Array<TopicSetting>;
}
@@ -0,0 +1,27 @@
/*
* Copyright 2026 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// ******************************************************************
// * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. *
// ******************************************************************
/**
* @public
*/
export interface TopicSetting {
id: string;
enabled: boolean;
}
@@ -0,0 +1,28 @@
/*
* Copyright 2026 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// ******************************************************************
// * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. *
// ******************************************************************
/**
* @public
*/
export interface UpdateNotificationsRequest {
ids: Array<string>;
read?: boolean;
saved?: boolean;
}
@@ -0,0 +1,38 @@
/*
* Copyright 2026 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export * from '../models/ChannelSetting.model';
export * from '../models/EntityRef.model';
export * from '../models/ErrorError.model';
export * from '../models/ErrorRequest.model';
export * from '../models/ErrorResponse.model';
export * from '../models/GetTopicsResponse.model';
export * from '../models/ListNotificationsResponse.model';
export * from '../models/ModelError.model';
export * from '../models/Notification.model';
export * from '../models/NotificationPayload.model';
export * from '../models/NotificationRecipients.model';
export * from '../models/NotificationRecipientsOneOf.model';
export * from '../models/NotificationRecipientsOneOf1.model';
export * from '../models/NotificationResponsePayload.model';
export * from '../models/NotificationSendOptions.model';
export * from '../models/NotificationSettings.model';
export * from '../models/NotificationSeverity.model';
export * from '../models/NotificationStatus.model';
export * from '../models/OrderField.model';
export * from '../models/OriginSetting.model';
export * from '../models/TopicSetting.model';
export * from '../models/UpdateNotificationsRequest.model';
@@ -0,0 +1,988 @@
/*
* Copyright 2026 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// ******************************************************************
// * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. *
// ******************************************************************
import { createValidatedOpenApiRouterFromGeneratedEndpointMap } from '@backstage/backend-openapi-utils';
import { EndpointMap } from './apis';
export const spec = {
openapi: '3.1.0',
info: {
title: 'notifications',
version: '1',
description: 'The Backstage backend plugin that powers user notifications.',
license: {
name: 'Apache-2.0',
url: 'http://www.apache.org/licenses/LICENSE-2.0.html',
},
contact: {},
},
servers: [
{
url: '/',
},
],
components: {
examples: {},
headers: {},
parameters: {
notificationId: {
name: 'id',
in: 'path',
required: true,
schema: {
type: 'string',
},
},
offset: {
name: 'offset',
in: 'query',
required: false,
allowReserved: true,
schema: {
type: 'integer',
minimum: 0,
},
},
limit: {
name: 'limit',
in: 'query',
required: false,
allowReserved: true,
schema: {
type: 'integer',
minimum: 0,
},
},
orderField: {
name: 'orderField',
in: 'query',
required: false,
allowReserved: true,
description:
'One or more "field,direction" pairs. Direction is "asc" or "desc". Pass multiple to express compound sorts.',
schema: {
$ref: '#/components/schemas/OrderField',
},
},
topic: {
name: 'topic',
in: 'query',
required: false,
allowReserved: true,
schema: {
type: 'string',
},
},
search: {
name: 'search',
in: 'query',
required: false,
allowReserved: true,
schema: {
type: 'string',
},
},
read: {
name: 'read',
in: 'query',
required: false,
allowReserved: true,
schema: {
type: 'string',
enum: ['true', 'false'],
},
},
saved: {
name: 'saved',
in: 'query',
required: false,
allowReserved: true,
schema: {
type: 'string',
enum: ['true', 'false'],
},
},
createdAfter: {
name: 'createdAfter',
in: 'query',
required: false,
allowReserved: true,
description: 'ISO 8601 date-time string. Validated by the handler.',
schema: {
type: 'string',
},
},
minimumSeverity: {
name: 'minimumSeverity',
in: 'query',
required: false,
allowReserved: true,
schema: {
$ref: '#/components/schemas/NotificationSeverity',
},
},
},
requestBodies: {},
responses: {
ErrorResponse: {
description: 'An error response from the backend.',
content: {
'application/json; charset=utf-8': {
schema: {
$ref: '#/components/schemas/Error',
},
},
},
},
},
schemas: {
NotificationSeverity: {
type: 'string',
enum: ['critical', 'high', 'normal', 'low'],
},
NotificationPayload: {
type: 'object',
required: ['title'],
properties: {
title: {
type: 'string',
description: 'Notification title',
},
description: {
type: 'string',
description: 'Optional longer description for the notification',
},
link: {
type: 'string',
description: 'Optional link where the notification is pointing to',
},
severity: {
$ref: '#/components/schemas/NotificationSeverity',
},
topic: {
type: 'string',
description: 'Optional notification topic',
},
scope: {
type: 'string',
description:
'Notification scope, can be used to re-send same notifications in case the scope and origin matches.',
},
icon: {
type: 'string',
description: 'Optional notification icon',
},
metadata: {
type: 'object',
description: 'Optional additional customizable metadata.',
additionalProperties: true,
},
},
},
NotificationResponsePayload: {
description:
'Response variant of NotificationPayload. Optional string fields may be returned as null when unset.',
type: 'object',
required: ['title'],
properties: {
title: {
type: 'string',
},
description: {
type: ['string', 'null'],
},
link: {
type: ['string', 'null'],
},
severity: {
$ref: '#/components/schemas/NotificationSeverity',
},
topic: {
type: ['string', 'null'],
},
scope: {
type: ['string', 'null'],
},
icon: {
type: ['string', 'null'],
},
metadata: {
type: 'object',
additionalProperties: true,
},
},
},
Notification: {
type: 'object',
required: ['id', 'user', 'created', 'origin', 'payload'],
properties: {
id: {
type: 'string',
description: 'Unique identifier for the notification.',
},
user: {
type: ['string', 'null'],
description:
'User entity reference, or null for broadcast notifications.',
},
created: {
type: 'string',
format: 'date-time',
},
saved: {
type: ['string', 'null'],
format: 'date-time',
},
read: {
type: ['string', 'null'],
format: 'date-time',
},
updated: {
type: ['string', 'null'],
format: 'date-time',
},
origin: {
type: 'string',
description: "Origin of the notification (the sender's reference).",
},
payload: {
$ref: '#/components/schemas/NotificationResponsePayload',
},
},
},
NotificationStatus: {
type: 'object',
required: ['unread', 'read'],
properties: {
unread: {
type: 'integer',
},
read: {
type: 'integer',
},
},
},
TopicSetting: {
type: 'object',
required: ['id', 'enabled'],
properties: {
id: {
type: 'string',
},
enabled: {
type: 'boolean',
},
},
},
OriginSetting: {
type: 'object',
required: ['id', 'enabled'],
properties: {
id: {
type: 'string',
},
enabled: {
type: 'boolean',
},
topics: {
type: 'array',
items: {
$ref: '#/components/schemas/TopicSetting',
},
},
},
},
ChannelSetting: {
type: 'object',
required: ['id', 'origins'],
properties: {
id: {
type: 'string',
},
enabled: {
type: 'boolean',
},
origins: {
type: 'array',
items: {
$ref: '#/components/schemas/OriginSetting',
},
},
},
},
NotificationSettings: {
type: 'object',
required: ['channels'],
properties: {
channels: {
type: 'array',
items: {
$ref: '#/components/schemas/ChannelSetting',
},
},
},
},
EntityRef: {
description: 'A single entity ref, or a list of entity refs.',
oneOf: [
{
type: 'string',
},
{
type: 'array',
items: {
type: 'string',
},
},
],
},
OrderField: {
description:
'One or more "field,direction" pairs, given as a single string or an array of strings.',
oneOf: [
{
type: 'string',
},
{
type: 'array',
items: {
type: 'string',
},
},
],
},
NotificationRecipients: {
oneOf: [
{
type: 'object',
required: ['type', 'entityRef'],
properties: {
type: {
type: 'string',
enum: ['entity'],
},
entityRef: {
$ref: '#/components/schemas/EntityRef',
},
excludeEntityRef: {
$ref: '#/components/schemas/EntityRef',
},
},
},
{
type: 'object',
required: ['type'],
properties: {
type: {
type: 'string',
enum: ['broadcast'],
},
},
},
],
},
NotificationSendOptions: {
type: 'object',
required: ['recipients', 'payload'],
properties: {
recipients: {
$ref: '#/components/schemas/NotificationRecipients',
},
payload: {
$ref: '#/components/schemas/NotificationPayload',
},
},
},
UpdateNotificationsRequest: {
type: 'object',
required: ['ids'],
properties: {
ids: {
type: 'array',
items: {
type: 'string',
},
},
read: {
type: 'boolean',
},
saved: {
type: 'boolean',
},
},
},
ListNotificationsResponse: {
type: 'object',
required: ['totalCount', 'notifications'],
properties: {
totalCount: {
type: 'integer',
},
notifications: {
type: 'array',
items: {
$ref: '#/components/schemas/Notification',
},
},
},
},
GetTopicsResponse: {
type: 'object',
required: ['topics'],
properties: {
topics: {
type: 'array',
items: {
type: 'string',
},
},
},
},
Error: {
type: 'object',
properties: {
error: {
type: 'object',
properties: {
name: {
type: 'string',
},
message: {
type: 'string',
},
},
required: ['name', 'message'],
},
request: {
type: 'object',
properties: {
method: {
type: 'string',
},
url: {
type: 'string',
},
},
required: ['method', 'url'],
},
response: {
type: 'object',
properties: {
statusCode: {
type: 'number',
},
},
required: ['statusCode'],
},
},
required: ['error', 'request', 'response'],
},
},
securitySchemes: {
JWT: {
type: 'http',
scheme: 'bearer',
bearerFormat: 'JWT',
},
},
},
paths: {
'/': {
get: {
operationId: 'ListNotificationsLegacy',
deprecated: true,
description: 'Deprecated alias for `GET /notifications`.',
parameters: [
{
$ref: '#/components/parameters/offset',
},
{
$ref: '#/components/parameters/limit',
},
{
$ref: '#/components/parameters/orderField',
},
{
$ref: '#/components/parameters/topic',
},
{
$ref: '#/components/parameters/search',
},
{
$ref: '#/components/parameters/read',
},
{
$ref: '#/components/parameters/saved',
},
{
$ref: '#/components/parameters/createdAfter',
},
{
$ref: '#/components/parameters/minimumSeverity',
},
],
responses: {
'200': {
description: 'A page of notifications for the current user.',
content: {
'application/json': {
schema: {
$ref: '#/components/schemas/ListNotificationsResponse',
},
},
},
},
default: {
$ref: '#/components/responses/ErrorResponse',
},
},
security: [
{},
{
JWT: [],
},
],
},
post: {
operationId: 'CreateNotificationsLegacy',
deprecated: true,
description: 'Deprecated alias for `POST /notifications`.',
requestBody: {
required: true,
content: {
'application/json': {
schema: {
$ref: '#/components/schemas/NotificationSendOptions',
},
},
},
},
responses: {
'200': {
description: 'The notifications that were created.',
content: {
'application/json': {
schema: {
type: 'array',
items: {
$ref: '#/components/schemas/Notification',
},
},
},
},
},
default: {
$ref: '#/components/responses/ErrorResponse',
},
},
security: [
{},
{
JWT: [],
},
],
},
},
'/notifications': {
get: {
operationId: 'ListNotifications',
description: 'List notifications for the current user.',
parameters: [
{
$ref: '#/components/parameters/offset',
},
{
$ref: '#/components/parameters/limit',
},
{
$ref: '#/components/parameters/orderField',
},
{
$ref: '#/components/parameters/topic',
},
{
$ref: '#/components/parameters/search',
},
{
$ref: '#/components/parameters/read',
},
{
$ref: '#/components/parameters/saved',
},
{
$ref: '#/components/parameters/createdAfter',
},
{
$ref: '#/components/parameters/minimumSeverity',
},
],
responses: {
'200': {
description: 'A page of notifications for the current user.',
content: {
'application/json': {
schema: {
$ref: '#/components/schemas/ListNotificationsResponse',
},
},
},
},
default: {
$ref: '#/components/responses/ErrorResponse',
},
},
security: [
{},
{
JWT: [],
},
],
},
post: {
operationId: 'CreateNotifications',
description:
'Send a notification to one or more recipients (service credentials required).',
requestBody: {
required: true,
content: {
'application/json': {
schema: {
$ref: '#/components/schemas/NotificationSendOptions',
},
},
},
},
responses: {
'200': {
description: 'The notifications that were created.',
content: {
'application/json': {
schema: {
type: 'array',
items: {
$ref: '#/components/schemas/Notification',
},
},
},
},
},
default: {
$ref: '#/components/responses/ErrorResponse',
},
},
security: [
{},
{
JWT: [],
},
],
},
},
'/update': {
post: {
operationId: 'UpdateNotificationsLegacy',
deprecated: true,
description: 'Deprecated alias for `POST /notifications/update`.',
requestBody: {
required: true,
content: {
'application/json': {
schema: {
$ref: '#/components/schemas/UpdateNotificationsRequest',
},
},
},
},
responses: {
'200': {
description: 'The notifications that were updated.',
content: {
'application/json': {
schema: {
type: 'array',
items: {
$ref: '#/components/schemas/Notification',
},
},
},
},
},
default: {
$ref: '#/components/responses/ErrorResponse',
},
},
security: [
{},
{
JWT: [],
},
],
},
},
'/notifications/update': {
post: {
operationId: 'UpdateNotifications',
description: 'Mark notifications as read/unread or saved/unsaved.',
requestBody: {
required: true,
content: {
'application/json': {
schema: {
$ref: '#/components/schemas/UpdateNotificationsRequest',
},
},
},
},
responses: {
'200': {
description: 'The notifications that were updated.',
content: {
'application/json': {
schema: {
type: 'array',
items: {
$ref: '#/components/schemas/Notification',
},
},
},
},
},
default: {
$ref: '#/components/responses/ErrorResponse',
},
},
security: [
{},
{
JWT: [],
},
],
},
},
'/status': {
get: {
operationId: 'GetStatus',
description: 'Get the unread/read counts for the current user.',
responses: {
'200': {
description: "The current user's notification status.",
content: {
'application/json': {
schema: {
$ref: '#/components/schemas/NotificationStatus',
},
},
},
},
default: {
$ref: '#/components/responses/ErrorResponse',
},
},
security: [
{},
{
JWT: [],
},
],
},
},
'/settings': {
get: {
operationId: 'GetSettings',
description:
"Get the current user's notification settings, merged with defaults.",
responses: {
'200': {
description: 'Effective notification settings for the user.',
content: {
'application/json': {
schema: {
$ref: '#/components/schemas/NotificationSettings',
},
},
},
},
default: {
$ref: '#/components/responses/ErrorResponse',
},
},
security: [
{},
{
JWT: [],
},
],
},
post: {
operationId: 'UpdateSettings',
description: "Update the current user's notification settings.",
requestBody: {
required: true,
content: {
'application/json': {
schema: {
$ref: '#/components/schemas/NotificationSettings',
},
},
},
},
responses: {
'200': {
description: 'Effective notification settings for the user.',
content: {
'application/json': {
schema: {
$ref: '#/components/schemas/NotificationSettings',
},
},
},
},
default: {
$ref: '#/components/responses/ErrorResponse',
},
},
security: [
{},
{
JWT: [],
},
],
},
},
'/topics': {
get: {
operationId: 'GetTopics',
description:
'List notification topics that the user has received notifications on.',
parameters: [
{
$ref: '#/components/parameters/search',
},
{
$ref: '#/components/parameters/read',
},
{
$ref: '#/components/parameters/saved',
},
{
$ref: '#/components/parameters/createdAfter',
},
{
$ref: '#/components/parameters/minimumSeverity',
},
],
responses: {
'200': {
description: 'The list of topics.',
content: {
'application/json': {
schema: {
$ref: '#/components/schemas/GetTopicsResponse',
},
},
},
},
default: {
$ref: '#/components/responses/ErrorResponse',
},
},
security: [
{},
{
JWT: [],
},
],
},
},
'/{id}': {
get: {
operationId: 'GetNotificationLegacy',
deprecated: true,
description: 'Deprecated alias for `GET /notifications/{id}`.',
parameters: [
{
$ref: '#/components/parameters/notificationId',
},
],
responses: {
'200': {
description: 'The requested notification.',
content: {
'application/json': {
schema: {
$ref: '#/components/schemas/Notification',
},
},
},
},
default: {
$ref: '#/components/responses/ErrorResponse',
},
},
security: [
{},
{
JWT: [],
},
],
},
},
'/notifications/{id}': {
get: {
operationId: 'GetNotification',
description: 'Get a single notification by id for the current user.',
parameters: [
{
$ref: '#/components/parameters/notificationId',
},
],
responses: {
'200': {
description: 'The requested notification.',
content: {
'application/json': {
schema: {
$ref: '#/components/schemas/Notification',
},
},
},
},
default: {
$ref: '#/components/responses/ErrorResponse',
},
},
security: [
{},
{
JWT: [],
},
],
},
},
},
} as const;
export const createOpenApiRouter = async (
options?: Parameters<
typeof createValidatedOpenApiRouterFromGeneratedEndpointMap
>['1'],
) =>
createValidatedOpenApiRouterFromGeneratedEndpointMap<EndpointMap>(
spec,
options,
);
@@ -0,0 +1,17 @@
/*
* Copyright 2026 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export * from './generated';
@@ -14,8 +14,9 @@
* limitations under the License.
*/
import express, { Request, Response } from 'express';
import Router from 'express-promise-router';
import express, { Request } from 'express';
import { createOpenApiRouter, EndpointMap } from '../schema/openapi';
import { internal as openapi } from '@backstage/backend-openapi-utils';
import {
normalizeSeverity,
NotificationGetOptions,
@@ -45,7 +46,6 @@ import {
NotificationReadSignal,
NotificationSettings,
notificationSeverities,
NotificationStatus,
OriginSetting,
} from '@backstage/plugin-notifications-common';
import { parseEntityOrderFieldParams } from './parseEntityOrderFieldParams';
@@ -108,7 +108,7 @@ export async function createRouter(
recipientResolver ??
new DefaultNotificationRecipientResolver(auth, catalog);
const getUser = async (req: Request<unknown>) => {
const getUser = async (req: Request<any, any, any, any, any>) => {
const credentials = await httpAuth.credentials(req, { allow: ['user'] });
const info = await userInfo.getUserInfo(credentials);
return info.userEntityRef;
@@ -454,114 +454,112 @@ export async function createRouter(
};
const appendCommonOptions = (
req: Request,
req: {
query: openapi.StaticQueryParamsSchema<EndpointMap, '/topics', 'get'>;
},
opts: NotificationGetOptions | TopicGetOptions,
) => {
if (req.query.search) {
opts.search = req.query.search.toString();
opts.search = req.query.search;
}
if (req.query.read === 'true') {
opts.read = true;
} else if (req.query.read === 'false') {
opts.read = false;
// or keep undefined
}
if (req.query.saved === 'true') {
opts.saved = true;
} else if (req.query.saved === 'false') {
opts.saved = false;
// or keep undefined
}
if (req.query.createdAfter) {
const sinceEpoch = Date.parse(String(req.query.createdAfter));
const sinceEpoch = Date.parse(req.query.createdAfter);
if (isNaN(sinceEpoch)) {
throw new InputError('Unexpected date format');
}
opts.createdAfter = new Date(sinceEpoch);
}
if (req.query.minimumSeverity) {
opts.minimumSeverity = normalizeSeverity(
req.query.minimumSeverity.toString(),
);
opts.minimumSeverity = normalizeSeverity(req.query.minimumSeverity);
}
};
// TODO: Move to use OpenAPI router instead
const router = Router();
router.use(express.json());
const router = await createOpenApiRouter();
const listNotificationsHandler = async (req: Request, res: Response) => {
const listNotificationsHandler: openapi.EndpointMapRequestHandler<
EndpointMap,
'/notifications',
'get'
> = async (req, res) => {
const user = await getUser(req);
const opts: NotificationGetOptions = {
user: user,
};
if (req.query.offset) {
opts.offset = Number.parseInt(req.query.offset.toString(), 10);
const opts: NotificationGetOptions = { user };
if (req.query.offset !== undefined) {
opts.offset = req.query.offset;
}
if (req.query.limit) {
opts.limit = Number.parseInt(req.query.limit.toString(), 10);
if (req.query.limit !== undefined) {
opts.limit = req.query.limit;
}
if (req.query.orderField) {
opts.orderField = parseEntityOrderFieldParams(req.query);
if (req.query.orderField !== undefined) {
opts.orderField = parseEntityOrderFieldParams({
orderField: req.query.orderField,
});
}
if (req.query.topic) {
opts.topic = req.query.topic.toString();
opts.topic = req.query.topic;
}
appendCommonOptions(req, opts);
const [notifications, totalCount] = await Promise.all([
store.getNotifications(opts),
store.getNotificationsCount(opts),
]);
res.json({
totalCount,
notifications,
});
res.json({ totalCount, notifications });
};
router.get('/', listNotificationsHandler); // Deprecated endpoint
router.get('/', listNotificationsHandler); // Deprecated alias of /notifications
router.get('/notifications', listNotificationsHandler);
router.get('/status', async (req: Request<any, NotificationStatus>, res) => {
router.get('/status', async (req, res) => {
const user = await getUser(req);
const status = await store.getStatus({ user });
res.json(status);
});
router.get(
'/settings',
async (req: Request<any, NotificationSettings>, res) => {
const user = await getUser(req);
const response = await getNotificationSettings(user);
res.json(response);
},
);
router.get('/settings', async (req, res) => {
const user = await getUser(req);
const response = await getNotificationSettings(user);
res.json(response);
});
router.post(
'/settings',
async (
req: Request<any, NotificationSettings, NotificationSettings>,
res,
) => {
const user = await getUser(req);
const channels = getNotificationChannels();
const settings: NotificationSettings = req.body;
if (settings.channels.some(c => !channels.includes(c.id))) {
throw new InputError('Invalid channel');
}
await store.saveNotificationSettings({ user, settings });
const response = await getNotificationSettings(user);
res.json(response);
},
);
router.post('/settings', async (req, res) => {
const user = await getUser(req);
const channels = getNotificationChannels();
const settings: NotificationSettings = req.body;
if (settings.channels.some(c => !channels.includes(c.id))) {
throw new InputError('Invalid channel');
}
await store.saveNotificationSettings({ user, settings });
const response = await getNotificationSettings(user);
res.json(response);
});
const getNotificationHandler = async (req: Request, res: Response) => {
router.get('/topics', async (req, res) => {
const user = await getUser(req);
const opts: TopicGetOptions = { user };
appendCommonOptions(req, opts);
const topics = await store.getTopics(opts);
res.json(topics);
});
const getNotificationHandler: openapi.EndpointMapRequestHandler<
EndpointMap,
'/notifications/{id}',
'get'
> = async (req, res) => {
const user = await getUser(req);
const opts: NotificationGetOptions = {
user: user,
user,
limit: 1,
ids: [req.params.id],
};
@@ -572,31 +570,17 @@ export async function createRouter(
res.json(notifications[0]);
};
// Get topics
const listTopicsHandler = async (req: Request, res: Response) => {
const user = await getUser(req);
const opts: TopicGetOptions = {
user: user,
};
appendCommonOptions(req, opts);
const topics = await store.getTopics(opts);
res.json(topics);
};
router.get('/topics', listTopicsHandler);
// Make sure this is the last "GET" handler
router.get('/:id', getNotificationHandler); // Deprecated endpoint
// Make sure this is the last "GET" handler.
router.get('/:id', getNotificationHandler); // Deprecated alias of /notifications/:id
router.get('/notifications/:id', getNotificationHandler);
const updateNotificationsHandler = async (req: Request, res: Response) => {
const updateNotificationsHandler: openapi.EndpointMapRequestHandler<
EndpointMap,
'/notifications/update',
'post'
> = async (req, res) => {
const user = await getUser(req);
const { ids, read, saved } = req.body;
if (!ids || !Array.isArray(ids)) {
throw new InputError();
}
if (read === true) {
await store.markRead({ user, ids });
@@ -609,7 +593,7 @@ export async function createRouter(
});
}
} else if (read === false) {
await store.markUnread({ user: user, ids });
await store.markUnread({ user, ids });
if (signals) {
await signals.publish<NotificationReadSignal>({
@@ -621,16 +605,16 @@ export async function createRouter(
}
if (saved === true) {
await store.markSaved({ user: user, ids });
await store.markSaved({ user, ids });
} else if (saved === false) {
await store.markUnsaved({ user: user, ids });
await store.markUnsaved({ user, ids });
}
const notifications = await store.getNotifications({ ids, user: user });
const notifications = await store.getNotifications({ ids, user });
res.json(notifications);
};
router.post('/update', updateNotificationsHandler); // Deprecated endpoint
router.post('/update', updateNotificationsHandler); // Deprecated alias of /notifications/update
router.post('/notifications/update', updateNotificationsHandler);
const sendBroadcastNotification = async (
@@ -768,10 +752,11 @@ export async function createRouter(
return sent.filter(n => n !== undefined);
};
const createNotificationHandler = async (
req: Request<any, Notification[], NotificationSendOptions>,
res: Response,
) => {
const createNotificationsHandler: openapi.EndpointMapRequestHandler<
EndpointMap,
'/notifications',
'post'
> = async (req, res) => {
const credentials = await httpAuth.credentials(req, {
allow: ['service'],
});
@@ -848,8 +833,8 @@ export async function createRouter(
};
// Add new notification
router.post('/', createNotificationHandler);
router.post('/notifications', createNotificationHandler);
router.post('/', createNotificationsHandler); // Deprecated alias of /notifications
router.post('/notifications', createNotificationsHandler);
return router;
}