init commit
This commit is contained in:
83
store/db/sqlite/activity.go
Normal file
83
store/db/sqlite/activity.go
Normal file
@@ -0,0 +1,83 @@
|
||||
package sqlite
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"google.golang.org/protobuf/encoding/protojson"
|
||||
|
||||
storepb "github.com/usememos/memos/proto/gen/store"
|
||||
"github.com/usememos/memos/store"
|
||||
)
|
||||
|
||||
func (d *DB) CreateActivity(ctx context.Context, create *store.Activity) (*store.Activity, error) {
|
||||
payloadString := "{}"
|
||||
if create.Payload != nil {
|
||||
bytes, err := protojson.Marshal(create.Payload)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to marshal activity payload")
|
||||
}
|
||||
payloadString = string(bytes)
|
||||
}
|
||||
|
||||
fields := []string{"`creator_id`", "`type`", "`level`", "`payload`"}
|
||||
placeholder := []string{"?", "?", "?", "?"}
|
||||
args := []any{create.CreatorID, create.Type.String(), create.Level.String(), payloadString}
|
||||
|
||||
stmt := "INSERT INTO activity (" + strings.Join(fields, ", ") + ") VALUES (" + strings.Join(placeholder, ", ") + ") RETURNING `id`, `created_ts`"
|
||||
if err := d.db.QueryRowContext(ctx, stmt, args...).Scan(
|
||||
&create.ID,
|
||||
&create.CreatedTs,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return create, nil
|
||||
}
|
||||
|
||||
func (d *DB) ListActivities(ctx context.Context, find *store.FindActivity) ([]*store.Activity, error) {
|
||||
where, args := []string{"1 = 1"}, []any{}
|
||||
if find.ID != nil {
|
||||
where, args = append(where, "`id` = ?"), append(args, *find.ID)
|
||||
}
|
||||
if find.Type != nil {
|
||||
where, args = append(where, "`type` = ?"), append(args, find.Type.String())
|
||||
}
|
||||
|
||||
query := "SELECT `id`, `creator_id`, `type`, `level`, `payload`, `created_ts` FROM `activity` WHERE " + strings.Join(where, " AND ") + " ORDER BY `created_ts` DESC"
|
||||
rows, err := d.db.QueryContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
list := []*store.Activity{}
|
||||
for rows.Next() {
|
||||
activity := &store.Activity{}
|
||||
var payloadBytes []byte
|
||||
if err := rows.Scan(
|
||||
&activity.ID,
|
||||
&activity.CreatorID,
|
||||
&activity.Type,
|
||||
&activity.Level,
|
||||
&payloadBytes,
|
||||
&activity.CreatedTs,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
payload := &storepb.ActivityPayload{}
|
||||
if err := protojsonUnmarshaler.Unmarshal(payloadBytes, payload); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
activity.Payload = payload
|
||||
list = append(list, activity)
|
||||
}
|
||||
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return list, nil
|
||||
}
|
||||
182
store/db/sqlite/attachment.go
Normal file
182
store/db/sqlite/attachment.go
Normal file
@@ -0,0 +1,182 @@
|
||||
package sqlite
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"google.golang.org/protobuf/encoding/protojson"
|
||||
|
||||
storepb "github.com/usememos/memos/proto/gen/store"
|
||||
"github.com/usememos/memos/store"
|
||||
)
|
||||
|
||||
func (d *DB) CreateAttachment(ctx context.Context, create *store.Attachment) (*store.Attachment, error) {
|
||||
fields := []string{"`uid`", "`filename`", "`blob`", "`type`", "`size`", "`creator_id`", "`memo_id`", "`storage_type`", "`reference`", "`payload`"}
|
||||
placeholder := []string{"?", "?", "?", "?", "?", "?", "?", "?", "?", "?"}
|
||||
storageType := ""
|
||||
if create.StorageType != storepb.AttachmentStorageType_ATTACHMENT_STORAGE_TYPE_UNSPECIFIED {
|
||||
storageType = create.StorageType.String()
|
||||
}
|
||||
payloadString := "{}"
|
||||
if create.Payload != nil {
|
||||
bytes, err := protojson.Marshal(create.Payload)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to marshal attachment payload")
|
||||
}
|
||||
payloadString = string(bytes)
|
||||
}
|
||||
args := []any{create.UID, create.Filename, create.Blob, create.Type, create.Size, create.CreatorID, create.MemoID, storageType, create.Reference, payloadString}
|
||||
|
||||
stmt := "INSERT INTO `resource` (" + strings.Join(fields, ", ") + ") VALUES (" + strings.Join(placeholder, ", ") + ") RETURNING `id`, `created_ts`, `updated_ts`"
|
||||
if err := d.db.QueryRowContext(ctx, stmt, args...).Scan(&create.ID, &create.CreatedTs, &create.UpdatedTs); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return create, nil
|
||||
}
|
||||
|
||||
func (d *DB) ListAttachments(ctx context.Context, find *store.FindAttachment) ([]*store.Attachment, error) {
|
||||
where, args := []string{"1 = 1"}, []any{}
|
||||
|
||||
if v := find.ID; v != nil {
|
||||
where, args = append(where, "`id` = ?"), append(args, *v)
|
||||
}
|
||||
if v := find.UID; v != nil {
|
||||
where, args = append(where, "`uid` = ?"), append(args, *v)
|
||||
}
|
||||
if v := find.CreatorID; v != nil {
|
||||
where, args = append(where, "`creator_id` = ?"), append(args, *v)
|
||||
}
|
||||
if v := find.Filename; v != nil {
|
||||
where, args = append(where, "`filename` = ?"), append(args, *v)
|
||||
}
|
||||
if v := find.FilenameSearch; v != nil {
|
||||
where, args = append(where, "`filename` LIKE ?"), append(args, fmt.Sprintf("%%%s%%", *v))
|
||||
}
|
||||
if v := find.MemoID; v != nil {
|
||||
where, args = append(where, "`memo_id` = ?"), append(args, *v)
|
||||
}
|
||||
if find.HasRelatedMemo {
|
||||
where = append(where, "`memo_id` IS NOT NULL")
|
||||
}
|
||||
if find.StorageType != nil {
|
||||
where, args = append(where, "`storage_type` = ?"), append(args, find.StorageType.String())
|
||||
}
|
||||
|
||||
fields := []string{"`id`", "`uid`", "`filename`", "`type`", "`size`", "`creator_id`", "`created_ts`", "`updated_ts`", "`memo_id`", "`storage_type`", "`reference`", "`payload`"}
|
||||
if find.GetBlob {
|
||||
fields = append(fields, "`blob`")
|
||||
}
|
||||
|
||||
query := fmt.Sprintf("SELECT %s FROM `resource` WHERE %s ORDER BY `updated_ts` DESC", strings.Join(fields, ", "), strings.Join(where, " AND "))
|
||||
if find.Limit != nil {
|
||||
query = fmt.Sprintf("%s LIMIT %d", query, *find.Limit)
|
||||
if find.Offset != nil {
|
||||
query = fmt.Sprintf("%s OFFSET %d", query, *find.Offset)
|
||||
}
|
||||
}
|
||||
|
||||
rows, err := d.db.QueryContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
list := make([]*store.Attachment, 0)
|
||||
for rows.Next() {
|
||||
attachment := store.Attachment{}
|
||||
var memoID sql.NullInt32
|
||||
var storageType string
|
||||
var payloadBytes []byte
|
||||
dests := []any{
|
||||
&attachment.ID,
|
||||
&attachment.UID,
|
||||
&attachment.Filename,
|
||||
&attachment.Type,
|
||||
&attachment.Size,
|
||||
&attachment.CreatorID,
|
||||
&attachment.CreatedTs,
|
||||
&attachment.UpdatedTs,
|
||||
&memoID,
|
||||
&storageType,
|
||||
&attachment.Reference,
|
||||
&payloadBytes,
|
||||
}
|
||||
if find.GetBlob {
|
||||
dests = append(dests, &attachment.Blob)
|
||||
}
|
||||
if err := rows.Scan(dests...); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if memoID.Valid {
|
||||
attachment.MemoID = &memoID.Int32
|
||||
}
|
||||
attachment.StorageType = storepb.AttachmentStorageType(storepb.AttachmentStorageType_value[storageType])
|
||||
payload := &storepb.AttachmentPayload{}
|
||||
if err := protojsonUnmarshaler.Unmarshal(payloadBytes, payload); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
attachment.Payload = payload
|
||||
list = append(list, &attachment)
|
||||
}
|
||||
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return list, nil
|
||||
}
|
||||
|
||||
func (d *DB) UpdateAttachment(ctx context.Context, update *store.UpdateAttachment) error {
|
||||
set, args := []string{}, []any{}
|
||||
|
||||
if v := update.UID; v != nil {
|
||||
set, args = append(set, "`uid` = ?"), append(args, *v)
|
||||
}
|
||||
if v := update.UpdatedTs; v != nil {
|
||||
set, args = append(set, "`updated_ts` = ?"), append(args, *v)
|
||||
}
|
||||
if v := update.Filename; v != nil {
|
||||
set, args = append(set, "`filename` = ?"), append(args, *v)
|
||||
}
|
||||
if v := update.MemoID; v != nil {
|
||||
set, args = append(set, "`memo_id` = ?"), append(args, *v)
|
||||
}
|
||||
if v := update.Reference; v != nil {
|
||||
set, args = append(set, "`reference` = ?"), append(args, *v)
|
||||
}
|
||||
if v := update.Payload; v != nil {
|
||||
bytes, err := protojson.Marshal(v)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to marshal attachment payload")
|
||||
}
|
||||
set, args = append(set, "`payload` = ?"), append(args, string(bytes))
|
||||
}
|
||||
|
||||
args = append(args, update.ID)
|
||||
stmt := "UPDATE `resource` SET " + strings.Join(set, ", ") + " WHERE `id` = ?"
|
||||
result, err := d.db.ExecContext(ctx, stmt, args...)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to update attachment")
|
||||
}
|
||||
if _, err := result.RowsAffected(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *DB) DeleteAttachment(ctx context.Context, delete *store.DeleteAttachment) error {
|
||||
stmt := "DELETE FROM `resource` WHERE `id` = ?"
|
||||
result, err := d.db.ExecContext(ctx, stmt, delete.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := result.RowsAffected(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
9
store/db/sqlite/common.go
Normal file
9
store/db/sqlite/common.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package sqlite
|
||||
|
||||
import "google.golang.org/protobuf/encoding/protojson"
|
||||
|
||||
var (
|
||||
protojsonUnmarshaler = protojson.UnmarshalOptions{
|
||||
DiscardUnknown: true,
|
||||
}
|
||||
)
|
||||
117
store/db/sqlite/idp.go
Normal file
117
store/db/sqlite/idp.go
Normal file
@@ -0,0 +1,117 @@
|
||||
package sqlite
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
storepb "github.com/usememos/memos/proto/gen/store"
|
||||
"github.com/usememos/memos/store"
|
||||
)
|
||||
|
||||
func (d *DB) CreateIdentityProvider(ctx context.Context, create *store.IdentityProvider) (*store.IdentityProvider, error) {
|
||||
placeholders := []string{"?", "?", "?", "?"}
|
||||
fields := []string{"`name`", "`type`", "`identifier_filter`", "`config`"}
|
||||
args := []any{create.Name, create.Type.String(), create.IdentifierFilter, create.Config}
|
||||
|
||||
stmt := "INSERT INTO `idp` (" + strings.Join(fields, ", ") + ") VALUES (" + strings.Join(placeholders, ", ") + ") RETURNING `id`"
|
||||
if err := d.db.QueryRowContext(ctx, stmt, args...).Scan(&create.ID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
identityProvider := create
|
||||
return identityProvider, nil
|
||||
}
|
||||
|
||||
func (d *DB) ListIdentityProviders(ctx context.Context, find *store.FindIdentityProvider) ([]*store.IdentityProvider, error) {
|
||||
where, args := []string{"1 = 1"}, []any{}
|
||||
if v := find.ID; v != nil {
|
||||
where, args = append(where, fmt.Sprintf("id = $%d", len(args)+1)), append(args, *v)
|
||||
}
|
||||
|
||||
rows, err := d.db.QueryContext(ctx, `
|
||||
SELECT
|
||||
id,
|
||||
name,
|
||||
type,
|
||||
identifier_filter,
|
||||
config
|
||||
FROM idp
|
||||
WHERE `+strings.Join(where, " AND ")+` ORDER BY id ASC`,
|
||||
args...,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var identityProviders []*store.IdentityProvider
|
||||
for rows.Next() {
|
||||
var identityProvider store.IdentityProvider
|
||||
var typeString string
|
||||
if err := rows.Scan(
|
||||
&identityProvider.ID,
|
||||
&identityProvider.Name,
|
||||
&typeString,
|
||||
&identityProvider.IdentifierFilter,
|
||||
&identityProvider.Config,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
identityProvider.Type = storepb.IdentityProvider_Type(storepb.IdentityProvider_Type_value[typeString])
|
||||
identityProviders = append(identityProviders, &identityProvider)
|
||||
}
|
||||
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return identityProviders, nil
|
||||
}
|
||||
|
||||
func (d *DB) UpdateIdentityProvider(ctx context.Context, update *store.UpdateIdentityProvider) (*store.IdentityProvider, error) {
|
||||
set, args := []string{}, []any{}
|
||||
if v := update.Name; v != nil {
|
||||
set, args = append(set, "name = ?"), append(args, *v)
|
||||
}
|
||||
if v := update.IdentifierFilter; v != nil {
|
||||
set, args = append(set, "identifier_filter = ?"), append(args, *v)
|
||||
}
|
||||
if v := update.Config; v != nil {
|
||||
set, args = append(set, "config = ?"), append(args, *v)
|
||||
}
|
||||
args = append(args, update.ID)
|
||||
|
||||
stmt := `
|
||||
UPDATE idp
|
||||
SET ` + strings.Join(set, ", ") + `
|
||||
WHERE id = ?
|
||||
RETURNING id, name, type, identifier_filter, config
|
||||
`
|
||||
var identityProvider store.IdentityProvider
|
||||
var typeString string
|
||||
if err := d.db.QueryRowContext(ctx, stmt, args...).Scan(
|
||||
&identityProvider.ID,
|
||||
&identityProvider.Name,
|
||||
&typeString,
|
||||
&identityProvider.IdentifierFilter,
|
||||
&identityProvider.Config,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
identityProvider.Type = storepb.IdentityProvider_Type(storepb.IdentityProvider_Type_value[typeString])
|
||||
return &identityProvider, nil
|
||||
}
|
||||
|
||||
func (d *DB) DeleteIdentityProvider(ctx context.Context, delete *store.DeleteIdentityProvider) error {
|
||||
where, args := []string{"id = ?"}, []any{delete.ID}
|
||||
stmt := `DELETE FROM idp WHERE ` + strings.Join(where, " AND ")
|
||||
result, err := d.db.ExecContext(ctx, stmt, args...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err = result.RowsAffected(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
132
store/db/sqlite/inbox.go
Normal file
132
store/db/sqlite/inbox.go
Normal file
@@ -0,0 +1,132 @@
|
||||
package sqlite
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"google.golang.org/protobuf/encoding/protojson"
|
||||
|
||||
storepb "github.com/usememos/memos/proto/gen/store"
|
||||
"github.com/usememos/memos/store"
|
||||
)
|
||||
|
||||
func (d *DB) CreateInbox(ctx context.Context, create *store.Inbox) (*store.Inbox, error) {
|
||||
messageString := "{}"
|
||||
if create.Message != nil {
|
||||
bytes, err := protojson.Marshal(create.Message)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to marshal inbox message")
|
||||
}
|
||||
messageString = string(bytes)
|
||||
}
|
||||
|
||||
fields := []string{"`sender_id`", "`receiver_id`", "`status`", "`message`"}
|
||||
placeholder := []string{"?", "?", "?", "?"}
|
||||
args := []any{create.SenderID, create.ReceiverID, create.Status, messageString}
|
||||
|
||||
stmt := "INSERT INTO `inbox` (" + strings.Join(fields, ", ") + ") VALUES (" + strings.Join(placeholder, ", ") + ") RETURNING `id`, `created_ts`"
|
||||
if err := d.db.QueryRowContext(ctx, stmt, args...).Scan(
|
||||
&create.ID,
|
||||
&create.CreatedTs,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return create, nil
|
||||
}
|
||||
|
||||
func (d *DB) ListInboxes(ctx context.Context, find *store.FindInbox) ([]*store.Inbox, error) {
|
||||
where, args := []string{"1 = 1"}, []any{}
|
||||
|
||||
if find.ID != nil {
|
||||
where, args = append(where, "`id` = ?"), append(args, *find.ID)
|
||||
}
|
||||
if find.SenderID != nil {
|
||||
where, args = append(where, "`sender_id` = ?"), append(args, *find.SenderID)
|
||||
}
|
||||
if find.ReceiverID != nil {
|
||||
where, args = append(where, "`receiver_id` = ?"), append(args, *find.ReceiverID)
|
||||
}
|
||||
if find.Status != nil {
|
||||
where, args = append(where, "`status` = ?"), append(args, *find.Status)
|
||||
}
|
||||
|
||||
query := "SELECT `id`, `created_ts`, `sender_id`, `receiver_id`, `status`, `message` FROM `inbox` WHERE " + strings.Join(where, " AND ") + " ORDER BY `created_ts` DESC"
|
||||
if find.Limit != nil {
|
||||
query = fmt.Sprintf("%s LIMIT %d", query, *find.Limit)
|
||||
if find.Offset != nil {
|
||||
query = fmt.Sprintf("%s OFFSET %d", query, *find.Offset)
|
||||
}
|
||||
}
|
||||
rows, err := d.db.QueryContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
list := []*store.Inbox{}
|
||||
for rows.Next() {
|
||||
inbox := &store.Inbox{}
|
||||
var messageBytes []byte
|
||||
if err := rows.Scan(
|
||||
&inbox.ID,
|
||||
&inbox.CreatedTs,
|
||||
&inbox.SenderID,
|
||||
&inbox.ReceiverID,
|
||||
&inbox.Status,
|
||||
&messageBytes,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
message := &storepb.InboxMessage{}
|
||||
if err := protojsonUnmarshaler.Unmarshal(messageBytes, message); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
inbox.Message = message
|
||||
list = append(list, inbox)
|
||||
}
|
||||
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return list, nil
|
||||
}
|
||||
|
||||
func (d *DB) UpdateInbox(ctx context.Context, update *store.UpdateInbox) (*store.Inbox, error) {
|
||||
set, args := []string{"`status` = ?"}, []any{update.Status.String()}
|
||||
args = append(args, update.ID)
|
||||
query := "UPDATE `inbox` SET " + strings.Join(set, ", ") + " WHERE `id` = ? RETURNING `id`, `created_ts`, `sender_id`, `receiver_id`, `status`, `message`"
|
||||
inbox := &store.Inbox{}
|
||||
var messageBytes []byte
|
||||
if err := d.db.QueryRowContext(ctx, query, args...).Scan(
|
||||
&inbox.ID,
|
||||
&inbox.CreatedTs,
|
||||
&inbox.SenderID,
|
||||
&inbox.ReceiverID,
|
||||
&inbox.Status,
|
||||
&messageBytes,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
message := &storepb.InboxMessage{}
|
||||
if err := protojsonUnmarshaler.Unmarshal(messageBytes, message); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
inbox.Message = message
|
||||
return inbox, nil
|
||||
}
|
||||
|
||||
func (d *DB) DeleteInbox(ctx context.Context, delete *store.DeleteInbox) error {
|
||||
result, err := d.db.ExecContext(ctx, "DELETE FROM `inbox` WHERE `id` = ?", delete.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := result.RowsAffected(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
265
store/db/sqlite/memo.go
Normal file
265
store/db/sqlite/memo.go
Normal file
@@ -0,0 +1,265 @@
|
||||
package sqlite
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"google.golang.org/protobuf/encoding/protojson"
|
||||
|
||||
"github.com/usememos/memos/plugin/filter"
|
||||
storepb "github.com/usememos/memos/proto/gen/store"
|
||||
"github.com/usememos/memos/store"
|
||||
)
|
||||
|
||||
func (d *DB) CreateMemo(ctx context.Context, create *store.Memo) (*store.Memo, error) {
|
||||
fields := []string{"`uid`", "`creator_id`", "`content`", "`visibility`", "`payload`"}
|
||||
placeholder := []string{"?", "?", "?", "?", "?"}
|
||||
payload := "{}"
|
||||
if create.Payload != nil {
|
||||
payloadBytes, err := protojson.Marshal(create.Payload)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
payload = string(payloadBytes)
|
||||
}
|
||||
args := []any{create.UID, create.CreatorID, create.Content, create.Visibility, payload}
|
||||
|
||||
stmt := "INSERT INTO `memo` (" + strings.Join(fields, ", ") + ") VALUES (" + strings.Join(placeholder, ", ") + ") RETURNING `id`, `created_ts`, `updated_ts`, `row_status`"
|
||||
if err := d.db.QueryRowContext(ctx, stmt, args...).Scan(
|
||||
&create.ID,
|
||||
&create.CreatedTs,
|
||||
&create.UpdatedTs,
|
||||
&create.RowStatus,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return create, nil
|
||||
}
|
||||
|
||||
func (d *DB) ListMemos(ctx context.Context, find *store.FindMemo) ([]*store.Memo, error) {
|
||||
where, args := []string{"1 = 1"}, []any{}
|
||||
|
||||
if v := find.ID; v != nil {
|
||||
where, args = append(where, "`memo`.`id` = ?"), append(args, *v)
|
||||
}
|
||||
if v := find.UID; v != nil {
|
||||
where, args = append(where, "`memo`.`uid` = ?"), append(args, *v)
|
||||
}
|
||||
if v := find.CreatorID; v != nil {
|
||||
where, args = append(where, "`memo`.`creator_id` = ?"), append(args, *v)
|
||||
}
|
||||
if v := find.RowStatus; v != nil {
|
||||
where, args = append(where, "`memo`.`row_status` = ?"), append(args, *v)
|
||||
}
|
||||
if v := find.CreatedTsBefore; v != nil {
|
||||
where, args = append(where, "`memo`.`created_ts` < ?"), append(args, *v)
|
||||
}
|
||||
if v := find.CreatedTsAfter; v != nil {
|
||||
where, args = append(where, "`memo`.`created_ts` > ?"), append(args, *v)
|
||||
}
|
||||
if v := find.UpdatedTsBefore; v != nil {
|
||||
where, args = append(where, "`memo`.`updated_ts` < ?"), append(args, *v)
|
||||
}
|
||||
if v := find.UpdatedTsAfter; v != nil {
|
||||
where, args = append(where, "`memo`.`updated_ts` > ?"), append(args, *v)
|
||||
}
|
||||
if v := find.ContentSearch; len(v) != 0 {
|
||||
for _, s := range v {
|
||||
where, args = append(where, "`memo`.`content` LIKE ?"), append(args, fmt.Sprintf("%%%s%%", s))
|
||||
}
|
||||
}
|
||||
if v := find.VisibilityList; len(v) != 0 {
|
||||
placeholder := []string{}
|
||||
for _, visibility := range v {
|
||||
placeholder = append(placeholder, "?")
|
||||
args = append(args, visibility.String())
|
||||
}
|
||||
where = append(where, fmt.Sprintf("`memo`.`visibility` IN (%s)", strings.Join(placeholder, ",")))
|
||||
}
|
||||
if v := find.Pinned; v != nil {
|
||||
where, args = append(where, "`memo`.`pinned` = ?"), append(args, *v)
|
||||
}
|
||||
if v := find.PayloadFind; v != nil {
|
||||
if v.Raw != nil {
|
||||
where, args = append(where, "`memo`.`payload` = ?"), append(args, *v.Raw)
|
||||
}
|
||||
if len(v.TagSearch) != 0 {
|
||||
for _, tag := range v.TagSearch {
|
||||
where, args = append(where, "(JSON_EXTRACT(`memo`.`payload`, '$.tags') LIKE ? OR JSON_EXTRACT(`memo`.`payload`, '$.tags') LIKE ?)"), append(args, fmt.Sprintf(`%%"%s"%%`, tag), fmt.Sprintf(`%%"%s/%%`, tag))
|
||||
}
|
||||
}
|
||||
if v.HasLink {
|
||||
where = append(where, "JSON_EXTRACT(`memo`.`payload`, '$.property.hasLink') IS TRUE")
|
||||
}
|
||||
if v.HasTaskList {
|
||||
where = append(where, "JSON_EXTRACT(`memo`.`payload`, '$.property.hasTaskList') IS TRUE")
|
||||
}
|
||||
if v.HasCode {
|
||||
where = append(where, "JSON_EXTRACT(`memo`.`payload`, '$.property.hasCode') IS TRUE")
|
||||
}
|
||||
if v.HasIncompleteTasks {
|
||||
where = append(where, "JSON_EXTRACT(`memo`.`payload`, '$.property.hasIncompleteTasks') IS TRUE")
|
||||
}
|
||||
}
|
||||
if v := find.Filter; v != nil {
|
||||
// Parse filter string and return the parsed expression.
|
||||
// The filter string should be a CEL expression.
|
||||
parsedExpr, err := filter.Parse(*v, filter.MemoFilterCELAttributes...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
convertCtx := filter.NewConvertContext()
|
||||
// ConvertExprToSQL converts the parsed expression to a SQL condition string.
|
||||
if err := d.ConvertExprToSQL(convertCtx, parsedExpr.GetExpr()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
condition := convertCtx.Buffer.String()
|
||||
if condition != "" {
|
||||
where = append(where, fmt.Sprintf("(%s)", condition))
|
||||
args = append(args, convertCtx.Args...)
|
||||
}
|
||||
}
|
||||
if find.ExcludeComments {
|
||||
where = append(where, "`parent_id` IS NULL")
|
||||
}
|
||||
|
||||
order := "DESC"
|
||||
if find.OrderByTimeAsc {
|
||||
order = "ASC"
|
||||
}
|
||||
orderBy := []string{}
|
||||
if find.OrderByPinned {
|
||||
orderBy = append(orderBy, "`pinned` DESC")
|
||||
}
|
||||
if find.OrderByUpdatedTs {
|
||||
orderBy = append(orderBy, "`updated_ts` "+order)
|
||||
} else {
|
||||
orderBy = append(orderBy, "`created_ts` "+order)
|
||||
}
|
||||
fields := []string{
|
||||
"`memo`.`id` AS `id`",
|
||||
"`memo`.`uid` AS `uid`",
|
||||
"`memo`.`creator_id` AS `creator_id`",
|
||||
"`memo`.`created_ts` AS `created_ts`",
|
||||
"`memo`.`updated_ts` AS `updated_ts`",
|
||||
"`memo`.`row_status` AS `row_status`",
|
||||
"`memo`.`visibility` AS `visibility`",
|
||||
"`memo`.`pinned` AS `pinned`",
|
||||
"`memo`.`payload` AS `payload`",
|
||||
"`memo_relation`.`related_memo_id` AS `parent_id`",
|
||||
}
|
||||
if !find.ExcludeContent {
|
||||
fields = append(fields, "`memo`.`content` AS `content`")
|
||||
}
|
||||
|
||||
query := "SELECT " + strings.Join(fields, ", ") + "FROM `memo` " +
|
||||
"LEFT JOIN `memo_relation` ON `memo`.`id` = `memo_relation`.`memo_id` AND `memo_relation`.`type` = \"COMMENT\" " +
|
||||
"WHERE " + strings.Join(where, " AND ") + " " +
|
||||
"ORDER BY " + strings.Join(orderBy, ", ")
|
||||
if find.Limit != nil {
|
||||
query = fmt.Sprintf("%s LIMIT %d", query, *find.Limit)
|
||||
if find.Offset != nil {
|
||||
query = fmt.Sprintf("%s OFFSET %d", query, *find.Offset)
|
||||
}
|
||||
}
|
||||
|
||||
rows, err := d.db.QueryContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
list := make([]*store.Memo, 0)
|
||||
for rows.Next() {
|
||||
var memo store.Memo
|
||||
var payloadBytes []byte
|
||||
dests := []any{
|
||||
&memo.ID,
|
||||
&memo.UID,
|
||||
&memo.CreatorID,
|
||||
&memo.CreatedTs,
|
||||
&memo.UpdatedTs,
|
||||
&memo.RowStatus,
|
||||
&memo.Visibility,
|
||||
&memo.Pinned,
|
||||
&payloadBytes,
|
||||
&memo.ParentID,
|
||||
}
|
||||
if !find.ExcludeContent {
|
||||
dests = append(dests, &memo.Content)
|
||||
}
|
||||
if err := rows.Scan(dests...); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
payload := &storepb.MemoPayload{}
|
||||
if err := protojsonUnmarshaler.Unmarshal(payloadBytes, payload); err != nil {
|
||||
return nil, errors.Wrap(err, "failed to unmarshal payload")
|
||||
}
|
||||
memo.Payload = payload
|
||||
list = append(list, &memo)
|
||||
}
|
||||
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return list, nil
|
||||
}
|
||||
|
||||
func (d *DB) UpdateMemo(ctx context.Context, update *store.UpdateMemo) error {
|
||||
set, args := []string{}, []any{}
|
||||
if v := update.UID; v != nil {
|
||||
set, args = append(set, "`uid` = ?"), append(args, *v)
|
||||
}
|
||||
if v := update.CreatedTs; v != nil {
|
||||
set, args = append(set, "`created_ts` = ?"), append(args, *v)
|
||||
}
|
||||
if v := update.UpdatedTs; v != nil {
|
||||
set, args = append(set, "`updated_ts` = ?"), append(args, *v)
|
||||
}
|
||||
if v := update.RowStatus; v != nil {
|
||||
set, args = append(set, "`row_status` = ?"), append(args, *v)
|
||||
}
|
||||
if v := update.Content; v != nil {
|
||||
set, args = append(set, "`content` = ?"), append(args, *v)
|
||||
}
|
||||
if v := update.Visibility; v != nil {
|
||||
set, args = append(set, "`visibility` = ?"), append(args, *v)
|
||||
}
|
||||
if v := update.Pinned; v != nil {
|
||||
set, args = append(set, "`pinned` = ?"), append(args, *v)
|
||||
}
|
||||
if v := update.Payload; v != nil {
|
||||
payloadBytes, err := protojson.Marshal(v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
set, args = append(set, "`payload` = ?"), append(args, string(payloadBytes))
|
||||
}
|
||||
if len(set) == 0 {
|
||||
return nil
|
||||
}
|
||||
args = append(args, update.ID)
|
||||
|
||||
stmt := "UPDATE `memo` SET " + strings.Join(set, ", ") + " WHERE `id` = ?"
|
||||
if _, err := d.db.ExecContext(ctx, stmt, args...); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *DB) DeleteMemo(ctx context.Context, delete *store.DeleteMemo) error {
|
||||
where, args := []string{"`id` = ?"}, []any{delete.ID}
|
||||
stmt := "DELETE FROM `memo` WHERE " + strings.Join(where, " AND ")
|
||||
result, err := d.db.ExecContext(ctx, stmt, args...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := result.RowsAffected(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
304
store/db/sqlite/memo_filter.go
Normal file
304
store/db/sqlite/memo_filter.go
Normal file
@@ -0,0 +1,304 @@
|
||||
package sqlite
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
exprv1 "google.golang.org/genproto/googleapis/api/expr/v1alpha1"
|
||||
|
||||
"github.com/usememos/memos/plugin/filter"
|
||||
)
|
||||
|
||||
func (d *DB) ConvertExprToSQL(ctx *filter.ConvertContext, expr *exprv1.Expr) error {
|
||||
return d.convertWithTemplates(ctx, expr)
|
||||
}
|
||||
|
||||
func (d *DB) convertWithTemplates(ctx *filter.ConvertContext, expr *exprv1.Expr) error {
|
||||
const dbType = filter.SQLiteTemplate
|
||||
|
||||
if v, ok := expr.ExprKind.(*exprv1.Expr_CallExpr); ok {
|
||||
switch v.CallExpr.Function {
|
||||
case "_||_", "_&&_":
|
||||
if len(v.CallExpr.Args) != 2 {
|
||||
return errors.Errorf("invalid number of arguments for %s", v.CallExpr.Function)
|
||||
}
|
||||
if _, err := ctx.Buffer.WriteString("("); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := d.convertWithTemplates(ctx, v.CallExpr.Args[0]); err != nil {
|
||||
return err
|
||||
}
|
||||
operator := "AND"
|
||||
if v.CallExpr.Function == "_||_" {
|
||||
operator = "OR"
|
||||
}
|
||||
if _, err := ctx.Buffer.WriteString(fmt.Sprintf(" %s ", operator)); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := d.convertWithTemplates(ctx, v.CallExpr.Args[1]); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := ctx.Buffer.WriteString(")"); err != nil {
|
||||
return err
|
||||
}
|
||||
case "!_":
|
||||
if len(v.CallExpr.Args) != 1 {
|
||||
return errors.Errorf("invalid number of arguments for %s", v.CallExpr.Function)
|
||||
}
|
||||
if _, err := ctx.Buffer.WriteString("NOT ("); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := d.convertWithTemplates(ctx, v.CallExpr.Args[0]); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := ctx.Buffer.WriteString(")"); err != nil {
|
||||
return err
|
||||
}
|
||||
case "_==_", "_!=_", "_<_", "_>_", "_<=_", "_>=_":
|
||||
if len(v.CallExpr.Args) != 2 {
|
||||
return errors.Errorf("invalid number of arguments for %s", v.CallExpr.Function)
|
||||
}
|
||||
// Check if the left side is a function call like size(tags)
|
||||
if leftCallExpr, ok := v.CallExpr.Args[0].ExprKind.(*exprv1.Expr_CallExpr); ok {
|
||||
if leftCallExpr.CallExpr.Function == "size" {
|
||||
// Handle size(tags) comparison
|
||||
if len(leftCallExpr.CallExpr.Args) != 1 {
|
||||
return errors.New("size function requires exactly one argument")
|
||||
}
|
||||
identifier, err := filter.GetIdentExprName(leftCallExpr.CallExpr.Args[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if identifier != "tags" {
|
||||
return errors.Errorf("size function only supports 'tags' identifier, got: %s", identifier)
|
||||
}
|
||||
value, err := filter.GetExprValue(v.CallExpr.Args[1])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
valueInt, ok := value.(int64)
|
||||
if !ok {
|
||||
return errors.New("size comparison value must be an integer")
|
||||
}
|
||||
operator := d.getComparisonOperator(v.CallExpr.Function)
|
||||
|
||||
if _, err := ctx.Buffer.WriteString(fmt.Sprintf("%s %s ?",
|
||||
filter.GetSQL("json_array_length", dbType), operator)); err != nil {
|
||||
return err
|
||||
}
|
||||
ctx.Args = append(ctx.Args, valueInt)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
identifier, err := filter.GetIdentExprName(v.CallExpr.Args[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !slices.Contains([]string{"creator_id", "created_ts", "updated_ts", "visibility", "content", "has_task_list"}, identifier) {
|
||||
return errors.Errorf("invalid identifier for %s", v.CallExpr.Function)
|
||||
}
|
||||
value, err := filter.GetExprValue(v.CallExpr.Args[1])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
operator := d.getComparisonOperator(v.CallExpr.Function)
|
||||
|
||||
if identifier == "created_ts" || identifier == "updated_ts" {
|
||||
valueInt, ok := value.(int64)
|
||||
if !ok {
|
||||
return errors.New("invalid integer timestamp value")
|
||||
}
|
||||
|
||||
timestampSQL := fmt.Sprintf(filter.GetSQL("timestamp_field", dbType), identifier)
|
||||
if _, err := ctx.Buffer.WriteString(fmt.Sprintf("%s %s ?", timestampSQL, operator)); err != nil {
|
||||
return err
|
||||
}
|
||||
ctx.Args = append(ctx.Args, valueInt)
|
||||
} else if identifier == "visibility" || identifier == "content" {
|
||||
if operator != "=" && operator != "!=" {
|
||||
return errors.Errorf("invalid operator for %s", v.CallExpr.Function)
|
||||
}
|
||||
valueStr, ok := value.(string)
|
||||
if !ok {
|
||||
return errors.New("invalid string value")
|
||||
}
|
||||
|
||||
var sqlTemplate string
|
||||
if identifier == "visibility" {
|
||||
sqlTemplate = filter.GetSQL("table_prefix", dbType) + ".`visibility`"
|
||||
} else if identifier == "content" {
|
||||
sqlTemplate = filter.GetSQL("table_prefix", dbType) + ".`content`"
|
||||
}
|
||||
if _, err := ctx.Buffer.WriteString(fmt.Sprintf("%s %s ?", sqlTemplate, operator)); err != nil {
|
||||
return err
|
||||
}
|
||||
ctx.Args = append(ctx.Args, valueStr)
|
||||
} else if identifier == "creator_id" {
|
||||
if operator != "=" && operator != "!=" {
|
||||
return errors.Errorf("invalid operator for %s", v.CallExpr.Function)
|
||||
}
|
||||
valueInt, ok := value.(int64)
|
||||
if !ok {
|
||||
return errors.New("invalid int value")
|
||||
}
|
||||
|
||||
sqlTemplate := filter.GetSQL("table_prefix", dbType) + ".`creator_id`"
|
||||
if _, err := ctx.Buffer.WriteString(fmt.Sprintf("%s %s ?", sqlTemplate, operator)); err != nil {
|
||||
return err
|
||||
}
|
||||
ctx.Args = append(ctx.Args, valueInt)
|
||||
} else if identifier == "has_task_list" {
|
||||
if operator != "=" && operator != "!=" {
|
||||
return errors.Errorf("invalid operator for %s", v.CallExpr.Function)
|
||||
}
|
||||
valueBool, ok := value.(bool)
|
||||
if !ok {
|
||||
return errors.New("invalid boolean value for has_task_list")
|
||||
}
|
||||
// Use template for boolean comparison
|
||||
var sqlTemplate string
|
||||
if operator == "=" {
|
||||
if valueBool {
|
||||
sqlTemplate = filter.GetSQL("boolean_true", dbType)
|
||||
} else {
|
||||
sqlTemplate = filter.GetSQL("boolean_false", dbType)
|
||||
}
|
||||
} else { // operator == "!="
|
||||
if valueBool {
|
||||
sqlTemplate = filter.GetSQL("boolean_not_true", dbType)
|
||||
} else {
|
||||
sqlTemplate = filter.GetSQL("boolean_not_false", dbType)
|
||||
}
|
||||
}
|
||||
if _, err := ctx.Buffer.WriteString(sqlTemplate); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
case "@in":
|
||||
if len(v.CallExpr.Args) != 2 {
|
||||
return errors.Errorf("invalid number of arguments for %s", v.CallExpr.Function)
|
||||
}
|
||||
|
||||
// Check if this is "element in collection" syntax
|
||||
if identifier, err := filter.GetIdentExprName(v.CallExpr.Args[1]); err == nil {
|
||||
// This is "element in collection" - the second argument is the collection
|
||||
if !slices.Contains([]string{"tags"}, identifier) {
|
||||
return errors.Errorf("invalid collection identifier for %s: %s", v.CallExpr.Function, identifier)
|
||||
}
|
||||
|
||||
if identifier == "tags" {
|
||||
// Handle "element" in tags
|
||||
element, err := filter.GetConstValue(v.CallExpr.Args[0])
|
||||
if err != nil {
|
||||
return errors.Errorf("first argument must be a constant value for 'element in tags': %v", err)
|
||||
}
|
||||
if _, err := ctx.Buffer.WriteString(filter.GetSQL("json_contains_element", dbType)); err != nil {
|
||||
return err
|
||||
}
|
||||
ctx.Args = append(ctx.Args, filter.GetParameterValue(dbType, "json_contains_element", element))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Original logic for "identifier in [list]" syntax
|
||||
identifier, err := filter.GetIdentExprName(v.CallExpr.Args[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !slices.Contains([]string{"tag", "visibility"}, identifier) {
|
||||
return errors.Errorf("invalid identifier for %s", v.CallExpr.Function)
|
||||
}
|
||||
|
||||
values := []any{}
|
||||
for _, element := range v.CallExpr.Args[1].GetListExpr().Elements {
|
||||
value, err := filter.GetConstValue(element)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
values = append(values, value)
|
||||
}
|
||||
if identifier == "tag" {
|
||||
subconditions := []string{}
|
||||
args := []any{}
|
||||
for _, v := range values {
|
||||
subconditions = append(subconditions, filter.GetSQL("json_contains_tag", dbType))
|
||||
args = append(args, filter.GetParameterValue(dbType, "json_contains_tag", v))
|
||||
}
|
||||
if len(subconditions) == 1 {
|
||||
if _, err := ctx.Buffer.WriteString(subconditions[0]); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if _, err := ctx.Buffer.WriteString(fmt.Sprintf("(%s)", strings.Join(subconditions, " OR "))); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
ctx.Args = append(ctx.Args, args...)
|
||||
} else if identifier == "visibility" {
|
||||
placeholders := filter.FormatPlaceholders(dbType, len(values), 1)
|
||||
visibilitySQL := fmt.Sprintf(filter.GetSQL("visibility_in", dbType), strings.Join(placeholders, ","))
|
||||
if _, err := ctx.Buffer.WriteString(visibilitySQL); err != nil {
|
||||
return err
|
||||
}
|
||||
ctx.Args = append(ctx.Args, values...)
|
||||
}
|
||||
case "contains":
|
||||
if len(v.CallExpr.Args) != 1 {
|
||||
return errors.Errorf("invalid number of arguments for %s", v.CallExpr.Function)
|
||||
}
|
||||
identifier, err := filter.GetIdentExprName(v.CallExpr.Target)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if identifier != "content" {
|
||||
return errors.Errorf("invalid identifier for %s", v.CallExpr.Function)
|
||||
}
|
||||
arg, err := filter.GetConstValue(v.CallExpr.Args[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := ctx.Buffer.WriteString(filter.GetSQL("content_like", dbType)); err != nil {
|
||||
return err
|
||||
}
|
||||
ctx.Args = append(ctx.Args, fmt.Sprintf("%%%s%%", arg))
|
||||
}
|
||||
} else if v, ok := expr.ExprKind.(*exprv1.Expr_IdentExpr); ok {
|
||||
identifier := v.IdentExpr.GetName()
|
||||
if !slices.Contains([]string{"pinned", "has_task_list"}, identifier) {
|
||||
return errors.Errorf("invalid identifier %s", identifier)
|
||||
}
|
||||
if identifier == "pinned" {
|
||||
if _, err := ctx.Buffer.WriteString(filter.GetSQL("table_prefix", dbType) + ".`pinned` IS TRUE"); err != nil {
|
||||
return err
|
||||
}
|
||||
} else if identifier == "has_task_list" {
|
||||
// Handle has_task_list as a standalone boolean identifier
|
||||
if _, err := ctx.Buffer.WriteString(filter.GetSQL("boolean_check", dbType)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (*DB) getComparisonOperator(function string) string {
|
||||
switch function {
|
||||
case "_==_":
|
||||
return "="
|
||||
case "_!=_":
|
||||
return "!="
|
||||
case "_<_":
|
||||
return "<"
|
||||
case "_>_":
|
||||
return ">"
|
||||
case "_<=_":
|
||||
return "<="
|
||||
case "_>=_":
|
||||
return ">="
|
||||
default:
|
||||
return "="
|
||||
}
|
||||
}
|
||||
151
store/db/sqlite/memo_filter_test.go
Normal file
151
store/db/sqlite/memo_filter_test.go
Normal file
@@ -0,0 +1,151 @@
|
||||
package sqlite
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/usememos/memos/plugin/filter"
|
||||
)
|
||||
|
||||
func TestConvertExprToSQL(t *testing.T) {
|
||||
tests := []struct {
|
||||
filter string
|
||||
want string
|
||||
args []any
|
||||
}{
|
||||
{
|
||||
filter: `tag in ["tag1", "tag2"]`,
|
||||
want: "(JSON_EXTRACT(`memo`.`payload`, '$.tags') LIKE ? OR JSON_EXTRACT(`memo`.`payload`, '$.tags') LIKE ?)",
|
||||
args: []any{`%"tag1"%`, `%"tag2"%`},
|
||||
},
|
||||
{
|
||||
filter: `!(tag in ["tag1", "tag2"])`,
|
||||
want: "NOT ((JSON_EXTRACT(`memo`.`payload`, '$.tags') LIKE ? OR JSON_EXTRACT(`memo`.`payload`, '$.tags') LIKE ?))",
|
||||
args: []any{`%"tag1"%`, `%"tag2"%`},
|
||||
},
|
||||
{
|
||||
filter: `tag in ["tag1", "tag2"] || tag in ["tag3", "tag4"]`,
|
||||
want: "((JSON_EXTRACT(`memo`.`payload`, '$.tags') LIKE ? OR JSON_EXTRACT(`memo`.`payload`, '$.tags') LIKE ?) OR (JSON_EXTRACT(`memo`.`payload`, '$.tags') LIKE ? OR JSON_EXTRACT(`memo`.`payload`, '$.tags') LIKE ?))",
|
||||
args: []any{`%"tag1"%`, `%"tag2"%`, `%"tag3"%`, `%"tag4"%`},
|
||||
},
|
||||
{
|
||||
filter: `content.contains("memos")`,
|
||||
want: "`memo`.`content` LIKE ?",
|
||||
args: []any{"%memos%"},
|
||||
},
|
||||
{
|
||||
filter: `visibility in ["PUBLIC"]`,
|
||||
want: "`memo`.`visibility` IN (?)",
|
||||
args: []any{"PUBLIC"},
|
||||
},
|
||||
{
|
||||
filter: `visibility in ["PUBLIC", "PRIVATE"]`,
|
||||
want: "`memo`.`visibility` IN (?,?)",
|
||||
args: []any{"PUBLIC", "PRIVATE"},
|
||||
},
|
||||
{
|
||||
filter: `tag in ['tag1'] || content.contains('hello')`,
|
||||
want: "(JSON_EXTRACT(`memo`.`payload`, '$.tags') LIKE ? OR `memo`.`content` LIKE ?)",
|
||||
args: []any{`%"tag1"%`, "%hello%"},
|
||||
},
|
||||
{
|
||||
filter: `1`,
|
||||
want: "",
|
||||
args: []any{},
|
||||
},
|
||||
{
|
||||
filter: `pinned`,
|
||||
want: "`memo`.`pinned` IS TRUE",
|
||||
args: []any{},
|
||||
},
|
||||
{
|
||||
filter: `!pinned`,
|
||||
want: "NOT (`memo`.`pinned` IS TRUE)",
|
||||
args: []any{},
|
||||
},
|
||||
{
|
||||
filter: `creator_id == 101 || visibility in ["PUBLIC", "PRIVATE"]`,
|
||||
want: "(`memo`.`creator_id` = ? OR `memo`.`visibility` IN (?,?))",
|
||||
args: []any{int64(101), "PUBLIC", "PRIVATE"},
|
||||
},
|
||||
{
|
||||
filter: `has_task_list`,
|
||||
want: "JSON_EXTRACT(`memo`.`payload`, '$.property.hasTaskList') IS TRUE",
|
||||
args: []any{},
|
||||
},
|
||||
{
|
||||
filter: `has_task_list == true`,
|
||||
want: "JSON_EXTRACT(`memo`.`payload`, '$.property.hasTaskList') = 1",
|
||||
args: []any{},
|
||||
},
|
||||
{
|
||||
filter: `has_task_list != false`,
|
||||
want: "JSON_EXTRACT(`memo`.`payload`, '$.property.hasTaskList') != 0",
|
||||
args: []any{},
|
||||
},
|
||||
{
|
||||
filter: `has_task_list == false`,
|
||||
want: "JSON_EXTRACT(`memo`.`payload`, '$.property.hasTaskList') = 0",
|
||||
args: []any{},
|
||||
},
|
||||
{
|
||||
filter: `!has_task_list`,
|
||||
want: "NOT (JSON_EXTRACT(`memo`.`payload`, '$.property.hasTaskList') IS TRUE)",
|
||||
args: []any{},
|
||||
},
|
||||
{
|
||||
filter: `has_task_list && pinned`,
|
||||
want: "(JSON_EXTRACT(`memo`.`payload`, '$.property.hasTaskList') IS TRUE AND `memo`.`pinned` IS TRUE)",
|
||||
args: []any{},
|
||||
},
|
||||
{
|
||||
filter: `has_task_list && content.contains("todo")`,
|
||||
want: "(JSON_EXTRACT(`memo`.`payload`, '$.property.hasTaskList') IS TRUE AND `memo`.`content` LIKE ?)",
|
||||
args: []any{"%todo%"},
|
||||
},
|
||||
{
|
||||
filter: `created_ts > now() - 60 * 60 * 24`,
|
||||
want: "`memo`.`created_ts` > ?",
|
||||
args: []any{time.Now().Unix() - 60*60*24},
|
||||
},
|
||||
{
|
||||
filter: `size(tags) == 0`,
|
||||
want: "JSON_ARRAY_LENGTH(COALESCE(JSON_EXTRACT(`memo`.`payload`, '$.tags'), JSON_ARRAY())) = ?",
|
||||
args: []any{int64(0)},
|
||||
},
|
||||
{
|
||||
filter: `size(tags) > 0`,
|
||||
want: "JSON_ARRAY_LENGTH(COALESCE(JSON_EXTRACT(`memo`.`payload`, '$.tags'), JSON_ARRAY())) > ?",
|
||||
args: []any{int64(0)},
|
||||
},
|
||||
{
|
||||
filter: `"work" in tags`,
|
||||
want: "JSON_EXTRACT(`memo`.`payload`, '$.tags') LIKE ?",
|
||||
args: []any{`%"work"%`},
|
||||
},
|
||||
{
|
||||
filter: `size(tags) == 2`,
|
||||
want: "JSON_ARRAY_LENGTH(COALESCE(JSON_EXTRACT(`memo`.`payload`, '$.tags'), JSON_ARRAY())) = ?",
|
||||
args: []any{int64(2)},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
db := &DB{}
|
||||
parsedExpr, err := filter.Parse(tt.filter, filter.MemoFilterCELAttributes...)
|
||||
if err != nil {
|
||||
t.Logf("Failed to parse filter: %s, error: %v", tt.filter, err)
|
||||
}
|
||||
require.NoError(t, err)
|
||||
convertCtx := filter.NewConvertContext()
|
||||
err = db.ConvertExprToSQL(convertCtx, parsedExpr.GetExpr())
|
||||
if err != nil {
|
||||
t.Logf("Failed to convert filter: %s, error: %v", tt.filter, err)
|
||||
}
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tt.want, convertCtx.Buffer.String())
|
||||
require.Equal(t, tt.args, convertCtx.Args)
|
||||
}
|
||||
}
|
||||
125
store/db/sqlite/memo_relation.go
Normal file
125
store/db/sqlite/memo_relation.go
Normal file
@@ -0,0 +1,125 @@
|
||||
package sqlite
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/usememos/memos/plugin/filter"
|
||||
"github.com/usememos/memos/store"
|
||||
)
|
||||
|
||||
func (d *DB) UpsertMemoRelation(ctx context.Context, create *store.MemoRelation) (*store.MemoRelation, error) {
|
||||
stmt := `
|
||||
INSERT INTO memo_relation (
|
||||
memo_id,
|
||||
related_memo_id,
|
||||
type
|
||||
)
|
||||
VALUES (?, ?, ?)
|
||||
RETURNING memo_id, related_memo_id, type
|
||||
`
|
||||
memoRelation := &store.MemoRelation{}
|
||||
if err := d.db.QueryRowContext(
|
||||
ctx,
|
||||
stmt,
|
||||
create.MemoID,
|
||||
create.RelatedMemoID,
|
||||
create.Type,
|
||||
).Scan(
|
||||
&memoRelation.MemoID,
|
||||
&memoRelation.RelatedMemoID,
|
||||
&memoRelation.Type,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return memoRelation, nil
|
||||
}
|
||||
|
||||
func (d *DB) ListMemoRelations(ctx context.Context, find *store.FindMemoRelation) ([]*store.MemoRelation, error) {
|
||||
where, args := []string{"TRUE"}, []any{}
|
||||
if find.MemoID != nil {
|
||||
where, args = append(where, "memo_id = ?"), append(args, find.MemoID)
|
||||
}
|
||||
if find.RelatedMemoID != nil {
|
||||
where, args = append(where, "related_memo_id = ?"), append(args, find.RelatedMemoID)
|
||||
}
|
||||
if find.Type != nil {
|
||||
where, args = append(where, "type = ?"), append(args, find.Type)
|
||||
}
|
||||
if find.MemoFilter != nil {
|
||||
// Parse filter string and return the parsed expression.
|
||||
// The filter string should be a CEL expression.
|
||||
parsedExpr, err := filter.Parse(*find.MemoFilter, filter.MemoFilterCELAttributes...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
convertCtx := filter.NewConvertContext()
|
||||
// ConvertExprToSQL converts the parsed expression to a SQL condition string.
|
||||
if err := d.ConvertExprToSQL(convertCtx, parsedExpr.GetExpr()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
condition := convertCtx.Buffer.String()
|
||||
if condition != "" {
|
||||
where = append(where, fmt.Sprintf("memo_id IN (SELECT id FROM memo WHERE %s)", condition))
|
||||
where = append(where, fmt.Sprintf("related_memo_id IN (SELECT id FROM memo WHERE %s)", condition))
|
||||
args = append(args, append(convertCtx.Args, convertCtx.Args...)...)
|
||||
}
|
||||
}
|
||||
|
||||
rows, err := d.db.QueryContext(ctx, `
|
||||
SELECT
|
||||
memo_id,
|
||||
related_memo_id,
|
||||
type
|
||||
FROM memo_relation
|
||||
WHERE `+strings.Join(where, " AND "), args...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
list := []*store.MemoRelation{}
|
||||
for rows.Next() {
|
||||
memoRelation := &store.MemoRelation{}
|
||||
if err := rows.Scan(
|
||||
&memoRelation.MemoID,
|
||||
&memoRelation.RelatedMemoID,
|
||||
&memoRelation.Type,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
list = append(list, memoRelation)
|
||||
}
|
||||
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return list, nil
|
||||
}
|
||||
|
||||
func (d *DB) DeleteMemoRelation(ctx context.Context, delete *store.DeleteMemoRelation) error {
|
||||
where, args := []string{"TRUE"}, []any{}
|
||||
if delete.MemoID != nil {
|
||||
where, args = append(where, "memo_id = ?"), append(args, delete.MemoID)
|
||||
}
|
||||
if delete.RelatedMemoID != nil {
|
||||
where, args = append(where, "related_memo_id = ?"), append(args, delete.RelatedMemoID)
|
||||
}
|
||||
if delete.Type != nil {
|
||||
where, args = append(where, "type = ?"), append(args, delete.Type)
|
||||
}
|
||||
stmt := `
|
||||
DELETE FROM memo_relation
|
||||
WHERE ` + strings.Join(where, " AND ")
|
||||
result, err := d.db.ExecContext(ctx, stmt, args...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err = result.RowsAffected(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
57
store/db/sqlite/migration_history.go
Normal file
57
store/db/sqlite/migration_history.go
Normal file
@@ -0,0 +1,57 @@
|
||||
package sqlite
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/usememos/memos/store"
|
||||
)
|
||||
|
||||
func (d *DB) FindMigrationHistoryList(ctx context.Context, _ *store.FindMigrationHistory) ([]*store.MigrationHistory, error) {
|
||||
query := "SELECT `version`, `created_ts` FROM `migration_history` ORDER BY `created_ts` DESC"
|
||||
rows, err := d.db.QueryContext(ctx, query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
list := make([]*store.MigrationHistory, 0)
|
||||
for rows.Next() {
|
||||
var migrationHistory store.MigrationHistory
|
||||
if err := rows.Scan(
|
||||
&migrationHistory.Version,
|
||||
&migrationHistory.CreatedTs,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
list = append(list, &migrationHistory)
|
||||
}
|
||||
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return list, nil
|
||||
}
|
||||
|
||||
func (d *DB) UpsertMigrationHistory(ctx context.Context, upsert *store.UpsertMigrationHistory) (*store.MigrationHistory, error) {
|
||||
stmt := `
|
||||
INSERT INTO migration_history (
|
||||
version
|
||||
)
|
||||
VALUES (?)
|
||||
ON CONFLICT(version) DO UPDATE
|
||||
SET
|
||||
version=EXCLUDED.version
|
||||
RETURNING version, created_ts
|
||||
`
|
||||
var migrationHistory store.MigrationHistory
|
||||
if err := d.db.QueryRowContext(ctx, stmt, upsert.Version).Scan(
|
||||
&migrationHistory.Version,
|
||||
&migrationHistory.CreatedTs,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &migrationHistory, nil
|
||||
}
|
||||
80
store/db/sqlite/reaction.go
Normal file
80
store/db/sqlite/reaction.go
Normal file
@@ -0,0 +1,80 @@
|
||||
package sqlite
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"github.com/usememos/memos/store"
|
||||
)
|
||||
|
||||
func (d *DB) UpsertReaction(ctx context.Context, upsert *store.Reaction) (*store.Reaction, error) {
|
||||
fields := []string{"`creator_id`", "`content_id`", "`reaction_type`"}
|
||||
placeholder := []string{"?", "?", "?"}
|
||||
args := []interface{}{upsert.CreatorID, upsert.ContentID, upsert.ReactionType}
|
||||
stmt := "INSERT INTO `reaction` (" + strings.Join(fields, ", ") + ") VALUES (" + strings.Join(placeholder, ", ") + ") RETURNING `id`, `created_ts`"
|
||||
if err := d.db.QueryRowContext(ctx, stmt, args...).Scan(
|
||||
&upsert.ID,
|
||||
&upsert.CreatedTs,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
reaction := upsert
|
||||
return reaction, nil
|
||||
}
|
||||
|
||||
func (d *DB) ListReactions(ctx context.Context, find *store.FindReaction) ([]*store.Reaction, error) {
|
||||
where, args := []string{"1 = 1"}, []interface{}{}
|
||||
if find.ID != nil {
|
||||
where, args = append(where, "id = ?"), append(args, *find.ID)
|
||||
}
|
||||
if find.CreatorID != nil {
|
||||
where, args = append(where, "creator_id = ?"), append(args, *find.CreatorID)
|
||||
}
|
||||
if find.ContentID != nil {
|
||||
where, args = append(where, "content_id = ?"), append(args, *find.ContentID)
|
||||
}
|
||||
|
||||
rows, err := d.db.QueryContext(ctx, `
|
||||
SELECT
|
||||
id,
|
||||
created_ts,
|
||||
creator_id,
|
||||
content_id,
|
||||
reaction_type
|
||||
FROM reaction
|
||||
WHERE `+strings.Join(where, " AND ")+`
|
||||
ORDER BY id ASC`,
|
||||
args...,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
list := []*store.Reaction{}
|
||||
for rows.Next() {
|
||||
reaction := &store.Reaction{}
|
||||
if err := rows.Scan(
|
||||
&reaction.ID,
|
||||
&reaction.CreatedTs,
|
||||
&reaction.CreatorID,
|
||||
&reaction.ContentID,
|
||||
&reaction.ReactionType,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
list = append(list, reaction)
|
||||
}
|
||||
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return list, nil
|
||||
}
|
||||
|
||||
func (d *DB) DeleteReaction(ctx context.Context, delete *store.DeleteReaction) error {
|
||||
_, err := d.db.ExecContext(ctx, "DELETE FROM `reaction` WHERE `id` = ?", delete.ID)
|
||||
return err
|
||||
}
|
||||
70
store/db/sqlite/sqlite.go
Normal file
70
store/db/sqlite/sqlite.go
Normal file
@@ -0,0 +1,70 @@
|
||||
package sqlite
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
// Import the SQLite driver.
|
||||
_ "modernc.org/sqlite"
|
||||
|
||||
"github.com/usememos/memos/internal/profile"
|
||||
"github.com/usememos/memos/store"
|
||||
)
|
||||
|
||||
type DB struct {
|
||||
db *sql.DB
|
||||
profile *profile.Profile
|
||||
}
|
||||
|
||||
// NewDB opens a database specified by its database driver name and a
|
||||
// driver-specific data source name, usually consisting of at least a
|
||||
// database name and connection information.
|
||||
func NewDB(profile *profile.Profile) (store.Driver, error) {
|
||||
// Ensure a DSN is set before attempting to open the database.
|
||||
if profile.DSN == "" {
|
||||
return nil, errors.New("dsn required")
|
||||
}
|
||||
|
||||
// Connect to the database with some sane settings:
|
||||
// - No shared-cache: it's obsolete; WAL journal mode is a better solution.
|
||||
// - No foreign key constraints: it's currently disabled by default, but it's a
|
||||
// good practice to be explicit and prevent future surprises on SQLite upgrades.
|
||||
// - Journal mode set to WAL: it's the recommended journal mode for most applications
|
||||
// as it prevents locking issues.
|
||||
//
|
||||
// Notes:
|
||||
// - When using the `modernc.org/sqlite` driver, each pragma must be prefixed with `_pragma=`.
|
||||
//
|
||||
// References:
|
||||
// - https://pkg.go.dev/modernc.org/sqlite#Driver.Open
|
||||
// - https://www.sqlite.org/sharedcache.html
|
||||
// - https://www.sqlite.org/pragma.html
|
||||
sqliteDB, err := sql.Open("sqlite", profile.DSN+"?_pragma=foreign_keys(0)&_pragma=busy_timeout(10000)&_pragma=journal_mode(WAL)")
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to open db with dsn: %s", profile.DSN)
|
||||
}
|
||||
|
||||
driver := DB{db: sqliteDB, profile: profile}
|
||||
|
||||
return &driver, nil
|
||||
}
|
||||
|
||||
func (d *DB) GetDB() *sql.DB {
|
||||
return d.db
|
||||
}
|
||||
|
||||
func (d *DB) Close() error {
|
||||
return d.db.Close()
|
||||
}
|
||||
|
||||
func (d *DB) IsInitialized(ctx context.Context) (bool, error) {
|
||||
// Check if the database is initialized by checking if the memo table exists.
|
||||
var exists bool
|
||||
err := d.db.QueryRowContext(ctx, "SELECT EXISTS(SELECT 1 FROM sqlite_master WHERE type='table' AND name='memo')").Scan(&exists)
|
||||
if err != nil {
|
||||
return false, errors.Wrap(err, "failed to check if database is initialized")
|
||||
}
|
||||
return exists, nil
|
||||
}
|
||||
170
store/db/sqlite/user.go
Normal file
170
store/db/sqlite/user.go
Normal file
@@ -0,0 +1,170 @@
|
||||
package sqlite
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/usememos/memos/store"
|
||||
)
|
||||
|
||||
func (d *DB) CreateUser(ctx context.Context, create *store.User) (*store.User, error) {
|
||||
fields := []string{"`username`", "`role`", "`email`", "`nickname`", "`password_hash`, `avatar_url`"}
|
||||
placeholder := []string{"?", "?", "?", "?", "?", "?"}
|
||||
args := []any{create.Username, create.Role, create.Email, create.Nickname, create.PasswordHash, create.AvatarURL}
|
||||
stmt := "INSERT INTO user (" + strings.Join(fields, ", ") + ") VALUES (" + strings.Join(placeholder, ", ") + ") RETURNING id, description, created_ts, updated_ts, row_status"
|
||||
if err := d.db.QueryRowContext(ctx, stmt, args...).Scan(
|
||||
&create.ID,
|
||||
&create.Description,
|
||||
&create.CreatedTs,
|
||||
&create.UpdatedTs,
|
||||
&create.RowStatus,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return create, nil
|
||||
}
|
||||
|
||||
func (d *DB) UpdateUser(ctx context.Context, update *store.UpdateUser) (*store.User, error) {
|
||||
set, args := []string{}, []any{}
|
||||
if v := update.UpdatedTs; v != nil {
|
||||
set, args = append(set, "updated_ts = ?"), append(args, *v)
|
||||
}
|
||||
if v := update.RowStatus; v != nil {
|
||||
set, args = append(set, "row_status = ?"), append(args, *v)
|
||||
}
|
||||
if v := update.Username; v != nil {
|
||||
set, args = append(set, "username = ?"), append(args, *v)
|
||||
}
|
||||
if v := update.Email; v != nil {
|
||||
set, args = append(set, "email = ?"), append(args, *v)
|
||||
}
|
||||
if v := update.Nickname; v != nil {
|
||||
set, args = append(set, "nickname = ?"), append(args, *v)
|
||||
}
|
||||
if v := update.AvatarURL; v != nil {
|
||||
set, args = append(set, "avatar_url = ?"), append(args, *v)
|
||||
}
|
||||
if v := update.PasswordHash; v != nil {
|
||||
set, args = append(set, "password_hash = ?"), append(args, *v)
|
||||
}
|
||||
if v := update.Description; v != nil {
|
||||
set, args = append(set, "description = ?"), append(args, *v)
|
||||
}
|
||||
if v := update.Role; v != nil {
|
||||
set, args = append(set, "role = ?"), append(args, *v)
|
||||
}
|
||||
args = append(args, update.ID)
|
||||
|
||||
query := `
|
||||
UPDATE user
|
||||
SET ` + strings.Join(set, ", ") + `
|
||||
WHERE id = ?
|
||||
RETURNING id, username, role, email, nickname, password_hash, avatar_url, description, created_ts, updated_ts, row_status
|
||||
`
|
||||
user := &store.User{}
|
||||
if err := d.db.QueryRowContext(ctx, query, args...).Scan(
|
||||
&user.ID,
|
||||
&user.Username,
|
||||
&user.Role,
|
||||
&user.Email,
|
||||
&user.Nickname,
|
||||
&user.PasswordHash,
|
||||
&user.AvatarURL,
|
||||
&user.Description,
|
||||
&user.CreatedTs,
|
||||
&user.UpdatedTs,
|
||||
&user.RowStatus,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (d *DB) ListUsers(ctx context.Context, find *store.FindUser) ([]*store.User, error) {
|
||||
where, args := []string{"1 = 1"}, []any{}
|
||||
|
||||
if v := find.ID; v != nil {
|
||||
where, args = append(where, "id = ?"), append(args, *v)
|
||||
}
|
||||
if v := find.Username; v != nil {
|
||||
where, args = append(where, "username = ?"), append(args, *v)
|
||||
}
|
||||
if v := find.Role; v != nil {
|
||||
where, args = append(where, "role = ?"), append(args, *v)
|
||||
}
|
||||
if v := find.Email; v != nil {
|
||||
where, args = append(where, "email = ?"), append(args, *v)
|
||||
}
|
||||
if v := find.Nickname; v != nil {
|
||||
where, args = append(where, "nickname = ?"), append(args, *v)
|
||||
}
|
||||
|
||||
orderBy := []string{"created_ts DESC", "row_status DESC"}
|
||||
query := `
|
||||
SELECT
|
||||
id,
|
||||
username,
|
||||
role,
|
||||
email,
|
||||
nickname,
|
||||
password_hash,
|
||||
avatar_url,
|
||||
description,
|
||||
created_ts,
|
||||
updated_ts,
|
||||
row_status
|
||||
FROM user
|
||||
WHERE ` + strings.Join(where, " AND ") + ` ORDER BY ` + strings.Join(orderBy, ", ")
|
||||
if v := find.Limit; v != nil {
|
||||
query += fmt.Sprintf(" LIMIT %d", *v)
|
||||
}
|
||||
|
||||
rows, err := d.db.QueryContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
list := make([]*store.User, 0)
|
||||
for rows.Next() {
|
||||
var user store.User
|
||||
if err := rows.Scan(
|
||||
&user.ID,
|
||||
&user.Username,
|
||||
&user.Role,
|
||||
&user.Email,
|
||||
&user.Nickname,
|
||||
&user.PasswordHash,
|
||||
&user.AvatarURL,
|
||||
&user.Description,
|
||||
&user.CreatedTs,
|
||||
&user.UpdatedTs,
|
||||
&user.RowStatus,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
list = append(list, &user)
|
||||
}
|
||||
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return list, nil
|
||||
}
|
||||
|
||||
func (d *DB) DeleteUser(ctx context.Context, delete *store.DeleteUser) error {
|
||||
result, err := d.db.ExecContext(ctx, `
|
||||
DELETE FROM user WHERE id = ?
|
||||
`, delete.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := result.RowsAffected(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
68
store/db/sqlite/user_setting.go
Normal file
68
store/db/sqlite/user_setting.go
Normal file
@@ -0,0 +1,68 @@
|
||||
package sqlite
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
storepb "github.com/usememos/memos/proto/gen/store"
|
||||
"github.com/usememos/memos/store"
|
||||
)
|
||||
|
||||
func (d *DB) UpsertUserSetting(ctx context.Context, upsert *store.UserSetting) (*store.UserSetting, error) {
|
||||
stmt := `
|
||||
INSERT INTO user_setting (
|
||||
user_id, key, value
|
||||
)
|
||||
VALUES (?, ?, ?)
|
||||
ON CONFLICT(user_id, key) DO UPDATE
|
||||
SET value = EXCLUDED.value
|
||||
`
|
||||
if _, err := d.db.ExecContext(ctx, stmt, upsert.UserID, upsert.Key.String(), upsert.Value); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return upsert, nil
|
||||
}
|
||||
|
||||
func (d *DB) ListUserSettings(ctx context.Context, find *store.FindUserSetting) ([]*store.UserSetting, error) {
|
||||
where, args := []string{"1 = 1"}, []any{}
|
||||
|
||||
if v := find.Key; v != storepb.UserSetting_KEY_UNSPECIFIED {
|
||||
where, args = append(where, "key = ?"), append(args, v.String())
|
||||
}
|
||||
if v := find.UserID; v != nil {
|
||||
where, args = append(where, "user_id = ?"), append(args, *find.UserID)
|
||||
}
|
||||
|
||||
query := `
|
||||
SELECT
|
||||
user_id,
|
||||
key,
|
||||
value
|
||||
FROM user_setting
|
||||
WHERE ` + strings.Join(where, " AND ")
|
||||
rows, err := d.db.QueryContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
userSettingList := make([]*store.UserSetting, 0)
|
||||
for rows.Next() {
|
||||
userSetting := &store.UserSetting{}
|
||||
var keyString string
|
||||
if err := rows.Scan(
|
||||
&userSetting.UserID,
|
||||
&keyString,
|
||||
&userSetting.Value,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
userSetting.Key = storepb.UserSetting_Key(storepb.UserSetting_Key_value[keyString])
|
||||
userSettingList = append(userSettingList, userSetting)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return userSettingList, nil
|
||||
}
|
||||
72
store/db/sqlite/workspace_setting.go
Normal file
72
store/db/sqlite/workspace_setting.go
Normal file
@@ -0,0 +1,72 @@
|
||||
package sqlite
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"github.com/usememos/memos/store"
|
||||
)
|
||||
|
||||
func (d *DB) UpsertWorkspaceSetting(ctx context.Context, upsert *store.WorkspaceSetting) (*store.WorkspaceSetting, error) {
|
||||
stmt := `
|
||||
INSERT INTO system_setting (
|
||||
name, value, description
|
||||
)
|
||||
VALUES (?, ?, ?)
|
||||
ON CONFLICT(name) DO UPDATE
|
||||
SET
|
||||
value = EXCLUDED.value,
|
||||
description = EXCLUDED.description
|
||||
`
|
||||
if _, err := d.db.ExecContext(ctx, stmt, upsert.Name, upsert.Value, upsert.Description); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return upsert, nil
|
||||
}
|
||||
|
||||
func (d *DB) ListWorkspaceSettings(ctx context.Context, find *store.FindWorkspaceSetting) ([]*store.WorkspaceSetting, error) {
|
||||
where, args := []string{"1 = 1"}, []any{}
|
||||
if find.Name != "" {
|
||||
where, args = append(where, "name = ?"), append(args, find.Name)
|
||||
}
|
||||
|
||||
query := `
|
||||
SELECT
|
||||
name,
|
||||
value,
|
||||
description
|
||||
FROM system_setting
|
||||
WHERE ` + strings.Join(where, " AND ")
|
||||
|
||||
rows, err := d.db.QueryContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
list := []*store.WorkspaceSetting{}
|
||||
for rows.Next() {
|
||||
systemSettingMessage := &store.WorkspaceSetting{}
|
||||
if err := rows.Scan(
|
||||
&systemSettingMessage.Name,
|
||||
&systemSettingMessage.Value,
|
||||
&systemSettingMessage.Description,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
list = append(list, systemSettingMessage)
|
||||
}
|
||||
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return list, nil
|
||||
}
|
||||
|
||||
func (d *DB) DeleteWorkspaceSetting(ctx context.Context, delete *store.DeleteWorkspaceSetting) error {
|
||||
stmt := "DELETE FROM system_setting WHERE name = ?"
|
||||
_, err := d.db.ExecContext(ctx, stmt, delete.Name)
|
||||
return err
|
||||
}
|
||||
Reference in New Issue
Block a user