1
0
mirror of https://github.com/bpatrik/pigallery2.git synced 2024-11-28 08:58:49 +02:00

updating settings page

This commit is contained in:
Patrik J. Braun 2019-08-20 12:54:45 +02:00
parent 3633168ee4
commit 2012ec3d91
31 changed files with 313 additions and 60 deletions

View File

@ -74,6 +74,7 @@
"tsConfig": "frontend/tsconfig.spec.json",
"scripts": [],
"styles": [
"./node_modules/bootstrap/dist/css/bootstrap.min.css",
"node_modules/ngx-toastr/toastr.css",
"node_modules/bootstrap/dist/css/bootstrap.css",
"node_modules/open-iconic/font/css/open-iconic-bootstrap.css",
@ -131,4 +132,4 @@
"prefix": "app"
}
}
}
}

View File

@ -3,7 +3,7 @@ import {ErrorCodes, ErrorDTO} from '../../common/entities/Error';
import {ObjectManagers} from '../model/ObjectManagers';
import {Logger} from '../Logger';
import {SQLConnection} from '../model/sql/SQLConnection';
import {DataBaseConfig, DatabaseType, IndexingConfig, ThumbnailConfig} from '../../common/config/private/IPrivateConfig';
import {DataBaseConfig, DatabaseType, IndexingConfig, TaskConfig, ThumbnailConfig} from '../../common/config/private/IPrivateConfig';
import {Config} from '../../common/config/private/Config';
import {ConfigDiagnostics} from '../model/diagnostics/ConfigDiagnostics';
import {ClientConfig} from '../../common/config/public/ConfigClass';
@ -440,6 +440,35 @@ export class AdminMWs {
}
public static async updateTasksSettings(req: Request, res: Response, next: NextFunction) {
if ((typeof req.body === 'undefined') || (typeof req.body.settings === 'undefined')) {
return next(new ErrorDTO(ErrorCodes.INPUT_ERROR, 'settings is needed'));
}
try {
// only updating explicitly set config (not saving config set by the diagnostics)
const settings: TaskConfig = req.body.settings;
const original = Config.original();
await ConfigDiagnostics.testTasksConfig(settings, original);
Config.Server.tasks = settings;
original.Server.tasks = settings;
original.save();
await ConfigDiagnostics.runDiagnostics();
Logger.info(LOG_TAG, 'new config:');
Logger.info(LOG_TAG, JSON.stringify(Config, null, '\t'));
return next();
} catch (err) {
if (err instanceof Error) {
return next(new ErrorDTO(ErrorCodes.SETTINGS_ERROR, 'Settings error: ' + err.toString(), err));
}
return next(new ErrorDTO(ErrorCodes.SETTINGS_ERROR, 'Settings error: ' + JSON.stringify(err, null, ' '), err));
}
}
public static startTask(req: Request, res: Response, next: NextFunction) {
try {
const id = req.params.id;

View File

@ -3,6 +3,7 @@ import {
DataBaseConfig,
DatabaseType,
IPrivateConfig,
TaskConfig,
ThumbnailConfig,
ThumbnailProcessingLib
} from '../../../common/config/private/IPrivateConfig';
@ -127,6 +128,10 @@ export class ConfigDiagnostics {
}
static async testTasksConfig(faces: TaskConfig, config: IPrivateConfig) {
}
static async testFacesConfig(faces: ClientConfig.FacesConfig, config: IPrivateConfig) {
if (faces.enabled === true) {
if (config.Server.database.type === DatabaseType.memory) {
@ -281,6 +286,17 @@ export class ConfigDiagnostics {
Config.Client.Faces.enabled = false;
}
try {
await ConfigDiagnostics.testTasksConfig(Config.Server.tasks, Config);
} catch (ex) {
const err: Error = ex;
NotificationManager.warning('Some Tasks are not supported with these settings. Disabling temporally. ' +
'Please adjust the config properly.', err.toString());
Logger.warn(LOG_TAG, 'Some Tasks not supported with these settings, switching off..', err.toString());
Config.Client.Faces.enabled = false;
}
try {
await ConfigDiagnostics.testSharingConfig(Config.Client.Sharing, Config);
} catch (ex) {

View File

@ -3,6 +3,7 @@ import {TaskProgressDTO} from '../../../common/entities/settings/TaskProgressDTO
import {ITask} from './ITask';
import {TaskRepository} from './TaskRepository';
import {Config} from '../../../common/config/private/Config';
import {TaskTriggerType} from '../../../common/entities/task/TaskScheduleDTO';
export class TaskManager implements ITaskManager {
@ -31,6 +32,34 @@ export class TaskManager implements ITaskManager {
return TaskRepository.Instance.getAvailableTasks();
}
public runSchedules(): void {
Config.Server.tasks.scheduled.forEach(schedule => {
let nextRun = null;
switch (schedule.trigger.type) {
case TaskTriggerType.scheduled:
nextRun = Date.now() - schedule.trigger.time;
break;
/*case TaskTriggerType.periodic:
//TODo finish it
const getNextDayOfTheWeek = (dayOfWeek: number) => {
const refDate = new Date();
refDate.setHours(0, 0, 0, 0);
refDate.setDate(refDate.getDate() + (dayOfWeek + 7 - refDate.getDay()) % 7);
return refDate;
};
nextRun = Date.now() - schedule.trigger.periodicity;
break;*/
}
if (nextRun != null) {
setTimeout(() => {
this.start(schedule.taskName, schedule.config);
}, nextRun);
}
});
}
protected findTask(taskName: string): ITask<any> {
return this.getAvailableTasks().find(t => t.Name === taskName);

View File

@ -174,6 +174,12 @@ export class AdminRouter {
AdminMWs.updateIndexingSettings,
RenderingMWs.renderOK
);
app.put('/api/settings/tasks',
AuthenticationMWs.authenticate,
AuthenticationMWs.authorise(UserRoles.Admin),
AdminMWs.updateTasksSettings,
RenderingMWs.renderOK
);
}

View File

@ -12,7 +12,7 @@ import * as path from 'path';
import {ConfigLoader} from 'typeconfig';
import {Utils} from '../../Utils';
import {UserRoles} from '../../entities/UserDTO';
import {TaskScheduleDTO, TaskTriggerType} from '../../entities/task/TaskScheduleDTO';
import {TaskScheduleDTO} from '../../entities/task/TaskScheduleDTO';
import {Config} from './Config';
/**
@ -65,28 +65,7 @@ export class PrivateConfigClass extends PublicConfigClass implements IPrivateCon
listingLimit: 1000
},
tasks: {
scheduled: [
{
priority: 1,
taskName: 'indexing',
config: null,
trigger: {
type: TaskTriggerType.periodic,
time: {
offset: 0,
repeat: 10
}
}
},
{
priority: 2,
taskName: 'Database reset',
config: null,
trigger: {
type: TaskTriggerType.never
}
}
]
scheduled: []
}
};
private ConfigLoader: any;

View File

@ -12,15 +12,13 @@ export interface NeverTaskTrigger {
export interface ScheduledTaskTrigger extends TaskTrigger {
type: TaskTriggerType.scheduled;
time: number;
time: number; // data time
}
export interface PeriodicTaskTrigger extends TaskTrigger {
type: TaskTriggerType.periodic;
time: {
offset: number,
repeat: number
};
periodicity: number; // 1-7: week days 8+ every x days
atTime: number; // day time
}
export interface TaskScheduleDTO {

View File

@ -83,6 +83,10 @@ import {ControlsLightboxComponent} from './ui/gallery/lightbox/controls/controls
import {FacesSettingsComponent} from './ui/settings/faces/faces.settings.component';
import {TasksSettingsComponent} from './ui/settings/tasks/tasks.settings.component';
import {ScheduledTasksService} from './ui/settings/scheduled-tasks.service';
import {TimepickerModule} from 'ngx-bootstrap/timepicker';
import {TimeStampDatePickerComponent} from './ui/utils/timestamp-datepicker/datepicker.component';
import {TimeStampTimePickerComponent} from './ui/utils/timestamp-timepicker/timepicker.component';
@Injectable()
@ -141,7 +145,8 @@ export function translationsFactory(locale: string) {
BsDropdownModule.forRoot(),
SlimLoadingBarModule.forRoot(),
BsDatepickerModule.forRoot(),
YagaModule
YagaModule,
TimepickerModule.forRoot()
],
declarations: [AppComponent,
LoginComponent,
@ -151,6 +156,8 @@ export function translationsFactory(locale: string) {
// misc
FrameComponent,
LanguageComponent,
TimeStampDatePickerComponent,
TimeStampTimePickerComponent,
// Gallery
GalleryLightboxMediaComponent,
GalleryPhotoLoadingComponent,

View File

@ -14,7 +14,7 @@ import {Config} from '../../../../common/config/public/Config';
})
export class AdminComponent implements OnInit {
simplifiedMode = true;
simplifiedMode = false;
text = {
Advanced: 'Advanced',
Simplified: 'Simplified'

View File

@ -74,7 +74,8 @@ export abstract class SettingsComponent<T extends { [key: string]: any }, S exte
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
if (typeof original[key] === 'undefined') {
throw new Error('unknown settings: ' + key);
console.warn('unknown settings: ' + key);
return false;
}
if (typeof original[key] === 'object') {
if (this.settingsSame(newSettings[key], original[key]) === false) {
@ -88,6 +89,13 @@ export abstract class SettingsComponent<T extends { [key: string]: any }, S exte
return true;
}
public testSettingChanges() {
// TODO: fix after this issue is fixed: https://github.com/angular/angular/issues/24818
setTimeout(() => {
this.changed = !this.settingsSame(this.settings, this.original);
}, 0);
}
ngOnInit() {
if (!this._authService.isAuthenticated() ||
this._authService.user.value.role < UserRoles.Admin) {
@ -98,9 +106,7 @@ export abstract class SettingsComponent<T extends { [key: string]: any }, S exte
// TODO: fix after this issue is fixed: https://github.com/angular/angular/issues/24818
this._subscription = this.form.valueChanges.subscribe(() => {
setTimeout(() => {
this.changed = !this.settingsSame(this.settings, this.original);
}, 0);
this.testSettingChanges();
});
}

View File

@ -1,7 +1,7 @@
<form #settingsForm="ngForm" class="form-horizontal">
<div class="card mb-4">
<h5 class="card-header" i18n>
Basic settings
<ng-container i18n>Basic settings</ng-container><ng-container *ngIf="changed">*</ng-container>
</h5>
<div class="card-body">
<div [hidden]="!error" class="alert alert-danger" role="alert"><strong>Error: </strong>{{error}}</div>

View File

@ -1,6 +1,6 @@
<div class="card mb-4">
<h5 class="card-header" i18n>
Database settings
<ng-container i18n>Database settings</ng-container><ng-container *ngIf="changed">*</ng-container>
</h5>
<div class="card-body">
<div [hidden]="!error" class="alert alert-danger" role="alert"><strong>Error: </strong>{{error}}</div>

View File

@ -2,7 +2,7 @@
<div class="card mb-4"
[ngClass]="settings.enabled && !_settingsService.isSupported()?'panel-warning':''">
<h5 class="card-header">
<ng-container i18n>Faces settings</ng-container>
<ng-container i18n>Faces settings</ng-container><ng-container *ngIf="changed">*</ng-container>
<div class="switch-wrapper">
<bSwitch
class="switch"

View File

@ -1,7 +1,7 @@
<form #settingsForm="ngForm" class="form-horizontal">
<div class="card mb-4">
<h5 class="card-header" i18n>
Folder indexing
<ng-container i18n>Folder indexing</ng-container><ng-container *ngIf="changed">*</ng-container>
</h5>
<div class="card-body">
<div [hidden]="!error" class="alert alert-danger" role="alert"><strong>Error: </strong>{{error}}</div>

View File

@ -1,7 +1,7 @@
<form #settingsForm="ngForm" class="form-horizontal">
<div class="card mb-4">
<h5 class="card-header">
<ng-container i18n>Map settings</ng-container>
<ng-container i18n>Map settings</ng-container><ng-container *ngIf="changed">*</ng-container>
<div class="switch-wrapper">
<bSwitch
class="switch"

View File

@ -1,7 +1,7 @@
<form #settingsForm="ngForm" class="form-horizontal">
<div class="card mb-4">
<h5 class="card-header">
<ng-container i18n>Meta file settings</ng-container>
<ng-container i18n>Meta file settings</ng-container><ng-container *ngIf="changed">*</ng-container>
<div class="switch-wrapper">
<bSwitch
class="switch"

View File

@ -1,7 +1,7 @@
<form #settingsForm="ngForm" class="form-horizontal" >
<div class="card mb-4">
<h5 class="card-header" i18n>
Other settings
<ng-container i18n>Other settings</ng-container><ng-container *ngIf="changed">*</ng-container>
</h5>
<div class="card-body">
<div [hidden]="!error" class="alert alert-danger" role="alert"><strong i18n>Error: </strong>{{error}}</div>

View File

@ -2,7 +2,7 @@
<div class="card mb-4"
[ngClass]="settings.enabled && !_settingsService.isSupported()?'panel-warning':''">
<h5 class="card-header">
<ng-container i18n>Random Photo settings</ng-container>
<ng-container i18n>Random Photo settings</ng-container><ng-container *ngIf="changed">*</ng-container>
<div class="switch-wrapper">
<bSwitch
class="switch"

View File

@ -44,11 +44,15 @@ export class ScheduledTasksService {
if (this.timer != null || this.subscribers === 0) {
return;
}
let repeatTime = 5000;
if (Object.values(this.progress.value).length === 0) {
repeatTime = 10000;
}
this.timer = window.setTimeout(async () => {
await this.getProgress();
this.timer = null;
this.getProgressPeriodically();
}, 5000);
}, repeatTime);
}
private incSubscribers() {

View File

@ -2,7 +2,7 @@
<div class="card mb-4"
[ngClass]="settings.enabled && !_settingsService.isSupported()?'panel-warning':''">
<h5 class="card-header">
<ng-container i18n>Search settings</ng-container>
<ng-container i18n>Search settings</ng-container><ng-container *ngIf="changed">*</ng-container>
<div class="switch-wrapper">
<bSwitch
class="switch"

View File

@ -2,7 +2,7 @@
<div class="card mb-4"
[ngClass]="settings.enabled && !_settingsService.isSupported()?'panel-warning':''">
<h5 class="card-header">
<ng-container i18n>Share settings</ng-container>
<ng-container i18n>Share settings</ng-container><ng-container *ngIf="changed">*</ng-container>
<div class="switch-wrapper">
<bSwitch
class="switch"

View File

@ -1,7 +1,8 @@
<form #settingsForm="ngForm" class="form-horizontal">
<div class="card mb-4">
<h5 class="card-header" i18n>
Tasks
<ng-container i18n>Tasks</ng-container>
<ng-container *ngIf="changed">*</ng-container>
</h5>
<div class="card-body">
<div [hidden]="!error" class="alert alert-danger" role="alert"><strong>Error: </strong>{{error}}</div>
@ -24,6 +25,36 @@
[ngValue]="availableTask.Name">{{availableTask.Name}}
</option>
</select>
<div class="row">
<select class="form-control" [(ngModel)]="schedule.trigger.type"
(ngModelChange)="taskTriggerTypeChanged($event,schedule)"
[name]="'repeatType'+i" required>
<option *ngFor="let taskTrigger of taskTriggerType"
[ngValue]="taskTrigger.key">{{taskTrigger.value}}
</option>
</select>
<app-timestamp-datepicker
[name]="'triggerTime'+i"
*ngIf="schedule.trigger.type== TaskTriggerType.scheduled"
[(timestamp)]="schedule.trigger.time"></app-timestamp-datepicker>
<select *ngIf="schedule.trigger.type== TaskTriggerType.periodic"
class="form-control" [(ngModel)]="schedule.trigger.periodicity" [name]="'periodicity'+i"
required>
<option *ngFor="let period of periods; let i= index"
[ngValue]="i">
<ng-container i18n>every</ng-container>
{{period}}
</option>
</select>
<app-timestamp-timepicker
[name]="'atTime'+i"
(timestampChange)="testSettingChanges()"
*ngIf="schedule.trigger.type== TaskTriggerType.periodic"
[(timestamp)]="schedule.trigger.atTime"></app-timestamp-timepicker>
</div>
</div>
<div>
<button class="btn btn-success"
@ -31,7 +62,7 @@
[disabled]="disableButtons"
title="Trigger task run manually"
i18n-title
(click)="start(schedule)" i18n>Start
(click)="start(schedule)" i18n>Start now
</button>
<button class="btn btn-secondary"
*ngIf="tasksService.progress.value[schedule.taskName]"
@ -110,6 +141,13 @@
</div>
</div>
</div>
<button class="btn btn-success float-right"
[disabled]="!settingsForm.form.valid || !changed || inProgress"
(click)="save()" i18n>Save
</button>
<button class="btn btn-secondary float-right"
(click)="reset()" i18n>Reset
</button>
</div>
</div>

View File

@ -8,19 +8,29 @@ import {SettingsComponent} from '../_abstract/abstract.settings.component';
import {I18n} from '@ngx-translate/i18n-polyfill';
import {ErrorDTO} from '../../../../../common/entities/Error';
import {ScheduledTasksService} from '../scheduled-tasks.service';
import {TaskScheduleDTO} from '../../../../../common/entities/task/TaskScheduleDTO';
import {
NeverTaskTrigger,
PeriodicTaskTrigger,
ScheduledTaskTrigger,
TaskScheduleDTO,
TaskTriggerType
} from '../../../../../common/entities/task/TaskScheduleDTO';
import {Utils} from '../../../../../common/Utils';
@Component({
selector: 'app-settings-tasks',
templateUrl: './tasks.settings.component.html',
styleUrls: ['./tasks.settings.component.css',
'./../_abstract/abstract.settings.component.css'],
providers: [TasksSettingsService],
providers: [TasksSettingsService]
})
export class TasksSettingsComponent extends SettingsComponent<TaskConfig, TasksSettingsService>
implements OnInit, OnDestroy, OnChanges {
disableButtons = false;
taskTriggerType: { key: number, value: string }[];
TaskTriggerType = TaskTriggerType;
periods: string[] = [];
constructor(_authService: AuthenticationService,
_navigation: NavigationService,
@ -37,7 +47,15 @@ export class TasksSettingsComponent extends SettingsComponent<TaskConfig, TasksS
i18n,
s => s.Server.tasks);
this.hasAvailableSettings = !this.simplifiedMode;
this.taskTriggerType = Utils.enumToArray(TaskTriggerType);
this.periods = [this.i18n('Monday'),
this.i18n('Tuesday'),
this.i18n('Wednesday'),
this.i18n('Thursday'),
this.i18n('Friday'),
this.i18n('Saturday'),
this.i18n('Sunday'),
this.i18n('day')];
}
@ -130,6 +148,34 @@ export class TasksSettingsComponent extends SettingsComponent<TaskConfig, TasksS
}
update($event: string, trigger: ScheduledTaskTrigger) {
if (!$event) {
return;
}
console.log(typeof $event);
console.log($event);
console.log(new Date($event));
console.log(new Date($event).getTime());
trigger.time = new Date($event).getTime();
}
toDate(time: number) {
return new Date(time);
}
taskTriggerTypeChanged(triggerType: TaskTriggerType, schedule: TaskScheduleDTO) {
schedule.trigger = <NeverTaskTrigger>{type: triggerType};
switch (triggerType) {
case TaskTriggerType.scheduled:
(<ScheduledTaskTrigger><unknown>schedule.trigger).time = (Date.now());
break;
case TaskTriggerType.periodic:
(<PeriodicTaskTrigger><unknown>schedule.trigger).periodicity = null;
(<PeriodicTaskTrigger><unknown>schedule.trigger).atTime = null;
break;
}
}
}

View File

@ -1,7 +1,7 @@
<form #settingsForm="ngForm" class="form-horizontal">
<div class="card mb-4">
<h5 class="card-header" i18n>
Thumbnail settings
<ng-container i18n>Thumbnail settings</ng-container><ng-container *ngIf="changed">*</ng-container>
</h5>
<div class="card-body">
<div [hidden]="!error" class="alert alert-danger" role="alert"><strong>Error: </strong>{{error}}</div>

View File

@ -1,7 +1,7 @@
<form #settingsForm="ngForm" class="form-horizontal">
<div class="card mb-4">
<h5 class="card-header">
<ng-container i18n>Video settings</ng-container>
<ng-container i18n>Video settings</ng-container><ng-container *ngIf="changed">*</ng-container>
<div class="switch-wrapper">
<bSwitch
class="switch"

View File

@ -0,0 +1,8 @@
<input
class="form-control"
[name]="name"
bsDatepicker
[ngModel]="date"
(ngModelChange)="onChange($event)"
[bsConfig]="{ dateInputFormat: 'YYYY.MM.DD, h:mm' }"
required>

View File

@ -0,0 +1,37 @@
import {Component, EventEmitter, Input, Output} from '@angular/core';
@Component({
selector: 'app-timestamp-datepicker',
templateUrl: './datepicker.component.html',
})
export class TimeStampDatePickerComponent {
timestampValue = 0;
@Output() timestampChange = new EventEmitter<number>();
date: Date = new Date();
@Input() name: string;
@Input()
public get timestamp() {
return this.timestampValue;
}
public set timestamp(val: number) {
this.date.setTime(val);
if (this.timestampValue === val) {
return;
}
this.timestampValue = val;
this.timestampChange.emit(this.timestampValue);
}
onChange(date: Date | string) {
this.timestamp = (new Date(date)).getTime();
}
}

View File

@ -0,0 +1,10 @@
<timepicker
class="form-control"
[name]="name"
[ngModel]="date"
[showSpinners]="false"
[showMeridian]="false"
[mousewheel]="true"
[arrowkeys]="true"
(ngModelChange)="onChange($event)"
required></timepicker>

View File

@ -0,0 +1,38 @@
import {Component, EventEmitter, Input, Output} from '@angular/core';
@Component({
selector: 'app-timestamp-timepicker',
templateUrl: './timepicker.component.html',
})
export class TimeStampTimePickerComponent {
timestampValue = 0;
@Output() timestampChange = new EventEmitter<number>();
date: Date = new Date();
@Input() name: string;
@Input()
public get timestamp() {
return this.timestampValue;
}
public set timestamp(val: number) {
this.date.setTime(val);
if (this.timestampValue === val) {
return;
}
this.timestampValue = val;
this.timestampChange.emit(this.timestampValue);
}
onChange(date: Date | string) {
this.timestamp = (new Date(date)).getTime();
}
}

11
package-lock.json generated
View File

@ -1,6 +1,6 @@
{
"name": "pigallery2",
"version": "1.6.5",
"version": "1.7.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@ -4536,8 +4536,7 @@
"bootstrap": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-4.3.1.tgz",
"integrity": "sha512-rXqOmH1VilAt2DyPzluTi2blhk17bO7ef+zLLPlWvG494pDxcM234pJ8wTc/6R40UWizAIIMgxjvxZg5kmsbag==",
"dev": true
"integrity": "sha512-rXqOmH1VilAt2DyPzluTi2blhk17bO7ef+zLLPlWvG494pDxcM234pJ8wTc/6R40UWizAIIMgxjvxZg5kmsbag=="
},
"boxen": {
"version": "3.2.0",
@ -12243,9 +12242,9 @@
"dev": true
},
"ngx-bootstrap": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/ngx-bootstrap/-/ngx-bootstrap-5.1.0.tgz",
"integrity": "sha512-gHmIH1dZcZgbgu9Y88iPa8JaMkSM1QrU1zPDSJIw5TUNXVbwhvi5bzh2ttjvL88agyVWmTHM0mgyntPAgULxCQ==",
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ngx-bootstrap/-/ngx-bootstrap-4.3.0.tgz",
"integrity": "sha512-ZPS6V2yLEeqB/7KIlVohS8qUdtFa1bgUB/sSPWRcXqOWU3EKhORetZoXG6m2F5ILYDe5hwQvBEjdHPlEz2piOg==",
"dev": true
},
"ngx-clipboard": {

View File

@ -29,6 +29,7 @@
"dependencies": {
"bcryptjs": "2.4.3",
"body-parser": "1.19.0",
"bootstrap": "4.1.1",
"cookie-parser": "1.4.4",
"cookie-session": "2.0.0-beta.3",
"ejs": "2.6.2",
@ -39,6 +40,7 @@
"jdataview": "2.5.0",
"jimp": "0.6.4",
"locale": "0.1.0",
"ngx-bootstrap": "^4.1.1",
"npm-check-updates": "^3.1.20",
"reflect-metadata": "0.1.13",
"sqlite3": "4.0.9",