함수로 알림 내용과 수신자 구성
알림 함수는 Foundry 안에 표시할 메시지와 외부 이메일 내용을 만들거나 수신자를 계산합니다. Notification을 반환하는 것만으로 직접 메일을 보내지는 않습니다. 액션의 알림 규칙에서 내용 함수와 수신자 함수를 각각 선택해야 합니다.
반환 타입
| 목적 | TSv1 | TSv2 | Python |
|---|---|---|---|
| 알림 내용 | Notification builder | platformNotification과 emailNotification을 가진 Notification | Notification 및 PlatformNotification, EmailNotification |
| 사용자 | User | UserId | UserId |
| 사용자 또는 그룹 | Principal | type과 id를 가진 Principal | Principal.user 또는 Principal.group |
플랫폼 알림은 짧은 heading, content와 링크를, 이메일은 subject, body와 링크를 가집니다. 이메일 body는 headless HTML을 사용할 수 있으므로 사용자 이름이나 객체 설명을 넣을 때 HTML로 해석되지 않도록 이스케이프합니다.
아래의 가상 Issue는 description, assignee, reporter와 group 속성을 가집니다. description은 String이며 나머지는 Foundry의 실제 사용자 또는 그룹 ID를 저장한 String입니다. 객체 링크를 포함한 Python 알림은 Notification[Issue]처럼 반환 타입에 해당 객체 유형을 선언합니다.
TypeScript의 HTML 텍스트 처리
다음 helper를 각 TypeScript 저장소의 src/notificationText.ts에 둡니다. 타입과 import는 선택한 함수 언어에 맞는 아래 예제를 사용합니다.
export function escapeHtml(value: string): string {
return value.replace(/[&<>"']/g, character => {
switch (character) {
case "&": return "&";
case "<": return "<";
case ">": return ">";
case '"': return """;
case "'": return "'";
default: return character;
}
});
}
TypeScript v1
User 입력은 알림 문구의 개인화에 사용합니다. 수신자 지정은 별도 규칙입니다. firstName이 없으면 실제 username을 사용합니다.
src/notificationFunctions.ts에 작성하고 index.ts에서 클래스를 내보냅니다.
import {
EmailNotificationContent, Function, Notification, ShortNotification,
User, UserFacingError, Users,
} from "@foundry/functions-api";
import { Issue } from "@foundry/ontology-api";
import { escapeHtml } from "./notificationText";
export class NotificationFunctions {
@Function()
public createIssueNotification(issue: Issue, user: User): Notification {
return this.build(issue, user, "New issue", "A new issue has been assigned to you.");
}
@Function()
public async createIssueReminderNotification(issue: Issue): Promise<Notification> {
if (!issue.assignee) throw new UserFacingError("담당자가 없는 이슈입니다.");
const user = await Users.getUserByIdAsync(issue.assignee);
if (user === undefined) throw new UserFacingError("담당자 계정을 조회할 수 없습니다.");
return this.build(issue, user, "Issue reminder", "Investigate this issue.");
}
@Function()
public async getIssueAssigneeAndReporter(issue: Issue): Promise<User[]> {
if (!issue.assignee || !issue.reporter) throw new UserFacingError("담당자와 신고자가 필요합니다.");
const [assignee, reporter] = await Promise.all([
Users.getUserByIdAsync(issue.assignee),
Users.getUserByIdAsync(issue.reporter),
]);
if (assignee === undefined || reporter === undefined) {
throw new UserFacingError("담당자 또는 신고자 계정을 조회할 수 없습니다.");
}
return [assignee, reporter];
}
private build(issue: Issue, user: User, heading: string, content: string): Notification {
if (issue.description === undefined) throw new UserFacingError("이슈 설명이 필요합니다.");
const body = `<p>Hello, ${escapeHtml(user.firstName ?? user.username)}.</p>` +
`<p>${escapeHtml(content)} ${escapeHtml(issue.description)}</p>`;
return Notification.builder()
.shortNotification(ShortNotification.builder()
.heading(heading).content(content).addObjectLink("Issue", issue).build())
.emailNotificationContent(EmailNotificationContent.builder()
.subject(heading).body(body).addObjectLink("Issue", issue).build())
.build();
}
}
그룹이 필요한 함수는 Users.getGroupByIdAsync로 조회하고 Principal 또는 Principal[]로 반환할 수 있습니다.
TypeScript v2
TSv2 관리자 API의 사용자 표시 이름 필드는 givenName이며 TSv1 User.firstName과 다릅니다. 객체를 함께 읽는 함수는 첫 인수에 런타임이 주입하는 Client를 사용합니다.
공통 내용 생성은 src/issueNotification.ts에 둡니다. 이 helper는 게시 함수가 아니며 아래 src/functions의 기본 내보내기 함수에서 호출합니다.
import { Osdk } from "@osdk/client";
import { Notification, NotificationLink, UserFacingError } from "@osdk/functions";
import { Issue } from "@ontology/sdk";
import { escapeHtml } from "./notificationText";
export function issueNotification(
issue: Osdk.Instance<Issue>, heading: string, content: string, recipientName?: string
): Notification {
if (issue.description === undefined) throw new UserFacingError("이슈 설명이 필요합니다.");
const links: NotificationLink[] = [{ label: "Issue", linkTarget: { type: "object", object: issue } }];
const greeting = recipientName === undefined ? "Hello" : `Hello, ${escapeHtml(recipientName)}`;
return {
platformNotification: { heading, content, links },
emailNotification: {
subject: heading,
body: `<p>${greeting}.</p><p>${escapeHtml(content)} ${escapeHtml(issue.description)}</p>`,
links,
},
};
}
createIssueNotification.ts는 사용자 조회 없이 이슈 내용과 링크를 반환합니다.
import { Osdk } from "@osdk/client";
import { Notification } from "@osdk/functions";
import { Issue } from "@ontology/sdk";
import { issueNotification } from "../issueNotification";
export default function createIssueNotification(issue: Osdk.Instance<Issue>): Notification {
return issueNotification(issue, "New issue", "A new issue has been assigned to you.");
}
createIssueReminderNotification.ts는 담당자 정보를 조회합니다. 관리자 API를 사용하는 패키지를 설치해야 합니다.
import { Client, Osdk } from "@osdk/client";
import { Notification, UserFacingError } from "@osdk/functions";
import { Users } from "@osdk/foundry.admin";
import { Issue } from "@ontology/sdk";
import { issueNotification } from "../issueNotification";
export default async function createIssueReminderNotification(
client: Client, issue: Osdk.Instance<Issue>
): Promise<Notification> {
if (!issue.assignee) throw new UserFacingError("담당자가 없는 이슈입니다.");
const user = await Users.get(client, issue.assignee);
return issueNotification(issue, "Issue reminder", "Investigate this issue.", user.givenName ?? user.username);
}
getIssueAssigneeAndReporter.ts는 사용자 ID 목록을 반환합니다.
import { Client, Osdk } from "@osdk/client";
import { UserId, UserFacingError } from "@osdk/functions";
import { Users } from "@osdk/foundry.admin";
import { Issue } from "@ontology/sdk";
export default async function getIssueAssigneeAndReporter(
client: Client, issue: Osdk.Instance<Issue>
): Promise<UserId[]> {
if (!issue.assignee || !issue.reporter) throw new UserFacingError("담당자와 신고자가 필요합니다.");
const [user, reporter] = await Promise.all([
Users.get(client, issue.assignee), Users.get(client, issue.reporter),
]);
return [user.id, reporter.id];
}
사용자와 그룹을 함께 반환하는 getIssueAssigneeAndGroups.ts는 Principal을 사용합니다.
import { Client, Osdk } from "@osdk/client";
import { Principal, UserFacingError } from "@osdk/functions";
import { Users, Groups } from "@osdk/foundry.admin";
import { Issue } from "@ontology/sdk";
export default async function getIssueAssigneeAndGroups(
client: Client, issue: Osdk.Instance<Issue>
): Promise<Principal[]> {
if (!issue.assignee || !issue.group) throw new UserFacingError("담당자와 그룹이 필요합니다.");
const [user, group] = await Promise.all([
Users.get(client, issue.assignee), Groups.get(client, issue.group),
]);
return [{ type: "user", id: user.id }, { type: "group", id: group.id }];
}
Python
foundry-platform-sdk를 설치한 함수 환경에서는 FoundryClient()가 런타임의 인증 문맥을 사용합니다. 사용자 토큰이나 호스트를 코드에 넣지 않습니다. 생성 ontology_sdk의 Issue와 플랫폼 SDK의 FoundryClient를 구분합니다.
from html import escape
from functions.api import (
function, Notification, PlatformNotification, NotificationObjectLink,
EmailNotification, Principal, UserId, UserFacingError,
)
from foundry_sdk import FoundryClient
from ontology_sdk.ontology.objects import Issue
def _notification(issue: Issue, heading: str, content: str, recipient_name: str | None = None) -> Notification[Issue]:
if issue.description is None:
raise UserFacingError("이슈 설명이 필요합니다.")
links = [NotificationObjectLink(label="Issue", objectTarget=issue)]
greeting = "Hello" if recipient_name is None else f"Hello, {escape(recipient_name)}"
return Notification(
platform_notification=PlatformNotification(heading=heading, content=content, links=links),
email_notification=EmailNotification(
subject=heading,
body=f"<p>{greeting}.</p><p>{escape(content)} {escape(issue.description)}</p>",
links=links,
),
)
@function
def create_issue_notification(issue: Issue) -> Notification[Issue]:
return _notification(issue, "New issue", "A new issue has been assigned to you.")
@function
def create_issue_reminder_notification(issue: Issue) -> Notification[Issue]:
if not issue.assignee:
raise UserFacingError("담당자가 없는 이슈입니다.")
user = FoundryClient().admin.User.get(issue.assignee)
name = user.username if user.given_name is None else user.given_name
return _notification(issue, "Issue reminder", "Investigate this issue.", name)
@function
def get_issue_assignee_and_reporter(issue: Issue) -> list[UserId]:
if not issue.assignee or not issue.reporter:
raise UserFacingError("담당자와 신고자가 필요합니다.")
client = FoundryClient()
user = client.admin.User.get(issue.assignee)
reporter = client.admin.User.get(issue.reporter)
return [user.id, reporter.id]
@function
def get_issue_assignee_and_group(issue: Issue) -> list[Principal]:
if not issue.assignee or not issue.group:
raise UserFacingError("담당자와 그룹이 필요합니다.")
client = FoundryClient()
user = client.admin.User.get(issue.assignee)
group = client.admin.Group.get(issue.group)
return [Principal.user(user.id), Principal.group(group.id)]
Python 관리자 API는 given_name을 사용합니다. 사용자나 그룹 조회가 권한 또는 존재 여부 오류로 실패하면 정상 수신자 목록으로 바꾸지 않고 실패가 전파됩니다. 런타임의 플랫폼 API 범위는 Platform SDK 권한을 따릅니다.
액션 연결과 권한 경계
액션 알림 설정의 From a Function에서 내용 함수를 선택하고 수신자는 별도로 구성합니다. 함수 내용의 최대 수신자는 50명입니다. 그룹은 개인 사용자로 풀어 권한을 검사합니다.
- Require all users to have permissions가 기본값입니다. 한 명이라도 알림 내용에 쓰인 객체 데이터 권한이 없으면 객체 편집과 알림을 수행하지 않습니다.
- Require any user to have permissions를 선택하면 적어도 한 명이 접근할 수 있을 때 액션이 성공하며 권한이 있는 사람에게만 보냅니다.
이 제출 전 검사와 커밋 뒤 전송 실패는 다릅니다. 전송이 실패하더라도 온톨로지 편집은 이미 성공했을 수 있습니다. 라이브 미리보기에서는 수신자 데이터 권한을 검사하지 않으므로 미리보기 성공으로 액션 전달 권한을 판단할 수 없습니다.
수신자 개인 설정과 플랫폼의 이메일 가림 정책에 따라 이메일 대신 플랫폼 알림만 받거나 일반 안내 이메일을 받을 수 있습니다.