Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { DialogService } from 'primeng/dynamicdialog';
import { AbstractUIBlockComponent } from '../models/abstract-ui-block.component';
import { RequestDocumentBlockDialog } from '../request-document-block/dialog/request-document-block-dialog.component';
import { SchemaRulesService } from 'src/app/services/schema-rules.service';
import { SchemaService } from 'src/app/services/schema.service';
import { prepareVcData } from 'src/app/modules/common/models/prepare-vc-data';
import { PolicyTestAutomationService } from '../../policy-test-automation/policy-test-automation.service';

Expand Down Expand Up @@ -94,12 +95,30 @@ export class RequestDocumentBlockAddonComponent
private dialogService: DialogService,
private router: Router,
private changeDetectorRef: ChangeDetectorRef,
private policyTest: PolicyTestAutomationService
private policyTest: PolicyTestAutomationService,
private schemaService: SchemaService,
) {
super(policyEngineService, profile, wsService);
this.dataForm = this.fb.group({});
}

protected override _onSuccess(data: any) {
// Resolve a schema reference (id, no document) to the full schema before setData.
const schemaRef = data?.schema;
if (schemaRef && !schemaRef.document && schemaRef.id) {
this.loading = true;
this.schemaService.resolveSchemaById(schemaRef.id).subscribe({
next: (full) => {
data.schema = full;
super._onSuccess(data);
},
error: () => super._onSuccess(data)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same issue as the non-addon block: on error this falls back to the reference schema (no document) and renders an empty form. Use error: (e) => this._onError(e) so the failure goes through the normal error/loading path.

});
return;
}
super._onSuccess(data);
}

ngOnInit(): void {
this.init();
(window as any).__requestLast = this;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { AbstractUIBlockComponent } from '../models/abstract-ui-block.component'
import { PolicyHelper } from 'src/app/services/policy-helper.service';
import { RequestDocumentBlockDialog } from './dialog/request-document-block-dialog.component';
import { SchemaRulesService } from 'src/app/services/schema-rules.service';
import { SchemaService } from 'src/app/services/schema.service';
import { audit, finalize, takeUntil } from 'rxjs/operators';
import { interval, Subject, Subscription, firstValueFrom } from 'rxjs';
import { prepareVcData } from 'src/app/modules/common/models/prepare-vc-data';
Expand Down Expand Up @@ -151,6 +152,7 @@ export class RequestDocumentBlockComponent
private tablePersist: TablePersistenceService,
private ipfsService: IPFSService,
private policyTest: PolicyTestAutomationService,
private schemaService: SchemaService,
) {
super(policyEngineService, profile, wsService);
this.dataForm = this.fb.group({});
Expand Down Expand Up @@ -209,6 +211,24 @@ export class RequestDocumentBlockComponent
}

protected override _onSuccess(data: any) {
// A schema reference (id, no document) is resolved to the full schema once
// before the render flow; a full schema is used as-is.
const schemaRef = data?.schema;
if (schemaRef && !schemaRef.document && schemaRef.id) {
this.loading = true;
this.schemaService.resolveSchemaById(schemaRef.id).subscribe({
next: (full) => {
data.schema = full;
this._applyBlockData(data);
},
error: () => this._applyBlockData(data)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On resolution failure this renders with the bare reference schema. new Schema(reference) has no document, so parseDocument() is skipped (interfaces/src/models/schema.ts:234) and fields stays empty so the user gets a silently blank form. Route the failure through the existing error handler instead:

Suggested change
error: () => this._applyBlockData(data)
error: (e) => this._onError(e)

});
return;
}
this._applyBlockData(data);
}

