Skip to content

Commit

Permalink
Store PDF documents in db table (#371)
Browse files Browse the repository at this point in the history
* Add form_documents table and an initializeForm service, responsible for creating a form that's initialized with data from a PDF. initializeForm is not currently wired to the UI.

* Improve pattern builder docs

* Fix test and typing errors; implement localStorage version of addDocument

* Wire initializeForm to UI

* PDF attachment download working

* Fix imports

* Rebuild lock file

* Revert "Rebuild lock file"

This reverts commit 0b0f6c7.

* Remove pdf bytes from blueprint object

* Parse/validate Blueprint JSON

* Delete associated documents when a form is deleted.

* Document support in-browser

* Add tests for parseForm

* Inject a FormConfig into the forms db adapter

* Remove console.log

* Call parseStringForm() in getForm db function

* Update BrowserFormRepository for interface changes

* Fix typo
  • Loading branch information
danielnaab authored Nov 27, 2024
1 parent 47ec6d4 commit 47ea8ce
Show file tree
Hide file tree
Showing 67 changed files with 1,357 additions and 321 deletions.
2 changes: 2 additions & 0 deletions apps/spotlight/src/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {
type FormConfig,
type FormService,
createFormService,
parsePdf,
} from '@atj/forms';
import { defaultFormConfig } from '@atj/forms';
import { BrowserFormRepository } from '@atj/forms/context';
Expand Down Expand Up @@ -43,6 +44,7 @@ const createAppFormService = () => {
repository,
config: defaultFormConfig,
isUserLoggedIn: () => true,
parsePdf,
});
} else {
return createTestBrowserFormService();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@ export const getFormSession: GetFormSession = async (ctx, opts) => {
},
});
} else {
console.log('using session', result.data.data);
ctx.setState({
formSessionResponse: {
status: 'loaded',
Expand Down
1 change: 1 addition & 0 deletions packages/common/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export type Result<T, E = string> = Success<T> | Failure<E>;
export type VoidResult<E = string> = VoidSuccess | Failure<E>;

export const success = <T>(data: T): Success<T> => ({ success: true, data });
export const voidSuccess: VoidSuccess = { success: true };
export const failure = <E>(error: E): Failure<E> => ({ success: false, error });

export { en as enLocale } from './locales/en/app.js';
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
export async function up(knex) {
await knex.schema.createTable('form_documents', table => {
table.uuid('id').primary();
table.string('type').notNullable();
table.string('file_name').notNullable();
table.binary('data').notNullable();
table.string('extract').notNullable();
});
}

/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
export async function down(knex) {
await knex.schema.dropTableIfExists('form_documents');
}
16 changes: 14 additions & 2 deletions packages/database/src/clients/kysely/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type {
ColumnType,
Generated,
Insertable,
Kysely,
Expand All @@ -14,7 +15,9 @@ export interface Database<T extends Engine = Engine> {
sessions: SessionsTable<T>;
forms: FormsTable;
form_sessions: FormSessionsTable;
form_documents: FormDocumentsTable;
}
export type DatabaseClient = Kysely<Database>;

interface UsersTable {
id: string;
Expand Down Expand Up @@ -48,8 +51,6 @@ export type FormsTableSelectable = Selectable<FormsTable>;
export type FormsTableInsertable = Insertable<FormsTable>;
export type FormsTableUpdateable = Updateable<FormsTable>;

export type DatabaseClient = Kysely<Database>;

interface FormSessionsTable {
id: string;
form_id: string;
Expand All @@ -60,3 +61,14 @@ interface FormSessionsTable {
export type FormSessionsTableSelectable = Selectable<FormSessionsTable>;
export type FormSessionsTableInsertable = Insertable<FormSessionsTable>;
export type FormSessionsTableUpdateable = Updateable<FormSessionsTable>;

interface FormDocumentsTable {
id: string;
type: string;
data: ColumnType<Buffer, Buffer, Buffer>;
file_name: string;
extract: string;
}
export type FormDocumentsTableSelectable = Selectable<FormDocumentsTable>;
export type FormDocumentsTableInsertable = Insertable<FormDocumentsTable>;
export type FormDocumentsTableUpdateable = Updateable<FormDocumentsTable>;
39 changes: 24 additions & 15 deletions packages/design/src/FormManager/FormList/store.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { type StateCreator } from 'zustand';

import { BlueprintBuilder } from '@atj/forms';
import { BlueprintBuilder, uint8ArrayToBase64 } from '@atj/forms';
import { type FormManagerContext } from '../../FormManager/index.js';
import { type Result, failure } from '@atj/common';

Expand All @@ -23,18 +23,17 @@ export const createFormListSlice =
() => ({
context,
createNewFormByPDFUrl: async url => {
const data = await fetchUint8Array(`${context.baseUrl}${url}`);

const builder = new BlueprintBuilder(context.config);
builder.setFormSummary({
title: url,
description: '',
});
await builder.addDocument({
name: url,
data,
const data = await fetchAsBase64(`${context.baseUrl}${url}`);
const result = await context.formService.initializeForm({
summary: {
title: url,
description: '',
},
document: {
fileName: url,
data,
},
});
const result = await context.formService.addForm(builder.form);
if (result.success) {
return {
success: true,
Expand All @@ -51,7 +50,16 @@ export const createFormListSlice =
description: '',
});
await builder.addDocument(fileDetails);
const result = await context.formService.addForm(builder.form);
const result = await context.formService.initializeForm({
summary: {
title: fileDetails.name,
description: '',
},
document: {
fileName: fileDetails.name,
data: await uint8ArrayToBase64(fileDetails.data),
},
});
if (result.success) {
return {
success: true,
Expand All @@ -63,8 +71,9 @@ export const createFormListSlice =
},
});

const fetchUint8Array = async (url: string) => {
const fetchAsBase64 = async (url: string) => {
const response = await fetch(url);
const blob = await response.blob();
return new Uint8Array(await blob.arrayBuffer());
const data = new Uint8Array(await blob.arrayBuffer());
return uint8ArrayToBase64(data);
};
12 changes: 6 additions & 6 deletions packages/design/src/FormManager/store.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
import { createContext } from 'zustand-utils';

import { type Result, failure } from '@atj/common';
import { type FormSession, type Blueprint, BlueprintBuilder } from '@atj/forms';
import { type FormSession, type Blueprint } from '@atj/forms';

import { type FormListSlice, createFormListSlice } from './FormList/store.js';
import { type FormEditSlice, createFormEditSlice } from './FormEdit/store.js';
Expand Down Expand Up @@ -79,12 +79,12 @@ const createFormManagerSlice =
inProgress: false,
},
createNewForm: async function () {
const builder = new BlueprintBuilder(context.config);
builder.setFormSummary({
title: `My form - ${new Date().toISOString()}`,
description: '',
const result = await context.formService.initializeForm({
summary: {
title: `My form - ${new Date().toISOString()}`,
description: '',
},
});
const result = await context.formService.addForm(builder.form);
if (!result.success) {
return failure(result.error.message);
}
Expand Down
6 changes: 3 additions & 3 deletions packages/forms/src/blueprint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,11 +71,11 @@ export const createForm = (
patterns: [
{
id: 'root',
type: 'sequence',
type: 'page-set',
data: {
patterns: [],
pages: [],
},
} satisfies SequencePattern,
} satisfies PageSetPattern,
],
root: 'root',
}
Expand Down
12 changes: 11 additions & 1 deletion packages/forms/src/builder/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
removePatternFromBlueprint,
updateFormSummary,
} from '../blueprint.js';
import { addDocument } from '../documents/document.js';
import { addDocument, addParsedPdfToForm } from '../documents/document.js';
import type { FormErrors } from '../error.js';
import {
createDefaultPattern,
Expand All @@ -23,6 +23,7 @@ import {
import { type FieldsetPattern } from '../patterns/fieldset/config.js';
import { type PageSetPattern } from '../patterns/page-set/config.js';
import type { Blueprint, FormSummary } from '../types.js';
import type { ParsedPdf } from '../documents/pdf/parsing-api.js';

export class BlueprintBuilder {
bp: Blueprint;
Expand All @@ -47,6 +48,15 @@ export class BlueprintBuilder {
this.bp = updatedForm;
}

async addDocumentRef(opts: { id: string; extract: ParsedPdf }) {
const { updatedForm } = await addParsedPdfToForm(this.form, {
id: opts.id,
label: opts.extract.title,
extract: opts.extract,
});
this.bp = updatedForm;
}

addPage() {
const newPage = createDefaultPattern(this.config, 'page');
this.bp = addPageToPageSet(this.form, newPage);
Expand Down
153 changes: 153 additions & 0 deletions packages/forms/src/builder/parse-form.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
import { describe, it, expect } from 'vitest';
import { failure, success } from '@atj/common';

import { parseForm, parseFormString } from './parse-form';
import { defaultFormConfig, type InputPattern } from '../patterns';
import type { Blueprint } from '../types';

describe('parseForm', () => {
it('should return success when form data is valid', () => {
const formData: Blueprint = {
summary: {
title: 'Test Title',
description: 'Test Description',
},
root: 'rootValue',
patterns: {
validPattern: {
type: 'input',
id: 'validPattern',
data: {
label: 'label',
required: true,
maxLength: 100,
},
} satisfies InputPattern,
},
outputs: [
{
id: 'output1',
path: 'path/to/output',
fields: {
field1: {
type: 'TextField',
name: 'name',
label: 'label',
value: 'value',
required: true,
},
},
formFields: {
formField1: 'formValue1',
},
},
],
};

const result = parseForm(defaultFormConfig, formData);
expect(result).toEqual(success(formData));
});

it('should return failure when form data is invalid', () => {
const formData = {
summary: {
title: 'Test Title',
description: 'Test Description',
},
root: 'rootValue',
patterns: {
invalidPattern: {
type: 'invalidPattern',
data: {},
},
},
outputs: [
{
id: 'output1',
path: 'path/to/output',
fields: {
field1: 'value1',
},
formFields: {
formField1: 'formValue1',
},
},
],
};

const result = parseForm(defaultFormConfig, formData);
expect(result.success).toEqual(false);
});
});

describe('parseFormString', () => {
it('should return success when JSON string is valid', () => {
const jsonString = JSON.stringify({
summary: {
title: 'Test Title',
description: 'Test Description',
},
root: 'rootValue',
patterns: {
validPattern: {
type: 'input',
id: 'validPattern',
data: {
label: 'label',
required: true,
maxLength: 100,
initial: '',
},
} satisfies InputPattern,
},
outputs: [],
} satisfies Blueprint);

const result = parseFormString(defaultFormConfig, jsonString);
expect(result).toEqual(success(JSON.parse(jsonString)));
});

it('should return failure when JSON string is invalid', () => {
const jsonString = JSON.stringify({
summary: {
title: 'Test Title',
description: 'Test Description',
},
root: 'rootValue',
patterns: {
invalidPattern: {
type: 'invalidPattern',
data: {},
},
},
outputs: [
{
id: 'output1',
path: 'path/to/output',
fields: {
field1: 'value1',
},
formFields: {
formField1: 'formValue1',
},
},
],
});

const result = parseFormString(defaultFormConfig, jsonString);
expect(result).toEqual({
success: false,
error:
'[\n' +
' {\n' +
' "code": "custom",\n' +
' "message": "Invalid pattern",\n' +
' "path": [\n' +
' "patterns",\n' +
' "invalidPattern"\n' +
' ]\n' +
' }\n' +
']',
});
});
});
Loading

0 comments on commit 47ea8ce

Please sign in to comment.