Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Workflows with XState #1019

Open
wants to merge 64 commits into
base: feature/svelte
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 60 commits
Commits
Show all changes
64 commits
Select commit Hold shift + click to select a range
20d71af
Define simple workflow with xstate
FyreByrd Sep 9, 2024
8c0b33c
Move workflow definition to common
FyreByrd Sep 9, 2024
a0359ca
Move workflow to common/public
FyreByrd Sep 9, 2024
a7f75e0
Change to full state names
FyreByrd Sep 9, 2024
551d040
Use workflow in tasks/id/server
FyreByrd Sep 9, 2024
b606476
Filter form based on workflow state
FyreByrd Sep 9, 2024
7868c38
Fix task link
7dev7urandom Sep 9, 2024
912c6c9
Add table for workflow instances
FyreByrd Sep 9, 2024
9652c71
Add workflow instances to paraglide
FyreByrd Sep 10, 2024
8245ed9
Don't drop columns for upcoming feature
FyreByrd Sep 10, 2024
2e0dc33
Basic workflow pages, open inspector
FyreByrd Sep 11, 2024
6dbc98d
Basic workflow pages, open inspector
FyreByrd Sep 11, 2024
ebf651e
Switch to Svelvet for visualization
FyreByrd Sep 12, 2024
a9d918b
Jump to arbitrary state from visualizer
FyreByrd Sep 12, 2024
9ef1946
Style and todos
7dev7urandom Sep 12, 2024
782ba2b
Improved instance management menu
FyreByrd Sep 13, 2024
7dfab70
Retrieve snapshot from db
FyreByrd Sep 13, 2024
a0fb661
Add more states to flow
FyreByrd Sep 13, 2024
ebce6f9
Dynamic number of input anchors on visualization
FyreByrd Sep 13, 2024
3e943c7
Add Email Reviewers state
FyreByrd Sep 16, 2024
e9413a1
Force based graph layout
FyreByrd Sep 16, 2024
3f30715
Support nodes with fixed position
FyreByrd Sep 17, 2024
fcf3434
Create fixed nodes, show graph before finished
FyreByrd Sep 17, 2024
b72cf67
Show workflow diagram ASAP
FyreByrd Sep 17, 2024
e6f8dcc
Create product function
FyreByrd Sep 17, 2024
8413cea
Create database entry for instance
FyreByrd Sep 17, 2024
e116d71
Move workflow to server-only
FyreByrd Sep 18, 2024
7e58204
Display comment in task list page
FyreByrd Sep 18, 2024
bd9cf0e
Create snapshots on transitions
FyreByrd Sep 18, 2024
89b32db
Update UserTasks from state machine
FyreByrd Sep 18, 2024
1f872e2
Remove hidden product field
FyreByrd Sep 18, 2024
c9cf707
Filter actions by role, submit actions
FyreByrd Sep 18, 2024
c9cbd78
Update ProductTransitions table on state transition
FyreByrd Sep 18, 2024
4b79a9d
Fully parameterized state machine
FyreByrd Sep 23, 2024
133c1b6
Filter visualization
FyreByrd Sep 23, 2024
5e37f9f
Filter and handle actions
FyreByrd Sep 23, 2024
4604f49
Add dummy guards, change filterMeta
FyreByrd Sep 24, 2024
b609ed1
Static position for more nodes
FyreByrd Sep 24, 2024
64ae312
Turn action states to just actions
FyreByrd Sep 24, 2024
6dc163b
Switch on correct build env conditions
FyreByrd Sep 24, 2024
6413f70
Remove unused packages
FyreByrd Sep 24, 2024
6663e32
Add some comments
FyreByrd Sep 24, 2024
0dbf036
Fix transitions DB query
FyreByrd Sep 24, 2024
b8ad704
Fix snapshot issue
FyreByrd Sep 24, 2024
12408e5
Change formData action to flowAction
FyreByrd Sep 24, 2024
2228c69
Fix start state
FyreByrd Sep 24, 2024
809dc7c
Fix command log in transit action
FyreByrd Sep 24, 2024
1692030
Rename transform function
FyreByrd Sep 25, 2024
adad484
Add note to redo visualization layout later
FyreByrd Sep 25, 2024
dd262bf
Change later: to TODO:
FyreByrd Sep 25, 2024
b634484
Delete unneeded DB queries
FyreByrd Sep 25, 2024
9c49c9d
Change types for getSnapshot and resolveSnapshot
FyreByrd Sep 25, 2024
fd5a826
Fix bad state name
FyreByrd Sep 25, 2024
473fce8
Filter task fields in query
FyreByrd Sep 25, 2024
2e7ac65
Change filter to just arrays. Rename AdminLevel
FyreByrd Sep 26, 2024
17ec8f7
Wrap DefaultWorkflow in a class
FyreByrd Sep 27, 2024
2256da0
Provide more explanatory comments
FyreByrd Sep 27, 2024
71475ad
Fix workflow url
FyreByrd Sep 27, 2024
77ab2d9
Include transition command in workflow admin data
FyreByrd Sep 27, 2024
1ac77dc
Remove unneeded console logs
FyreByrd Sep 27, 2024
a5ca574
Update source/SIL.AppBuilder.Portal/common/workflow/index.ts
7dev7urandom Sep 30, 2024
801024e
Rename WorkflowAdminLevel to RequiredAdminLevel
FyreByrd Sep 30, 2024
3dd3423
Change Workflow construction methods
FyreByrd Sep 30, 2024
e3094e9
Fix last reference to pre-OOP workflow
FyreByrd Oct 1, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
154 changes: 154 additions & 0 deletions source/SIL.AppBuilder.Portal/common/databaseProxy/Products.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,70 @@
import type { Prisma } from '@prisma/client';
import prisma from '../prisma.js';
import { RequirePrimitive } from './utility.js';

