- addes @adonisjs/redis fo saving session into redis with redis.ts contract and config
Some checks failed
CI Pipeline / japa-tests (push) Failing after 52s

- npm updated
- added createHashValues and dlete inside File.ts
- added dataset_count property inside Subject.ts
- corrected rotes.ts with correct permissions
This commit is contained in:
Kaimbacher 2023-11-27 17:17:22 +01:00
parent d8bdce1369
commit b6fdfbff41
29 changed files with 496 additions and 201 deletions

View File

@ -19,11 +19,15 @@
"./start/kernel",
{
"file": "./start/inertia",
"environment": ["web"]
"environment": [
"web"
]
},
{
"file": "./start/validator",
"environment": ["web"]
"environment": [
"web"
]
}
],
"providers": [
@ -37,7 +41,8 @@
"@adonisjs/auth",
"@eidellev/adonis-stardust",
"./providers/QueryBuilderProvider",
"./providers/TokenWorkerProvider"
"./providers/TokenWorkerProvider",
"@adonisjs/redis"
],
"metaFiles": [
{
@ -49,15 +54,21 @@
"reloadServer": false
}
],
"aceProviders": ["@adonisjs/repl"],
"aceProviders": [
"@adonisjs/repl"
],
"tests": {
"suites": [
{
"name": "functional",
"files": ["tests/functional/**/*.spec(.ts|.js)"],
"files": [
"tests/functional/**/*.spec(.ts|.js)"
],
"timeout": 60000
}
]
},
"testProviders": ["@japa/preset-adonis/TestsProvider"]
"testProviders": [
"@japa/preset-adonis/TestsProvider"
]
}

View File

@ -11,3 +11,7 @@ PG_PORT=5432
PG_USER=lucid
PG_PASSWORD=
PG_DB_NAME=lucid
REDIS_CONNECTION=local
REDIS_HOST=127.0.0.1
REDIS_PORT=6379
REDIS_PASSWORD=

View File

