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

Merged
merged 99 commits into from
Oct 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
99 commits
Select commit Hold shift + click to select a range
ee6ffd5
Define simple workflow with xstate
FyreByrd Sep 9, 2024
be0413d
Move workflow definition to common
FyreByrd Sep 9, 2024
6214894
Move workflow to common/public
FyreByrd Sep 9, 2024
d88a345
Change to full state names
FyreByrd Sep 9, 2024
782f03e
Use workflow in tasks/id/server
FyreByrd Sep 9, 2024
d0ddbd7
Filter form based on workflow state
FyreByrd Sep 9, 2024
54b27ee
Fix task link
7dev7urandom Sep 9, 2024
c50ef92
Add table for workflow instances
FyreByrd Sep 9, 2024
6601d9e
Add workflow instances to paraglide
FyreByrd Sep 10, 2024
68264de
Don't drop columns for upcoming feature
FyreByrd Sep 10, 2024
7f60baf
Basic workflow pages, open inspector
FyreByrd Sep 11, 2024
9543e2f
Basic workflow pages, open inspector
FyreByrd Sep 11, 2024
d0f03a1
Switch to Svelvet for visualization
FyreByrd Sep 12, 2024
f10abdf
Jump to arbitrary state from visualizer
FyreByrd Sep 12, 2024
9c2fb7a
Style and todos
7dev7urandom Sep 12, 2024
036f0b3
Improved instance management menu
FyreByrd Sep 13, 2024
411cd7f
Retrieve snapshot from db
FyreByrd Sep 13, 2024
325743c
Add more states to flow
FyreByrd Sep 13, 2024
1748b1a
Dynamic number of input anchors on visualization
FyreByrd Sep 13, 2024
15f539b
Add Email Reviewers state
FyreByrd Sep 16, 2024
45b2fa5
Force based graph layout
FyreByrd Sep 16, 2024
737292b
Support nodes with fixed position
FyreByrd Sep 17, 2024
6b0e4f0
Create fixed nodes, show graph before finished
FyreByrd Sep 17, 2024
332ae67
Show workflow diagram ASAP
FyreByrd Sep 17, 2024
70da079
Create product function
FyreByrd Sep 17, 2024
8098be9
Create database entry for instance
FyreByrd Sep 17, 2024
f108e61
Move workflow to server-only
FyreByrd Sep 18, 2024
0a96e24
Display comment in task list page
FyreByrd Sep 18, 2024
07126c2
Create snapshots on transitions
FyreByrd Sep 18, 2024
a9377d3
Update UserTasks from state machine
FyreByrd Sep 18, 2024
c4c2a34
Remove hidden product field
FyreByrd Sep 18, 2024
6ab8d12
Filter actions by role, submit actions
FyreByrd Sep 18, 2024
73b0eef
Update ProductTransitions table on state transition
FyreByrd Sep 18, 2024
d9aa17c
Fully parameterized state machine
FyreByrd Sep 23, 2024
5fb7a1f
Filter visualization
FyreByrd Sep 23, 2024
155bc4d
Filter and handle actions
FyreByrd Sep 23, 2024
0e4a7f4
Add dummy guards, change filterMeta
FyreByrd Sep 24, 2024
09d85dc
Static position for more nodes
FyreByrd Sep 24, 2024
82e7c62
Turn action states to just actions
FyreByrd Sep 24, 2024
61bb682
Switch on correct build env conditions
FyreByrd Sep 24, 2024
5bce88a
Remove unused packages
FyreByrd Sep 24, 2024
7d3c2b6
Add some comments
FyreByrd Sep 24, 2024
58dade3
Fix transitions DB query
FyreByrd Sep 24, 2024
8ee2880
Fix snapshot issue
FyreByrd Sep 24, 2024
2b8670c
Change formData action to flowAction
FyreByrd Sep 24, 2024
aa57a47
Fix start state
FyreByrd Sep 24, 2024
e4bface
Fix command log in transit action
FyreByrd Sep 24, 2024
0955387
Rename transform function
FyreByrd Sep 25, 2024
843253e
Add note to redo visualization layout later
FyreByrd Sep 25, 2024
8209113
Change later: to TODO:
FyreByrd Sep 25, 2024
71ca070
Delete unneeded DB queries
FyreByrd Sep 25, 2024
c276496
Change types for getSnapshot and resolveSnapshot
FyreByrd Sep 25, 2024
911a43e
Fix bad state name
FyreByrd Sep 25, 2024
c6f46e0
Filter task fields in query
FyreByrd Sep 25, 2024
82c8c21
Change filter to just arrays. Rename AdminLevel
FyreByrd Sep 26, 2024
379fb5e
Wrap DefaultWorkflow in a class
FyreByrd Sep 27, 2024
ed60c9f
Provide more explanatory comments
FyreByrd Sep 27, 2024
973972d
Fix workflow url
FyreByrd Sep 27, 2024
d5040a3
Include transition command in workflow admin data
FyreByrd Sep 27, 2024
23c1a6e
Remove unneeded console logs
FyreByrd Sep 27, 2024
3e63b68
Update source/SIL.AppBuilder.Portal/common/workflow/index.ts
7dev7urandom Sep 30, 2024
25fc2f1
Rename WorkflowAdminLevel to RequiredAdminLevel
FyreByrd Sep 30, 2024
b5ad276
Change Workflow construction methods
FyreByrd Sep 30, 2024
f73ef98
Fix last reference to pre-OOP workflow
FyreByrd Oct 1, 2024
a789bee
Fix workflow creation
FyreByrd Oct 7, 2024
277deb8
Fix product validation
FyreByrd Oct 7, 2024
7291b7c
Rework RequiredAdminLevel to UserRoleFeature
FyreByrd Oct 8, 2024
76ce371
Add TODO note to databaseProxy/Projects
FyreByrd Oct 3, 2024
aba8cb9
Fix installs
FyreByrd Oct 3, 2024
2580eaa
Fix Date type for databaseWrites
FyreByrd Oct 7, 2024
fc73c75
await project validation
FyreByrd Oct 7, 2024
57c0416
Add some permission checks
FyreByrd Oct 10, 2024
e9db4ca
Add allUsersByRole to UserRoles.ts
FyreByrd Oct 10, 2024
cecceab
Add Author and Reviewer check to Workflow
FyreByrd Oct 10, 2024
a6f8ba6
Convert some Workflow methods to static
FyreByrd Oct 11, 2024
ba1b621
Remove BuildEnv type from Workflow
FyreByrd Oct 16, 2024
0e39939
Convert task and jump to use superforms
FyreByrd Oct 16, 2024
c6e68be
Write WorkflowType to ProductTransitions
FyreByrd Oct 18, 2024
0f61f7b
Update in accordance with auto date
FyreByrd Oct 25, 2024
431c07c
Fix text visibility on workflow admin menu
FyreByrd Oct 25, 2024
f0f7f9a
Rename UserRoleFeatures to WorkflowAdminRequirements
FyreByrd Oct 25, 2024
1f1bf40
Move workflow instance administration to subfolder of admin/settings
FyreByrd Oct 25, 2024
53f414e
Fix remainder of switch to WorkflowAdminFeatures
FyreByrd Oct 25, 2024
b38c3a7
Fix assignment of hasAuthors and hasReviewers
FyreByrd Oct 25, 2024
5375425
Prettier formatting
FyreByrd Oct 25, 2024
39d9526
ignore product-id from snapshot
FyreByrd Oct 28, 2024
d86d044
Don't write productId, authors, or reviewers to snapshot
FyreByrd Oct 28, 2024
4ac6007
Add i18n to individual tasks page
FyreByrd Oct 28, 2024
bc3b077
Convert from strings to enum for state names
FyreByrd Oct 28, 2024
5bc28bf
Create function to compress the always block for jumping
FyreByrd Oct 28, 2024
6fa3476
Add breadcrumbs to top of task page
FyreByrd Oct 28, 2024
ba7d5b0
Fix productTransitions generation
FyreByrd Oct 28, 2024
e91b0f6
Add Start to StateName enum
FyreByrd Oct 28, 2024
834f8bb
Create and use WorkflowAction enum
FyreByrd Oct 28, 2024
e327ae2
Rename StateName to WorkflowState
FyreByrd Oct 28, 2024
cad2ca9
StaticWorkflowInput type without dynamic db fields
FyreByrd Oct 28, 2024
6aaf12e
Better solution that StaticWorkflowInput
FyreByrd Oct 28, 2024
6d3a9f5
Add my tasks to breadcrumbs
FyreByrd Oct 29, 2024
a8af76c
Rename Default to Startup
FyreByrd Oct 29, 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
164 changes: 164 additions & 0 deletions source/SIL.AppBuilder.Portal/common/databaseProxy/Products.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,79 @@
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 (
!(await 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 (!(await 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
return prisma.$transaction([
prisma.workflowInstances.delete({
where: {
ProductId: productId
}
}),
prisma.userTasks.deleteMany({
where: {
ProductId: productId
Expand All @@ -14,5 +85,98 @@ function deleteProduct(productId: string) {
}
})
]);
// TODO: delete from BuildEngine
}
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, if the StoreLanguage is defined
StoreLanguages: storeLanguageId === undefined || storeLanguageId === null ?
undefined : {
where: {
Id: storeLanguageId
},
select: {
Id: true
}
}
}
}
}
}
}
},
// 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
(storeLanguageId ?? project.Organization.OrganizationStores[0].Store.StoreType.StoreLanguages.length > 0) &&
// 5. The product type is allowed by the organization
project.Organization.OrganizationProductDefinitions.length > 0
);
}
7 changes: 4 additions & 3 deletions source/SIL.AppBuilder.Portal/common/databaseProxy/Projects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ import type { RequirePrimitive } from './utility.js';

