mirror of
https://github.com/chenasraf/nextcloud-jukebox.git
synced 2026-05-18 01:39:00 +00:00
feat: db migration + save tracks to db
This commit is contained in:
@@ -11,6 +11,7 @@ use OCP\AppFramework\Bootstrap\IRegistrationContext;
|
||||
|
||||
class Application extends App implements IBootstrap {
|
||||
public const APP_ID = 'jukebox';
|
||||
public const PREFIX = 'jukebox_';
|
||||
public const DIST_DIR = '../dist';
|
||||
public const JS_DIR = self::DIST_DIR . '/js';
|
||||
public const CSS_DIR = self::DIST_DIR . '/css';
|
||||
@@ -24,6 +25,10 @@ class Application extends App implements IBootstrap {
|
||||
include_once __DIR__ . '/../../vendor/autoload.php';
|
||||
}
|
||||
|
||||
public static function tableName(string $name): string {
|
||||
return Application::PREFIX . $name;
|
||||
}
|
||||
|
||||
public function boot(IBootContext $context): void {
|
||||
}
|
||||
}
|
||||
|
||||
87
lib/Db/JukeboxMedia.php
Normal file
87
lib/Db/JukeboxMedia.php
Normal file
@@ -0,0 +1,87 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
// SPDX-FileCopyrightText: Chen Asraf <contact@casraf.dev>
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
namespace OCA\Jukebox\Db;
|
||||
|
||||
use JsonSerializable;
|
||||
use OCP\AppFramework\Db\Entity;
|
||||
|
||||
/**
|
||||
* @method int getId()
|
||||
* @method void setId(int $id)
|
||||
* @method string getMediaType()
|
||||
* @method void setMediaType(string $mediaType)
|
||||
* @method string getPath()
|
||||
* @method void setPath(string $path)
|
||||
* @method string|null getTitle()
|
||||
* @method void setTitle(?string $title)
|
||||
* @method int|null getTrackNumber()
|
||||
* @method void setTrackNumber(?int $trackNumber)
|
||||
* @method string|null getArtist()
|
||||
* @method void setArtist(?string $artist)
|
||||
* @method string|null getAlbum()
|
||||
* @method void setAlbum(?string $album)
|
||||
* @method string|null getAlbumArtist()
|
||||
* @method void setAlbumArtist(?string $albumArtist)
|
||||
* @method int|null getDuration()
|
||||
* @method void setDuration(?int $duration)
|
||||
* @method string|null getAlbumArt()
|
||||
* @method void setAlbumArt(?string $albumArt)
|
||||
* @method string|null getGenre()
|
||||
* @method void setGenre(?string $genre)
|
||||
* @method int|null getYear()
|
||||
* @method void setYear(?int $year)
|
||||
* @method int|null getBitrate()
|
||||
* @method void setBitrate(?int $bitrate)
|
||||
* @method string|null getCodec()
|
||||
* @method void setCodec(?string $codec)
|
||||
* @method string getUserId()
|
||||
* @method void setUserId(string $userId)
|
||||
* @method int getMtime()
|
||||
* @method void setMtime(int $mtime)
|
||||
* @method string|null getRawId3()
|
||||
* @method void setRawId3(?string $rawId3)
|
||||
*/
|
||||
class JukeboxMedia extends Entity implements JsonSerializable {
|
||||
protected string $mediaType = 'track';
|
||||
protected string $path = '';
|
||||
protected ?string $title = null;
|
||||
protected ?int $trackNumber = null;
|
||||
protected ?string $artist = null;
|
||||
protected ?string $album = null;
|
||||
protected ?string $albumArtist = null;
|
||||
protected ?int $duration = null;
|
||||
protected ?string $albumArt = null;
|
||||
protected ?string $genre = null;
|
||||
protected ?int $year = null;
|
||||
protected ?int $bitrate = null;
|
||||
protected ?string $codec = null;
|
||||
protected string $userId = '';
|
||||
protected int $mtime = 0;
|
||||
protected ?string $rawId3 = null;
|
||||
|
||||
public function jsonSerialize(): array {
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'mediaType' => $this->mediaType,
|
||||
'path' => $this->path,
|
||||
'title' => $this->title,
|
||||
'trackNumber' => $this->trackNumber,
|
||||
'artist' => $this->artist,
|
||||
'album' => $this->album,
|
||||
'albumArtist' => $this->albumArtist,
|
||||
'duration' => $this->duration,
|
||||
'albumArt' => $this->albumArt,
|
||||
'genre' => $this->genre,
|
||||
'year' => $this->year,
|
||||
'bitrate' => $this->bitrate,
|
||||
'codec' => $this->codec,
|
||||
'userId' => $this->userId,
|
||||
'mtime' => $this->mtime,
|
||||
'rawId3' => $this->rawId3,
|
||||
];
|
||||
}
|
||||
}
|
||||
162
lib/Db/JukeboxMediaMapper.php
Normal file
162
lib/Db/JukeboxMediaMapper.php
Normal file
@@ -0,0 +1,162 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
// SPDX-FileCopyrightText: Chen Asraf <contact@casraf.dev>
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
namespace OCA\Jukebox\Db;
|
||||
|
||||
use OCA\Jukebox\AppInfo\Application;
|
||||
use OCP\AppFramework\Db\DoesNotExistException;
|
||||
use OCP\AppFramework\Db\QBMapper;
|
||||
use OCP\DB\QueryBuilder\IQueryBuilder;
|
||||
use OCP\IDBConnection;
|
||||
|
||||
/**
|
||||
* @template-extends QBMapper<JukeboxMedia>
|
||||
*/
|
||||
class JukeboxMediaMapper extends QBMapper {
|
||||
public function __construct(
|
||||
IDBConnection $db,
|
||||
) {
|
||||
parent::__construct($db, Application::tableName('media'), JukeboxMedia::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \OCP\AppFramework\Db\MultipleObjectsReturnedException
|
||||
* @throws DoesNotExistException
|
||||
*/
|
||||
public function find(string $id): JukeboxMedia {
|
||||
$qb = $this->db->getQueryBuilder();
|
||||
$qb->select('*')
|
||||
->from($this->getTableName())
|
||||
->where(
|
||||
$qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT))
|
||||
);
|
||||
return $this->findEntity($qb);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<JukeboxMedia>
|
||||
*/
|
||||
public function findAll(): array {
|
||||
$qb = $this->db->getQueryBuilder();
|
||||
$qb->select('*')->from($this->getTableName());
|
||||
return $this->findEntities($qb);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $userId
|
||||
* @param string $mediaType
|
||||
* @return array<JukeboxMedia>
|
||||
*/
|
||||
public function findByMediaType(string $userId, string $mediaType): array {
|
||||
$qb = $this->db->getQueryBuilder();
|
||||
$qb->select('*')
|
||||
->from($this->getTableName())
|
||||
->where(
|
||||
$qb->expr()->andX(
|
||||
$qb->expr()->eq('user_id', $qb->createNamedParameter($userId)),
|
||||
$qb->expr()->eq('media_type', $qb->createNamedParameter($mediaType))
|
||||
)
|
||||
);
|
||||
return $this->findEntities($qb);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $userId
|
||||
* @param string|null $mediaType
|
||||
* @param string $query
|
||||
* @return array<JukeboxMedia>
|
||||
*/
|
||||
public function searchMedia(string $userId, ?string $mediaType, string $query): array {
|
||||
$qb = $this->db->getQueryBuilder();
|
||||
$expr = $qb->expr();
|
||||
|
||||
$searchExpr = $expr->orX(
|
||||
$expr->iLike('title', $qb->createNamedParameter('%' . $query . '%')),
|
||||
$expr->iLike('artist', $qb->createNamedParameter('%' . $query . '%')),
|
||||
$expr->iLike('album', $qb->createNamedParameter('%' . $query . '%'))
|
||||
);
|
||||
|
||||
$conditions = [
|
||||
$expr->eq('user_id', $qb->createNamedParameter($userId)),
|
||||
$searchExpr,
|
||||
];
|
||||
|
||||
if ($mediaType !== null) {
|
||||
$conditions[] = $expr->eq('media_type', $qb->createNamedParameter($mediaType));
|
||||
}
|
||||
|
||||
$qb->select('*')
|
||||
->from($this->getTableName())
|
||||
->where($expr->andX(...$conditions));
|
||||
|
||||
return $this->findEntities($qb);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $userId
|
||||
* @param string $album
|
||||
* @return array<JukeboxMedia>
|
||||
*/
|
||||
public function findByAlbum(string $userId, string $album): array {
|
||||
$qb = $this->db->getQueryBuilder();
|
||||
$qb->select('*')
|
||||
->from($this->getTableName())
|
||||
->where(
|
||||
$qb->expr()->andX(
|
||||
$qb->expr()->eq('user_id', $qb->createNamedParameter($userId)),
|
||||
$qb->expr()->eq('album', $qb->createNamedParameter($album))
|
||||
)
|
||||
);
|
||||
return $this->findEntities($qb);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $userId
|
||||
* @param string $artist
|
||||
* @return array<JukeboxMedia>
|
||||
*/
|
||||
public function findByArtist(string $userId, string $artist): array {
|
||||
$qb = $this->db->getQueryBuilder();
|
||||
$expr = $qb->expr();
|
||||
|
||||
$qb->select('*')
|
||||
->from($this->getTableName())
|
||||
->where(
|
||||
$expr->andX(
|
||||
$expr->eq('user_id', $qb->createNamedParameter($userId)),
|
||||
$expr->orX(
|
||||
$expr->eq('artist', $qb->createNamedParameter($artist)),
|
||||
$expr->eq('album_artist', $qb->createNamedParameter($artist))
|
||||
)
|
||||
)
|
||||
);
|
||||
return $this->findEntities($qb);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $userId
|
||||
* @param string $path
|
||||
* @return JukeboxMedia|null
|
||||
*/
|
||||
public function findByUserIdAndPath(string $userId, string $path): ?JukeboxMedia {
|
||||
$qb = $this->db->getQueryBuilder();
|
||||
$qb->select('*')
|
||||
->from($this->getTableName())
|
||||
->where(
|
||||
$qb->expr()->andX(
|
||||
$qb->expr()->eq('user_id', $qb->createNamedParameter($userId)),
|
||||
$qb->expr()->eq('path', $qb->createNamedParameter($path))
|
||||
)
|
||||
)
|
||||
->setMaxResults(1);
|
||||
|
||||
try {
|
||||
return $this->findEntity($qb);
|
||||
} catch (DoesNotExistException) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
141
lib/Migration/Version1Date20250607001010.php
Normal file
141
lib/Migration/Version1Date20250607001010.php
Normal file
@@ -0,0 +1,141 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* SPDX-FileCopyrightText: Chen Asraf <casraf@pm.me>
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\Jukebox\Migration;
|
||||
|
||||
use Closure;
|
||||
use OCP\DB\ISchemaWrapper;
|
||||
use OCP\Migration\IOutput;
|
||||
use OCP\Migration\SimpleMigrationStep;
|
||||
|
||||
class Version1Date20250607001010 extends SimpleMigrationStep {
|
||||
/**
|
||||
* @param IOutput $output
|
||||
* @param Closure(): ISchemaWrapper $schemaClosure
|
||||
* @param array $options
|
||||
*/
|
||||
public function preSchemaChange(IOutput $output, Closure $schemaClosure, array $options): void {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param IOutput $output
|
||||
* @param Closure(): ISchemaWrapper $schemaClosure
|
||||
* @param array $options
|
||||
* @return null|ISchemaWrapper
|
||||
*/
|
||||
public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper {
|
||||
$schema = $schemaClosure();
|
||||
|
||||
if ($schema->hasTable('jukebox_media')) {
|
||||
$schema->dropTable('jukebox_media');
|
||||
}
|
||||
|
||||
$table = $schema->createTable('jukebox_media');
|
||||
|
||||
$table->addColumn('id', 'integer', [
|
||||
'autoincrement' => true,
|
||||
'notnull' => true,
|
||||
]);
|
||||
|
||||
$table->addColumn('media_type', 'string', [
|
||||
'length' => 20,
|
||||
'notnull' => true,
|
||||
'default' => 'track',
|
||||
'comment' => 'track, podcast, audiobook, video',
|
||||
]);
|
||||
|
||||
$table->addColumn('path', 'string', [
|
||||
'notnull' => true,
|
||||
'length' => 1024,
|
||||
]);
|
||||
|
||||
$table->addColumn('title', 'string', [
|
||||
'notnull' => false,
|
||||
'length' => 255,
|
||||
]);
|
||||
|
||||
$table->addColumn('track_number', 'integer', [
|
||||
'notnull' => false,
|
||||
]);
|
||||
|
||||
$table->addColumn('artist', 'string', [
|
||||
'notnull' => false,
|
||||
'length' => 255,
|
||||
]);
|
||||
|
||||
$table->addColumn('album', 'string', [
|
||||
'notnull' => false,
|
||||
'length' => 255,
|
||||
]);
|
||||
|
||||
$table->addColumn('album_artist', 'string', [
|
||||
'notnull' => false,
|
||||
'length' => 255,
|
||||
]);
|
||||
|
||||
$table->addColumn('duration', 'integer', [
|
||||
'notnull' => false,
|
||||
'comment' => 'Duration in seconds',
|
||||
]);
|
||||
|
||||
$table->addColumn('album_art', 'text', [
|
||||
'notnull' => false,
|
||||
'comment' => 'Path or encoded blob of album artwork',
|
||||
]);
|
||||
|
||||
$table->addColumn('genre', 'string', [
|
||||
'notnull' => false,
|
||||
'length' => 255,
|
||||
]);
|
||||
|
||||
$table->addColumn('year', 'smallint', [
|
||||
'notnull' => false,
|
||||
]);
|
||||
|
||||
$table->addColumn('bitrate', 'integer', [
|
||||
'notnull' => false,
|
||||
'comment' => 'In kbps',
|
||||
]);
|
||||
|
||||
$table->addColumn('codec', 'string', [
|
||||
'notnull' => false,
|
||||
'length' => 100,
|
||||
]);
|
||||
|
||||
$table->addColumn('user_id', 'string', [
|
||||
'notnull' => true,
|
||||
'length' => 64,
|
||||
]);
|
||||
|
||||
$table->addColumn('mtime', 'bigint', [
|
||||
'notnull' => true,
|
||||
'comment' => 'File modified time',
|
||||
]);
|
||||
|
||||
$table->addColumn('raw_id3', 'text', [
|
||||
'notnull' => false,
|
||||
'comment' => 'Raw ID3 metadata as JSON',
|
||||
]);
|
||||
|
||||
$table->setPrimaryKey(['id']);
|
||||
$table->addIndex(['user_id'], 'media_user_idx');
|
||||
$table->addIndex(['media_type'], 'media_type_idx');
|
||||
$table->addIndex(['path'], 'media_path_idx');
|
||||
|
||||
return $schema;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param IOutput $output
|
||||
* @param Closure(): ISchemaWrapper $schemaClosure
|
||||
* @param array $options
|
||||
*/
|
||||
public function postSchemaChange(IOutput $output, Closure $schemaClosure, array $options): void {
|
||||
}
|
||||
}
|
||||
@@ -6,11 +6,14 @@ namespace OCA\Jukebox\Service;
|
||||
|
||||
use getID3;
|
||||
use OCA\Jukebox\AppInfo\Application;
|
||||
use OCA\Jukebox\Db\JukeboxMedia;
|
||||
use OCA\Jukebox\Db\JukeboxMediaMapper;
|
||||
use OCP\Files\File;
|
||||
use OCP\Files\Folder;
|
||||
use OCP\Files\IRootFolder;
|
||||
use OCP\Files\NotFoundException;
|
||||
use OCP\IAppConfig;
|
||||
use OCP\IDBConnection;
|
||||
use OCP\IUserSession;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
@@ -28,6 +31,8 @@ class MusicScanner {
|
||||
IUserSession $userSession,
|
||||
private LoggerInterface $logger,
|
||||
private IAppConfig $appConfig,
|
||||
private JukeboxMediaMapper $mediaMapper,
|
||||
private IDBConnection $db,
|
||||
) {
|
||||
$this->rootFolder = $rootFolder;
|
||||
$this->userSession = $userSession;
|
||||
@@ -57,6 +62,7 @@ class MusicScanner {
|
||||
*/
|
||||
public function scanUserByUID(string $uid): void {
|
||||
try {
|
||||
$this->db->beginTransaction();
|
||||
$userFolder = $this->rootFolder->getUserFolder($uid);
|
||||
|
||||
$relativePath = $this->appConfig->getValueString(Application::APP_ID, 'music_folder_path_' . $uid, 'Music');
|
||||
@@ -70,8 +76,12 @@ class MusicScanner {
|
||||
|
||||
$this->logger->info("Starting music scan for user '$uid' in folder '$relativePath'");
|
||||
$this->traverseFolder($musicFolder, $uid);
|
||||
$this->db->commit();
|
||||
} catch (NotFoundException $e) {
|
||||
$this->logger->error("Could not find music folder for user $uid: " . $e->getMessage());
|
||||
} catch (\Throwable $e) {
|
||||
$this->db->rollBack();
|
||||
$this->logger->error('Scan failed: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,16 +124,11 @@ class MusicScanner {
|
||||
return;
|
||||
}
|
||||
|
||||
$handle = fopen($tempPath, 'w');
|
||||
if ($handle === false) {
|
||||
$this->logger->error('Failed to open temporary file handle.');
|
||||
return;
|
||||
}
|
||||
|
||||
$stream = $file->fopen('r');
|
||||
if ($stream === false) {
|
||||
fclose($handle);
|
||||
$this->logger->error('Failed to open file stream for ' . $file->getPath());
|
||||
$handle = fopen($tempPath, 'w');
|
||||
|
||||
if ($stream === false || $handle === false) {
|
||||
$this->logger->error('Failed to open file stream or temp handle.');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -133,36 +138,70 @@ class MusicScanner {
|
||||
|
||||
$getID3 = new getID3();
|
||||
$info = $getID3->analyze($tempPath);
|
||||
|
||||
$songArtist = $info['tags']['id3v2']['artist'][0] ?? '';
|
||||
$albumArtist =
|
||||
$info['tags']['id3v2']['band'][0] ??
|
||||
$info['tags']['id3v2']['album_artist'][0] ??
|
||||
$info['tags']['quicktime']['album_artist'][0] ??
|
||||
$info['tags']['asf']['WM/AlbumArtist'][0] ??
|
||||
'';
|
||||
$album = $info['tags']['id3v2']['album'][0] ?? '';
|
||||
$title = $info['tags']['id3v2']['title'][0] ?? $file->getName();
|
||||
|
||||
$this->logger->info("Scanned metadata for '{$file->getPath()}': Song Artist='{$songArtist}', Album Artist='{$albumArtist}', Album='{$album}', Title='{$title}'");
|
||||
|
||||
unlink($tempPath);
|
||||
|
||||
$this->saveMetadataToDatabase($uid, $file->getId(), $songArtist, $album, $title);
|
||||
$this->saveMetadataToDatabase($uid, $file, $info);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Placeholder method to save music metadata.
|
||||
*
|
||||
* @param string $userId
|
||||
* @param int $fileId
|
||||
* @param string $artist
|
||||
* @param string $album
|
||||
* @param string $title
|
||||
* @param File $fileId
|
||||
* @param array $info
|
||||
* @return void
|
||||
*/
|
||||
private function saveMetadataToDatabase(string $userId, int $fileId, string $artist, string $album, string $title): void {
|
||||
// TODO: Implement database saving logic
|
||||
private function saveMetadataToDatabase(string $userId, File $file, array $info): void {
|
||||
try {
|
||||
$path = $file->getPath();
|
||||
$mtime = $file->getMTime();
|
||||
|
||||
$title = $info['tags']['id3v2']['title'][0]
|
||||
?? $file->getName();
|
||||
|
||||
$trackArtist = $info['tags']['id3v2']['artist'][0] ?? '';
|
||||
$albumArtist =
|
||||
$info['tags']['id3v2']['band'][0]
|
||||
?? $info['tags']['id3v2']['album_artist'][0]
|
||||
?? $info['tags']['quicktime']['album_artist'][0]
|
||||
?? $info['tags']['asf']['WM/AlbumArtist'][0]
|
||||
?? '';
|
||||
$album = $info['tags']['id3v2']['album'][0] ?? '';
|
||||
|
||||
// Check for existing
|
||||
$existing = $this->mediaMapper->findByUserIdAndPath($userId, $path);
|
||||
$media = $existing ?? new JukeboxMedia();
|
||||
|
||||
$media->setUserId($userId);
|
||||
$media->setPath($path);
|
||||
$media->setMtime($mtime);
|
||||
$media->setMediaType('track');
|
||||
$media->setTitle($title);
|
||||
$media->setArtist($trackArtist);
|
||||
$media->setAlbumArtist($albumArtist);
|
||||
$media->setAlbum($album);
|
||||
|
||||
$media->setTrackNumber($info['tags']['id3v2']['track_number'][0] ?? null);
|
||||
$media->setDuration((int)($info['playtime_seconds'] ?? 0));
|
||||
$media->setGenre($info['tags']['id3v2']['genre'][0] ?? null);
|
||||
$media->setYear((int)($info['tags']['id3v2']['year'][0] ?? 0));
|
||||
$media->setBitrate((int)($info['audio']['bitrate'] ?? 0) / 1000);
|
||||
$media->setCodec($info['audio']['dataformat'] ?? null);
|
||||
|
||||
$rawId3 = json_encode($info);
|
||||
if ($rawId3 !== false) {
|
||||
$media->setRawId3($rawId3);
|
||||
}
|
||||
|
||||
if ($existing) {
|
||||
$this->mediaMapper->update($media);
|
||||
} else {
|
||||
$this->mediaMapper->insert($media);
|
||||
}
|
||||
|
||||
$this->logger->info("Saved metadata for '$path'");
|
||||
} catch (\Throwable $e) {
|
||||
$this->logger->error("Failed to save metadata for file '{$file->getPath()}': " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ const { format } = require('date-fns')
|
||||
const fs = require('node:fs')
|
||||
|
||||
function getLatestMigration() {
|
||||
const migrationDir = 'lib/migration'
|
||||
const migrationDir = 'lib/Migration'
|
||||
const files = fs.readdirSync(migrationDir)
|
||||
const migrationFiles = files.sort((a, b) => a.localeCompare(b))
|
||||
const latestMigration = migrationFiles[migrationFiles.length - 1]
|
||||
@@ -17,6 +17,7 @@ function getLatestMigration() {
|
||||
|
||||
// eslint-disable-next-line no-undef
|
||||
module.exports = () => {
|
||||
const latestMigrationVersion = getLatestMigration()
|
||||
return {
|
||||
component: {
|
||||
templates: ['gen/component'],
|
||||
@@ -63,17 +64,14 @@ module.exports = () => {
|
||||
output: 'lib/Controller',
|
||||
subDir: false,
|
||||
},
|
||||
migration: () => {
|
||||
const latestMigrationVersion = getLatestMigration()
|
||||
return {
|
||||
templates: ['gen/migration'],
|
||||
output: 'lib/Migration',
|
||||
name: '-',
|
||||
data: {
|
||||
version: latestMigrationVersion,
|
||||
dt: format(new Date(), 'yyyyMMddHHmmss'),
|
||||
},
|
||||
}
|
||||
migration: {
|
||||
templates: ['gen/migration'],
|
||||
output: 'lib/Migration',
|
||||
name: '-',
|
||||
data: {
|
||||
version: latestMigrationVersion,
|
||||
dt: format(new Date(), 'yyyyMMddHHmmss'),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user