private _applyBlockData(data: any) {
this.setData(data);
if (this.type === 'dialog') {
setTimeout(() => {
Expand Down
63 changes: 63 additions & 0 deletions frontend/src/app/services/schema.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { TestBed } from '@angular/core/testing';
import { SchemaService } from './schema.service';
import { AuthService } from './auth.service';
import { API_BASE_URL } from './api';

const SINGLE_URL = `${API_BASE_URL}/schema`;

describe('SchemaService single-schema resolution', () => {
let service: SchemaService;
let httpMock: HttpTestingController;

beforeEach(() => {
TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
providers: [
SchemaService,
{ provide: AuthService, useValue: {} },
],
});
service = TestBed.inject(SchemaService);
httpMock = TestBed.inject(HttpTestingController);
});

afterEach(() => httpMock.verify());

it('getSchemaById GETs /schema/:id', () => {
service.getSchemaById('s1').subscribe();
const req = httpMock.expectOne(`${SINGLE_URL}/s1`);
expect(req.request.method).toBe('GET');
req.flush({ id: 's1' });
});

it('resolveSchemaById de-duplicates concurrent subscribers into a single request', () => {
const results: any[] = [];
service.resolveSchemaById('s1').subscribe((v) => results.push(v));
service.resolveSchemaById('s1').subscribe((v) => results.push(v));
const req = httpMock.expectOne(`${SINGLE_URL}/s1`);
req.flush({ id: 's1', document: { x: 1 } });
expect(results.length).toBe(2);
expect(results[0]).toEqual(results[1]);
});

it('serves a cached result without a second request', () => {
service.resolveSchemaById('s1').subscribe();
httpMock.expectOne(`${SINGLE_URL}/s1`).flush({ id: 's1' });
let got: any;
service.resolveSchemaById('s1').subscribe((v) => (got = v));
httpMock.expectNone(`${SINGLE_URL}/s1`);
expect(got).toEqual({ id: 's1' } as any);
});

it('evicts a failed fetch so a later call retries', () => {
let err: any;
service.resolveSchemaById('s1').subscribe({ error: (e) => (err = e) });
httpMock.expectOne(`${SINGLE_URL}/s1`).flush('boom', { status: 500, statusText: 'Server Error' });
expect(err).toBeTruthy();
service.resolveSchemaById('s1').subscribe();
const retry = httpMock.expectOne(`${SINGLE_URL}/s1`);
expect(retry.request.method).toBe('GET');
retry.flush({ id: 's1' });
});
});
26 changes: 25 additions & 1 deletion frontend/src/app/services/schema.service.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { HttpClient, HttpParams, HttpResponse } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { ISchema, ISchemaDeletionPreview, SchemaCategory, SchemaEntity, SchemaNode } from '@guardian/interfaces';
import { Observable } from 'rxjs';
import { Observable, catchError, shareReplay, throwError } from 'rxjs';
import { API_BASE_URL } from './api';
import { AuthService } from './auth.service';
import { headersV2 } from '../constants';
Expand All @@ -16,12 +16,36 @@ export class SchemaService {
private readonly url: string = `${API_BASE_URL}/schemas`;
private readonly singleSchemaUrl: string = `${API_BASE_URL}/schema`;

// Resolved single-schema requests keyed by id, so a block that receives a
// schema reference resolves the full schema once and shares it.
private readonly schemaByIdCache = new Map<string, Observable<ISchema>>();

constructor(
private http: HttpClient,
private auth: AuthService,
) {
}

public getSchemaById(id: string): Observable<ISchema> {
return this.http.get<ISchema>(`${this.singleSchemaUrl}/${id}`);
}

// Resolve a full schema by id, cached and de-duplicated; evict on error to retry.
public resolveSchemaById(id: string): Observable<ISchema> {
let cached = this.schemaByIdCache.get(id);
if (!cached) {
cached = this.getSchemaById(id).pipe(
catchError((error) => {
this.schemaByIdCache.delete(id);
return throwError(() => error);
}),
shareReplay(1)
);
this.schemaByIdCache.set(id, cached);
}
return cached;
}

public static getOptions(filters?: {
pageIndex?: number,
pageSize?: number | string,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,14 @@ export class RequestVcDocumentBlockAddon {
user.location === LocationType.REMOTE
),
...options,
schema: { ...this._schema, fields: [], conditions: [] },
// Lightweight schema reference; the client resolves the full schema by id.
schema: {
id: this._schema.id,
iri: this._schema.iri,
uuid: this._schema.uuid,
name: this._schema.name,
version: this._schema.version,
},
};
return data;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,14 @@ export class RequestVcDocumentBlock {
ref.actionType === LocationType.REMOTE &&
user.location === LocationType.REMOTE
),
schema: { ...this._schema, fields: [], conditions: [] },
// Lightweight schema reference; the client resolves the full schema by id.
schema: {
id: this._schema.id,
iri: this._schema.iri,
uuid: this._schema.uuid,
name: this._schema.name,
version: this._schema.version,
},
presetSchema: options.presetSchema,
presetFields: options.presetFields,
editType: options.editType || 'new',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { assert } from 'chai';
import { RequestVcDocumentBlock } from '../../../dist/policy-engine/blocks/request-vc-document-block.js';
import { RequestVcDocumentBlockAddon } from '../../../dist/policy-engine/blocks/request-vc-document-block-addon.js';
import { PolicyComponentsUtils } from '../../../dist/policy-engine/policy-components-utils.js';

// getData must ship only a lightweight schema reference, not the heavy
// document + context (the full schema, resolved client-side by id).

const origGetRef = PolicyComponentsUtils.GetBlockRef;
const origGetOpts = PolicyComponentsUtils.GetBlockUniqueOptionsObject;

const fullSchema = () => ({
id: 'schema-db-id',
iri: '#SchemaIri',
uuid: 'schema-uuid',
name: 'Project',
version: '1.0.0',
document: { $id: '#doc', properties: { big: 'x'.repeat(1000) } },
context: { '@context': { big: 'y'.repeat(1000) } },
fields: [{ name: 'f' }],
conditions: [{ if: 1 }],
});

function assertReference(schema) {
assert.equal(schema.id, 'schema-db-id');
assert.equal(schema.iri, '#SchemaIri');
assert.equal(schema.uuid, 'schema-uuid');
assert.equal(schema.name, 'Project');
assert.equal(schema.version, '1.0.0');
assert.notProperty(schema, 'document');
assert.notProperty(schema, 'context');
assert.notProperty(schema, 'fields');
assert.notProperty(schema, 'conditions');
}

after(() => {
PolicyComponentsUtils.GetBlockRef = origGetRef;
PolicyComponentsUtils.GetBlockUniqueOptionsObject = origGetOpts;
});

describe('RequestVcDocumentBlock runtime — getData schema reference', () => {
it('emits only light schema identifiers, not document/context', async () => {
const block = Object.create(RequestVcDocumentBlock.prototype);
block._schema = fullSchema();
block._accessMap = new Map();
block.state = {};
const ref = {
uuid: 'blk',
blockType: 'requestVcDocumentBlock',
actionType: 'local',
async getSources() { return []; },
};
PolicyComponentsUtils.GetBlockRef = () => ref;
PolicyComponentsUtils.GetBlockUniqueOptionsObject = () => ({});

const data = await block.getData({ id: 'u1', location: 'local' });
assertReference(data.schema);
});
});

describe('RequestVcDocumentBlockAddon runtime — getData schema reference', () => {
it('emits only light schema identifiers, not document/context', async () => {
const block = Object.create(RequestVcDocumentBlockAddon.prototype);
block._schema = fullSchema();
block._accessMap = new Map();
const ref = {
uuid: 'blk',
blockType: 'requestVcDocumentBlockAddon',
actionType: 'local',
async getOptions() { return {}; },
};
PolicyComponentsUtils.GetBlockRef = () => ref;

const data = await block.getData({ id: 'u1', location: 'local' });
assertReference(data.schema);
});
});
Loading