export async function create(
projectData: RequirePrimitive<Prisma.ProjectsUncheckedCreateInput>
): Promise<boolean> {
if (!validateProjectBase(projectData.OrganizationId, projectData.GroupId, projectData.OwnerId))
): Promise<boolean | number> {
if (!(await validateProjectBase(projectData.OrganizationId, projectData.GroupId, projectData.OwnerId)))
return false;

// No additional verification steps
Expand Down Expand Up @@ -49,7 +49,7 @@ export async function update(
const orgId = projectData.OrganizationId ?? existing!.OrganizationId;
const groupId = projectData.GroupId ?? existing!.GroupId;
const ownerId = projectData.OwnerId ?? existing!.OwnerId;
if (!validateProjectBase(orgId, groupId, ownerId)) return false;
if (!(await validateProjectBase(orgId, groupId, ownerId))) return false;

// No additional verification steps

Expand All @@ -61,6 +61,7 @@ export async function update(
data: projectData
});
// If the owner has changed, we need to reassign all the user tasks related to this project
// TODO: But we don't need to change *every* user task, just the tasks associated with the owner.
if (ownerId && ownerId !== existing?.OwnerId) {
scriptoriaQueue.add(ScriptoriaJobType.ReassignUserTasks, {
type: ScriptoriaJobType.ReassignUserTasks,
Expand Down
41 changes: 41 additions & 0 deletions source/SIL.AppBuilder.Portal/common/databaseProxy/UserRoles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,44 @@ export async function setUserRolesForOrganization(
})
]);
}