export async function create(
productData: RequirePrimitive<Prisma.ProductsUncheckedCreateInput>
): Promise<boolean | string> {
if (
!validateProductBase(
productData.ProjectId,
productData.ProductDefinitionId,
productData.StoreId,
productData.StoreLanguageId
)
)
return false;

// No additional verification steps

try {
const res = await prisma.products.create({
data: productData
});
return res.Id;
} catch (e) {
return false;
}
}

export async function update(
id: string,
productData: RequirePrimitive<Prisma.ProductsUncheckedUpdateInput>
): Promise<boolean> {
// There are cases where a db lookup is not necessary to verify that it will
// be a legal relation after the update, such as if none of the relevant
// columns are changed, but for simplicity we just lookup once anyway
const existing = await prisma.products.findUnique({
where: {
Id: id
}
});
const projectId = productData.ProjectId ?? existing!.ProjectId;
const productDefinitionId = productData.ProductDefinitionId ?? existing!.ProductDefinitionId;
const storeId = productData.StoreId ?? existing!.StoreId;
const storeLanguageId = productData.StoreLanguageId ?? existing!.StoreLanguageId;
if (!validateProductBase(
projectId,
productDefinitionId,
storeId,
storeLanguageId
)) return false;

// No additional verification steps

try {
await prisma.products.update({
where: {
Id: id
},
data: productData
});
// TODO: Are there any other updates that need to be done?
} catch (e) {
return false;
}
return true;
}

function deleteProduct(productId: string) {
// Delete all userTasks for this product, and delete the product
Expand All @@ -16,3 +82,91 @@ function deleteProduct(productId: string) {
]);
}
export { deleteProduct as delete };

