mirror of
https://github.com/usememos/memos.git
synced 2025-01-26 15:12:10 +08:00
chore: update database migrator
This commit is contained in:
parent
fd8333eeda
commit
d67eaaaee2
21 changed files with 71 additions and 385 deletions
|
@ -1,113 +0,0 @@
|
|||
# Documenting the API
|
||||
|
||||
## Principles
|
||||
|
||||
- The documentation is generated by [swaggo/swag](https://github.com/swaggo/swag) from comments in the API code.
|
||||
|
||||
- Documentation is written using [Declarative Comments Format](https://github.com/swaggo/swag#declarative-comments-format).
|
||||
|
||||
- The documentation is generated in the `./api/v1` folder as `docs.go`.
|
||||
|
||||
- [echo-swagger](https://github.com/swaggo/echo-swagger) is used to integrate with Echo framework and serve the documentation with [Swagger-UI](https://swagger.io/tools/swagger-ui/) at `http://memos.host:5230/api/index.html`
|
||||
|
||||
## Updating the documentation
|
||||
|
||||
1. Update or add API-related comments in the code. Make sure to follow the [Declarative Comments Format](https://github.com/swaggo/swag#declarative-comments-format):
|
||||
|
||||
```go
|
||||
// signIn godoc
|
||||
//
|
||||
// @Summary Sign-in to memos.
|
||||
// @Tags auth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param body body SignIn true "Sign-in object"
|
||||
// @Success 200 {object} store.User "User information"
|
||||
// @Failure 400 {object} nil "Malformatted signin request"
|
||||
// @Failure 401 {object} nil "Password login is deactivated | Incorrect login credentials, please try again"
|
||||
// @Failure 403 {object} nil "User has been archived with username {username}"
|
||||
// @Failure 500 {object} nil "Failed to find system setting | Failed to unmarshal system setting | Incorrect login credentials, please try again | Failed to generate tokens | Failed to create activity"
|
||||
// @Router /api/v1/auth/signin [POST]
|
||||
func (s *APIV1Service) signIn(c echo.Context) error {
|
||||
...
|
||||
```
|
||||
|
||||
> Sample from [api/v1/auth.go](https://github.com/usememos/memos/tree/main/api/v1/auth.go)
|
||||
> You can check existing comments at [api/v1](https://github.com/usememos/memos/tree/main/api/v1)
|
||||
|
||||
2. Run one of the following provided scripts:
|
||||
|
||||
- Linux: `./scripts/gen-api-v1-docs.sh` (remember to `chmod +x` the script first)
|
||||
- Windows: `./scripts/gen-api-v1-docs.ps1`
|
||||
|
||||
> The scripts will install swag if needed (via go install), then run `swag fmt` and `swag init` commands.
|
||||
|
||||
3. That's it! The documentation is updated. You can check it at `http://memos.host:5230/api/index.html`
|
||||
|
||||
### Extra tips
|
||||
|
||||
- If you reference a custom Go struct from outside the API file, use a relative definition, like `store.IdentityProvider`. This works because `./` is passed to swag at `--dir` argument. If swag can't resolve the reference, it will fail.
|
||||
|
||||
- If the API grows or you need to reference some type from another location, remember to update ./scripts/gen-api-v1-docs.cfg file with the new paths.
|
||||
|
||||
- It's possible to list multiple errors for the same code using enum-like structs, that will show a proper, spec-conformant model with all entries at Swagger-UI. The drawback is that this approach requires a major refactoring and will add a lot of boilerplate code, as there are inconsistencies between API methods error responses.
|
||||
|
||||
```go
|
||||
type signInInternalServerError string
|
||||
|
||||
const signInErrorFailedToFindSystemSetting signInInternalServerError = "Failed to find system setting"
|
||||
const signInErrorFailedToUnmarshalSystemSetting signInInternalServerError = "Failed to unmarshal system setting"
|
||||
const signInErrorIncorrectLoginCredentials signInInternalServerError = "Incorrect login credentials, please try again"
|
||||
const signInErrorFailedToGenerateTokens signInInternalServerError = "Failed to generate tokens"
|
||||
const signInErrorFailedToCreateActivity signInInternalServerError = "Failed to create activity"
|
||||
|
||||
type signInUnauthorized string
|
||||
|
||||
const signInErrorPasswordLoginDeactivated signInUnauthorized = "Password login is deactivated"
|
||||
const signInErrorIncorrectCredentials signInUnauthorized = "Incorrect login credentials, please try again"
|
||||
|
||||
// signIn godoc
|
||||
//
|
||||
// @Summary Sign-in to memos.
|
||||
// @Tags auth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param body body SignIn true "Sign-in object"
|
||||
// @Success 200 {object} store.User "User information"
|
||||
// @Failure 400 {object} nil "Malformatted signin request"
|
||||
// @Failure 401 {object} signInUnauthorized
|
||||
// @Failure 403 {object} nil "User has been archived with username {username}"
|
||||
// @Failure 500 {object} signInInternalServerError
|
||||
// @Router /api/v1/auth/signin [POST]
|
||||
func (s *APIV1Service) signIn(c echo.Context) error {
|
||||
...
|
||||
```
|
||||
|
||||
### Step-by-step (no scripts)
|
||||
|
||||
#### Required tools
|
||||
|
||||
```bash
|
||||
# Swag v1.8.12 or newer
|
||||
# Also updates swag if needed
|
||||
$ go install github.com/swaggo/swag/cmd/swag@latest
|
||||
```
|
||||
|
||||
If `$HOME/go/bin` is not in your `PATH`, you can call `swag` directly at `$HOME/go/bin/swag`.
|
||||
|
||||
#### Generate the documentation
|
||||
|
||||
1. Run `swag fmt` to format the comments
|
||||
|
||||
```bash
|
||||
swag fmt --dir ./api/v1 && go fmt
|
||||
```
|
||||
|
||||
2. Run `swag init` to generate the documentation
|
||||
|
||||
```bash
|
||||
cd <project-root>
|
||||
swag init --output ./api/v1 --generalInfo ./api/v1/v1.go --dir ./,./api/v1
|
||||
```
|
||||
|
||||
> If the API gets a new version, you'll need to add the file system path to swag's `--dir` parameter.
|
|
@ -1,98 +0,0 @@
|
|||
# Installing memos as a service on Windows
|
||||
|
||||
While memos first-class support is for Docker, you may also install memos as a Windows service. It will run under SYSTEM account and start automatically at system boot.
|
||||
|
||||
❗ All service management methods requires admin privileges. Use [gsudo](https://gerardog.github.io/gsudo/docs/install), or open a new PowerShell terminal as admin:
|
||||
|
||||
```powershell
|
||||
Start-Process powershell -Verb RunAs
|
||||
```
|
||||
|
||||
## Choose one of the following methods
|
||||
|
||||
### 1. Using [NSSM](https://nssm.cc/download)
|
||||
|
||||
NSSM is a lightweight service wrapper.
|
||||
|
||||
You may put `nssm.exe` in the same directory as `memos.exe`, or add its directory to your system PATH. Prefer the latest 64-bit version of `nssm.exe`.
|
||||
|
||||
```powershell
|
||||
# Install memos as a service
|
||||
nssm install memos "C:\path\to\memos.exe" --mode prod --port 5230
|
||||
|
||||
# Delay auto start
|
||||
nssm set memos DisplayName "memos service"
|
||||
|
||||
# Configure extra service parameters
|
||||
nssm set memos Description "A lightweight, self-hosted memo hub. https://usememos.com/"
|
||||
|
||||
# Delay auto start
|
||||
nssm set memos Start SERVICE_DELAYED_AUTO_START
|
||||
|
||||
# Edit service using NSSM GUI
|
||||
nssm edit memos
|
||||
|
||||
# Start the service
|
||||
nssm start memos
|
||||
|
||||
# Remove the service, if ever needed
|
||||
nssm remove memos confirm
|
||||
```
|
||||
|
||||
### 2. Using [WinSW](https://github.com/winsw/winsw)
|
||||
|
||||
Find the latest release tag and download the asset `WinSW-net46x.exe`. Then, put it in the same directory as `memos.exe` and rename it to `memos-service.exe`.
|
||||
|
||||
Now, in the same directory, create the service configuration file `memos-service.xml`:
|
||||
|
||||
```xml
|
||||
<service>
|
||||
<id>memos</id>
|
||||
<name>memos service</name>
|
||||
<description>A lightweight, self-hosted memo hub. https://usememos.com/</description>
|
||||
<onfailure action="restart" delay="10 sec"/>
|
||||
<executable>%BASE%\memos.exe</executable>
|
||||
<arguments>--mode prod --port 5230</arguments>
|
||||
<delayedAutoStart>true</delayedAutoStart>
|
||||
<log mode="none" />
|
||||
</service>
|
||||
```
|
||||
|
||||
Then, install the service:
|
||||
|
||||
```powershell
|
||||
# Install the service
|
||||
.\memos-service.exe install
|
||||
|
||||
# Start the service
|
||||
.\memos-service.exe start
|
||||
|
||||
# Remove the service, if ever needed
|
||||
.\memos-service.exe uninstall
|
||||
```
|
||||
|
||||
### Manage the service
|
||||
|
||||
You may use the `net` command to manage the service:
|
||||
|
||||
```powershell
|
||||
net start memos
|
||||
net stop memos
|
||||
```
|
||||
|
||||
Also, by using one of the provided methods, the service will appear in the Windows Services Manager `services.msc`.
|
||||
|
||||
## Notes
|
||||
|
||||
- On Windows, memos store its data in the following directory:
|
||||
|
||||
```powershell
|
||||
$env:ProgramData\memos
|
||||
# Typically, this will resolve to C:\ProgramData\memos
|
||||
```
|
||||
|
||||
You may specify a custom directory by appending `--data <path>` to the service command line.
|
||||
|
||||
- If the service fails to start, you should inspect the Windows Event Viewer `eventvwr.msc`.
|
||||
|
||||
- Memos will be accessible at [http://localhost:5230](http://localhost:5230) by default.
|
1
go.mod
1
go.mod
|
@ -16,6 +16,7 @@ require (
|
|||
github.com/gorilla/feeds v1.1.2
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0
|
||||
github.com/improbable-eng/grpc-web v0.15.0
|
||||
github.com/joho/godotenv v1.5.1
|
||||
github.com/labstack/echo/v4 v4.11.4
|
||||
github.com/lib/pq v1.10.9
|
||||
github.com/microcosm-cc/bluemonday v1.0.26
|
||||
|
|
2
go.sum
2
go.sum
|
@ -249,6 +249,8 @@ github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9Y
|
|||
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
|
||||
github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
|
||||
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
|
||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
|
||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
|
|
|
@ -24,7 +24,7 @@ func (d *DB) CreateActivity(ctx context.Context, create *store.Activity) (*store
|
|||
placeholder := []string{"?", "?", "?", "?"}
|
||||
args := []any{create.CreatorID, create.Type.String(), create.Level.String(), payloadString}
|
||||
|
||||
stmt := "INSERT INTO activity (" + strings.Join(fields, ", ") + ") VALUES (" + strings.Join(placeholder, ", ") + ")"
|
||||
stmt := "INSERT INTO `activity` (" + strings.Join(fields, ", ") + ") VALUES (" + strings.Join(placeholder, ", ") + ")"
|
||||
result, err := d.db.ExecContext(ctx, stmt, args...)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to execute statement")
|
||||
|
|
|
@ -16,7 +16,7 @@ func (d *DB) CreateMemo(ctx context.Context, create *store.Memo) (*store.Memo, e
|
|||
placeholder := []string{"?", "?", "?"}
|
||||
args := []any{create.CreatorID, create.Content, create.Visibility}
|
||||
|
||||
stmt := "INSERT INTO memo (" + strings.Join(fields, ", ") + ") VALUES (" + strings.Join(placeholder, ", ") + ")"
|
||||
stmt := "INSERT INTO `memo` (" + strings.Join(fields, ", ") + ") VALUES (" + strings.Join(placeholder, ", ") + ")"
|
||||
result, err := d.db.ExecContext(ctx, stmt, args...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -89,12 +89,15 @@ func (d *DB) ListMemos(ctx context.Context, find *store.FindMemo) ([]*store.Memo
|
|||
"UNIX_TIMESTAMP(`memo`.`created_ts`) AS `created_ts`",
|
||||
"UNIX_TIMESTAMP(`memo`.`updated_ts`) AS `updated_ts`",
|
||||
"`memo`.`row_status` AS `row_status`",
|
||||
"`memo`.`content` AS `content`",
|
||||
"`memo`.`visibility` AS `visibility`",
|
||||
"`memo_organizer`.`pinned` AS `pinned`",
|
||||
"`memo_relation`.`related_memo_id` AS `parent_id`",
|
||||
}
|
||||
query := "SELECT " + strings.Join(fields, ",\n") + " FROM `memo` LEFT JOIN `memo_organizer` ON `memo`.`id` = `memo_organizer`.`memo_id` AND `memo`.`creator_id` = `memo_organizer`.`user_id` LEFT JOIN `memo_relation` ON `memo`.`id` = `memo_relation`.`memo_id` AND `memo_relation`.`type` = \"COMMENT\" WHERE " + strings.Join(where, " AND ") + " HAVING " + strings.Join(having, " AND ") + " ORDER BY " + strings.Join(orders, ", ")
|
||||
if !find.ExcludeContent {
|
||||
fields = append(fields, "`memo`.`content` AS `content`")
|
||||
}
|
||||
|
||||
query := "SELECT " + strings.Join(fields, ", ") + " FROM `memo` LEFT JOIN `memo_organizer` ON `memo`.`id` = `memo_organizer`.`memo_id` AND `memo`.`creator_id` = `memo_organizer`.`user_id` LEFT JOIN `memo_relation` ON `memo`.`id` = `memo_relation`.`memo_id` AND `memo_relation`.`type` = \"COMMENT\" WHERE " + strings.Join(where, " AND ") + " HAVING " + strings.Join(having, " AND ") + " ORDER BY " + strings.Join(orders, ", ")
|
||||
if find.Limit != nil {
|
||||
query = fmt.Sprintf("%s LIMIT %d", query, *find.Limit)
|
||||
if find.Offset != nil {
|
||||
|
@ -112,17 +115,20 @@ func (d *DB) ListMemos(ctx context.Context, find *store.FindMemo) ([]*store.Memo
|
|||
for rows.Next() {
|
||||
var memo store.Memo
|
||||
pinned := sql.NullBool{}
|
||||
if err := rows.Scan(
|
||||
dests := []any{
|
||||
&memo.ID,
|
||||
&memo.CreatorID,
|
||||
&memo.CreatedTs,
|
||||
&memo.UpdatedTs,
|
||||
&memo.RowStatus,
|
||||
&memo.Content,
|
||||
&memo.Visibility,
|
||||
&pinned,
|
||||
&memo.ParentID,
|
||||
); err != nil {
|
||||
}
|
||||
if !find.ExcludeContent {
|
||||
dests = append(dests, &memo.Content)
|
||||
}
|
||||
if err := rows.Scan(dests...); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if pinned.Valid {
|
||||
|
|
|
@ -1,19 +1,3 @@
|
|||
-- drop all tables first
|
||||
DROP TABLE IF EXISTS `migration_history`;
|
||||
DROP TABLE IF EXISTS `system_setting`;
|
||||
DROP TABLE IF EXISTS `user`;
|
||||
DROP TABLE IF EXISTS `user_setting`;
|
||||
DROP TABLE IF EXISTS `memo`;
|
||||
DROP TABLE IF EXISTS `memo_organizer`;
|
||||
DROP TABLE IF EXISTS `memo_relation`;
|
||||
DROP TABLE IF EXISTS `resource`;
|
||||
DROP TABLE IF EXISTS `tag`;
|
||||
DROP TABLE IF EXISTS `activity`;
|
||||
DROP TABLE IF EXISTS `storage`;
|
||||
DROP TABLE IF EXISTS `idp`;
|
||||
DROP TABLE IF EXISTS `inbox`;
|
||||
DROP TABLE IF EXISTS `webhook`;
|
||||
|
||||
-- migration_history
|
||||
CREATE TABLE `migration_history` (
|
||||
`version` VARCHAR(256) NOT NULL PRIMARY KEY,
|
||||
|
|
|
@ -1,19 +1,3 @@
|
|||
-- drop all tables first
|
||||
DROP TABLE IF EXISTS `migration_history`;
|
||||
DROP TABLE IF EXISTS `system_setting`;
|
||||
DROP TABLE IF EXISTS `user`;
|
||||
DROP TABLE IF EXISTS `user_setting`;
|
||||
DROP TABLE IF EXISTS `memo`;
|
||||
DROP TABLE IF EXISTS `memo_organizer`;
|
||||
DROP TABLE IF EXISTS `memo_relation`;
|
||||
DROP TABLE IF EXISTS `resource`;
|
||||
DROP TABLE IF EXISTS `tag`;
|
||||
DROP TABLE IF EXISTS `activity`;
|
||||
DROP TABLE IF EXISTS `storage`;
|
||||
DROP TABLE IF EXISTS `idp`;
|
||||
DROP TABLE IF EXISTS `inbox`;
|
||||
DROP TABLE IF EXISTS `webhook`;
|
||||
|
||||
-- migration_history
|
||||
CREATE TABLE `migration_history` (
|
||||
`version` VARCHAR(256) NOT NULL PRIMARY KEY,
|
||||
|
|
|
@ -49,6 +49,5 @@ func (d *DB) UpsertMigrationHistory(ctx context.Context, upsert *store.UpsertMig
|
|||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &migrationHistory, nil
|
||||
}
|
||||
|
|
|
@ -15,13 +15,13 @@ import (
|
|||
"github.com/usememos/memos/store"
|
||||
)
|
||||
|
||||
//go:embed migration
|
||||
var migrationFS embed.FS
|
||||
|
||||
const (
|
||||
latestSchemaFileName = "LATEST__SCHEMA.sql"
|
||||
)
|
||||
|
||||
//go:embed migration
|
||||
var migrationFS embed.FS
|
||||
|
||||
func (d *DB) Migrate(ctx context.Context) error {
|
||||
if d.profile.IsDev() {
|
||||
return d.nonProdMigrate(ctx)
|
||||
|
@ -54,8 +54,6 @@ func (d *DB) nonProdMigrate(ctx context.Context) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
println("no tables in the database. start migration")
|
||||
|
||||
buf, err := migrationFS.ReadFile("migration/dev/" + latestSchemaFileName)
|
||||
if err != nil {
|
||||
return errors.Errorf("failed to read latest schema file: %s", err)
|
||||
|
@ -65,13 +63,6 @@ func (d *DB) nonProdMigrate(ctx context.Context) error {
|
|||
if _, err := d.db.ExecContext(ctx, stmt); err != nil {
|
||||
return errors.Errorf("failed to exec SQL %s: %s", stmt, err)
|
||||
}
|
||||
|
||||
// In demo mode, we should seed the database.
|
||||
if d.profile.Mode == "demo" {
|
||||
if err := d.seed(ctx); err != nil {
|
||||
return errors.Wrap(err, "failed to seed")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -84,10 +75,8 @@ func (d *DB) prodMigrate(ctx context.Context) error {
|
|||
if err != nil {
|
||||
return errors.Errorf("failed to read latest schema file: %s", err)
|
||||
}
|
||||
|
||||
stmt := string(buf)
|
||||
if _, err := d.db.ExecContext(ctx, stmt); err != nil {
|
||||
return errors.Errorf("failed to exec SQL %s: %s", stmt, err)
|
||||
if _, err := d.db.ExecContext(ctx, string(buf)); err != nil {
|
||||
return errors.Errorf("failed to exec latest schema: %s", err)
|
||||
}
|
||||
if _, err := d.UpsertMigrationHistory(ctx, &store.UpsertMigrationHistory{
|
||||
Version: currentVersion,
|
||||
|
@ -107,11 +96,11 @@ func (d *DB) prodMigrate(ctx context.Context) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
println("start migrate")
|
||||
println("start to migrate database schema")
|
||||
for _, minorVersion := range getMinorVersionList() {
|
||||
normalizedVersion := minorVersion + ".0"
|
||||
if version.IsVersionGreaterThan(normalizedVersion, latestMigrationHistoryVersion) && version.IsVersionGreaterOrEqualThan(currentVersion, normalizedVersion) {
|
||||
println("applying migration for", normalizedVersion)
|
||||
println("applying migration of", normalizedVersion)
|
||||
if err := d.applyMigrationForMinorVersion(ctx, minorVersion); err != nil {
|
||||
return errors.Wrap(err, "failed to apply minor version migration")
|
||||
}
|
||||
|
@ -153,35 +142,6 @@ func (d *DB) applyMigrationForMinorVersion(ctx context.Context, minorVersion str
|
|||
return nil
|
||||
}
|
||||
|
||||
//go:embed seed
|
||||
var seedFS embed.FS
|
||||
|
||||
func (d *DB) seed(ctx context.Context) error {
|
||||
filenames, err := fs.Glob(seedFS, "seed/*.sql")
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to read seed files")
|
||||
}
|
||||
|
||||
sort.Strings(filenames)
|
||||
// Loop over all seed files and execute them in order.
|
||||
for _, filename := range filenames {
|
||||
buf, err := seedFS.ReadFile(filename)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "failed to read seed file, filename=%s", filename)
|
||||
}
|
||||
|
||||
for _, stmt := range strings.Split(string(buf), ";") {
|
||||
if strings.TrimSpace(stmt) == "" {
|
||||
continue
|
||||
}
|
||||
if _, err := d.db.ExecContext(ctx, stmt); err != nil {
|
||||
return errors.Wrapf(err, "seed error: %s", stmt)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// minorDirRegexp is a regular expression for minor version directory.
|
||||
var minorDirRegexp = regexp.MustCompile(`^migration/prod/[0-9]+\.[0-9]+$`)
|
||||
|
||||
|
|
|
@ -1,4 +0,0 @@
|
|||
TRUNCATE TABLE memo_organizer;
|
||||
TRUNCATE TABLE resource;
|
||||
TRUNCATE TABLE memo;
|
||||
TRUNCATE TABLE user;
|
|
@ -25,7 +25,7 @@ func (d *DB) CreateWebhook(ctx context.Context, create *storepb.Webhook) (*store
|
|||
}
|
||||
|
||||
create.Id = int32(id)
|
||||
return create, nil
|
||||
return d.GetWebhook(ctx, &store.FindWebhook{ID: &create.Id})
|
||||
}
|
||||
|
||||
func (d *DB) ListWebhooks(ctx context.Context, find *store.FindWebhook) ([]*storepb.Webhook, error) {
|
||||
|
@ -37,7 +37,7 @@ func (d *DB) ListWebhooks(ctx context.Context, find *store.FindWebhook) ([]*stor
|
|||
where, args = append(where, "`creator_id` = ?"), append(args, *find.CreatorID)
|
||||
}
|
||||
|
||||
rows, err := d.db.QueryContext(ctx, "SELECT `id`, `created_ts`, `updated_ts`, `row_status`, `creator_id`, `name`, `url` FROM `webhook` WHERE "+strings.Join(where, " AND ")+" ORDER BY `id` DESC",
|
||||
rows, err := d.db.QueryContext(ctx, "SELECT `id`, UNIX_TIMESTAMP(`created_ts`), UNIX_TIMESTAMP(`updated_ts`), `row_status`, `creator_id`, `name`, `url` FROM `webhook` WHERE "+strings.Join(where, " AND ")+" ORDER BY `id` DESC",
|
||||
args...,
|
||||
)
|
||||
if err != nil {
|
||||
|
|
|
@ -181,7 +181,6 @@ func (d *DB) UpdateMemo(ctx context.Context, update *store.UpdateMemo) error {
|
|||
func (d *DB) DeleteMemo(ctx context.Context, delete *store.DeleteMemo) error {
|
||||
where, args := []string{"id = " + placeholder(1)}, []any{delete.ID}
|
||||
stmt := `DELETE FROM memo WHERE ` + strings.Join(where, " AND ")
|
||||
println("stmt", stmt, delete.ID)
|
||||
result, err := d.db.ExecContext(ctx, stmt, args...)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to delete memo")
|
||||
|
|
|
@ -1,19 +1,3 @@
|
|||
-- drop all tables first (PostgreSQL style)
|
||||
DROP TABLE IF EXISTS migration_history CASCADE;
|
||||
DROP TABLE IF EXISTS system_setting CASCADE;
|
||||
DROP TABLE IF EXISTS "user" CASCADE;
|
||||
DROP TABLE IF EXISTS user_setting CASCADE;
|
||||
DROP TABLE IF EXISTS memo CASCADE;
|
||||
DROP TABLE IF EXISTS memo_organizer CASCADE;
|
||||
DROP TABLE IF EXISTS memo_relation CASCADE;
|
||||
DROP TABLE IF EXISTS resource CASCADE;
|
||||
DROP TABLE IF EXISTS tag CASCADE;
|
||||
DROP TABLE IF EXISTS activity CASCADE;
|
||||
DROP TABLE IF EXISTS storage CASCADE;
|
||||
DROP TABLE IF EXISTS idp CASCADE;
|
||||
DROP TABLE IF EXISTS inbox CASCADE;
|
||||
DROP TABLE IF EXISTS webhook CASCADE;
|
||||
|
||||
-- migration_history
|
||||
CREATE TABLE migration_history (
|
||||
version TEXT NOT NULL PRIMARY KEY,
|
||||
|
|
|
@ -1,19 +1,3 @@
|
|||
-- drop all tables first (PostgreSQL style)
|
||||
DROP TABLE IF EXISTS migration_history CASCADE;
|
||||
DROP TABLE IF EXISTS system_setting CASCADE;
|
||||
DROP TABLE IF EXISTS "user" CASCADE;
|
||||
DROP TABLE IF EXISTS user_setting CASCADE;
|
||||
DROP TABLE IF EXISTS memo CASCADE;
|
||||
DROP TABLE IF EXISTS memo_organizer CASCADE;
|
||||
DROP TABLE IF EXISTS memo_relation CASCADE;
|
||||
DROP TABLE IF EXISTS resource CASCADE;
|
||||
DROP TABLE IF EXISTS tag CASCADE;
|
||||
DROP TABLE IF EXISTS activity CASCADE;
|
||||
DROP TABLE IF EXISTS storage CASCADE;
|
||||
DROP TABLE IF EXISTS idp CASCADE;
|
||||
DROP TABLE IF EXISTS inbox CASCADE;
|
||||
DROP TABLE IF EXISTS webhook CASCADE;
|
||||
|
||||
-- migration_history
|
||||
CREATE TABLE migration_history (
|
||||
version TEXT NOT NULL PRIMARY KEY,
|
||||
|
|
|
@ -15,13 +15,13 @@ import (
|
|||
"github.com/usememos/memos/store"
|
||||
)
|
||||
|
||||
//go:embed migration
|
||||
var migrationFS embed.FS
|
||||
|
||||
const (
|
||||
latestSchemaFileName = "LATEST__SCHEMA.sql"
|
||||
)
|
||||
|
||||
//go:embed migration
|
||||
var migrationFS embed.FS
|
||||
|
||||
func (d *DB) Migrate(ctx context.Context) error {
|
||||
if d.profile.IsDev() {
|
||||
return d.nonProdMigrate(ctx)
|
||||
|
@ -54,8 +54,6 @@ func (d *DB) nonProdMigrate(ctx context.Context) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
println("no tables in the database. start migration")
|
||||
|
||||
buf, err := migrationFS.ReadFile("migration/dev/" + latestSchemaFileName)
|
||||
if err != nil {
|
||||
return errors.Errorf("failed to read latest schema file: %s", err)
|
||||
|
@ -66,12 +64,6 @@ func (d *DB) nonProdMigrate(ctx context.Context) error {
|
|||
return errors.Errorf("failed to exec SQL %s: %s", stmt, err)
|
||||
}
|
||||
|
||||
// In demo mode, we should seed the database.
|
||||
if d.profile.Mode == "demo" {
|
||||
if err := d.seed(ctx); err != nil {
|
||||
return errors.Wrap(err, "failed to seed")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -153,35 +145,6 @@ func (d *DB) applyMigrationForMinorVersion(ctx context.Context, minorVersion str
|
|||
return nil
|
||||
}
|
||||
|
||||
//go:embed seed
|
||||
var seedFS embed.FS
|
||||
|
||||
func (d *DB) seed(ctx context.Context) error {
|
||||
filenames, err := fs.Glob(seedFS, "seed/*.sql")
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to read seed files")
|
||||
}
|
||||
|
||||
sort.Strings(filenames)
|
||||
// Loop over all seed files and execute them in order.
|
||||
for _, filename := range filenames {
|
||||
buf, err := seedFS.ReadFile(filename)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "failed to read seed file, filename=%s", filename)
|
||||
}
|
||||
|
||||
for _, stmt := range strings.Split(string(buf), ";") {
|
||||
if strings.TrimSpace(stmt) == "" {
|
||||
continue
|
||||
}
|
||||
if _, err := d.db.ExecContext(ctx, stmt); err != nil {
|
||||
return errors.Wrapf(err, "seed error: %s", stmt)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// minorDirRegexp is a regular expression for minor version directory.
|
||||
var minorDirRegexp = regexp.MustCompile(`^migration/prod/[0-9]+\.[0-9]+$`)
|
||||
|
||||
|
|
|
@ -1,4 +0,0 @@
|
|||
TRUNCATE TABLE memo_organizer;
|
||||
TRUNCATE TABLE resource;
|
||||
TRUNCATE TABLE memo;
|
||||
TRUNCATE TABLE "user";
|
|
@ -49,7 +49,7 @@ func (s *Store) MigrateResourceInternalPath(ctx context.Context) error {
|
|||
migratedCount++
|
||||
}
|
||||
|
||||
if migratedCount > 0 {
|
||||
if migratedCount > 0 && s.Profile.Mode == "prod" {
|
||||
log.Info(fmt.Sprintf("migrated %d local resource paths in %s", migratedCount, time.Since(migrateStartTime)))
|
||||
}
|
||||
return nil
|
||||
|
|
|
@ -30,8 +30,42 @@ func NewTestingStore(ctx context.Context, t *testing.T) *store.Store {
|
|||
}
|
||||
|
||||
func resetTestingDB(ctx context.Context, profile *profile.Profile, dbDriver store.Driver) {
|
||||
if profile.Driver == "postgres" {
|
||||
_, err := dbDriver.GetDB().ExecContext(ctx, `DROP SCHEMA public CASCADE; CREATE SCHEMA public;`)
|
||||
if profile.Driver == "mysql" {
|
||||
_, err := dbDriver.GetDB().ExecContext(ctx, `
|
||||
DROP TABLE IF EXISTS migration_history;
|
||||
DROP TABLE IF EXISTS system_setting;
|
||||
DROP TABLE IF EXISTS user;
|
||||
DROP TABLE IF EXISTS user_setting;
|
||||
DROP TABLE IF EXISTS memo;
|
||||
DROP TABLE IF EXISTS memo_organizer;
|
||||
DROP TABLE IF EXISTS memo_relation;
|
||||
DROP TABLE IF EXISTS resource;
|
||||
DROP TABLE IF EXISTS tag;
|
||||
DROP TABLE IF EXISTS activity;
|
||||
DROP TABLE IF EXISTS storage;
|
||||
DROP TABLE IF EXISTS idp;
|
||||
DROP TABLE IF EXISTS inbox;
|
||||
DROP TABLE IF EXISTS webhook;`)
|
||||
if err != nil {
|
||||
fmt.Printf("failed to reset testing db, error: %+v\n", err)
|
||||
panic(err)
|
||||
}
|
||||
} else if profile.Driver == "postgres" {
|
||||
_, err := dbDriver.GetDB().ExecContext(ctx, `
|
||||
DROP TABLE IF EXISTS migration_history CASCADE;
|
||||
DROP TABLE IF EXISTS system_setting CASCADE;
|
||||
DROP TABLE IF EXISTS "user" CASCADE;
|
||||
DROP TABLE IF EXISTS user_setting CASCADE;
|
||||
DROP TABLE IF EXISTS memo CASCADE;
|
||||
DROP TABLE IF EXISTS memo_organizer CASCADE;
|
||||
DROP TABLE IF EXISTS memo_relation CASCADE;
|
||||
DROP TABLE IF EXISTS resource CASCADE;
|
||||
DROP TABLE IF EXISTS tag CASCADE;
|
||||
DROP TABLE IF EXISTS activity CASCADE;
|
||||
DROP TABLE IF EXISTS storage CASCADE;
|
||||
DROP TABLE IF EXISTS idp CASCADE;
|
||||
DROP TABLE IF EXISTS inbox CASCADE;
|
||||
DROP TABLE IF EXISTS webhook CASCADE;`)
|
||||
if err != nil {
|
||||
fmt.Printf("failed to reset testing db, error: %+v\n", err)
|
||||
panic(err)
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
package teststore
|
|
@ -6,6 +6,8 @@ import (
|
|||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/joho/godotenv"
|
||||
|
||||
"github.com/usememos/memos/server/profile"
|
||||
"github.com/usememos/memos/server/version"
|
||||
)
|
||||
|
@ -24,6 +26,10 @@ func getUnusedPort() int {
|
|||
}
|
||||
|
||||
func GetTestingProfile(t *testing.T) *profile.Profile {
|
||||
if err := godotenv.Load(".env"); err != nil {
|
||||
t.Fatal("failed to load .env file", err)
|
||||
}
|
||||
|
||||
// Get a temporary directory for the test data.
|
||||
dir := t.TempDir()
|
||||
mode := "dev"
|
||||
|
|
Loading…
Reference in a new issue