@arcforgelabs/flow 2026.10.33 → 2026.10.34

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/README.md +6 -0
  2. package/npm-shrinkwrap.json +2 -2
  3. package/package.json +1 -1
  4. package/server/api/custom-fields.js +114 -0
  5. package/server/api/invoicing.js +2 -1
  6. package/server/api/work-moves.js +80 -0
  7. package/server/app.js +12 -0
  8. package/server/core/custom-fields.js +300 -0
  9. package/server/core/job-board.js +2 -0
  10. package/server/core/tracing.js +64 -0
  11. package/server/db/migrations/0018_custom_fields.sql +78 -0
  12. package/server/db/migrations/meta/0018_snapshot.json +4289 -0
  13. package/server/db/migrations/meta/_journal.json +7 -0
  14. package/server/db/schema.js +59 -0
  15. package/server/index.js +6 -1
  16. package/server/integrations/port.js +7 -1
  17. package/server/integrations/seazona/index.js +47 -1
  18. package/server/integrations/seazona/migrations/0007_stage_moves.sql +18 -0
  19. package/server/integrations/seazona/migrations/meta/0007_snapshot.json +1519 -0
  20. package/server/integrations/seazona/migrations/meta/_journal.json +7 -0
  21. package/server/integrations/seazona/schema.js +26 -0
  22. package/server/integrations/seazona/settings.js +14 -0
  23. package/server/integrations/seazona/stage-moves.js +135 -0
  24. package/server/integrations/seazona/tables.js +2 -0
  25. package/server/integrations/sentry/index.js +61 -10
  26. package/server/ui/app-page.js +1 -1
  27. package/server/ui/app.js +40 -2
  28. package/server/ui/custom-fields.js +190 -0
  29. package/server/ui/flow.css +48 -9
  30. package/server/ui/invoicing.js +83 -5
  31. package/server/ui/jobs.js +3 -1
  32. package/server/ui/monitoring.js +44 -2
  33. package/server/ui/routes.js +1 -1
