feat(appwrite): Schema für Community-Logos, Herzen und Logo-Meldungen
- Setup-Skript: Tabellen `logos`, `logo_likes` (Mehrspalten-Index `user_logo` über userId, logoId) und `logo_reports` sowie der Bucket `logo-images` (nur PNG, höchstens 1 MiB); der Kopfkommentar nennt die neuen Objekte. Der Index-Request schickt `columns` schon als Liste, dort war nichts zu ändern. - Tests: DESIRED mit genau diesen Werten, Anlegen samt Indizes gegen den Fake-Server, --check meldet jedes fehlende Objekt (51 statt 28) mit Exit 1, bestehende Tabellen und Buckets bleiben unberührt. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
e1fb632cdd
commit
88abc2061d
2 files changed
+315
-20
No files matched your search
@@ -3,8 +3,9 @@
|
|||||||
* AirCard – Appwrite-Schema-Setup (idempotent)
|
* AirCard – Appwrite-Schema-Setup (idempotent)
|
||||||
*
|
*
|
||||||
* Legt im selbst gehosteten Appwrite (Projekt `aircard`) die Datenbank `aircard`,
|
* Legt im selbst gehosteten Appwrite (Projekt `aircard`) die Datenbank `aircard`,
|
||||||
* die Tabellen `cards`, `gallery` und `reports` (TablesDB, mit Spalten und
|
* die Tabellen `cards`, `gallery`, `reports`, `logos`, `logo_likes` und
|
||||||
* Indizes) und die Buckets `card-images` und `gallery-images` an.
|
* `logo_reports` (TablesDB, mit Spalten und Indizes) und die Buckets
|
||||||
|
* `card-images`, `gallery-images` und `logo-images` an.
|
||||||
* Reines `fetch`, keine Abhängigkeiten, Node 22+.
|
* Reines `fetch`, keine Abhängigkeiten, Node 22+.
|
||||||
*
|
*
|
||||||
* Aufruf (vom Repo-Wurzelverzeichnis):
|
* Aufruf (vom Repo-Wurzelverzeichnis):
|
||||||
@@ -40,7 +41,7 @@ const str = (key, size, required) => ({ key, type: 'string', size, required });
|
|||||||
|
|
||||||
export const DESIRED = {
|
export const DESIRED = {
|
||||||
database: { id: 'aircard', name: 'AirCard' },
|
database: { id: 'aircard', name: 'AirCard' },
|
||||||
// Reihenfolge ist Ablauf: cards, gallery, reports (je Tabelle: Spalten, dann Indizes).
|
// Reihenfolge ist Ablauf: cards, gallery, reports, logos, logo_likes, logo_reports (je Tabelle: Spalten, dann Indizes).
|
||||||
tables: [
|
tables: [
|
||||||
{
|
{
|
||||||
id: 'cards',
|
id: 'cards',
|
||||||
@@ -88,6 +89,54 @@ export const DESIRED = {
|
|||||||
],
|
],
|
||||||
indexes: [{ key: 'gallery', type: 'key', columns: ['galleryId'] }],
|
indexes: [{ key: 'gallery', type: 'key', columns: ['galleryId'] }],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
// Community-Logos. Wie `gallery`: Zeilenrechte setzt nur die API, Clients dürfen nichts auf Tabellenebene.
|
||||||
|
id: 'logos',
|
||||||
|
name: 'Logos',
|
||||||
|
permissions: [],
|
||||||
|
rowSecurity: true,
|
||||||
|
columns: [
|
||||||
|
str('name', 40, true),
|
||||||
|
str('displayName', 30, false),
|
||||||
|
str('fileId', 36, true),
|
||||||
|
str('ownerId', 36, true),
|
||||||
|
{ key: 'mono', type: 'boolean', required: false, default: false },
|
||||||
|
{ key: 'likeCount', type: 'integer', required: false, default: 0, min: 0 },
|
||||||
|
{ key: 'hidden', type: 'boolean', required: false, default: false },
|
||||||
|
{ key: 'reportCount', type: 'integer', required: false, default: 0, min: 0 },
|
||||||
|
],
|
||||||
|
indexes: [
|
||||||
|
{ key: 'owner', type: 'key', columns: ['ownerId'] },
|
||||||
|
{ key: 'hidden', type: 'key', columns: ['hidden'] },
|
||||||
|
{ key: 'likes', type: 'key', columns: ['likeCount'] },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Ein Herz je Konto und Logo: Zeilen-ID hash32("<logoId>|<userId>"), lesen darf nur das Konto selbst; geschrieben wird nur über die API.
|
||||||
|
id: 'logo_likes',
|
||||||
|
name: 'Logo Likes',
|
||||||
|
permissions: [],
|
||||||
|
rowSecurity: true,
|
||||||
|
columns: [str('logoId', 36, true), str('userId', 36, true)],
|
||||||
|
indexes: [
|
||||||
|
{ key: 'logo', type: 'key', columns: ['logoId'] },
|
||||||
|
// Mehrspalten-Index: die Reihenfolge der Spalten zählt (und wird verglichen).
|
||||||
|
{ key: 'user_logo', type: 'key', columns: ['userId', 'logoId'] },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Nur Admins lesen (Label `admin`); geschrieben wird ausschließlich über die API.
|
||||||
|
id: 'logo_reports',
|
||||||
|
name: 'Logo Reports',
|
||||||
|
permissions: ['read("label:admin")'],
|
||||||
|
rowSecurity: false,
|
||||||
|
columns: [
|
||||||
|
str('logoId', 36, true),
|
||||||
|
{ key: 'reason', type: 'enum', elements: ['unangemessen', 'kartendaten', 'urheberrecht', 'spam', 'sonstiges'], required: true },
|
||||||
|
str('reporterHash', 64, true),
|
||||||
|
],
|
||||||
|
indexes: [{ key: 'logo', type: 'key', columns: ['logoId'] }],
|
||||||
|
},
|
||||||
],
|
],
|
||||||
buckets: [
|
buckets: [
|
||||||
{
|
{
|
||||||
@@ -110,6 +159,17 @@ export const DESIRED = {
|
|||||||
compression: 'none',
|
compression: 'none',
|
||||||
antivirus: false,
|
antivirus: false,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
// Logo-PNG (höchstens 1 MiB); die Dateirechte setzt die API je Datei.
|
||||||
|
id: 'logo-images',
|
||||||
|
name: 'Logo Images',
|
||||||
|
permissions: ['create("users")'],
|
||||||
|
fileSecurity: true,
|
||||||
|
maximumFileSize: 1048576,
|
||||||
|
allowedFileExtensions: ['png'],
|
||||||
|
compression: 'none',
|
||||||
|
antivirus: false,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -236,6 +236,16 @@ async function run(fake, opts = {}) {
|
|||||||
|
|
||||||
const startsWith = (lines, prefix) => lines.filter((l) => l.startsWith(prefix));
|
const startsWith = (lines, prefix) => lines.filter((l) => l.startsWith(prefix));
|
||||||
|
|
||||||
|
/** Stellt einen älteren Schema-Stand nach: entfernt die Tabellen (samt Spalten und Indizes) und Buckets aus dem Fake. */
|
||||||
|
function dropFromFake(fake, { tables = [], buckets = [] }) {
|
||||||
|
for (const map of [fake.state.tables, fake.state.columns, fake.state.indexes]) {
|
||||||
|
for (const id of [...map.keys()]) {
|
||||||
|
if (tables.some((t) => id === `aircard/${t}` || id.startsWith(`aircard/${t}/`))) map.delete(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const id of buckets) fake.state.buckets.delete(id);
|
||||||
|
}
|
||||||
|
|
||||||
function runCli(args, env) {
|
function runCli(args, env) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const child = spawn(process.execPath, [SCRIPT, ...args], {
|
const child = spawn(process.execPath, [SCRIPT, ...args], {
|
||||||
@@ -260,10 +270,10 @@ describe('DESIRED', () => {
|
|||||||
const bucket = (id) => DESIRED.buckets.find((b) => b.id === id);
|
const bucket = (id) => DESIRED.buckets.find((b) => b.id === id);
|
||||||
const str = (key, size, required) => ({ key, type: 'string', size, required });
|
const str = (key, size, required) => ({ key, type: 'string', size, required });
|
||||||
|
|
||||||
test('Reihenfolge: cards zuerst, dann gallery, reports; Buckets card-images, gallery-images', () => {
|
test('Reihenfolge: cards zuerst, dann gallery, reports, logos, logo_likes, logo_reports; Buckets card-images, gallery-images, logo-images', () => {
|
||||||
assert.deepEqual(DESIRED.database, { id: 'aircard', name: 'AirCard' });
|
assert.deepEqual(DESIRED.database, { id: 'aircard', name: 'AirCard' });
|
||||||
assert.deepEqual(DESIRED.tables.map((t) => t.id), ['cards', 'gallery', 'reports']);
|
assert.deepEqual(DESIRED.tables.map((t) => t.id), ['cards', 'gallery', 'reports', 'logos', 'logo_likes', 'logo_reports']);
|
||||||
assert.deepEqual(DESIRED.buckets.map((b) => b.id), ['card-images', 'gallery-images']);
|
assert.deepEqual(DESIRED.buckets.map((b) => b.id), ['card-images', 'gallery-images', 'logo-images']);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('cards entspricht dem bisherigen Schema (unverändert)', () => {
|
test('cards entspricht dem bisherigen Schema (unverändert)', () => {
|
||||||
@@ -336,6 +346,66 @@ describe('DESIRED', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('logos entspricht Logo-Spec §8', () => {
|
||||||
|
const t = table('logos');
|
||||||
|
assert.equal(t.name, 'Logos');
|
||||||
|
assert.deepEqual(t.permissions, []);
|
||||||
|
assert.equal(t.rowSecurity, true);
|
||||||
|
assert.deepEqual(t.columns, [
|
||||||
|
str('name', 40, true),
|
||||||
|
str('displayName', 30, false),
|
||||||
|
str('fileId', 36, true),
|
||||||
|
str('ownerId', 36, true),
|
||||||
|
{ key: 'mono', type: 'boolean', required: false, default: false },
|
||||||
|
{ key: 'likeCount', type: 'integer', required: false, default: 0, min: 0 },
|
||||||
|
{ key: 'hidden', type: 'boolean', required: false, default: false },
|
||||||
|
{ key: 'reportCount', type: 'integer', required: false, default: 0, min: 0 },
|
||||||
|
]);
|
||||||
|
assert.deepEqual(t.indexes, [
|
||||||
|
{ key: 'owner', type: 'key', columns: ['ownerId'] },
|
||||||
|
{ key: 'hidden', type: 'key', columns: ['hidden'] },
|
||||||
|
{ key: 'likes', type: 'key', columns: ['likeCount'] },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('logo_likes entspricht Logo-Spec §8 (Mehrspalten-Index user_logo: userId, logoId)', () => {
|
||||||
|
const t = table('logo_likes');
|
||||||
|
assert.equal(t.name, 'Logo Likes');
|
||||||
|
assert.deepEqual(t.permissions, []);
|
||||||
|
assert.equal(t.rowSecurity, true);
|
||||||
|
assert.deepEqual(t.columns, [str('logoId', 36, true), str('userId', 36, true)]);
|
||||||
|
assert.deepEqual(t.indexes, [
|
||||||
|
{ key: 'logo', type: 'key', columns: ['logoId'] },
|
||||||
|
{ key: 'user_logo', type: 'key', columns: ['userId', 'logoId'] },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('logo_reports entspricht Logo-Spec §8', () => {
|
||||||
|
const t = table('logo_reports');
|
||||||
|
assert.equal(t.name, 'Logo Reports');
|
||||||
|
assert.deepEqual(t.permissions, ['read("label:admin")']);
|
||||||
|
assert.equal(t.rowSecurity, false);
|
||||||
|
assert.deepEqual(t.columns, [
|
||||||
|
str('logoId', 36, true),
|
||||||
|
{ key: 'reason', type: 'enum', elements: ['unangemessen', 'kartendaten', 'urheberrecht', 'spam', 'sonstiges'], required: true },
|
||||||
|
str('reporterHash', 64, true),
|
||||||
|
]);
|
||||||
|
assert.deepEqual(t.indexes, [{ key: 'logo', type: 'key', columns: ['logoId'] }]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('logo-images entspricht Logo-Spec §8', () => {
|
||||||
|
assert.deepEqual(bucket('logo-images'), {
|
||||||
|
id: 'logo-images',
|
||||||
|
name: 'Logo Images',
|
||||||
|
permissions: ['create("users")'],
|
||||||
|
fileSecurity: true,
|
||||||
|
maximumFileSize: 1048576,
|
||||||
|
allowedFileExtensions: ['png'],
|
||||||
|
compression: 'none',
|
||||||
|
antivirus: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
test('Fulltext-Indizes decken genau eine Spalte ab, alle Index-Spalten existieren', () => {
|
test('Fulltext-Indizes decken genau eine Spalte ab, alle Index-Spalten existieren', () => {
|
||||||
for (const t of DESIRED.tables) {
|
for (const t of DESIRED.tables) {
|
||||||
const keys = t.columns.map((c) => c.key);
|
const keys = t.columns.map((c) => c.key);
|
||||||
@@ -538,6 +608,18 @@ describe('compareIndex', () => {
|
|||||||
const { columns, ...none } = ok();
|
const { columns, ...none } = ok();
|
||||||
assert.deepEqual(compareIndex(none, desired), ['columns ist [], erwartet [searchText]']);
|
assert.deepEqual(compareIndex(none, desired), ['columns ist [], erwartet [searchText]']);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('Mehrspalten-Index (logo_likes.user_logo): die Reihenfolge der Spalten zählt', () => {
|
||||||
|
const userLogo = DESIRED.tables.find((t) => t.id === 'logo_likes').indexes.find((i) => i.key === 'user_logo');
|
||||||
|
const okMulti = { key: 'user_logo', type: 'key', columns: ['userId', 'logoId'], lengths: [], orders: [], status: 'available' };
|
||||||
|
assert.deepEqual(compareIndex(okMulti, userLogo), []);
|
||||||
|
assert.deepEqual(compareIndex({ ...okMulti, columns: ['logoId', 'userId'] }, userLogo), [
|
||||||
|
'columns ist [logoId, userId], erwartet [userId, logoId]',
|
||||||
|
]);
|
||||||
|
assert.deepEqual(compareIndex({ ...okMulti, columns: ['userId'] }, userLogo), [
|
||||||
|
'columns ist [userId], erwartet [userId, logoId]',
|
||||||
|
]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('compareBucket', () => {
|
describe('compareBucket', () => {
|
||||||
@@ -588,6 +670,21 @@ describe('compareBucket', () => {
|
|||||||
'allowedFileExtensions ist [jpg, png, webp], erwartet [png, webp]',
|
'allowedFileExtensions ist [jpg, png, webp], erwartet [png, webp]',
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('logo-images: nur png, höchstens 1 MiB', () => {
|
||||||
|
const logo = DESIRED.buckets[2];
|
||||||
|
const okLogo = {
|
||||||
|
$id: 'logo-images', name: 'Logo Images', $permissions: ['create("users")'],
|
||||||
|
fileSecurity: true, maximumFileSize: 1048576, allowedFileExtensions: ['png'], compression: 'none', antivirus: false,
|
||||||
|
};
|
||||||
|
assert.deepEqual(compareBucket(okLogo, logo), []);
|
||||||
|
assert.deepEqual(compareBucket({ ...okLogo, allowedFileExtensions: ['png', 'webp'] }, logo), [
|
||||||
|
'allowedFileExtensions ist [png, webp], erwartet [png]',
|
||||||
|
]);
|
||||||
|
assert.deepEqual(compareBucket({ ...okLogo, maximumFileSize: 5242880 }, logo), [
|
||||||
|
'maximumFileSize ist 5242880, erwartet 1048576',
|
||||||
|
]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -622,10 +719,35 @@ describe('main gegen Fake-Server', () => {
|
|||||||
'Spalte reports.reason',
|
'Spalte reports.reason',
|
||||||
'Spalte reports.reporterHash',
|
'Spalte reports.reporterHash',
|
||||||
'Index reports.gallery',
|
'Index reports.gallery',
|
||||||
|
'Tabelle logos',
|
||||||
|
'Spalte logos.name',
|
||||||
|
'Spalte logos.displayName',
|
||||||
|
'Spalte logos.fileId',
|
||||||
|
'Spalte logos.ownerId',
|
||||||
|
'Spalte logos.mono',
|
||||||
|
'Spalte logos.likeCount',
|
||||||
|
'Spalte logos.hidden',
|
||||||
|
'Spalte logos.reportCount',
|
||||||
|
'Index logos.owner',
|
||||||
|
'Index logos.hidden',
|
||||||
|
'Index logos.likes',
|
||||||
|
'Tabelle logo_likes',
|
||||||
|
'Spalte logo_likes.logoId',
|
||||||
|
'Spalte logo_likes.userId',
|
||||||
|
'Index logo_likes.logo',
|
||||||
|
'Index logo_likes.user_logo',
|
||||||
|
'Tabelle logo_reports',
|
||||||
|
'Spalte logo_reports.logoId',
|
||||||
|
'Spalte logo_reports.reason',
|
||||||
|
'Spalte logo_reports.reporterHash',
|
||||||
|
'Index logo_reports.logo',
|
||||||
'Bucket card-images',
|
'Bucket card-images',
|
||||||
'Bucket gallery-images',
|
'Bucket gallery-images',
|
||||||
|
'Bucket logo-images',
|
||||||
];
|
];
|
||||||
const NEW_OBJECTS = EXPECTED_OBJECTS.filter((o) => /gallery|reports/.test(o));
|
// Erweiterung um die Galerie (gallery, reports, gallery-images) bzw. um die Logos (logos, logo_likes, logo_reports, logo-images)
|
||||||
|
const GALLERY_OBJECTS = EXPECTED_OBJECTS.filter((o) => /^(Tabelle|Spalte|Index) (gallery|reports)(\.|$)|^Bucket gallery-images$/.test(o));
|
||||||
|
const LOGO_OBJECTS = EXPECTED_OBJECTS.filter((o) => /^(Tabelle|Spalte|Index) logo(s|_likes|_reports)(\.|$)|^Bucket logo-images$/.test(o));
|
||||||
|
|
||||||
const T = '/v1/tablesdb/aircard/tables';
|
const T = '/v1/tablesdb/aircard/tables';
|
||||||
const str = (table, key, size, required) => [`${T}/${table}/columns/string`, { key, size, required }];
|
const str = (table, key, size, required) => [`${T}/${table}/columns/string`, { key, size, required }];
|
||||||
@@ -658,6 +780,31 @@ describe('main gegen Fake-Server', () => {
|
|||||||
}],
|
}],
|
||||||
str('reports', 'reporterHash', 64, true),
|
str('reports', 'reporterHash', 64, true),
|
||||||
[`${T}/reports/indexes`, { key: 'gallery', type: 'key', columns: ['galleryId'] }],
|
[`${T}/reports/indexes`, { key: 'gallery', type: 'key', columns: ['galleryId'] }],
|
||||||
|
[T, { tableId: 'logos', name: 'Logos', permissions: [], rowSecurity: true }],
|
||||||
|
str('logos', 'name', 40, true),
|
||||||
|
str('logos', 'displayName', 30, false),
|
||||||
|
str('logos', 'fileId', 36, true),
|
||||||
|
str('logos', 'ownerId', 36, true),
|
||||||
|
[`${T}/logos/columns/boolean`, { key: 'mono', required: false, default: false }],
|
||||||
|
[`${T}/logos/columns/integer`, { key: 'likeCount', required: false, default: 0, min: 0 }],
|
||||||
|
[`${T}/logos/columns/boolean`, { key: 'hidden', required: false, default: false }],
|
||||||
|
[`${T}/logos/columns/integer`, { key: 'reportCount', required: false, default: 0, min: 0 }],
|
||||||
|
[`${T}/logos/indexes`, { key: 'owner', type: 'key', columns: ['ownerId'] }],
|
||||||
|
[`${T}/logos/indexes`, { key: 'hidden', type: 'key', columns: ['hidden'] }],
|
||||||
|
[`${T}/logos/indexes`, { key: 'likes', type: 'key', columns: ['likeCount'] }],
|
||||||
|
[T, { tableId: 'logo_likes', name: 'Logo Likes', permissions: [], rowSecurity: true }],
|
||||||
|
str('logo_likes', 'logoId', 36, true),
|
||||||
|
str('logo_likes', 'userId', 36, true),
|
||||||
|
[`${T}/logo_likes/indexes`, { key: 'logo', type: 'key', columns: ['logoId'] }],
|
||||||
|
// Mehrspalten-Index: `columns` geht als Liste in dieser Reihenfolge an Appwrite
|
||||||
|
[`${T}/logo_likes/indexes`, { key: 'user_logo', type: 'key', columns: ['userId', 'logoId'] }],
|
||||||
|
[T, { tableId: 'logo_reports', name: 'Logo Reports', permissions: ['read("label:admin")'], rowSecurity: false }],
|
||||||
|
str('logo_reports', 'logoId', 36, true),
|
||||||
|
[`${T}/logo_reports/columns/enum`, {
|
||||||
|
key: 'reason', elements: ['unangemessen', 'kartendaten', 'urheberrecht', 'spam', 'sonstiges'], required: true,
|
||||||
|
}],
|
||||||
|
str('logo_reports', 'reporterHash', 64, true),
|
||||||
|
[`${T}/logo_reports/indexes`, { key: 'logo', type: 'key', columns: ['logoId'] }],
|
||||||
['/v1/storage/buckets', {
|
['/v1/storage/buckets', {
|
||||||
bucketId: 'card-images', name: 'Card Images', permissions: ['create("users")'], fileSecurity: true,
|
bucketId: 'card-images', name: 'Card Images', permissions: ['create("users")'], fileSecurity: true,
|
||||||
maximumFileSize: 5242880, allowedFileExtensions: ['png', 'jpg', 'jpeg', 'webp'],
|
maximumFileSize: 5242880, allowedFileExtensions: ['png', 'jpg', 'jpeg', 'webp'],
|
||||||
@@ -668,16 +815,22 @@ describe('main gegen Fake-Server', () => {
|
|||||||
maximumFileSize: 5242880, allowedFileExtensions: ['png', 'webp'],
|
maximumFileSize: 5242880, allowedFileExtensions: ['png', 'webp'],
|
||||||
compression: 'none', antivirus: false,
|
compression: 'none', antivirus: false,
|
||||||
}],
|
}],
|
||||||
|
['/v1/storage/buckets', {
|
||||||
|
bucketId: 'logo-images', name: 'Logo Images', permissions: ['create("users")'], fileSecurity: true,
|
||||||
|
maximumFileSize: 1048576, allowedFileExtensions: ['png'],
|
||||||
|
compression: 'none', antivirus: false,
|
||||||
|
}],
|
||||||
];
|
];
|
||||||
|
const isLogoPost = ([path, body]) => /\/tables\/logo/.test(path) || /^logo/.test(body.tableId ?? body.bucketId ?? '');
|
||||||
|
|
||||||
test('--check auf leerem Projekt: Exit 1, alle 28 Objekte FEHLT, nichts angelegt', async () => {
|
test('--check auf leerem Projekt: Exit 1, alle 51 Objekte FEHLT, nichts angelegt', async () => {
|
||||||
await withFake({}, async (fake) => {
|
await withFake({}, async (fake) => {
|
||||||
const { code, lines } = await run(fake, { check: true });
|
const { code, lines } = await run(fake, { check: true });
|
||||||
assert.equal(code, 1);
|
assert.equal(code, 1);
|
||||||
assert.equal(EXPECTED_OBJECTS.length, 28);
|
assert.equal(EXPECTED_OBJECTS.length, 51);
|
||||||
assert.deepEqual(startsWith(lines, 'FEHLT'), EXPECTED_OBJECTS.map((o) => `FEHLT: ${o}`));
|
assert.deepEqual(startsWith(lines, 'FEHLT'), EXPECTED_OBJECTS.map((o) => `FEHLT: ${o}`));
|
||||||
assert.equal(startsWith(lines, 'OK').length, 0);
|
assert.equal(startsWith(lines, 'OK').length, 0);
|
||||||
assert.ok(lines.includes('UNVOLLSTÄNDIG: 28 fehlend.'), lines.join('\n'));
|
assert.ok(lines.includes('UNVOLLSTÄNDIG: 51 fehlend.'), lines.join('\n'));
|
||||||
assert.equal(fake.posts().length, 0);
|
assert.equal(fake.posts().length, 0);
|
||||||
assert.equal(fake.otherWrites().length, 0);
|
assert.equal(fake.otherWrites().length, 0);
|
||||||
});
|
});
|
||||||
@@ -706,13 +859,16 @@ describe('main gegen Fake-Server', () => {
|
|||||||
assert.ok(!first.lines.join('\n').includes(KEY));
|
assert.ok(!first.lines.join('\n').includes(KEY));
|
||||||
|
|
||||||
// Alle Spalten und Indizes wurden vor dem Ende ausgewartet (Status available)
|
// Alle Spalten und Indizes wurden vor dem Ende ausgewartet (Status available)
|
||||||
assert.equal(fake.state.columns.size, 18);
|
assert.equal(fake.state.columns.size, 31);
|
||||||
assert.equal(fake.state.indexes.size, 4);
|
assert.equal(fake.state.indexes.size, 10);
|
||||||
for (const col of fake.state.columns.values()) assert.ok(col.gets >= 2, `Spalte ${col.key} nicht bis available geprüft`);
|
for (const col of fake.state.columns.values()) assert.ok(col.gets >= 2, `Spalte ${col.key} nicht bis available geprüft`);
|
||||||
for (const idx of fake.state.indexes.values()) assert.ok(idx.gets >= 2, `Index ${idx.key} nicht bis available geprüft`);
|
for (const idx of fake.state.indexes.values()) assert.ok(idx.gets >= 2, `Index ${idx.key} nicht bis available geprüft`);
|
||||||
|
|
||||||
// Indizes wurden erst angelegt, nachdem ALLE Spalten der Tabelle available waren
|
// Indizes wurden erst angelegt, nachdem ALLE Spalten der Tabelle available waren
|
||||||
assert.deepEqual(fake.state.indexPosts.map((p) => `${p.table}.${p.key}`), ['gallery.search_text', 'gallery.owner', 'gallery.hidden', 'reports.gallery']);
|
assert.deepEqual(fake.state.indexPosts.map((p) => `${p.table}.${p.key}`), [
|
||||||
|
'gallery.search_text', 'gallery.owner', 'gallery.hidden', 'reports.gallery',
|
||||||
|
'logos.owner', 'logos.hidden', 'logos.likes', 'logo_likes.logo', 'logo_likes.user_logo', 'logo_reports.logo',
|
||||||
|
]);
|
||||||
assert.ok(fake.state.indexPosts.every((p) => p.allColumnsAvailable), JSON.stringify(fake.state.indexPosts));
|
assert.ok(fake.state.indexPosts.every((p) => p.allColumnsAvailable), JSON.stringify(fake.state.indexPosts));
|
||||||
|
|
||||||
// Zweite Prüfung
|
// Zweite Prüfung
|
||||||
@@ -732,24 +888,21 @@ describe('main gegen Fake-Server', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Bestehendes Schema (nur cards + card-images) wird ergänzt, Bestehendes nicht angefasst', async () => {
|
test('Bestehendes Schema ohne gallery, reports, gallery-images wird ergänzt, Bestehendes nicht angefasst', async () => {
|
||||||
await withFake({}, async (fake) => {
|
await withFake({}, async (fake) => {
|
||||||
assert.equal((await run(fake)).code, 0);
|
assert.equal((await run(fake)).code, 0);
|
||||||
// Zustand vor der Erweiterung nachstellen
|
// Zustand vor der Erweiterung nachstellen
|
||||||
for (const map of [fake.state.tables, fake.state.columns, fake.state.indexes]) {
|
dropFromFake(fake, { tables: ['gallery', 'reports'], buckets: ['gallery-images'] });
|
||||||
for (const id of [...map.keys()]) if (/^aircard\/(gallery|reports)(\/|$)/.test(id)) map.delete(id);
|
|
||||||
}
|
|
||||||
fake.state.buckets.delete('gallery-images');
|
|
||||||
const requestsBefore = fake.state.requests.length;
|
const requestsBefore = fake.state.requests.length;
|
||||||
|
|
||||||
const check = await run(fake, { check: true });
|
const check = await run(fake, { check: true });
|
||||||
assert.equal(check.code, 1);
|
assert.equal(check.code, 1);
|
||||||
assert.deepEqual(startsWith(check.lines, 'FEHLT'), NEW_OBJECTS.map((o) => `FEHLT: ${o}`));
|
assert.deepEqual(startsWith(check.lines, 'FEHLT'), GALLERY_OBJECTS.map((o) => `FEHLT: ${o}`));
|
||||||
assert.equal(fake.posts().length, EXPECTED_POSTS.length);
|
assert.equal(fake.posts().length, EXPECTED_POSTS.length);
|
||||||
|
|
||||||
const apply = await run(fake);
|
const apply = await run(fake);
|
||||||
assert.equal(apply.code, 0, apply.lines.join('\n'));
|
assert.equal(apply.code, 0, apply.lines.join('\n'));
|
||||||
assert.deepEqual(startsWith(apply.lines, 'ANGELEGT'), NEW_OBJECTS.map((o) => `ANGELEGT: ${o}`));
|
assert.deepEqual(startsWith(apply.lines, 'ANGELEGT'), GALLERY_OBJECTS.map((o) => `ANGELEGT: ${o}`));
|
||||||
assert.ok(apply.lines.includes('OK: Tabelle cards'));
|
assert.ok(apply.lines.includes('OK: Tabelle cards'));
|
||||||
assert.ok(apply.lines.includes('OK: Bucket card-images'));
|
assert.ok(apply.lines.includes('OK: Bucket card-images'));
|
||||||
const newPosts = fake.state.requests.slice(requestsBefore).filter((r) => r.method === 'POST');
|
const newPosts = fake.state.requests.slice(requestsBefore).filter((r) => r.method === 'POST');
|
||||||
@@ -760,6 +913,49 @@ describe('main gegen Fake-Server', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('Bestehendes Schema ohne die Logo-Objekte wird um sie ergänzt, cards/gallery/reports und die Buckets bleiben unberührt', async () => {
|
||||||
|
await withFake({}, async (fake) => {
|
||||||
|
assert.equal((await run(fake)).code, 0);
|
||||||
|
// Stand vor den Logos nachstellen: cards, gallery, reports, card-images, gallery-images
|
||||||
|
dropFromFake(fake, { tables: ['logos', 'logo_likes', 'logo_reports'], buckets: ['logo-images'] });
|
||||||
|
const kept = EXPECTED_OBJECTS.filter((o) => !LOGO_OBJECTS.includes(o));
|
||||||
|
assert.equal(LOGO_OBJECTS.length, 23);
|
||||||
|
assert.equal(kept.length, 28);
|
||||||
|
const requestsBefore = fake.state.requests.length;
|
||||||
|
const postsBefore = fake.posts().length;
|
||||||
|
|
||||||
|
// --check: jedes fehlende neue Objekt als FEHLT, sonst nichts auffällig, nichts angelegt
|
||||||
|
const check = await run(fake, { check: true });
|
||||||
|
assert.equal(check.code, 1);
|
||||||
|
assert.deepEqual(startsWith(check.lines, 'FEHLT'), LOGO_OBJECTS.map((o) => `FEHLT: ${o}`));
|
||||||
|
assert.equal(startsWith(check.lines, 'ABWEICHUNG').length, 0);
|
||||||
|
assert.deepEqual(startsWith(check.lines, 'OK: ').filter((l) => l !== 'OK: Schema vollständig'), kept.map((o) => `OK: ${o}`));
|
||||||
|
assert.ok(check.lines.includes('UNVOLLSTÄNDIG: 23 fehlend.'), check.lines.join('\n'));
|
||||||
|
assert.equal(fake.posts().length, postsBefore);
|
||||||
|
|
||||||
|
// Anlegen: genau die Logo-Objekte, samt Mehrspalten-Index, in der Reihenfolge von DESIRED
|
||||||
|
const apply = await run(fake);
|
||||||
|
assert.equal(apply.code, 0, apply.lines.join('\n'));
|
||||||
|
assert.deepEqual(startsWith(apply.lines, 'ANGELEGT'), LOGO_OBJECTS.map((o) => `ANGELEGT: ${o}`));
|
||||||
|
assert.deepEqual(startsWith(apply.lines, 'OK: ').filter((l) => l !== 'OK: Schema vollständig'), kept.map((o) => `OK: ${o}`));
|
||||||
|
const newPosts = fake.state.requests.slice(requestsBefore).filter((r) => r.method === 'POST');
|
||||||
|
assert.deepEqual(newPosts.map((r) => [r.path, r.body]), EXPECTED_POSTS.filter(isLogoPost));
|
||||||
|
// Mehrspalten-Index: `columns` geht als Liste in dieser Reihenfolge an Appwrite
|
||||||
|
const userLogo = newPosts.find((r) => r.body.key === 'user_logo');
|
||||||
|
assert.equal(userLogo.path, `${T}/logo_likes/indexes`);
|
||||||
|
assert.deepEqual(userLogo.body.columns, ['userId', 'logoId']);
|
||||||
|
assert.ok(fake.state.indexPosts.every((p) => p.allColumnsAvailable));
|
||||||
|
// Alles Bestehende wurde nur gelesen
|
||||||
|
assert.equal(fake.otherWrites().length, 0);
|
||||||
|
assert.equal(fake.state.columns.size, 31);
|
||||||
|
assert.equal(fake.state.indexes.size, 10);
|
||||||
|
|
||||||
|
// danach ist das Schema vollständig
|
||||||
|
const again = await run(fake, { check: true });
|
||||||
|
assert.equal(again.code, 0, again.lines.join('\n'));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
test('Fehlende Indizes bei vorhandener Tabelle: --check meldet FEHLT, Anlegen legt nur sie an', async () => {
|
test('Fehlende Indizes bei vorhandener Tabelle: --check meldet FEHLT, Anlegen legt nur sie an', async () => {
|
||||||
await withFake({}, async (fake) => {
|
await withFake({}, async (fake) => {
|
||||||
assert.equal((await run(fake)).code, 0);
|
assert.equal((await run(fake)).code, 0);
|
||||||
@@ -887,6 +1083,41 @@ describe('main gegen Fake-Server', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('Abweichende Logo-Objekte werden gemeldet, aber nicht verändert (Exit 1)', async () => {
|
||||||
|
await withFake({}, async (fake) => {
|
||||||
|
assert.equal((await run(fake)).code, 0);
|
||||||
|
fake.state.tables.get('aircard/logos').$permissions = ['read("any")'];
|
||||||
|
fake.state.tables.get('aircard/logo_reports').rowSecurity = true;
|
||||||
|
fake.state.columns.get('aircard/logos/name').size = 80;
|
||||||
|
fake.state.columns.get('aircard/logos/mono').default = true;
|
||||||
|
fake.state.columns.get('aircard/logos/likeCount').default = 1;
|
||||||
|
fake.state.columns.get('aircard/logo_likes/userId').required = false;
|
||||||
|
fake.state.indexes.get('aircard/logo_likes/user_logo').columns = ['logoId', 'userId'];
|
||||||
|
fake.state.buckets.get('logo-images').maximumFileSize = 5242880;
|
||||||
|
fake.state.buckets.get('logo-images').allowedFileExtensions = ['png', 'webp'];
|
||||||
|
const requestsBefore = fake.state.requests.length;
|
||||||
|
|
||||||
|
for (const check of [true, false]) {
|
||||||
|
const { code, lines } = await run(fake, { check });
|
||||||
|
assert.equal(code, 1);
|
||||||
|
assert.deepEqual(startsWith(lines, 'ABWEICHUNG'), [
|
||||||
|
'ABWEICHUNG: logos.$permissions ist [read("any")], erwartet []',
|
||||||
|
'ABWEICHUNG: logos.name.size ist 80, erwartet 40',
|
||||||
|
'ABWEICHUNG: logos.mono.default ist true, erwartet false',
|
||||||
|
'ABWEICHUNG: logos.likeCount.default ist 1, erwartet 0',
|
||||||
|
'ABWEICHUNG: logo_likes.userId.required ist false, erwartet true',
|
||||||
|
'ABWEICHUNG: logo_likes.user_logo.columns ist [logoId, userId], erwartet [userId, logoId]',
|
||||||
|
'ABWEICHUNG: logo_reports.rowSecurity ist true, erwartet false',
|
||||||
|
'ABWEICHUNG: logo-images.maximumFileSize ist 5242880, erwartet 1048576',
|
||||||
|
'ABWEICHUNG: logo-images.allowedFileExtensions ist [png, webp], erwartet [png]',
|
||||||
|
]);
|
||||||
|
assert.equal(startsWith(lines, 'ANGELEGT').length, 0);
|
||||||
|
assert.ok(!lines.includes('OK: Schema vollständig'));
|
||||||
|
}
|
||||||
|
assert.ok(fake.state.requests.slice(requestsBefore).every((r) => r.method === 'GET'));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
test('Integer-Spalte mit riesigen Standard-min/max der Antwort gilt als in Ordnung', async () => {
|
test('Integer-Spalte mit riesigen Standard-min/max der Antwort gilt als in Ordnung', async () => {
|
||||||
await withFake({}, async (fake) => {
|
await withFake({}, async (fake) => {
|
||||||
assert.equal((await run(fake)).code, 0);
|
assert.equal((await run(fake)).code, 0);
|
||||||
@@ -1069,6 +1300,10 @@ describe('CLI', () => {
|
|||||||
assert.match(stdout, /FEHLT: Index gallery\.search_text/);
|
assert.match(stdout, /FEHLT: Index gallery\.search_text/);
|
||||||
assert.match(stdout, /FEHLT: Tabelle reports/);
|
assert.match(stdout, /FEHLT: Tabelle reports/);
|
||||||
assert.match(stdout, /FEHLT: Bucket gallery-images/);
|
assert.match(stdout, /FEHLT: Bucket gallery-images/);
|
||||||
|
assert.match(stdout, /FEHLT: Tabelle logos/);
|
||||||
|
assert.match(stdout, /FEHLT: Index logo_likes\.user_logo/);
|
||||||
|
assert.match(stdout, /FEHLT: Tabelle logo_reports/);
|
||||||
|
assert.match(stdout, /FEHLT: Bucket logo-images/);
|
||||||
assert.ok(!(stdout + stderr).includes(KEY));
|
assert.ok(!(stdout + stderr).includes(KEY));
|
||||||
assert.equal(fake.posts().length, 0);
|
assert.equal(fake.posts().length, 0);
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in new issue
Block a user