UNPKG

node-red-contrib-cloud-firestore

Version:

Node-RED nodes to handle google cloud firestore read and write operations.

507 lines (452 loc) 18.3 kB
<script type="text/x-red" data-template-name="Firestore in"> <div class="firestore"> <div class="form-row"> <label for="node-input-admin"><i class="fa fa-cogs"></i> Admin </label> <input type="text" id="node-input-admin"> </div> <div class="form-row"> <label for="node-input-name"><i class="fa fa-tag"></i> Node Name </label> <input type="text" id="node-input-name"> </div> <div class="form-row"> <label for="node-input-collection"><i class="fa fa-archive"></i> Collection </label> <input type="text" id="node-input-collection"> </div> <div class="form-row"> <label for="node-input-document"><i class="fa fa-file"></i> Document </label> <input type="text" id="node-input-document" placeholder="doc name"> </div> <div class="form-row"> <label for="node-input-realtime"><i class="fa fa-bolt"></i> Realtime </label> <input type="checkbox" id="node-input-realtime"> </div> <div class="form-row"> <label for="node-input-group"><i class="fa fa-sort"></i> Group</label> <input type="checkbox" id="node-input-group"> </div> <div class="form-row"> <label><i class="fa fa-search"></i> Query</label> </div> <div class="node-input-query-container-row"> <ol id="node-input-query-container"></ol> </div> <hr/> <div class="collapsible"> <div class="form-row-header">Other Settings</div> <div> <div class="form-row"> <label style="margin-bottom: 0;" for="node-input-snapHandler" class="long"><i class="fa fa-wrench"></i> Snapshot handler</label> </div> <div class="form-row" style="grid-template-columns: 2.1fr 3fr .5fr;"> <label for="input-snap-name" style="width: 100%;">File Name (optional)</label> <input type="text" id="input-snap-name"> </div> <div id="node-text-editor-row" style="position:relative"> <div style="position: absolute; top: -100px; right:-30px; bottom:calc(100% + 3px);"><button id="node-function-expand-js" class="red-ui-button red-ui-button-small"><i class="fa fa-expand"></i></button></div> <div style="height: 250px; min-height:150px;" class="node-text-editor" id="node-input-func-editor" ></div> </div> <div class="form-row" style="margin-top: 1rem; grid-template-columns: 3fr 4fr"> <label for="node-input-eject" style="width: 100%"><i class="fa fa-bolt"></i> Eject Firebase Object </label> <input type="checkbox" id="node-input-eject"> </div> <div> </div> </div> </script> <script type="text/x-red" data-help-name="Firestore in"> <b>The node fetches data from a referenced collection or referenced document</b> <h3>Inputs</h3> <p>Received on the <code>msg.firestore</code> property</p> <dl class="message-properties"> <dt> collection <span class="property-type">string</span> </dt> <dd>The collection reference point</dd> <dt class="optional"> document <span class="property-type">string</span> </dt> <dd>The document reference to the set collection</dd> <dt class="optional"> realtime <span class="property-type">boolean</span> </dt> <dd>If set, the node emits live data changes on the watched reference</dd> <dt class="optional"> group <span class="property-type">boolean</span> </dt> <dd>If set, the query will be run against a collection group (may require indexing) <dt class="optional"> query <span class="property-type">array&lt;object&gt;</span> </dt> <dd>an array of objects defining query methods to apply to the read</dd> <dt class="optional"> disableHandler <span class="property-type">boolean</span> </dt> <dd>disables the default snapshot handler, exposing a built query reference as the payload</dd> <dt class="optional"> eject <span class="property-type">boolean</span> </dt> <dd>Output the firebase app and admin instance on the <code>msg.firebase</code> property</dd> </dl> <p> The above inputs can also be set from within the node itself but can be overridden by corresponding message properties. <br> Sample <code>msg.firestore</code><br> <pre>msg.firestore = { collection: 'farm', document: 'cows', realtime: true, group: false, query: [{where: ["name", "==", "milky"]}] }</pre> </p> <h3>Mustache Templating</h3> <p>The node supports Mustache templating on the <code>collection</code> and <code>document</code> properties.</p> <p>For Example:</p> <pre>msg = { collection: 'foo', document: 'bar' }</pre> <p>With the <code>collection</code> property set to <code>{{collection}}</code> and the <code>document</code> property set to <code>{{document}}</code> in the editor, the queried collection will be <code>foo</code> & document will be <code>bar</code>.</p> <h3>Outputs</h3> Response data from the operation is output through the <code>msg.payload</code> property <h3>Other Settings</h3> <p>In the other settings, you can write your own snapshot handler. By leaving it empty, it resorts to using the default handler.</p> <p>You'll contextually have access to the following objects: <code>config</code>(this nodes settings), <code>snap</code> (query snapshot), <code>util</code>(nodejs), <code>msg</code>, <code>context</code>, <code>RED.util</code> & <code>console</code>. The <code>Promise</code>, <code>Buffer</code> and <code>Date</code> objects are also supported</p> <p>The <code>snap</code> object contains the resulting query snapshot. What you <code>return</code> will then be sent in the output payload.</p> <p>For Example:</p> <pre>const docs = []; let added = context.flow.get('added'); snap.docChanges().forEach(change => { docs.push(change.doc.data()); if (change.type === 'added') { added++; console.log('Added: ', change.doc.data()); } if (change.type === 'modified') { console.log('Modified: ', change.doc.data()); } if (change.type === 'removed') { console.log('Removed: ', change.doc.data()); } }); context.flow.set('added', added); return docs;</pre> <p>You can also save your snippets into the snippet library by giving it a file name and clicking the <code>Save to Library</code> button</p> </script> <style type="text/css"> .firestore .node-input-query-container-row { width: 100%; } .firestore .red-ui-editableList-container { height: auto !important; overflow-y: unset !important; } .firestore #input-snap-name { width: 99% !important; } #admin-input.form-row div:first-of-type { height: 34px !important; } #node-input-query-container .input-query-row { display: grid; grid-gap: 10px; grid-template-columns: 1fr 3fr; margin-top: 8px; } .input-query-row .input-query-label { text-align: right; } .input-query-row input, .input-query-row select { width: 100%; } label[for="node-input-snapHandler"].long { width: 150px; } #node-text-editor-row .form-row { display: block; } </style> <script type="text/javascript"> RED.nodes.registerType('Firestore in', { category: 'firebase', defaults: { name: {required: false}, collection: {value: '', required: false}, group: {value: false}, document: {value: ''}, realtime: {value: false}, query: {value: []}, admin: {type: 'firebase admin', required: true}, snapHandler: {required: false}, eject: {required: false, value: false} }, color: '#F3B567', inputs: 1, outputs: 1, icon: 'file.png', align: 'left', label: function () { return this.name || this.collection || 'Firestore Read'; }, labelStyle: function () { return (this.collection && !this.name) ? 'node_label_italic' : ''; }, oneditprepare: function () { const listContainer = $('#node-input-query-container'); const node = this; listContainer .css('min-height', '250px') .css('min-width', '300px') .editableList({ header: $('<div>').append($.parseHTML('<div style=\'width:28%; display: inline-grid\'>Method</div><div style=\'display: inline-grid\'>Arguments</div>')), addItem(container, i, data) { container.css({ overflow: 'hidden', whiteSpace: 'nowrap' }); // query option item let baseRow = $('<div/>', {class: 'input-query-row'}).appendTo(container); let methodSelect = $('<select/>', { class: 'node-input-query-method-type', style: 'margin-top:0;' }).appendTo(baseRow); let methodOptions = ['where', 'orderBy', 'startAt', 'endAt', 'startAfter', 'endBefore', 'offset', 'limit']; for (let x = 0; x < methodOptions.length; x++) { methodSelect.append($('<option></option>').val(methodOptions[x]).text(methodOptions[x])); } // query field argument in text format let textArgument = $('<input/>', { class: 'node-input-query-text-field', required: 'required', type: 'text' }).appendTo(baseRow); // query field argument in numerical format let numberArgument = $('<input/>', { class: 'node-input-query-number-field', required: 'required', type: 'number' }).appendTo(baseRow); // direction string drop down option let directionStrRow = $('<div/>', {class: 'input-query-row'}).appendTo(container); $('<div/>', {class: 'input-query-label'}).text('direction').appendTo(directionStrRow); let directionSelect = $('<select/>', {class: 'node-input-query-direction-select', type: 'text'}) .appendTo(directionStrRow); let directionSelectOptions = [{v: null, l: 'none'}, {v: 'asc', l: 'ascending'}, { v: 'desc', l: 'descending' }]; for (let x = 0; x < directionSelectOptions.length; x++) { directionSelect.append($('<option></option>').val(directionSelectOptions[x].v).text(directionSelectOptions[x].l)); } // operation dropdown option let operationOptRow = $('<div/>', {class: 'input-query-row'}).appendTo(container); $('<div/>', {class: 'input-query-label'}).text('operation').appendTo(operationOptRow); let operationSelect = $('<select/>', { class: 'node-input-query-operation-string' }).appendTo(operationOptRow); let operationOptions = ['<', '<=', '==', '>', '>=', 'array-contains']; for (let x = 0; x < operationOptions.length; x++) { operationSelect.append($('<option></option>').val(operationOptions[x]).text(operationOptions[x])); } // value field option let valueFieldRow = $('<div/>', {class: 'input-query-row'}).appendTo(container); $('<div/>', {class: 'input-query-label'}).text('value').appendTo(valueFieldRow); let valueFieldInput = $('<input/>', { class: 'node-input-query-value-field', required: 'required', type: 'text', placeholder: 'value' }).appendTo(valueFieldRow); methodSelect.change(function () { let type = $(this).val(); switch (type) { case 'where': textArgument.show(); valueFieldRow.show(); operationOptRow.show(); numberArgument.hide(); directionStrRow.hide(); break; case 'orderBy': textArgument.show(); directionStrRow.show(); numberArgument.hide(); operationOptRow.hide(); valueFieldRow.hide(); break; case 'startAt': case 'endAt': case 'startAfter': case 'endBefore': textArgument.show(); numberArgument.hide(); directionStrRow.hide(); operationOptRow.hide(); valueFieldRow.hide(); break; case 'offset': case 'limit': numberArgument.show(); textArgument.hide(); directionStrRow.hide(); operationOptRow.hide(); valueFieldRow.hide(); break; } }); const dataKeys = Object.keys(data); if (dataKeys.length > 0) { const method = dataKeys[0]; const value = data[method]; methodSelect.val(method); switch (method) { case 'where': textArgument.val(value.field); operationSelect.val(value.operation); valueFieldInput.val(value.value); break; case 'orderBy': textArgument.val(value.field); directionSelect.val(value.direction); break; case 'startAt': case 'endAt': case 'startAfter': case 'endBefore': textArgument.val(value); break; case 'limit': case 'offset': numberArgument.val(value); break; } methodSelect.change(); textArgument.change(); numberArgument.change(); directionSelect.change(); operationSelect.change(); } else { // Default method methodSelect.val('orderBy').change(); } }, removable: true, sortable: true }); if (!this.query) this.query = []; for (var i = 0; i < this.query.length; i++) { var query = this.query[i]; listContainer.editableList('addItem', query); } // Other settings accordion $('.collapsible').accordion({ collapsible: true, active: this.snapHandler ? 0 : false }); // Snap Handler editor this.editor = RED.editor.createEditor({ id: 'node-input-func-editor', mode: 'ace/mode/nrjavascript', value: this.snapHandler, globals: { msg: true, send: true, error: true, context: true, RED: true, util: true, Buffer: true, console: true, // can be accessed from within the context object flow: false, global: false, // A little too much bloat to support setTimeout: false, setInterval: false, clearTimeout: false, clearInterval: false } }); this.editor.focus(); RED.library.create({ url: 'functions', type: 'function', editor: this.editor, mode: 'ace/mode/nrjavascript', elementPrefix: 'input-snap-', fields: ['name'], ext: 'js' }); const fullScreenEditor = $('#node-function-expand-js'); RED.popover.tooltip(fullScreenEditor, RED._('node-red:common.label.expand')); fullScreenEditor.on('click', function (e) { e.preventDefault(); RED.editor.editJavaScript({ value: node.snapHandler, width: 'Infinity', cursor: node.editor.getCursorPosition(), mode: 'ace/mode/nrjavascript', complete: function (v, cursor) { node.editor.setValue(v, -1); node.editor.gotoLine(cursor.row + 1, cursor.column, false); setTimeout(function () { node.editor.focus(); }, 300); } }); }); }, oneditsave: function () { const queryItems = $('#node-input-query-container').editableList('items'); const node = this; node.query = []; queryItems.each(function () { const queries = $(this); const method = queries.find('.node-input-query-method-type').val(); let q = {}; const textFieldArgument = queries.find('.node-input-query-text-field').val(); const numberFieldArgument = queries.find('.node-input-query-number-field').val(); const value = queries.find('.node-input-query-value-field').val(); switch (method) { case 'where': const operation = queries.find('.node-input-query-operation-string').val(); q.where = {field: textFieldArgument, operation, value}; break; case 'orderBy': const direction = queries.find('.node-input-query-direction-select').val(); q.orderBy = {field: textFieldArgument, direction}; break; case 'startAt': case 'endAt': case 'startAfter': case 'endBefore': let valueArray = textFieldArgument.split(',').map((item) => item.trim()); q[method] = valueArray.length === 1 ? valueArray[0] : valueArray; break; case 'limit': case 'offset': q[method] = parseInt(numberFieldArgument); break; } node.query.push(q); }); // Snap handler editor var annot = this.editor.getSession().getAnnotations(); const errors = []; for (var k = 0; k < annot.length; k++) { if (annot[k].type === 'error') { errors.push(annot[k]) } } if(errors.length) { console.group('Snapshot handler errors'); errors.forEach(e => console.error(`${e.text} at column ${e.column} row ${e.row}`)); console.groupEnd(); } node.snapHandler = this.editor.getValue(); } }); </script>