Skip to content
Merged
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
1 change: 1 addition & 0 deletions labs/playground1/profile.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@
persistent-path: ./test-data/moma.db
log-queries: true
log-parameters: true
allow: '*'
20 changes: 13 additions & 7 deletions packages/build/src/lib/schema-parser/middleware/checkProfile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,25 @@ export class CheckProfile extends SchemaParserMiddleware {
}

public async handle(schemas: RawAPISchema, next: () => Promise<void>) {
if (!schemas.profiles && schemas.profile) {
schemas.profiles = [schemas.profile];
}

await next();
const transformedSchemas = schemas as APISchema;
if (!transformedSchemas.profile)
if (!transformedSchemas.profiles)
throw new Error(
`The profile of schema ${transformedSchemas.urlPath} is not defined`
);

try {
this.dataSourceFactory(transformedSchemas.profile);
} catch (e: any) {
throw new Error(
`The profile of schema ${transformedSchemas.urlPath} is invalid: ${e?.message}`
);
for (const profile of transformedSchemas.profiles) {
try {
this.dataSourceFactory(profile);
} catch (e: any) {
throw new Error(
`The profile ${profile} of schema ${transformedSchemas.urlPath} is invalid: ${e?.message}`
);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export interface RawAPISchema
request?: DeepPartial<RawRequestParameter[]>;
response?: DeepPartial<RawResponseProperty[]>;
metadata?: Record<string, any>;
profile?: string;
}

@injectable()
Expand Down
1 change: 1 addition & 0 deletions packages/build/test/builder/profile.yaml
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
- name: test
type: pg
allow: '*'
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ it('Should throw error when the profile is invalid', async () => {
await expect(
checkProfile.handle(schema, async () => Promise.resolve())
).rejects.toThrow(
`The profile of schema /user is invalid: profile not found`
`The profile profile1 of schema /user is invalid: profile not found`
);
});

Expand Down
8 changes: 8 additions & 0 deletions packages/core/src/containers/modules/executor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { TYPES } from '../types';
import {
DataSource,
EXTENSION_IDENTIFIER_METADATA_KEY,
EXTENSION_TYPE_METADATA_KEY,
} from '../../models/extensions';
import { Profile } from '../../models/profile';
import 'reflect-metadata';
Expand Down Expand Up @@ -38,6 +39,13 @@ export const executorModule = (profiles: Map<string, Profile>) =>
// See https://github.com/inversify/InversifyJS/blob/master/src/syntax/constraint_helpers.ts#L32
const constructor = request.parentRequest?.bindings[0]
.implementationType as ClassType<DataSource>;
const parentType = Reflect.getMetadata(
EXTENSION_TYPE_METADATA_KEY,
constructor
);
// Always fulfill the request while the injector isn't a data source
if (parentType !== TYPES.Extension_DataSource) return true;

const dataSourceId = Reflect.getMetadata(
EXTENSION_IDENTIFIER_METADATA_KEY,
constructor
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export class NunjucksExecutionMetadata {
context: {
params: this.parameters,
user: this.userInfo,
profile: this.profileName,
},
[ReservedContextKeys.CurrentProfileName]: this.profileName,
};
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/models/artifact.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ export interface APISchema {
// If not set pagination, then API request not provide the field to do it
pagination?: PaginationSchema;
sample?: Sample;
profile: string;
profiles: Array<string>;
}

export interface BuiltArtifact {
Expand Down
15 changes: 15 additions & 0 deletions packages/core/src/models/profile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,19 @@
// host: example.com
// username: vulcan
// password: xxxx
// allow:
// - name: admin

export type ProfileAllowConstraints =
// allow: *
| string
// allow:
// name: admin
| Record<string, any>
// allow:
// - admin
// - name: admin
| Array<string | Record<string, any>>;

export interface Profile<C = Record<string, any>> {
/** This unique name of this profile */
Expand All @@ -12,4 +25,6 @@ export interface Profile<C = Record<string, any>> {
type: string;
/** Connection info, which depends on drivers */
connection?: C;
/** What users have access to this profile */
allow: ProfileAllowConstraints;
}
70 changes: 57 additions & 13 deletions packages/core/test/containers/executor.spec.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,41 @@
import {
DataResult,
DataSource,
executorModule,
Profile,
TYPES,
VulcanExtensionId,
} from '@vulcan-sql/core';
import { Container, injectable, interfaces, multiInject } from 'inversify';

@injectable()
@VulcanExtensionId('ds1')
class DataSource1 {
constructor(@multiInject(TYPES.Profile) public profiles: Profile[]) {}
class DataSource1 extends DataSource {
@multiInject(TYPES.Profile) public injectedProfiles!: Profile[];
public execute(): Promise<DataResult> {
throw new Error('Method not implemented.');
}

public prepare(): Promise<string> {
throw new Error('Method not implemented.');
}
}

@injectable()
@VulcanExtensionId('ds2')
class DataSource2 {
constructor(@multiInject(TYPES.Profile) public profiles: Profile[]) {}
class DataSource2 extends DataSource {
@multiInject(TYPES.Profile) public injectedProfiles!: Profile[];
public execute(): Promise<DataResult> {
throw new Error('Method not implemented.');
}

public prepare(): Promise<string> {
throw new Error('Method not implemented.');
}
}

@VulcanExtensionId('ds3')
@injectable()
class DataSource3 {
@multiInject(TYPES.Profile) public injectedProfiles!: Profile[];
}

it('Executor module should bind correct profiles to data sources and create a factory which return proper data sources', async () => {
Expand All @@ -30,9 +50,11 @@ it('Executor module should bind correct profiles to data sources and create a fa
.to(DataSource2)
.whenTargetNamed('ds2');
const profiles = new Map<string, Profile>();
profiles.set('p1', { name: 'p1', type: 'ds1' });
profiles.set('p2', { name: 'p2', type: 'ds1' });
profiles.set('p3', { name: 'p3', type: 'ds2' });
profiles.set('p1', { name: 'p1', type: 'ds1', allow: '*' });
profiles.set('p2', { name: 'p2', type: 'ds1', allow: '*' });
profiles.set('p3', { name: 'p3', type: 'ds2', allow: '*' });
container.bind(TYPES.ExtensionConfig).toConstantValue({});
container.bind(TYPES.ExtensionName).toConstantValue('');
await container.loadAsync(executorModule(profiles));
const factory = container.get<interfaces.Factory<any>>(
TYPES.Factory_DataSource
Expand All @@ -47,18 +69,22 @@ it('Executor module should bind correct profiles to data sources and create a fa
expect(dsFromP1 instanceof DataSource1).toBeTruthy();
expect(dsFromP2 instanceof DataSource1).toBeTruthy();
expect(dsFromP3 instanceof DataSource2).toBeTruthy();
expect(dsFromP1.profiles).toEqual([
{ name: 'p1', type: 'ds1' },
{ name: 'p2', type: 'ds1' },
expect(dsFromP1.injectedProfiles).toEqual([
{ name: 'p1', type: 'ds1', allow: '*' },
{ name: 'p2', type: 'ds1', allow: '*' },
]);
expect(dsFromP3.injectedProfiles).toEqual([
{ name: 'p3', type: 'ds2', allow: '*' },
]);
expect(dsFromP3.profiles).toEqual([{ name: 'p3', type: 'ds2' }]);
});

it('Data source factory should throw error with invalid profile name', async () => {
// Arrange
const container = new Container();
const profiles = new Map<string, Profile>();
await container.loadAsync(executorModule(profiles));
container.bind(TYPES.ExtensionConfig).toConstantValue({});
container.bind(TYPES.ExtensionName).toConstantValue('');
const factory = container.get<interfaces.Factory<any>>(
TYPES.Factory_DataSource
);
Expand All @@ -67,3 +93,21 @@ it('Data source factory should throw error with invalid profile name', async ()
`Profile some-invalid-profile not found`
);
});

it('When the requestor is not a data source, container should return all profiles', async () => {
// Arrange
const container = new Container();
const profiles = new Map<string, Profile>();
profiles.set('p1', { name: 'p1', type: 'ds1', allow: '*' });
profiles.set('p2', { name: 'p2', type: 'ds1', allow: '*' });
profiles.set('p3', { name: 'p3', type: 'ds2', allow: '*' });
container.bind(TYPES.Extension_DataSource).to(DataSource3);
await container.loadAsync(executorModule(profiles));

// Act
const ds3 = container.get<DataSource3>(TYPES.Extension_DataSource);
const profilesInjected = ds3.injectedProfiles;

// Assert
expect(profilesInjected.length).toEqual(3);
});
4 changes: 4 additions & 0 deletions packages/core/test/data-source/dataSource.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,12 @@ it(`GetProfiles function should return all profiles which belong to us`, async (
{
name: 'profile1',
type: 'mock',
allow: '*',
},
{
name: 'profile2',
type: 'mock',
allow: '*',
},
]);
// Act
Expand All @@ -48,10 +50,12 @@ it(`GetProfile function should correct profile`, async () => {
{
name: 'profile1',
type: 'mock',
allow: '*',
},
{
name: 'profile2',
type: 'mock',
allow: '*',
},
]);
// Act
Expand Down
14 changes: 13 additions & 1 deletion packages/extension-driver-duckdb/tests/duckdbDataSource.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ afterAll(async () => {
it('Should work with memory-only database', async () => {
// Arrange
const dataSource = new DuckDBDataSource(null as any, 'duckdb', [
{ name: 'mocked-profile', type: 'duck' },
{ name: 'mocked-profile', type: 'duck', allow: '*' },
]); // set connection to undefined, test the tolerance
await dataSource.activate();
const bindParams = new Map<string, any>();
Expand Down Expand Up @@ -87,6 +87,7 @@ it('Should work with persistent database', async () => {
connection: {
'persistent-path': testFile,
},
allow: '*',
},
]);
await dataSource.activate();
Expand Down Expand Up @@ -126,6 +127,7 @@ it('Should send correct data with chunks', async () => {
connection: {
'persistent-path': testFile,
},
allow: '*',
},
]);
await dataSource.activate();
Expand Down Expand Up @@ -163,6 +165,7 @@ it('Should throw error from upstream', async () => {
connection: {
'persistent-path': testFile,
},
allow: '*',
},
]);
await dataSource.activate();
Expand All @@ -187,6 +190,7 @@ it('Should return empty data and column with zero result', async () => {
connection: {
'persistent-path': testFile,
},
allow: '*',
},
]);
await dataSource.activate();
Expand Down Expand Up @@ -215,6 +219,7 @@ it('Should print queries without binding when log-queries = true', async () => {
connection: {
'log-queries': true,
},
allow: '*',
},
]);
await dataSource.activate();
Expand Down Expand Up @@ -256,6 +261,7 @@ it('Should print queries with binding when log-queries = true and log-parameters
'log-queries': true,
'log-parameters': true,
},
allow: '*',
},
]);
await dataSource.activate();
Expand Down Expand Up @@ -297,30 +303,35 @@ it('Should share db instances for same path besides in-memory only db', async ()
connection: {
'persistent-path': path.resolve(__dirname, 'db1.db'),
},
allow: '*',
},
{
name: 'db2',
type: 'duck',
connection: {
'persistent-path': path.resolve(__dirname, 'db1.db'),
},
allow: '*',
},
{
name: 'db3',
type: 'duck',
connection: {
'persistent-path': path.resolve(__dirname, 'db2.db'),
},
allow: '*',
},
{
name: 'db4',
type: 'duck',
connection: {},
allow: '*',
},
{
name: 'db5',
type: 'duck',
connection: {},
allow: '*',
},
]);
await dataSource.activate();
Expand Down Expand Up @@ -363,6 +374,7 @@ it('Should throw error when profile instance not found', async () => {
{
name: 'mocked-profile',
type: 'duck',
allow: '*',
},
]);
await dataSource.activate();
Expand Down
1 change: 1 addition & 0 deletions packages/integration-testing/src/example1/profile.yaml
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
- name: pg-mem
type: pg-mem
allow: '*'
2 changes: 2 additions & 0 deletions packages/serve/src/containers/container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Container as CoreContainer } from '@vulcan-sql/core';
import {
applicationModule,
documentRouterModule,
evaluationModule,
extensionModule,
routeGeneratorModule,
} from './modules';
Expand Down Expand Up @@ -32,6 +33,7 @@ export class Container {
await this.inversifyContainer.loadAsync(extensionModule(config));
await this.inversifyContainer.loadAsync(applicationModule());
await this.inversifyContainer.loadAsync(documentRouterModule());
await this.inversifyContainer.loadAsync(evaluationModule());
}

public async unload() {
Expand Down
8 changes: 8 additions & 0 deletions packages/serve/src/containers/modules/evaluation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { Evaluator } from '@vulcan-sql/serve/evaluator';
import { AsyncContainerModule } from 'inversify';
import { TYPES } from '../types';

export const evaluationModule = () =>
new AsyncContainerModule(async (bind) => {
bind<Evaluator>(TYPES.Evaluator).to(Evaluator).inSingletonScope();
});
1 change: 1 addition & 0 deletions packages/serve/src/containers/modules/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ export * from './routeGenerator';
export * from './extension';
export * from './application';
export * from './documentRouter';
export * from './evaluation';
Loading