/** A product is valid if:
* 1. The store's type matches the Workflow's store type
* 2. The project has a WorkflowProjectUrl
* 3. The store is allowed by the organization
* 4. The language is allowed by the store
* 5. The product type is allowed by the organization
*/
async function validateProductBase(
projectId: number,
productDefinitionId: number,
storeId: number,
storeLanguageId: number
) {
const productDefinition = await prisma.productDefinitions.findUnique({
where: {
Id: productDefinitionId
},
select: {
Id: true,
// Store type must match Workflow store type
Workflow: {
select: {
StoreTypeId: true
}
}
}
});
const project = await prisma.projects.findUnique({
where: {
Id: projectId,
// Project must have a WorkflowProjectUrl
WorkflowProjectUrl: {
not: null
}
},
select: {
Organization: {
select: {
// Store must be allowed by Organization
OrganizationStores: {
where: {
StoreId: storeId
},
select: {
Store: {
select: {
StoreType: {
select: {
// Store type must match Workflow store type
Id: true,
// StoreLanguage must be allowed by Store
StoreLanguages: {
where: {
Id: storeLanguageId
}
}
}
}
}
}
}
},
// Product type must be allowed by Organization
OrganizationProductDefinitions: {
where: {
ProductDefinitionId: productDefinition?.Id
}
}
}
}
}
});

// 3. The store is allowed by the organization
return (
project?.Organization.OrganizationStores.length > 0 &&
// 1. The store's type matches the Workflow's store type
productDefinition?.Workflow.StoreTypeId ===
project.Organization.OrganizationStores[0].Store.StoreType.Id &&
// 2. The project has a WorkflowProjectUrl
// handled by query
// 4. The language is allowed by the store
project.Organization.OrganizationStores[0].Store.StoreType.StoreLanguages.length > 0 &&
// 5. The product type is allowed by the organization
project.Organization.OrganizationProductDefinitions.length > 0
);
}
2 changes: 1 addition & 1 deletion source/SIL.AppBuilder.Portal/common/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@ export * as BullMQ from './BullJobTypes.js';
export { scriptoriaQueue } from './bullmq.js';
export { default as DatabaseWrites } from './databaseProxy/index.js';
export { readonlyPrisma as prisma } from './prisma.js';

export { Workflow } from './workflow/index.js';
3 changes: 2 additions & 1 deletion source/SIL.AppBuilder.Portal/common/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
"description": "",
"exports": {
".": "./index.js",
"./prisma": "./public/prisma.js"
"./prisma": "./public/prisma.js",
"./workflow": "./public/workflow.js"
},
"author": "",
"type": "module",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
-- CreateTable
CREATE TABLE "WorkflowInstances" (
"Id" SERIAL NOT NULL,
"Snapshot" TEXT NOT NULL,
"ProductId" UUID NOT NULL,

CONSTRAINT "PK_WorkflowInstances" PRIMARY KEY ("Id")
);

-- CreateIndex
CREATE UNIQUE INDEX "WorkflowInstances_ProductId_key" ON "WorkflowInstances"("ProductId");