@ -34,6 +34,7 @@ import ClamScan from 'clamscan';
import { ValidationException } from '@ioc:Adonis/Core/Validator';
import Drive from '@ioc:Adonis/Core/Drive';
import { Exception } from '@adonisjs/core/build/standalone';
import { MultipartFileContract } from '@ioc:Adonis/Core/BodyParser';
export default class DatasetController {
public async index({ auth, request, inertia }: HttpContextContract) {
@ -335,8 +336,8 @@ export default class DatasetController {
}
session.flash('message', 'Dataset has been created successfully');
// return response.redirect().toRoute('user.index');
return response.redirect().back();
return response.redirect().toRoute('user.index');
// return response.redirect().back();
}
private async createDatasetAndAssociations(user: User, request: HttpContextContract['request'], trx: TransactionClientContract) {
@ -691,7 +692,10 @@ export default class DatasetController {
.preload('licenses')
.preload('authors')
.preload('contributors')
.preload('subjects')
// .preload('subjects')
.preload('subjects', (builder) => {
builder.orderBy('id', 'asc').withCount('datasets');
})
.preload('references')
.preload('files');
@ -779,6 +783,7 @@ export default class DatasetController {
throw error;
// return response.badRequest(error.messages);
}
// await request.validate(UpdateDatasetValidator);
const id = request.param('id');
let trx: TransactionClientContract | null = null;
@ -843,6 +848,25 @@ export default class DatasetController {
}
}
// await dataset.useTransaction(trx).related('subjects').sync([]);
const keywords = request.input('subjects');
for (const keywordData of keywords) {
if (keywordData.id) {
const subject = await Subject.findOrFail(keywordData.id);
// await dataset.useTransaction(trx).related('subjects').attach([keywordData.id]);
subject.value = keywordData.value;
subject.type = keywordData.type;
subject.external_key = keywordData.external_key;
if (subject.$isDirty) {
await subject.save();
}
} else {
const keyword = new Subject();
keyword.fill(keywordData);
await dataset.useTransaction(trx).related('subjects').save(keyword, false);
}
}
// Save already existing files
const files = request.input('fileInputs', []);
for (const fileData of files) {
@ -857,43 +881,57 @@ export default class DatasetController {
}
// handle new uploaded files:
const uploadedFiles = request.files('files');
const uploadedFiles: MultipartFileContract[] = request.files('files');
if (Array.isArray(uploadedFiles) && uploadedFiles.length > 0) {
// let index = 1;
// for (const key in files) {
// const formFile = files[key]
// for (const fileData of files) {
for (const [index, fileData] of uploadedFiles.entries()) {
// const uploads = request.file('uploads');
// const fileIndex = formFile.file;
// const file = uploads[fileIndex];
const fileName = `file-${cuid()}.${fileData.extname}`;
const mimeType = fileData.headers['content-type'] || 'application/octet-stream'; // Fallback to a default MIME type
const datasetFolder = `files/${dataset.id}`;
await fileData.moveToDisk(
datasetFolder,
{
name: fileName,
overwrite: true, // overwrite in case of conflict
},
'local',
);
// save file metadata into db
const newFile = new File();
newFile.pathName = `${datasetFolder}/${fileName}`;
newFile.fileSize = fileData.size;
newFile.mimeType = mimeType;
newFile.label = fileData.clientName;
newFile.sortOrder = index;
newFile.visibleInFrontdoor = true;
newFile.visibleInOai = true;
await fileData.moveToDisk(datasetFolder, { name: fileName, overwrite: true }, 'local');
// let path = coverImage.filePath;
await dataset.useTransaction(trx).related('files').save(newFile);
await newFile.createHashValues();
const { clientFileName, sortOrder } = this.extractVariableNameAndSortOrder(fileData.clientName);
const mimeType = fileData.headers['content-type'] || 'application/octet-stream'; // Fallback to a default MIME type
// save file metadata into db
// const newFile = new File();
// newFile.pathName = `${datasetFolder}/${fileName}`;
// newFile.fileSize = fileData.size;
// newFile.mimeType = mimeType;
// newFile.label = clientFileName;
// newFile.sortOrder = sortOrder ? sortOrder : index;
// newFile.visibleInFrontdoor = true;
// newFile.visibleInOai = true;
const newFile = await dataset
.useTransaction(trx)
.related('files')
.create({
pathName: `${datasetFolder}/${fileName}`,
fileSize: fileData.size,
mimeType,
label: clientFileName,
sortOrder: sortOrder || index,
visibleInFrontdoor: true,
visibleInOai: true,
});
// save many related HashValue Instances to the file:
await newFile.createHashValues(trx);
}
}
// save collection
// const collection: Collection | null = await Collection.query().where('id', 21).first();
// collection && (await dataset.useTransaction(trx).related('collections').attach([collection.id]));
// // Save coverage
// if (data.coverage && !this.containsOnlyNull(data.coverage)) {
// const formCoverage = request.input('coverage');
// const coverage = await dataset.related('coverage').updateOrCreate({ dataset_id: dataset.id }, formCoverage);
// } else if (data.coverage && this.containsOnlyNull(data.coverage) && !dataset.coverage) {
// await dataset.coverage().delete();
// }
const input = request.only(['project_id', 'embargo_date', 'language', 'type', 'creating_corporation']);
// dataset.type = request.input('type');
dataset.merge(input);
@ -911,11 +949,30 @@ export default class DatasetController {
throw error;
}
session.flash('message', 'Dataset has been created successfully');
session.flash('message', 'Dataset has been updated successfully');
// return response.redirect().toRoute('user.index');
return response.redirect().back();
}
private extractVariableNameAndSortOrder(inputString: string): { clientFileName: string; sortOrder?: number } {
const regex = /^([^?]+)(?:\?([^=]+)=([^&]+))?/;
const match = inputString.match(regex);
if (match) {
const clientFileName = match[1];
const param = match[2];
let sortOrder;
if (param && param.toLowerCase() === 'sortorder') {
sortOrder = parseInt(match[3], 10);
}
return { clientFileName, sortOrder };
} else {
return { clientFileName: '', sortOrder: undefined }; // Or handle as needed for no match
}
}
public async delete({ request, inertia, response, session }) {
const id = request.param('id');
try {
@ -923,8 +980,8 @@ export default class DatasetController {
.preload('user', (builder) => {
builder.select('id', 'login');
})
.preload('files')
.where('id', id)
.preload('files')
.firstOrFail();
const validStates = ['inprogress', 'rejected_editor'];
if (!validStates.includes(dataset.server_state)) {
@ -958,11 +1015,16 @@ export default class DatasetController {
if (validStates.includes(dataset.server_state)) {
if (dataset.files && dataset.files.length > 0) {
for (const file of dataset.files) {
if (file.pathName) {
// delete file from filesystem
await Drive.delete(file.pathName);
// overwriten delete method also delets file on filespace
await file.delete();
}
}
const datasetFolder = `files/${params.id}`;
const folderExists = await Drive.exists(datasetFolder);
if (folderExists) {
const folderContents = await Drive.list(datasetFolder).toArray();
if (folderContents.length === 0) {
await Drive.delete(datasetFolder);
}
// delete dataset wirh relation from db
await dataset.delete();
@ -970,9 +1032,10 @@ export default class DatasetController {
return response.redirect().toRoute('dataset.list');
} else {
session.flash({
warning: `You cannot delete this dataset! The status of this dataset is "${dataset.server_state}"!`,
warning: `You cannot delete this dataset! Invalid server_state: "${dataset.server_state}"!`,
});
return response.redirect().back();
return response.status(400).redirect().back();
}
}
} catch (error) {
if (error instanceof ValidationException) {

View File

@ -203,7 +203,7 @@ export default class Dataset extends DatasetExtension {
pivotForeignKey: 'document_id',
pivotRelatedForeignKey: 'person_id',
pivotTable: 'link_documents_persons',
pivotColumns: ['role', 'sort_order', 'allow_email_contact'],
pivotColumns: ['role', 'sort_order', 'allow_email_contact', 'contributor_type'],
onQuery(query) {
query.wherePivot('role', 'contributor');
},

View File

@ -7,6 +7,7 @@ import BaseModel from './BaseModel';
import * as fs from 'fs';
import crypto from 'crypto';
import { TransactionClientContract } from '@ioc:Adonis/Lucid/Database';
import Drive from '@ioc:Adonis/Core/Drive';
export default class File extends BaseModel {
// private readonly _data: Uint8Array;
@ -116,21 +117,20 @@ export default class File extends BaseModel {
serializeAs: 'fileData',
})
public get fileData(): string {
// return this.fileData;
// const fileData = fs.readFileSync(path.resolve(__dirname, this.filePath));
// const fileData = fs.readFileSync(this.filePath);
try {
const fileContent: Buffer = fs.readFileSync(this.filePath);
// Create a Blob from the file content
// const blob = new Blob([fileContent], { type: this.type }); // Adjust
// let fileSrc = URL.createObjectURL(blob);
// return fileSrc;
// return Buffer.from(fileContent);
// get the buffer from somewhere
// const buff = fs.readFileSync('./test.bin');
// create a JSON string that contains the data in the property "blob"
const json = JSON.stringify({ blob: fileContent.toString('base64') });
return json;
} catch (err) {
// console.error(`Error reading file: ${err}`);
return '';
}
}
public async createHashValues(trx?: TransactionClientContract) {
@ -139,7 +139,7 @@ export default class File extends BaseModel {
for (const type of hashtypes) {
const hash = new HashValue();
hash.type = type;
const hashString = await this.checksumFile(this.filePath, type); // Assuming getRealHash is a method in the same model
const hashString = await this._checksumFile(this.filePath, type); // Assuming getRealHash is a method in the same model
hash.value = hashString;
// https://github.com/adonisjs/core/discussions/1872#discussioncomment-132289
@ -152,7 +152,17 @@ export default class File extends BaseModel {
}
}
private async checksumFile(path, hashName = 'md5'): Promise<string> {
public async delete() {
if (this.pathName) {
// Delete file from additional storage
await Drive.delete(this.pathName);
}
// Call the original delete method of the BaseModel to remove the record from the database
await super.delete();
}
private async _checksumFile(path, hashName = 'md5'): Promise<string> {
return new Promise((resolve, reject) => {
const hash = crypto.createHash(hashName);
const stream = fs.createReadStream(path);

View File

@ -69,6 +69,12 @@ export default class Person extends BaseModel {
return stock;
}
@computed()
public get pivot_contributor_type() {
const contributor_type = this.$extras.pivot_contributor_type; //my pivot column name was "stock"
return contributor_type;
}
@manyToMany(() => Dataset, {
pivotForeignKey: 'person_id',
pivotRelatedForeignKey: 'document_id',

View File

@ -1,4 +1,4 @@
import { column, SnakeCaseNamingStrategy, manyToMany, ManyToMany, beforeCreate, beforeUpdate } from '@ioc:Adonis/Lucid/Orm';
import { column, SnakeCaseNamingStrategy, manyToMany, ManyToMany, computed} from '@ioc:Adonis/Lucid/Orm';
import BaseModel from './BaseModel';
import { DateTime } from 'luxon';
@ -44,28 +44,33 @@ export default class Subject extends BaseModel {
})
public updated_at: DateTime;
@beforeCreate()
@beforeUpdate()
public static async resetDate(role) {
role.created_at = this.formatDateTime(role.created_at);
role.updated_at = this.formatDateTime(role.updated_at);
}
// @beforeCreate()
// @beforeUpdate()
// public static async resetDate(role) {
// role.created_at = this.formatDateTime(role.created_at);
// role.updated_at = this.formatDateTime(role.updated_at);
// }
private static formatDateTime(datetime) {
let value = new Date(datetime);
return datetime
? value.getFullYear() +
'-' +
(value.getMonth() + 1) +
'-' +
value.getDate() +
' ' +
value.getHours() +
':' +
value.getMinutes() +
':' +
value.getSeconds()
: datetime;
// private static formatDateTime(datetime) {
// let value = new Date(datetime);
// return datetime
// ? value.getFullYear() +
// '-' +
// (value.getMonth() + 1) +
// '-' +
// value.getDate() +
// ' ' +
// value.getHours() +
// ':' +
// value.getMinutes() +
// ':' +
// value.getSeconds()
// : datetime;
// }
@computed()
public get dataset_count() : number{
const count = this.$extras.datasets_count; //my pivot column name was "stock"
return count;
}
@manyToMany(() => Dataset, {

View File

@ -135,7 +135,7 @@ export default class CreateDatasetValidator {
'required': '{{ field }} is required',
'unique': '{{ field }} must be unique, and this value is already taken',
// 'confirmed': '{{ field }} is not correct',
'licenses.minLength': 'at least {{ options.minLength }} permission must be defined',
'licenses.minLength': 'at least {{ options.minLength }} licenses must be defined',
'licenses.*.number': 'Define licences as valid numbers',
'rights.equalTo': 'you must agree to continue',

View File

@ -136,7 +136,7 @@ export default class UpdateDatasetValidator {
'required': '{{ field }} is required',
'unique': '{{ field }} must be unique, and this value is already taken',
// 'confirmed': '{{ field }} is not correct',
'licenses.minLength': 'at least {{ options.minLength }} permission must be defined',
'licenses.minLength': 'at least {{ options.minLength }} licenses must be defined',
'licenses.*.number': 'Define licences as valid numbers',
'rights.equalTo': 'you must agree to continue',

46
config/redis.ts Normal file
View File

@ -0,0 +1,46 @@
/**
* Config source: https://git.io/JemcF
*
* Feel free to let us know via PR, if you find something broken in this config
* file.
*/
import Env from '@ioc:Adonis/Core/Env'
import { redisConfig } from '@adonisjs/redis/build/config'
/*
|--------------------------------------------------------------------------
| Redis configuration
|--------------------------------------------------------------------------
|
| Following is the configuration used by the Redis provider to connect to
| the redis server and execute redis commands.
|
| Do make sure to pre-define the connections type inside `contracts/redis.ts`
| file for AdonisJs to recognize connections.
|
| Make sure to check `contracts/redis.ts` file for defining extra connections
*/
export default redisConfig({
connection: Env.get('REDIS_CONNECTION'),
connections: {
/*
|--------------------------------------------------------------------------
| The default connection
|--------------------------------------------------------------------------
|
| The main connection you want to use to execute redis commands. The same
| connection will be used by the session provider, if you rely on the
| redis driver.
|
*/
local: {
host: Env.get('REDIS_HOST'),
port: Env.get('REDIS_PORT'),
password: Env.get('REDIS_PASSWORD', ''),
db: 0,
keyPrefix: '',
},
},
})

13
contracts/redis.ts Normal file
View File

@ -0,0 +1,13 @@
/**
* Contract source: https://git.io/JemcN
*
* Feel free to let us know via PR, if you find something broken in this config
* file.
*/
import { InferConnectionsFromConfig } from '@adonisjs/redis/build/config'
import redisConfig from '../config/redis'
declare module '@ioc:Adonis/Addons/Redis' {
interface RedisConnectionsList extends InferConnectionsFromConfig<typeof redisConfig> {}
}

92
package-lock.json generated
View File

@ -11,6 +11,7 @@
"@adonisjs/auth": "^8.2.3",
"@adonisjs/core": "^5.9.0",
"@adonisjs/lucid": "^18.3.0",
"@adonisjs/redis": "^7.3.4",
"@adonisjs/repl": "^3.1.11",
"@adonisjs/session": "^6.4.0",
"@adonisjs/shield": "^7.1.0",
@ -529,6 +530,19 @@
"truncatise": "0.0.8"
}
},
"node_modules/@adonisjs/redis": {
"version": "7.3.4",
"resolved": "https://registry.npmjs.org/@adonisjs/redis/-/redis-7.3.4.tgz",
"integrity": "sha512-74SApmgimjwU8QflnhANeo7CpQeP9aoObM217LJ51AtKwTvnb0yXaqdj2v60G9uCqcqZAIFWJmeUdXGgUwGcXw==",
"dependencies": {
"@poppinss/utils": "^5.0.0",
"@types/ioredis": "^4.28.10",
"ioredis": "^5.2.3"
},
"peerDependencies": {
"@adonisjs/core": "^5.1.0"
}
},
"node_modules/@adonisjs/repl": {
"version": "3.1.11",
"resolved": "https://registry.npmjs.org/@adonisjs/repl/-/repl-3.1.11.tgz",
@ -2886,6 +2900,11 @@
"vue": "^3.0.0"
}
},
"node_modules/@ioredis/commands": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.2.0.tgz",
"integrity": "sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg=="
},
"node_modules/@japa/api-client": {
"version": "1.4.4",
"resolved": "https://registry.npmjs.org/@japa/api-client/-/api-client-1.4.4.tgz",
@ -4055,6 +4074,14 @@
"@types/node": "*"
}
},
"node_modules/@types/ioredis": {
"version": "4.28.10",
"resolved": "https://registry.npmjs.org/@types/ioredis/-/ioredis-4.28.10.tgz",
"integrity": "sha512-69LyhUgrXdgcNDv7ogs1qXZomnfOEnSmrmMFqKgt1XMJxmoOSG/u3wYy13yACIfKuMJ8IhKgHafDO3sx19zVQQ==",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/istanbul-lib-coverage": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz",
@ -7794,6 +7821,14 @@
"node": ">=0.4.0"
}
},
"node_modules/denque": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz",
"integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==",
"engines": {
"node": ">=0.10"
}
},
"node_modules/depd": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
@ -10365,6 +10400,29 @@
"node": ">= 0.10"
}
},
"node_modules/ioredis": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.3.2.tgz",
"integrity": "sha512-1DKMMzlIHM02eBBVOFQ1+AolGjs6+xEcM4PDL7NqOS6szq7H9jSaEkIUH6/a5Hl241LzW6JLSiAbNvTQjUupUA==",
"dependencies": {
"@ioredis/commands": "^1.1.1",
"cluster-key-slot": "^1.1.0",
"debug": "^4.3.4",
"denque": "^2.1.0",
"lodash.defaults": "^4.2.0",
"lodash.isarguments": "^3.1.0",
"redis-errors": "^1.2.0",
"redis-parser": "^3.0.0",
"standard-as-callback": "^2.1.0"
},
"engines": {
"node": ">=12.22.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/ioredis"
}
},
"node_modules/ipaddr.js": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
@ -11493,6 +11551,11 @@
"integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==",
"dev": true
},
"node_modules/lodash.defaults": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz",
"integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ=="
},
"node_modules/lodash.flatten": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz",
@ -11505,6 +11568,11 @@
"integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==",
"dev": true
},
"node_modules/lodash.isarguments": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz",
"integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg=="
},
"node_modules/lodash.isequal": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
@ -14363,6 +14431,25 @@
"@redis/time-series": "1.0.5"
}
},
"node_modules/redis-errors": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz",
"integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==",
"engines": {
"node": ">=4"
}
},
"node_modules/redis-parser": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz",
"integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==",
"dependencies": {
"redis-errors": "^1.0.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/reflect-metadata": {
"version": "0.1.13",
"resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.13.tgz",
@ -15514,6 +15601,11 @@
"get-source": "^2.0.12"
}
},
"node_modules/standard-as-callback": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz",
"integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A=="
},
"node_modules/static-extend": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz",

View File

@ -69,6 +69,7 @@
"@adonisjs/auth": "^8.2.3",
"@adonisjs/core": "^5.9.0",
"@adonisjs/lucid": "^18.3.0",
"@adonisjs/redis": "^7.3.4",
"@adonisjs/repl": "^3.1.11",
"@adonisjs/session": "^6.4.0",
"@adonisjs/shield": "^7.1.0",

View File

@ -10,6 +10,7 @@ import OverlayLayer from '@/Components/OverlayLayer.vue';
// let menu = reactive({});
// menu = computed(() => usePage().props.navigation?.menu);
const layoutService = LayoutService();
</script>

View File

@ -1,6 +1,6 @@
<script setup>
import { ref, computed } from 'vue';
import { Link } from '@inertiajs/vue3';
<script lang="ts" setup>
import { ref, computed, ComputedRef } from 'vue';
import { Link, usePage } from '@inertiajs/vue3';
// import { Link } from '@inertiajs/inertia-vue3';
import { StyleService } from '@/Stores/style';
@ -9,6 +9,7 @@ import { getButtonColor } from '@/colors.js';
import BaseIcon from '@/Components/BaseIcon.vue';
import AsideMenuList from '@/Components/AsideMenuList.vue';
import { stardust } from '@eidellev/adonis-stardust/client';
import type { User } from '@/Dataset';
const props = defineProps({
item: {
@ -18,6 +19,10 @@ const props = defineProps({
isDropdownList: Boolean,
});
const user: ComputedRef<User> = computed(() => {
return usePage().props.authUser as User;
});
const itemRoute = computed(() => (props.item && props.item.route ? stardust.route(props.item.route) : ''));
// const isCurrentRoute = computed(() => (props.item && props.item.route ? stardust.isCurrent(props.item.route): false));
const itemHref = computed(() => (props.item && props.item.href ? props.item.href : ''));
@ -65,12 +70,20 @@ const is = computed(() => {
return 'div';
});
const hasRoles = computed(() => {
if (props.item.roles) {
return user.value.roles.some(role => props.item.roles.includes(role.name));
// return test;
}
return true
});
// props.routeName && stardust.isCurrent(props.routeName) ? props.activeColor : null
</script>
<!-- :target="props.item.target ?? null" -->
<template>
<li>
<li v-if="hasRoles">
<!-- <component :is="itemHref ? 'div' : Link" :href="itemHref ? itemHref : itemRoute" -->
<component
:is="is"

View File

@ -19,10 +19,10 @@ const menuClick = (event, item) => {
<template>
<ul>
<AsideMenuItem
v-for="(item, index) in menu"
v-for="(menuItem, index) in menu"
:key="index"
v-bind:item="item"
:is-dropdown-list="item.children?.length > 0"
v-bind:item="menuItem"
:is-dropdown-list="menuItem.children?.length > 0"
@menu-click="menuClick"
/>
</ul>

View File

@ -250,7 +250,7 @@ class FileUploadComponent extends Vue {
}
@Watch("files", {
deep: true
deep: true //also in case of pushing
})
public propertyWatcher(newItems: Array<TethysFile>) {
// Update sort_order based on the new index when the list is changed
@ -361,10 +361,11 @@ class FileUploadComponent extends Vue {
let localUrl: string = "";
if (file instanceof File) {
localUrl = URL.createObjectURL(file as Blob);
} else if (file.filePath) {
} else if (file.fileData) {
// const blob = new Blob([file.fileData]);
// localUrl = URL.createObjectURL(blob);
const parsed = JSON.parse(file.fileData);
file.fileData = "";
// retrieve the original buffer of data
const buff = Buffer.from(parsed.blob, "base64");
const blob = new Blob([buff], { type: 'application/octet-stream' });

View File

@ -34,7 +34,7 @@ import UserAvatarCurrentUser from '@/Components/UserAvatarCurrentUser.vue';
import BaseIcon from '@/Components/BaseIcon.vue';
import NavBarSearch from '@/Components/NavBarSearch.vue';
import { stardust } from '@eidellev/adonis-stardust/client';
import type User from 'App/Models/User';
import type { User } from '@/Dataset';
// const mainStore = MainService();
// const userName = computed(() =>mainStore.userName);
@ -68,6 +68,9 @@ const menuNavBarToggle = () => {
const menuOpenLg = () => {
layoutStore.isAsideLgActive = true;
};
const userHasRoles = (roleNames): boolean => {
return user.value.roles.some(role => roleNames.includes(role.name));
};
// const logout = () => {
// // router.post(route('logout'))
@ -133,7 +136,7 @@ const logout = async () => {
<NavBarItem :route-name="'admin.account.info'">
<NavBarItemLabel :icon="mdiAccount" label="My Profile" />
</NavBarItem>
<NavBarItem :route-name="'settings'">
<NavBarItem v-if="userHasRoles(['moderator', 'administrator'])" :route-name="'settings'">
<NavBarItemLabel :icon="mdiCogOutline" label="Settings" />
</NavBarItem>
<NavBarItem>

View File

@ -3,13 +3,9 @@
<div class="flex">
<!-- <label for="search-dropdown" class="mb-2 text-sm font-medium text-gray-900 sr-only dark:text-white">Your Email</label> -->
<div class="relative" data-te-dropdown-ref>
<button
id="states-button"
data-dropdown-toggle="dropdown-states"
<button id="states-button" data-dropdown-toggle="dropdown-states"
class="whitespace-nowrap h-12 z-10 inline-flex items-center py-2.5 px-4 text-sm font-medium text-center text-gray-500 bg-gray-100 border border-gray-300 rounded-l-lg hover:bg-gray-200 focus:ring-4 focus:outline-none focus:ring-gray-100 dark:bg-gray-700 dark:hover:bg-gray-600 dark:focus:ring-gray-700 dark:text-white dark:border-gray-600"
type="button"
@click.prevent="showStates"
>
type="button" @click.prevent="showStates">
<!-- <svg aria-hidden="true" class="h-3 mr-2" viewBox="0 0 15 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="0.5" width="14" height="12" rx="2" fill="white" />
<mask id="mask0_12694_49953" style="mask-type: alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="15" height="12">
@ -69,26 +65,20 @@
</svg> -->
<!-- eng -->
{{ language }}
<svg aria-hidden="true" class="w-4 h-4 ml-1" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<path
fill-rule="evenodd"
<svg aria-hidden="true" class="w-4 h-4 ml-1" fill="currentColor" viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd"
d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
clip-rule="evenodd"
></path>
clip-rule="evenodd"></path>
</svg>
</button>
<!-- class="w-full overflow-visible z-10 bg-white divide-y divide-gray-100 rounded-lg shadow w-44 dark:bg-gray-700"-->
<div
id="dropdown-states"
v-show="statesToggle"
class="absolute z-[1000] float-left m-0 min-w-max list-none overflow-hidden rounded-lg border-none bg-white bg-clip-padding text-left text-base shadow-lg dark:bg-neutral-700"
>
<div id="dropdown-states" v-show="statesToggle"
class="absolute z-[1000] float-left m-0 min-w-max list-none overflow-hidden rounded-lg border-none bg-white bg-clip-padding text-left text-base shadow-lg dark:bg-neutral-700">
<ul class="py-2 text-sm text-gray-700 dark:text-gray-200" aria-labelledby="states-button">
<li v-for="(item, index) in dropDownStates" :key="index" @click.prevent="setLanguage(item)">
<button
type="button"
class="inline-flex w-full px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-600 dark:hover:text-white"
>
<button type="button"
class="inline-flex w-full px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-600 dark:hover:text-white">
<div class="inline-flex items-center">
<span v-html="item.svg"></span>
{{ item.name }}
@ -102,57 +92,30 @@
<div class="w-full relative">
<!-- :class="inputElClass" -->
<!-- class="block p-2.5 w-full z-20 text-sm text-gray-900 bg-gray-50 rounded-r-lg border-l-gray-50 border-l-2 border border-gray-300 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-l-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:border-blue-500" -->
<input
v-model="computedValue"
type="text"
:name="props.name"
autocomplete="off"
:class="inputElClass"
placeholder="Search Keywords..."
required
@input="handleInput"
/>
<input v-model="computedValue" type="text" :name="props.name" autocomplete="off" :class="inputElClass"
placeholder="Search Keywords..." required @input="handleInput" />
<!-- v-model="data.search" -->
<svg
class="w-4 h-4 absolute left-2.5 top-3.5"
v-show="computedValue.length < 2"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
<svg class="w-4 h-4 absolute left-2.5 top-3.5" v-show="computedValue.length < 2"
xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
<svg
class="w-4 h-4 absolute left-2.5 top-3.5"
v-show="computedValue.length >= 2"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
@click="
() => {
<svg class="w-4 h-4 absolute left-2.5 top-3.5" v-show="computedValue.length >= 2"
xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" @click="() => {
computedValue = '';
data.isOpen = false;
}
"
>
">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
</div>
<slot />
</div>
<ul
v-if="data.isOpen"
class="absolute absolute z-[1000] float-left m-0 list-none bg-white dark:bg-slate-800 m-0 max-h-32 overflow-y-auto scroll-smooth min-w-full"
>
<li
class="leading-3 pl-4 py-3 border-b-2 line border-gray-100 relative cursor-pointer hover:bg-yellow-50 hover:text-gray-900"
v-for="(item, index) in data.results"
@click.prevent="setResult(item)"
:key="index"
>
<ul v-if="data.isOpen"
class="absolute absolute z-[1000] float-left m-0 list-none bg-white dark:bg-slate-800 m-0 max-h-32 overflow-y-auto scroll-smooth min-w-full">
<li class="leading-3 pl-4 py-3 border-b-2 line border-gray-100 relative cursor-pointer hover:bg-yellow-50 hover:text-gray-900"
v-for="(item, index) in data.results" @click.prevent="setResult(item)" :key="index">
<!-- <a href="${BASE}?uri=${a.s.value}&lang=${USER_LANG}"> -->
<strong class="text-sm"> {{ item.title.value }}</strong>
<!-- </a> -->
@ -406,7 +369,10 @@ function setResult(item) {
computedValue.value = item.title.value;
clear();
// this.$emit('person', person);
emit('subject', language.value);
emit('subject', {
language: language.value,
uri: item.s.value
});
}
function clear() {

View File

@ -8,7 +8,6 @@ import { mdiTrashCan } from '@mdi/js';
import BaseLevel from '@/Components/BaseLevel.vue';
import BaseButtons from '@/Components/BaseButtons.vue';
import BaseButton from '@/Components/BaseButton.vue';
// import Person from 'App/Models/Person';
import { Subject } from '@/Dataset';
// import FormField from '@/Components/FormField.vue';
import FormControl from '@/Components/FormControl.vue';
@ -120,7 +119,7 @@ const removeItem = (key) => {
<UserAvatar :username="client.value" class="w-24 h-24 mx-auto lg:w-6 lg:h-6" />
</td> -->
<td data-label="Type" scope="row">
<FormControl required v-model="item.type" :type="'select'" placeholder="[Enter Language]" :options="props.subjectTypes">
<FormControl required v-model="item.type" @update:modelValue="() => {item.external_key = undefined; item.value= '';}" :type="'select'" placeholder="[Enter Language]" :options="props.subjectTypes">
<div class="text-red-400 text-sm" v-if="errors[`subjects.${index}.type`]">
{{ errors[`subjects.${index}.type`].join(', ') }}
</div>
@ -132,8 +131,9 @@ const removeItem = (key) => {
v-if="item.type !== 'uncontrolled'"
v-model="item.value"
@subject="
(language) => {
item.language = language;
(result) => {
item.language = result.language;
item.external_key = result.uri;
}
"
>

View File

@ -1,4 +1,24 @@
import { Ref } from 'vue';
import { DateTime } from 'luxon';
export interface User {
id: number;
login: string;
email: string;
password: string;
createdAt: DateTime;
updatedAt: DateTime;
roles: Array<Role>;
}
export interface Role {
id: number;
display_name: string;
name: string;
description: string;
created_at: DateTime;
updated_at: DateTime;
}
export interface Dataset {
[key: string]:
@ -75,11 +95,12 @@ export interface TethysFile {
}
export interface Subject {
// id: number;
id?: number;
language: string;
type: string;
value: string;
external_key?: string;
dataset_count: number;
}
export interface DatasetReference {
// id: number;

View File

@ -290,14 +290,15 @@ const submit = async () => {
// this.currentStatus = STATUS_SAVING;
// serrors = [];
// const files = form.files.map((obj) => {
// return new File([obj.blob], obj.label, { type: obj.type, lastModified: obj.lastModified });
// });
const files = form.files.map((obj) => {
return new File([obj.blob], obj.label, { type: obj.type, lastModified: obj.lastModified });
});
// formStep.value++;
await form
.transform((data) => ({
...data,
files: files,
rights: form.rights && form.rights == true ? 'true' : 'false',
}))
.post(route, {
@ -404,7 +405,7 @@ const onMapInitialized = (newItem) => {
adds a new Keyword
*/
const addKeyword = () => {
let newSubject: Subject = { value: 'test', language: '', type: 'uncontrolled' };
let newSubject: Subject = { value: 'test', language: '', type: 'uncontrolled', dataset_count: 0 };
//this.dataset.files.push(uploadedFiles[i]);
form.subjects.push(newSubject);
};

View File

@ -8,11 +8,15 @@
color="white" rounded-full small />
</SectionTitleLineWithButton>
<NotificationBar v-if="flash.message" color="success" :icon="mdiAlertBoxOutline">
{{ flash.message }}
</NotificationBar>
<FormValidationErrors v-bind:errors="errors" />
<!-- max-w-2xl max-width: 42rem; /* 672px */ -->
<!-- <div class="max-w-2xl mx-auto"> -->
<CardBox :form="true">
<FormValidationErrors v-bind:errors="errors" />
<!-- <FormValidationErrors v-bind:errors="errors" /> -->
<div class="mb-4">
<!-- <label for="title" class="block text-gray-700 font-bold mb-2">Title:</label>
<input
@ -218,8 +222,8 @@
<td class="before:hidden lg:w-1 whitespace-nowrap">
<BaseButtons type="justify-start lg:justify-end" no-wrap>
<!-- <BaseButton color="info" :icon="mdiEye" small @click="isModalActive = true" /> -->
<BaseButton color="danger" :icon="mdiTrashCan" small v-if="item.id == undefined"
@click.prevent="removeDescription(index)" />
<BaseButton color="danger" :icon="mdiTrashCan" small
v-if="item.id == undefined" @click.prevent="removeDescription(index)" />
</BaseButtons>
</td>
</tr>
@ -469,7 +473,8 @@
// import { Component, Vue, Prop, Setup, toNative } from 'vue-facing-decorator';
// import AuthLayout from '@/Layouts/Auth.vue';
import LayoutAuthenticated from '@/Layouts/LayoutAuthenticated.vue';
import { useForm, Head } from '@inertiajs/vue3';
import { useForm, Head, usePage } from '@inertiajs/vue3';
import { computed, ComputedRef } from 'vue';
// import { ref } from 'vue';
// import { MainService } from '@/Stores/main';
// import FormInput from '@/Components/FormInput.vue'; // @/Components/FormInput.vue'
@ -502,8 +507,10 @@ import {
mdiTrashCan,
mdiBookOpenPageVariant,
mdiEarthPlus,
mdiAlertBoxOutline,
} from '@mdi/js';
import { notify } from '@/notiwind';
import NotificationBar from '@/Components/NotificationBar.vue';
const props = defineProps({
errors: {
@ -557,6 +564,11 @@ const props = defineProps({
});
const flash: ComputedRef<any> = computed(() => {
// let test = usePage();
// console.log(test);
return usePage().props.flash;
});
// const projects = reactive([]);
@ -639,11 +651,17 @@ const submit = async (): Promise<void> => {
form.licenses = form.licenses.map((obj) => obj.id.toString());
}
// const files = form.files.map((obj) => {
// return new File([obj.blob], obj.label, { type: obj.type, lastModified: obj.lastModified });
// });
const [fileUploads, fileInputs] = form.files?.reduce(
([fileUploads, fileInputs], obj) => {
if (!obj.id) {
// return MultipartFile for file upload
fileUploads[obj.sort_order] = new File([obj.blob], obj.label, { type: obj.type, lastModified: obj.lastModified });
let file = new File([obj.blob], `${obj.label}?sortOrder=${obj.sort_order}`, { type: obj.type, lastModified: obj.lastModified });
// fileUploads[obj.sort_order] = file;
fileUploads.push(file);
} else {
// return normal request input
fileInputs.push(obj);
@ -723,7 +741,7 @@ const onAddContributor = (person) => {
};
const addKeyword = () => {
let newSubject: Subject = { value: 'test', language: '', type: 'uncontrolled' };
let newSubject: Subject = { value: 'test', language: '', type: 'uncontrolled', dataset_count: 0 };
//this.dataset.files.push(uploadedFiles[i]);
form.subjects.push(newSubject);
};
@ -743,7 +761,7 @@ const onMapInitialized = (newItem) => {
};
</script>
<style>
<style scoped>
.max-w-2xl {
max-width: 2xl;
}

View File

@ -118,7 +118,7 @@ const flash: ComputedRef<any> = computed(() => {
:route-name="stardust.route('dataset.release', [dataset.id])" color="info"
:icon="mdiLockOpen" :label="'Release'" small />
<!-- && (dataset.server_state === 'inprogress' || dataset.server_state === 'rejected_editor')" -->
<BaseButton v-if="can.edit" :route-name="stardust.route('dataset.edit', [dataset.id])"
<BaseButton :route-name="stardust.route('dataset.edit', [dataset.id])"
color="info" :icon="mdiSquareEditOutline" :label="'Edit'" small />
<!-- @click="destroy(dataset.id)" -->
<BaseButton v-if="can.delete" color="danger"

View File

@ -216,7 +216,7 @@ class EditComponent extends Vue {
}
public addKeyword() {
let newSubject: Subject = { value: 'test', language: '', type: 'uncontrolled' };
let newSubject: Subject = { value: 'test', language: '', type: 'uncontrolled', dataset_count: 0 };
//this.dataset.files.push(uploadedFiles[i]);
this.form.subjects.push(newSubject);
}

View File

@ -36,6 +36,7 @@ export default [
// route: 'dataset.create',
icon: mdiDatabasePlus,
label: 'Submitter',
permissions: ['submitter'],
children: [
{
route: 'dataset.list',

View File

@ -24,8 +24,9 @@ Inertia.share({
},
// params: ({ params }) => params,
authUser: ({ auth }: HttpContextContract) => {
authUser: async ({ auth }: HttpContextContract) => {
if (auth.user) {
await auth.user.load('roles');
return auth.user;
// {
// 'id': auth.user.id,

View File

@ -94,6 +94,8 @@ Route.post('/app/login', 'Auth/AuthController.login').as('login.store');
// Route.post("/signup", "AuthController.signup");
Route.post('/signout', 'Auth/AuthController.logout').as('logout');
// administrator
Route.group(() => {
Route.get('/settings', async ({ inertia }) => {
return inertia.render('Admin/Settings');
@ -133,6 +135,10 @@ Route.group(() => {
// .middleware(['auth', 'can:dataset-list,dataset-publish']);
.middleware(['auth', 'is:administrator,moderator']);
Route.get('/edit-account-info', 'UsersController.accountInfo')
.as('admin.account.info')
.namespace('App/Controllers/Http/Admin')
@ -145,9 +151,10 @@ Route.post('/edit-account-info/store/:id', 'UsersController.accountInfoStore')
.middleware(['auth']);
// Route::post('change-password', 'UserController@changePasswordStore')->name('admin.account.password.store');
// submitter:
Route.group(() => {
// Route.get('/user', 'UsersController.index').as('user.index');
Route.get('/dataset', 'DatasetController.index').as('dataset.list').middleware(['auth']); //.middleware(['can:dataset-list']);
Route.get('/dataset', 'DatasetController.index').as('dataset.list').middleware(['auth', 'can:dataset-list']);
Route.get('/dataset/create', 'DatasetController.create').as('dataset.create').middleware(['auth', 'can:dataset-submit']);
Route.post('/dataset/first/first-step', 'DatasetController.firstStep')
.as('dataset.first.step')
@ -158,25 +165,23 @@ Route.group(() => {
Route.post('/dataset/second/third-step', 'DatasetController.thirdStep')
.as('dataset.third.step')
.middleware(['auth', 'can:dataset-submit']);
Route.post('/dataset/submit', 'DatasetController.store').as('dataset.submit').middleware(['auth', 'can:dataset-submit']);
Route.get('/dataset/:id/release', 'DatasetController.release')
.as('dataset.release')
.where('id', Route.matchers.number())
.middleware(['auth']); //, 'can:dataset-submit']);
.middleware(['auth', 'can:dataset-edit']);
Route.put('/dataset/:id/releaseupdate', 'DatasetController.releaseUpdate')
.as('dataset.releaseUpdate')
.middleware(['auth', 'can:dataset-submit']);
.middleware(['auth', 'can:dataset-edit']);
Route.get('/dataset/:id/edit', 'DatasetController.edit')
.as('dataset.edit')
.where('id', Route.matchers.number())
.middleware(['auth', 'can:dataset-submit']);
.middleware(['auth', 'can:dataset-edit']);
Route.put('/dataset/:id/update', 'DatasetController.update')
.as('dataset.update')
.where('id', Route.matchers.number())
.middleware(['auth', 'can:dataset-submit']);
.middleware(['auth', 'can:dataset-edit']);
Route.get('/dataset/:id/delete', 'DatasetController.delete').as('dataset.delete').middleware(['auth', 'can:dataset-delete']);
Route.put('/dataset/:id/deleteupdate', 'DatasetController.deleteUpdate')
@ -195,7 +200,7 @@ Route.group(() => {
// .middleware(['auth', 'is:submitter']);
Route.group(() => {
Route.put('/dataset/:id/update', 'DatasetsController.update').as('editor.dataset.update').middleware(['auth', 'can:dataset-submit']);
Route.put('/dataset/:id/update', 'DatasetsController.update').as('editor.dataset.update').middleware(['auth', 'can:dataset-editor-edit']);
})
.namespace('App/Controllers/Http/Editor')
.prefix('editor');

View File

@ -30,12 +30,22 @@
"esModuleInterop": true, //neu
"allowSyntheticDefaultImports": true, //neu,
"paths": {
"App/*": ["./app/*"],
"Config/*": ["./config/*"],
"Contracts/*": ["./contracts/*"],
"Database/*": ["./database/*"],
"App/*": [
"./app/*"
],
"Config/*": [
"./config/*"
],
"Contracts/*": [
"./contracts/*"
],
"Database/*": [
"./database/*"
],
// "@/*": ["./resources/js/"],
"@/*": ["./resources/js/*"]
"@/*": [
"./resources/js/*"
]
// "vue$": ["vue/dist/vue.runtime.esm-bundler.js"],
},
"types": [
@ -48,8 +58,11 @@
"@eidellev/inertia-adonisjs",
"naive-ui/volar",
"@adonisjs/lucid",
"@adonisjs/auth"
"@adonisjs/auth",
"@adonisjs/redis"
]
},
"files": ["index.d.ts"]
"files": [
"index.d.ts"
]
}