Skip to content

Commit 60e3d60

Browse files
parseWhereFlat added to tags. / safeParseString NOT issue fixed.
1 parent 1862449 commit 60e3d60

File tree

2 files changed

+98
-4
lines changed

2 files changed

+98
-4
lines changed

src/PuddySqlTags.mjs

Lines changed: 81 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import _ from 'lodash';
22
import { isJsonObject } from 'tiny-essentials';
33

4-
/** @typedef {{ title: string; parser?: function(string): string }} SpecialQuery */
4+
/** @typedef {{ title: string; parser?: (value: string) => string }} SpecialQuery */
55
/** @typedef {import('./PuddySqlQuery.mjs').Pcache} Pcache */
66
/** @typedef {import('./PuddySqlQuery.mjs').TagCriteria} TagCriteria */
77

@@ -235,6 +235,7 @@ class PuddySqlTags {
235235
*
236236
* @param {Object} config - The special query object to be added.
237237
* @param {string} config.title - The unique title identifier of the special query.
238+
* @param {(value: string) => string} [config.parser] The special query function to convert the final value.
238239
*/
239240
addSpecialQuery(config) {
240241
if (!isJsonObject(config) || typeof config.title !== 'string')
@@ -520,6 +521,84 @@ class PuddySqlTags {
520521
return where.length ? `(${where.join(' AND ')})` : '1';
521522
}
522523

524+
/**
525+
* Builds an SQL WHERE clause for "flat" tag tables (one tag per row).
526+
*
527+
* Works like parseWhere, but does NOT use EXISTS/json_each.
528+
* Filters rows by direct equality or LIKE, supports negation (!tag),
529+
* wildcards, OR/AND groups, and updates pCache for parameterized queries.
530+
*
531+
* @param {TagCriteria} [group={}] - Tag group definition
532+
* @param {Pcache} [pCache={ index: 1, values: [] }] - Placeholder cache object
533+
* @returns {string} SQL WHERE clause string
534+
*/
535+
parseWhereFlat(group = {}, pCache = { index: 1, values: [] }) {
536+
if (!isJsonObject(pCache))
537+
throw new TypeError(`Expected pCache to be a valid object, but got ${typeof pCache}`);
538+
if (!isJsonObject(group))
539+
throw new TypeError(`Expected group to be a valid object, but got ${typeof group}`);
540+
if (typeof pCache.index !== 'number')
541+
throw new TypeError(`Invalid or missing pCache.index; expected number`);
542+
if (!Array.isArray(pCache.values))
543+
throw new TypeError(`Invalid or missing pCache.values; expected array`);
544+
545+
const where = [];
546+
const tagsColumn = group.column || 'tag';
547+
const allowWildcards = typeof group.allowWildcards === 'boolean' ? group.allowWildcards : false;
548+
const include = Array.isArray(group.include) ? group.include : [];
549+
550+
/**
551+
* @param {string} tag
552+
* @returns {{ param: string; usesWildcard: boolean; not: boolean; }}
553+
*/
554+
const filterTag = (tag) => {
555+
if (typeof tag !== 'string')
556+
throw new TypeError(`Each tag must be a string, but received: ${typeof tag}`);
557+
558+
const not = tag.startsWith('!');
559+
const cleanTag = not ? tag.slice(1) : tag;
560+
561+
if (typeof pCache.index !== 'number') throw new Error('Invalid pCache index');
562+
const param = `$${pCache.index++}`;
563+
564+
const usesWildcard =
565+
allowWildcards &&
566+
(cleanTag.includes(this.#wildcardA) || cleanTag.includes(this.#wildcardB));
567+
568+
const filteredTag = usesWildcard
569+
? cleanTag
570+
.replace(/([%_])/g, '\\$1')
571+
.replaceAll(this.#wildcardA, '%')
572+
.replaceAll(this.#wildcardB, '_')
573+
: cleanTag;
574+
575+
if (!Array.isArray(pCache.values)) throw new Error('Invalid pCache values');
576+
pCache.values.push(filteredTag);
577+
return { param, usesWildcard, not };
578+
};
579+
580+
/** @param {{ param: string; usesWildcard: boolean; not: boolean; }} tagObj */
581+
const createQuery = (tagObj) => {
582+
const { param, usesWildcard, not } = tagObj;
583+
const operator = usesWildcard ? 'LIKE' : '=';
584+
return `${tagsColumn} ${not ? '!=' : operator} ${param}`;
585+
};
586+
587+
for (const clause of include) {
588+
if (Array.isArray(clause)) {
589+
// OR group
590+
const ors = clause.map((tag) => createQuery(filterTag(tag)));
591+
if (ors.length) where.push(`(${ors.join(' OR ')})`);
592+
} else {
593+
// single tag
594+
where.push(createQuery(filterTag(clause)));
595+
}
596+
}
597+
598+
// Combine all with AND
599+
return where.length ? `(${where.join(' AND ')})` : '1';
600+
}
601+
523602
/**
524603
* Extracts special query elements and custom tag input groups from parsed search chunks.
525604
*
@@ -825,7 +904,7 @@ class PuddySqlTags {
825904
.map((item) => item.trim())
826905
.join(' AND ')
827906
.replace(/(?:^|[\s(,])-(?=\w)/g, (match) => match.replace('-', '!'))
828-
.replace(/\bNOT\b/g, '!')
907+
.replace(/\s*\bNOT\b\s*/g, '!')
829908
.replace(/\&\&/g, 'AND')
830909
.replace(/\|\|/g, 'OR'),
831910
strictMode,

test/index.mjs

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,8 +107,6 @@ const db = new PuddySql.Instance();
107107
['tags', 'TAGS'],
108108
]);
109109

110-
const tagManager = tagTable.getTagEditor('tags');
111-
112110
await tagTable.set('a1', {
113111
title: 'Post 1',
114112
tags: ['cute', 'funny', 'smiling', 'safe', 'pony', 'solo'],
@@ -258,6 +256,23 @@ const db = new PuddySql.Instance();
258256
}),
259257
);
260258

259+
const tagManager = tagTable.getTagEditor('tags');
260+
tagManager.addSpecialQuery({ title: 'rating', parser: (value) => {
261+
console.log(`\n🔖 \x1b[34mRating Tag Detected: ${value}\x1b[0m\n`);
262+
return value;
263+
}});
264+
265+
const tagsList = `(pinkie pie OR rarity) AND (applejack OR rarity) AND (farm OR boutique) AND (!party OR balloons) AND rating:safe AND order:random AND NOT order:random2`;
266+
267+
console.log('\n🔖 \x1b[34mParse Tags: JSON\x1b[0m\n');
268+
console.log(tagsList);
269+
const tagParse = tagManager.safeParseString(tagsList);
270+
console.log(tagParse);
271+
console.log('\n🔖 \x1b[34mParse JSON: JSON\x1b[0m\n');
272+
console.log(tagManager.parseWhere(tagParse));
273+
console.log('\n🔖 \x1b[34mParse Tags: Normal\x1b[0m\n');
274+
console.log(tagManager.parseWhereFlat(tagParse));
275+
261276
console.log('\n✅ \x1b[1;32mAll tag tests done.\x1b[0m');
262277
console.log('\n🎉 \x1b[1;32mDone. Everything looks delicious! 🍮\x1b[0m\n');
263278
})();

0 commit comments

Comments
 (0)