export async function allUsersByRole(projectId: number) {
const project = await prisma.projects.findUnique({
where: {
Id: projectId
},
select: {
Organization: {
select: {
UserRoles: {
where: {
RoleId: RoleId.OrgAdmin
},
select: {
UserId: true
}
}
}
},
OwnerId: true,
Authors: {
select: {
UserId: true
}
}
}
});

const map = new Map<RoleId, number[]>();

map.set(
RoleId.OrgAdmin,
project.Organization.UserRoles.map((u) => u.UserId)
);
map.set(RoleId.AppBuilder, [project.OwnerId]);
map.set(
RoleId.Author,
project.Authors.map((a) => a.UserId)
);
return map;
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import prisma from '../prisma.js';

export type RequirePrimitive<T> = {
[K in keyof T]: Extract<T[K], string | number | boolean>;
[K in keyof T]: Extract<T[K], string | number | boolean | Date>;
};

export async function getOrCreateUser(profile: {
Expand Down
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';
14 changes: 13 additions & 1 deletion source/SIL.AppBuilder.Portal/common/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 4 additions & 2 deletions 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 All @@ -14,6 +15,7 @@
"bullmq": "^5.12.2"
},
"devDependencies": {
"prisma": "^5.18.0"
"prisma": "^5.18.0",
"xstate": "^5.18.2"
}
}
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
6 changes: 6 additions & 0 deletions source/SIL.AppBuilder.Portal/common/public/prisma.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,9 @@ export enum ProductTransitionType {
CancelWorkflow,
ProjectAccess
}

export enum WorkflowType {
Startup = 1,
Rebuild,
Republish
}
Loading