Skip to content

Commit

Permalink
test deleteMissing with schema changes
Browse files Browse the repository at this point in the history
  • Loading branch information
ganigeorgiev committed Aug 10, 2022
1 parent ac0c23f commit 65b8301
Show file tree
Hide file tree
Showing 15 changed files with 488 additions and 136 deletions.
2 changes: 1 addition & 1 deletion cmd/migrate.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ Supported arguments are:
- up - runs all available migrations.
- down [number] - reverts the last [number] applied migrations.
- create name [folder] - creates new migration template file.
- collections [folder] - creates new migration file with the current collections configuration.
- collections [folder] - (Experimental) creates new migration file with the most recent local collections configuration.
`
var databaseFlag string

Expand Down
11 changes: 10 additions & 1 deletion daos/collection.go
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ func (dao *Dao) ImportCollections(

mappedImported := make(map[string]*models.Collection, len(importedCollections))
for _, imported := range importedCollections {
// normalize
// normalize ids
if !imported.HasId() {
// generate id if not set
imported.MarkAsNew()
Expand All @@ -199,6 +199,15 @@ func (dao *Dao) ImportCollections(
imported.MarkAsNew()
}

// extend existing schema
if existing, ok := mappedExisting[imported.GetId()]; ok && !deleteMissing {
schema, _ := existing.Schema.Clone()
for _, f := range imported.Schema.Fields() {
schema.AddField(f) // add or replace
}
imported.Schema = *schema
}

mappedImported[imported.GetId()] = imported
}

Expand Down
33 changes: 31 additions & 2 deletions forms/collection_upsert.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package forms

import (
"fmt"
"regexp"
"strings"

Expand Down Expand Up @@ -115,6 +116,7 @@ func (form *CollectionUpsert) Validate() error {
&form.Schema,
validation.By(form.ensureNoSystemFieldsChange),
validation.By(form.ensureNoFieldsTypeChange),
validation.By(form.ensureExistingRelationCollectionId),
),
validation.Field(&form.ListRule, validation.By(form.checkRule)),
validation.Field(&form.ViewRule, validation.By(form.checkRule)),
Expand Down Expand Up @@ -161,11 +163,38 @@ func (form *CollectionUpsert) ensureNoSystemFlagChange(value any) error {
func (form *CollectionUpsert) ensureNoFieldsTypeChange(value any) error {
v, _ := value.(schema.Schema)

for _, field := range v.Fields() {
for i, field := range v.Fields() {
oldField := form.collection.Schema.GetFieldById(field.Id)

if oldField != nil && oldField.Type != field.Type {
return validation.NewError("validation_field_type_change", "Field type cannot be changed.")
return validation.Errors{fmt.Sprint(i): validation.NewError(
"validation_field_type_change",
"Field type cannot be changed.",
)}
}
}

return nil
}

func (form *CollectionUpsert) ensureExistingRelationCollectionId(value any) error {
v, _ := value.(schema.Schema)

for i, field := range v.Fields() {
if field.Type != schema.FieldTypeRelation {
continue
}

options, _ := field.Options.(*schema.RelationOptions)
if options == nil {
continue
}

if _, err := form.config.TxDao.FindCollectionByNameOrId(options.CollectionId); err != nil {
return validation.Errors{fmt.Sprint(i): validation.NewError(
"validation_field_invalid_relation",
"The relation collection doesn't exist.",
)}
}
}

Expand Down
4 changes: 2 additions & 2 deletions forms/collections_import.go
Original file line number Diff line number Diff line change
Expand Up @@ -139,11 +139,11 @@ func (form *CollectionsImport) beforeRecordsSync(txDao *daos.Dao, mappedNew, map

if err := upsertForm.Validate(); err != nil {
// serialize the validation error(s)
serializedErr, _ := json.Marshal(err)
serializedErr, _ := json.MarshalIndent(err, "", " ")

return validation.Errors{"collections": validation.NewError(
"collections_import_validate_failure",
fmt.Sprintf("Data validations failed for collection %q (%s): %s", collection.Name, collection.Id, serializedErr),
fmt.Sprintf("Data validations failed for collection %q (%s):\n%s", collection.Name, collection.Id, serializedErr),
)}
}
}
Expand Down
2 changes: 1 addition & 1 deletion ui/src/components/base/Field.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
{#each fieldErrors as error}
<div class="help-block help-block-error">
{#if typeof error === "object"}
{error?.message || error?.code || defaultError}
<pre>{error?.message || error?.code || defaultError}</pre>
{:else}
{error || defaultError}
{/if}
Expand Down
2 changes: 2 additions & 0 deletions ui/src/components/base/Toasts.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
<i class="ri-information-line" />
{:else if toast.type === "success"}
<i class="ri-checkbox-circle-line" />
{:else if toast.type === "warning"}
<i class="ri-error-warning-line" />
{:else}
<i class="ri-alert-line" />
{/if}
Expand Down
264 changes: 264 additions & 0 deletions ui/src/components/collections/CollectionsDiffTable.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,264 @@
<script>
import { Collection } from "pocketbase";
import CommonHelper from "@/utils/CommonHelper";
export let collectionA = new Collection();
export let collectionB = new Collection();
export let deleteMissing = false;
$: isDeleteDiff = !collectionB?.id && !collectionB?.name;
$: isCreateDiff = !isDeleteDiff && !collectionA?.id;
$: schemaA = Array.isArray(collectionA?.schema) ? collectionA?.schema : [];
$: schemaB = Array.isArray(collectionB?.schema) ? collectionB?.schema : [];
$: removedFields = schemaA.filter((fieldA) => {
return !schemaB.find((fieldB) => fieldA.id == fieldB.id);
});
$: sharedFields = schemaB.filter((fieldB) => {
return schemaA.find((fieldA) => fieldA.id == fieldB.id);
});
$: addedFields = schemaB.filter((fieldB) => {
return !schemaA.find((fieldA) => fieldA.id == fieldB.id);
});
$: if (typeof deleteMissing !== "undefined") {
normalizeSchemaB();
}
$: hasAnyChange = detectChanges(collectionA, collectionB);
const mainModelProps = Object.keys(new Collection().export()).filter(
(key) => !["schema", "created", "updated"].includes(key)
);
function normalizeSchemaB() {
schemaB = Array.isArray(collectionB?.schema) ? collectionB?.schema : [];
if (!deleteMissing) {
schemaB = schemaB.concat(removedFields);
}
}
function getFieldById(schema, id) {
schema = schema || [];
for (let field of schema) {
if (field.id == id) {
return field;
}
}
return null;
}
function detectChanges() {
// added or removed fields
if (addedFields?.length || (deleteMissing && removedFields?.length)) {
return true;
}
// changes in the main model props
for (let prop of mainModelProps) {
if (hasChanges(collectionA?.[prop], collectionB?.[prop])) {
return true;
}
}
// changes in the schema fields
for (let field of sharedFields) {
if (hasChanges(field, CommonHelper.findByKey(schemaA, "id", field.id))) {
return true;
}
}
return false;
}
function hasChanges(valA, valB) {
// direct match
if (valA === valB) {
return false;
}
return JSON.stringify(valA) !== JSON.stringify(valB);
}
function displayValue(value) {
if (typeof value === "undefined") {
return "N/A";
}
return CommonHelper.isObject(value) ? JSON.stringify(value, null, 4) : value;
}
</script>
<div class="section-title">
{#if !collectionA?.id}
<strong>{collectionB?.name}</strong>
<span class="label label-success">Added</span>
{:else if !collectionB?.id}
<strong>{collectionA?.name}</strong>
<span class="label label-danger">Removed</span>
{:else}
<div class="inline-flex fleg-gap-5">
{#if collectionA.name !== collectionB.name}
<strong class="txt-strikethrough txt-hint">{collectionA.name}</strong>
<i class="ri-arrow-right-line txt-sm" />
{/if}
<strong class="txt">{collectionB.name}</strong>
{#if hasAnyChange}
<span class="label label-warning">Changed</span>
{/if}
</div>
{/if}
</div>
<table class="table collections-diff-table m-b-base">
<thead>
<tr>
<th>Props</th>
<th width="10%">Old</th>
<th width="10%">New</th>
</tr>
</thead>
<tbody>
{#each mainModelProps as prop}
<tr class:txt-primary={hasChanges(collectionA?.[prop], collectionB?.[prop])}>
<td class="min-width">
<span>{prop}</span>
</td>
<td
class:changed-old-col={!isCreateDiff &&
hasChanges(collectionA?.[prop], collectionB?.[prop])}
class:changed-none-col={isCreateDiff}
>
<pre class="txt">{displayValue(collectionA?.[prop])}</pre>
</td>
<td
class:changed-new-col={!isDeleteDiff &&
hasChanges(collectionA?.[prop], collectionB?.[prop])}
class:changed-none-col={isDeleteDiff}
>
<pre class="txt">{displayValue(collectionB?.[prop])}</pre>
</td>
</tr>
{/each}
{#if deleteMissing || isDeleteDiff}
{#each removedFields as field}
<tr>
<th class="min-width" colspan="3">
<span class="txt">schema.{field.name}</span>
<span class="label label-danger m-l-5">
Removed - <small>
All stored data related to <strong>{field.name}</strong> will be deleted!
</small>
</span>
</th>
</tr>
{#each Object.entries(field) as [key, value]}
<tr class="txt-primary">
<td class="min-width field-key-col">{key}</td>
<td class="changed-old-col">
<pre class="txt">{displayValue(value)}</pre>
</td>
<td class="changed-none-col" />
</tr>
{/each}
{/each}
{/if}
{#each sharedFields as field}
<tr>
<th class="min-width" colspan="3">
<span class="txt">schema.{field.name}</span>
{#if hasChanges(getFieldById(schemaA, field.id), getFieldById(schemaB, field.id))}
<span class="label label-warning m-l-5">Changed</span>
{/if}
</th>
</tr>
{#each Object.entries(field) as [key, newValue]}
<tr class:txt-primary={hasChanges(getFieldById(schemaA, field.id)?.[key], newValue)}>
<td class="min-width field-key-col">{key}</td>
<td class:changed-old-col={hasChanges(getFieldById(schemaA, field.id)?.[key], newValue)}>
<pre class="txt">{displayValue(getFieldById(schemaA, field.id)?.[key])}</pre>
</td>
<td class:changed-new-col={hasChanges(getFieldById(schemaA, field.id)?.[key], newValue)}>
<pre class="txt">{displayValue(newValue)}</pre>
</td>
</tr>
{/each}
{/each}
{#each addedFields as field}
<tr>
<th class="min-width" colspan="3">
<span class="txt">schema.{field.name}</span>
<span class="label label-success m-l-5">Added</span>
</th>
</tr>
{#each Object.entries(field) as [key, value]}
<tr class="txt-primary">
<td class="min-width field-key-col">{key}</td>
<td class="changed-none-col" />
<td class="changed-new-col">
<pre class="txt">{displayValue(value)}</pre>
</td>
</tr>
{/each}
{/each}
</tbody>
</table>
<style lang="scss">
.collections-diff-table {
color: var(--txtHintColor);
border: 2px solid var(--primaryColor);
tr {
background: none;
}
th,
td {
height: auto;
padding: 2px 15px;
border-bottom: 1px solid rgba(#000, 0.07);
}
th {
height: 35px;
padding: 4px 15px;
color: var(--txtPrimaryColor);
}
thead tr {
background: var(--primaryColor);
th {
color: var(--baseColor);
background: none;
}
}
.label {
font-weight: normal;
}
.changed-none-col {
color: var(--txtDisabledColor);
background: var(--baseAlt1Color);
}
.changed-old-col {
color: var(--txtPrimaryColor);
background: var(--dangerAltColor);
}
.changed-new-col {
color: var(--txtPrimaryColor);
background: var(--successAltColor);
}
.field-key-col {
padding-left: 30px;
}
}
</style>
Loading

0 comments on commit 65b8301

Please sign in to comment.