-- AddForeignKey
ALTER TABLE "WorkflowInstances" ADD CONSTRAINT "FK_WorkflowInstances_Products_ProductId" FOREIGN KEY ("ProductId") REFERENCES "Products"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION;
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (i.e. Git)
provider = "postgresql"
2 changes: 1 addition & 1 deletion source/SIL.AppBuilder.Portal/common/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -287,7 +287,7 @@ model Products {
StoreLanguage StoreLanguages? @relation(fields: [StoreLanguageId], references: [Id], onDelete: Restrict, onUpdate: NoAction, map: "FK_Products_StoreLanguages_StoreLanguageId")
Store Stores? @relation(fields: [StoreId], references: [Id], onDelete: Restrict, onUpdate: NoAction, map: "FK_Products_Stores_StoreId")
UserTasks UserTasks[]
WorkflowInstances WorkflowInstances?
WorkflowInstance WorkflowInstances?

@@index([ProductDefinitionId], map: "IX_Products_ProductDefinitionId")
@@index([ProjectId], map: "IX_Products_ProjectId")
Expand Down
138 changes: 138 additions & 0 deletions source/SIL.AppBuilder.Portal/common/public/workflow.ts
FyreByrd marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import type { RoleId } from './prisma.js';

export enum ActionType {
/** Automated Action */
Auto = 0,
/** User-initiated Action */
User
}

/**
* The administrative requirements of the workflow.
* Examples:
* - If the flow has `WorkflowAdminLevel.High` it will include extra state to represent the organizational approval process
* - If the flow has `WorkflowAdminLevel.Low` it will not include those states, but there are still some states that require action from an OrgAdmin to complete certain actions
* - If the flow has `WorkflowAdminLevel.None` none of the states or actions for the workflow instance will require an OrgAdmin.
*
* Any state or transition can have a list of specified `WorkflowAdminLevel`s. What this means is that those states and transitions will be included in a workflow instance ONLY when the instance's `WorkflowAdminLevel` is in the state's or transition's list.
*
* If a state or transition does not specify any `WorkflowAdminLevel` it will be included (provided it passes other conditions not dependent on `WorkflowAdminLevel`).
*/
export enum WorkflowAdminLevel {
FyreByrd marked this conversation as resolved.
Show resolved Hide resolved
/** NoAdmin/OwnerAdmin */
None = 0,
/** LowAdmin */
Low,
/** Approval required */
High
}

export enum ProductType {
Android_GooglePlay = 0,
Android_S3,
AssetPackage,
Web
}

export type StateName =
| 'Readiness Check'
| 'Approval'
| 'Approval Pending'
| 'Terminated'
| 'App Builder Configuration'
| 'Author Configuration'
| 'Synchronize Data'
| 'Author Download'
| 'Author Upload'
| 'Product Build'
| 'App Store Preview'
| 'Create App Store Entry'
| 'Verify and Publish'
| 'Product Publish'
| 'Make It Live'
| 'Published'
| 'Product Creation';

export type WorkflowContext = {
instructions:
| 'asset_package_verify_and_publish'
| 'app_configuration'
| 'create_app_entry'
| 'authors_download'
| 'googleplay_configuration'
| 'googleplay_verify_and_publish'
| 'make_it_live'
| 'approval_pending'
| 'readiness_check'
| 'synchronize_data'
| 'authors_upload'
| 'verify_and_publish'
| 'waiting'
| 'web_verify'
| null;
includeFields: (
| 'ownerName'
| 'ownerEmail'
| 'storeDescription'
| 'listingLanguageCode'
| 'projectURL'
| 'productDescription'
| 'appType'
| 'projectLanguageCode'
)[];
includeReviewers: boolean;
includeArtifacts: 'apk' | 'aab' | boolean;
start?: StateName;
adminLevel: WorkflowAdminLevel;
environment: BuildEnv;
productType: ProductType;
};

// These are all specific to the Google Play workflows
// Not sure how these are used, but will figure out when integrating into backend
export type BuildEnv = {
googlePlayDraft?: boolean;
googlePlayExisting?: boolean;
googlePlayUploaded?: boolean;
};

export type WorkflowInput = {
adminLevel: WorkflowAdminLevel;
productType: ProductType;
};

/** Used for filtering based on AdminLevel and/or ProductType */
export type MetaFilter = {
level?: WorkflowAdminLevel[];
product?: ProductType[];
};

export type WorkflowStateMeta = MetaFilter;

export type WorkflowTransitionMeta = MetaFilter & {
type: ActionType;
user?: RoleId;
};

export type WorkflowEvent = {
type: any;
comment?: string;
target?: StateName;
userId: number | null;
};

export type StateNode = {
id: number;
label: string;
connections: { id: number; target: string; label: string }[];
inCount: number;
start?: boolean;
final?: boolean;
action?: boolean;
};

export type Snapshot = {
value: string;
context: WorkflowContext;
input: WorkflowInput;
};
Loading