@@ -0,0 +1,64 @@
1
+ import { routePattern } from '../ui/monitoring.js';
2
+
3
+ // Performance traces for the installation's own tracing service
4
+ // (docs/SENTRY.md, "Traces"). The first integration that is on and offers the
5
+ // `traceSpan` and `traceRequest` capabilities gets them. With none on, each
6
+ // function here runs the work as it is and adds nothing.
7
+ //
8
+ // `tracers()` answers the adapters to trace with; it is read at each call,
9
+ // so an integration turned on or off later is followed.
10
+ export function createTracing({ tracers = () => [] } = {}) {
11
+ const first = () => tracers()[0]?.capabilities ?? null;
12
+ return {
13
+ // Runs `work` inside a span named `name`, of kind `op`.
14
+ span(name, op, work, attributes = {}) {
15
+ const tracer = first();
16
+ return tracer ? tracer.traceSpan({ name, op, attributes }, work) : work();
17
+ },
18
+ // Express middleware: names each request's trace by its route pattern
19
+ // ("GET /api/invoicing/invoices/:id"), never by its URL, so the same
20
+ // click on another invoice is the same name.
21
+ requests() {
22
+ return (req, res, next) => {
23
+ const rename = first()?.traceRequest() ?? null;
24
+ if (rename) res.once('finish', () => rename(requestName(req)));
25
+ next();
26
+ };
27
+ },
28
+ };
29
+ }
30
+
31
+ // The route Express matched, else the path with its ids as ":id".
32
+ export function requestName(req) {
33
+ const route = typeof req.route?.path === 'string' ? `${req.baseUrl ?? ''}${req.route.path}` : null;
34
+ return `${req.method} ${route ?? routePattern(req.originalUrl ?? req.url)}`;
35
+ }
36
+
37
+ // A span for each database query the pool runs. The span's name is the SQL
38
+ // with its values left out: Flow passes values as parameters, and any quoted
39
+ // text or number in the SQL itself becomes "?".
40
+ export function traceQueries(pool, tracing) {
41
+ const wrap = (target) => {
42
+ const query = target.query.bind(target);
43
+ target.query = (...args) => {
44
+ // Callback calls are pg's own (the pool's query runs through a
45
+ // client's): the promise call around them already has its span.
46
+ if (typeof args.at(-1) === 'function') return query(...args);
47
+ return tracing.span(sqlName(args[0]), 'db', () => query(...args), { 'db.system': 'postgresql', 'db.system.name': 'postgresql' });
48
+ };
49
+ };
50
+ wrap(pool);
51
+ // A client a transaction holds (pool.connect()) queries directly.
52
+ pool.on('connect', wrap);
53
+ return pool;
54
+ }
55
+
56
+ export function sqlName(statement) {
57
+ const text = typeof statement === 'string' ? statement : String(statement?.text ?? '');
58
+ return text
59
+ .replace(/'(?:[^']|'')*'/g, '?')
60
+ .replace(/(?<![\w$])\d+(\.\d+)?\b/g, '?')
61
+ .replace(/\s+/g, ' ')
62
+ .trim()
63
+ .slice(0, 300) || 'query';
64
+ }
@@ -0,0 +1,78 @@
1
+ CREATE TABLE IF NOT EXISTS "job_field_events" (
2
+ "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
3
+ "organisation_id" uuid NOT NULL,
4
+ "field_id" uuid NOT NULL,
5
+ "source_system" text NOT NULL,
6
+ "source_job_ref" text NOT NULL,
7
+ "from_value" jsonb,
8
+ "to_value" jsonb,
9
+ "actor" uuid,
10
+ "created_at" timestamp with time zone DEFAULT now() NOT NULL
11
+ );
12
+ --> statement-breakpoint
13
+ CREATE TABLE IF NOT EXISTS "job_field_values" (
14
+ "organisation_id" uuid NOT NULL,
15
+ "field_id" uuid NOT NULL,
16
+ "source_system" text NOT NULL,
17
+ "source_job_ref" text NOT NULL,
18
+ "value" jsonb NOT NULL,
19
+ "updated_by" uuid,
20
+ "updated_at" timestamp with time zone DEFAULT now() NOT NULL,
21
+ CONSTRAINT "job_field_values_organisation_id_field_id_source_system_source_job_ref_pk" PRIMARY KEY("organisation_id","field_id","source_system","source_job_ref")
22
+ );
23
+ --> statement-breakpoint
24
+ CREATE TABLE IF NOT EXISTS "job_fields" (
25
+ "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
26
+ "organisation_id" uuid NOT NULL,
27
+ "name" text NOT NULL,
28
+ "type" text NOT NULL,
29
+ "options" jsonb DEFAULT '[]'::jsonb NOT NULL,
30
+ "position" integer NOT NULL,
31
+ "archived_at" timestamp with time zone,
32
+ "created_by" uuid,
33
+ "updated_by" uuid,
34
+ "created_at" timestamp with time zone DEFAULT now() NOT NULL,
35
+ "updated_at" timestamp with time zone DEFAULT now() NOT NULL,
36
+ CONSTRAINT "job_fields_org_id" UNIQUE("organisation_id","id"),
37
+ CONSTRAINT "job_fields_type_valid" CHECK ("job_fields"."type" in ('dropdown', 'person', 'checkbox', 'date', 'number', 'text')),
38
+ CONSTRAINT "job_fields_name_valid" CHECK (length("job_fields"."name") between 1 and 60)
39
+ );
40
+ --> statement-breakpoint
41
+ DO $flow_guard$ BEGIN
42
+ ALTER TABLE "job_field_events" ADD CONSTRAINT "job_field_events_organisation_id_organisations_id_fk" FOREIGN KEY ("organisation_id") REFERENCES "public"."organisations"("id") ON DELETE no action ON UPDATE no action;
43
+ EXCEPTION WHEN duplicate_object OR duplicate_table OR invalid_table_definition THEN NULL;
44
+ END $flow_guard$;--> statement-breakpoint
45
+ DO $flow_guard$ BEGIN
46
+ ALTER TABLE "job_field_events" ADD CONSTRAINT "job_field_events_actor_people_id_fk" FOREIGN KEY ("actor") REFERENCES "public"."people"("id") ON DELETE set null ON UPDATE no action;
47
+ EXCEPTION WHEN duplicate_object OR duplicate_table OR invalid_table_definition THEN NULL;
48
+ END $flow_guard$;--> statement-breakpoint
49
+ DO $flow_guard$ BEGIN
50
+ ALTER TABLE "job_field_events" ADD CONSTRAINT "job_field_events_field_same_org" FOREIGN KEY ("organisation_id","field_id") REFERENCES "public"."job_fields"("organisation_id","id") ON DELETE cascade ON UPDATE no action;
51
+ EXCEPTION WHEN duplicate_object OR duplicate_table OR invalid_table_definition THEN NULL;
52
+ END $flow_guard$;--> statement-breakpoint
53
+ DO $flow_guard$ BEGIN
54
+ ALTER TABLE "job_field_values" ADD CONSTRAINT "job_field_values_organisation_id_organisations_id_fk" FOREIGN KEY ("organisation_id") REFERENCES "public"."organisations"("id") ON DELETE no action ON UPDATE no action;
55
+ EXCEPTION WHEN duplicate_object OR duplicate_table OR invalid_table_definition THEN NULL;
56
+ END $flow_guard$;--> statement-breakpoint
57
+ DO $flow_guard$ BEGIN
58
+ ALTER TABLE "job_field_values" ADD CONSTRAINT "job_field_values_updated_by_people_id_fk" FOREIGN KEY ("updated_by") REFERENCES "public"."people"("id") ON DELETE set null ON UPDATE no action;
59
+ EXCEPTION WHEN duplicate_object OR duplicate_table OR invalid_table_definition THEN NULL;
60
+ END $flow_guard$;--> statement-breakpoint
61
+ DO $flow_guard$ BEGIN
62
+ ALTER TABLE "job_field_values" ADD CONSTRAINT "job_field_values_field_same_org" FOREIGN KEY ("organisation_id","field_id") REFERENCES "public"."job_fields"("organisation_id","id") ON DELETE cascade ON UPDATE no action;
63
+ EXCEPTION WHEN duplicate_object OR duplicate_table OR invalid_table_definition THEN NULL;
64
+ END $flow_guard$;--> statement-breakpoint
65
+ DO $flow_guard$ BEGIN
66
+ ALTER TABLE "job_fields" ADD CONSTRAINT "job_fields_organisation_id_organisations_id_fk" FOREIGN KEY ("organisation_id") REFERENCES "public"."organisations"("id") ON DELETE no action ON UPDATE no action;
67
+ EXCEPTION WHEN duplicate_object OR duplicate_table OR invalid_table_definition THEN NULL;
68
+ END $flow_guard$;--> statement-breakpoint
69
+ DO $flow_guard$ BEGIN
70
+ ALTER TABLE "job_fields" ADD CONSTRAINT "job_fields_created_by_people_id_fk" FOREIGN KEY ("created_by") REFERENCES "public"."people"("id") ON DELETE set null ON UPDATE no action;
71
+ EXCEPTION WHEN duplicate_object OR duplicate_table OR invalid_table_definition THEN NULL;
72
+ END $flow_guard$;--> statement-breakpoint
73
+ DO $flow_guard$ BEGIN
74
+ ALTER TABLE "job_fields" ADD CONSTRAINT "job_fields_updated_by_people_id_fk" FOREIGN KEY ("updated_by") REFERENCES "public"."people"("id") ON DELETE set null ON UPDATE no action;
75
+ EXCEPTION WHEN duplicate_object OR duplicate_table OR invalid_table_definition THEN NULL;
76
+ END $flow_guard$;--> statement-breakpoint
77
+ CREATE INDEX IF NOT EXISTS "job_field_events_job" ON "job_field_events" USING btree ("organisation_id","source_system","source_job_ref","created_at");--> statement-breakpoint
78
+ CREATE INDEX IF NOT EXISTS "job_field_values_job" ON "job_field_values" USING btree ("organisation_id","source_system","source_job_ref");