diff --git a/src/entities/note.js b/src/entities/note.js index 592c47c34..9339c1fc7 100644 --- a/src/entities/note.js +++ b/src/entities/note.js @@ -6,6 +6,9 @@ const protectedSessionService = require('../services/protected_session'); const repository = require('../services/repository'); const dateUtils = require('../services/date_utils'); +const LABEL = 'label'; +const RELATION = 'relation'; + class Note extends Entity { static get tableName() { return "notes"; } static get primaryKeyName() { return "noteId"; } @@ -78,6 +81,14 @@ class Note extends Entity { return this.__attributeCache; } + async getLabels() { + return (await this.getAttributes()).filter(attr => attr.type === LABEL); + } + + async getRelations() { + return (await this.getAttributes()).filter(attr => attr.type === RELATION); + } + invalidateAttributeCache() { this.__attributeCache = null; } @@ -145,55 +156,55 @@ class Note extends Entity { this.__attributeCache = filteredAttributes; } - async hasLabel(name) { - return !!await this.getLabel(name); + async hasAttribute(type, name) { + return !!await this.getAttribute(type, name); } // WARNING: this doesn't take into account the possibility to have multi-valued labels! - async getLabel(name) { + async getAttribute(type, name) { const attributes = await this.getAttributes(); - return attributes.find(attr => attr.type === 'label' && attr.name === name); + return attributes.find(attr => attr.type === type && attr.name === name); } - async getLabelValue(name) { - const label = await this.getLabel(name); + async getAttributeValue(type, name) { + const attr = await this.getAttribute(type, name); - return label ? label.value : null; + return attr ? attr.value : null; } - async toggleLabel(enabled, name, value = "") { + async toggleAttribute(type, enabled, name, value = "") { if (enabled) { - await this.setLabel(name, value); + await this.setAttribute(type, name, value); } else { - await this.removeLabel(name, value); + await this.removeAttribute(type, name, value); } } - async setLabel(name, value = "") { + async setAttribute(type, name, value = "") { const attributes = await this.getOwnedAttributes(); - let label = attributes.find(attr => attr.type === 'label' && attr.value === value); + let attr = attributes.find(attr => attr.type === type && attr.value === value); - if (!label) { - label = new Attribute({ + if (!attr) { + attr = new Attribute({ noteId: this.noteId, - type: 'label', + type: type, name: name, value: value }); - await label.save(); + await attr.save(); this.invalidateAttributeCache(); } } - async removeLabel(name, value = "") { + async removeAttribute(type, name, value = "") { const attributes = await this.getOwnedAttributes(); for (const attribute of attributes) { - if (attribute.type === 'label' && (!value || value === attribute.value)) { + if (attribute.type === type && (!value || value === attribute.value)) { attribute.isDeleted = true; await attribute.save(); @@ -202,6 +213,24 @@ class Note extends Entity { } } + async hasLabel(name) { return await this.hasAttribute(LABEL, name); } + async hasRelation(name) { return await this.hasAttribute(RELATION, name); } + + async getLabel(name) { return await this.getAttribute(LABEL, name); } + async getRelation(name) { return await this.getAttribute(RELATION, name); } + + async getLabelValue(name) { return await this.getAttributeValue(LABEL, name); } + async getRelationValue(name) { return await this.getAttributeValue(RELATION, name); } + + async toggleLabel(enabled, name, value = "") { return await this.toggleAttribute(LABEL, enabled, name, value); } + async toggleRelation(enabled, name, value = "") { return await this.toggleAttribute(RELATION, enabled, name, value); } + + async setLabel(name, value = "") { return await this.setAttribute(LABEL, name, value); } + async setRelation(name, value = "") { return await this.setAttribute(RELATION, name, value); } + + async removeLabel(name, value = "") { return await this.removeAttribute(LABEL, name, value); } + async removeRelation(name, value = "") { return await this.removeAttribute(RELATION, name, value); } + async getRevisions() { return await repository.getEntities("SELECT * FROM note_revisions WHERE noteId = ?", [this.noteId]); } diff --git a/src/services/attributes.js b/src/services/attributes.js index cb676b7e9..2933750da 100644 --- a/src/services/attributes.js +++ b/src/services/attributes.js @@ -19,7 +19,11 @@ const BUILTIN_ATTRIBUTES = [ // relation names { type: 'relation', name: 'runOnNoteView' }, + { type: 'relation', name: 'runOnNoteCreation' }, { type: 'relation', name: 'runOnNoteTitleChange' }, + { type: 'relation', name: 'runOnNoteChange' }, + { type: 'relation', name: 'runOnChildNoteCreation' }, + { type: 'relation', name: 'runOnAttributeCreation' }, { type: 'relation', name: 'runOnAttributeChange' }, { type: 'relation', name: 'inheritAttributes' } ]; @@ -59,20 +63,19 @@ async function createAttribute(attribute) { } async function getAttributeNames(type, nameLike) { - let names; + nameLike = nameLike.toLowerCase(); - if (!nameLike) { - names = BUILTIN_ATTRIBUTES - .filter(attribute => attribute.type === type) - .map(attribute => attribute.name); - } - else { - names = await sql.getColumn( - `SELECT DISTINCT name + const names = await sql.getColumn( + `SELECT DISTINCT name FROM attributes WHERE isDeleted = 0 AND type = ? AND name LIKE '%${utils.sanitizeSql(nameLike)}%'`, [type]); + + for (const attr of BUILTIN_ATTRIBUTES) { + if (attr.type === type && attr.name.toLowerCase().includes(nameLike) && !names.includes(attr.name)) { + names.push(attr.name); + } } names.sort(); diff --git a/src/services/events.js b/src/services/events.js index dc6440ae5..8eb2e689e 100644 --- a/src/services/events.js +++ b/src/services/events.js @@ -2,7 +2,9 @@ const log = require('./log'); const NOTE_TITLE_CHANGED = "NOTE_TITLE_CHANGED"; const ENTER_PROTECTED_SESSION = "ENTER_PROTECTED_SESSION"; +const ENTITY_CREATED = "ENTITY_CREATED"; const ENTITY_CHANGED = "ENTITY_CHANGED"; +const CHILD_NOTE_CREATED = "CHILD_NOTE_CREATED"; const eventListeners = {}; @@ -33,5 +35,7 @@ module.exports = { // event types: NOTE_TITLE_CHANGED, ENTER_PROTECTED_SESSION, - ENTITY_CHANGED + ENTITY_CREATED, + ENTITY_CHANGED, + CHILD_NOTE_CREATED }; \ No newline at end of file diff --git a/src/services/handlers.js b/src/services/handlers.js index 3e70e2a2a..f648fcfe5 100644 --- a/src/services/handlers.js +++ b/src/services/handlers.js @@ -2,12 +2,10 @@ const eventService = require('./events'); const scriptService = require('./script'); const treeService = require('./tree'); const messagingService = require('./messaging'); -const repository = require('./repository'); const log = require('./log'); async function runAttachedRelations(note, relationName, originEntity) { - const attributes = await note.getAttributes(); - const runRelations = attributes.filter(relation => relation.type === 'relation' && relation.name === relationName); + const runRelations = (await note.getRelations()).filter(relation => relation.name === relationName); for (const relation of runRelations) { const scriptNote = await relation.getTargetNote(); @@ -37,10 +35,24 @@ eventService.subscribe(eventService.NOTE_TITLE_CHANGED, async note => { } }); -eventService.subscribe(eventService.ENTITY_CHANGED, async ({ entityId, entityName }) => { +eventService.subscribe(eventService.ENTITY_CHANGED, async ({ entityName, entity }) => { if (entityName === 'attributes') { - const attribute = await repository.getEntityFromName(entityName, entityId); - - await runAttachedRelations(await attribute.getNote(), 'runOnAttributeChange', attribute); + await runAttachedRelations(await entity.getNote(), 'runOnAttributeChange', entity); } + else if (entityName === 'notes') { + await runAttachedRelations(entity, 'runOnNoteChange', entity); + } +}); + +eventService.subscribe(eventService.ENTITY_CREATED, async ({ entityName, entity }) => { + if (entityName === 'attributes') { + await runAttachedRelations(await entity.getNote(), 'runOnAttributeCreation', entity); + } + else if (entityName === 'notes') { + await runAttachedRelations(entity, 'runOnNoteCreation', entity); + } +}); + +eventService.subscribe(eventService.CHILD_NOTE_CREATED, async ({ parentNote, childNote }) => { + await runAttachedRelations(parentNote, 'runOnChildNoteCreation', childNote); }); \ No newline at end of file diff --git a/src/services/note_cache.js b/src/services/note_cache.js index 0c69d9c65..802ab751f 100644 --- a/src/services/note_cache.js +++ b/src/services/note_cache.js @@ -228,13 +228,13 @@ function getNotePath(noteId) { } } -eventService.subscribe(eventService.ENTITY_CHANGED, async ({entityName, entityId}) => { +eventService.subscribe(eventService.ENTITY_CHANGED, async ({entityName, entity}) => { if (!loaded) { return; } if (entityName === 'notes') { - const note = await repository.getNote(entityId); + const note = entity; if (note.isDeleted) { delete noteTitles[note.noteId]; @@ -245,7 +245,7 @@ eventService.subscribe(eventService.ENTITY_CHANGED, async ({entityName, entityId } } else if (entityName === 'branches') { - const branch = await repository.getBranch(entityId); + const branch = entity; if (childToParent[branch.noteId]) { childToParent[branch.noteId] = childToParent[branch.noteId].filter(noteId => noteId !== branch.parentNoteId) @@ -266,7 +266,7 @@ eventService.subscribe(eventService.ENTITY_CHANGED, async ({entityName, entityId } } else if (entityName === 'attributes') { - const attribute = await repository.getAttribute(entityId); + const attribute = entity; if (attribute.type === 'label' && attribute.name === 'archived') { // we're not using label object directly, since there might be other non-deleted archived label diff --git a/src/services/notes.js b/src/services/notes.js index de27966f9..50f16cefe 100644 --- a/src/services/notes.js +++ b/src/services/notes.js @@ -35,6 +35,10 @@ async function getNewNotePosition(parentNoteId, noteData) { return newNotePos; } +async function triggerChildNoteCreated(childNote, parentNote) { + await eventService.emit(eventService.CHILD_NOTE_CREATED, { childNote, parentNote }); +} + async function triggerNoteTitleChanged(note) { await eventService.emit(eventService.NOTE_TITLE_CHANGED, note); } @@ -42,12 +46,10 @@ async function triggerNoteTitleChanged(note) { async function createNewNote(parentNoteId, noteData) { const newNotePos = await getNewNotePosition(parentNoteId, noteData); - if (parentNoteId !== 'root') { - const parent = await repository.getNote(parentNoteId); + const parentNote = await repository.getNote(parentNoteId); - noteData.type = noteData.type || parent.type; - noteData.mime = noteData.mime || parent.mime; - } + noteData.type = noteData.type || parentNote.type; + noteData.mime = noteData.mime || parentNote.mime; const note = await new Note({ title: noteData.title, @@ -66,6 +68,7 @@ async function createNewNote(parentNoteId, noteData) { }).save(); await triggerNoteTitleChanged(note); + await triggerChildNoteCreated(note, parentNote); return { note, diff --git a/src/services/repository.js b/src/services/repository.js index c77dce31f..f3f323f89 100644 --- a/src/services/repository.js +++ b/src/services/repository.js @@ -2,6 +2,7 @@ const sql = require('./sql'); const syncTableService = require('../services/sync_table'); +const eventService = require('./events'); let entityConstructor; @@ -56,6 +57,11 @@ async function getOption(name) { } async function updateEntity(entity) { + const entityName = entity.constructor.tableName; + const primaryKeyName = entity.constructor.primaryKeyName; + + const isNewEntity = !entity[primaryKeyName]; + if (entity.beforeSaving) { await entity.beforeSaving(); } @@ -75,12 +81,24 @@ async function updateEntity(entity) { } await sql.transactional(async () => { - await sql.replace(entity.constructor.tableName, clone); + await sql.replace(entityName, clone); - const primaryKey = entity[entity.constructor.primaryKeyName]; + const primaryKey = entity[primaryKeyName]; - if (entity.isChanged && (entity.constructor.tableName !== 'options' || entity.isSynced)) { - await syncTableService.addEntitySync(entity.constructor.tableName, primaryKey); + if (entity.isChanged && (entityName !== 'options' || entity.isSynced)) { + await syncTableService.addEntitySync(entityName, primaryKey); + + if (isNewEntity) { + await eventService.emit(eventService.ENTITY_CREATED, { + entityName, + entity + }); + } + + await eventService.emit(eventService.ENTITY_CHANGED, { + entityName, + entity + }); } }); } diff --git a/src/services/sync_table.js b/src/services/sync_table.js index e2b6ef5b8..af9c814ed 100644 --- a/src/services/sync_table.js +++ b/src/services/sync_table.js @@ -3,7 +3,6 @@ const sourceIdService = require('./source_id'); const dateUtils = require('./date_utils'); const log = require('./log'); const cls = require('./cls'); -const eventService = require('./events'); async function addNoteSync(noteId, sourceId) { await addEntitySync("notes", noteId, sourceId) @@ -52,11 +51,6 @@ async function addEntitySync(entityName, entityId, sourceId) { syncDate: dateUtils.nowDate(), sourceId: sourceId || cls.getSourceId() || sourceIdService.getCurrentSourceId() }); - - await eventService.emit(eventService.ENTITY_CHANGED, { - entityName, - entityId - }); } async function cleanupSyncRowsForMissingEntities(entityName, entityKey) { @@ -116,6 +110,5 @@ module.exports = { addAttributeSync, addApiTokenSync, addEntitySync, - cleanupSyncRowsForMissingEntities, fillAllSyncRows }; \ No newline at end of file